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": {
|
||||
"": {
|
||||
"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=="],
|
||||
|
||||
|
||||
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,
|
||||
"tag": "0001_curved_sunspot",
|
||||
"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: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"
|
||||
}
|
||||
|
||||
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';
|
||||
|
||||
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 {
|
||||
|
||||
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 { 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<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 = {
|
||||
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<Parameters<typeof betterAuth>[0], 'database'>;
|
||||
|
||||
@@ -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],
|
||||
}),
|
||||
}));
|
||||
|
||||
@@ -1,12 +1,35 @@
|
||||
<script lang="ts">
|
||||
import { authClient } from '$lib';
|
||||
import { enhance } from '$app/forms';
|
||||
import type { PageServerData } from './$types';
|
||||
|
||||
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>
|
||||
|
||||
<h1>Hi, {data.user.name}!</h1>
|
||||
<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>
|
||||
<button>Sign out</button>
|
||||
</form>
|
||||
|
||||
@@ -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'),
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -1,25 +1,43 @@
|
||||
<script lang="ts">
|
||||
import { goto } from '$app/navigation';
|
||||
import { enhance } from '$app/forms';
|
||||
import { authClient } from '$lib';
|
||||
import type { ActionData } from './$types';
|
||||
|
||||
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>
|
||||
|
||||
<h1>Login</h1>
|
||||
<form method="post" action="?/signInEmail" use:enhance>
|
||||
<h1>Account Login</h1>
|
||||
<form method="post" action="?/signInAccountNumber" use:enhance>
|
||||
<label>
|
||||
Email
|
||||
<input type="email" name="email" />
|
||||
Account number
|
||||
<input name="accountNumber" inputmode="numeric" maxlength="16" autocomplete="webauthn" />
|
||||
</label>
|
||||
<label>
|
||||
Password
|
||||
<input type="password" name="password" />
|
||||
</label>
|
||||
<label>
|
||||
Name (for registration)
|
||||
<input name="name" />
|
||||
</label>
|
||||
<button>Login</button>
|
||||
<button formaction="?/signUpEmail">Register</button>
|
||||
<button>Sign in with account number</button>
|
||||
<button formaction="?/createAccount">Create account number</button>
|
||||
</form>
|
||||
<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