From 34821bdb9c51377e0fa1b432b12563fff6a9544b Mon Sep 17 00:00:00 2001 From: debuggingdan Date: Thu, 22 Jan 2026 15:03:01 +0100 Subject: [PATCH 1/2] feat: integrate SQLite support and enhance authentication logic - Added SQLite database support by introducing a new database adapter that handles both SQLite and MySQL connection strings. - Updated the authentication initialization to utilize the new database adapter. - Modified the default database connection to use SQLite when no DB environment variable is set. - Enhanced the login page with improved form handling and error notifications. - Updated environment variables to include a flag for skipping automatic schema synchronization in production. --- .gitignore | 3 + next.config.ts | 2 +- package.json | 15 +- pnpm-lock.yaml | 103 ++++++++----- pnpm-workspace.yaml | 2 + scripts/start-server.mjs | 25 ++-- src/app/(auth)/login/page.tsx | 146 +++++++++++++++++-- src/app/(auth)/signup/page.tsx | 123 ++++++++++++++++ src/app/@sidebar/[...catchAll]/page.tsx | 1 + src/app/@sidebar/{default.tsx => page.tsx} | 2 +- src/app/api/v1/config/route.ts | 23 +++ src/components/layout.tsx | 5 +- src/components/molecules/page-breadcrumb.tsx | 6 +- src/lib/auth/better-auth.ts | 8 + src/lib/auth/config.ts | 27 +++- src/lib/database/index.ts | 13 +- src/lib/database/migrations/better-auth.ts | 32 ++++ src/lib/database/migrations/index.ts | 30 ++++ src/lib/environment.ts | 4 +- src/types/api/endpoints/get-auth-config.ts | 9 ++ src/types/api/endpoints/index.ts | 1 + 21 files changed, 497 insertions(+), 83 deletions(-) create mode 100644 src/app/(auth)/signup/page.tsx create mode 100644 src/app/@sidebar/[...catchAll]/page.tsx rename src/app/@sidebar/{default.tsx => page.tsx} (75%) create mode 100644 src/app/api/v1/config/route.ts create mode 100644 src/lib/auth/better-auth.ts create mode 100644 src/lib/database/migrations/better-auth.ts create mode 100644 src/lib/database/migrations/index.ts create mode 100644 src/types/api/endpoints/get-auth-config.ts diff --git a/.gitignore b/.gitignore index 5ef6a52..a16de15 100755 --- a/.gitignore +++ b/.gitignore @@ -39,3 +39,6 @@ yarn-error.log* # typescript *.tsbuildinfo next-env.d.ts + +db.sqlite +db.sqlite-* \ No newline at end of file diff --git a/next.config.ts b/next.config.ts index ff0c528..02c1b0f 100755 --- a/next.config.ts +++ b/next.config.ts @@ -2,7 +2,7 @@ import type { NextConfig } from 'next'; const nextConfig: NextConfig = { output: 'standalone', - serverExternalPackages: ['mysql2'], + serverExternalPackages: ['better-sqlite3', 'mysql2'], }; export default nextConfig; diff --git a/package.json b/package.json index 2f687df..cd34093 100755 --- a/package.json +++ b/package.json @@ -12,7 +12,7 @@ "lint:prettier": "prettier --check --log-level=warn src", "format": "prettier --write src && eslint --ext=js,jsx,ts,tsx --fix src", "lint:tsc": "tsc --noEmit", - "better-auth:generate": "npx @better-auth/cli generate --config ./dist/modules/auth/better-auth.js --output schema.sql", + "better-auth:generate": "npx @better-auth/cli generate --config ./src/lib/auth/better-auth.ts --output schema.sql", "scripts:clear-auth": "tsx scripts/clear-auth.ts" }, "dependencies": { @@ -28,14 +28,16 @@ "@tabler/icons-react": "^3.35.0", "axios": "^1.12.2", "better-auth": "^1.4.2", + "better-sqlite3": "^12.4.1", "dotenv": "^17.2.3", "envalid": "^8.1.0", + "mantine-form-zod-resolver": "^1.3.0", "mysql2": "^3.15.2", "nanostores": "^1.0.1", "next": "^15.5.9", "react": "19.1.0", "react-dom": "19.1.0", - "supersave": "^0.20.0", + "supersave": "1.0.0-beta4", "swr": "^2.3.6", "winston": "^3.18.3", "zod": "^4.1.12" @@ -46,10 +48,10 @@ "@eslint/eslintrc": "^3", "@next/eslint-plugin-next": "^15.5.6", "@tsconfig/strictest": "^2.0.6", + "@types/better-sqlite3": "^7.6.13", "@types/node": "^22.13.14", "@types/react": "^19", "@types/react-dom": "^19", - "better-sqlite3": "^12.4.1", "concurrently": "^9.2.1", "eslint": "^9", "eslint-config-next": "15.5.6", @@ -62,5 +64,10 @@ "rimraf": "^6.0.1", "typescript": "^5.9.3" }, - "packageManager": "pnpm@10.17.0" + "packageManager": "pnpm@10.17.0", + "pnpm": { + "onlyBuiltDependencies": [ + "better-sqlite3" + ] + } } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index fd4675b..398c241 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -48,12 +48,18 @@ importers: better-auth: specifier: ^1.4.2 version: 1.4.12(better-sqlite3@12.4.1)(mysql2@3.15.3)(next@15.5.9(@babel/core@7.28.4)(react-dom@19.1.0(react@19.1.0))(react@19.1.0))(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + better-sqlite3: + specifier: ^12.4.1 + version: 12.4.1 dotenv: specifier: ^17.2.3 version: 17.2.3 envalid: specifier: ^8.1.0 version: 8.1.0 + mantine-form-zod-resolver: + specifier: ^1.3.0 + version: 1.3.0(@mantine/form@8.3.5(react@19.1.0))(zod@4.1.12) mysql2: specifier: ^3.15.2 version: 3.15.3 @@ -70,8 +76,8 @@ importers: specifier: 19.1.0 version: 19.1.0(react@19.1.0) supersave: - specifier: ^0.20.0 - version: 0.20.0(better-sqlite3@12.4.1)(express@5.1.0)(mysql2@3.15.3) + specifier: 1.0.0-beta4 + version: 1.0.0-beta4(better-sqlite3@12.4.1)(mysql2@3.15.3) swr: specifier: ^2.3.6 version: 2.3.6(react@19.1.0) @@ -97,6 +103,9 @@ importers: '@tsconfig/strictest': specifier: ^2.0.6 version: 2.0.6 + '@types/better-sqlite3': + specifier: ^7.6.13 + version: 7.6.13 '@types/node': specifier: ^22.13.14 version: 22.18.12 @@ -106,9 +115,6 @@ importers: '@types/react-dom': specifier: ^19 version: 19.2.2(@types/react@19.2.2) - better-sqlite3: - specifier: ^12.4.1 - version: 12.4.1 concurrently: specifier: ^9.2.1 version: 9.2.1 @@ -533,49 +539,42 @@ packages: { integrity: sha512-I4RxkXU90cpufazhGPyVujYwfIm9Nk1QDEmiIsaPwdnm013F7RIceaCc87kAH+oUB1ezqEvC6ga4m7MSlqsJvQ== } cpu: [arm64] os: [linux] - libc: [glibc] '@img/sharp-libvips-linux-arm@1.2.3': resolution: { integrity: sha512-x1uE93lyP6wEwGvgAIV0gP6zmaL/a0tGzJs/BIDDG0zeBhMnuUPm7ptxGhUbcGs4okDJrk4nxgrmxpib9g6HpA== } cpu: [arm] os: [linux] - libc: [glibc] '@img/sharp-libvips-linux-ppc64@1.2.3': resolution: { integrity: sha512-Y2T7IsQvJLMCBM+pmPbM3bKT/yYJvVtLJGfCs4Sp95SjvnFIjynbjzsa7dY1fRJX45FTSfDksbTp6AGWudiyCg== } cpu: [ppc64] os: [linux] - libc: [glibc] '@img/sharp-libvips-linux-s390x@1.2.3': resolution: { integrity: sha512-RgWrs/gVU7f+K7P+KeHFaBAJlNkD1nIZuVXdQv6S+fNA6syCcoboNjsV2Pou7zNlVdNQoQUpQTk8SWDHUA3y/w== } cpu: [s390x] os: [linux] - libc: [glibc] '@img/sharp-libvips-linux-x64@1.2.3': resolution: { integrity: sha512-3JU7LmR85K6bBiRzSUc/Ff9JBVIFVvq6bomKE0e63UXGeRw2HPVEjoJke1Yx+iU4rL7/7kUjES4dZ/81Qjhyxg== } cpu: [x64] os: [linux] - libc: [glibc] '@img/sharp-libvips-linuxmusl-arm64@1.2.3': resolution: { integrity: sha512-F9q83RZ8yaCwENw1GieztSfj5msz7GGykG/BA+MOUefvER69K/ubgFHNeSyUu64amHIYKGDs4sRCMzXVj8sEyw== } cpu: [arm64] os: [linux] - libc: [musl] '@img/sharp-libvips-linuxmusl-x64@1.2.3': resolution: { integrity: sha512-U5PUY5jbc45ANM6tSJpsgqmBF/VsL6LnxJmIf11kB7J5DctHgqm0SkuXzVWtIY90GnJxKnC/JT251TDnk1fu/g== } cpu: [x64] os: [linux] - libc: [musl] '@img/sharp-linux-arm64@0.34.4': resolution: @@ -583,7 +582,6 @@ packages: engines: { node: ^18.17.0 || ^20.3.0 || >=21.0.0 } cpu: [arm64] os: [linux] - libc: [glibc] '@img/sharp-linux-arm@0.34.4': resolution: @@ -591,7 +589,6 @@ packages: engines: { node: ^18.17.0 || ^20.3.0 || >=21.0.0 } cpu: [arm] os: [linux] - libc: [glibc] '@img/sharp-linux-ppc64@0.34.4': resolution: @@ -599,7 +596,6 @@ packages: engines: { node: ^18.17.0 || ^20.3.0 || >=21.0.0 } cpu: [ppc64] os: [linux] - libc: [glibc] '@img/sharp-linux-s390x@0.34.4': resolution: @@ -607,7 +603,6 @@ packages: engines: { node: ^18.17.0 || ^20.3.0 || >=21.0.0 } cpu: [s390x] os: [linux] - libc: [glibc] '@img/sharp-linux-x64@0.34.4': resolution: @@ -615,7 +610,6 @@ packages: engines: { node: ^18.17.0 || ^20.3.0 || >=21.0.0 } cpu: [x64] os: [linux] - libc: [glibc] '@img/sharp-linuxmusl-arm64@0.34.4': resolution: @@ -623,7 +617,6 @@ packages: engines: { node: ^18.17.0 || ^20.3.0 || >=21.0.0 } cpu: [arm64] os: [linux] - libc: [musl] '@img/sharp-linuxmusl-x64@0.34.4': resolution: @@ -631,7 +624,6 @@ packages: engines: { node: ^18.17.0 || ^20.3.0 || >=21.0.0 } cpu: [x64] os: [linux] - libc: [musl] '@img/sharp-wasm32@0.34.4': resolution: @@ -786,7 +778,6 @@ packages: engines: { node: '>= 10' } cpu: [arm64] os: [linux] - libc: [glibc] '@next/swc-linux-arm64-musl@15.5.7': resolution: @@ -794,7 +785,6 @@ packages: engines: { node: '>= 10' } cpu: [arm64] os: [linux] - libc: [musl] '@next/swc-linux-x64-gnu@15.5.7': resolution: @@ -802,7 +792,6 @@ packages: engines: { node: '>= 10' } cpu: [x64] os: [linux] - libc: [glibc] '@next/swc-linux-x64-musl@15.5.7': resolution: @@ -810,7 +799,6 @@ packages: engines: { node: '>= 10' } cpu: [x64] os: [linux] - libc: [musl] '@next/swc-win32-arm64-msvc@15.5.7': resolution: @@ -1017,6 +1005,10 @@ packages: resolution: { integrity: sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg== } + '@types/better-sqlite3@7.6.13': + resolution: + { integrity: sha512-NMv9ASNARoKksWtsq/SHakpYAYnhBrQgGD8zkLYk/jaK8jUGn08CfEdTRgYhMypUQAfzSP8W6gNLe0q19/t4VA== } + '@types/conventional-commits-parser@5.0.1': resolution: { integrity: sha512-7uz5EHdzz2TqoMfV7ee61Egf5y6NkcO4FB/1iCCQnbeiI1F3xzv3vK5dBCXUCLQgGYS+mUeigK1iKQzvED+QnQ== } @@ -1207,56 +1199,48 @@ packages: { integrity: sha512-34gw7PjDGB9JgePJEmhEqBhWvCiiWCuXsL9hYphDF7crW7UgI05gyBAi6MF58uGcMOiOqSJ2ybEeCvHcq0BCmQ== } cpu: [arm64] os: [linux] - libc: [glibc] '@unrs/resolver-binding-linux-arm64-musl@1.11.1': resolution: { integrity: sha512-RyMIx6Uf53hhOtJDIamSbTskA99sPHS96wxVE/bJtePJJtpdKGXO1wY90oRdXuYOGOTuqjT8ACccMc4K6QmT3w== } cpu: [arm64] os: [linux] - libc: [musl] '@unrs/resolver-binding-linux-ppc64-gnu@1.11.1': resolution: { integrity: sha512-D8Vae74A4/a+mZH0FbOkFJL9DSK2R6TFPC9M+jCWYia/q2einCubX10pecpDiTmkJVUH+y8K3BZClycD8nCShA== } cpu: [ppc64] os: [linux] - libc: [glibc] '@unrs/resolver-binding-linux-riscv64-gnu@1.11.1': resolution: { integrity: sha512-frxL4OrzOWVVsOc96+V3aqTIQl1O2TjgExV4EKgRY09AJ9leZpEg8Ak9phadbuX0BA4k8U5qtvMSQQGGmaJqcQ== } cpu: [riscv64] os: [linux] - libc: [glibc] '@unrs/resolver-binding-linux-riscv64-musl@1.11.1': resolution: { integrity: sha512-mJ5vuDaIZ+l/acv01sHoXfpnyrNKOk/3aDoEdLO/Xtn9HuZlDD6jKxHlkN8ZhWyLJsRBxfv9GYM2utQ1SChKew== } cpu: [riscv64] os: [linux] - libc: [musl] '@unrs/resolver-binding-linux-s390x-gnu@1.11.1': resolution: { integrity: sha512-kELo8ebBVtb9sA7rMe1Cph4QHreByhaZ2QEADd9NzIQsYNQpt9UkM9iqr2lhGr5afh885d/cB5QeTXSbZHTYPg== } cpu: [s390x] os: [linux] - libc: [glibc] '@unrs/resolver-binding-linux-x64-gnu@1.11.1': resolution: { integrity: sha512-C3ZAHugKgovV5YvAMsxhq0gtXuwESUKc5MhEtjBpLoHPLYM+iuwSj3lflFwK3DPm68660rZ7G8BMcwSro7hD5w== } cpu: [x64] os: [linux] - libc: [glibc] '@unrs/resolver-binding-linux-x64-musl@1.11.1': resolution: { integrity: sha512-rV0YSoyhK2nZ4vEswT/QwqzqQXw5I6CjoaYMOX0TqBlWhojUf8P94mvI7nuJTeaCkkds3QE4+zS8Ko+GdXuZtA== } cpu: [x64] os: [linux] - libc: [musl] '@unrs/resolver-binding-wasm32-wasi@1.11.1': resolution: @@ -1521,6 +1505,15 @@ packages: zod: optional: true + better-call@1.2.0: + resolution: + { integrity: sha512-7msprrikJah2Pq+OPJOUkqqlDOpVQysBPfxLfXQZr1XeIPqUD3O5z6qxh0vsAU8m/GDFbJD0n1a4vvTnekM7Kw== } + peerDependencies: + zod: ^4.0.0 + peerDependenciesMeta: + zod: + optional: true + better-sqlite3@12.4.1: resolution: { integrity: sha512-3yVdyZhklTiNrtg+4WqHpJpFDd+WHTg2oM7UcR80GqL05AOV0xEJzc6qNvFYoEtE+hRp1n9MpN6/+4yhlGkDXQ== } @@ -3097,6 +3090,14 @@ packages: { integrity: sha512-Nv9KddBcQSlQopmBHXSsZVY5xsdlZkdH/Iey0BlcBYggMd4two7cZnKOK9vmy3nY0O5RGH99z1PCeTpPqszUYg== } engines: { bun: '>=1.0.0', deno: '>=1.30.0', node: '>=8.0.0' } + mantine-form-zod-resolver@1.3.0: + resolution: + { integrity: sha512-XlXXkJCYuUuOllW0zedYW+m/lbdFQ/bso1Vz+pJOYkxgjhoGvzN2EXWCS2+0iTOT9Q7WnOwWvHmvpTJN3PxSXw== } + engines: { node: '>=16.6.0' } + peerDependencies: + '@mantine/form': '>=7.0.0' + zod: '>=3.25.0' + markdown-it@14.1.0: resolution: { integrity: sha512-a54IwgWPaeBCAAsv13YgmALOF1elABB08FxO9i+r4VFk5Vl4pKokRPeX8u5TCgSsPi6ec1otfLjdOpVcgbpshg== } @@ -4306,19 +4307,16 @@ packages: babel-plugin-macros: optional: true - supersave@0.20.0: + supersave@1.0.0-beta4: resolution: - { integrity: sha512-+khXfa3WfT+cOULrt6So7oZw55PSERt3H9RUVu1LlIQCSLMldoshZ5uva+KFh8PGnwPNYAYzBlTx0kzoqQYyJw== } - engines: { node: '>=18' } + { integrity: sha512-oVwQbiGaQRhNFQWcF1Hxd+VGcVBFZIaa7Sjb1K9YRQ1OCKjLcA4KsQ5jOd17FIPPyhJwl3p1NUSIFfxs7HKYRQ== } + engines: { node: '>=20' } peerDependencies: - better-sqlite3: ^11.1.2 - express: ^4.17.0 || ^5.0.0 + better-sqlite3: ^11.1.2 || ^12.0.0 mysql2: ^3.11.0 peerDependenciesMeta: better-sqlite3: optional: true - express: - optional: true mysql2: optional: true @@ -4764,6 +4762,10 @@ packages: resolution: { integrity: sha512-JInaHOamG8pt5+Ey8kGmdcAcg3OL9reK8ltczgHTAwNhMys/6ThXHityHxVV2p3fkw/c+MAvBHFVYHFZDmjMCQ== } + zod@4.3.5: + resolution: + { integrity: sha512-k7Nwx6vuWx1IJ9Bjuf4Zt1PEllcwe7cls3VNzm4CQ1/hgtFUK2bRNG3rvnpPUhFjmqJKAKtjV576KnUkHocg/g== } + zwitch@2.0.4: resolution: { integrity: sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A== } @@ -5595,6 +5597,10 @@ snapshots: tslib: 2.8.1 optional: true + '@types/better-sqlite3@7.6.13': + dependencies: + '@types/node': 22.18.12 + '@types/conventional-commits-parser@5.0.1': dependencies: '@types/node': 22.18.12 @@ -5983,6 +5989,15 @@ snapshots: optionalDependencies: zod: 4.1.12 + better-call@1.2.0(zod@4.3.5): + dependencies: + '@better-auth/utils': 0.3.0 + '@better-fetch/fetch': 1.1.21 + rou3: 0.7.12 + set-cookie-parser: 2.7.1 + optionalDependencies: + zod: 4.3.5 + better-sqlite3@12.4.1: dependencies: bindings: 1.5.0 @@ -7440,6 +7455,11 @@ snapshots: lru.min@1.1.2: {} + mantine-form-zod-resolver@1.3.0(@mantine/form@8.3.5(react@19.1.0))(zod@4.1.12): + dependencies: + '@mantine/form': 8.3.5(react@19.1.0) + zod: 4.1.12 + markdown-it@14.1.0: dependencies: argparse: 2.0.1 @@ -8705,15 +8725,16 @@ snapshots: optionalDependencies: '@babel/core': 7.28.4 - supersave@0.20.0(better-sqlite3@12.4.1)(express@5.1.0)(mysql2@3.15.3): + supersave@1.0.0-beta4(better-sqlite3@12.4.1)(mysql2@3.15.3): dependencies: + better-call: 1.2.0(zod@4.3.5) debug: 4.4.3 pluralize: 8.0.0 short-uuid: 4.2.2 slug: 4.1.0 + zod: 4.3.5 optionalDependencies: better-sqlite3: 12.4.1 - express: 5.1.0 mysql2: 3.15.3 transitivePeerDependencies: - supports-color @@ -9133,4 +9154,6 @@ snapshots: zod@4.1.12: {} + zod@4.3.5: {} + zwitch@2.0.4: {} diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index 9806ad8..fcda6a3 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -1,4 +1,6 @@ minimumReleaseAge: 1440 +onlyBuiltDependencies: + - better-sqlite3 overrides: body-parser@>=2.2.0 <2.2.1: '>=2.2.1' diff --git a/scripts/start-server.mjs b/scripts/start-server.mjs index 8b08e19..e7dc87e 100644 --- a/scripts/start-server.mjs +++ b/scripts/start-server.mjs @@ -4,7 +4,7 @@ import path from 'node:path'; import { randomBytes } from 'node:crypto'; import { spawn } from 'node:child_process'; -const DEFAULT_DB = 'mysql://root@localhost:3306/thoth'; +const DEFAULT_DB_FILENAME = 'thoth.db'; const SECRET_FILENAME = 'secret'; /** @@ -31,15 +31,19 @@ function ensureHomeDirectory(homeDirectory) { /** * Resolves the database connection string. - * Uses DB env var if set, otherwise returns default MySQL connection. + * Uses DB env var if set, otherwise defaults to SQLite file in home directory. */ -function resolveDatabase() { +function resolveDatabase(homeDirectory) { const database = process.env.DB; if (database) { return database; } - console.log(`DB not set, using default: ${DEFAULT_DB}`); - return DEFAULT_DB; + + // Default to SQLite file in home directory + const sqlitePath = path.join(homeDirectory, DEFAULT_DB_FILENAME); + const defaultDatabase = `sqlite://${sqlitePath}`; + console.log(`DB not set, using default SQLite: ${defaultDatabase}`); + return defaultDatabase; } /** @@ -69,15 +73,14 @@ function resolveSecret(homeDirectory) { } /** - * Starts the Next.js server with the resolved environment variables. + * Starts the Next.js standalone server with the resolved environment variables. */ function startServer(environment) { - console.log('Starting server with pnpm start...'); + console.log('Starting standalone server...'); - const child = spawn('pnpm', ['start'], { + const child = spawn('node', ['.next/standalone/server.js'], { stdio: 'inherit', env: { ...process.env, ...environment }, - shell: true, }); child.on('error', (error) => { @@ -97,8 +100,10 @@ const homeDirectory = resolveHomeDirectory(); ensureHomeDirectory(homeDirectory); const environment = { - DB: resolveDatabase(), + DB: resolveDatabase(homeDirectory), BETTER_AUTH_SECRET: resolveSecret(homeDirectory), + // In production (start-server), skip auto-sync and use migrations + SUPERSAVE_SKIP_SYNC: 'true', }; startServer(environment); diff --git a/src/app/(auth)/login/page.tsx b/src/app/(auth)/login/page.tsx index 391b83b..219e66f 100755 --- a/src/app/(auth)/login/page.tsx +++ b/src/app/(auth)/login/page.tsx @@ -1,14 +1,108 @@ 'use client'; -import { Button, Center, Container, Paper, Stack, Text, Title } from '@mantine/core'; +import { useEffect, useState } from 'react'; +import { useRouter } from 'next/navigation'; +import { + Anchor, + Button, + Center, + Container, + Loader, + Paper, + PasswordInput, + Stack, + Text, + TextInput, + Title, +} from '@mantine/core'; +import { useForm } from '@mantine/form'; +import { zodResolver } from 'mantine-form-zod-resolver'; +import { z } from 'zod'; +import type { GetAuthConfigResponse } from '@/types/api'; import { authClient } from '@/lib/auth/client'; +import { useNotification } from '@/lib/hooks/use-notification'; + +const loginSchema = z.object({ + email: z.string().email('Invalid email address'), + password: z.string().min(1, 'Password is required'), +}); + +type LoginFormValues = z.infer; export default function LoginPage() { + const router = useRouter(); + const { showError } = useNotification(); + const [authMode, setAuthMode] = useState(null); + const [isLoading, setIsLoading] = useState(false); + const [isOidcLoading, setIsOidcLoading] = useState(false); + + const form = useForm({ + mode: 'uncontrolled', + initialValues: { + email: '', + password: '', + }, + validate: zodResolver(loginSchema), + }); + + useEffect(() => { + const fetchAuthConfig = async () => { + try { + const response = await fetch('/api/v1/config'); + const data: GetAuthConfigResponse = await response.json(); + setAuthMode(data.authMode); + } catch { + // Default to credentials if we can't fetch config + setAuthMode('credentials'); + } + }; + + fetchAuthConfig(); + }, []); + + const handleCredentialsLogin = async (values: LoginFormValues) => { + setIsLoading(true); + try { + const result = await authClient.signIn.email({ + email: values.email, + password: values.password, + }); + + if (result.error) { + showError(result.error.message ?? 'Failed to sign in'); + return; + } + + router.push('/'); + } catch (error) { + const errorMessage = error instanceof Error ? error.message : 'Failed to sign in'; + showError(errorMessage); + } finally { + setIsLoading(false); + } + }; + + const handleOidcLogin = () => { + setIsOidcLoading(true); + authClient.signIn.social({ + provider: 'oidc', + callbackURL: `${globalThis.location.origin}/`, + }); + }; + + if (authMode === null) { + return ( +
+ +
+ ); + } + return (
- +
Welcome to Thoth @@ -18,18 +112,42 @@ export default function LoginPage() { </Text> </div> - <Button - size="md" - fullWidth - onClick={() => - authClient.signIn.social({ - provider: 'oidc', - callbackURL: `${globalThis.location.origin}/`, - }) - } - > - Sign In - </Button> + {authMode === 'credentials' && ( + <> + <form onSubmit={form.onSubmit(handleCredentialsLogin)}> + <Stack gap="md"> + <TextInput + label="Email" + placeholder="your@email.com" + key={form.key('email')} + {...form.getInputProps('email')} + /> + <PasswordInput + label="Password" + placeholder="Your password" + key={form.key('password')} + {...form.getInputProps('password')} + /> + <Button type="submit" fullWidth loading={isLoading}> + Sign In + </Button> + </Stack> + </form> + + <Text c="dimmed" size="sm" ta="center"> + Don't have an account?{' '} + <Anchor href="/signup" size="sm"> + Sign up + </Anchor> + </Text> + </> + )} + + {authMode === 'oidc' && ( + <Button fullWidth onClick={handleOidcLogin} loading={isOidcLoading}> + Sign In + </Button> + )} </Stack> </Paper> </Container> diff --git a/src/app/(auth)/signup/page.tsx b/src/app/(auth)/signup/page.tsx new file mode 100644 index 0000000..ab586c9 --- /dev/null +++ b/src/app/(auth)/signup/page.tsx @@ -0,0 +1,123 @@ +'use client'; + +import { useState } from 'react'; +import { useRouter } from 'next/navigation'; +import { Anchor, Button, Center, Container, Paper, PasswordInput, Stack, Text, TextInput, Title } from '@mantine/core'; +import { useForm } from '@mantine/form'; +import { zodResolver } from 'mantine-form-zod-resolver'; +import { z } from 'zod'; +import { authClient } from '@/lib/auth/client'; +import { useNotification } from '@/lib/hooks/use-notification'; + +const signupSchema = z + .object({ + name: z.string().min(2, 'Name must be at least 2 characters'), + email: z.string().email('Invalid email address'), + password: z.string().min(8, 'Password must be at least 8 characters'), + confirmPassword: z.string(), + }) + .refine((data) => data.password === data.confirmPassword, { + message: 'Passwords do not match', + path: ['confirmPassword'], + }); + +type SignupFormValues = z.infer<typeof signupSchema>; + +export default function SignupPage() { + const router = useRouter(); + const { showError, showSuccess } = useNotification(); + const [isLoading, setIsLoading] = useState(false); + + const form = useForm<SignupFormValues>({ + mode: 'uncontrolled', + initialValues: { + name: '', + email: '', + password: '', + confirmPassword: '', + }, + validate: zodResolver(signupSchema), + }); + + const handleSignup = async (values: SignupFormValues) => { + setIsLoading(true); + try { + const result = await authClient.signUp.email({ + name: values.name, + email: values.email, + password: values.password, + }); + + if (result.error) { + showError(result.error.message ?? 'Failed to create account'); + return; + } + + showSuccess('Account created successfully! Redirecting...'); + router.push('/'); + } catch (error) { + const errorMessage = error instanceof Error ? error.message : 'Failed to create account'; + showError(errorMessage); + } finally { + setIsLoading(false); + } + }; + + return ( + <Center style={{ minHeight: '100vh' }}> + <Container size="xs" w="100%"> + <Paper shadow="md" p="xl" radius="md" withBorder> + <Stack gap="lg"> + <div style={{ textAlign: 'center' }}> + <Title order={2} c="var(--mantine-color-blue-6)"> + Create an Account + + + Sign up to get started with Thoth + +
+ +
+ + + + + + + +
+ + + Already have an account?{' '} + + Sign in + + +
+
+
+
+ ); +} diff --git a/src/app/@sidebar/[...catchAll]/page.tsx b/src/app/@sidebar/[...catchAll]/page.tsx new file mode 100644 index 0000000..2e74e2d --- /dev/null +++ b/src/app/@sidebar/[...catchAll]/page.tsx @@ -0,0 +1 @@ +export { default } from '../page'; diff --git a/src/app/@sidebar/default.tsx b/src/app/@sidebar/page.tsx similarity index 75% rename from src/app/@sidebar/default.tsx rename to src/app/@sidebar/page.tsx index 678f41e..d20e20c 100644 --- a/src/app/@sidebar/default.tsx +++ b/src/app/@sidebar/page.tsx @@ -1,5 +1,5 @@ import { LoggedInContainer } from '@/components/molecules/sidebar/logged-in-container'; -export default function Sidebar() { +export default function SidebarRoot() { return ; } diff --git a/src/app/api/v1/config/route.ts b/src/app/api/v1/config/route.ts new file mode 100644 index 0000000..58c743c --- /dev/null +++ b/src/app/api/v1/config/route.ts @@ -0,0 +1,23 @@ +import { NextResponse } from 'next/server'; +import type { GetAuthConfigResponse } from '@/types/api'; +import { getEnvironment } from '@/lib/environment'; + +/** + * Returns the authentication mode configured for this instance. + * - 'oidc': OpenID Connect / SSO authentication + * - 'credentials': Email/password authentication + */ +export async function GET(): Promise> { + const environment = await getEnvironment(); + + const hasOidcConfig = Boolean( + environment.OIDC_CLIENT_ID && + environment.OIDC_CLIENT_SECRET && + environment.OIDC_DISCOVERY_URL && + environment.OIDC_AUTHORIZATION_URL + ); + + return NextResponse.json({ + authMode: hasOidcConfig ? 'oidc' : 'credentials', + }); +} diff --git a/src/components/layout.tsx b/src/components/layout.tsx index fab80e1..85db614 100755 --- a/src/components/layout.tsx +++ b/src/components/layout.tsx @@ -6,6 +6,7 @@ import { useRouter } from 'next/navigation'; import { type PropsWithChildren, type ReactNode, useEffect } from 'react'; import { useAuth } from '@/lib/auth/provider'; import Image from 'next/image'; +import Link from 'next/link'; type LayoutProperties = PropsWithChildren & { sidebar: ReactNode; @@ -47,7 +48,7 @@ export default function Layout({ children, sidebar }: LayoutProperties) { Thoth Logo Thoth - Logout - + diff --git a/src/components/molecules/page-breadcrumb.tsx b/src/components/molecules/page-breadcrumb.tsx index c1c3464..b12d5de 100644 --- a/src/components/molecules/page-breadcrumb.tsx +++ b/src/components/molecules/page-breadcrumb.tsx @@ -17,7 +17,7 @@ export function PageBreadcrumb({ pages }: PageBreadcrumbProperties) { {pages.map((page, index) => { const isLast = index === pages.length - 1; const content = ( - + {page.emoji && {page.emoji}} {page.name} @@ -25,14 +25,14 @@ export function PageBreadcrumb({ pages }: PageBreadcrumbProperties) { if (isLast) { return ( - + {content} ); } return ( - + {content} ); diff --git a/src/lib/auth/better-auth.ts b/src/lib/auth/better-auth.ts new file mode 100644 index 0000000..6cf8af3 --- /dev/null +++ b/src/lib/auth/better-auth.ts @@ -0,0 +1,8 @@ +import { getAuth } from './config'; + +/** + * This file is not used in this application itself, but is used for the better-auth CLI to generate + * the schema.sql file. + */ + +export const auth = await getAuth(true); diff --git a/src/lib/auth/config.ts b/src/lib/auth/config.ts index 0f1304f..2f8185b 100755 --- a/src/lib/auth/config.ts +++ b/src/lib/auth/config.ts @@ -1,14 +1,27 @@ /* eslint-disable unicorn/prefer-ternary */ import { betterAuth } from 'better-auth'; import { genericOAuth } from 'better-auth/plugins'; +import Database from 'better-sqlite3'; import { createPool } from 'mysql2/promise'; +import { connection } from 'next/server'; import type { PageContainerCreate, WorkspaceCreate } from '@/types/database'; import { getContainerRepository, getDatabase, getWorkspaceRepository } from '../database'; import { getEnvironment } from '../environment'; -import { connection } from 'next/server'; let authInstance: ReturnType | null = null; +/** + * Creates a database adapter based on the connection string. + * Supports both SQLite (sqlite://) and MySQL (mysql://) connection strings. + */ +function createDatabaseAdapter(connectionString: string) { + if (connectionString.startsWith('sqlite://')) { + const databasePath = connectionString.replace('sqlite://', ''); + return new Database(databasePath); + } + return createPool(connectionString); +} + /** * Checks if all OIDC environment variables are configured. * If all are present, OIDC authentication will be used. @@ -25,7 +38,7 @@ function hasOidcConfig(environment: Awaited>): async function initializeAuth() { if (authInstance === null) { - await connection().then(getDatabase); + await getDatabase(); const environment = await getEnvironment(); const useOidc = hasOidcConfig(environment); @@ -63,7 +76,7 @@ async function initializeAuth() { if (useOidc) { // OIDC authentication mode authInstance = betterAuth({ - database: createPool(environment.DB), + database: createDatabaseAdapter(environment.DB), plugins: [ genericOAuth({ config: [ @@ -86,7 +99,7 @@ async function initializeAuth() { } else { // Credentials (email/password) authentication mode authInstance = betterAuth({ - database: createPool(environment.DB), + database: createDatabaseAdapter(environment.DB), emailAndPassword: { enabled: true, }, @@ -100,7 +113,9 @@ async function initializeAuth() { return authInstance; } -export async function getAuth() { - await connection(); +export async function getAuth(outsideRequest = false) { + if (!outsideRequest) { + await connection(); + } return await initializeAuth(); } diff --git a/src/lib/database/index.ts b/src/lib/database/index.ts index 3ede6c5..00feecd 100755 --- a/src/lib/database/index.ts +++ b/src/lib/database/index.ts @@ -2,6 +2,7 @@ import { SuperSave } from 'supersave'; import type { Container, Workspace, DataView } from '@/types/database'; import { getEnvironment } from '../environment'; import * as entities from './entities'; +import { migrations } from './migrations'; let database: SuperSave; @@ -11,11 +12,21 @@ export async function getDatabase() { } const environment = await getEnvironment(); - database = await SuperSave.create(environment.DB); + const skipSync = environment.SUPERSAVE_SKIP_SYNC; + + database = await SuperSave.create(environment.DB, { + migrations, + skipSync, + }); await database.addEntity(entities.Container); await database.addEntity(entities.Workspace); await database.addEntity(entities.DataView); + + if (!skipSync) { + await database.runMigrations(); + } + return database; } diff --git a/src/lib/database/migrations/better-auth.ts b/src/lib/database/migrations/better-auth.ts new file mode 100644 index 0000000..b1e2d31 --- /dev/null +++ b/src/lib/database/migrations/better-auth.ts @@ -0,0 +1,32 @@ +/** + * These migrations are generated using the better-auth CLI. + * ``` + * export DB=sqlite://:memory: + * export NODE_ENV=development + * export BETTER_AUTH_SECRET=4d675dd169730483103eb302793617397357b4b86a63e19eb77c9cda3d0d64ac + * npx @better-auth/cli generate --config ./src/lib/auth/better-auth.ts + * ``` + * + * to generate for mysql, replace DB= env var with a mysql alternative. + */ +// SQLite schema for better-auth tables +export const BETTER_AUTH_SQLITE_SQL = ` +create table "user" ("id" text not null primary key, "name" text not null, "email" text not null unique, "emailVerified" integer not null, "image" text, "createdAt" date not null, "updatedAt" date not null); +create table "session" ("id" text not null primary key, "expiresAt" date not null, "token" text not null unique, "createdAt" date not null, "updatedAt" date not null, "ipAddress" text, "userAgent" text, "userId" text not null references "user" ("id") on delete cascade); +create table "account" ("id" text not null primary key, "accountId" text not null, "providerId" text not null, "userId" text not null references "user" ("id") on delete cascade, "accessToken" text, "refreshToken" text, "idToken" text, "accessTokenExpiresAt" date, "refreshTokenExpiresAt" date, "scope" text, "password" text, "createdAt" date not null, "updatedAt" date not null); +create table "verification" ("id" text not null primary key, "identifier" text not null, "value" text not null, "expiresAt" date not null, "createdAt" date not null, "updatedAt" date not null); +create index "session_userId_idx" on "session" ("userId"); +create index "account_userId_idx" on "account" ("userId"); +create index "verification_identifier_idx" on "verification" ("identifier"); +`; + +// MySQL schema for better-auth tables +export const BETTER_AUTH_MYSQL_SQL = ` +create table \`user\` (\`id\` varchar(36) not null primary key, \`name\` varchar(255) not null, \`email\` varchar(255) not null unique, \`emailVerified\` boolean not null, \`image\` text, \`createdAt\` timestamp(3) default CURRENT_TIMESTAMP(3) not null, \`updatedAt\` timestamp(3) default CURRENT_TIMESTAMP(3) not null); +create table \`session\` (\`id\` varchar(36) not null primary key, \`expiresAt\` timestamp(3) not null, \`token\` varchar(255) not null unique, \`createdAt\` timestamp(3) default CURRENT_TIMESTAMP(3) not null, \`updatedAt\` timestamp(3) not null, \`ipAddress\` text, \`userAgent\` text, \`userId\` varchar(36) not null references \`user\` (\`id\`) on delete cascade); +create table \`account\` (\`id\` varchar(36) not null primary key, \`accountId\` text not null, \`providerId\` text not null, \`userId\` varchar(36) not null references \`user\` (\`id\`) on delete cascade, \`accessToken\` text, \`refreshToken\` text, \`idToken\` text, \`accessTokenExpiresAt\` timestamp(3), \`refreshTokenExpiresAt\` timestamp(3), \`scope\` text, \`password\` text, \`createdAt\` timestamp(3) default CURRENT_TIMESTAMP(3) not null, \`updatedAt\` timestamp(3) not null); +create table \`verification\` (\`id\` varchar(36) not null primary key, \`identifier\` varchar(255) not null, \`value\` text not null, \`expiresAt\` timestamp(3) not null, \`createdAt\` timestamp(3) default CURRENT_TIMESTAMP(3) not null, \`updatedAt\` timestamp(3) default CURRENT_TIMESTAMP(3) not null); +create index \`session_userId_idx\` on \`session\` (\`userId\`); +create index \`account_userId_idx\` on \`account\` (\`userId\`); +create index \`verification_identifier_idx\` on \`verification\` (\`identifier\`); +`; diff --git a/src/lib/database/migrations/index.ts b/src/lib/database/migrations/index.ts new file mode 100644 index 0000000..b773033 --- /dev/null +++ b/src/lib/database/migrations/index.ts @@ -0,0 +1,30 @@ +import type { Migration, SuperSave } from 'supersave'; +import type { Database } from 'better-sqlite3'; +import type { Pool } from 'mysql2/promise'; +import { BETTER_AUTH_SQLITE_SQL, BETTER_AUTH_MYSQL_SQL } from './better-auth'; + +export const migrations: Migration[] = [ + { + name: 'better-auth-tables', + engine: 'sqlite', + run: async (superSave: SuperSave) => { + const database = superSave.getConnection(); + database.exec(BETTER_AUTH_SQLITE_SQL); + }, + }, + { + name: 'better-auth-tables', + engine: 'mysql', + run: async (superSave: SuperSave) => { + const pool = superSave.getConnection(); + // MySQL requires executing statements one at a time + const statements = BETTER_AUTH_MYSQL_SQL.split(';') + .map((s) => s.trim()) + .filter((s) => s.length > 0); + + for (const statement of statements) { + await pool.query(statement); + } + }, + }, +]; diff --git a/src/lib/environment.ts b/src/lib/environment.ts index 69b2c4e..8c84aec 100755 --- a/src/lib/environment.ts +++ b/src/lib/environment.ts @@ -1,4 +1,4 @@ -import { cleanEnv, str, url } from 'envalid'; +import { bool, cleanEnv, str, url } from 'envalid'; const environmentSchema = { NODE_ENV: str({ choices: ['development', 'production', 'test'] }), @@ -8,6 +8,8 @@ const environmentSchema = { default: 'info', }), BETTER_AUTH_SECRET: str(), + // If true, skip automatic schema sync and use migrations instead (for production) + SUPERSAVE_SKIP_SYNC: bool({ default: false }), // OIDC variables are optional - if not set, credentials auth will be used OIDC_CLIENT_ID: str({ default: undefined }), OIDC_CLIENT_SECRET: str({ default: undefined }), diff --git a/src/types/api/endpoints/get-auth-config.ts b/src/types/api/endpoints/get-auth-config.ts new file mode 100644 index 0000000..805431d --- /dev/null +++ b/src/types/api/endpoints/get-auth-config.ts @@ -0,0 +1,9 @@ +import { z } from 'zod'; + +export const GET_AUTH_CONFIG_ENDPOINT = '/v1/config'; + +export const getAuthConfigResponseSchema = z.object({ + authMode: z.enum(['oidc', 'credentials']), +}); + +export type GetAuthConfigResponse = z.infer; diff --git a/src/types/api/endpoints/index.ts b/src/types/api/endpoints/index.ts index 2a887a8..b667c47 100755 --- a/src/types/api/endpoints/index.ts +++ b/src/types/api/endpoints/index.ts @@ -1,6 +1,7 @@ export * from './create-page'; export * from './create-data-source'; export * from './create-data-view'; +export * from './get-auth-config'; export * from './get-data-source'; export * from './get-data-sources'; export * from './get-data-view'; From 8191a6791f8deb4310fcce3f159396fcdda6b4b3 Mon Sep 17 00:00:00 2001 From: debuggingdan Date: Thu, 22 Jan 2026 16:18:14 +0100 Subject: [PATCH 2/2] chore: review notes --- src/app/(auth)/login/page.tsx | 21 +++++++++------ src/lib/database/migrations/better-auth.ts | 30 +++++++++++----------- 2 files changed, 28 insertions(+), 23 deletions(-) diff --git a/src/app/(auth)/login/page.tsx b/src/app/(auth)/login/page.tsx index 219e66f..ebee60f 100755 --- a/src/app/(auth)/login/page.tsx +++ b/src/app/(auth)/login/page.tsx @@ -19,6 +19,7 @@ import { useForm } from '@mantine/form'; import { zodResolver } from 'mantine-form-zod-resolver'; import { z } from 'zod'; import type { GetAuthConfigResponse } from '@/types/api'; +import { apiClient } from '@/lib/api/client'; import { authClient } from '@/lib/auth/client'; import { useNotification } from '@/lib/hooks/use-notification'; @@ -48,9 +49,8 @@ export default function LoginPage() { useEffect(() => { const fetchAuthConfig = async () => { try { - const response = await fetch('/api/v1/config'); - const data: GetAuthConfigResponse = await response.json(); - setAuthMode(data.authMode); + const response = await apiClient.get('/config'); + setAuthMode(response.data.authMode); } catch { // Default to credentials if we can't fetch config setAuthMode('credentials'); @@ -82,12 +82,17 @@ export default function LoginPage() { } }; - const handleOidcLogin = () => { + const handleOidcLogin = async () => { setIsOidcLoading(true); - authClient.signIn.social({ - provider: 'oidc', - callbackURL: `${globalThis.location.origin}/`, - }); + try { + await authClient.signIn.social({ + provider: 'oidc', + callbackURL: `${globalThis.location.origin}/`, + }); + } catch (error) { + console.error('OIDC sign-in failed:', error); + setIsOidcLoading(false); + } }; if (authMode === null) { diff --git a/src/lib/database/migrations/better-auth.ts b/src/lib/database/migrations/better-auth.ts index b1e2d31..18bc326 100644 --- a/src/lib/database/migrations/better-auth.ts +++ b/src/lib/database/migrations/better-auth.ts @@ -3,7 +3,7 @@ * ``` * export DB=sqlite://:memory: * export NODE_ENV=development - * export BETTER_AUTH_SECRET=4d675dd169730483103eb302793617397357b4b86a63e19eb77c9cda3d0d64ac + * export BETTER_AUTH_SECRET=some_secret_here_doesnt_matter * npx @better-auth/cli generate --config ./src/lib/auth/better-auth.ts * ``` * @@ -11,22 +11,22 @@ */ // SQLite schema for better-auth tables export const BETTER_AUTH_SQLITE_SQL = ` -create table "user" ("id" text not null primary key, "name" text not null, "email" text not null unique, "emailVerified" integer not null, "image" text, "createdAt" date not null, "updatedAt" date not null); -create table "session" ("id" text not null primary key, "expiresAt" date not null, "token" text not null unique, "createdAt" date not null, "updatedAt" date not null, "ipAddress" text, "userAgent" text, "userId" text not null references "user" ("id") on delete cascade); -create table "account" ("id" text not null primary key, "accountId" text not null, "providerId" text not null, "userId" text not null references "user" ("id") on delete cascade, "accessToken" text, "refreshToken" text, "idToken" text, "accessTokenExpiresAt" date, "refreshTokenExpiresAt" date, "scope" text, "password" text, "createdAt" date not null, "updatedAt" date not null); -create table "verification" ("id" text not null primary key, "identifier" text not null, "value" text not null, "expiresAt" date not null, "createdAt" date not null, "updatedAt" date not null); -create index "session_userId_idx" on "session" ("userId"); -create index "account_userId_idx" on "account" ("userId"); -create index "verification_identifier_idx" on "verification" ("identifier"); +create table if not exists "user" ("id" text not null primary key, "name" text not null, "email" text not null unique, "emailVerified" integer not null, "image" text, "createdAt" date not null, "updatedAt" date not null); +create table if not exists "session" ("id" text not null primary key, "expiresAt" date not null, "token" text not null unique, "createdAt" date not null, "updatedAt" date not null, "ipAddress" text, "userAgent" text, "userId" text not null references "user" ("id") on delete cascade); +create table if not exists "account" ("id" text not null primary key, "accountId" text not null, "providerId" text not null, "userId" text not null references "user" ("id") on delete cascade, "accessToken" text, "refreshToken" text, "idToken" text, "accessTokenExpiresAt" date, "refreshTokenExpiresAt" date, "scope" text, "password" text, "createdAt" date not null, "updatedAt" date not null); +create table if not exists "verification" ("id" text not null primary key, "identifier" text not null, "value" text not null, "expiresAt" date not null, "createdAt" date not null, "updatedAt" date not null); +create index if not exists "session_userId_idx" on "session" ("userId"); +create index if not exists "account_userId_idx" on "account" ("userId"); +create index if not exists "verification_identifier_idx" on "verification" ("identifier"); `; // MySQL schema for better-auth tables export const BETTER_AUTH_MYSQL_SQL = ` -create table \`user\` (\`id\` varchar(36) not null primary key, \`name\` varchar(255) not null, \`email\` varchar(255) not null unique, \`emailVerified\` boolean not null, \`image\` text, \`createdAt\` timestamp(3) default CURRENT_TIMESTAMP(3) not null, \`updatedAt\` timestamp(3) default CURRENT_TIMESTAMP(3) not null); -create table \`session\` (\`id\` varchar(36) not null primary key, \`expiresAt\` timestamp(3) not null, \`token\` varchar(255) not null unique, \`createdAt\` timestamp(3) default CURRENT_TIMESTAMP(3) not null, \`updatedAt\` timestamp(3) not null, \`ipAddress\` text, \`userAgent\` text, \`userId\` varchar(36) not null references \`user\` (\`id\`) on delete cascade); -create table \`account\` (\`id\` varchar(36) not null primary key, \`accountId\` text not null, \`providerId\` text not null, \`userId\` varchar(36) not null references \`user\` (\`id\`) on delete cascade, \`accessToken\` text, \`refreshToken\` text, \`idToken\` text, \`accessTokenExpiresAt\` timestamp(3), \`refreshTokenExpiresAt\` timestamp(3), \`scope\` text, \`password\` text, \`createdAt\` timestamp(3) default CURRENT_TIMESTAMP(3) not null, \`updatedAt\` timestamp(3) not null); -create table \`verification\` (\`id\` varchar(36) not null primary key, \`identifier\` varchar(255) not null, \`value\` text not null, \`expiresAt\` timestamp(3) not null, \`createdAt\` timestamp(3) default CURRENT_TIMESTAMP(3) not null, \`updatedAt\` timestamp(3) default CURRENT_TIMESTAMP(3) not null); -create index \`session_userId_idx\` on \`session\` (\`userId\`); -create index \`account_userId_idx\` on \`account\` (\`userId\`); -create index \`verification_identifier_idx\` on \`verification\` (\`identifier\`); +create table if not exists \`user\` (\`id\` varchar(36) not null primary key, \`name\` varchar(255) not null, \`email\` varchar(255) not null unique, \`emailVerified\` boolean not null, \`image\` text, \`createdAt\` timestamp(3) default CURRENT_TIMESTAMP(3) not null, \`updatedAt\` timestamp(3) default CURRENT_TIMESTAMP(3) not null); +create table if not exists \`session\` (\`id\` varchar(36) not null primary key, \`expiresAt\` timestamp(3) not null, \`token\` varchar(255) not null unique, \`createdAt\` timestamp(3) default CURRENT_TIMESTAMP(3) not null, \`updatedAt\` timestamp(3) not null, \`ipAddress\` text, \`userAgent\` text, \`userId\` varchar(36) not null references \`user\` (\`id\`) on delete cascade); +create table if not exists \`account\` (\`id\` varchar(36) not null primary key, \`accountId\` text not null, \`providerId\` text not null, \`userId\` varchar(36) not null references \`user\` (\`id\`) on delete cascade, \`accessToken\` text, \`refreshToken\` text, \`idToken\` text, \`accessTokenExpiresAt\` timestamp(3), \`refreshTokenExpiresAt\` timestamp(3), \`scope\` text, \`password\` text, \`createdAt\` timestamp(3) default CURRENT_TIMESTAMP(3) not null, \`updatedAt\` timestamp(3) not null); +create table if not exists \`verification\` (\`id\` varchar(36) not null primary key, \`identifier\` varchar(255) not null, \`value\` text not null, \`expiresAt\` timestamp(3) not null, \`createdAt\` timestamp(3) default CURRENT_TIMESTAMP(3) not null, \`updatedAt\` timestamp(3) default CURRENT_TIMESTAMP(3) not null); +create index if not exists \`session_userId_idx\` on \`session\` (\`userId\`); +create index if not exists \`account_userId_idx\` on \`account\` (\`userId\`); +create index if not exists \`verification_identifier_idx\` on \`verification\` (\`identifier\`); `;