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
@@ -0,0 +1,88 @@
|
||||
'use strict';
|
||||
|
||||
/**
|
||||
* Benutzernamen im API-Dienst `aircard-api` (Node 22, nur Bordmittel): ein Name je Konto, eindeutig ohne Rücksicht auf Groß- und
|
||||
* Kleinschreibung. app.js hängt die Route an und gibt Anmeldung, Sperre je Schlüssel und das Log mit (wie bei logos.js).
|
||||
*
|
||||
* POST /api/profil Nutzer-JWT {username}
|
||||
* 401 · 400 `usernameInvalid` (Muster) · 400 `usernameReserved` (Sperrliste) · 409 `usernameTaken` (ein anderes Konto hat den
|
||||
* Namen) · 200 {username}
|
||||
* Legt die Zeile des Kontos an (Zeilen-ID = Konto-ID, Recht genau read("any")) oder ändert sie: der alte Name ist damit sofort
|
||||
* wieder frei. Die eigene Schreibweise zu ändern („kenso“ → „Kenso“) ist erlaubt.
|
||||
*
|
||||
* Tabelle `profiles`: `username` (der Name, wie eingegeben) und `usernameKey` (klein geschrieben). Eindeutig ist `usernameKey`: die
|
||||
* Vorabprüfung fängt den Normalfall mit einer klaren Antwort ab, bei einem Rennen zweier Konten entscheidet der Unique-Index
|
||||
* `username_key` (Appwrite antwortet 409, das wird ebenfalls `usernameTaken`). Sperre je Konto: `profile:<Konto>`.
|
||||
* Hochladen und Veröffentlichen verlangen ein Profil (`hasProfile`, 403 `usernameRequired`), siehe logos.js und app.js.
|
||||
* Logs nennen nie Namen, Schlüssel, Konto-IDs oder JWTs: nur „profile created“ bzw. „profile updated“.
|
||||
*/
|
||||
|
||||
const { queryEqual, queryLimit } = require('./appwrite.js');
|
||||
const {
|
||||
MSG,
|
||||
USERNAME_PATTERN,
|
||||
HttpError,
|
||||
isDuplicate,
|
||||
parseJsonObject,
|
||||
visibleRowPermissions,
|
||||
usernameKey,
|
||||
isReservedUsername,
|
||||
} = require('./common.js');
|
||||
|
||||
const TABLE_PROFILES = 'profiles';
|
||||
|
||||
/**
|
||||
* Hat das Konto einen Benutzernamen? Die Zeilen-ID des Profils ist die Konto-ID. Ein Appwrite-Fehler wird nicht verschluckt
|
||||
* (auch eine fehlende Tabelle nicht): der Aufrufer antwortet dann mit 502 bzw. 500, nie mit „kein Profil“.
|
||||
* @param {object} appwrite Objekt aus createAppwrite
|
||||
* @param {string} userId Konto-ID
|
||||
* @returns {Promise<boolean>}
|
||||
*/
|
||||
async function hasProfile(appwrite, userId) {
|
||||
return (await appwrite.getRow(TABLE_PROFILES, userId)) !== null;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {object} options
|
||||
* @param {object} options.appwrite Objekt aus createAppwrite
|
||||
* @param {(message: string) => void} options.say Ereigniszeile ins Log (mit Zeilenbudget)
|
||||
* @param {(key: string, fn: () => Promise<any>) => Promise<any>} options.withLock Je Schlüssel nacheinander
|
||||
* @param {(req) => Promise<{id: string, isAdmin: boolean}>} options.requireUser wirft 401 ohne gültiges JWT
|
||||
* @returns {Array<[string, {name: string, json: true, run: (ctx: {req, raw: string}) => Promise<{status: number, body: object}>}]>}
|
||||
*/
|
||||
function createProfileRoutes({ appwrite, say, withLock, requireUser }) {
|
||||
async function saveProfile({ req, raw }) {
|
||||
const user = await requireUser(req);
|
||||
const body = parseJsonObject(raw);
|
||||
// Kein Trimmen: das Muster lässt keine Leerzeichen zu, der Client schickt den Namen schon bereinigt.
|
||||
const { username } = body;
|
||||
if (typeof username !== 'string' || !USERNAME_PATTERN.test(username)) throw new HttpError(400, MSG.usernameInvalid);
|
||||
if (isReservedUsername(username)) throw new HttpError(400, MSG.usernameReserved);
|
||||
const key = usernameKey(username);
|
||||
|
||||
// Je Konto nacheinander: Nachsehen, ob es schon eine Zeile gibt, und Anlegen bzw. Ändern dürfen sich nicht überholen
|
||||
// (zwei Anfragen desselben Kontos würden sonst beide anlegen wollen und die zweite fälschlich „vergeben“ melden).
|
||||
return withLock(`profile:${user.id}`, async () => {
|
||||
// Vorabprüfung: hat ein anderes Konto diesen Schlüssel? Der eigene Eintrag zählt nicht (nur die Schreibweise ändern ist erlaubt).
|
||||
const holders = await appwrite.listRows(TABLE_PROFILES, [queryEqual('usernameKey', [key]), queryLimit(1)]);
|
||||
if (holders.some((row) => row.$id !== user.id)) throw new HttpError(409, MSG.usernameTaken);
|
||||
|
||||
const own = await appwrite.getRow(TABLE_PROFILES, user.id);
|
||||
try {
|
||||
if (own) await appwrite.updateRow(TABLE_PROFILES, user.id, { username, usernameKey: key });
|
||||
else await appwrite.createRow(TABLE_PROFILES, user.id, { username, usernameKey: key }, visibleRowPermissions());
|
||||
} catch (err) {
|
||||
// Ein anderes Konto war schneller: der Unique-Index auf usernameKey meldet 409, beim Anlegen wie beim Ändern. Ein 409 wegen der
|
||||
// eigenen Zeilen-ID gibt es nicht: unter der Sperre des Kontos gibt es die Zeile jetzt (dann ändern wir) oder nicht (dann legen wir an).
|
||||
if (isDuplicate(err)) throw new HttpError(409, MSG.usernameTaken);
|
||||
throw err;
|
||||
}
|
||||
say(own ? 'profile updated' : 'profile created');
|
||||
return { status: 200, body: { username } };
|
||||
});
|
||||
}
|
||||
|
||||
return [['POST /api/profil', { name: 'profil', json: true, run: saveProfile }]];
|
||||
}
|
||||
|
||||
module.exports = { createProfileRoutes, hasProfile };
|
||||
Reference in new issue
Block a user