Update auth spec: passkeys + magic links, no passwords

- Passkey (WebAuthn) as primary auth for registration and login
- Magic link email as fallback for new devices
- Removed password_hash from users table
- Added credentials (WebAuthn) and magic_tokens tables
- Updated tasks (7.1-7.10)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Ullrich Schäfer 2026-03-23 17:24:27 +01:00
parent afb413b026
commit 3604a2d066
No known key found for this signature in database
GPG key ID: A32FF691A0F752D9
3 changed files with 81 additions and 20 deletions

View file

@ -25,7 +25,6 @@ export const journalSchema = pgSchema("journal");
export const users = journalSchema.table("users", {
id: text("id").primaryKey(),
email: text("email").notNull().unique(),
passwordHash: text("password_hash").notNull(),
username: text("username").notNull().unique(),
displayName: text("display_name"),
bio: text("bio"),
@ -33,6 +32,28 @@ export const users = journalSchema.table("users", {
createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow(),
});
export const credentials = journalSchema.table("credentials", {
id: text("id").primaryKey(),
userId: text("user_id")
.notNull()
.references(() => users.id, { onDelete: "cascade" }),
credentialId: bytea("credential_id").notNull(),
publicKey: bytea("public_key").notNull(),
counter: integer("counter").notNull().default(0),
deviceType: text("device_type"),
transports: jsonb("transports").$type<string[]>(),
createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow(),
});
export const magicTokens = journalSchema.table("magic_tokens", {
id: text("id").primaryKey(),
email: text("email").notNull(),
token: text("token").notNull().unique(),
expiresAt: timestamp("expires_at", { withTimezone: true }).notNull(),
usedAt: timestamp("used_at", { withTimezone: true }),
createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow(),
});
export const routes = journalSchema.table("routes", {
id: text("id").primaryKey(),
ownerId: text("owner_id")