'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:`. * 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} */ 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) => Promise} 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 };