diff --git a/drizzle.config.pg.ts b/drizzle.config.pg.ts index 75b384e..663a9f7 100644 --- a/drizzle.config.pg.ts +++ b/drizzle.config.pg.ts @@ -5,13 +5,12 @@ dotenv.config(); let url = process.env.POSTGRES_URL; if (!url) { - //throw new Error('POSTGRES_URL is required for drizzle.config.pg.ts'); console.warn('[drizzle.config.pg.ts] POSTGRES_URL is not set; using a placeholder URL.'); url = 'postgresql://placeholder:placeholder@localhost:5432/placeholder'; } export default { - schema: './src/db/schema_postgres.ts', + schema: ['./src/db/schema_postgres.ts', './src/db/schema_auth_postgres.ts'], out: './drizzle/postgres', dialect: 'postgresql', dbCredentials: { diff --git a/drizzle.config.sqlite.ts b/drizzle.config.sqlite.ts index b2ea43e..74cc605 100644 --- a/drizzle.config.sqlite.ts +++ b/drizzle.config.sqlite.ts @@ -4,7 +4,7 @@ import * as dotenv from 'dotenv'; dotenv.config(); export default { - schema: './src/db/schema_sqlite.ts', + schema: ['./src/db/schema_sqlite.ts', './src/db/schema_auth_sqlite.ts'], out: './drizzle/sqlite', dialect: 'sqlite', dbCredentials: { diff --git a/drizzle/postgres/0000_solid_colossus.sql b/drizzle/postgres/0000_complex_ricochet.sql similarity index 86% rename from drizzle/postgres/0000_solid_colossus.sql rename to drizzle/postgres/0000_complex_ricochet.sql index aff2619..dbd9ea5 100644 --- a/drizzle/postgres/0000_solid_colossus.sql +++ b/drizzle/postgres/0000_complex_ricochet.sql @@ -1,19 +1,3 @@ -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" timestamp, - "refresh_token_expires_at" timestamp, - "scope" text, - "password" text, - "created_at" timestamp NOT NULL, - "updated_at" timestamp NOT NULL -); ---> statement-breakpoint CREATE TABLE "audiobook_chapters" ( "id" text NOT NULL, "book_id" text NOT NULL, @@ -71,30 +55,6 @@ CREATE TABLE "documents" ( CONSTRAINT "documents_id_user_id_pk" PRIMARY KEY("id","user_id") ); --> statement-breakpoint -CREATE TABLE "session" ( - "id" text PRIMARY KEY NOT NULL, - "expires_at" timestamp NOT NULL, - "token" text NOT NULL, - "created_at" timestamp NOT NULL, - "updated_at" timestamp NOT NULL, - "ip_address" text, - "user_agent" text, - "user_id" text NOT NULL, - CONSTRAINT "session_token_unique" UNIQUE("token") -); ---> statement-breakpoint -CREATE TABLE "user" ( - "id" text PRIMARY KEY NOT NULL, - "name" text NOT NULL, - "email" text NOT NULL, - "email_verified" boolean NOT NULL, - "image" text, - "created_at" timestamp NOT NULL, - "updated_at" timestamp NOT NULL, - "is_anonymous" boolean, - CONSTRAINT "user_email_unique" UNIQUE("email") -); ---> statement-breakpoint CREATE TABLE "user_document_progress" ( "user_id" text NOT NULL, "document_id" text NOT NULL, @@ -124,11 +84,53 @@ CREATE TABLE "user_tts_chars" ( CONSTRAINT "user_tts_chars_user_id_date_pk" PRIMARY KEY("user_id","date") ); --> statement-breakpoint +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" timestamp, + "refresh_token_expires_at" timestamp, + "scope" text, + "password" text, + "created_at" timestamp DEFAULT now() NOT NULL, + "updated_at" timestamp NOT NULL +); +--> statement-breakpoint +CREATE TABLE "session" ( + "id" text PRIMARY KEY NOT NULL, + "expires_at" timestamp NOT NULL, + "token" text NOT NULL, + "created_at" timestamp DEFAULT now() NOT NULL, + "updated_at" timestamp NOT NULL, + "ip_address" text, + "user_agent" text, + "user_id" text NOT NULL, + CONSTRAINT "session_token_unique" UNIQUE("token") +); +--> statement-breakpoint +CREATE TABLE "user" ( + "id" text PRIMARY KEY NOT NULL, + "name" text NOT NULL, + "email" text NOT NULL, + "email_verified" boolean DEFAULT false NOT NULL, + "image" text, + "created_at" timestamp DEFAULT now() NOT NULL, + "updated_at" timestamp DEFAULT now() NOT NULL, + "is_anonymous" boolean DEFAULT false, + CONSTRAINT "user_email_unique" UNIQUE("email") +); +--> statement-breakpoint CREATE TABLE "verification" ( "id" text PRIMARY KEY NOT NULL, "identifier" text NOT NULL, "value" text NOT NULL, - "updated_at" timestamp + "expires_at" timestamp NOT NULL, + "created_at" timestamp DEFAULT now() NOT NULL, + "updated_at" timestamp DEFAULT now() NOT NULL ); --> statement-breakpoint ALTER TABLE "account" ADD CONSTRAINT "account_user_id_user_id_fk" FOREIGN KEY ("user_id") REFERENCES "public"."user"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint @@ -137,4 +139,7 @@ CREATE INDEX "idx_document_previews_status_lease" ON "document_previews" USING b CREATE INDEX "idx_documents_user_id" ON "documents" USING btree ("user_id");--> statement-breakpoint CREATE INDEX "idx_documents_user_id_last_modified" ON "documents" USING btree ("user_id","last_modified");--> statement-breakpoint CREATE INDEX "idx_user_document_progress_user_id_updated_at" ON "user_document_progress" USING btree ("user_id","updated_at");--> statement-breakpoint -CREATE INDEX "idx_user_tts_chars_date" ON "user_tts_chars" USING btree ("date"); \ No newline at end of file +CREATE INDEX "idx_user_tts_chars_date" ON "user_tts_chars" USING btree ("date");--> statement-breakpoint +CREATE INDEX "account_userId_idx" ON "account" USING btree ("user_id");--> statement-breakpoint +CREATE INDEX "session_userId_idx" ON "session" USING btree ("user_id");--> statement-breakpoint +CREATE INDEX "verification_identifier_idx" ON "verification" USING btree ("identifier"); \ No newline at end of file diff --git a/drizzle/postgres/meta/0000_snapshot.json b/drizzle/postgres/meta/0000_snapshot.json index 7ceeb38..053d7d7 100644 --- a/drizzle/postgres/meta/0000_snapshot.json +++ b/drizzle/postgres/meta/0000_snapshot.json @@ -1,114 +1,9 @@ { - "id": "161f4dc9-0c06-4d77-87e5-3496ffad1b97", + "id": "1c067f9b-aa2c-4099-a3b0-021e1a1c7ecb", "prevId": "00000000-0000-0000-0000-000000000000", "version": "7", "dialect": "postgresql", "tables": { - "public.account": { - "name": "account", - "schema": "", - "columns": { - "id": { - "name": "id", - "type": "text", - "primaryKey": true, - "notNull": true - }, - "account_id": { - "name": "account_id", - "type": "text", - "primaryKey": false, - "notNull": true - }, - "provider_id": { - "name": "provider_id", - "type": "text", - "primaryKey": false, - "notNull": true - }, - "user_id": { - "name": "user_id", - "type": "text", - "primaryKey": false, - "notNull": true - }, - "access_token": { - "name": "access_token", - "type": "text", - "primaryKey": false, - "notNull": false - }, - "refresh_token": { - "name": "refresh_token", - "type": "text", - "primaryKey": false, - "notNull": false - }, - "id_token": { - "name": "id_token", - "type": "text", - "primaryKey": false, - "notNull": false - }, - "access_token_expires_at": { - "name": "access_token_expires_at", - "type": "timestamp", - "primaryKey": false, - "notNull": false - }, - "refresh_token_expires_at": { - "name": "refresh_token_expires_at", - "type": "timestamp", - "primaryKey": false, - "notNull": false - }, - "scope": { - "name": "scope", - "type": "text", - "primaryKey": false, - "notNull": false - }, - "password": { - "name": "password", - "type": "text", - "primaryKey": false, - "notNull": false - }, - "created_at": { - "name": "created_at", - "type": "timestamp", - "primaryKey": false, - "notNull": true - }, - "updated_at": { - "name": "updated_at", - "type": "timestamp", - "primaryKey": false, - "notNull": true - } - }, - "indexes": {}, - "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": {}, - "policies": {}, - "checkConstraints": {}, - "isRLSEnabled": false - }, "public.audiobook_chapters": { "name": "audiobook_chapters", "schema": "", @@ -512,158 +407,6 @@ "checkConstraints": {}, "isRLSEnabled": false }, - "public.session": { - "name": "session", - "schema": "", - "columns": { - "id": { - "name": "id", - "type": "text", - "primaryKey": true, - "notNull": true - }, - "expires_at": { - "name": "expires_at", - "type": "timestamp", - "primaryKey": false, - "notNull": true - }, - "token": { - "name": "token", - "type": "text", - "primaryKey": false, - "notNull": true - }, - "created_at": { - "name": "created_at", - "type": "timestamp", - "primaryKey": false, - "notNull": true - }, - "updated_at": { - "name": "updated_at", - "type": "timestamp", - "primaryKey": false, - "notNull": true - }, - "ip_address": { - "name": "ip_address", - "type": "text", - "primaryKey": false, - "notNull": false - }, - "user_agent": { - "name": "user_agent", - "type": "text", - "primaryKey": false, - "notNull": false - }, - "user_id": { - "name": "user_id", - "type": "text", - "primaryKey": false, - "notNull": true - } - }, - "indexes": {}, - "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": { - "session_token_unique": { - "name": "session_token_unique", - "nullsNotDistinct": false, - "columns": [ - "token" - ] - } - }, - "policies": {}, - "checkConstraints": {}, - "isRLSEnabled": false - }, - "public.user": { - "name": "user", - "schema": "", - "columns": { - "id": { - "name": "id", - "type": "text", - "primaryKey": true, - "notNull": true - }, - "name": { - "name": "name", - "type": "text", - "primaryKey": false, - "notNull": true - }, - "email": { - "name": "email", - "type": "text", - "primaryKey": false, - "notNull": true - }, - "email_verified": { - "name": "email_verified", - "type": "boolean", - "primaryKey": false, - "notNull": true - }, - "image": { - "name": "image", - "type": "text", - "primaryKey": false, - "notNull": false - }, - "created_at": { - "name": "created_at", - "type": "timestamp", - "primaryKey": false, - "notNull": true - }, - "updated_at": { - "name": "updated_at", - "type": "timestamp", - "primaryKey": false, - "notNull": true - }, - "is_anonymous": { - "name": "is_anonymous", - "type": "boolean", - "primaryKey": false, - "notNull": false - } - }, - "indexes": {}, - "foreignKeys": {}, - "compositePrimaryKeys": {}, - "uniqueConstraints": { - "user_email_unique": { - "name": "user_email_unique", - "nullsNotDistinct": false, - "columns": [ - "email" - ] - } - }, - "policies": {}, - "checkConstraints": {}, - "isRLSEnabled": false - }, "public.user_document_progress": { "name": "user_document_progress", "schema": "", @@ -875,6 +618,301 @@ "checkConstraints": {}, "isRLSEnabled": false }, + "public.account": { + "name": "account", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "account_id": { + "name": "account_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "provider_id": { + "name": "provider_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "access_token": { + "name": "access_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "refresh_token": { + "name": "refresh_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "id_token": { + "name": "id_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "access_token_expires_at": { + "name": "access_token_expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "refresh_token_expires_at": { + "name": "refresh_token_expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "scope": { + "name": "scope", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "password": { + "name": "password", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "account_userId_idx": { + "name": "account_userId_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "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": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.session": { + "name": "session", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "token": { + "name": "token", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "ip_address": { + "name": "ip_address", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "user_agent": { + "name": "user_agent", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "session_userId_idx": { + "name": "session_userId_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "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": { + "session_token_unique": { + "name": "session_token_unique", + "nullsNotDistinct": false, + "columns": [ + "token" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.user": { + "name": "user", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "email_verified": { + "name": "email_verified", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "image": { + "name": "image", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "is_anonymous": { + "name": "is_anonymous", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "user_email_unique": { + "name": "user_email_unique", + "nullsNotDistinct": false, + "columns": [ + "email" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, "public.verification": { "name": "verification", "schema": "", @@ -897,14 +935,44 @@ "primaryKey": false, "notNull": true }, + "expires_at": { + "name": "expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, "updated_at": { "name": "updated_at", "type": "timestamp", "primaryKey": false, - "notNull": false + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "verification_identifier_idx": { + "name": "verification_identifier_idx", + "columns": [ + { + "expression": "identifier", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} } }, - "indexes": {}, "foreignKeys": {}, "compositePrimaryKeys": {}, "uniqueConstraints": {}, diff --git a/drizzle/postgres/meta/_journal.json b/drizzle/postgres/meta/_journal.json index bd88253..174caec 100644 --- a/drizzle/postgres/meta/_journal.json +++ b/drizzle/postgres/meta/_journal.json @@ -5,8 +5,8 @@ { "idx": 0, "version": "7", - "when": 1770846273728, - "tag": "0000_solid_colossus", + "when": 1771184128245, + "tag": "0000_complex_ricochet", "breakpoints": true } ] diff --git a/drizzle/scripts/generate.mjs b/drizzle/scripts/generate.mjs index e112795..320c334 100644 --- a/drizzle/scripts/generate.mjs +++ b/drizzle/scripts/generate.mjs @@ -20,17 +20,58 @@ function loadEnvFiles() { loadEnvFiles(); -// if (!process.env.POSTGRES_URL) { -// console.error('[generate] POSTGRES_URL is required to generate postgres migrations.'); -// process.exit(1); -// } +// Ensure AUTH_SECRET and BASE_URL are set so Better Auth CLI can evaluate the config. +const env = { ...process.env }; +if (!env.AUTH_SECRET) env.AUTH_SECRET = 'generate-placeholder-secret-value-32chars!!'; +if (!env.BASE_URL) env.BASE_URL = 'http://localhost:3003'; const extraArgs = process.argv.slice(2); +// --------------------------------------------------------------------------- +// Step 1: Generate Better Auth schema files (one per dialect). +// +// The Better Auth CLI reads our auth config and produces a Drizzle schema file +// matching the adapter in use. We run it twice: +// - Without POSTGRES_URL → SQLite schema +// - With POSTGRES_URL → Postgres schema +// +// These files are checked in and should NOT be hand-edited. +// --------------------------------------------------------------------------- +console.log('\n--- Generating Better Auth schema (SQLite) ---'); +{ + const envSqlite = { ...env }; + delete envSqlite.POSTGRES_URL; // force SQLite adapter + const result = spawnSync('npx', [ + '@better-auth/cli', 'generate', + '--output', 'src/db/schema_auth_sqlite.ts', + '--yes', + ], { stdio: 'inherit', env: envSqlite }); + if ((result.status ?? 1) !== 0) process.exit(result.status ?? 1); +} + +console.log('\n--- Generating Better Auth schema (Postgres) ---'); +{ + const envPg = { ...env }; + // A placeholder URL is enough – the CLI doesn't connect, it just reads the config. + if (!envPg.POSTGRES_URL) { + envPg.POSTGRES_URL = 'postgresql://placeholder:placeholder@localhost:5432/placeholder'; + } + const result = spawnSync('npx', [ + '@better-auth/cli', 'generate', + '--output', 'src/db/schema_auth_postgres.ts', + '--yes', + ], { stdio: 'inherit', env: envPg }); + if ((result.status ?? 1) !== 0) process.exit(result.status ?? 1); +} + +// --------------------------------------------------------------------------- +// Step 2: Generate Drizzle migrations for both dialects. +// --------------------------------------------------------------------------- for (const configFile of ['drizzle.config.sqlite.ts', 'drizzle.config.pg.ts']) { + console.log(`\n--- Drizzle generate (${configFile}) ---`); const result = spawnSync('drizzle-kit', ['generate', '--config', configFile, ...extraArgs], { stdio: 'inherit', - env: process.env, + env, }); if ((result.status ?? 1) !== 0) { diff --git a/drizzle/sqlite/0000_cool_gwen_stacy.sql b/drizzle/sqlite/0000_eager_rick_jones.sql similarity index 83% rename from drizzle/sqlite/0000_cool_gwen_stacy.sql rename to drizzle/sqlite/0000_eager_rick_jones.sql index 0c37a89..0edbce5 100644 --- a/drizzle/sqlite/0000_cool_gwen_stacy.sql +++ b/drizzle/sqlite/0000_eager_rick_jones.sql @@ -1,20 +1,3 @@ -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 NOT NULL, - `updated_at` integer NOT NULL, - FOREIGN KEY (`user_id`) REFERENCES `user`(`id`) ON UPDATE no action ON DELETE cascade -); ---> statement-breakpoint CREATE TABLE `audiobook_chapters` ( `id` text NOT NULL, `book_id` text NOT NULL, @@ -75,31 +58,6 @@ CREATE TABLE `documents` ( --> statement-breakpoint CREATE INDEX `idx_documents_user_id` ON `documents` (`user_id`);--> statement-breakpoint CREATE INDEX `idx_documents_user_id_last_modified` ON `documents` (`user_id`,`last_modified`);--> statement-breakpoint -CREATE TABLE `session` ( - `id` text PRIMARY KEY NOT NULL, - `expires_at` integer NOT NULL, - `token` text NOT NULL, - `created_at` 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 TABLE `user` ( - `id` text PRIMARY KEY NOT NULL, - `name` text NOT NULL, - `email` text NOT NULL, - `email_verified` integer NOT NULL, - `image` text, - `created_at` integer NOT NULL, - `updated_at` integer NOT NULL, - `is_anonymous` integer -); ---> statement-breakpoint -CREATE UNIQUE INDEX `user_email_unique` ON `user` (`email`);--> statement-breakpoint CREATE TABLE `user_document_progress` ( `user_id` text NOT NULL, `document_id` text NOT NULL, @@ -131,9 +89,57 @@ CREATE TABLE `user_tts_chars` ( ); --> statement-breakpoint CREATE INDEX `idx_user_tts_chars_date` ON `user_tts_chars` (`date`);--> statement-breakpoint +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, + `is_anonymous` integer DEFAULT false +); +--> 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, - `updated_at` integer + `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`); \ No newline at end of file diff --git a/drizzle/sqlite/meta/0000_snapshot.json b/drizzle/sqlite/meta/0000_snapshot.json index 6ca5cca..aea1e12 100644 --- a/drizzle/sqlite/meta/0000_snapshot.json +++ b/drizzle/sqlite/meta/0000_snapshot.json @@ -1,124 +1,9 @@ { "version": "6", "dialect": "sqlite", - "id": "eb884adf-c9c7-494b-b0a1-3601f63bcbbe", + "id": "b45e86c4-7d98-4cfb-9b38-5d1423b17b5e", "prevId": "00000000-0000-0000-0000-000000000000", "tables": { - "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 - }, - "updated_at": { - "name": "updated_at", - "type": "integer", - "primaryKey": false, - "notNull": true, - "autoincrement": false - } - }, - "indexes": {}, - "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": {} - }, "audiobook_chapters": { "name": "audiobook_chapters", "columns": { @@ -517,168 +402,6 @@ "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 - }, - "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 - } - }, - "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 - }, - "image": { - "name": "image", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "created_at": { - "name": "created_at", - "type": "integer", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "updated_at": { - "name": "updated_at", - "type": "integer", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "is_anonymous": { - "name": "is_anonymous", - "type": "integer", - "primaryKey": false, - "notNull": false, - "autoincrement": false - } - }, - "indexes": { - "user_email_unique": { - "name": "user_email_unique", - "columns": [ - "email" - ], - "isUnique": true - } - }, - "foreignKeys": {}, - "compositePrimaryKeys": {}, - "uniqueConstraints": {}, - "checkConstraints": {} - }, "user_document_progress": { "name": "user_document_progress", "columns": { @@ -878,6 +601,304 @@ "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))" + }, + "is_anonymous": { + "name": "is_anonymous", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": false + } + }, + "indexes": { + "user_email_unique": { + "name": "user_email_unique", + "columns": [ + "email" + ], + "isUnique": true + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, "verification": { "name": "verification", "columns": { @@ -902,15 +923,39 @@ "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": false, - "autoincrement": false + "notNull": true, + "autoincrement": false, + "default": "(cast(unixepoch('subsecond') * 1000 as integer))" + } + }, + "indexes": { + "verification_identifier_idx": { + "name": "verification_identifier_idx", + "columns": [ + "identifier" + ], + "isUnique": false } }, - "indexes": {}, "foreignKeys": {}, "compositePrimaryKeys": {}, "uniqueConstraints": {}, diff --git a/drizzle/sqlite/meta/_journal.json b/drizzle/sqlite/meta/_journal.json index 4edffc4..710f047 100644 --- a/drizzle/sqlite/meta/_journal.json +++ b/drizzle/sqlite/meta/_journal.json @@ -5,8 +5,8 @@ { "idx": 0, "version": "6", - "when": 1770846273460, - "tag": "0000_cool_gwen_stacy", + "when": 1771184127967, + "tag": "0000_eager_rick_jones", "breakpoints": true } ] diff --git a/src/app/api/account/delete/route.ts b/src/app/api/account/delete/route.ts index abed1f5..fd7e0b0 100644 --- a/src/app/api/account/delete/route.ts +++ b/src/app/api/account/delete/route.ts @@ -1,9 +1,6 @@ import { headers } from 'next/headers'; import { NextResponse } from 'next/server'; import { auth } from '@/lib/server/auth'; -import { db } from '@/db'; -import { user } from '@/db/schema'; -import { eq } from 'drizzle-orm'; import { isAuthEnabled } from '@/lib/server/auth-config'; export async function DELETE() { @@ -20,9 +17,11 @@ export async function DELETE() { } try { - // Directly delete user from database - // Drizzle will handle cascade if configured in foreign keys, but typical cascading happens in DB engine - await db.delete(user).where(eq(user.id, session.user.id)); + // Use Better Auth's built-in deleteUser to handle cascading cleanup + await auth.api.deleteUser({ + headers: await headers(), + body: {}, + }); return NextResponse.json({ success: true }); } catch (error) { diff --git a/src/db/index.ts b/src/db/index.ts index d405189..556bd76 100644 --- a/src/db/index.ts +++ b/src/db/index.ts @@ -5,12 +5,13 @@ import Database from 'better-sqlite3'; import path from 'path'; import fs from 'fs'; import * as schema from './schema'; +import * as authSchemaSqlite from './schema_auth_sqlite'; +import * as authSchemaPostgres from './schema_auth_postgres'; -// Singleton logic not strictly needed if Next.js handles module caching, but good for safety // eslint-disable-next-line @typescript-eslint/no-explicit-any let dbInstance: any = null; -export function getDrizzleDB() { +function getDrizzleDB() { if (dbInstance) return dbInstance; if (process.env.POSTGRES_URL) { @@ -18,7 +19,7 @@ export function getDrizzleDB() { connectionString: process.env.POSTGRES_URL, ssl: process.env.NODE_ENV === "production" ? { rejectUnauthorized: false } : false, }); - dbInstance = drizzlePg(pool, { schema }); + dbInstance = drizzlePg(pool, { schema: { ...schema, ...authSchemaPostgres } }); } else { // Fallback to SQLite const dbPath = path.join(process.cwd(), 'docstore', 'sqlite3.db'); @@ -27,10 +28,20 @@ export function getDrizzleDB() { fs.mkdirSync(dir, { recursive: true }); } const sqlite = new Database(dbPath); - dbInstance = drizzleSqlite(sqlite, { schema }); + dbInstance = drizzleSqlite(sqlite, { schema: { ...schema, ...authSchemaSqlite } }); } return dbInstance; } -export const db = getDrizzleDB(); +// Lazy proxy: the actual DB connection is only opened on first property access. +// This prevents side effects (e.g. creating an empty sqlite3.db) when modules +// import `db` but never use it, such as during Better Auth CLI schema generation. +// eslint-disable-next-line @typescript-eslint/no-explicit-any +export const db: any = new Proxy({} as any, { + get(_target, prop, receiver) { + const instance = getDrizzleDB(); + const value = Reflect.get(instance, prop, receiver); + return typeof value === 'function' ? value.bind(instance) : value; + }, +}); diff --git a/src/db/schema.ts b/src/db/schema.ts index 726969b..429e5a2 100644 --- a/src/db/schema.ts +++ b/src/db/schema.ts @@ -3,14 +3,12 @@ import * as postgresSchema from './schema_postgres'; const usePostgres = !!process.env.POSTGRES_URL; -// Export the correct table variants dynamically +// Auth tables (user, session, account, verification) are managed by Better Auth +// and are NOT part of the Drizzle schema. Only app-specific tables are exported here. + export const documents = usePostgres ? postgresSchema.documents : sqliteSchema.documents; export const audiobooks = usePostgres ? postgresSchema.audiobooks : sqliteSchema.audiobooks; export const audiobookChapters = usePostgres ? postgresSchema.audiobookChapters : sqliteSchema.audiobookChapters; -export const user = usePostgres ? postgresSchema.user : sqliteSchema.user; -export const session = usePostgres ? postgresSchema.session : sqliteSchema.session; -export const account = usePostgres ? postgresSchema.account : sqliteSchema.account; -export const verification = usePostgres ? postgresSchema.verification : sqliteSchema.verification; export const userTtsChars = usePostgres ? postgresSchema.userTtsChars : sqliteSchema.userTtsChars; export const userPreferences = usePostgres ? postgresSchema.userPreferences : sqliteSchema.userPreferences; export const userDocumentProgress = usePostgres ? postgresSchema.userDocumentProgress : sqliteSchema.userDocumentProgress; diff --git a/src/db/schema_auth_postgres.ts b/src/db/schema_auth_postgres.ts new file mode 100644 index 0000000..8a71082 --- /dev/null +++ b/src/db/schema_auth_postgres.ts @@ -0,0 +1,94 @@ +import { relations } from "drizzle-orm"; +import { pgTable, text, timestamp, boolean, index } from "drizzle-orm/pg-core"; + +export const user = pgTable("user", { + id: text("id").primaryKey(), + name: text("name").notNull(), + email: text("email").notNull().unique(), + emailVerified: boolean("email_verified").default(false).notNull(), + image: text("image"), + createdAt: timestamp("created_at").defaultNow().notNull(), + updatedAt: timestamp("updated_at") + .defaultNow() + .$onUpdate(() => /* @__PURE__ */ new Date()) + .notNull(), + isAnonymous: boolean("is_anonymous").default(false), +}); + +export const session = pgTable( + "session", + { + id: text("id").primaryKey(), + expiresAt: timestamp("expires_at").notNull(), + token: text("token").notNull().unique(), + createdAt: timestamp("created_at").defaultNow().notNull(), + updatedAt: timestamp("updated_at") + .$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 = pgTable( + "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: timestamp("access_token_expires_at"), + refreshTokenExpiresAt: timestamp("refresh_token_expires_at"), + scope: text("scope"), + password: text("password"), + createdAt: timestamp("created_at").defaultNow().notNull(), + updatedAt: timestamp("updated_at") + .$onUpdate(() => /* @__PURE__ */ new Date()) + .notNull(), + }, + (table) => [index("account_userId_idx").on(table.userId)], +); + +export const verification = pgTable( + "verification", + { + id: text("id").primaryKey(), + identifier: text("identifier").notNull(), + value: text("value").notNull(), + expiresAt: timestamp("expires_at").notNull(), + createdAt: timestamp("created_at").defaultNow().notNull(), + updatedAt: timestamp("updated_at") + .defaultNow() + .$onUpdate(() => /* @__PURE__ */ new Date()) + .notNull(), + }, + (table) => [index("verification_identifier_idx").on(table.identifier)], +); + +export const userRelations = relations(user, ({ many }) => ({ + sessions: many(session), + accounts: many(account), +})); + +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], + }), +})); diff --git a/src/db/schema_auth_sqlite.ts b/src/db/schema_auth_sqlite.ts new file mode 100644 index 0000000..b18f93c --- /dev/null +++ b/src/db/schema_auth_sqlite.ts @@ -0,0 +1,108 @@ +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), +}); + +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 userRelations = relations(user, ({ many }) => ({ + sessions: many(session), + accounts: many(account), +})); + +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], + }), +})); diff --git a/src/db/schema_postgres.ts b/src/db/schema_postgres.ts index 2bc955e..6c7d9b8 100644 --- a/src/db/schema_postgres.ts +++ b/src/db/schema_postgres.ts @@ -1,4 +1,4 @@ -import { pgTable, text, integer, real, boolean, timestamp, date, bigint, primaryKey, index, jsonb } from 'drizzle-orm/pg-core'; +import { pgTable, text, integer, real, timestamp, date, bigint, primaryKey, index, jsonb } from 'drizzle-orm/pg-core'; export const documents = pgTable('documents', { id: text('id').notNull(), @@ -41,50 +41,9 @@ export const audiobookChapters = pgTable('audiobook_chapters', { pk: primaryKey({ columns: [table.id, table.userId] }), })); -export const user = pgTable("user", { - id: text("id").primaryKey(), - name: text('name').notNull(), - email: text('email').notNull().unique(), - emailVerified: boolean('email_verified').notNull(), - image: text('image'), - createdAt: timestamp('created_at').notNull(), - updatedAt: timestamp('updated_at').notNull(), - isAnonymous: boolean('is_anonymous'), -}); - -export const session = pgTable("session", { - id: text("id").primaryKey(), - expiresAt: timestamp('expires_at').notNull(), - token: text('token').notNull().unique(), - createdAt: timestamp('created_at').notNull(), - updatedAt: timestamp('updated_at').notNull(), - ipAddress: text('ip_address'), - userAgent: text('user_agent'), - userId: text('user_id').notNull().references(() => user.id, { onDelete: 'cascade' }) -}); - -export const account = pgTable("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: timestamp('access_token_expires_at'), - refreshTokenExpiresAt: timestamp('refresh_token_expires_at'), - scope: text('scope'), - password: text('password'), - createdAt: timestamp('created_at').notNull(), - updatedAt: timestamp('updated_at').notNull(), -}); - -export const verification = pgTable("verification", { - id: text("id").primaryKey(), - identifier: text('identifier').notNull(), - value: text('value').notNull(), - updatedAt: timestamp('updated_at'), -}); +// Auth tables (user, session, account, verification) are managed by Better Auth. +// They are created/migrated via `@better-auth/cli migrate` and should NOT be +// defined here. Only application-specific tables belong in this file. export const userTtsChars = pgTable("user_tts_chars", { userId: text('user_id').notNull(), diff --git a/src/db/schema_sqlite.ts b/src/db/schema_sqlite.ts index b82386c..e373121 100644 --- a/src/db/schema_sqlite.ts +++ b/src/db/schema_sqlite.ts @@ -42,50 +42,9 @@ export const audiobookChapters = sqliteTable('audiobook_chapters', { pk: primaryKey({ columns: [table.id, table.userId] }), })); -export const user = sqliteTable("user", { - id: text("id").primaryKey(), - name: text('name').notNull(), - email: text('email').notNull().unique(), - emailVerified: integer('email_verified', { mode: 'boolean' }).notNull(), - image: text('image'), - createdAt: integer('created_at', { mode: 'timestamp' }).notNull(), - updatedAt: integer('updated_at', { mode: 'timestamp' }).notNull(), - isAnonymous: integer('is_anonymous', { mode: 'boolean' }), -}); - -export const session = sqliteTable("session", { - id: text("id").primaryKey(), - expiresAt: integer('expires_at', { mode: 'timestamp' }).notNull(), - token: text('token').notNull().unique(), - createdAt: integer('created_at', { mode: 'timestamp' }).notNull(), - updatedAt: integer('updated_at', { mode: 'timestamp' }).notNull(), - ipAddress: text('ip_address'), - userAgent: text('user_agent'), - userId: text('user_id').notNull().references(() => user.id, { onDelete: 'cascade' }) -}); - -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' }), - refreshTokenExpiresAt: integer('refresh_token_expires_at', { mode: 'timestamp' }), - scope: text('scope'), - password: text('password'), - createdAt: integer('created_at', { mode: 'timestamp' }).notNull(), - updatedAt: integer('updated_at', { mode: 'timestamp' }).notNull(), -}); - -export const verification = sqliteTable("verification", { - id: text("id").primaryKey(), - identifier: text('identifier').notNull(), - value: text('value').notNull(), - updatedAt: integer('updated_at', { mode: 'timestamp' }), -}); +// Auth tables (user, session, account, verification) are managed by Better Auth. +// They are created/migrated via `@better-auth/cli migrate` and should NOT be +// defined here. Only application-specific tables belong in this file. export const userTtsChars = sqliteTable("user_tts_chars", { userId: text('user_id').notNull(), diff --git a/src/lib/server/auth.ts b/src/lib/server/auth.ts index 6617ae6..8851f70 100644 --- a/src/lib/server/auth.ts +++ b/src/lib/server/auth.ts @@ -7,6 +7,8 @@ import type { NextRequest } from 'next/server'; import { db } from "@/db"; import { rateLimiter } from "@/lib/server/rate-limiter"; import { isAuthEnabled, isAnonymousAuthSessionsEnabled } from "@/lib/server/auth-config"; +import * as authSchemaSqlite from "@/db/schema_auth_sqlite"; +import * as authSchemaPostgres from "@/db/schema_auth_postgres"; import { transferUserAudiobooks, transferUserDocuments, @@ -14,8 +16,6 @@ import { transferUserProgress, } from "@/lib/server/claim-data"; -import * as schema from "@/db/schema"; // Import the dynamic schema - // ... function tryGetOrigin(url: string | undefined): string | null { @@ -55,17 +55,13 @@ function envFlagEnabled(name: string, defaultValue: boolean): boolean { return defaultValue; } +const authSchema = process.env.POSTGRES_URL ? authSchemaPostgres : authSchemaSqlite; + const createAuth = () => betterAuth({ // eslint-disable-next-line @typescript-eslint/no-explicit-any database: drizzleAdapter(db as any, { - provider: process.env.POSTGRES_URL ? "pg" : "sqlite", // Dynamic provider - schema: { - ...schema, - user: schema.user, - session: schema.session, - account: schema.account, - verification: schema.verification, - } + provider: process.env.POSTGRES_URL ? "pg" : "sqlite", + schema: authSchema as Record, }), secret: process.env.AUTH_SECRET!, baseURL: process.env.BASE_URL!, @@ -78,6 +74,11 @@ const createAuth = () => betterAuth({ console.log("Password reset requested for:", data.user.email); }, }, + user: { + deleteUser: { + enabled: true, + }, + }, rateLimit: { // Better Auth built-in rate limiting is enabled by default. // Set DISABLE_AUTH_RATE_LIMIT=true to disable it.