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

+61 -2
View File
@@ -1,8 +1,9 @@
'use strict';
/**
* Gemeinsame Helfer des API-Dienstes `aircard-api` für die Karten (app.js) und die Logos (logos.js): Fehlertexte und
* -codes, Prüfungen, Rechte-Strings, Melder-Hash, Bildmaße. Nur reine Funktionen und Konstanten, kein Zustand.
* Gemeinsame Helfer des API-Dienstes `aircard-api` für die Karten (app.js), die Logos (logos.js) und die Benutzernamen
* (profiles.js): Fehlertexte und -codes, Prüfungen, Rechte-Strings, Melder-Hash, Bildmaße, Muster und Sperrliste der Benutzernamen.
* Nur reine Funktionen und Konstanten, kein Zustand.
*
* Fehlerantworten sind immer {error: <deutscher Text>, code: <Schlüssel in MSG>}. Texte ohne festen Schlüssel (dynamische
* Prüftexte wie „Der Titel ist zu lang (höchstens 60 Zeichen).“) haben bei Status 400 den code `badRequest`, sonst `error`.
@@ -23,6 +24,41 @@ const IMAGE_HEAD_BYTES = 64;
const REASONS = ['unangemessen', 'kartendaten', 'urheberrecht', 'spam', 'sonstiges'];
const ID_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._-]{0,35}$/;
/** Benutzername: 3-20 Zeichen, das erste ein Buchstabe oder eine Zahl, danach Buchstaben, Zahlen, Punkt, Unterstrich, Bindestrich (nur ASCII). */
const USERNAME_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._-]{2,19}$/;
/**
* Gesperrte Benutzernamen als Schlüssel (klein geschrieben, siehe `usernameKey`): Rollen, der Dienst selbst und Platzhalter, die nach
* einem Konto aussehen. Jeder Eintrag passt zu USERNAME_PATTERN (sonst wäre er wirkungslos: der Name scheiterte schon als ungültig).
*/
const RESERVED_USERNAMES = Object.freeze([
'admin',
'administrator',
'root',
'system',
'support',
'moderator',
'mod',
'aircard',
'aircardios',
'aircard-ios',
'aircard.ios',
'aircard_ios',
'webklar',
'anonym',
'anonymous',
'null',
'undefined',
'team',
'staff',
'official',
'offiziell',
'hilfe',
'help',
'kontakt',
'contact',
'info',
]);
const RESERVED_USERNAME_SET = new Set(RESERVED_USERNAMES);
// Steuerzeichen (C0, DEL, C1), Nullbreiten- und Bidi-Zeichen (U+200B-200F, U+202A-202E, U+2060-206F) und das BOM (U+FEFF):
// sie sind unsichtbar, ordnen Text um oder lassen Titel leer aussehen. Dasselbe Muster steht in publish-dialog.ts.
const CONTROL_CHARACTERS = /[\u0000-\u001f\u007f-\u009f\u200b-\u200f\u202a-\u202e\u2060-\u206f\ufeff]/;
@@ -63,6 +99,12 @@ const MSG = {
tooManyLogos: `Du hast schon ${MAX_LOGOS} Logos hochgeladen. Lösche eins, um ein neues hochzuladen.`,
badHeart: 'Ungültige Angabe zum Herz.',
badMono: 'Ungültige Angabe zu „einfarbig“.',
// Benutzernamen
usernameInvalid:
'Der Benutzername muss 3–20 Zeichen lang sein, mit einem Buchstaben oder einer Zahl beginnen und darf nur Buchstaben, Zahlen, Punkt, Unterstrich und Bindestrich enthalten.',
usernameReserved: 'Dieser Benutzername ist nicht erlaubt.',
usernameTaken: 'Dieser Benutzername ist schon vergeben.',
usernameRequired: 'Bitte wähle zuerst einen Benutzernamen.',
tooLarge: 'Die Anfrage ist zu groß.',
jsonOnly: 'Nur application/json wird unterstützt.',
unavailable: 'Der Dienst ist gerade nicht erreichbar.',
@@ -97,6 +139,19 @@ function isId(value) {
return typeof value === 'string' && ID_PATTERN.test(value);
}
/**
* Der Schlüssel eines Benutzernamens (Spalte `usernameKey`): der Name klein geschrieben. Über ihn ist ein Name nur einmal
* vergeben, egal wie er geschrieben ist („Kenso“ und „kenso“ sind derselbe). Für Zeichenketten, die schon USERNAME_PATTERN erfüllen.
*/
function usernameKey(name) {
return name.toLowerCase();
}
/** Steht der Schlüssel dieses Namens auf der Sperrliste? (In jeder Schreibweise: „Admin“, „AirCard-IOS“.) */
function isReservedUsername(name) {
return RESERVED_USERNAME_SET.has(usernameKey(name));
}
function assertOwner(owner) {
if (!isId(owner)) throw new TypeError('Ungültige Nutzer-ID');
return owner;
@@ -309,11 +364,15 @@ module.exports = {
IMAGE_HEAD_BYTES,
REASONS,
ID_PATTERN,
USERNAME_PATTERN,
RESERVED_USERNAMES,
CONTROL_CHARACTERS,
MSG,
codeFor,
HttpError,
isId,
usernameKey,
isReservedUsername,
isPlainObject,
isDuplicate,
parseJsonObject,