make translation api public to fetch on first app start
This commit is contained in:
@ -4,33 +4,34 @@
|
|||||||
*
|
*
|
||||||
* GET (no params) -> ordered questionnaire list with conditions
|
* GET (no params) -> ordered questionnaire list with conditions
|
||||||
* GET ?id=<questionnaireID> -> single questionnaire in app JSON format
|
* GET ?id=<questionnaireID> -> single questionnaire in app JSON format
|
||||||
* GET ?translations=1 -> global app UI strings only (not questionnaire content)
|
* GET ?translations=1 -> global app UI strings only; **no auth** (login screen).
|
||||||
*
|
*
|
||||||
* POST (coach interview upload) is handled by api/index.php -> handlers/app_questionnaires.php
|
* POST (coach interview upload) is handled by api/index.php -> handlers/app_questionnaires.php
|
||||||
*/
|
*/
|
||||||
require_once __DIR__ . '/../common.php';
|
require_once __DIR__ . '/../common.php';
|
||||||
require_once __DIR__ . '/../db_init.php';
|
require_once __DIR__ . '/../db_init.php';
|
||||||
|
require_once __DIR__ . '/../lib/response.php';
|
||||||
|
|
||||||
header('Content-Type: application/json; charset=UTF-8');
|
header('Content-Type: application/json; charset=UTF-8');
|
||||||
|
|
||||||
|
$method = $_SERVER['REQUEST_METHOD'];
|
||||||
|
|
||||||
|
if ($method === 'GET' && !empty($_GET['translations'])) {
|
||||||
|
[$pdo, $tmpDb, $lockFp] = qdb_open(false);
|
||||||
|
json_success(['translations' => qdb_build_app_translations_map($pdo)]);
|
||||||
|
qdb_discard($tmpDb, $lockFp);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
$tokenRec = require_valid_token();
|
$tokenRec = require_valid_token();
|
||||||
|
|
||||||
if ($_SERVER['REQUEST_METHOD'] !== 'GET') {
|
if ($method !== 'GET') {
|
||||||
http_response_code(405);
|
json_error('METHOD_NOT_ALLOWED', 'Method not allowed', 405);
|
||||||
echo json_encode(["error" => "Method not allowed"]);
|
|
||||||
exit;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
[$pdo, $tmpDb, $lockFp] = qdb_open(false);
|
[$pdo, $tmpDb, $lockFp] = qdb_open(false);
|
||||||
|
|
||||||
$qnID = $_GET['id'] ?? '';
|
$qnID = $_GET['id'] ?? '';
|
||||||
$translations = $_GET['translations'] ?? '';
|
|
||||||
|
|
||||||
if ($translations) {
|
|
||||||
qdb_discard($tmpDb, $lockFp);
|
|
||||||
echo json_encode(['success' => true, 'translations' => qdb_build_app_translations_map($pdo)]);
|
|
||||||
exit;
|
|
||||||
}
|
|
||||||
|
|
||||||
if ($qnID) {
|
if ($qnID) {
|
||||||
// Single questionnaire in app JSON format
|
// Single questionnaire in app JSON format
|
||||||
|
|||||||
@ -265,6 +265,8 @@ Common failures:
|
|||||||
|
|
||||||
`GET /api/app_questionnaires?translations=1`
|
`GET /api/app_questionnaires?translations=1`
|
||||||
|
|
||||||
|
**Authentication:** not required. The Android app calls this on the login screen before the coach signs in. All other `app_questionnaires` routes still require `Authorization: Bearer <token>`.
|
||||||
|
|
||||||
Success:
|
Success:
|
||||||
|
|
||||||
```json
|
```json
|
||||||
@ -307,16 +309,30 @@ Android should apply this map while a questionnaire is open (overlay on top of a
|
|||||||
|
|
||||||
## Recommended Android Sync Flow
|
## Recommended Android Sync Flow
|
||||||
|
|
||||||
1. Login with `/api/auth/login` and store the returned token securely.
|
1. On app start (login screen), fetch app UI translations with `GET /api/app_questionnaires?translations=1` (no token) and cache locally.
|
||||||
2. Fetch the assigned client list with `GET /api/app_questionnaires?clients=1` (coach role only).
|
2. Login with `/api/auth/login` and store the returned token securely (EncryptedSharedPreferences on Android).
|
||||||
3. Fetch app UI translations with `GET /api/app_questionnaires?translations=1`.
|
3. Fetch the assigned client list with `GET /api/app_questionnaires?clients=1` (coach role only).
|
||||||
4. Fetch the active questionnaire list with `GET /api/app_questionnaires`.
|
4. Re-fetch app UI translations optionally after login (same public endpoint, or authenticated refresh).
|
||||||
5. For each list item, fetch details with `GET /api/app_questionnaires?id=<id>` (includes per-questionnaire `translations`).
|
6. For each list item, fetch details with `GET /api/app_questionnaires?id=<id>` (includes per-questionnaire `translations`).
|
||||||
6. Cache the client list, app translations, the questionnaire list, and questionnaire details (with embedded translations) locally for offline use.
|
7. Cache the client list, app translations, the questionnaire list, and questionnaire details (with embedded translations) locally for offline use.
|
||||||
7. When a questionnaire is completed, upload answers with `POST /api/app_questionnaires`.
|
8. When a questionnaire is completed, upload answers with `POST /api/app_questionnaires` (plaintext JSON over HTTPS; server stores answers in the database — not the app's local ciphertext).
|
||||||
|
|
||||||
There is currently no version or `updatedAt` field in the app fetch response, so the safest refresh strategy is to re-fetch translations, list, and details at login or app start when network is available.
|
There is currently no version or `updatedAt` field in the app fetch response, so the safest refresh strategy is to re-fetch translations, list, and details at login or app start when network is available.
|
||||||
|
|
||||||
|
## Android local data security
|
||||||
|
|
||||||
|
The Android app encrypts sensitive data **at rest on the device**:
|
||||||
|
|
||||||
|
- **Answer text** and **client codes** in the local Room database use AES-256-GCM with keys in Android Keystore (ciphertext on disk; plaintext only inside the running app).
|
||||||
|
- **Assigned client list** cache and **session token** use the same encryption / EncryptedSharedPreferences respectively.
|
||||||
|
- **Questionnaire definitions** and UI translations cached from this API are not encrypted (no user answers).
|
||||||
|
|
||||||
|
After a successful `POST /api/app_questionnaires`, answer data is readable on the **server** (normal database storage, RBAC-protected). The upload body is **decrypted plaintext** (AES-GCM applies only to the on-device Room database). The app keeps encrypted local copies so coaches can review, edit, and re-upload work offline.
|
||||||
|
|
||||||
|
Re-uploading the same `clientCode` + `questionnaireID` updates server rows in place (see upload behavior below). This matches upcoming editable flows on web and mobile.
|
||||||
|
|
||||||
|
Legacy full-database download endpoints (`downloadFull.php`, encrypted SQLite master file) are **not** used by the current Android app.
|
||||||
|
|
||||||
## Upload Questionnaire Data
|
## Upload Questionnaire Data
|
||||||
|
|
||||||
`POST /api/app_questionnaires`
|
`POST /api/app_questionnaires`
|
||||||
@ -377,7 +393,7 @@ Current upload behavior:
|
|||||||
- Unknown or empty `questionID` values are skipped.
|
- Unknown or empty `questionID` values are skipped.
|
||||||
- Unknown `answerOptionKey` values are stored as no selected option for that question.
|
- Unknown `answerOptionKey` values are stored as no selected option for that question.
|
||||||
- Re-uploading the same `clientCode` and `questionID` overwrites the previous answer.
|
- Re-uploading the same `clientCode` and `questionID` overwrites the previous answer.
|
||||||
- Re-uploading the same `clientCode` and `questionnaireID` overwrites the completed questionnaire status and point sum.
|
- Re-uploading the same `clientCode` and `questionnaireID` overwrites the completed questionnaire status and point sum (supports correction / re-edit workflows).
|
||||||
- `sumPoints` is calculated from recognized `answerOptionKey` values.
|
- `sumPoints` is calculated from recognized `answerOptionKey` values.
|
||||||
- The current database stores one selected answer option per question. If the Android UI allows multiple selections for a layout, coordinate a server change before uploading multiple option keys for one question.
|
- The current database stores one selected answer option per question. If the Android UI allows multiple selections for a layout, coordinate a server change before uploading multiple option keys for one question.
|
||||||
|
|
||||||
|
|||||||
@ -3,11 +3,21 @@
|
|||||||
* Android app API endpoint.
|
* Android app API endpoint.
|
||||||
* GET (no params) -> ordered questionnaire list with conditions
|
* GET (no params) -> ordered questionnaire list with conditions
|
||||||
* GET ?id=<questionnaireID> -> single questionnaire in app JSON format
|
* GET ?id=<questionnaireID> -> single questionnaire in app JSON format
|
||||||
* GET ?translations=1 -> global app UI strings only (not questionnaire content)
|
* GET ?translations=1 -> global app UI strings only (not questionnaire content); **no auth** (login screen).
|
||||||
* GET ?clients=1 -> list of clients assigned to the authenticated coach
|
* GET ?clients=1 -> list of clients assigned to the authenticated coach
|
||||||
* POST -> submit interview answers for a client (coach / supervisor / admin)
|
* POST -> submit interview answers for a client (coach / supervisor / admin).
|
||||||
|
* Re-posting the same clientCode + questionnaireID updates answers in place (re-edit).
|
||||||
|
* Mobile clients encrypt answers at rest on device; POST body is plaintext JSON over HTTPS.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
// Public app UI catalog for the login screen (no token, no PII).
|
||||||
|
if ($method === 'GET' && !empty($_GET['translations'])) {
|
||||||
|
[$pdo, $tmpDb, $lockFp] = qdb_open(false);
|
||||||
|
json_success(['translations' => qdb_build_app_translations_map($pdo)]);
|
||||||
|
qdb_discard($tmpDb, $lockFp);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
$tokenRec = require_valid_token();
|
$tokenRec = require_valid_token();
|
||||||
|
|
||||||
if ($method === 'POST') {
|
if ($method === 'POST') {
|
||||||
@ -252,11 +262,6 @@ if ($fetchClients) {
|
|||||||
json_success(['coachID' => $coachID, 'clients' => $clients]);
|
json_success(['coachID' => $coachID, 'clients' => $clients]);
|
||||||
}
|
}
|
||||||
|
|
||||||
if ($translations) {
|
|
||||||
qdb_discard($tmpDb, $lockFp);
|
|
||||||
json_success(['translations' => qdb_build_app_translations_map($pdo)]);
|
|
||||||
}
|
|
||||||
|
|
||||||
if ($qnID) {
|
if ($qnID) {
|
||||||
$qn = $pdo->prepare("SELECT * FROM questionnaire WHERE questionnaireID = :id");
|
$qn = $pdo->prepare("SELECT * FROM questionnaire WHERE questionnaireID = :id");
|
||||||
$qn->execute([':id' => $qnID]);
|
$qn->execute([':id' => $qnID]);
|
||||||
|
|||||||
@ -602,6 +602,12 @@ th.sort-desc::after { content: ' \2193'; opacity: 1; }
|
|||||||
font-size: .9rem;
|
font-size: .9rem;
|
||||||
margin-bottom: 24px;
|
margin-bottom: 24px;
|
||||||
}
|
}
|
||||||
|
.login-card .login-privacy-note {
|
||||||
|
color: var(--text-secondary);
|
||||||
|
font-size: .8rem;
|
||||||
|
line-height: 1.4;
|
||||||
|
margin: -12px 0 20px;
|
||||||
|
}
|
||||||
.error-text { color: var(--danger); font-size: .85rem; margin-top: 8px; }
|
.error-text { color: var(--danger); font-size: .85rem; margin-top: 8px; }
|
||||||
|
|
||||||
/* Toast */
|
/* Toast */
|
||||||
|
|||||||
@ -10,6 +10,7 @@ export function loginPage() {
|
|||||||
<div class="login-card card">
|
<div class="login-card card">
|
||||||
<h1>BW Schützt</h1>
|
<h1>BW Schützt</h1>
|
||||||
<p class="subtitle">Questionnaire Management</p>
|
<p class="subtitle">Questionnaire Management</p>
|
||||||
|
<p class="login-privacy-note">Coach app stores interview answers encrypted on device; data shown here is loaded from the server after upload.</p>
|
||||||
<form id="loginForm">
|
<form id="loginForm">
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label for="username">Username</label>
|
<label for="username">Username</label>
|
||||||
|
|||||||
Reference in New Issue
Block a user