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 ?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
|
||||
*/
|
||||
require_once __DIR__ . '/../common.php';
|
||||
require_once __DIR__ . '/../db_init.php';
|
||||
require_once __DIR__ . '/../lib/response.php';
|
||||
|
||||
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();
|
||||
|
||||
if ($_SERVER['REQUEST_METHOD'] !== 'GET') {
|
||||
http_response_code(405);
|
||||
echo json_encode(["error" => "Method not allowed"]);
|
||||
exit;
|
||||
if ($method !== 'GET') {
|
||||
json_error('METHOD_NOT_ALLOWED', 'Method not allowed', 405);
|
||||
}
|
||||
|
||||
[$pdo, $tmpDb, $lockFp] = qdb_open(false);
|
||||
|
||||
$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) {
|
||||
// Single questionnaire in app JSON format
|
||||
|
||||
@ -265,6 +265,8 @@ Common failures:
|
||||
|
||||
`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:
|
||||
|
||||
```json
|
||||
@ -307,16 +309,30 @@ Android should apply this map while a questionnaire is open (overlay on top of a
|
||||
|
||||
## Recommended Android Sync Flow
|
||||
|
||||
1. Login with `/api/auth/login` and store the returned token securely.
|
||||
2. Fetch the assigned client list with `GET /api/app_questionnaires?clients=1` (coach role only).
|
||||
3. Fetch app UI translations with `GET /api/app_questionnaires?translations=1`.
|
||||
4. Fetch the active questionnaire list with `GET /api/app_questionnaires`.
|
||||
5. 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. When a questionnaire is completed, upload answers with `POST /api/app_questionnaires`.
|
||||
1. On app start (login screen), fetch app UI translations with `GET /api/app_questionnaires?translations=1` (no token) and cache locally.
|
||||
2. Login with `/api/auth/login` and store the returned token securely (EncryptedSharedPreferences on Android).
|
||||
3. Fetch the assigned client list with `GET /api/app_questionnaires?clients=1` (coach role only).
|
||||
4. Re-fetch app UI translations optionally after login (same public endpoint, or authenticated refresh).
|
||||
6. For each list item, fetch details with `GET /api/app_questionnaires?id=<id>` (includes per-questionnaire `translations`).
|
||||
7. Cache the client list, app translations, the questionnaire list, and questionnaire details (with embedded translations) locally for offline use.
|
||||
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.
|
||||
|
||||
## 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
|
||||
|
||||
`POST /api/app_questionnaires`
|
||||
@ -377,7 +393,7 @@ Current upload behavior:
|
||||
- Unknown or empty `questionID` values are skipped.
|
||||
- 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 `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.
|
||||
- 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.
|
||||
* GET (no params) -> ordered questionnaire list with conditions
|
||||
* 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
|
||||
* 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();
|
||||
|
||||
if ($method === 'POST') {
|
||||
@ -252,11 +262,6 @@ if ($fetchClients) {
|
||||
json_success(['coachID' => $coachID, 'clients' => $clients]);
|
||||
}
|
||||
|
||||
if ($translations) {
|
||||
qdb_discard($tmpDb, $lockFp);
|
||||
json_success(['translations' => qdb_build_app_translations_map($pdo)]);
|
||||
}
|
||||
|
||||
if ($qnID) {
|
||||
$qn = $pdo->prepare("SELECT * FROM questionnaire WHERE questionnaireID = :id");
|
||||
$qn->execute([':id' => $qnID]);
|
||||
|
||||
@ -602,6 +602,12 @@ th.sort-desc::after { content: ' \2193'; opacity: 1; }
|
||||
font-size: .9rem;
|
||||
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; }
|
||||
|
||||
/* Toast */
|
||||
|
||||
@ -10,6 +10,7 @@ export function loginPage() {
|
||||
<div class="login-card card">
|
||||
<h1>BW Schützt</h1>
|
||||
<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">
|
||||
<div class="form-group">
|
||||
<label for="username">Username</label>
|
||||
|
||||
Reference in New Issue
Block a user