Passkey browser compatibility: detect support, magic link fallback

- Login: hide passkey button when browser lacks WebAuthn, default to
  magic link mode, hide "back to passkey" link
- Register: detect passkey support and offer magic link registration as
  fallback. New server-side registerWithMagicLink() creates account
  without credential and sends verification email.
- Home: show passkey count for logged-in users, show amber unsupported
  message instead of hiding the add-passkey prompt entirely
- i18n: add passkeyNotSupported, passkeyNotSupportedRegister,
  registerWithMagicLink, passkeyStatus keys (en + de)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Ullrich Schäfer 2026-03-29 09:54:37 +02:00
parent 9c7891402f
commit 9adf9b977a
No known key found for this signature in database
GPG key ID: A32FF691A0F752D9
7 changed files with 199 additions and 30 deletions

View file

@ -144,6 +144,36 @@ export async function addPasskeyFinish(
});
}
// --- Registration via Magic Link (no passkey) ---
export async function registerWithMagicLink(email: string, username: string): Promise<string> {
const db = getDb();
const [existingEmail] = await db.select().from(users).where(eq(users.email, email));
if (existingEmail) throw new Error("Email already in use");
const [existingUsername] = await db.select().from(users).where(eq(users.username, username));
if (existingUsername) throw new Error("Username already taken");
const userId = randomUUID();
const domain = process.env.DOMAIN ?? "localhost";
await db.insert(users).values({ id: userId, email, username, domain });
// Create magic token for verification
const token = randomBytes(32).toString("base64url");
const expiresAt = new Date(Date.now() + 15 * 60 * 1000);
await db.insert(magicTokens).values({
id: randomUUID(),
email,
token,
expiresAt,
});
return token;
}
// --- Passkey Login ---
export async function startAuthentication() {