feat(api): Benutzernamen (Profil-Tabelle, /api/profil, Pflicht beim Hochladen und Veröffentlichen)
- Schema: Tabelle `profiles` (Zeilen-ID = Konto-ID, `username` und `usernameKey`
je 20 Zeichen, Unique-Index `username_key` auf `usernameKey`, rowSecurity an,
keine Tabellenrechte) im Setup-Skript.
- Neues Modul `deploy/api/profiles.js`: `POST /api/profil` legt das Profil an
oder ändert es (Zeilenrecht genau read("any"), Sperre je Konto). Muster,
Sperrliste und Schlüssel (`usernameKey`) stehen in `common.js`; vergeben ->
409 `usernameTaken`, auch wenn erst der Unique-Index bei einem Rennen
entscheidet. Dazu die MSG-Texte `usernameInvalid`, `usernameReserved`,
`usernameTaken`, `usernameRequired`.
- `POST /api/logos/hochladen` und `POST /api/veroeffentlichen` antworten 403
`usernameRequired`, wenn das Konto kein Profil hat (`hasProfile`, nach der
Anmeldung und vor allen anderen Prüfungen).
- Fake-Appwrite: Tabelle `profiles` mit Unique-Index (409), `addProfile`,
`holdNext`. API-Dienst besteht jetzt aus sechs Dateien (READMEs).
- Client: die vier API-Texte in `API_MESSAGES` und `en.ts` (Gleichlauf mit MSG).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
a65c075004
commit
19c8151c7e
15 files changed
+980
-53
No files matched your search
@@ -10,7 +10,10 @@
|
||||
* GET /v1/storage/buckets/{b}/files/{f}/view Dateiinhalt (Bytes aus `content`)
|
||||
*
|
||||
* Die Tabelle `cards` ist nur zum Lesen da (Zeilen legt der Test mit `addCard` an). Es gibt außerdem die Tabellen
|
||||
* `gallery`, `reports`, `logos`, `logo_likes` und `logo_reports` und die Buckets `gallery-images`, `card-images`, `logo-images`.
|
||||
* `gallery`, `reports`, `logos`, `logo_likes`, `logo_reports` und `profiles` und die Buckets `gallery-images`, `card-images`,
|
||||
* `logo-images`. `profiles` hat wie in Appwrite einen Unique-Index auf `usernameKey` (UNIQUE_INDEXES): eine zweite Zeile mit
|
||||
* demselben Schlüssel, angelegt oder per PATCH geändert, antwortet 409 `row_already_exists` (so meldet Appwrite auch eine
|
||||
* Verletzung des Unique-Index); Testhilfe: `addProfile`.
|
||||
*
|
||||
* Abfragen: `queries[]=<json>` mit den Methoden equal, cursorAfter und limit. Die Antwort enthält wie bei Appwrite `total`:
|
||||
* die Zahl aller Treffer der equal-Abfragen, unabhängig von limit und cursorAfter (darauf baut `countRows`).
|
||||
@@ -18,8 +21,8 @@
|
||||
* Test auffallen.
|
||||
* Dateien werden nach ID gesucht und liefern ihren echten `bucketId` zurück (der Bucket in der URL zählt nicht),
|
||||
* so lässt sich der Fall „Datei liegt in einem anderen Bucket“ nachstellen.
|
||||
* Testhilfen: failNext/hangNext/delayNext (Aufruf scheitert, hängt, wird verzögert) und swapContentAfterPermissions
|
||||
* (Dateiinhalt ändert sich nach dem nächsten Rechte-Update).
|
||||
* Testhilfen: failNext/hangNext/delayNext/holdNext (Aufruf scheitert, hängt, wird verzögert, wartet auf die Freigabe des Tests)
|
||||
* und swapContentAfterPermissions (Dateiinhalt ändert sich nach dem nächsten Rechte-Update).
|
||||
*/
|
||||
|
||||
const http = require('node:http');
|
||||
@@ -105,6 +108,16 @@ const COLUMNS = {
|
||||
reason: { enum: ['unangemessen', 'kartendaten', 'urheberrecht', 'spam', 'sonstiges'], required: true },
|
||||
reporterHash: { size: 64, required: true },
|
||||
},
|
||||
// Benutzernamen: Zeilen-ID = Konto-ID, `usernameKey` = der Name klein geschrieben (Unique-Index)
|
||||
profiles: {
|
||||
username: { size: 20, required: true },
|
||||
usernameKey: { size: 20, required: true },
|
||||
},
|
||||
};
|
||||
|
||||
/** Unique-Indizes wie in web/scripts/appwrite-setup.mjs: je Tabelle die Spalten, deren Werte nur in einer Zeile vorkommen dürfen. */
|
||||
const UNIQUE_INDEXES = {
|
||||
profiles: ['usernameKey'],
|
||||
};
|
||||
|
||||
/** Standardwerte der Spalten, die Appwrite beim Anlegen einsetzt, wenn die Anfrage sie weglässt. */
|
||||
@@ -121,6 +134,7 @@ async function startFakeAppwrite({ project = 'proj-test', key = 'KEY-test-secret
|
||||
logos: new Map(),
|
||||
logo_likes: new Map(),
|
||||
logo_reports: new Map(),
|
||||
profiles: new Map(),
|
||||
};
|
||||
const files = new Map();
|
||||
const contents = new Map();
|
||||
@@ -162,6 +176,11 @@ async function startFakeAppwrite({ project = 'proj-test', key = 'KEY-test-secret
|
||||
return addRow('cards', id, { name: 'Karte' }, permissions || [`read("user:${owner}")`, `update("user:${owner}")`, `delete("user:${owner}")`]);
|
||||
}
|
||||
|
||||
/** Benutzername eines Kontos (Tabelle `profiles`) mit den Rechten, die die API vergibt: Zeilen-ID = Konto-ID, öffentlich lesbar. */
|
||||
function addProfile(userId, username = userId) {
|
||||
return addRow('profiles', userId, { username, usernameKey: username.toLowerCase() }, ['read("any")']);
|
||||
}
|
||||
|
||||
function addRow(table, id, data, permissions) {
|
||||
const row = { $id: id, $createdAt: new Date(1_700_000_000_000 + (++sequence) * 1000).toISOString(), $permissions: permissions, ...data };
|
||||
tables[table].set(id, row);
|
||||
@@ -183,6 +202,20 @@ async function startFakeAppwrite({ project = 'proj-test', key = 'KEY-test-secret
|
||||
injections.push({ predicate, delayMs });
|
||||
}
|
||||
|
||||
/**
|
||||
* Nächster passender Aufruf wartet, bis die zurückgegebene Funktion aufgerufen wird (er steht dann schon in `calls`). So erzwingt
|
||||
* ein Test eine Reihenfolge ohne Zeitannahmen, etwa: beide Anfragen haben vorab geprüft, bevor eine von ihnen schreibt.
|
||||
* @returns {() => void} gibt den Aufruf frei
|
||||
*/
|
||||
function holdNext(predicate) {
|
||||
let release;
|
||||
const gate = new Promise((resolve) => {
|
||||
release = resolve;
|
||||
});
|
||||
injections.push({ predicate, gate });
|
||||
return release;
|
||||
}
|
||||
|
||||
/**
|
||||
* Nach dem nächsten Rechte-Update (PUT) der Datei `id` hat sie den Inhalt `content` (einmalig). So lässt sich eine Datei
|
||||
* nachstellen, die zwischen der ersten Prüfung und dem Festlegen der Rechte ausgetauscht wurde: der Anfang ist beim ersten
|
||||
@@ -213,6 +246,13 @@ async function startFakeAppwrite({ project = 'proj-test', key = 'KEY-test-secret
|
||||
return null;
|
||||
}
|
||||
|
||||
/** Hat eine andere Zeile der Tabelle schon diese Werte in den Spalten eines Unique-Index? (`values` = die Zeile, wie sie danach aussähe.) */
|
||||
function violatesUnique(table, id, values) {
|
||||
const columns = UNIQUE_INDEXES[table];
|
||||
if (!columns) return false;
|
||||
return [...tables[table].values()].some((row) => row.$id !== id && columns.every((name) => row[name] === values[name]));
|
||||
}
|
||||
|
||||
const server = http.createServer((req, res) => {
|
||||
const chunks = [];
|
||||
req.on('data', (chunk) => chunks.push(chunk));
|
||||
@@ -244,7 +284,9 @@ async function startFakeAppwrite({ project = 'proj-test', key = 'KEY-test-secret
|
||||
if (injectionIndex !== -1) {
|
||||
const [entry] = injections.splice(injectionIndex, 1);
|
||||
if (entry.hang) return;
|
||||
if (entry.delayMs) {
|
||||
if (entry.gate) {
|
||||
await entry.gate;
|
||||
} else if (entry.delayMs) {
|
||||
await new Promise((resolve) => setTimeout(resolve, entry.delayMs));
|
||||
} else {
|
||||
return error(entry.status, entry.type);
|
||||
@@ -279,7 +321,9 @@ async function startFakeAppwrite({ project = 'proj-test', key = 'KEY-test-secret
|
||||
const data = body.data || {};
|
||||
const problem = validateData(table, data, { partial: false });
|
||||
if (problem) return error(400, 'document_invalid_structure', problem);
|
||||
const row = addRow(table, body.rowId, { ...DEFAULTS[table], ...data }, body.permissions || []);
|
||||
const values = { ...DEFAULTS[table], ...data };
|
||||
if (violatesUnique(table, body.rowId, values)) return error(409, 'row_already_exists');
|
||||
const row = addRow(table, body.rowId, values, body.permissions || []);
|
||||
return reply(201, row);
|
||||
}
|
||||
|
||||
@@ -302,6 +346,7 @@ async function startFakeAppwrite({ project = 'proj-test', key = 'KEY-test-secret
|
||||
const data = body.data || {};
|
||||
const problem = validateData(table, data, { partial: true });
|
||||
if (problem) return error(400, 'document_invalid_structure', problem);
|
||||
if (violatesUnique(table, segments[6], { ...row, ...data })) return error(409, 'row_already_exists');
|
||||
Object.assign(row, data);
|
||||
if (Array.isArray(body.permissions)) row.$permissions = body.permissions;
|
||||
return reply(200, row);
|
||||
@@ -360,10 +405,12 @@ async function startFakeAppwrite({ project = 'proj-test', key = 'KEY-test-secret
|
||||
addRawUser,
|
||||
addFile,
|
||||
addCard,
|
||||
addProfile,
|
||||
addRow,
|
||||
failNext,
|
||||
hangNext,
|
||||
delayNext,
|
||||
holdNext,
|
||||
swapContentAfterPermissions,
|
||||
row: (table, id) => tables[table].get(id) || null,
|
||||
rows: (table) => [...tables[table].values()],
|
||||
|
||||
Reference in new issue
Block a user