make translation api public to fetch on first app start

This commit is contained in:
2026-05-27 17:43:17 +02:00
parent 5a2faa3ecc
commit 0f98393d1d
5 changed files with 57 additions and 28 deletions

View File

@ -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;
}
$qnID = $_GET['id'] ?? '';
if ($qnID) {
// Single questionnaire in app JSON format

View File

@ -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.

View File

@ -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]);

View File

@ -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 */

View File

@ -10,6 +10,7 @@ export function loginPage() {
<div class="login-card card">
<h1>BW Sch&uuml;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>