mirror of
https://github.com/SrIzan10/lofi.git
synced 2026-06-06 00:56:53 +00:00
15
.env.example
Normal file
15
.env.example
Normal file
@@ -0,0 +1,15 @@
|
||||
# Cloudflare D1
|
||||
CLOUDFLARE_ACCOUNT_ID=""
|
||||
CLOUDFLARE_DATABASE_ID=""
|
||||
CLOUDFLARE_D1_TOKEN=""
|
||||
|
||||
ORIGIN=""
|
||||
|
||||
# Better Auth
|
||||
# For production use 32 characters and generated with high entropy
|
||||
# https://www.better-auth.com/docs/installation
|
||||
BETTER_AUTH_SECRET=""
|
||||
|
||||
# Cloudflare Turnstile
|
||||
PUBLIC_TURNSTILE_SITE_KEY=""
|
||||
TURNSTILE_SECRET_KEY=""
|
||||
9
.gitignore
vendored
9
.gitignore
vendored
@@ -21,4 +21,11 @@ Thumbs.db
|
||||
# Vite
|
||||
vite.config.js.timestamp-*
|
||||
vite.config.ts.timestamp-*
|
||||
.cloudflare
|
||||
.cloudflare
|
||||
# Cloudflare Types
|
||||
/worker-configuration.d.ts
|
||||
|
||||
.dev.vars*
|
||||
!.dev.vars.example
|
||||
|
||||
.codex
|
||||
1
.prettierignore
Normal file
1
.prettierignore
Normal file
@@ -0,0 +1 @@
|
||||
/drizzle/
|
||||
@@ -1,8 +1,6 @@
|
||||
{
|
||||
"$schema": "https://next.shadcn-svelte.com/schema.json",
|
||||
"style": "new-york",
|
||||
"tailwind": {
|
||||
"config": "tailwind.config.ts",
|
||||
"css": "src/app.css",
|
||||
"baseColor": "neutral"
|
||||
},
|
||||
@@ -10,8 +8,9 @@
|
||||
"components": "$lib/components",
|
||||
"utils": "$lib/utils",
|
||||
"ui": "$lib/components/ui",
|
||||
"hooks": "$lib/hooks"
|
||||
"hooks": "$lib/hooks",
|
||||
"lib": "$lib"
|
||||
},
|
||||
"typescript": true,
|
||||
"registry": "https://next.shadcn-svelte.com/registry"
|
||||
"registry": "https://tw3.shadcn-svelte.com/registry/new-york"
|
||||
}
|
||||
|
||||
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,
|
||||
});
|
||||
18
drizzle.config.ts
Normal file
18
drizzle.config.ts
Normal file
@@ -0,0 +1,18 @@
|
||||
import { defineConfig } from 'drizzle-kit';
|
||||
|
||||
if (!process.env.CLOUDFLARE_ACCOUNT_ID) throw new Error('CLOUDFLARE_ACCOUNT_ID is not set');
|
||||
if (!process.env.CLOUDFLARE_DATABASE_ID) throw new Error('CLOUDFLARE_DATABASE_ID is not set');
|
||||
if (!process.env.CLOUDFLARE_D1_TOKEN) throw new Error('CLOUDFLARE_D1_TOKEN is not set');
|
||||
|
||||
export default defineConfig({
|
||||
schema: './src/lib/server/db/schema.ts',
|
||||
dialect: 'sqlite',
|
||||
driver: 'd1-http',
|
||||
dbCredentials: {
|
||||
accountId: process.env.CLOUDFLARE_ACCOUNT_ID,
|
||||
databaseId: process.env.CLOUDFLARE_DATABASE_ID,
|
||||
token: process.env.CLOUDFLARE_D1_TOKEN,
|
||||
},
|
||||
verbose: true,
|
||||
strict: true,
|
||||
});
|
||||
5
drizzle/0000_wooden_magdalene.sql
Normal file
5
drizzle/0000_wooden_magdalene.sql
Normal file
@@ -0,0 +1,5 @@
|
||||
CREATE TABLE `task` (
|
||||
`id` text PRIMARY KEY NOT NULL,
|
||||
`title` text NOT NULL,
|
||||
`priority` integer DEFAULT 1 NOT NULL
|
||||
);
|
||||
53
drizzle/0001_curved_sunspot.sql
Normal file
53
drizzle/0001_curved_sunspot.sql
Normal file
@@ -0,0 +1,53 @@
|
||||
CREATE TABLE `account` (
|
||||
`id` text PRIMARY KEY NOT NULL,
|
||||
`account_id` text NOT NULL,
|
||||
`provider_id` text NOT NULL,
|
||||
`user_id` text NOT NULL,
|
||||
`access_token` text,
|
||||
`refresh_token` text,
|
||||
`id_token` text,
|
||||
`access_token_expires_at` integer,
|
||||
`refresh_token_expires_at` integer,
|
||||
`scope` text,
|
||||
`password` text,
|
||||
`created_at` integer DEFAULT (cast(unixepoch('subsecond') * 1000 as integer)) NOT NULL,
|
||||
`updated_at` integer NOT NULL,
|
||||
FOREIGN KEY (`user_id`) REFERENCES `user`(`id`) ON UPDATE no action ON DELETE cascade
|
||||
);
|
||||
--> statement-breakpoint
|
||||
CREATE INDEX `account_userId_idx` ON `account` (`user_id`);--> statement-breakpoint
|
||||
CREATE TABLE `session` (
|
||||
`id` text PRIMARY KEY NOT NULL,
|
||||
`expires_at` integer NOT NULL,
|
||||
`token` text NOT NULL,
|
||||
`created_at` integer DEFAULT (cast(unixepoch('subsecond') * 1000 as integer)) NOT NULL,
|
||||
`updated_at` integer NOT NULL,
|
||||
`ip_address` text,
|
||||
`user_agent` text,
|
||||
`user_id` text NOT NULL,
|
||||
FOREIGN KEY (`user_id`) REFERENCES `user`(`id`) ON UPDATE no action ON DELETE cascade
|
||||
);
|
||||
--> statement-breakpoint
|
||||
CREATE UNIQUE INDEX `session_token_unique` ON `session` (`token`);--> statement-breakpoint
|
||||
CREATE INDEX `session_userId_idx` ON `session` (`user_id`);--> statement-breakpoint
|
||||
CREATE TABLE `user` (
|
||||
`id` text PRIMARY KEY NOT NULL,
|
||||
`name` text NOT NULL,
|
||||
`email` text NOT NULL,
|
||||
`email_verified` integer DEFAULT false NOT NULL,
|
||||
`image` text,
|
||||
`created_at` integer DEFAULT (cast(unixepoch('subsecond') * 1000 as integer)) NOT NULL,
|
||||
`updated_at` integer DEFAULT (cast(unixepoch('subsecond') * 1000 as integer)) NOT NULL
|
||||
);
|
||||
--> statement-breakpoint
|
||||
CREATE UNIQUE INDEX `user_email_unique` ON `user` (`email`);--> statement-breakpoint
|
||||
CREATE TABLE `verification` (
|
||||
`id` text PRIMARY KEY NOT NULL,
|
||||
`identifier` text NOT NULL,
|
||||
`value` text NOT NULL,
|
||||
`expires_at` integer NOT NULL,
|
||||
`created_at` integer DEFAULT (cast(unixepoch('subsecond') * 1000 as integer)) NOT NULL,
|
||||
`updated_at` integer DEFAULT (cast(unixepoch('subsecond') * 1000 as integer)) NOT NULL
|
||||
);
|
||||
--> statement-breakpoint
|
||||
CREATE INDEX `verification_identifier_idx` ON `verification` (`identifier`);
|
||||
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`);
|
||||
14
drizzle/0003_stiff_captain_universe.sql
Normal file
14
drizzle/0003_stiff_captain_universe.sql
Normal file
@@ -0,0 +1,14 @@
|
||||
CREATE TABLE `user_stat_bucket` (
|
||||
`id` text PRIMARY KEY NOT NULL,
|
||||
`user_id` text NOT NULL,
|
||||
`metric` text NOT NULL,
|
||||
`value` integer NOT NULL,
|
||||
`bucket_start` integer NOT NULL,
|
||||
`station_id` integer,
|
||||
`file_id` text,
|
||||
`created_at` integer DEFAULT (cast(unixepoch('subsecond') * 1000 as integer)) NOT NULL,
|
||||
FOREIGN KEY (`user_id`) REFERENCES `user`(`id`) ON UPDATE no action ON DELETE cascade
|
||||
);
|
||||
--> statement-breakpoint
|
||||
CREATE INDEX `user_stat_bucket_user_metric_bucket_idx` ON `user_stat_bucket` (`user_id`,`metric`,`bucket_start`);--> statement-breakpoint
|
||||
DROP TABLE `task`;
|
||||
9
drizzle/0004_abandoned_captain_stacy.sql
Normal file
9
drizzle/0004_abandoned_captain_stacy.sql
Normal file
@@ -0,0 +1,9 @@
|
||||
CREATE TABLE `song_ids` (
|
||||
`id` text PRIMARY KEY NOT NULL,
|
||||
`file_id` text NOT NULL,
|
||||
`spotify_id` text,
|
||||
`title` text NOT NULL,
|
||||
`artists` text NOT NULL,
|
||||
`image` text NOT NULL,
|
||||
`label` text NOT NULL
|
||||
);
|
||||
33
drizzle/0005_chubby_master_mold.sql
Normal file
33
drizzle/0005_chubby_master_mold.sql
Normal file
@@ -0,0 +1,33 @@
|
||||
PRAGMA foreign_keys=OFF;--> statement-breakpoint
|
||||
CREATE TABLE `__new_song_ids` (
|
||||
`id` text PRIMARY KEY NOT NULL,
|
||||
`file_id` text NOT NULL,
|
||||
`spotify_id` text,
|
||||
`title` text NOT NULL,
|
||||
`artists` text NOT NULL,
|
||||
`image` text NOT NULL,
|
||||
`label` text
|
||||
);
|
||||
--> statement-breakpoint
|
||||
INSERT INTO `__new_song_ids`("id", "file_id", "spotify_id", "title", "artists", "image", "label") SELECT "id", "file_id", "spotify_id", "title", "artists", "image", "label" FROM `song_ids`;--> statement-breakpoint
|
||||
DROP TABLE `song_ids`;--> statement-breakpoint
|
||||
ALTER TABLE `__new_song_ids` RENAME TO `song_ids`;--> statement-breakpoint
|
||||
PRAGMA foreign_keys=ON;--> statement-breakpoint
|
||||
CREATE UNIQUE INDEX `song_ids_file_id_idx` ON `song_ids` (`file_id`);--> statement-breakpoint
|
||||
CREATE TABLE `__new_user_stat_bucket` (
|
||||
`id` text PRIMARY KEY NOT NULL,
|
||||
`user_id` text NOT NULL,
|
||||
`metric` text NOT NULL,
|
||||
`value` integer NOT NULL,
|
||||
`bucket_start` integer NOT NULL,
|
||||
`station_id` integer DEFAULT 0 NOT NULL,
|
||||
`file_id` text DEFAULT '' NOT NULL,
|
||||
`created_at` integer DEFAULT (cast(unixepoch('subsecond') * 1000 as integer)) NOT NULL,
|
||||
FOREIGN KEY (`user_id`) REFERENCES `user`(`id`) ON UPDATE no action ON DELETE cascade
|
||||
);
|
||||
--> statement-breakpoint
|
||||
INSERT INTO `__new_user_stat_bucket`("id", "user_id", "metric", "value", "bucket_start", "station_id", "file_id", "created_at") SELECT "id", "user_id", "metric", "value", "bucket_start", "station_id", "file_id", "created_at" FROM `user_stat_bucket`;--> statement-breakpoint
|
||||
DROP TABLE `user_stat_bucket`;--> statement-breakpoint
|
||||
ALTER TABLE `__new_user_stat_bucket` RENAME TO `user_stat_bucket`;--> statement-breakpoint
|
||||
CREATE INDEX `user_stat_bucket_user_metric_bucket_idx` ON `user_stat_bucket` (`user_id`,`metric`,`bucket_start`);--> statement-breakpoint
|
||||
CREATE UNIQUE INDEX `user_stat_bucket_unique_idx` ON `user_stat_bucket` (`user_id`,`metric`,`bucket_start`,`station_id`,`file_id`);
|
||||
1
drizzle/0006_known_avengers.sql
Normal file
1
drizzle/0006_known_avengers.sql
Normal file
@@ -0,0 +1 @@
|
||||
ALTER TABLE `user` ADD `statistics_opt_out` integer DEFAULT false NOT NULL;
|
||||
50
drizzle/meta/0000_snapshot.json
Normal file
50
drizzle/meta/0000_snapshot.json
Normal file
@@ -0,0 +1,50 @@
|
||||
{
|
||||
"version": "6",
|
||||
"dialect": "sqlite",
|
||||
"id": "03052b1a-aca5-4aeb-84b1-03783814ed6a",
|
||||
"prevId": "00000000-0000-0000-0000-000000000000",
|
||||
"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": {}
|
||||
}
|
||||
},
|
||||
"views": {},
|
||||
"enums": {},
|
||||
"_meta": {
|
||||
"schemas": {},
|
||||
"tables": {},
|
||||
"columns": {}
|
||||
},
|
||||
"internal": {
|
||||
"indexes": {}
|
||||
}
|
||||
}
|
||||
402
drizzle/meta/0001_snapshot.json
Normal file
402
drizzle/meta/0001_snapshot.json
Normal file
@@ -0,0 +1,402 @@
|
||||
{
|
||||
"version": "6",
|
||||
"dialect": "sqlite",
|
||||
"id": "7c532ee7-a504-49a0-91fa-c0206fa1f3b0",
|
||||
"prevId": "03052b1a-aca5-4aeb-84b1-03783814ed6a",
|
||||
"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": {}
|
||||
},
|
||||
"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
|
||||
},
|
||||
"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
|
||||
}
|
||||
},
|
||||
"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": {}
|
||||
}
|
||||
}
|
||||
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": {}
|
||||
}
|
||||
}
|
||||
599
drizzle/meta/0003_snapshot.json
Normal file
599
drizzle/meta/0003_snapshot.json
Normal file
@@ -0,0 +1,599 @@
|
||||
{
|
||||
"version": "6",
|
||||
"dialect": "sqlite",
|
||||
"id": "d97ddeeb-3d7a-46a2-9a50-5d90fc51516e",
|
||||
"prevId": "773dd59c-33d4-4b87-8541-dda2e9151482",
|
||||
"tables": {
|
||||
"user_stat_bucket": {
|
||||
"name": "user_stat_bucket",
|
||||
"columns": {
|
||||
"id": {
|
||||
"name": "id",
|
||||
"type": "text",
|
||||
"primaryKey": true,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"user_id": {
|
||||
"name": "user_id",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"metric": {
|
||||
"name": "metric",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"value": {
|
||||
"name": "value",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"bucket_start": {
|
||||
"name": "bucket_start",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"station_id": {
|
||||
"name": "station_id",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"file_id": {
|
||||
"name": "file_id",
|
||||
"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))"
|
||||
}
|
||||
},
|
||||
"indexes": {
|
||||
"user_stat_bucket_user_metric_bucket_idx": {
|
||||
"name": "user_stat_bucket_user_metric_bucket_idx",
|
||||
"columns": [
|
||||
"user_id",
|
||||
"metric",
|
||||
"bucket_start"
|
||||
],
|
||||
"isUnique": false
|
||||
}
|
||||
},
|
||||
"foreignKeys": {
|
||||
"user_stat_bucket_user_id_user_id_fk": {
|
||||
"name": "user_stat_bucket_user_id_user_id_fk",
|
||||
"tableFrom": "user_stat_bucket",
|
||||
"tableTo": "user",
|
||||
"columnsFrom": [
|
||||
"user_id"
|
||||
],
|
||||
"columnsTo": [
|
||||
"id"
|
||||
],
|
||||
"onDelete": "cascade",
|
||||
"onUpdate": "no action"
|
||||
}
|
||||
},
|
||||
"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
|
||||
},
|
||||
"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))"
|
||||
},
|
||||
"is_anonymous": {
|
||||
"name": "is_anonymous",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false,
|
||||
"default": false
|
||||
},
|
||||
"account_number": {
|
||||
"name": "account_number",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
}
|
||||
},
|
||||
"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": {}
|
||||
}
|
||||
}
|
||||
658
drizzle/meta/0004_snapshot.json
Normal file
658
drizzle/meta/0004_snapshot.json
Normal file
@@ -0,0 +1,658 @@
|
||||
{
|
||||
"version": "6",
|
||||
"dialect": "sqlite",
|
||||
"id": "9eba4b9d-41cd-4908-9356-2bb7075467b9",
|
||||
"prevId": "d97ddeeb-3d7a-46a2-9a50-5d90fc51516e",
|
||||
"tables": {
|
||||
"song_ids": {
|
||||
"name": "song_ids",
|
||||
"columns": {
|
||||
"id": {
|
||||
"name": "id",
|
||||
"type": "text",
|
||||
"primaryKey": true,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"file_id": {
|
||||
"name": "file_id",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"spotify_id": {
|
||||
"name": "spotify_id",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"title": {
|
||||
"name": "title",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"artists": {
|
||||
"name": "artists",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"image": {
|
||||
"name": "image",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"label": {
|
||||
"name": "label",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
}
|
||||
},
|
||||
"indexes": {},
|
||||
"foreignKeys": {},
|
||||
"compositePrimaryKeys": {},
|
||||
"uniqueConstraints": {},
|
||||
"checkConstraints": {}
|
||||
},
|
||||
"user_stat_bucket": {
|
||||
"name": "user_stat_bucket",
|
||||
"columns": {
|
||||
"id": {
|
||||
"name": "id",
|
||||
"type": "text",
|
||||
"primaryKey": true,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"user_id": {
|
||||
"name": "user_id",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"metric": {
|
||||
"name": "metric",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"value": {
|
||||
"name": "value",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"bucket_start": {
|
||||
"name": "bucket_start",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"station_id": {
|
||||
"name": "station_id",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"file_id": {
|
||||
"name": "file_id",
|
||||
"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))"
|
||||
}
|
||||
},
|
||||
"indexes": {
|
||||
"user_stat_bucket_user_metric_bucket_idx": {
|
||||
"name": "user_stat_bucket_user_metric_bucket_idx",
|
||||
"columns": [
|
||||
"user_id",
|
||||
"metric",
|
||||
"bucket_start"
|
||||
],
|
||||
"isUnique": false
|
||||
}
|
||||
},
|
||||
"foreignKeys": {
|
||||
"user_stat_bucket_user_id_user_id_fk": {
|
||||
"name": "user_stat_bucket_user_id_user_id_fk",
|
||||
"tableFrom": "user_stat_bucket",
|
||||
"tableTo": "user",
|
||||
"columnsFrom": [
|
||||
"user_id"
|
||||
],
|
||||
"columnsTo": [
|
||||
"id"
|
||||
],
|
||||
"onDelete": "cascade",
|
||||
"onUpdate": "no action"
|
||||
}
|
||||
},
|
||||
"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
|
||||
},
|
||||
"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))"
|
||||
},
|
||||
"is_anonymous": {
|
||||
"name": "is_anonymous",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false,
|
||||
"default": false
|
||||
},
|
||||
"account_number": {
|
||||
"name": "account_number",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
}
|
||||
},
|
||||
"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": {}
|
||||
}
|
||||
}
|
||||
679
drizzle/meta/0005_snapshot.json
Normal file
679
drizzle/meta/0005_snapshot.json
Normal file
@@ -0,0 +1,679 @@
|
||||
{
|
||||
"version": "6",
|
||||
"dialect": "sqlite",
|
||||
"id": "9e75c298-5f62-44f0-9f1a-40481e7df553",
|
||||
"prevId": "9eba4b9d-41cd-4908-9356-2bb7075467b9",
|
||||
"tables": {
|
||||
"song_ids": {
|
||||
"name": "song_ids",
|
||||
"columns": {
|
||||
"id": {
|
||||
"name": "id",
|
||||
"type": "text",
|
||||
"primaryKey": true,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"file_id": {
|
||||
"name": "file_id",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"spotify_id": {
|
||||
"name": "spotify_id",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"title": {
|
||||
"name": "title",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"artists": {
|
||||
"name": "artists",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"image": {
|
||||
"name": "image",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"label": {
|
||||
"name": "label",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
}
|
||||
},
|
||||
"indexes": {
|
||||
"song_ids_file_id_idx": {
|
||||
"name": "song_ids_file_id_idx",
|
||||
"columns": [
|
||||
"file_id"
|
||||
],
|
||||
"isUnique": true
|
||||
}
|
||||
},
|
||||
"foreignKeys": {},
|
||||
"compositePrimaryKeys": {},
|
||||
"uniqueConstraints": {},
|
||||
"checkConstraints": {}
|
||||
},
|
||||
"user_stat_bucket": {
|
||||
"name": "user_stat_bucket",
|
||||
"columns": {
|
||||
"id": {
|
||||
"name": "id",
|
||||
"type": "text",
|
||||
"primaryKey": true,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"user_id": {
|
||||
"name": "user_id",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"metric": {
|
||||
"name": "metric",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"value": {
|
||||
"name": "value",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"bucket_start": {
|
||||
"name": "bucket_start",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"station_id": {
|
||||
"name": "station_id",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": 0
|
||||
},
|
||||
"file_id": {
|
||||
"name": "file_id",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": "''"
|
||||
},
|
||||
"created_at": {
|
||||
"name": "created_at",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": "(cast(unixepoch('subsecond') * 1000 as integer))"
|
||||
}
|
||||
},
|
||||
"indexes": {
|
||||
"user_stat_bucket_user_metric_bucket_idx": {
|
||||
"name": "user_stat_bucket_user_metric_bucket_idx",
|
||||
"columns": [
|
||||
"user_id",
|
||||
"metric",
|
||||
"bucket_start"
|
||||
],
|
||||
"isUnique": false
|
||||
},
|
||||
"user_stat_bucket_unique_idx": {
|
||||
"name": "user_stat_bucket_unique_idx",
|
||||
"columns": [
|
||||
"user_id",
|
||||
"metric",
|
||||
"bucket_start",
|
||||
"station_id",
|
||||
"file_id"
|
||||
],
|
||||
"isUnique": true
|
||||
}
|
||||
},
|
||||
"foreignKeys": {
|
||||
"user_stat_bucket_user_id_user_id_fk": {
|
||||
"name": "user_stat_bucket_user_id_user_id_fk",
|
||||
"tableFrom": "user_stat_bucket",
|
||||
"tableTo": "user",
|
||||
"columnsFrom": [
|
||||
"user_id"
|
||||
],
|
||||
"columnsTo": [
|
||||
"id"
|
||||
],
|
||||
"onDelete": "cascade",
|
||||
"onUpdate": "no action"
|
||||
}
|
||||
},
|
||||
"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
|
||||
},
|
||||
"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))"
|
||||
},
|
||||
"is_anonymous": {
|
||||
"name": "is_anonymous",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false,
|
||||
"default": false
|
||||
},
|
||||
"account_number": {
|
||||
"name": "account_number",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
}
|
||||
},
|
||||
"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": {}
|
||||
}
|
||||
}
|
||||
687
drizzle/meta/0006_snapshot.json
Normal file
687
drizzle/meta/0006_snapshot.json
Normal file
@@ -0,0 +1,687 @@
|
||||
{
|
||||
"version": "6",
|
||||
"dialect": "sqlite",
|
||||
"id": "ec11112d-e50d-4aff-9914-e3d270cc2f5a",
|
||||
"prevId": "9e75c298-5f62-44f0-9f1a-40481e7df553",
|
||||
"tables": {
|
||||
"song_ids": {
|
||||
"name": "song_ids",
|
||||
"columns": {
|
||||
"id": {
|
||||
"name": "id",
|
||||
"type": "text",
|
||||
"primaryKey": true,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"file_id": {
|
||||
"name": "file_id",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"spotify_id": {
|
||||
"name": "spotify_id",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"title": {
|
||||
"name": "title",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"artists": {
|
||||
"name": "artists",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"image": {
|
||||
"name": "image",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"label": {
|
||||
"name": "label",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
}
|
||||
},
|
||||
"indexes": {
|
||||
"song_ids_file_id_idx": {
|
||||
"name": "song_ids_file_id_idx",
|
||||
"columns": [
|
||||
"file_id"
|
||||
],
|
||||
"isUnique": true
|
||||
}
|
||||
},
|
||||
"foreignKeys": {},
|
||||
"compositePrimaryKeys": {},
|
||||
"uniqueConstraints": {},
|
||||
"checkConstraints": {}
|
||||
},
|
||||
"user_stat_bucket": {
|
||||
"name": "user_stat_bucket",
|
||||
"columns": {
|
||||
"id": {
|
||||
"name": "id",
|
||||
"type": "text",
|
||||
"primaryKey": true,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"user_id": {
|
||||
"name": "user_id",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"metric": {
|
||||
"name": "metric",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"value": {
|
||||
"name": "value",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"bucket_start": {
|
||||
"name": "bucket_start",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"station_id": {
|
||||
"name": "station_id",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": 0
|
||||
},
|
||||
"file_id": {
|
||||
"name": "file_id",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": "''"
|
||||
},
|
||||
"created_at": {
|
||||
"name": "created_at",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": "(cast(unixepoch('subsecond') * 1000 as integer))"
|
||||
}
|
||||
},
|
||||
"indexes": {
|
||||
"user_stat_bucket_user_metric_bucket_idx": {
|
||||
"name": "user_stat_bucket_user_metric_bucket_idx",
|
||||
"columns": [
|
||||
"user_id",
|
||||
"metric",
|
||||
"bucket_start"
|
||||
],
|
||||
"isUnique": false
|
||||
},
|
||||
"user_stat_bucket_unique_idx": {
|
||||
"name": "user_stat_bucket_unique_idx",
|
||||
"columns": [
|
||||
"user_id",
|
||||
"metric",
|
||||
"bucket_start",
|
||||
"station_id",
|
||||
"file_id"
|
||||
],
|
||||
"isUnique": true
|
||||
}
|
||||
},
|
||||
"foreignKeys": {
|
||||
"user_stat_bucket_user_id_user_id_fk": {
|
||||
"name": "user_stat_bucket_user_id_user_id_fk",
|
||||
"tableFrom": "user_stat_bucket",
|
||||
"tableTo": "user",
|
||||
"columnsFrom": [
|
||||
"user_id"
|
||||
],
|
||||
"columnsTo": [
|
||||
"id"
|
||||
],
|
||||
"onDelete": "cascade",
|
||||
"onUpdate": "no action"
|
||||
}
|
||||
},
|
||||
"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
|
||||
},
|
||||
"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))"
|
||||
},
|
||||
"is_anonymous": {
|
||||
"name": "is_anonymous",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false,
|
||||
"default": false
|
||||
},
|
||||
"account_number": {
|
||||
"name": "account_number",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"statistics_opt_out": {
|
||||
"name": "statistics_opt_out",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": false
|
||||
}
|
||||
},
|
||||
"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": {}
|
||||
}
|
||||
}
|
||||
55
drizzle/meta/_journal.json
Normal file
55
drizzle/meta/_journal.json
Normal file
@@ -0,0 +1,55 @@
|
||||
{
|
||||
"version": "7",
|
||||
"dialect": "sqlite",
|
||||
"entries": [
|
||||
{
|
||||
"idx": 0,
|
||||
"version": "6",
|
||||
"when": 1775242146068,
|
||||
"tag": "0000_wooden_magdalene",
|
||||
"breakpoints": true
|
||||
},
|
||||
{
|
||||
"idx": 1,
|
||||
"version": "6",
|
||||
"when": 1775242876895,
|
||||
"tag": "0001_curved_sunspot",
|
||||
"breakpoints": true
|
||||
},
|
||||
{
|
||||
"idx": 2,
|
||||
"version": "6",
|
||||
"when": 1775246437951,
|
||||
"tag": "0002_account_number_auth",
|
||||
"breakpoints": true
|
||||
},
|
||||
{
|
||||
"idx": 3,
|
||||
"version": "6",
|
||||
"when": 1775423880350,
|
||||
"tag": "0003_stiff_captain_universe",
|
||||
"breakpoints": true
|
||||
},
|
||||
{
|
||||
"idx": 4,
|
||||
"version": "6",
|
||||
"when": 1775425092281,
|
||||
"tag": "0004_abandoned_captain_stacy",
|
||||
"breakpoints": true
|
||||
},
|
||||
{
|
||||
"idx": 5,
|
||||
"version": "6",
|
||||
"when": 1777011840981,
|
||||
"tag": "0005_chubby_master_mold",
|
||||
"breakpoints": true
|
||||
},
|
||||
{
|
||||
"idx": 6,
|
||||
"version": "6",
|
||||
"when": 1777041984571,
|
||||
"tag": "0006_known_avengers",
|
||||
"breakpoints": true
|
||||
}
|
||||
]
|
||||
}
|
||||
34
package.json
34
package.json
@@ -6,38 +6,52 @@
|
||||
"scripts": {
|
||||
"dev": "vite dev",
|
||||
"build": "vite build",
|
||||
"preview": "vite preview",
|
||||
"preview": "bun run build && wrangler dev",
|
||||
"prepare": "svelte-kit sync || echo ''",
|
||||
"check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json",
|
||||
"check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch",
|
||||
"ui:add": "npx shadcn-svelte@next add",
|
||||
"format": "prettier --write ."
|
||||
"format": "prettier --write .",
|
||||
"gen": "wrangler types",
|
||||
"deploy": "bun run build && wrangler deploy",
|
||||
"cf-typegen": "wrangler types",
|
||||
"db:push": "drizzle-kit push",
|
||||
"db:generate": "drizzle-kit generate",
|
||||
"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": {
|
||||
"@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-auto": "^6.0.0",
|
||||
"@sveltejs/adapter-cloudflare": "^7.0.3",
|
||||
"@sveltejs/adapter-cloudflare": "^7.2.6",
|
||||
"@sveltejs/kit": "^2.16.0",
|
||||
"@sveltejs/vite-plugin-svelte": "^5.0.0",
|
||||
"@types/node": "^24",
|
||||
"autoprefixer": "^10.4.20",
|
||||
"bits-ui": "^1.3.19",
|
||||
"better-auth": "~1.4.21",
|
||||
"bits-ui": "^1.4.7",
|
||||
"clsx": "^2.1.1",
|
||||
"drizzle-kit": "^0.31.8",
|
||||
"drizzle-orm": "^0.45.2",
|
||||
"mode-watcher": "^1.0.5",
|
||||
"prettier": "^3.5.3",
|
||||
"prettier-plugin-svelte": "^3.3.3",
|
||||
"svelte": "^5.0.0",
|
||||
"svelte-check": "^4.0.0",
|
||||
"svelte-sonner": "^0.3.28",
|
||||
"svelte-turnstile": "^0.11.0",
|
||||
"tailwind-merge": "^3.2.0",
|
||||
"tailwind-variants": "^1.0.0",
|
||||
"tailwindcss": "^3.4.17",
|
||||
"tailwindcss-animate": "^1.0.7",
|
||||
"typescript": "^5.0.0",
|
||||
"vite": "^6.0.0",
|
||||
"wrangler": "^4.15.2"
|
||||
"wrangler": "^4.63.0"
|
||||
},
|
||||
"packageManager": "yarn@1.22.22+sha512.a6b2f7906b721bba3d67d4aff083df04dad64c399707841b7acf00f6b133b7ac24255f2652fa22ae3534329dc6180534e98d17432037ff6fd140556e2bb3137e",
|
||||
"dependencies": {
|
||||
"mode-watcher": "^1.0.5"
|
||||
}
|
||||
"packageManager": "bun@1.3.5"
|
||||
}
|
||||
|
||||
21
src/app.d.ts
vendored
21
src/app.d.ts
vendored
@@ -1,14 +1,29 @@
|
||||
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 {
|
||||
namespace App {
|
||||
// interface Error {}
|
||||
// interface Locals {}
|
||||
// interface PageData {}
|
||||
// interface PageState {}
|
||||
interface Platform {
|
||||
caches: CacheStorage & { default: Cache }
|
||||
}
|
||||
caches: CacheStorage & { default: Cache };
|
||||
env: Env;
|
||||
ctx: ExecutionContext;
|
||||
caches: CacheStorage;
|
||||
cf?: IncomingRequestCfProperties;
|
||||
}
|
||||
|
||||
interface Locals {
|
||||
user?: User;
|
||||
session?: Session;
|
||||
auth: ReturnType<typeof createAuth>;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
23
src/hooks.server.ts
Normal file
23
src/hooks.server.ts
Normal file
@@ -0,0 +1,23 @@
|
||||
import type { Handle } from '@sveltejs/kit';
|
||||
import { building } from '$app/environment';
|
||||
import { createAuth } from '$lib/server/auth';
|
||||
import { svelteKitHandler } from 'better-auth/svelte-kit';
|
||||
|
||||
const handleBetterAuth: Handle = async ({ event, resolve }) => {
|
||||
if (!event.platform?.env?.DB)
|
||||
throw new Error('D1 binding "DB" not found - are you running with wrangler?');
|
||||
|
||||
event.locals.auth = createAuth(event.platform.env.DB, event.url.origin);
|
||||
|
||||
const { auth } = event.locals;
|
||||
const session = await auth.api.getSession({ headers: event.request.headers });
|
||||
|
||||
if (session) {
|
||||
event.locals.session = session.session;
|
||||
event.locals.user = session.user;
|
||||
}
|
||||
|
||||
return svelteKitHandler({ event, resolve, auth, building });
|
||||
};
|
||||
|
||||
export const handle: Handle = handleBetterAuth;
|
||||
30
src/lib/auth-client.ts
Normal file
30
src/lib/auth-client.ts
Normal file
@@ -0,0 +1,30 @@
|
||||
import type { BetterAuthClientPlugin } from 'better-auth/client';
|
||||
import { anonymousClient, inferAdditionalFields } from 'better-auth/client/plugins';
|
||||
import { createAuthClient } from 'better-auth/svelte';
|
||||
import { passkeyClient } from '@better-auth/passkey/client';
|
||||
import type { auth } from '$lib/server/auth';
|
||||
|
||||
const accountNumberClient = {
|
||||
id: 'account-number',
|
||||
getActions: ($fetch) => ({
|
||||
createAccount: async (name?: string, turnstileToken?: string) =>
|
||||
$fetch('/create-account', {
|
||||
method: 'POST',
|
||||
body: {
|
||||
name,
|
||||
turnstileToken,
|
||||
},
|
||||
}),
|
||||
signInAccountNumber: async (accountNumber: string) =>
|
||||
$fetch('/sign-in/account-number', {
|
||||
method: 'POST',
|
||||
body: {
|
||||
accountNumber,
|
||||
},
|
||||
}),
|
||||
}),
|
||||
} satisfies BetterAuthClientPlugin;
|
||||
|
||||
export const authClient = createAuthClient({
|
||||
plugins: [inferAdditionalFields<typeof auth>(), anonymousClient(), passkeyClient(), accountNumberClient],
|
||||
});
|
||||
9
src/lib/components/app/auth-bar.svelte
Normal file
9
src/lib/components/app/auth-bar.svelte
Normal file
@@ -0,0 +1,9 @@
|
||||
<script lang="ts">
|
||||
import AuthDialog from './auth-dialog.svelte';
|
||||
</script>
|
||||
|
||||
<div
|
||||
class="flex absolute top-0 right-0 items-center p-4 bg-foreground/10 backdrop-blur-lg rounded-bl-xl shadow-lg space-x-2"
|
||||
>
|
||||
<AuthDialog />
|
||||
</div>
|
||||
524
src/lib/components/app/auth-dialog.svelte
Normal file
524
src/lib/components/app/auth-dialog.svelte
Normal file
@@ -0,0 +1,524 @@
|
||||
<script lang="ts">
|
||||
import * as Dialog from '$lib/components/ui/dialog/index.js';
|
||||
import { authClient } from '$lib';
|
||||
import LogIn from '@lucide/svelte/icons/log-in';
|
||||
import Settings from '@lucide/svelte/icons/settings-2';
|
||||
import { Button } from '../ui/button';
|
||||
import Label from '../ui/label/label.svelte';
|
||||
import Input from '../ui/input/input.svelte';
|
||||
import Key from '@lucide/svelte/icons/key';
|
||||
import Trash2 from '@lucide/svelte/icons/trash-2';
|
||||
import ArrowLeft from '@lucide/svelte/icons/arrow-left';
|
||||
import Fingerprint from '@lucide/svelte/icons/fingerprint';
|
||||
import Plus from '@lucide/svelte/icons/plus';
|
||||
import Loader2 from '@lucide/svelte/icons/loader-2';
|
||||
import type { Passkey } from '@better-auth/passkey';
|
||||
import { Turnstile } from 'svelte-turnstile';
|
||||
import { dev } from '$app/environment';
|
||||
import { env as publicEnv } from '$env/dynamic/public';
|
||||
import Switch from '../ui/switch/switch.svelte';
|
||||
|
||||
const session = authClient.useSession();
|
||||
|
||||
let open = $state(false);
|
||||
let accountNumber = $state('');
|
||||
let name = $state('');
|
||||
let authMessage = $state('');
|
||||
let busyAction = $state<string | null>(null);
|
||||
let passkeyMessage = $state('');
|
||||
const user = $derived($session.data?.user);
|
||||
let passkeys = $state<Passkey[]>([]);
|
||||
let loadedPasskeysForUserId = $state<string | null>(null);
|
||||
let authScreen = $state<'login' | 'create'>('login');
|
||||
let passkeyName = $state('');
|
||||
let statisticsMessage = $state('');
|
||||
let turnstileToken = $state('');
|
||||
let resetTurnstile = $state<(() => void) | undefined>();
|
||||
let createFormElement = $state<HTMLDivElement>();
|
||||
const turnstileSiteKey = $derived(
|
||||
dev ? '1x00000000000000000000AA' : (publicEnv.PUBLIC_TURNSTILE_SITE_KEY ?? '')
|
||||
);
|
||||
|
||||
const loadPasskeys = async () => {
|
||||
if (!user) {
|
||||
passkeys = [];
|
||||
loadedPasskeysForUserId = null;
|
||||
return;
|
||||
}
|
||||
|
||||
const result = await authClient.passkey.listUserPasskeys();
|
||||
passkeys = result.data ?? [];
|
||||
loadedPasskeysForUserId = user.id;
|
||||
};
|
||||
|
||||
$effect(() => {
|
||||
if (!user) {
|
||||
passkeys = [];
|
||||
loadedPasskeysForUserId = null;
|
||||
return;
|
||||
}
|
||||
|
||||
if (loadedPasskeysForUserId !== user.id) {
|
||||
loadPasskeys();
|
||||
}
|
||||
});
|
||||
|
||||
const runAuthAction = async (
|
||||
action: string,
|
||||
request: () => Promise<{ error?: { message?: string | null } | null }>,
|
||||
fallbackMessage: string,
|
||||
onSuccess?: () => void | Promise<void>
|
||||
) => {
|
||||
busyAction = action;
|
||||
authMessage = '';
|
||||
|
||||
const result = await request();
|
||||
|
||||
busyAction = null;
|
||||
|
||||
if (result.error) {
|
||||
authMessage = result.error.message || fallbackMessage;
|
||||
return;
|
||||
}
|
||||
|
||||
await onSuccess?.();
|
||||
open = false;
|
||||
};
|
||||
|
||||
const signInWithAccountNumber = () =>
|
||||
runAuthAction(
|
||||
'account-number',
|
||||
() => authClient.signInAccountNumber(accountNumber.replace(/\D/g, '')),
|
||||
'Account number sign-in failed',
|
||||
() => {
|
||||
accountNumber = '';
|
||||
}
|
||||
);
|
||||
|
||||
const clearTurnstile = () => {
|
||||
turnstileToken = '';
|
||||
resetTurnstile?.();
|
||||
};
|
||||
|
||||
const handleTurnstileCallback = (
|
||||
event: CustomEvent<{ token: string; preClearanceObtained: boolean }>
|
||||
) => {
|
||||
turnstileToken = event.detail.token;
|
||||
authMessage = '';
|
||||
};
|
||||
|
||||
const handleTurnstileError = () => {
|
||||
clearTurnstile();
|
||||
authMessage = 'Turnstile verification failed. Please try again.';
|
||||
};
|
||||
|
||||
const handleTurnstileExpired = () => {
|
||||
clearTurnstile();
|
||||
authMessage = 'Turnstile check expired. Please try again.';
|
||||
};
|
||||
|
||||
const getTurnstileToken = () => {
|
||||
const formToken = createFormElement
|
||||
?.querySelector<HTMLInputElement>('input[name="cf-turnstile-response"]')
|
||||
?.value
|
||||
.trim();
|
||||
|
||||
return turnstileToken || formToken || '';
|
||||
};
|
||||
|
||||
const createAccount = async () => {
|
||||
const token = getTurnstileToken();
|
||||
|
||||
if (!token) {
|
||||
authMessage = 'Please complete the Turnstile check before creating an account.';
|
||||
return;
|
||||
}
|
||||
|
||||
await runAuthAction(
|
||||
'create-account',
|
||||
() => authClient.createAccount(name, token),
|
||||
'Account creation failed',
|
||||
async () => {
|
||||
await session.get().refetch();
|
||||
name = '';
|
||||
authScreen = 'login';
|
||||
clearTurnstile();
|
||||
}
|
||||
);
|
||||
|
||||
if (busyAction !== 'create-account') {
|
||||
clearTurnstile();
|
||||
}
|
||||
};
|
||||
|
||||
const signInWithPasskey = () =>
|
||||
runAuthAction(
|
||||
'passkey-sign-in',
|
||||
() =>
|
||||
authClient.signIn.passkey({
|
||||
autoFill: true,
|
||||
}),
|
||||
'Passkey sign-in failed'
|
||||
);
|
||||
|
||||
const signOut = () => runAuthAction('sign-out', () => authClient.signOut(), 'Sign out failed');
|
||||
|
||||
const updateStatisticsOptOut = async (statisticsOptOut: boolean) => {
|
||||
busyAction = 'statistics-opt-out';
|
||||
statisticsMessage = '';
|
||||
|
||||
const response = await fetch('/api/account/statistics-opt-out', {
|
||||
method: 'PATCH',
|
||||
headers: { 'content-type': 'application/json' },
|
||||
body: JSON.stringify({ statisticsOptOut }),
|
||||
});
|
||||
|
||||
busyAction = null;
|
||||
if (!response.ok) {
|
||||
statisticsMessage = 'Failed to update statistics preference';
|
||||
return;
|
||||
}
|
||||
|
||||
await session.get().refetch();
|
||||
statisticsMessage = statisticsOptOut
|
||||
? 'Statistics collection is turned off.'
|
||||
: 'Statistics collection is turned on.';
|
||||
};
|
||||
|
||||
const addPasskey = async () => {
|
||||
if (!passkeyName) {
|
||||
passkeyMessage = 'Please enter a name for your passkey';
|
||||
return;
|
||||
}
|
||||
|
||||
busyAction = 'add-passkey';
|
||||
passkeyMessage = '';
|
||||
|
||||
const result = await authClient.passkey.addPasskey({
|
||||
name: passkeyName,
|
||||
authenticatorAttachment: 'platform',
|
||||
});
|
||||
|
||||
busyAction = null;
|
||||
if (result.error) {
|
||||
passkeyMessage = result.error.message || 'Failed to add passkey';
|
||||
return;
|
||||
}
|
||||
|
||||
await loadPasskeys();
|
||||
passkeyMessage = 'Passkey added successfully';
|
||||
passkeyName = '';
|
||||
};
|
||||
|
||||
const deletePasskey = async (id: string) => {
|
||||
busyAction = `delete-passkey-${id}`;
|
||||
passkeyMessage = '';
|
||||
|
||||
const result = await authClient.passkey.deletePasskey({ id });
|
||||
|
||||
busyAction = null;
|
||||
if (result.error) {
|
||||
passkeyMessage = result.error.message || 'Failed to delete passkey';
|
||||
return;
|
||||
}
|
||||
|
||||
await loadPasskeys();
|
||||
passkeyMessage = 'Passkey removed';
|
||||
};
|
||||
</script>
|
||||
|
||||
<Dialog.Root bind:open>
|
||||
<Dialog.Trigger>
|
||||
<Button class="group relative overflow-hidden gap-2 font-medium tracking-wide">
|
||||
<span class="relative z-10 flex items-center gap-2">
|
||||
{#if user}
|
||||
<Settings class="size-4 transition-transform duration-300 group-hover:rotate-45" />
|
||||
Account
|
||||
{:else}
|
||||
<LogIn class="size-4 transition-transform duration-300 group-hover:translate-x-0.5" />
|
||||
Sign in
|
||||
{/if}
|
||||
</span>
|
||||
</Button>
|
||||
</Dialog.Trigger>
|
||||
<Dialog.Content
|
||||
class="!p-0 !gap-0 overflow-hidden max-w-md w-[95vw]"
|
||||
style="--text-color: rgba(255, 255, 255, 0.92); --text-shadow: 0 1px 2px rgba(0, 0, 0, 0.3); --foreground: 0 0% 98%; --muted-foreground: 0 0% 72%;"
|
||||
>
|
||||
<Dialog.Header class="p-5 border-b border-white/[0.06]">
|
||||
<Dialog.Title class="!text-lg !font-semibold tracking-tight">
|
||||
{user ? 'Account Settings' : 'Welcome Back'}
|
||||
</Dialog.Title>
|
||||
<p class="text-sm text-white/50 mt-1">
|
||||
{user ? 'Manage your account and security' : 'Sign in to sync your preferences'}
|
||||
</p>
|
||||
</Dialog.Header>
|
||||
|
||||
<div class="p-6 pt-2 space-y-5">
|
||||
{#if user}
|
||||
<div
|
||||
class="flex items-center gap-4 p-4 rounded-xl bg-white/[0.04] border border-white/[0.08]"
|
||||
>
|
||||
<div
|
||||
class="flex-shrink-0 w-12 h-12 rounded-full bg-white/[0.1] flex items-center justify-center"
|
||||
>
|
||||
<span class="text-lg font-bold text-white">{user.name?.[0]?.toUpperCase() || '?'}</span>
|
||||
</div>
|
||||
<div class="min-w-0 flex-1">
|
||||
<p class="font-semibold text-white truncate">{user.name}</p>
|
||||
<p class="text-sm text-white/50 font-mono tracking-tight">#{user.accountNumber}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="space-y-3 p-4 rounded-xl bg-white/[0.03] border border-white/[0.06]">
|
||||
<div class="flex items-center justify-between gap-4">
|
||||
<div class="min-w-0 space-y-1">
|
||||
<h3 class="text-sm font-medium text-white/80">Listening statistics</h3>
|
||||
<p class="text-xs leading-relaxed text-white/45">
|
||||
{user.statisticsOptOut
|
||||
? 'New listening activity is not being collected for your account.'
|
||||
: 'Collect listening activity to power your account statistics.'}
|
||||
</p>
|
||||
</div>
|
||||
<Switch
|
||||
id="statisticsCollection"
|
||||
checked={!user.statisticsOptOut}
|
||||
onCheckedChange={(checked) => updateStatisticsOptOut(!checked)}
|
||||
disabled={busyAction === 'statistics-opt-out'}
|
||||
/>
|
||||
</div>
|
||||
{#if statisticsMessage}
|
||||
<p class="text-xs text-white/60">{statisticsMessage}</p>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<div class="space-y-3">
|
||||
<h3 class="text-sm font-medium text-white/80">Passkeys</h3>
|
||||
|
||||
<div class="space-y-2">
|
||||
{#each passkeys as passkey (passkey.id)}
|
||||
<div
|
||||
class="group flex items-center gap-3 p-3 rounded-lg bg-white/[0.03] border border-white/[0.06] transition-all duration-200 hover:bg-white/[0.06] hover:border-white/[0.1]"
|
||||
>
|
||||
<div
|
||||
class="flex-shrink-0 w-9 h-9 rounded-lg bg-white/[0.06] flex items-center justify-center"
|
||||
>
|
||||
<Key class="size-4 text-white/70" />
|
||||
</div>
|
||||
<div class="min-w-0 flex-1">
|
||||
<p class="text-sm font-medium text-white/90 truncate">{passkey.name}</p>
|
||||
<p class="text-xs text-white/40">
|
||||
{new Date(passkey.createdAt).toLocaleDateString(undefined, {
|
||||
month: 'short',
|
||||
day: 'numeric',
|
||||
year: 'numeric',
|
||||
})}
|
||||
</p>
|
||||
</div>
|
||||
<Button
|
||||
type="button"
|
||||
size="icon"
|
||||
variant="ghost"
|
||||
class="!size-8 opacity-0 group-hover:opacity-100 transition-opacity duration-200 !text-red-400/70 hover:!text-red-400 hover:!bg-red-500/10"
|
||||
aria-label="Delete passkey"
|
||||
onclick={() => deletePasskey(passkey.id)}
|
||||
disabled={busyAction === `delete-passkey-${passkey.id}`}
|
||||
>
|
||||
{#if busyAction === `delete-passkey-${passkey.id}`}
|
||||
<Loader2 class="size-4 animate-spin" />
|
||||
{:else}
|
||||
<Trash2 class="size-3.5" />
|
||||
{/if}
|
||||
</Button>
|
||||
</div>
|
||||
{:else}
|
||||
<div
|
||||
class="flex items-center gap-3 p-4 rounded-lg bg-white/[0.02] border border-dashed border-white/[0.1] text-white/40"
|
||||
>
|
||||
<Fingerprint class="size-5 !text-white/40" />
|
||||
<p class="text-sm !text-white/40">No passkeys added yet</p>
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="space-y-2">
|
||||
<div class="flex gap-2">
|
||||
<div class="flex-1">
|
||||
<Input
|
||||
type="text"
|
||||
id="passkeyName"
|
||||
bind:value={passkeyName}
|
||||
placeholder="Device name"
|
||||
class="!h-10"
|
||||
/>
|
||||
</div>
|
||||
<Button
|
||||
onclick={addPasskey}
|
||||
disabled={busyAction === 'add-passkey'}
|
||||
class="!h-10 px-4 !text-white/80 hover:!text-white"
|
||||
>
|
||||
{#if busyAction === 'add-passkey'}
|
||||
<Loader2 class="size-4 animate-spin" />
|
||||
{:else}
|
||||
<Plus class="size-4" />
|
||||
{/if}
|
||||
</Button>
|
||||
</div>
|
||||
{#if passkeyMessage}
|
||||
<p
|
||||
class="text-xs px-3 py-2 rounded-lg {passkeyMessage.includes('success') ||
|
||||
passkeyMessage.includes('removed') ||
|
||||
passkeyMessage.includes('added')
|
||||
? 'bg-white/[0.06] text-white/70 border border-white/[0.08]'
|
||||
: 'bg-red-500/10 text-red-400 border border-red-500/20'}"
|
||||
>
|
||||
{passkeyMessage}
|
||||
</p>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<div class="pt-3 border-t border-white/[0.06]">
|
||||
<Button
|
||||
variant="ghost"
|
||||
onclick={signOut}
|
||||
disabled={busyAction === 'sign-out'}
|
||||
class="w-full justify-center gap-2 !text-white/60 hover:!text-white hover:bg-white/[0.06]"
|
||||
>
|
||||
{#if busyAction === 'sign-out'}
|
||||
<Loader2 class="size-4 animate-spin" />
|
||||
{:else}
|
||||
<LogIn class="size-4 rotate-180" />
|
||||
{/if}
|
||||
Sign out
|
||||
</Button>
|
||||
</div>
|
||||
{:else if authScreen === 'login'}
|
||||
<div class="space-y-4" bind:this={createFormElement}>
|
||||
<div class="space-y-2">
|
||||
<Label for="accountNumber" class="text-white/70">Account Number</Label>
|
||||
<Input
|
||||
type="text"
|
||||
id="accountNumber"
|
||||
bind:value={accountNumber}
|
||||
placeholder="Enter your account number"
|
||||
autocomplete="one-time-code webauthn"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="flex gap-2">
|
||||
<Button
|
||||
type="button"
|
||||
onclick={signInWithAccountNumber}
|
||||
disabled={busyAction === 'account-number' || !accountNumber}
|
||||
class="flex-1 disabled:opacity-50"
|
||||
>
|
||||
{#if busyAction === 'account-number'}
|
||||
<Loader2 class="size-4 animate-spin" />
|
||||
Signing in...
|
||||
{:else}
|
||||
Sign in
|
||||
{/if}
|
||||
</Button>
|
||||
<Button
|
||||
type="button"
|
||||
size="icon"
|
||||
onclick={signInWithPasskey}
|
||||
disabled={busyAction === 'passkey-sign-in'}
|
||||
class="!w-auto px-3"
|
||||
title="Sign in with passkey"
|
||||
>
|
||||
{#if busyAction === 'passkey-sign-in'}
|
||||
<Loader2 class="size-4 animate-spin" />
|
||||
{:else}
|
||||
<Fingerprint class="size-4" />
|
||||
{/if}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="relative flex items-center gap-3 py-2">
|
||||
<div
|
||||
class="flex-1 h-px bg-gradient-to-r from-transparent via-white/15 to-transparent"
|
||||
></div>
|
||||
<span class="text-xs text-white/30 uppercase tracking-wider font-medium">or</span>
|
||||
<div
|
||||
class="flex-1 h-px bg-gradient-to-r from-transparent via-white/15 to-transparent"
|
||||
></div>
|
||||
</div>
|
||||
|
||||
<Button
|
||||
type="button"
|
||||
onclick={() => (authScreen = 'create')}
|
||||
disabled={busyAction === 'create-account'}
|
||||
variant="ghost"
|
||||
class="w-full justify-center text-white/60 hover:text-white hover:bg-white/[0.06]"
|
||||
>
|
||||
Create new account
|
||||
</Button>
|
||||
|
||||
{#if authMessage}
|
||||
<p
|
||||
class="text-xs text-red-400 bg-red-500/10 rounded-lg px-3 py-2 border border-red-500/20"
|
||||
>
|
||||
{authMessage}
|
||||
</p>
|
||||
{/if}
|
||||
{:else if authScreen === 'create'}
|
||||
<div class="space-y-4">
|
||||
<Button
|
||||
type="button"
|
||||
variant="ghost"
|
||||
onclick={() => (authScreen = 'login')}
|
||||
disabled={busyAction === 'create-account'}
|
||||
class="!p-0 h-auto text-white/50 hover:text-white hover:bg-transparent"
|
||||
>
|
||||
<ArrowLeft class="size-4 mr-1" />
|
||||
Back to sign in
|
||||
</Button>
|
||||
|
||||
<div class="space-y-2">
|
||||
<Label for="name" class="text-white/70">Your Name</Label>
|
||||
<Input
|
||||
type="text"
|
||||
id="name"
|
||||
bind:value={name}
|
||||
placeholder="What should we call you?"
|
||||
autocomplete="name webauthn"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<Turnstile
|
||||
siteKey={turnstileSiteKey}
|
||||
bind:reset={resetTurnstile}
|
||||
on:callback={handleTurnstileCallback}
|
||||
on:error={handleTurnstileError}
|
||||
on:expired={handleTurnstileExpired}
|
||||
/>
|
||||
|
||||
<Button
|
||||
type="button"
|
||||
onclick={createAccount}
|
||||
disabled={busyAction === 'create-account' || !name}
|
||||
class="w-full disabled:opacity-50"
|
||||
>
|
||||
{#if busyAction === 'create-account'}
|
||||
<Loader2 class="size-4 animate-spin" />
|
||||
Creating account...
|
||||
{:else}
|
||||
Create Account
|
||||
{/if}
|
||||
</Button>
|
||||
|
||||
{#if authMessage}
|
||||
<p
|
||||
class="text-xs text-red-400 bg-red-500/10 rounded-lg px-3 py-2 border border-red-500/20"
|
||||
>
|
||||
{authMessage}
|
||||
</p>
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
</Dialog.Content>
|
||||
</Dialog.Root>
|
||||
@@ -4,11 +4,10 @@
|
||||
import MusicPlayer from '@/components/app/now-playing.svelte';
|
||||
import Sounds from '@/components/app/atmospheres.svelte';
|
||||
import StationDropdown from '@/components/app/station-dropdown.svelte';
|
||||
import Feedback from '@/components/app/feedback.svelte';
|
||||
</script>
|
||||
|
||||
<div
|
||||
class="fixed bottom-5 left-2 right-2 z-50 flex flex-col sm:flex-row items-center p-4 bg-white/10 backdrop-blur-lg rounded-xl shadow-lg"
|
||||
class="fixed bottom-5 left-2 right-2 z-50 flex flex-col sm:flex-row items-center p-4 bg-foreground/10 backdrop-blur-lg rounded-xl shadow-lg"
|
||||
>
|
||||
<MusicPlayer />
|
||||
|
||||
@@ -17,7 +16,6 @@
|
||||
<StationDropdown />
|
||||
<BgDropdown />
|
||||
<Sounds />
|
||||
<Feedback />
|
||||
<Disclaimer />
|
||||
</div>
|
||||
</div>
|
||||
@@ -6,6 +6,12 @@
|
||||
import Window from '../ui/window/window.svelte';
|
||||
import TodoList from './todo-list.svelte';
|
||||
import Twentytwentytwenty from './twentytwentytwenty.svelte';
|
||||
import Pomodoro from './pomodoro.svelte';
|
||||
import Stats from './stats.svelte';
|
||||
import { authClient } from '$lib';
|
||||
|
||||
const session = authClient.useSession();
|
||||
const user = $derived($session.data?.user);
|
||||
|
||||
// svelte-ignore non_reactive_update
|
||||
let audioElement: HTMLAudioElement;
|
||||
@@ -160,6 +166,34 @@
|
||||
}
|
||||
});
|
||||
|
||||
onMount(() => {
|
||||
const listenInterval = setInterval(async () => {
|
||||
if (
|
||||
!appState.isPlaying ||
|
||||
!appState.currentSong ||
|
||||
!appState.currentStation ||
|
||||
!user ||
|
||||
user?.statisticsOptOut ||
|
||||
!audioElement ||
|
||||
audioElement.paused
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
await fetch('/api/listen', {
|
||||
method: 'POST',
|
||||
headers: { 'content-type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
fileId: appState.currentSong.fileId,
|
||||
stationId: appState.currentStation,
|
||||
seconds: 30,
|
||||
}),
|
||||
});
|
||||
}, 30_000);
|
||||
|
||||
return () => clearInterval(listenInterval);
|
||||
})
|
||||
|
||||
$effect(() => {
|
||||
if (!audioElement) return;
|
||||
togglePlayback(appState.isPlaying);
|
||||
@@ -260,4 +294,28 @@
|
||||
show={appState.show202020}
|
||||
>
|
||||
<Twentytwentytwenty></Twentytwentytwenty>
|
||||
</Window>
|
||||
</Window>
|
||||
|
||||
<Window
|
||||
title="Pomodoro Timer"
|
||||
showTitleBar={true}
|
||||
showCloseButton={true}
|
||||
width={320}
|
||||
height={250}
|
||||
onClose={() => appState.showPomodoro = false}
|
||||
show={appState.showPomodoro}
|
||||
>
|
||||
<Pomodoro></Pomodoro>
|
||||
</Window>
|
||||
|
||||
<Window
|
||||
title="Stats"
|
||||
showTitleBar={true}
|
||||
showCloseButton={true}
|
||||
width={500}
|
||||
height={400}
|
||||
onClose={() => appState.showStats = false}
|
||||
show={appState.showStats}
|
||||
>
|
||||
<Stats></Stats>
|
||||
</Window>
|
||||
|
||||
@@ -1,38 +0,0 @@
|
||||
<script lang="ts">
|
||||
import * as Dialog from '$lib/components/ui/dialog/index.js';
|
||||
import MessageSquare from '@lucide/svelte/icons/message-square';
|
||||
import { Button } from '../ui/button';
|
||||
import { toast } from 'svelte-sonner';
|
||||
let feedback = $state('');
|
||||
let open = $state(false);
|
||||
</script>
|
||||
|
||||
<Dialog.Root bind:open>
|
||||
<Dialog.Trigger><Button size="icon"><MessageSquare /></Button></Dialog.Trigger>
|
||||
<Dialog.Content>
|
||||
<Dialog.Header>
|
||||
<Dialog.Title>Send Feedback</Dialog.Title>
|
||||
</Dialog.Header>
|
||||
<p>
|
||||
Have suggestions or found a bug? Let me know!
|
||||
</p>
|
||||
<form class="mt-4 space-y-4" onsubmit={(e) => {
|
||||
e.preventDefault();
|
||||
fetch('https://cors.notesnook.com/https://echospace.dev/api/feedback/cma9juarn0000m40sko0qqt2t', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({
|
||||
message: feedback,
|
||||
})
|
||||
}).then(() => {
|
||||
open = false;
|
||||
feedback = '';
|
||||
toast('Thanks for your feedback!')
|
||||
})
|
||||
}}>
|
||||
<div class="grid w-full items-center gap-1.5">
|
||||
<textarea id="feedback" class="p-2 rounded-md bg-white/10 border border-white/20" rows="4" bind:value={feedback}></textarea>
|
||||
</div>
|
||||
<Button type="submit">Submit</Button>
|
||||
</form>
|
||||
</Dialog.Content>
|
||||
</Dialog.Root>
|
||||
@@ -3,10 +3,12 @@
|
||||
import Check from '@lucide/svelte/icons/check';
|
||||
import Button from '../ui/button/button.svelte';
|
||||
import Binoculars from '@lucide/svelte/icons/binoculars';
|
||||
import Clock from '@lucide/svelte/icons/clock';
|
||||
import ChartNoAxesColumn from '@lucide/svelte/icons/chart-no-axes-column';
|
||||
</script>
|
||||
|
||||
<div
|
||||
class="absolute left-2 top-1/2 transform -translate-y-1/2 p-4 bg-white/10 backdrop-blur-lg rounded-xl shadow-lg flex flex-col space-y-2"
|
||||
class="absolute left-2 top-1/2 transform -translate-y-1/2 p-4 bg-foreground/10 backdrop-blur-lg rounded-xl shadow-lg flex flex-col space-y-2"
|
||||
>
|
||||
<Button size="icon" onclick={() => (appState.showTodoList = !appState.showTodoList)}>
|
||||
<Check class="size-4" />
|
||||
@@ -17,4 +19,16 @@
|
||||
>
|
||||
<Binoculars class="size-4" />
|
||||
</Button>
|
||||
<Button
|
||||
size="icon"
|
||||
onclick={() => (appState.showPomodoro = !appState.showPomodoro)}
|
||||
>
|
||||
<Clock class="size-4" />
|
||||
</Button>
|
||||
<Button
|
||||
size="icon"
|
||||
onclick={() => (appState.showStats = !appState.showStats)}
|
||||
>
|
||||
<ChartNoAxesColumn class="size-4" />
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
130
src/lib/components/app/pomodoro.svelte
Normal file
130
src/lib/components/app/pomodoro.svelte
Normal file
@@ -0,0 +1,130 @@
|
||||
<script lang="ts">
|
||||
import Button from '@/components/ui/button/button.svelte';
|
||||
import { state as appState } from '@/state.svelte';
|
||||
import { onMount } from 'svelte';
|
||||
|
||||
let timeLeft = $state(0);
|
||||
let intervalHandle: ReturnType<typeof setInterval> | null = null;
|
||||
|
||||
let startSoundElement: HTMLAudioElement;
|
||||
let endSoundElement: HTMLAudioElement;
|
||||
|
||||
let minutes = $derived(Math.floor(timeLeft / 60));
|
||||
let seconds = $derived(timeLeft % 60);
|
||||
|
||||
onMount(() => {
|
||||
const defaultFirst = 20 * 60;
|
||||
const defaultSecond = 5 * 60;
|
||||
|
||||
if (!appState.pomodoroTimer) appState.pomodoroTimer = defaultFirst;
|
||||
if (!appState.pomodoroBreakTimer) appState.pomodoroBreakTimer = defaultSecond;
|
||||
|
||||
timeLeft = appState.pomodoroTimer;
|
||||
});
|
||||
|
||||
function playSound(element: HTMLAudioElement) {
|
||||
if (!element) return;
|
||||
|
||||
element.currentTime = 0;
|
||||
|
||||
const playPromise = element.play();
|
||||
|
||||
if (playPromise) {
|
||||
playPromise.catch((error) => {
|
||||
console.error('Audio play error:', error);
|
||||
if (error.name === 'NotAllowedError') {
|
||||
console.warn('Audio playback blocked by browser. User interaction required.');
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function startCountdown() {
|
||||
if (intervalHandle) {
|
||||
clearInterval(intervalHandle);
|
||||
intervalHandle = null;
|
||||
}
|
||||
|
||||
intervalHandle = setInterval(() => {
|
||||
if (timeLeft > 0) {
|
||||
timeLeft--;
|
||||
} else {
|
||||
if (appState.pomodoroWorkPhase) {
|
||||
appState.pomodoroWorkPhase = false;
|
||||
timeLeft = appState.pomodoroBreakTimer;
|
||||
playSound(startSoundElement);
|
||||
} else {
|
||||
appState.pomodoroWorkPhase = true;
|
||||
timeLeft = appState.pomodoroTimer;
|
||||
playSound(endSoundElement);
|
||||
appState.isPomodoroActive = false;
|
||||
}
|
||||
}
|
||||
}, 1000);
|
||||
}
|
||||
|
||||
$effect(() => {
|
||||
if (appState.isPomodoroActive) {
|
||||
startCountdown();
|
||||
} else if (intervalHandle) {
|
||||
clearInterval(intervalHandle);
|
||||
intervalHandle = null;
|
||||
}
|
||||
|
||||
return () => {
|
||||
if (intervalHandle) {
|
||||
clearInterval(intervalHandle);
|
||||
intervalHandle = null;
|
||||
appState.isPomodoroActive = false;
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
function startTimer() {
|
||||
reset();
|
||||
timeLeft = appState.pomodoroTimer;
|
||||
appState.pomodoroWorkPhase = true;
|
||||
appState.isPomodoroActive = true;
|
||||
}
|
||||
|
||||
function stopTimer() {
|
||||
reset();
|
||||
appState.isPomodoroActive = false;
|
||||
}
|
||||
|
||||
function reset() {
|
||||
appState.pomodoroTimer = 20 * 60;
|
||||
appState.pomodoroBreakTimer = 5 * 60;
|
||||
}
|
||||
</script>
|
||||
|
||||
<audio
|
||||
bind:this={startSoundElement}
|
||||
src="https://lofi-cdn.srizan.dev/assets/202020/start.mp3"
|
||||
preload="auto"
|
||||
></audio>
|
||||
|
||||
<audio
|
||||
bind:this={endSoundElement}
|
||||
src="https://lofi-cdn.srizan.dev/assets/202020/done.mp3"
|
||||
preload="auto"
|
||||
></audio>
|
||||
|
||||
<div class="flex flex-col p-4">
|
||||
<div class="mb-6">
|
||||
<div class="text-2xl font-bold">
|
||||
{appState.pomodoroWorkPhase ? 'Work Time' : 'Break Time'}
|
||||
</div>
|
||||
<div class="text-3xl font-bold mt-2">
|
||||
{minutes.toString().padStart(2, '0')}:{seconds.toString().padStart(2, '0')}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex gap-2">
|
||||
{#if appState.isPomodoroActive}
|
||||
<Button variant="destructive" onclick={stopTimer}>Stop</Button>
|
||||
{:else}
|
||||
<Button onclick={startTimer}>Start</Button>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
193
src/lib/components/app/stats.svelte
Normal file
193
src/lib/components/app/stats.svelte
Normal file
@@ -0,0 +1,193 @@
|
||||
<script lang="ts">
|
||||
import { onMount } from 'svelte';
|
||||
import { state as appState } from '@/state.svelte';
|
||||
import Radio from '@lucide/svelte/icons/radio';
|
||||
import ListMusic from '@lucide/svelte/icons/list-music';
|
||||
import { authClient } from '@/auth-client';
|
||||
|
||||
const session = authClient.useSession();
|
||||
|
||||
type TopSong = {
|
||||
fileId: string;
|
||||
title: string | null;
|
||||
artists: string | null;
|
||||
image: string | null;
|
||||
seconds: number;
|
||||
};
|
||||
|
||||
type TopStation = {
|
||||
stationId: number | null;
|
||||
seconds: number;
|
||||
};
|
||||
|
||||
type StatsResponse = {
|
||||
totalSeconds: number;
|
||||
todaySeconds: number;
|
||||
topSongs: TopSong[];
|
||||
topStations: TopStation[];
|
||||
};
|
||||
|
||||
let stats = $state<StatsResponse | null>(null);
|
||||
let isLoading = $state(true);
|
||||
let error = $state<string | null>(null);
|
||||
let activeTab = $state<'stations' | 'songs'>('stations');
|
||||
|
||||
const formatDuration = (seconds: number) => {
|
||||
const totalMinutes = Math.floor(seconds / 60);
|
||||
const hours = Math.floor(totalMinutes / 60);
|
||||
const minutes = totalMinutes % 60;
|
||||
|
||||
if (hours === 0) return `${minutes}m`;
|
||||
if (minutes === 0) return `${hours}h`;
|
||||
|
||||
return `${hours}h ${minutes}m`;
|
||||
};
|
||||
|
||||
const stationName = (stationId: number | null) => {
|
||||
const station = appState.stations.find((item) => item.id === stationId);
|
||||
return station?.name ?? 'Unknown station';
|
||||
};
|
||||
|
||||
const maxStationSeconds = $derived.by(() => {
|
||||
if (!stats) return 1;
|
||||
return Math.max(...stats.topStations.map((s) => s.seconds), 1);
|
||||
});
|
||||
|
||||
const maxSongSeconds = $derived.by(() => {
|
||||
if (!stats) return 1;
|
||||
return Math.max(...stats.topSongs.map((s) => s.seconds), 1);
|
||||
});
|
||||
|
||||
onMount(async () => {
|
||||
try {
|
||||
const response = await fetch('/api/stats');
|
||||
if (!response.ok) {
|
||||
error =
|
||||
response.status === 401
|
||||
? 'Sign in to see your listening stats.'
|
||||
: 'Could not load stats.';
|
||||
return;
|
||||
}
|
||||
|
||||
stats = await response.json();
|
||||
} catch {
|
||||
if (session) {
|
||||
error = 'Could not load stats. Please try again later.';
|
||||
} else {
|
||||
error = 'Sign in to see your listening stats.';
|
||||
}
|
||||
} finally {
|
||||
isLoading = false;
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<div class="flex h-full min-h-0 flex-col p-5 text-foreground">
|
||||
{#if isLoading}
|
||||
<div class="flex flex-1 flex-col items-center justify-center gap-3 text-foreground/40">
|
||||
<div class="h-4 w-4 animate-spin rounded-full border-2 border-foreground/15 border-t-foreground/60"></div>
|
||||
<span class="text-xs tracking-wide">Loading stats</span>
|
||||
</div>
|
||||
{:else if error}
|
||||
<div class="flex flex-1 flex-col items-center justify-center text-center">
|
||||
<p class="text-sm text-foreground/50">{error}</p>
|
||||
</div>
|
||||
{:else if stats}
|
||||
<div class="mb-5 shrink-0">
|
||||
<p class="mb-2 text-[10px] font-medium uppercase tracking-[0.2em] text-foreground/40">
|
||||
Listening Stats
|
||||
</p>
|
||||
<div class="flex items-baseline gap-5">
|
||||
<div>
|
||||
<span class="text-3xl">{formatDuration(stats.todaySeconds)}</span>
|
||||
<span class="ml-1.5 text-[10px] font-medium uppercase tracking-wider text-foreground/40">Today</span>
|
||||
</div>
|
||||
<div class="h-3 w-px bg-white/10"></div>
|
||||
<div>
|
||||
<span class="text-3xl">{formatDuration(stats.totalSeconds)}</span>
|
||||
<span class="ml-1.5 text-[10px] font-medium uppercase tracking-wider text-foreground/40">All time</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mb-3 flex shrink-0 items-center justify-between">
|
||||
<div class="flex rounded-lg border border-white/[0.06] bg-white/[0.03] p-0.5">
|
||||
<button
|
||||
class="flex items-center gap-1.5 rounded-md px-3 py-1 text-[11px] font-medium transition-all duration-200 {activeTab === 'stations' ? 'bg-white/10 text-foreground shadow-sm' : 'text-foreground/40 hover:text-foreground/70'}"
|
||||
onclick={() => (activeTab = 'stations')}
|
||||
>
|
||||
<Radio class="size-3" />
|
||||
Stations
|
||||
</button>
|
||||
<button
|
||||
class="flex items-center gap-1.5 rounded-md px-3 py-1 text-[11px] font-medium transition-all duration-200 {activeTab === 'songs' ? 'bg-white/10 text-foreground shadow-sm' : 'text-foreground/40 hover:text-foreground/70'}"
|
||||
onclick={() => (activeTab = 'songs')}
|
||||
>
|
||||
<ListMusic class="size-3" />
|
||||
Songs
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="min-h-0 flex-1 overflow-y-auto custom-scrollbar">
|
||||
{#if activeTab === 'stations'}
|
||||
{#each stats.topStations as station, i}
|
||||
<div class="flex items-center gap-3 border-b border-white/[0.04] py-2.5 last:border-0">
|
||||
<span class="w-4 text-right text-[10px] font-mono text-foreground/25">{i + 1}</span>
|
||||
<div class="min-w-0 flex-1">
|
||||
<div class="mb-1 flex items-center justify-between gap-3">
|
||||
<span class="truncate text-sm text-foreground/90">
|
||||
{stationName(station.stationId)}
|
||||
</span>
|
||||
<span class="shrink-0 text-xs font-mono text-foreground/40">
|
||||
{formatDuration(station.seconds)}
|
||||
</span>
|
||||
</div>
|
||||
<div class="h-[2px] overflow-hidden rounded-full bg-white/[0.06]">
|
||||
<div
|
||||
class="h-full rounded-full bg-white/20 transition-all duration-700 ease-out"
|
||||
style="width: {(station.seconds / maxStationSeconds) * 100}%"
|
||||
></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{:else}
|
||||
<div class="flex h-32 items-center justify-center">
|
||||
<p class="text-xs text-foreground/30">Station stats will appear after you listen for a bit.</p>
|
||||
</div>
|
||||
{/each}
|
||||
{:else}
|
||||
{#each stats.topSongs as song, i}
|
||||
<div class="flex items-center gap-3 border-b border-white/[0.04] py-2.5 last:border-0">
|
||||
<span class="w-4 text-right text-[10px] font-mono text-foreground/25">{i + 1}</span>
|
||||
{#if song.image}
|
||||
<img src={song.image} alt="" class="size-7 shrink-0 rounded-sm object-cover opacity-80" />
|
||||
{:else}
|
||||
<div class="size-7 shrink-0 rounded-sm bg-white/[0.06]"></div>
|
||||
{/if}
|
||||
<div class="min-w-0 flex-1">
|
||||
<div class="mb-1 flex items-center justify-between gap-3">
|
||||
<span class="truncate text-sm text-foreground/90">
|
||||
{song.title ?? song.fileId}
|
||||
</span>
|
||||
<span class="shrink-0 text-xs font-mono text-foreground/40">
|
||||
{formatDuration(song.seconds)}
|
||||
</span>
|
||||
</div>
|
||||
<div class="h-[2px] overflow-hidden rounded-full bg-white/[0.06]">
|
||||
<div
|
||||
class="h-full rounded-full bg-white/20 transition-all duration-700 ease-out"
|
||||
style="width: {(song.seconds / maxSongSeconds) * 100}%"
|
||||
></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{:else}
|
||||
<div class="flex h-32 items-center justify-center">
|
||||
<p class="text-xs text-foreground/30">No top songs yet.</p>
|
||||
</div>
|
||||
{/each}
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
@@ -35,22 +35,22 @@
|
||||
<input
|
||||
bind:value={newTodoText}
|
||||
placeholder="Add a new task..."
|
||||
class="flex-1 pl-3 rounded-lg bg-white bg-opacity-10 border border-white border-opacity-20 text-white placeholder-white placeholder-opacity-60 focus:outline-none focus:ring-2 focus:ring-white focus:ring-opacity-40"
|
||||
class="flex-1 pl-3 rounded-lg bg-white bg-opacity-10 border border-white border-opacity-20 text-foreground placeholder:text-foreground placeholder:opacity-60 focus:outline-none focus:ring-2 focus:ring-white focus:ring-opacity-40"
|
||||
onkeypress={(e) => e.key === 'Enter' && addTodo()}
|
||||
/>
|
||||
<Button onclick={addTodo} class="bg-white bg-opacity-20 hover:bg-opacity-30 text-white px-4 py-2 rounded-lg" size="icon"><Plus /></Button>
|
||||
<Button onclick={addTodo} class="bg-white bg-opacity-20 hover:bg-opacity-30 text-foreground px-4 py-2 rounded-lg" size="icon"><Plus /></Button>
|
||||
</div>
|
||||
|
||||
{#if appState.todoList.length === 0}
|
||||
<p class="text-white text-opacity-70 text-center py-4">No tasks yet. Add one above!</p>
|
||||
<p class="text-foreground text-opacity-70 text-center py-4">No tasks yet. Add one above!</p>
|
||||
{:else}
|
||||
<ul class="space-y-2">
|
||||
{#each appState.todoList as todo, index}
|
||||
<li class="flex items-center justify-between p-3 bg-white bg-opacity-10 rounded-lg group hover:bg-opacity-15 transition-all">
|
||||
<span class="text-white">{todo}</span>
|
||||
<span class="text-foreground">{todo}</span>
|
||||
<button
|
||||
onclick={() => removeTodo(index)}
|
||||
class="text-white hover:text-red-300 focus:outline-none"
|
||||
class="text-foreground hover:text-red-300 focus:outline-none"
|
||||
aria-label="Remove todo"
|
||||
>
|
||||
<X class="size-4" />
|
||||
|
||||
@@ -1,17 +1,17 @@
|
||||
<script lang="ts">
|
||||
import { Slider } from "$lib/components/ui/slider";
|
||||
import * as Popover from "$lib/components/ui/popover";
|
||||
import { state as appState } from "@/state.svelte";
|
||||
import VolumeZero from "@lucide/svelte/icons/volume";
|
||||
import VolumeOne from "@lucide/svelte/icons/volume-1";
|
||||
import VolumeTwo from "@lucide/svelte/icons/volume-2";
|
||||
import VolumeX from "@lucide/svelte/icons/volume-x";
|
||||
import { Button } from "../ui/button";
|
||||
import { Slider } from '$lib/components/ui/slider';
|
||||
import * as Popover from '$lib/components/ui/popover';
|
||||
import { state as appState } from '@/state.svelte';
|
||||
import VolumeZero from '@lucide/svelte/icons/volume';
|
||||
import VolumeOne from '@lucide/svelte/icons/volume-1';
|
||||
import VolumeTwo from '@lucide/svelte/icons/volume-2';
|
||||
import VolumeX from '@lucide/svelte/icons/volume-x';
|
||||
import { Button } from '../ui/button';
|
||||
|
||||
let value = $state(appState.volume);
|
||||
$effect(() => {
|
||||
appState.volume = value;
|
||||
window.localStorage.setItem("volume", value.toString());
|
||||
window.localStorage.setItem('volume', value.toString());
|
||||
});
|
||||
</script>
|
||||
|
||||
@@ -20,16 +20,16 @@
|
||||
<Button size="icon" class="size-10">
|
||||
{#if value === 0}
|
||||
<VolumeX />
|
||||
{:else if value > 0 && value <= 0.4}
|
||||
{:else if value > 0 && value <= 0.4}
|
||||
<VolumeZero />
|
||||
{:else if value > 0.4 && value <= 0.8}
|
||||
{:else if value > 0.4 && value <= 0.8}
|
||||
<VolumeOne />
|
||||
{:else}
|
||||
{:else}
|
||||
<VolumeTwo />
|
||||
{/if}
|
||||
</Button>
|
||||
</Popover.Trigger>
|
||||
<Popover.Content class="w-2 h-32" side="top">
|
||||
<Popover.Content class="w-10 h-32 !min-w-0" side="top">
|
||||
<Slider type="single" orientation="vertical" bind:value max={1} step={0.01} />
|
||||
</Popover.Content>
|
||||
</Popover.Root>
|
||||
</Popover.Root>
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
variants: {
|
||||
variant: {
|
||||
default:
|
||||
'bg-neutral-400/10 dark:bg-neutral-600/10 hover:bg-neutral-400/20 dark:hover:bg-neutral-600/20 text-foreground border border-neutral-400/20 dark:border-neutral-600/20 shadow-lg',
|
||||
'bg-foreground/10 dark:bg-foreground/20 hover:bg-foreground/20 dark:hover:bg-foreground/30 text-foreground border border-neutral-400/20 dark:border-neutral-600/30 shadow-lg',
|
||||
olddefault: 'bg-primary text-primary-foreground hover:bg-primary/90 shadow',
|
||||
destructive: 'bg-destructive text-destructive-foreground hover:bg-destructive/90 shadow-sm',
|
||||
outline:
|
||||
|
||||
@@ -23,20 +23,28 @@
|
||||
<DialogPrimitive.Content
|
||||
bind:ref
|
||||
class={cn(
|
||||
'data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[state=closed]:slide-out-to-left-1/2 data-[state=closed]:slide-out-to-top-[48%] data-[state=open]:slide-in-from-left-1/2 data-[state=open]:slide-in-from-top-[48%] fixed left-[50%] top-[50%] z-50 grid w-full max-w-lg translate-x-[-50%] translate-y-[-50%] gap-4 border p-6 shadow-lg duration-200 sm:rounded-lg',
|
||||
'backdrop-blur-md border drop-shadow-md shadow-lg',
|
||||
'dark:bg-black/20 bg-white/20 transition border-white/20 dark:border-black/20',
|
||||
'fixed left-[50%] top-[50%] z-[60] grid w-full max-w-lg translate-x-[-50%] translate-y-[-50%] gap-4 p-6 duration-300 sm:rounded-2xl',
|
||||
'bg-white/[0.08] dark:bg-black/[0.45] backdrop-blur-2xl',
|
||||
'border border-white/[0.12] dark:border-white/[0.08]',
|
||||
'shadow-[0_8px_32px_rgba(0,0,0,0.32),inset_0_1px_0_rgba(255,255,255,0.1)]',
|
||||
'text-white',
|
||||
'data-[state=open]:animate-in data-[state=closed]:animate-out',
|
||||
'data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0',
|
||||
'data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95',
|
||||
'data-[state=closed]:slide-out-to-left-1/2 data-[state=closed]:slide-out-to-top-[48%]',
|
||||
'data-[state=open]:slide-in-from-left-1/2 data-[state=open]:slide-in-from-top-[48%]',
|
||||
'before:absolute before:inset-0 before:rounded-2xl before:bg-gradient-to-br before:from-white/[0.08] before:to-transparent before:pointer-events-none',
|
||||
className
|
||||
)}
|
||||
{...restProps}
|
||||
>
|
||||
<div class="text-foreground/90 max-w-none space-y-3 font-medium">
|
||||
<div class="relative z-[60] text-white max-w-none space-y-3 font-medium">
|
||||
{@render children?.()}
|
||||
</div>
|
||||
<DialogPrimitive.Close
|
||||
class="absolute right-4 top-4 rounded-sm opacity-70 transition-opacity hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-offset-2 disabled:pointer-events-none backdrop-blur-sm p-1 shadow-sm dark:bg-black/20 bg-white/20 border-white/20 dark:border-black/20"
|
||||
class="absolute right-4 top-4 z-[60] rounded-lg p-2 opacity-60 transition-all duration-200 hover:opacity-100 hover:bg-white/10 focus:outline-none focus:ring-2 focus:ring-white/20 disabled:pointer-events-none backdrop-blur-sm border border-white/[0.08]"
|
||||
>
|
||||
<X class="size-4" color="white" />
|
||||
<X class="size-4 text-white" />
|
||||
<span class="sr-only">Close</span>
|
||||
</DialogPrimitive.Close>
|
||||
</DialogPrimitive.Content>
|
||||
|
||||
@@ -11,6 +11,6 @@
|
||||
|
||||
<DialogPrimitive.Description
|
||||
bind:ref
|
||||
class={cn("text-black/80 text-sm", className)}
|
||||
class={cn("text-white/70 text-sm leading-relaxed", className)}
|
||||
{...restProps}
|
||||
/>
|
||||
|
||||
@@ -13,7 +13,7 @@
|
||||
|
||||
<div
|
||||
bind:this={ref}
|
||||
class={cn("flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2", className)}
|
||||
class={cn("flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-3 pt-4 mt-2 border-t border-white/[0.08]", className)}
|
||||
{...restProps}
|
||||
>
|
||||
{@render children?.()}
|
||||
|
||||
@@ -13,7 +13,7 @@
|
||||
|
||||
<div
|
||||
bind:this={ref}
|
||||
class={cn("flex flex-col space-y-1.5 text-center sm:text-left", className)}
|
||||
class={cn("flex flex-col space-y-2 text-center sm:text-left pb-2", className)}
|
||||
{...restProps}
|
||||
>
|
||||
{@render children?.()}
|
||||
|
||||
@@ -12,7 +12,11 @@
|
||||
<DialogPrimitive.Overlay
|
||||
bind:ref
|
||||
class={cn(
|
||||
"data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 fixed inset-0 z-50 bg-black/80",
|
||||
"fixed inset-0 z-[60]",
|
||||
"data-[state=open]:animate-in data-[state=closed]:animate-out",
|
||||
"data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0",
|
||||
"bg-black/40 backdrop-blur-sm",
|
||||
"before:absolute before:inset-0 before:bg-gradient-to-b before:from-black/20 before:to-transparent",
|
||||
className
|
||||
)}
|
||||
{...restProps}
|
||||
|
||||
@@ -11,6 +11,6 @@
|
||||
|
||||
<DialogPrimitive.Title
|
||||
bind:ref
|
||||
class={cn("text-lg font-semibold leading-none tracking-tight", className)}
|
||||
class={cn("text-xl font-semibold leading-none tracking-tight text-white drop-shadow-sm", className)}
|
||||
{...restProps}
|
||||
/>
|
||||
|
||||
@@ -18,7 +18,7 @@
|
||||
bind:ref
|
||||
{sideOffset}
|
||||
class={cn(
|
||||
'bg-popover text-popover-foreground z-50 min-w-[8rem] overflow-hidden rounded-md border p-1 shadow-md',
|
||||
'bg-popover text-popover-foreground z-[100] min-w-[8rem] overflow-hidden rounded-md border p-1 shadow-md',
|
||||
'data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 outline-none',
|
||||
'bg-white/10 backdrop-blur-md transition hover:bg-white/15 text-foreground border border-white/20 shadow-lg',
|
||||
className
|
||||
|
||||
7
src/lib/components/ui/input/index.ts
Normal file
7
src/lib/components/ui/input/index.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
import Root from "./input.svelte";
|
||||
|
||||
export {
|
||||
Root,
|
||||
//
|
||||
Root as Input,
|
||||
};
|
||||
57
src/lib/components/ui/input/input.svelte
Normal file
57
src/lib/components/ui/input/input.svelte
Normal file
@@ -0,0 +1,57 @@
|
||||
<script lang="ts">
|
||||
import type { HTMLInputAttributes, HTMLInputTypeAttribute } from "svelte/elements";
|
||||
import type { WithElementRef } from "bits-ui";
|
||||
import { cn } from "$lib/utils.js";
|
||||
|
||||
type InputType = Exclude<HTMLInputTypeAttribute, "file">;
|
||||
|
||||
type Props = WithElementRef<
|
||||
Omit<HTMLInputAttributes, "type"> &
|
||||
({ type: "file"; files?: FileList } | { type?: InputType; files?: undefined })
|
||||
>;
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
value = $bindable(),
|
||||
type,
|
||||
files = $bindable(),
|
||||
class: className,
|
||||
...restProps
|
||||
}: Props = $props();
|
||||
</script>
|
||||
|
||||
{#if type === "file"}
|
||||
<input
|
||||
bind:this={ref}
|
||||
class={cn(
|
||||
"flex h-10 w-full rounded-lg px-3 py-2 text-sm transition-all duration-200",
|
||||
"bg-white/[0.06] dark:bg-black/20 backdrop-blur-sm",
|
||||
"border border-white/[0.1] dark:border-white/[0.08]",
|
||||
"placeholder:text-white/40 text-white",
|
||||
"focus:outline-none focus:ring-2 focus:ring-white/20 focus:border-white/20",
|
||||
"disabled:cursor-not-allowed disabled:opacity-50",
|
||||
"file:border-0 file:bg-transparent file:text-sm file:font-medium",
|
||||
className
|
||||
)}
|
||||
type="file"
|
||||
bind:files
|
||||
bind:value
|
||||
{...restProps}
|
||||
/>
|
||||
{:else}
|
||||
<input
|
||||
bind:this={ref}
|
||||
class={cn(
|
||||
"flex h-10 w-full rounded-lg px-3 py-2 text-sm transition-all duration-200",
|
||||
"bg-white/[0.06] dark:bg-black/20 backdrop-blur-sm",
|
||||
"border border-white/[0.1] dark:border-white/[0.08]",
|
||||
"placeholder:text-white/40 text-white",
|
||||
"focus:outline-none focus:ring-2 focus:ring-white/20 focus:border-white/20",
|
||||
"disabled:cursor-not-allowed disabled:opacity-50",
|
||||
className
|
||||
)}
|
||||
{type}
|
||||
bind:value
|
||||
{...restProps}
|
||||
/>
|
||||
{/if}
|
||||
7
src/lib/components/ui/label/index.ts
Normal file
7
src/lib/components/ui/label/index.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
import Root from "./label.svelte";
|
||||
|
||||
export {
|
||||
Root,
|
||||
//
|
||||
Root as Label,
|
||||
};
|
||||
19
src/lib/components/ui/label/label.svelte
Normal file
19
src/lib/components/ui/label/label.svelte
Normal file
@@ -0,0 +1,19 @@
|
||||
<script lang="ts">
|
||||
import { Label as LabelPrimitive } from "bits-ui";
|
||||
import { cn } from "$lib/utils.js";
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
class: className,
|
||||
...restProps
|
||||
}: LabelPrimitive.RootProps = $props();
|
||||
</script>
|
||||
|
||||
<LabelPrimitive.Root
|
||||
bind:ref
|
||||
class={cn(
|
||||
"text-sm font-medium leading-none text-white/90 peer-disabled:cursor-not-allowed peer-disabled:opacity-50",
|
||||
className
|
||||
)}
|
||||
{...restProps}
|
||||
/>
|
||||
@@ -39,7 +39,7 @@ get along, so we shut typescript up by casting `value` to `never`.
|
||||
index={thumb}
|
||||
class={cn(
|
||||
"border-primary/50 bg-background focus-visible:ring-ring block size-4 rounded-full border shadow transition-colors focus-visible:outline-none focus-visible:ring-1 disabled:pointer-events-none disabled:opacity-50",
|
||||
"bg-white/10 backdrop-blur-md hover:bg-white/15 text-foreground border border-white/20 shadow-lg translate-z-0 relative z-20",
|
||||
"bg-foreground backdrop-blur-md hover:bg-foreground/85 text-foreground border border-foreground/20 shadow-lg translate-z-0 relative z-20",
|
||||
)}
|
||||
/>
|
||||
{/each}
|
||||
|
||||
7
src/lib/components/ui/switch/index.ts
Normal file
7
src/lib/components/ui/switch/index.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
import Root from "./switch.svelte";
|
||||
|
||||
export {
|
||||
Root,
|
||||
//
|
||||
Root as Switch,
|
||||
};
|
||||
27
src/lib/components/ui/switch/switch.svelte
Normal file
27
src/lib/components/ui/switch/switch.svelte
Normal file
@@ -0,0 +1,27 @@
|
||||
<script lang="ts">
|
||||
import { Switch as SwitchPrimitive, type WithoutChildrenOrChild } from "bits-ui";
|
||||
import { cn } from "$lib/utils.js";
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
checked = $bindable(false),
|
||||
class: className,
|
||||
...restProps
|
||||
}: WithoutChildrenOrChild<SwitchPrimitive.RootProps> = $props();
|
||||
</script>
|
||||
|
||||
<SwitchPrimitive.Root
|
||||
bind:ref
|
||||
bind:checked
|
||||
class={cn(
|
||||
"focus-visible:ring-ring focus-visible:ring-offset-background data-[state=checked]:bg-primary data-[state=unchecked]:bg-input peer inline-flex h-5 w-9 shrink-0 cursor-pointer items-center rounded-full border-2 border-transparent shadow-sm transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50",
|
||||
className
|
||||
)}
|
||||
{...restProps}
|
||||
>
|
||||
<SwitchPrimitive.Thumb
|
||||
class={cn(
|
||||
"bg-background pointer-events-none block size-4 rounded-full shadow-lg ring-0 transition-transform data-[state=checked]:translate-x-4 data-[state=unchecked]:translate-x-0"
|
||||
)}
|
||||
/>
|
||||
</SwitchPrimitive.Root>
|
||||
@@ -112,8 +112,8 @@
|
||||
{#if show}
|
||||
<div
|
||||
bind:this={windowRef}
|
||||
class="fixed flex flex-col bg-white/10 backdrop-blur-md border border-white/20 shadow-lg rounded-lg overflow-hidden"
|
||||
style="width: {width}px; height: {height}px; left: {x}px; top: {y}px; z-index: {zIndex};"
|
||||
class="fixed flex flex-col overflow-hidden rounded-2xl border border-white/[0.1] bg-black/[0.48] text-white shadow-[0_8px_32px_rgba(0,0,0,0.32),inset_0_1px_0_rgba(255,255,255,0.1)] backdrop-blur-2xl before:pointer-events-none before:absolute before:inset-0 before:rounded-2xl before:bg-gradient-to-br before:from-white/[0.08] before:to-transparent"
|
||||
style="width: {width}px; height: {height}px; left: {x}px; top: {y}px; z-index: {zIndex}; --foreground: 0 0% 98%; --muted-foreground: 0 0% 72%; --text-color: rgba(255, 255, 255, 0.92); --text-shadow: 0 1px 2px rgba(0, 0, 0, 0.3);"
|
||||
onmousedown={handleMouseDown}
|
||||
role="dialog"
|
||||
tabindex="0"
|
||||
@@ -121,14 +121,14 @@
|
||||
{#if showTitleBar}
|
||||
<div
|
||||
bind:this={headerRef}
|
||||
class="h-8 px-3 flex items-center justify-between bg-black/10 border-b border-white/10 select-none"
|
||||
class="relative z-10 flex h-8 items-center justify-between border-b border-white/[0.08] bg-black/10 px-3 select-none"
|
||||
style="cursor: {isDragging ? 'grabbing' : 'grab'};"
|
||||
>
|
||||
<span class="text-sm font-medium text-white/90">{title}</span>
|
||||
{#if showCloseButton}
|
||||
<button
|
||||
onclick={onClose}
|
||||
class="w-5 h-5 flex items-center justify-center text-white/70 hover:text-white hover:bg-red-500/50 rounded-sm transition-colors"
|
||||
class="flex h-5 w-5 items-center justify-center rounded-sm text-white/70 transition-colors hover:bg-red-500/45 hover:text-white"
|
||||
aria-label="Close window"
|
||||
>
|
||||
<X class="size-4" />
|
||||
@@ -137,8 +137,8 @@
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<div class="flex-1 p-1 overflow-auto bg-transparent" role="dialog" tabindex="0">
|
||||
<div class="relative z-10 flex-1 overflow-auto bg-transparent p-1" role="dialog" tabindex="0">
|
||||
{@render children?.()}
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
{/if}
|
||||
|
||||
@@ -1 +1 @@
|
||||
// place files you want to import through the `$lib` alias in this folder.
|
||||
export { authClient } from './auth-client';
|
||||
|
||||
234
src/lib/server/auth.ts
Normal file
234
src/lib/server/auth.ts
Normal file
@@ -0,0 +1,234 @@
|
||||
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 { dev } from '$app/environment';
|
||||
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 generateOpaqueIdentifier = () => `${crypto.randomUUID()}@internal.invalid`;
|
||||
|
||||
const getAnonymousDisplayName = (name?: string | null) => {
|
||||
const trimmedName = name?.trim();
|
||||
return trimmedName ? trimmedName : 'Chillhop listener';
|
||||
};
|
||||
|
||||
type TurnstileVerifyResult = {
|
||||
success: boolean;
|
||||
hostname?: string;
|
||||
['error-codes']?: string[];
|
||||
};
|
||||
|
||||
const getClientIpAddress = () => {
|
||||
const headers = getRequestEvent().request.headers;
|
||||
return headers.get('CF-Connecting-IP') ?? headers.get('X-Forwarded-For') ?? undefined;
|
||||
};
|
||||
|
||||
const verifyTurnstileToken = async (token: string) => {
|
||||
const secretKey = dev ? '1x0000000000000000000000000000000AA' : env.TURNSTILE_SECRET_KEY;
|
||||
|
||||
if (!secretKey) {
|
||||
throw new APIError('INTERNAL_SERVER_ERROR', {
|
||||
message: 'Turnstile secret key is not configured',
|
||||
});
|
||||
}
|
||||
|
||||
const verificationBody = new FormData();
|
||||
verificationBody.set('secret', secretKey);
|
||||
verificationBody.set('response', token);
|
||||
|
||||
const remoteIp = getClientIpAddress();
|
||||
if (remoteIp) {
|
||||
verificationBody.set('remoteip', remoteIp);
|
||||
}
|
||||
|
||||
verificationBody.set('idempotency_key', crypto.randomUUID());
|
||||
|
||||
const response = await fetch('https://challenges.cloudflare.com/turnstile/v0/siteverify', {
|
||||
method: 'POST',
|
||||
body: verificationBody,
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new APIError('BAD_REQUEST', {
|
||||
message: 'Turnstile verification failed. Please try again.',
|
||||
});
|
||||
}
|
||||
|
||||
const result = (await response.json()) as TurnstileVerifyResult;
|
||||
|
||||
if (!result.success) {
|
||||
throw new APIError('BAD_REQUEST', {
|
||||
message:
|
||||
result['error-codes']?.includes('timeout-or-duplicate')
|
||||
? 'Turnstile check expired. Please try again.'
|
||||
: 'Turnstile verification failed. Please try again.',
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const createAnonymousSession = async (ctx: any, name?: string | null) => {
|
||||
const user = await ctx.context.internalAdapter.createUser({
|
||||
email: generateOpaqueIdentifier(),
|
||||
emailVerified: false,
|
||||
isAnonymous: true,
|
||||
name: getAnonymousDisplayName(name),
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date(),
|
||||
});
|
||||
|
||||
if (!user) {
|
||||
throw new APIError('INTERNAL_SERVER_ERROR', {
|
||||
message: 'Failed to create user',
|
||||
});
|
||||
}
|
||||
|
||||
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 } as Parameters<typeof setSessionCookie>[1],
|
||||
);
|
||||
|
||||
return {
|
||||
token: session.token,
|
||||
user,
|
||||
};
|
||||
};
|
||||
|
||||
const accountNumber = () =>
|
||||
({
|
||||
id: 'account-number',
|
||||
endpoints: {
|
||||
createAccount: createAuthEndpoint(
|
||||
'/create-account',
|
||||
{
|
||||
method: 'POST',
|
||||
body: z.object({
|
||||
name: z.string().trim().max(100).optional(),
|
||||
turnstileToken: z.string().min(1),
|
||||
}),
|
||||
},
|
||||
async (ctx) => {
|
||||
await verifyTurnstileToken(ctx.body.turnstileToken);
|
||||
return ctx.json(await createAnonymousSession(ctx, ctx.body.name));
|
||||
},
|
||||
),
|
||||
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 (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 } as Parameters<typeof setSessionCookie>[1],
|
||||
);
|
||||
|
||||
return ctx.json({
|
||||
token: session.token,
|
||||
user,
|
||||
});
|
||||
},
|
||||
),
|
||||
},
|
||||
}) satisfies BetterAuthPlugin;
|
||||
|
||||
const createAuthConfig = (baseURL = env.ORIGIN) =>
|
||||
({
|
||||
baseURL,
|
||||
secret: env.BETTER_AUTH_SECRET,
|
||||
user: {
|
||||
additionalFields: {
|
||||
accountNumber: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
input: false,
|
||||
unique: true,
|
||||
fieldName: 'account_number',
|
||||
defaultValue: generateAccountNumber,
|
||||
},
|
||||
statisticsOptOut: {
|
||||
type: 'boolean',
|
||||
required: true,
|
||||
input: false,
|
||||
fieldName: 'statistics_opt_out',
|
||||
defaultValue: false,
|
||||
},
|
||||
},
|
||||
},
|
||||
logger: {
|
||||
level: 'debug',
|
||||
},
|
||||
onAPIError: {
|
||||
onError(error: unknown) {
|
||||
console.error('Better Auth API error', error);
|
||||
},
|
||||
},
|
||||
plugins: [
|
||||
anonymous({
|
||||
generateName: () => getAnonymousDisplayName(),
|
||||
generateRandomEmail: generateOpaqueIdentifier,
|
||||
}),
|
||||
accountNumber(),
|
||||
passkey({
|
||||
rpID: new URL(baseURL).hostname,
|
||||
rpName: 'Chillhop',
|
||||
}),
|
||||
sveltekitCookies(getRequestEvent), // make sure this is the last plugin in the array
|
||||
],
|
||||
}) satisfies Omit<Parameters<typeof betterAuth>[0], 'database'>;
|
||||
|
||||
export const createAuth = (d1: D1Database, baseURL = env.ORIGIN) =>
|
||||
betterAuth({
|
||||
...createAuthConfig(baseURL),
|
||||
database: drizzleAdapter(getDb(d1), { provider: 'sqlite' }),
|
||||
});
|
||||
|
||||
/**
|
||||
* DO NOT USE!
|
||||
*
|
||||
* This instance is used by the `better-auth` CLI for schema generation ONLY.
|
||||
* To access `auth` at runtime, use `event.locals.auth`.
|
||||
*/
|
||||
export const auth = createAuth(null!, 'http://localhost');
|
||||
141
src/lib/server/db/auth.schema.ts
Normal file
141
src/lib/server/db/auth.schema.ts
Normal file
@@ -0,0 +1,141 @@
|
||||
import { relations, sql } from "drizzle-orm";
|
||||
import { sqliteTable, text, integer, index } from "drizzle-orm/sqlite-core";
|
||||
|
||||
export const user = sqliteTable("user", {
|
||||
id: text("id").primaryKey(),
|
||||
name: text("name").notNull(),
|
||||
email: text("email").notNull().unique(),
|
||||
emailVerified: integer("email_verified", { mode: "boolean" })
|
||||
.default(false)
|
||||
.notNull(),
|
||||
image: text("image"),
|
||||
createdAt: integer("created_at", { mode: "timestamp_ms" })
|
||||
.default(sql`(cast(unixepoch('subsecond') * 1000 as integer))`)
|
||||
.notNull(),
|
||||
updatedAt: integer("updated_at", { mode: "timestamp_ms" })
|
||||
.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(),
|
||||
statisticsOptOut: integer("statistics_opt_out", { mode: "boolean" }).default(false).notNull(),
|
||||
});
|
||||
|
||||
export const session = sqliteTable(
|
||||
"session",
|
||||
{
|
||||
id: text("id").primaryKey(),
|
||||
expiresAt: integer("expires_at", { mode: "timestamp_ms" }).notNull(),
|
||||
token: text("token").notNull().unique(),
|
||||
createdAt: integer("created_at", { mode: "timestamp_ms" })
|
||||
.default(sql`(cast(unixepoch('subsecond') * 1000 as integer))`)
|
||||
.notNull(),
|
||||
updatedAt: integer("updated_at", { mode: "timestamp_ms" })
|
||||
.$onUpdate(() => /* @__PURE__ */ new Date())
|
||||
.notNull(),
|
||||
ipAddress: text("ip_address"),
|
||||
userAgent: text("user_agent"),
|
||||
userId: text("user_id")
|
||||
.notNull()
|
||||
.references(() => user.id, { onDelete: "cascade" }),
|
||||
},
|
||||
(table) => [index("session_userId_idx").on(table.userId)],
|
||||
);
|
||||
|
||||
export const account = sqliteTable(
|
||||
"account",
|
||||
{
|
||||
id: text("id").primaryKey(),
|
||||
accountId: text("account_id").notNull(),
|
||||
providerId: text("provider_id").notNull(),
|
||||
userId: text("user_id")
|
||||
.notNull()
|
||||
.references(() => user.id, { onDelete: "cascade" }),
|
||||
accessToken: text("access_token"),
|
||||
refreshToken: text("refresh_token"),
|
||||
idToken: text("id_token"),
|
||||
accessTokenExpiresAt: integer("access_token_expires_at", {
|
||||
mode: "timestamp_ms",
|
||||
}),
|
||||
refreshTokenExpiresAt: integer("refresh_token_expires_at", {
|
||||
mode: "timestamp_ms",
|
||||
}),
|
||||
scope: text("scope"),
|
||||
password: text("password"),
|
||||
createdAt: integer("created_at", { mode: "timestamp_ms" })
|
||||
.default(sql`(cast(unixepoch('subsecond') * 1000 as integer))`)
|
||||
.notNull(),
|
||||
updatedAt: integer("updated_at", { mode: "timestamp_ms" })
|
||||
.$onUpdate(() => /* @__PURE__ */ new Date())
|
||||
.notNull(),
|
||||
},
|
||||
(table) => [index("account_userId_idx").on(table.userId)],
|
||||
);
|
||||
|
||||
export const verification = sqliteTable(
|
||||
"verification",
|
||||
{
|
||||
id: text("id").primaryKey(),
|
||||
identifier: text("identifier").notNull(),
|
||||
value: text("value").notNull(),
|
||||
expiresAt: integer("expires_at", { mode: "timestamp_ms" }).notNull(),
|
||||
createdAt: integer("created_at", { mode: "timestamp_ms" })
|
||||
.default(sql`(cast(unixepoch('subsecond') * 1000 as integer))`)
|
||||
.notNull(),
|
||||
updatedAt: integer("updated_at", { mode: "timestamp_ms" })
|
||||
.default(sql`(cast(unixepoch('subsecond') * 1000 as integer))`)
|
||||
.$onUpdate(() => /* @__PURE__ */ new Date())
|
||||
.notNull(),
|
||||
},
|
||||
(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 }) => ({
|
||||
user: one(user, {
|
||||
fields: [session.userId],
|
||||
references: [user.id],
|
||||
}),
|
||||
}));
|
||||
|
||||
export const accountRelations = relations(account, ({ one }) => ({
|
||||
user: one(user, {
|
||||
fields: [account.userId],
|
||||
references: [user.id],
|
||||
}),
|
||||
}));
|
||||
|
||||
export const passkeyRelations = relations(passkey, ({ one }) => ({
|
||||
user: one(user, {
|
||||
fields: [passkey.userId],
|
||||
references: [user.id],
|
||||
}),
|
||||
}));
|
||||
5
src/lib/server/db/index.ts
Normal file
5
src/lib/server/db/index.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
import { drizzle } from 'drizzle-orm/d1';
|
||||
import type { D1Database } from '@cloudflare/workers-types';
|
||||
import * as schema from './schema';
|
||||
|
||||
export const getDb = (d1: D1Database) => drizzle(d1, { schema });
|
||||
39
src/lib/server/db/schema.ts
Normal file
39
src/lib/server/db/schema.ts
Normal file
@@ -0,0 +1,39 @@
|
||||
import { index, integer, sqliteTable, text, uniqueIndex } from 'drizzle-orm/sqlite-core';
|
||||
import { user } from './auth.schema';
|
||||
import { sql } from 'drizzle-orm';
|
||||
|
||||
export const userStatBucket = sqliteTable('user_stat_bucket', {
|
||||
id: text('id').primaryKey().$defaultFn(() => crypto.randomUUID()),
|
||||
userId: text('user_id').notNull().references(() => user.id, { onDelete: 'cascade' }),
|
||||
metric: text('metric').notNull(),
|
||||
value: integer('value').notNull(), // seconds, count, etc.
|
||||
bucketStart: integer('bucket_start', { mode: 'timestamp_ms' }).notNull(),
|
||||
stationId: integer('station_id').notNull().default(0),
|
||||
fileId: text('file_id').notNull().default(''),
|
||||
createdAt: integer('created_at', { mode: 'timestamp_ms' })
|
||||
.default(sql`(cast(unixepoch('subsecond') * 1000 as integer))`)
|
||||
.notNull(),
|
||||
}, (table) => [
|
||||
index('user_stat_bucket_user_metric_bucket_idx').on(table.userId, table.metric, table.bucketStart),
|
||||
uniqueIndex('user_stat_bucket_unique_idx').on(
|
||||
table.userId,
|
||||
table.metric,
|
||||
table.bucketStart,
|
||||
table.stationId,
|
||||
table.fileId,
|
||||
),
|
||||
]);
|
||||
|
||||
export const songIds = sqliteTable('song_ids', {
|
||||
id: text('id').primaryKey().$defaultFn(() => crypto.randomUUID()),
|
||||
fileId: text('file_id').notNull(),
|
||||
spotifyId: text('spotify_id'),
|
||||
title: text('title').notNull(),
|
||||
artists: text('artists').notNull(),
|
||||
image: text('image').notNull(),
|
||||
label: text('label'),
|
||||
}, (table) => [
|
||||
uniqueIndex('song_ids_file_id_idx').on(table.fileId),
|
||||
]);
|
||||
|
||||
export * from './auth.schema';
|
||||
@@ -31,6 +31,14 @@ export const state = $state({
|
||||
workRuleTimer: 20 * 60,
|
||||
restRuleTimer: 20,
|
||||
|
||||
showPomodoro: false,
|
||||
pomodoroTimer: 20 * 60,
|
||||
pomodoroBreakTimer: 5 * 60,
|
||||
isPomodoroActive: false,
|
||||
pomodoroWorkPhase: true,
|
||||
|
||||
showStats: false,
|
||||
|
||||
// in daemon.svelte
|
||||
togglePlay: (() => {}) as () => void,
|
||||
});
|
||||
|
||||
@@ -1,18 +1,55 @@
|
||||
import type { CHSong, Song } from "@/types";
|
||||
import { getRequestEvent } from '$app/server';
|
||||
import { getDb } from '@/server/db';
|
||||
import { songIds } from '@/server/db/schema';
|
||||
import type { CHSong, Song } from '@/types';
|
||||
import { and, eq } from 'drizzle-orm';
|
||||
|
||||
|
||||
export async function getChillhopStation(id: number): Promise<Song[]> {
|
||||
const res = await fetch(`https://stream.chillhop.com/live/${id}`);
|
||||
const data = await res.json() as CHSong[];
|
||||
|
||||
const finalData = data.map(song => ({
|
||||
const data = (await res.json()) as CHSong[];
|
||||
|
||||
const finalData = data.map((song) => ({
|
||||
fileId: String(song.fileId),
|
||||
artists: song.artists,
|
||||
title: song.title,
|
||||
endpoint: `https://stream.chillhop.com/mp3/${song.fileId}`,
|
||||
endpoint: `/api/chstream/${song.fileId}`,
|
||||
image: song.image,
|
||||
label: 'Chillhop Music',
|
||||
spotifyId: song.spotifyId,
|
||||
duration: song.duration,
|
||||
duration: song.duration ?? 0,
|
||||
})) as Song[];
|
||||
|
||||
// should not await because it doesn't need to be done before returning the data
|
||||
for (const song of finalData) {
|
||||
analyticsData(song).catch((err) => {
|
||||
console.error('Failed to store analytics data for song:', song.title, err);
|
||||
});
|
||||
}
|
||||
|
||||
return finalData;
|
||||
}
|
||||
|
||||
async function analyticsData(song: Song) {
|
||||
const db = getRequestDb();
|
||||
const existingSong = await db.select().from(songIds).where(and(eq(songIds.title, song.title), eq(songIds.artists, song.artists))).get();
|
||||
|
||||
if (existingSong) return;
|
||||
|
||||
await db.insert(songIds).values({
|
||||
fileId: song.fileId,
|
||||
spotifyId: song.spotifyId,
|
||||
title: song.title,
|
||||
artists: song.artists,
|
||||
image: song.image,
|
||||
label: song.label,
|
||||
});
|
||||
}
|
||||
|
||||
function getRequestDb() {
|
||||
const d1 = getRequestEvent().platform?.env.DB;
|
||||
if (!d1) {
|
||||
throw new Error('D1 binding "DB" not found');
|
||||
}
|
||||
return getDb(d1);
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
export interface Song {
|
||||
fileId: string;
|
||||
artists: string;
|
||||
title: string;
|
||||
endpoint: string;
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
import BackgroundAnalyzer from '@/components/app/bg-analyzer.svelte';
|
||||
import Title from '@/components/app/title.svelte';
|
||||
import LeftBar from '@/components/app/left-bar.svelte';
|
||||
import AuthBar from '@/components/app/auth-bar.svelte';
|
||||
</script>
|
||||
|
||||
<BgImage />
|
||||
@@ -20,7 +21,7 @@
|
||||
</div>
|
||||
{:else if state.isLoading && state.hasInteracted}
|
||||
<div class="flex flex-col h-screen w-full items-center justify-center space-y-2">
|
||||
<Spinner class="size-10" />
|
||||
<Spinner class="size-10 animate-spin" />
|
||||
<p>Loading...</p>
|
||||
</div>
|
||||
{:else if state.error}
|
||||
@@ -29,6 +30,7 @@
|
||||
</div>
|
||||
{:else if state.hasInteracted}
|
||||
<Title />
|
||||
<AuthBar />
|
||||
<LeftBar />
|
||||
<BottomBar />
|
||||
{/if}
|
||||
|
||||
28
src/routes/api/account/statistics-opt-out/+server.ts
Normal file
28
src/routes/api/account/statistics-opt-out/+server.ts
Normal file
@@ -0,0 +1,28 @@
|
||||
import { json } from '@sveltejs/kit';
|
||||
import { eq } from 'drizzle-orm';
|
||||
import { getDb } from '@/server/db';
|
||||
import { user as userTable } from '@/server/db/schema';
|
||||
import type { RequestHandler } from './$types';
|
||||
|
||||
type StatisticsOptOutBody = {
|
||||
statisticsOptOut?: unknown;
|
||||
};
|
||||
|
||||
export const PATCH: RequestHandler = async (event) => {
|
||||
const user = event.locals.user;
|
||||
if (!user) {
|
||||
return json({ error: 'Unauthorized' }, { status: 401 });
|
||||
}
|
||||
|
||||
const body = (await event.request.json().catch(() => ({}))) as StatisticsOptOutBody;
|
||||
if (typeof body.statisticsOptOut !== 'boolean') {
|
||||
return json({ error: 'Invalid statistics preference' }, { status: 400 });
|
||||
}
|
||||
|
||||
await getDb(event.platform!.env.DB)
|
||||
.update(userTable)
|
||||
.set({ statisticsOptOut: body.statisticsOptOut, updatedAt: new Date() })
|
||||
.where(eq(userTable.id, user.id));
|
||||
|
||||
return json({ statisticsOptOut: body.statisticsOptOut });
|
||||
};
|
||||
57
src/routes/api/chstream/[fileId]/+server.ts
Normal file
57
src/routes/api/chstream/[fileId]/+server.ts
Normal file
@@ -0,0 +1,57 @@
|
||||
// overengineered proxy brought to you by codex.
|
||||
import type { RequestHandler } from '@sveltejs/kit';
|
||||
|
||||
const FILE_ID_PATTERN = /^[A-Za-z0-9_-]+$/;
|
||||
|
||||
function isValidFileId(fileId: string) {
|
||||
return FILE_ID_PATTERN.test(fileId);
|
||||
}
|
||||
|
||||
export const GET: RequestHandler = async ({ params, request, fetch }) => {
|
||||
const { fileId } = params;
|
||||
|
||||
if (!fileId || !isValidFileId(fileId)) {
|
||||
return new Response('Invalid file ID', { status: 400 });
|
||||
}
|
||||
|
||||
const upstreamHeaders = new Headers();
|
||||
const range = request.headers.get('range');
|
||||
|
||||
if (range) {
|
||||
upstreamHeaders.set('range', range);
|
||||
}
|
||||
|
||||
const upstreamResponse = await fetch(`https://stream.chillhop.com/mp3/${fileId}`, {
|
||||
headers: upstreamHeaders,
|
||||
});
|
||||
|
||||
if (!upstreamResponse.ok && upstreamResponse.status !== 206) {
|
||||
return new Response('File not found', { status: upstreamResponse.status === 404 ? 404 : 502 });
|
||||
}
|
||||
|
||||
const responseHeaders = new Headers();
|
||||
const headersToForward = [
|
||||
'content-type',
|
||||
'content-length',
|
||||
'content-range',
|
||||
'accept-ranges',
|
||||
'etag',
|
||||
'last-modified',
|
||||
'cache-control',
|
||||
];
|
||||
|
||||
for (const header of headersToForward) {
|
||||
const value = upstreamResponse.headers.get(header);
|
||||
if (value) {
|
||||
responseHeaders.set(header, value);
|
||||
}
|
||||
}
|
||||
|
||||
responseHeaders.set('Content-Disposition', `inline; filename="${fileId}.mp3"`);
|
||||
|
||||
return new Response(upstreamResponse.body, {
|
||||
status: upstreamResponse.status,
|
||||
statusText: upstreamResponse.statusText,
|
||||
headers: responseHeaders,
|
||||
});
|
||||
};
|
||||
74
src/routes/api/listen/+server.ts
Normal file
74
src/routes/api/listen/+server.ts
Normal file
@@ -0,0 +1,74 @@
|
||||
import { json } from '@sveltejs/kit';
|
||||
import { sql } from 'drizzle-orm';
|
||||
import { getDb } from '@/server/db';
|
||||
import { userStatBucket } from '@/server/db/schema';
|
||||
import type { RequestHandler } from './$types';
|
||||
|
||||
type ListenBody = {
|
||||
fileId?: unknown;
|
||||
stationId?: unknown;
|
||||
seconds?: unknown;
|
||||
};
|
||||
|
||||
const LISTEN_METRIC = 'listen_seconds';
|
||||
const BUCKET_SIZE_MS = 60 * 60 * 1000;
|
||||
|
||||
const getHourlyBucketStart = (date = new Date()) =>
|
||||
new Date(Math.floor(date.getTime() / BUCKET_SIZE_MS) * BUCKET_SIZE_MS);
|
||||
|
||||
const clampListenSeconds = (seconds: unknown) => {
|
||||
const parsed = typeof seconds === 'number' ? seconds : Number(seconds);
|
||||
|
||||
if (!Number.isFinite(parsed)) {
|
||||
return 30;
|
||||
}
|
||||
|
||||
return Math.max(1, Math.min(60, Math.round(parsed)));
|
||||
};
|
||||
|
||||
export const POST: RequestHandler = async (event) => {
|
||||
const user = event.locals.user;
|
||||
if (!user) {
|
||||
return json({ error: 'Unauthorized' }, { status: 401 });
|
||||
}
|
||||
|
||||
if (user.statisticsOptOut) {
|
||||
return json({ ok: true, skipped: true });
|
||||
}
|
||||
|
||||
const body = (await event.request.json().catch(() => ({}))) as ListenBody;
|
||||
const fileId = typeof body.fileId === 'string' ? body.fileId.trim() : '';
|
||||
const stationId = typeof body.stationId === 'number' ? body.stationId : Number(body.stationId);
|
||||
|
||||
if (!fileId || !Number.isInteger(stationId)) {
|
||||
return json({ error: 'Invalid listen payload' }, { status: 400 });
|
||||
}
|
||||
|
||||
const seconds = clampListenSeconds(body.seconds);
|
||||
const db = getDb(event.platform!.env.DB);
|
||||
|
||||
await db
|
||||
.insert(userStatBucket)
|
||||
.values({
|
||||
userId: user.id,
|
||||
metric: LISTEN_METRIC,
|
||||
value: seconds,
|
||||
bucketStart: getHourlyBucketStart(),
|
||||
stationId,
|
||||
fileId,
|
||||
})
|
||||
.onConflictDoUpdate({
|
||||
target: [
|
||||
userStatBucket.userId,
|
||||
userStatBucket.metric,
|
||||
userStatBucket.bucketStart,
|
||||
userStatBucket.stationId,
|
||||
userStatBucket.fileId,
|
||||
],
|
||||
set: {
|
||||
value: sql`${userStatBucket.value} + ${seconds}`,
|
||||
},
|
||||
});
|
||||
|
||||
return json({ ok: true });
|
||||
};
|
||||
67
src/routes/api/stats/+server.ts
Normal file
67
src/routes/api/stats/+server.ts
Normal file
@@ -0,0 +1,67 @@
|
||||
import { json } from '@sveltejs/kit';
|
||||
import { and, desc, eq, gte, sql } from 'drizzle-orm';
|
||||
import { getDb } from '@/server/db';
|
||||
import { songIds, userStatBucket } from '@/server/db/schema';
|
||||
import type { RequestHandler } from './$types';
|
||||
|
||||
const LISTEN_METRIC = 'listen_seconds';
|
||||
|
||||
const getUtcDayStart = (date = new Date()) =>
|
||||
new Date(Date.UTC(date.getUTCFullYear(), date.getUTCMonth(), date.getUTCDate()));
|
||||
|
||||
export const GET: RequestHandler = async (event) => {
|
||||
const user = event.locals.user;
|
||||
if (!user) {
|
||||
return json({ error: 'Unauthorized' }, { status: 401 });
|
||||
}
|
||||
|
||||
const db = getDb(event.platform!.env.DB);
|
||||
const todayStart = getUtcDayStart();
|
||||
const listenFilter = and(
|
||||
eq(userStatBucket.userId, user.id),
|
||||
eq(userStatBucket.metric, LISTEN_METRIC),
|
||||
);
|
||||
|
||||
const [totalListen] = await db
|
||||
.select({ seconds: sql<number>`coalesce(sum(${userStatBucket.value}), 0)` })
|
||||
.from(userStatBucket)
|
||||
.where(listenFilter);
|
||||
|
||||
const [todayListen] = await db
|
||||
.select({ seconds: sql<number>`coalesce(sum(${userStatBucket.value}), 0)` })
|
||||
.from(userStatBucket)
|
||||
.where(and(listenFilter, gte(userStatBucket.bucketStart, todayStart)));
|
||||
|
||||
const topSongs = await db
|
||||
.select({
|
||||
fileId: userStatBucket.fileId,
|
||||
title: songIds.title,
|
||||
artists: songIds.artists,
|
||||
image: songIds.image,
|
||||
seconds: sql<number>`sum(${userStatBucket.value})`,
|
||||
})
|
||||
.from(userStatBucket)
|
||||
.leftJoin(songIds, eq(userStatBucket.fileId, songIds.fileId))
|
||||
.where(listenFilter)
|
||||
.groupBy(userStatBucket.fileId, songIds.title, songIds.artists, songIds.image)
|
||||
.orderBy(desc(sql`sum(${userStatBucket.value})`))
|
||||
.limit(5);
|
||||
|
||||
const topStations = await db
|
||||
.select({
|
||||
stationId: userStatBucket.stationId,
|
||||
seconds: sql<number>`sum(${userStatBucket.value})`,
|
||||
})
|
||||
.from(userStatBucket)
|
||||
.where(listenFilter)
|
||||
.groupBy(userStatBucket.stationId)
|
||||
.orderBy(desc(sql`sum(${userStatBucket.value})`))
|
||||
.limit(5);
|
||||
|
||||
return json({
|
||||
totalSeconds: totalListen?.seconds ?? 0,
|
||||
todaySeconds: todayListen?.seconds ?? 0,
|
||||
topSongs,
|
||||
topStations,
|
||||
});
|
||||
};
|
||||
2
static/.assetsignore
Normal file
2
static/.assetsignore
Normal file
@@ -0,0 +1,2 @@
|
||||
_worker.js
|
||||
_routes.json
|
||||
@@ -6,13 +6,7 @@ const config = {
|
||||
// Consult https://svelte.dev/docs/kit/integrations
|
||||
// for more information about preprocessors
|
||||
preprocess: vitePreprocess(),
|
||||
|
||||
kit: {
|
||||
adapter: adapter(),
|
||||
alias: {
|
||||
'@/*': './src/lib/*',
|
||||
},
|
||||
},
|
||||
kit: { adapter: adapter(), alias: { '@/*': './src/lib/*' } }
|
||||
};
|
||||
|
||||
export default config;
|
||||
|
||||
@@ -9,11 +9,9 @@
|
||||
"skipLibCheck": true,
|
||||
"sourceMap": true,
|
||||
"strict": true,
|
||||
"moduleResolution": "bundler"
|
||||
"moduleResolution": "bundler",
|
||||
"types": [
|
||||
"./worker-configuration.d.ts"
|
||||
]
|
||||
}
|
||||
// Path aliases are handled by https://svelte.dev/docs/kit/configuration#alias
|
||||
// except $lib which is handled by https://svelte.dev/docs/kit/configuration#files
|
||||
//
|
||||
// If you want to overwrite includes/excludes, make sure to copy over the relevant includes/excludes
|
||||
// from the referenced tsconfig.json - TypeScript does not merge them in
|
||||
}
|
||||
|
||||
30
wrangler.jsonc
Normal file
30
wrangler.jsonc
Normal file
@@ -0,0 +1,30 @@
|
||||
{
|
||||
"$schema": "node_modules/wrangler/config-schema.json",
|
||||
"name": "lofi",
|
||||
"compatibility_date": "2026-03-17",
|
||||
"compatibility_flags": ["nodejs_compat"],
|
||||
"main": ".svelte-kit/cloudflare/_worker.js",
|
||||
"assets": {
|
||||
"binding": "ASSETS",
|
||||
"directory": ".svelte-kit/cloudflare",
|
||||
},
|
||||
"workers_dev": true,
|
||||
"preview_urls": true,
|
||||
"observability": {
|
||||
"enabled": true,
|
||||
},
|
||||
"routes": [
|
||||
{
|
||||
"pattern": "lofi.srizan.dev",
|
||||
"custom_domain": true,
|
||||
},
|
||||
],
|
||||
"d1_databases": [
|
||||
{
|
||||
"binding": "DB",
|
||||
"database_name": "lofi_db",
|
||||
"database_id": "c8b599fc-88ce-4b55-b386-bfffe122d5f8",
|
||||
"migrations_dir": "drizzle",
|
||||
},
|
||||
],
|
||||
}
|
||||
Reference in New Issue
Block a user