diff --git a/bun.lock b/bun.lock index 5b3e823..33a18ea 100644 --- a/bun.lock +++ b/bun.lock @@ -4,11 +4,9 @@ "workspaces": { "": { "name": "chillhop", - "dependencies": { - "@better-auth/passkey": "^1.5.6", - }, "devDependencies": { "@better-auth/cli": "~1.4.21", + "@better-auth/passkey": "~1.4.21", "@cloudflare/workers-types": "^4.20250517.0", "@lucide/svelte": "^0.492.0", "@sveltejs/adapter-cloudflare": "^7.2.6", @@ -110,7 +108,7 @@ "@better-auth/core": ["@better-auth/core@1.4.21", "", { "dependencies": { "@standard-schema/spec": "^1.0.0", "zod": "^4.3.5" }, "peerDependencies": { "@better-auth/utils": "0.3.0", "@better-fetch/fetch": "1.1.21", "better-call": "1.1.8", "jose": "^6.1.0", "kysely": "^0.28.5", "nanostores": "^1.0.1" } }, "sha512-R4s7pwShkqB21fZ599QASbXxqFcoxanLyz7DHSX6SJPNYV748wBLsm3xM9VrjfvWMpS+cQUErOCt9yWT1hMn6w=="], - "@better-auth/passkey": ["@better-auth/passkey@1.5.6", "", { "dependencies": { "@simplewebauthn/browser": "^13.2.2", "@simplewebauthn/server": "^13.2.3", "zod": "^4.3.6" }, "peerDependencies": { "@better-auth/core": "1.5.6", "@better-auth/utils": "0.3.1", "@better-fetch/fetch": "1.1.21", "better-auth": "1.5.6", "better-call": "1.3.2", "nanostores": "^1.0.1" } }, "sha512-2DQkPK5Rw7g6Zixa3MSoH31s4Au96O94+QvJl3F0LK3P6KDjEGlRh1CgzQmzafwBJjmsRx9jSwckGP6jiEEDtw=="], + "@better-auth/passkey": ["@better-auth/passkey@1.4.22", "", { "dependencies": { "@simplewebauthn/browser": "^13.1.2", "@simplewebauthn/server": "^13.1.2", "zod": "^4.3.5" }, "peerDependencies": { "@better-auth/core": "1.4.22", "@better-auth/utils": "0.3.0", "@better-fetch/fetch": "1.1.21", "better-auth": "1.4.22", "better-call": "1.1.8", "nanostores": "^1.0.1" } }, "sha512-wWXOCkF0MauMW3wfND5vNF9Dn9n8nfHV5k6F9qv9DWt9GrdzTWbV6dviXubDahMyVSiX3OrkupUBTRRT+dENlg=="], "@better-auth/telemetry": ["@better-auth/telemetry@1.4.21", "", { "dependencies": { "@better-auth/utils": "0.3.0", "@better-fetch/fetch": "1.1.21" }, "peerDependencies": { "@better-auth/core": "1.4.21" } }, "sha512-LX+FGMZnhR2KQZ0idHH1+UwlXvkOl6P8w3Gne4TtjvUCt3QjG9FKIuP9JD3MAmEEkwGt0SoAPHPJEGTjUl3ydg=="], diff --git a/drizzle.config.local.ts b/drizzle.config.local.ts new file mode 100644 index 0000000..b0f2f85 --- /dev/null +++ b/drizzle.config.local.ts @@ -0,0 +1,24 @@ +import { readdirSync } from 'node:fs'; +import { join } from 'node:path'; +import { defineConfig } from 'drizzle-kit'; + +const localD1Dir = '.wrangler/state/v3/d1/miniflare-D1DatabaseObject'; + +const localD1File = readdirSync(localD1Dir).find( + (file) => file.endsWith('.sqlite') && file !== 'metadata.sqlite', +); + +if (!localD1File) { + throw new Error(`No local D1 sqlite file found in ${localD1Dir}`); +} + +export default defineConfig({ + schema: './src/lib/server/db/schema.ts', + out: './drizzle', + dialect: 'sqlite', + dbCredentials: { + url: join(localD1Dir, localD1File), + }, + verbose: true, + strict: true, +}); diff --git a/drizzle/0002_account_number_auth.sql b/drizzle/0002_account_number_auth.sql new file mode 100644 index 0000000..abb8658 --- /dev/null +++ b/drizzle/0002_account_number_auth.sql @@ -0,0 +1,28 @@ +CREATE TABLE `passkey` ( + `id` text PRIMARY KEY NOT NULL, + `name` text, + `public_key` text NOT NULL, + `user_id` text NOT NULL, + `credential_id` text NOT NULL, + `counter` integer NOT NULL, + `device_type` text NOT NULL, + `backed_up` integer NOT NULL, + `transports` text, + `created_at` integer, + `aaguid` text, + FOREIGN KEY (`user_id`) REFERENCES `user`(`id`) ON UPDATE no action ON DELETE cascade +); +--> statement-breakpoint +CREATE INDEX `passkey_userId_idx` ON `passkey` (`user_id`); +--> statement-breakpoint +CREATE INDEX `passkey_credentialID_idx` ON `passkey` (`credential_id`); +--> statement-breakpoint +ALTER TABLE `user` ADD COLUMN `account_number` text NOT NULL DEFAULT ''; +--> statement-breakpoint +ALTER TABLE `user` ADD COLUMN `is_anonymous` integer DEFAULT false; +--> statement-breakpoint +UPDATE `user` +SET `account_number` = substr('0000000000000000' || abs(random()), -16, 16) +WHERE `account_number` = ''; +--> statement-breakpoint +CREATE UNIQUE INDEX `user_account_number_unique` ON `user` (`account_number`); diff --git a/drizzle/0003_add_passkey_table.sql b/drizzle/0003_add_passkey_table.sql new file mode 100644 index 0000000..49bac5a --- /dev/null +++ b/drizzle/0003_add_passkey_table.sql @@ -0,0 +1,18 @@ +CREATE TABLE `passkey` ( + `id` text PRIMARY KEY NOT NULL, + `name` text, + `public_key` text NOT NULL, + `user_id` text NOT NULL, + `credential_id` text NOT NULL, + `counter` integer NOT NULL, + `device_type` text NOT NULL, + `backed_up` integer NOT NULL, + `transports` text, + `created_at` integer, + `aaguid` text, + FOREIGN KEY (`user_id`) REFERENCES `user`(`id`) ON UPDATE no action ON DELETE cascade +); +--> statement-breakpoint +CREATE INDEX `passkey_userId_idx` ON `passkey` (`user_id`); +--> statement-breakpoint +CREATE INDEX `passkey_credentialID_idx` ON `passkey` (`credential_id`); diff --git a/drizzle/meta/0002_snapshot.json b/drizzle/meta/0002_snapshot.json new file mode 100644 index 0000000..c8584e7 --- /dev/null +++ b/drizzle/meta/0002_snapshot.json @@ -0,0 +1,540 @@ +{ + "version": "6", + "dialect": "sqlite", + "id": "773dd59c-33d4-4b87-8541-dda2e9151482", + "prevId": "7c532ee7-a504-49a0-91fa-c0206fa1f3b0", + "tables": { + "task": { + "name": "task", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "priority": { + "name": "priority", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 1 + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "account": { + "name": "account", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "account_id": { + "name": "account_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "provider_id": { + "name": "provider_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "access_token": { + "name": "access_token", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "refresh_token": { + "name": "refresh_token", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "id_token": { + "name": "id_token", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "access_token_expires_at": { + "name": "access_token_expires_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "refresh_token_expires_at": { + "name": "refresh_token_expires_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "scope": { + "name": "scope", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "password": { + "name": "password", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(cast(unixepoch('subsecond') * 1000 as integer))" + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "account_userId_idx": { + "name": "account_userId_idx", + "columns": [ + "user_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "account_user_id_user_id_fk": { + "name": "account_user_id_user_id_fk", + "tableFrom": "account", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "passkey": { + "name": "passkey", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "public_key": { + "name": "public_key", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "credential_id": { + "name": "credential_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "counter": { + "name": "counter", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "device_type": { + "name": "device_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "backed_up": { + "name": "backed_up", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "transports": { + "name": "transports", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "aaguid": { + "name": "aaguid", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": { + "passkey_userId_idx": { + "name": "passkey_userId_idx", + "columns": [ + "user_id" + ], + "isUnique": false + }, + "passkey_credentialID_idx": { + "name": "passkey_credentialID_idx", + "columns": [ + "credential_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "passkey_user_id_user_id_fk": { + "name": "passkey_user_id_user_id_fk", + "tableFrom": "passkey", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "session": { + "name": "session", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "expires_at": { + "name": "expires_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "token": { + "name": "token", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(cast(unixepoch('subsecond') * 1000 as integer))" + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "ip_address": { + "name": "ip_address", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "user_agent": { + "name": "user_agent", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "session_token_unique": { + "name": "session_token_unique", + "columns": [ + "token" + ], + "isUnique": true + }, + "session_userId_idx": { + "name": "session_userId_idx", + "columns": [ + "user_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "session_user_id_user_id_fk": { + "name": "session_user_id_user_id_fk", + "tableFrom": "session", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "user": { + "name": "user", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "account_number": { + "name": "account_number", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "is_anonymous": { + "name": "is_anonymous", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": false + }, + "email_verified": { + "name": "email_verified", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "image": { + "name": "image", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(cast(unixepoch('subsecond') * 1000 as integer))" + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(cast(unixepoch('subsecond') * 1000 as integer))" + } + }, + "indexes": { + "user_email_unique": { + "name": "user_email_unique", + "columns": [ + "email" + ], + "isUnique": true + }, + "user_account_number_unique": { + "name": "user_account_number_unique", + "columns": [ + "account_number" + ], + "isUnique": true + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "verification": { + "name": "verification", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "identifier": { + "name": "identifier", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "value": { + "name": "value", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "expires_at": { + "name": "expires_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(cast(unixepoch('subsecond') * 1000 as integer))" + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(cast(unixepoch('subsecond') * 1000 as integer))" + } + }, + "indexes": { + "verification_identifier_idx": { + "name": "verification_identifier_idx", + "columns": [ + "identifier" + ], + "isUnique": false + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + } + }, + "views": {}, + "enums": {}, + "_meta": { + "schemas": {}, + "tables": {}, + "columns": {} + }, + "internal": { + "indexes": {} + } +} \ No newline at end of file diff --git a/drizzle/meta/_journal.json b/drizzle/meta/_journal.json index 6137b11..5b60b9e 100644 --- a/drizzle/meta/_journal.json +++ b/drizzle/meta/_journal.json @@ -15,6 +15,13 @@ "when": 1775242876895, "tag": "0001_curved_sunspot", "breakpoints": true + }, + { + "idx": 2, + "version": "6", + "when": 1775246437951, + "tag": "0002_account_number_auth", + "breakpoints": true } ] -} \ No newline at end of file +} diff --git a/package.json b/package.json index 20e6bba..1a9cc67 100644 --- a/package.json +++ b/package.json @@ -20,6 +20,7 @@ "db:migrate:prod": "drizzle-kit migrate", "db:migrate:dev": "bun wrangler d1 migrations apply lofi_db --local", "db:studio": "drizzle-kit studio", + "db:studio:dev": "drizzle-kit studio --config drizzle.config.local.ts", "auth:schema": "better-auth generate --config src/lib/server/auth.ts --output src/lib/server/db/auth.schema.ts --yes" }, "devDependencies": { @@ -48,10 +49,8 @@ "wrangler": "^4.63.0", "better-auth": "~1.4.21", "drizzle-orm": "^0.45.2", - "mode-watcher": "^1.0.5" + "mode-watcher": "^1.0.5", + "@better-auth/passkey": "~1.4.21" }, - "packageManager": "bun@1.3.5", - "dependencies": { - "@better-auth/passkey": "^1.5.6" - } + "packageManager": "bun@1.3.5" } diff --git a/src/app.d.ts b/src/app.d.ts index 300d924..fb79edb 100644 --- a/src/app.d.ts +++ b/src/app.d.ts @@ -1,6 +1,9 @@ -import type { User, Session } from 'better-auth/minimal'; +import type { auth } from '$lib/server/auth'; import { createAuth } from '$lib/server/auth'; +type Session = typeof auth.$Infer.Session.session; +type User = typeof auth.$Infer.Session.user; + // See https://svelte.dev/docs/kit/types#app.d.ts // for information about these interfaces declare global { diff --git a/src/lib/auth-client.ts b/src/lib/auth-client.ts new file mode 100644 index 0000000..64713ba --- /dev/null +++ b/src/lib/auth-client.ts @@ -0,0 +1,7 @@ +import { createAuthClient } from 'better-auth/svelte'; +import { passkeyClient } from '@better-auth/passkey/client'; + +export const authClient = createAuthClient({ + plugins: [passkeyClient()], +}); + diff --git a/src/lib/index.ts b/src/lib/index.ts index 856f2b6..dc1708d 100644 --- a/src/lib/index.ts +++ b/src/lib/index.ts @@ -1 +1 @@ -// place files you want to import through the `$lib` alias in this folder. +export { authClient } from './auth-client'; diff --git a/src/lib/server/auth.ts b/src/lib/server/auth.ts index 8c7237c..8e97d31 100644 --- a/src/lib/server/auth.ts +++ b/src/lib/server/auth.ts @@ -1,15 +1,92 @@ -import { betterAuth } from 'better-auth'; +import { APIError, betterAuth, type BetterAuthPlugin } from 'better-auth'; import { drizzleAdapter } from 'better-auth/adapters/drizzle'; +import { createAuthEndpoint } from 'better-auth/api'; +import { setSessionCookie } from 'better-auth/cookies'; +import { anonymous } from 'better-auth/plugins'; import { sveltekitCookies } from 'better-auth/svelte-kit'; import type { D1Database } from '@cloudflare/workers-types'; import { env } from '$env/dynamic/private'; import { getRequestEvent } from '$app/server'; import { getDb } from '$lib/server/db'; +import { passkey } from '@better-auth/passkey'; +import * as z from 'zod'; + +const generateAccountNumber = () => + Array.from(crypto.getRandomValues(new Uint8Array(16)), (value) => (value % 10).toString()).join(''); + +const accountNumber = () => + ({ + id: 'account-number', + endpoints: { + signInAccountNumber: createAuthEndpoint( + '/sign-in/account-number', + { + method: 'POST', + body: z.object({ + accountNumber: z.string().length(16), + }), + }, + async (ctx) => { + const user = (await ctx.context.adapter.findOne({ + model: 'user', + where: [ + { + field: 'accountNumber', + value: ctx.body.accountNumber, + }, + ], + })) as + | ({ + id: string; + createdAt: Date; + updatedAt: Date; + email: string; + emailVerified: boolean; + name: string; + image?: string | null; + } & Record) + | null; + + if (!user) { + throw new APIError('UNAUTHORIZED', { + message: 'Invalid account number', + }); + } + + const session = await ctx.context.internalAdapter.createSession(user.id); + if (!session) { + throw new APIError('INTERNAL_SERVER_ERROR', { + message: 'Failed to create session', + }); + } + + await setSessionCookie(ctx, { session, user }); + + return ctx.json({ + token: session.token, + user, + }); + }, + ), + }, + }) satisfies BetterAuthPlugin; const authConfig = { baseURL: env.ORIGIN, secret: env.BETTER_AUTH_SECRET, - emailAndPassword: { enabled: true }, + emailAndPassword: { enabled: false }, + user: { + additionalFields: { + accountNumber: { + type: 'string', + required: true, + input: false, + unique: true, + fieldName: 'account_number', + defaultValue: generateAccountNumber, + }, + }, + }, logger: { level: 'debug', }, @@ -19,6 +96,15 @@ const authConfig = { }, }, plugins: [ + anonymous({ + generateName: () => 'Chillhop listener', + emailDomainName: 'accounts.chillhop.local', + }), + accountNumber(), + passkey({ + rpID: new URL(env.ORIGIN).hostname, + rpName: 'Chillhop', + }), sveltekitCookies(getRequestEvent), // make sure this is the last plugin in the array ], } satisfies Omit[0], 'database'>; diff --git a/src/lib/server/db/auth.schema.ts b/src/lib/server/db/auth.schema.ts index 66fc633..363810d 100644 --- a/src/lib/server/db/auth.schema.ts +++ b/src/lib/server/db/auth.schema.ts @@ -16,6 +16,8 @@ export const user = sqliteTable("user", { .default(sql`(cast(unixepoch('subsecond') * 1000 as integer))`) .$onUpdate(() => /* @__PURE__ */ new Date()) .notNull(), + isAnonymous: integer("is_anonymous", { mode: "boolean" }).default(false), + account_number: text("account_number").notNull().unique(), }); export const session = sqliteTable( @@ -87,9 +89,33 @@ export const verification = sqliteTable( (table) => [index("verification_identifier_idx").on(table.identifier)], ); +export const passkey = sqliteTable( + "passkey", + { + id: text("id").primaryKey(), + name: text("name"), + publicKey: text("public_key").notNull(), + userId: text("user_id") + .notNull() + .references(() => user.id, { onDelete: "cascade" }), + credentialID: text("credential_id").notNull(), + counter: integer("counter").notNull(), + deviceType: text("device_type").notNull(), + backedUp: integer("backed_up", { mode: "boolean" }).notNull(), + transports: text("transports"), + createdAt: integer("created_at", { mode: "timestamp_ms" }), + aaguid: text("aaguid"), + }, + (table) => [ + index("passkey_userId_idx").on(table.userId), + index("passkey_credentialID_idx").on(table.credentialID), + ], +); + export const userRelations = relations(user, ({ many }) => ({ sessions: many(session), accounts: many(account), + passkeys: many(passkey), })); export const sessionRelations = relations(session, ({ one }) => ({ @@ -105,3 +131,10 @@ export const accountRelations = relations(account, ({ one }) => ({ references: [user.id], }), })); + +export const passkeyRelations = relations(passkey, ({ one }) => ({ + user: one(user, { + fields: [passkey.userId], + references: [user.id], + }), +})); diff --git a/src/routes/demo/better-auth/+page.svelte b/src/routes/demo/better-auth/+page.svelte index d7f03d1..b1e0cb7 100644 --- a/src/routes/demo/better-auth/+page.svelte +++ b/src/routes/demo/better-auth/+page.svelte @@ -1,12 +1,35 @@

Hi, {data.user.name}!

Your user ID is {data.user.id}.

+

Your account number is {data.user.accountNumber}.

+ +

{passkeyMessage}

diff --git a/src/routes/demo/better-auth/login/+page.server.ts b/src/routes/demo/better-auth/login/+page.server.ts index 887febb..4669822 100644 --- a/src/routes/demo/better-auth/login/+page.server.ts +++ b/src/routes/demo/better-auth/login/+page.server.ts @@ -21,51 +21,36 @@ export const load: PageServerLoad = (event) => { }; export const actions: Actions = { - signInEmail: async (event) => { + signInAccountNumber: async (event) => { const { auth } = event.locals; const formData = await event.request.formData(); - const email = formData.get('email')?.toString() ?? ''; - const password = formData.get('password')?.toString() ?? ''; + const accountNumber = formData.get('accountNumber')?.toString().replace(/\D/g, '') ?? ''; try { - await auth.api.signInEmail({ + await auth.api.signInAccountNumber({ body: { - email, - password, - callbackURL: '/auth/verification-success', + accountNumber, }, }); } catch (error) { - console.error('Demo Better Auth sign-in failed', { email, error }); + console.error('Demo Better Auth account number sign-in failed', { accountNumber, error }); return fail(error instanceof APIError ? 400 : 500, { - message: getErrorMessage(error, 'Signin failed'), + message: getErrorMessage(error, 'Account number sign-in failed'), }); } return redirect(302, '/demo/better-auth'); }, - signUpEmail: async (event) => { + createAccount: async (event) => { const { auth } = event.locals; - const formData = await event.request.formData(); - const email = formData.get('email')?.toString() ?? ''; - const password = formData.get('password')?.toString() ?? ''; - const name = formData.get('name')?.toString() ?? ''; - try { - await auth.api.signUpEmail({ - body: { - email, - password, - name, - callbackURL: '/auth/verification-success', - }, - }); + await auth.api.signInAnonymous(); } catch (error) { - console.error('Demo Better Auth sign-up failed', { email, error }); + console.error('Demo Better Auth account creation failed', { error }); return fail(error instanceof APIError ? 400 : 500, { - message: getErrorMessage(error, 'Registration failed'), + message: getErrorMessage(error, 'Account creation failed'), }); } diff --git a/src/routes/demo/better-auth/login/+page.svelte b/src/routes/demo/better-auth/login/+page.svelte index 9b6e949..1bb9d27 100644 --- a/src/routes/demo/better-auth/login/+page.svelte +++ b/src/routes/demo/better-auth/login/+page.svelte @@ -1,25 +1,43 @@ -

Login

-
+

Account Login

+ - - - - + +

{form?.message ?? ''}

+ +

{passkeyError}