feat(account): Login, Registrierung, Passwort zurücksetzen

- appwrite 22.4.1 (exakt gepinnt); Client mit Endpoint aus VITE_APPWRITE_ENDPOINT
  bzw. location.origin + /v1, Projekt aus VITE_APPWRITE_PROJECT_ID bzw. aircard
- auth.ts: getCurrentUser (401 -> null, ungültige Antwort -> Fehler), login, register,
  logout, Passwort-Link anfordern/abschließen, onAuthChange, validatePassword
- errors.ts: deutsche Meldungen nach type, dann code (Tabelle aus dem Plan)
- Login-Dialog (Anmelden/Registrieren/Passwort vergessen) als gemeinsames <dialog>,
  Konto-Menü im Seitenkopf, Seite /passwort-neu
- Tests: errors, auth, Dialog, Passwort-neu-Seite

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
ClaudeandClaude Opus 5.5 committed 2026-10-04 08:54:10 +00:00
1 parent d1d4d621a7
commit 10c55ca933
15 files changed
+1866 -10

No files matched your search

+64
View File
@@ -0,0 +1,64 @@
// Konto-Menü im Seitenkopf: abgemeldet „Anmelden“, angemeldet E-Mail und „Abmelden“.
import './account.css';
import { el } from '../editor/dom';
import { currentUser, logout, onAuthChange } from './auth';
import { errorMessage } from './errors';
import { requireLogin } from './login-dialog';
export interface AccountMenu {
/** Zeigt das Menü erst, wenn der Anmeldestatus bekannt ist (kein Aufblitzen von „Anmelden“ bei angemeldeten Nutzern). */
ready(): void;
dispose(): void;
}
export function mountAccountMenu(slot: HTMLElement): AccountMenu {
let isReady = false;
function render(): void {
if (!isReady) {
slot.replaceChildren();
return;
}
const user = currentUser();
if (!user) {
const signIn = el('button', { type: 'button', class: 'auth-btn auth-btn-small' }, 'Anmelden');
signIn.addEventListener('click', () => void requireLogin('login'));
slot.replaceChildren(el('div', { class: 'account-menu' }, signIn));
return;
}
const email = el('span', { class: 'account-email', title: user.email }, user.email);
const signOut = el('button', { type: 'button', class: 'auth-btn auth-btn-small' }, 'Abmelden');
const error = el('span', { class: 'account-error', role: 'alert' });
signOut.addEventListener('click', () => {
signOut.disabled = true;
error.textContent = '';
// Bei Erfolg zeichnet onAuthChange das Menü neu; nur der Fehlerfall braucht Handarbeit.
logout().catch((err: unknown) => {
error.textContent = errorMessage(err, 'load');
signOut.disabled = false;
});
});
slot.replaceChildren(
el(
'div',
{ class: 'account-menu', role: 'group', 'aria-label': 'Konto' },
el('div', { class: 'account-row' }, email, signOut),
error,
),
);
}
const off = onAuthChange(render);
render();
return {
ready() {
isReady = true;
render();
},
dispose() {
off();
slot.replaceChildren();
},
};
}