mirror of
https://github.com/SrIzan10/lofi.git
synced 2026-06-06 00:56:53 +00:00
feat: very bad account number + passkey login
This commit is contained in:
6
bun.lock
6
bun.lock
@@ -4,11 +4,9 @@
|
|||||||
"workspaces": {
|
"workspaces": {
|
||||||
"": {
|
"": {
|
||||||
"name": "chillhop",
|
"name": "chillhop",
|
||||||
"dependencies": {
|
|
||||||
"@better-auth/passkey": "^1.5.6",
|
|
||||||
},
|
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@better-auth/cli": "~1.4.21",
|
"@better-auth/cli": "~1.4.21",
|
||||||
|
"@better-auth/passkey": "~1.4.21",
|
||||||
"@cloudflare/workers-types": "^4.20250517.0",
|
"@cloudflare/workers-types": "^4.20250517.0",
|
||||||
"@lucide/svelte": "^0.492.0",
|
"@lucide/svelte": "^0.492.0",
|
||||||
"@sveltejs/adapter-cloudflare": "^7.2.6",
|
"@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/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=="],
|
"@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=="],
|
||||||
|
|
||||||
|
|||||||
24
drizzle.config.local.ts
Normal file
24
drizzle.config.local.ts
Normal file
@@ -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,
|
||||||
|
});
|
||||||
28
drizzle/0002_account_number_auth.sql
Normal file
28
drizzle/0002_account_number_auth.sql
Normal file
@@ -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`);
|
||||||
18
drizzle/0003_add_passkey_table.sql
Normal file
18
drizzle/0003_add_passkey_table.sql
Normal file
@@ -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`);
|
||||||
540
drizzle/meta/0002_snapshot.json
Normal file
540
drizzle/meta/0002_snapshot.json
Normal file
@@ -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": {}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -15,6 +15,13 @@
|
|||||||
"when": 1775242876895,
|
"when": 1775242876895,
|
||||||
"tag": "0001_curved_sunspot",
|
"tag": "0001_curved_sunspot",
|
||||||
"breakpoints": true
|
"breakpoints": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idx": 2,
|
||||||
|
"version": "6",
|
||||||
|
"when": 1775246437951,
|
||||||
|
"tag": "0002_account_number_auth",
|
||||||
|
"breakpoints": true
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
@@ -20,6 +20,7 @@
|
|||||||
"db:migrate:prod": "drizzle-kit migrate",
|
"db:migrate:prod": "drizzle-kit migrate",
|
||||||
"db:migrate:dev": "bun wrangler d1 migrations apply lofi_db --local",
|
"db:migrate:dev": "bun wrangler d1 migrations apply lofi_db --local",
|
||||||
"db:studio": "drizzle-kit studio",
|
"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"
|
"auth:schema": "better-auth generate --config src/lib/server/auth.ts --output src/lib/server/db/auth.schema.ts --yes"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
@@ -48,10 +49,8 @@
|
|||||||
"wrangler": "^4.63.0",
|
"wrangler": "^4.63.0",
|
||||||
"better-auth": "~1.4.21",
|
"better-auth": "~1.4.21",
|
||||||
"drizzle-orm": "^0.45.2",
|
"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",
|
"packageManager": "bun@1.3.5"
|
||||||
"dependencies": {
|
|
||||||
"@better-auth/passkey": "^1.5.6"
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
5
src/app.d.ts
vendored
5
src/app.d.ts
vendored
@@ -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';
|
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
|
// See https://svelte.dev/docs/kit/types#app.d.ts
|
||||||
// for information about these interfaces
|
// for information about these interfaces
|
||||||
declare global {
|
declare global {
|
||||||
|
|||||||
7
src/lib/auth-client.ts
Normal file
7
src/lib/auth-client.ts
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
import { createAuthClient } from 'better-auth/svelte';
|
||||||
|
import { passkeyClient } from '@better-auth/passkey/client';
|
||||||
|
|
||||||
|
export const authClient = createAuthClient({
|
||||||
|
plugins: [passkeyClient()],
|
||||||
|
});
|
||||||
|
|
||||||
@@ -1 +1 @@
|
|||||||
// place files you want to import through the `$lib` alias in this folder.
|
export { authClient } from './auth-client';
|
||||||
|
|||||||
@@ -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 { 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 { sveltekitCookies } from 'better-auth/svelte-kit';
|
||||||
import type { D1Database } from '@cloudflare/workers-types';
|
import type { D1Database } from '@cloudflare/workers-types';
|
||||||
import { env } from '$env/dynamic/private';
|
import { env } from '$env/dynamic/private';
|
||||||
import { getRequestEvent } from '$app/server';
|
import { getRequestEvent } from '$app/server';
|
||||||
import { getDb } from '$lib/server/db';
|
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<string, any>)
|
||||||
|
| 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 = {
|
const authConfig = {
|
||||||
baseURL: env.ORIGIN,
|
baseURL: env.ORIGIN,
|
||||||
secret: env.BETTER_AUTH_SECRET,
|
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: {
|
logger: {
|
||||||
level: 'debug',
|
level: 'debug',
|
||||||
},
|
},
|
||||||
@@ -19,6 +96,15 @@ const authConfig = {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
plugins: [
|
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
|
sveltekitCookies(getRequestEvent), // make sure this is the last plugin in the array
|
||||||
],
|
],
|
||||||
} satisfies Omit<Parameters<typeof betterAuth>[0], 'database'>;
|
} satisfies Omit<Parameters<typeof betterAuth>[0], 'database'>;
|
||||||
|
|||||||
@@ -16,6 +16,8 @@ export const user = sqliteTable("user", {
|
|||||||
.default(sql`(cast(unixepoch('subsecond') * 1000 as integer))`)
|
.default(sql`(cast(unixepoch('subsecond') * 1000 as integer))`)
|
||||||
.$onUpdate(() => /* @__PURE__ */ new Date())
|
.$onUpdate(() => /* @__PURE__ */ new Date())
|
||||||
.notNull(),
|
.notNull(),
|
||||||
|
isAnonymous: integer("is_anonymous", { mode: "boolean" }).default(false),
|
||||||
|
account_number: text("account_number").notNull().unique(),
|
||||||
});
|
});
|
||||||
|
|
||||||
export const session = sqliteTable(
|
export const session = sqliteTable(
|
||||||
@@ -87,9 +89,33 @@ export const verification = sqliteTable(
|
|||||||
(table) => [index("verification_identifier_idx").on(table.identifier)],
|
(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 }) => ({
|
export const userRelations = relations(user, ({ many }) => ({
|
||||||
sessions: many(session),
|
sessions: many(session),
|
||||||
accounts: many(account),
|
accounts: many(account),
|
||||||
|
passkeys: many(passkey),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
export const sessionRelations = relations(session, ({ one }) => ({
|
export const sessionRelations = relations(session, ({ one }) => ({
|
||||||
@@ -105,3 +131,10 @@ export const accountRelations = relations(account, ({ one }) => ({
|
|||||||
references: [user.id],
|
references: [user.id],
|
||||||
}),
|
}),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
export const passkeyRelations = relations(passkey, ({ one }) => ({
|
||||||
|
user: one(user, {
|
||||||
|
fields: [passkey.userId],
|
||||||
|
references: [user.id],
|
||||||
|
}),
|
||||||
|
}));
|
||||||
|
|||||||
@@ -1,12 +1,35 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
|
import { authClient } from '$lib';
|
||||||
import { enhance } from '$app/forms';
|
import { enhance } from '$app/forms';
|
||||||
import type { PageServerData } from './$types';
|
import type { PageServerData } from './$types';
|
||||||
|
|
||||||
let { data }: { data: PageServerData } = $props();
|
let { data }: { data: PageServerData } = $props();
|
||||||
|
let passkeyMessage = $state('');
|
||||||
|
let addingPasskey = $state(false);
|
||||||
|
|
||||||
|
const addPasskey = async () => {
|
||||||
|
addingPasskey = true;
|
||||||
|
passkeyMessage = '';
|
||||||
|
|
||||||
|
const result = await authClient.passkey.addPasskey({
|
||||||
|
name: 'Primary passkey',
|
||||||
|
authenticatorAttachment: 'platform',
|
||||||
|
});
|
||||||
|
|
||||||
|
addingPasskey = false;
|
||||||
|
passkeyMessage = result.error
|
||||||
|
? result.error.message || 'Failed to add passkey'
|
||||||
|
: 'Passkey added to your account.';
|
||||||
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<h1>Hi, {data.user.name}!</h1>
|
<h1>Hi, {data.user.name}!</h1>
|
||||||
<p>Your user ID is {data.user.id}.</p>
|
<p>Your user ID is {data.user.id}.</p>
|
||||||
|
<p>Your account number is {data.user.accountNumber}.</p>
|
||||||
|
<button onclick={addPasskey} disabled={addingPasskey}>
|
||||||
|
{addingPasskey ? 'Waiting for passkey...' : 'Add a passkey'}
|
||||||
|
</button>
|
||||||
|
<p>{passkeyMessage}</p>
|
||||||
<form method="post" action="?/signOut" use:enhance>
|
<form method="post" action="?/signOut" use:enhance>
|
||||||
<button>Sign out</button>
|
<button>Sign out</button>
|
||||||
</form>
|
</form>
|
||||||
|
|||||||
@@ -21,51 +21,36 @@ export const load: PageServerLoad = (event) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export const actions: Actions = {
|
export const actions: Actions = {
|
||||||
signInEmail: async (event) => {
|
signInAccountNumber: async (event) => {
|
||||||
const { auth } = event.locals;
|
const { auth } = event.locals;
|
||||||
|
|
||||||
const formData = await event.request.formData();
|
const formData = await event.request.formData();
|
||||||
const email = formData.get('email')?.toString() ?? '';
|
const accountNumber = formData.get('accountNumber')?.toString().replace(/\D/g, '') ?? '';
|
||||||
const password = formData.get('password')?.toString() ?? '';
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await auth.api.signInEmail({
|
await auth.api.signInAccountNumber({
|
||||||
body: {
|
body: {
|
||||||
email,
|
accountNumber,
|
||||||
password,
|
|
||||||
callbackURL: '/auth/verification-success',
|
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} 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, {
|
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');
|
return redirect(302, '/demo/better-auth');
|
||||||
},
|
},
|
||||||
signUpEmail: async (event) => {
|
createAccount: async (event) => {
|
||||||
const { auth } = event.locals;
|
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 {
|
try {
|
||||||
await auth.api.signUpEmail({
|
await auth.api.signInAnonymous();
|
||||||
body: {
|
|
||||||
email,
|
|
||||||
password,
|
|
||||||
name,
|
|
||||||
callbackURL: '/auth/verification-success',
|
|
||||||
},
|
|
||||||
});
|
|
||||||
} catch (error) {
|
} 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, {
|
return fail(error instanceof APIError ? 400 : 500, {
|
||||||
message: getErrorMessage(error, 'Registration failed'),
|
message: getErrorMessage(error, 'Account creation failed'),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,25 +1,43 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
|
import { goto } from '$app/navigation';
|
||||||
import { enhance } from '$app/forms';
|
import { enhance } from '$app/forms';
|
||||||
|
import { authClient } from '$lib';
|
||||||
import type { ActionData } from './$types';
|
import type { ActionData } from './$types';
|
||||||
|
|
||||||
let { form }: { form: ActionData } = $props();
|
let { form }: { form: ActionData } = $props();
|
||||||
|
let passkeyError = $state('');
|
||||||
|
let signingInWithPasskey = $state(false);
|
||||||
|
|
||||||
|
const signInWithPasskey = async () => {
|
||||||
|
signingInWithPasskey = true;
|
||||||
|
passkeyError = '';
|
||||||
|
|
||||||
|
const result = await authClient.signIn.passkey({
|
||||||
|
autoFill: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
signingInWithPasskey = false;
|
||||||
|
|
||||||
|
if (result.error) {
|
||||||
|
passkeyError = result.error.message || 'Passkey sign-in failed';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
await goto('/demo/better-auth');
|
||||||
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<h1>Login</h1>
|
<h1>Account Login</h1>
|
||||||
<form method="post" action="?/signInEmail" use:enhance>
|
<form method="post" action="?/signInAccountNumber" use:enhance>
|
||||||
<label>
|
<label>
|
||||||
Email
|
Account number
|
||||||
<input type="email" name="email" />
|
<input name="accountNumber" inputmode="numeric" maxlength="16" autocomplete="webauthn" />
|
||||||
</label>
|
</label>
|
||||||
<label>
|
<button>Sign in with account number</button>
|
||||||
Password
|
<button formaction="?/createAccount">Create account number</button>
|
||||||
<input type="password" name="password" />
|
|
||||||
</label>
|
|
||||||
<label>
|
|
||||||
Name (for registration)
|
|
||||||
<input name="name" />
|
|
||||||
</label>
|
|
||||||
<button>Login</button>
|
|
||||||
<button formaction="?/signUpEmail">Register</button>
|
|
||||||
</form>
|
</form>
|
||||||
<p style="color: red">{form?.message ?? ''}</p>
|
<p style="color: red">{form?.message ?? ''}</p>
|
||||||
|
<button onclick={signInWithPasskey} disabled={signingInWithPasskey}>
|
||||||
|
{signingInWithPasskey ? 'Waiting for passkey...' : 'Sign in with passkey'}
|
||||||
|
</button>
|
||||||
|
<p style="color: red">{passkeyError}</p>
|
||||||
|
|||||||
Reference in New Issue
Block a user