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:
ClaudeandClaude Opus 5.5 committed 2026-10-05 06:40:44 +00:00
1 parent a65c075004
commit 19c8151c7e
15 files changed
+980 -53

No files matched your search

+50
View File
@@ -33,9 +33,12 @@ const SALT = 'test-salt-0123456789abcdef0123456789abcdef';
const ME = 'user-me';
const OTHER = 'user-other';
const ADMIN = 'user-admin';
/** Konto ohne Benutzernamen (alle anderen haben aus `setup` ein Profil): Veröffentlichen geht erst mit Profil. */
const NEWBIE = 'user-new';
const JWT_ME = 'eyJhbGciOiJI.payload-me.signature-me';
const JWT_OTHER = 'eyJhbGciOiJI.payload-other.signature-other';
const JWT_ADMIN = 'eyJhbGciOiJI.payload-admin.signature-admin';
const JWT_NEW = 'eyJhbGciOiJI.payload-new.signature-new';
const MIB5 = 5 * 1024 * 1024;
const TAKEN = 'Diese Bilder sind bereits veröffentlicht oder gesperrt.';
const NOT_CARD_FORMAT = 'Das Bild hat nicht das Kartenformat.';
@@ -54,6 +57,9 @@ async function setup() {
fake.addUser(JWT_ME, { id: ME });
fake.addUser(JWT_OTHER, { id: OTHER });
fake.addUser(JWT_ADMIN, { id: ADMIN, labels: ['admin'] });
fake.addUser(JWT_NEW, { id: NEWBIE });
// Veröffentlichen verlangt einen Benutzernamen (Tabelle `profiles`): ME, OTHER und ADMIN haben einen, NEWBIE nicht.
for (const id of [ME, OTHER, ADMIN]) fake.addProfile(id);
const logs = [];
const clock = { value: 1_700_000_000_000 };
@@ -406,6 +412,50 @@ test('Veröffentlichen ohne oder mit ungültigem JWT: 401 und nichts angelegt',
assert.equal(fake.calls.filter((c) => c.path === '/v1/account').length, 1, 'fehlendes oder kaputtes JWT erreicht Appwrite gar nicht erst');
});
test('Veröffentlichen ohne Benutzernamen: 403 usernameRequired noch vor jeder anderen Prüfung (auch vor Body, Dateien und Karte), nichts angelegt', async () => {
const { post, fake } = await setup();
seedUploads(fake, NEWBIE);
const publish = (body) => post('/api/veroeffentlichen', body, JWT_NEW);
expectError(await publish(publishBody()), 403, 'usernameRequired', 'gültiger Body');
expectError(await publish({}), 403, 'usernameRequired', 'ungültiger Body (kein Titel, keine Dateien)');
expectError(await publish(publishBody({ title: '' })), 403, 'usernameRequired', 'Titel fehlt');
expectError(await publish(publishBody({ imageFileId: 'gibts-nicht' })), 403, 'usernameRequired', 'Datei gibt es nicht');
expectError(await publish(publishBody({ sourceCardId: 'fremde-karte' })), 403, 'usernameRequired', 'Karte gibt es nicht');
expectError(await publish(null), 403, 'usernameRequired', 'kein Body');
assert.equal(fake.rows('gallery').length, 0);
assert.equal(storageCalls(fake).length, 0, 'keine Datei wird angefasst');
assert.deepEqual(fake.file('img1').$permissions, ['read("any")', 'delete("user:user-new")'], 'die Dateien bleiben, wie sie waren');
// Appwrite sieht je Versuch nur die Prüfung des JWT und die Suche nach dem Profil (Zeilen-ID = Konto-ID)
assert.deepEqual(
fake.calls.filter((c) => c.path !== '/v1/account').map((c) => `${c.method} ${c.path}`),
Array(6).fill('GET /v1/tablesdb/aircard/tables/profiles/rows/user-new'),
);
});
test('Veröffentlichen: das Profil gehört dem Konto – ein Profil der anderen Konten hilft nicht, mit eigenem Profil (über /api/profil) geht es', async () => {
const { post, fake } = await setup();
seedUploads(fake, NEWBIE);
expectError(await post('/api/veroeffentlichen', publishBody(), JWT_NEW), 403, 'usernameRequired');
const named = await post('/api/profil', { username: 'Neuling' }, JWT_NEW);
assert.deepEqual(named.json, { username: 'Neuling' });
const res = await post('/api/veroeffentlichen', publishBody(), JWT_NEW);
assert.equal(res.status, 201);
assert.equal(fake.row('gallery', res.json.id).ownerId, NEWBIE);
});
test('Veröffentlichen: lässt sich das Profil nicht prüfen (Appwrite-Fehler), 502 und nichts wird angelegt; der nächste Versuch gelingt', async () => {
const { post, fake } = await setup();
seedUploads(fake);
fake.failNext((c) => c.method === 'GET' && c.path.endsWith('/tables/profiles/rows/user-me'), { status: 500 });
expectError(await post('/api/veroeffentlichen', publishBody(), JWT_ME), 502, 'unavailable');
assert.equal(fake.rows('gallery').length, 0);
assert.equal(storageCalls(fake).length, 0);
assert.equal((await post('/api/veroeffentlichen', publishBody(), JWT_ME)).status, 201);
});
test('Veröffentlichen mit fremder Datei: 403 und nichts angelegt', async () => {
const { post, fake } = await setup();
fake.addFile({ id: 'img1', owner: OTHER });