From 47251dad3fbd51e8d33ace7f72f0104653e457bf Mon Sep 17 00:00:00 2001 From: Dmytro Stanchiev Date: Mon, 6 Apr 2026 22:40:01 -0400 Subject: [PATCH 01/25] chore: add debug.log to .gitignore --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index 18cce8c..78a4142 100644 --- a/.gitignore +++ b/.gitignore @@ -75,3 +75,4 @@ public/workbox-*.js /opencode.json /opencode.json.bak # END Ruler Generated Files +/debug.log From d8875e587e67757524b765796544971fe2efe3c8 Mon Sep 17 00:00:00 2001 From: Dmytro Stanchiev Date: Mon, 6 Apr 2026 22:40:15 -0400 Subject: [PATCH 02/25] feat: add PostgreSQL dev container compose file --- compose.dev.yml | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) create mode 100644 compose.dev.yml diff --git a/compose.dev.yml b/compose.dev.yml new file mode 100644 index 0000000..ea119c9 --- /dev/null +++ b/compose.dev.yml @@ -0,0 +1,20 @@ +services: + postgres: + image: postgres:17 + container_name: local-cal-postgres + ports: + - "5432:5432" + environment: + POSTGRES_USER: localcal + POSTGRES_PASSWORD: localcal + POSTGRES_DB: localcal + volumes: + - postgres_data:/var/lib/postgresql/data + healthcheck: + test: ["CMD-SHELL", "pg_isready -U localcal"] + interval: 5s + timeout: 5s + retries: 5 + +volumes: + postgres_data: From 8a500f07de17c43660d510afbaa4b44e855b0daf Mon Sep 17 00:00:00 2001 From: Dmytro Stanchiev Date: Mon, 6 Apr 2026 22:40:27 -0400 Subject: [PATCH 03/25] refactor: replace next-auth with better-auth dependency --- bun.lock | 59 ++++++++++++++++++++++++++++++++++++++-------------- package.json | 3 +-- 2 files changed, 44 insertions(+), 18 deletions(-) diff --git a/bun.lock b/bun.lock index 476caee..6a5c9cf 100644 --- a/bun.lock +++ b/bun.lock @@ -5,13 +5,13 @@ "": { "name": "ical-pwa", "dependencies": { - "@auth/drizzle-adapter": "^1.10.0", "@radix-ui/react-checkbox": "^1.3.3", "@radix-ui/react-dialog": "^1.1.15", "@radix-ui/react-dropdown-menu": "^2.1.16", "@radix-ui/react-label": "^2.1.7", "@radix-ui/react-select": "^2.2.6", "@radix-ui/react-slot": "^1.2.3", + "better-auth": "^1.6.0", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", "date-fns": "^4.1.0", @@ -22,7 +22,6 @@ "lucide-react": "^0.539.0", "nanoid": "^5.1.5", "next": "15.4.10", - "next-auth": "^5.0.0-beta.29", "next-themes": "^0.4.6", "pg": "^8.16.3", "postgres": "^3.4.7", @@ -55,9 +54,23 @@ "packages": { "@alloc/quick-lru": ["@alloc/quick-lru@5.2.0", "", {}, "sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw=="], - "@auth/core": ["@auth/core@0.41.1", "", { "dependencies": { "@panva/hkdf": "^1.2.1", "jose": "^6.0.6", "oauth4webapi": "^3.3.0", "preact": "10.24.3", "preact-render-to-string": "6.5.11" }, "peerDependencies": { "@simplewebauthn/browser": "^9.0.1", "@simplewebauthn/server": "^9.0.2", "nodemailer": "^7.0.7" }, "optionalPeers": ["@simplewebauthn/browser", "@simplewebauthn/server", "nodemailer"] }, "sha512-t9cJ2zNYAdWMacGRMT6+r4xr1uybIdmYa49calBPeTqwgAFPV/88ac9TEvCR85pvATiSPt8VaNf+Gt24JIT/uw=="], + "@better-auth/core": ["@better-auth/core@1.6.0", "", { "dependencies": { "@opentelemetry/semantic-conventions": "^1.39.0", "@standard-schema/spec": "^1.1.0", "zod": "^4.3.6" }, "peerDependencies": { "@better-auth/utils": "0.4.0", "@better-fetch/fetch": "1.1.21", "@cloudflare/workers-types": ">=4", "@opentelemetry/api": "^1.9.0", "better-call": "1.3.5", "jose": "^6.1.0", "kysely": "^0.28.5", "nanostores": "^1.0.1" }, "optionalPeers": ["@cloudflare/workers-types"] }, "sha512-LmdPTyKRDn6iCcXBGlOHOyzpJl1W/3w64zrEbhhHaWmtdpzQWlY8awlWBoDTL9eL4TAusr9dDvwIbMYTvEqaeA=="], - "@auth/drizzle-adapter": ["@auth/drizzle-adapter@1.11.1", "", { "dependencies": { "@auth/core": "0.41.1" } }, "sha512-cQTvDZqsyF7RPhDm/B6SvqdVP9EzQhy3oM4Muu7fjjmSYFLbSR203E6dH631ZHSKDn2b4WZkfMnjPDzRsPSAeA=="], + "@better-auth/drizzle-adapter": ["@better-auth/drizzle-adapter@1.6.0", "", { "peerDependencies": { "@better-auth/core": "^1.6.0", "@better-auth/utils": "0.4.0", "drizzle-orm": ">=0.41.0" }, "optionalPeers": ["drizzle-orm"] }, "sha512-iMgvZlrL4FI63CGaxLqE5rgA2Q9VVmc2fQIP7N5E79nGAEpHtztstHFPlen9RDLRJA4xa3wuyVaPSILylwE+LA=="], + + "@better-auth/kysely-adapter": ["@better-auth/kysely-adapter@1.6.0", "", { "peerDependencies": { "@better-auth/core": "^1.6.0", "@better-auth/utils": "0.4.0", "kysely": "^0.27.0 || ^0.28.0" }, "optionalPeers": ["kysely"] }, "sha512-ZLEp2j3jquX7wrPQ7tPOSRAjmMoHhdrsgkuH9Bp/fgNZV7M1eiwAY6fHRGKad6KIldoI+iazMUIm60v11fIHCg=="], + + "@better-auth/memory-adapter": ["@better-auth/memory-adapter@1.6.0", "", { "peerDependencies": { "@better-auth/core": "^1.6.0", "@better-auth/utils": "0.4.0" } }, "sha512-FbLmz6ujltw8RDUkBzutwIfoV+q9Mu0gLVrfhDAb9INe+jLcaQikiIjFdVwPzpx+bOs6bWTDfylrlI6+Ytxs3Q=="], + + "@better-auth/mongo-adapter": ["@better-auth/mongo-adapter@1.6.0", "", { "peerDependencies": { "@better-auth/core": "^1.6.0", "@better-auth/utils": "0.4.0", "mongodb": "^6.0.0 || ^7.0.0" }, "optionalPeers": ["mongodb"] }, "sha512-EYZwMpcpoaLRnfhEr+k+MTKS8SKi51TWh1b7bLSy+yHLL0PdbadFsGYZPgzLbZEaq4kUP0asMzXxA+blutjOQQ=="], + + "@better-auth/prisma-adapter": ["@better-auth/prisma-adapter@1.6.0", "", { "peerDependencies": { "@better-auth/core": "^1.6.0", "@better-auth/utils": "0.4.0", "@prisma/client": "^5.0.0 || ^6.0.0 || ^7.0.0", "prisma": "^5.0.0 || ^6.0.0 || ^7.0.0" }, "optionalPeers": ["@prisma/client", "prisma"] }, "sha512-8x/aqR1NckGiC49P02cxuH0wLzbJXvE/v2NnMEFo6h3uWq4ESYL0jTY9vNlFeVIKDyGSzrbteofzzG+yQv0wAQ=="], + + "@better-auth/telemetry": ["@better-auth/telemetry@1.6.0", "", { "peerDependencies": { "@better-auth/core": "^1.6.0", "@better-auth/utils": "0.4.0", "@better-fetch/fetch": "1.1.21" } }, "sha512-JrJyx1ioswEAh8rB7mVxEFIDLl6AK3W3rtqc2MK6BgvcmKveWJ730Eoi/PNvi0b4tFk4kczmuQITm69uMbnTvQ=="], + + "@better-auth/utils": ["@better-auth/utils@0.4.0", "", { "dependencies": { "@noble/hashes": "^2.0.1" } }, "sha512-RpMtLUIQAEWMgdPLNVbIF5ON2mm+CH0U3rCdUCU1VyeAUui4m38DyK7/aXMLZov2YDjG684pS1D0MBllrmgjQA=="], + + "@better-fetch/fetch": ["@better-fetch/fetch@1.1.21", "", {}, "sha512-/ImESw0sskqlVR94jB+5+Pxjf+xBwDZF/N5+y2/q4EqD7IARUTSpPfIo8uf39SYpCxyOCtbyYpUrZ3F/k0zT4A=="], "@date-fns/tz": ["@date-fns/tz@1.4.1", "", {}, "sha512-P5LUNhtbj6YfI3iJjw5EL9eUAG6OitD0W3fWQcpQjDRc/QIsL0tRNuO1PcDvPccWL1fSTXXdE1ds+l95DV/OFA=="], @@ -241,6 +254,10 @@ "@next/swc-win32-x64-msvc": ["@next/swc-win32-x64-msvc@15.4.8", "", { "os": "win32", "cpu": "x64" }, "sha512-Exsmf/+42fWVnLMaZHzshukTBxZrSwuuLKFvqhGHJ+mC1AokqieLY/XzAl3jc/CqhXLqLY3RRjkKJ9YnLPcRWg=="], + "@noble/ciphers": ["@noble/ciphers@2.1.1", "", {}, "sha512-bysYuiVfhxNJuldNXlFEitTVdNnYUc+XNJZd7Qm2a5j1vZHgY+fazadNFWFaMK/2vye0JVlxV3gHmC0WDfAOQw=="], + + "@noble/hashes": ["@noble/hashes@2.0.1", "", {}, "sha512-XlOlEbQcE9fmuXxrVTXCTlG2nlRXa9Rj3rr5Ue/+tX+nmkgbX720YHh0VR3hBF9xDvwnb8D2shVGOwNx+ulArw=="], + "@nodelib/fs.scandir": ["@nodelib/fs.scandir@2.1.5", "", { "dependencies": { "@nodelib/fs.stat": "2.0.5", "run-parallel": "^1.1.9" } }, "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g=="], "@nodelib/fs.stat": ["@nodelib/fs.stat@2.0.5", "", {}, "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A=="], @@ -249,7 +266,9 @@ "@nolyfill/is-core-module": ["@nolyfill/is-core-module@1.0.39", "", {}, "sha512-nn5ozdjYQpUCZlWGuxcJY/KpxkWQs4DcbMCmKojjyrYDEAGy4Ce19NN4v5MduafTwJlbKc99UA8YhSVqq9yPZA=="], - "@panva/hkdf": ["@panva/hkdf@1.2.1", "", {}, "sha512-6oclG6Y3PiDFcoyk8srjLfVKyMfVCKJ27JwNPViuXziFpmdz+MZnZN/aKY0JGXgYuO/VghU0jcOAZgWXZ1Dmrw=="], + "@opentelemetry/api": ["@opentelemetry/api@1.9.1", "", {}, "sha512-gLyJlPHPZYdAk1JENA9LeHejZe1Ti77/pTeFm/nMXmQH/HFZlcS/O2XJB+L8fkbrNSqhdtlvjBVjxwUYanNH5Q=="], + + "@opentelemetry/semantic-conventions": ["@opentelemetry/semantic-conventions@1.40.0", "", {}, "sha512-cifvXDhcqMwwTlTK04GBNeIe7yyo28Mfby85QXFe1Yk8nmi36Ab/5UQwptOx84SsoGNRg+EVSjwzfSZMy6pmlw=="], "@radix-ui/number": ["@radix-ui/number@1.1.1", "", {}, "sha512-MkKCwxlXTgz6CFoJx3pCwn07GKp36+aZyu/u2Ln2VrA5DcdyCZkASEDBTd8x5whTQQL5CiYf4prXKLcgQdv29g=="], @@ -321,6 +340,8 @@ "@rushstack/eslint-patch": ["@rushstack/eslint-patch@1.15.0", "", {}, "sha512-ojSshQPKwVvSMR8yT2L/QtUkV5SXi/IfDiJ4/8d6UbTPjiHVmxZzUAzGD8Tzks1b9+qQkZa0isUOvYObedITaw=="], + "@standard-schema/spec": ["@standard-schema/spec@1.1.0", "", {}, "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="], + "@swc/helpers": ["@swc/helpers@0.5.15", "", { "dependencies": { "tslib": "^2.8.0" } }, "sha512-JQ5TuMi45Owi4/BIMAJBoSQoOJu12oOk/gADqlcUL9JEdHB8vyjUSsxqeNXnmXHjYKMi2WcYtezGEEhqUI/E2g=="], "@tailwindcss/node": ["@tailwindcss/node@4.1.17", "", { "dependencies": { "@jridgewell/remapping": "^2.3.4", "enhanced-resolve": "^5.18.3", "jiti": "^2.6.1", "lightningcss": "1.30.2", "magic-string": "^0.30.21", "source-map-js": "^1.2.1", "tailwindcss": "4.1.17" } }, "sha512-csIkHIgLb3JisEFQ0vxr2Y57GUNYh447C8xzwj89U/8fdW8LhProdxvnVH6U8M2Y73QKiTIH+LWbK3V2BBZsAg=="], @@ -469,6 +490,10 @@ "balanced-match": ["balanced-match@1.0.2", "", {}, "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="], + "better-auth": ["better-auth@1.6.0", "", { "dependencies": { "@better-auth/core": "1.6.0", "@better-auth/drizzle-adapter": "1.6.0", "@better-auth/kysely-adapter": "1.6.0", "@better-auth/memory-adapter": "1.6.0", "@better-auth/mongo-adapter": "1.6.0", "@better-auth/prisma-adapter": "1.6.0", "@better-auth/telemetry": "1.6.0", "@better-auth/utils": "0.4.0", "@better-fetch/fetch": "1.1.21", "@noble/ciphers": "^2.1.1", "@noble/hashes": "^2.0.1", "better-call": "1.3.5", "defu": "^6.1.4", "jose": "^6.1.3", "kysely": "^0.28.14", "nanostores": "^1.1.1", "zod": "^4.3.6" }, "peerDependencies": { "@lynx-js/react": "*", "@prisma/client": "^5.0.0 || ^6.0.0 || ^7.0.0", "@sveltejs/kit": "^2.0.0", "@tanstack/react-start": "^1.0.0", "@tanstack/solid-start": "^1.0.0", "better-sqlite3": "^12.0.0", "drizzle-kit": ">=0.31.4", "drizzle-orm": ">=0.41.0", "mongodb": "^6.0.0 || ^7.0.0", "mysql2": "^3.0.0", "next": "^14.0.0 || ^15.0.0 || ^16.0.0", "pg": "^8.0.0", "prisma": "^5.0.0 || ^6.0.0 || ^7.0.0", "react": "^18.0.0 || ^19.0.0", "react-dom": "^18.0.0 || ^19.0.0", "solid-js": "^1.0.0", "svelte": "^4.0.0 || ^5.0.0", "vitest": "^2.0.0 || ^3.0.0 || ^4.0.0", "vue": "^3.0.0" }, "optionalPeers": ["@lynx-js/react", "@prisma/client", "@sveltejs/kit", "@tanstack/react-start", "@tanstack/solid-start", "better-sqlite3", "drizzle-kit", "drizzle-orm", "mongodb", "mysql2", "next", "pg", "prisma", "react", "react-dom", "solid-js", "svelte", "vitest", "vue"] }, "sha512-reEK4X37w/X0Wi0ZpNSo6w3j9F2tsA7ebWn2AmWTzkceiatkxcadRg9aK+Mirw2PY56GQqX9dBgqBG6XMNU/Zg=="], + + "better-call": ["better-call@1.3.5", "", { "dependencies": { "@better-auth/utils": "^0.4.0", "@better-fetch/fetch": "^1.1.21", "rou3": "^0.7.12", "set-cookie-parser": "^3.0.1" }, "peerDependencies": { "zod": "^4.0.0" }, "optionalPeers": ["zod"] }, "sha512-kOFJkBP7utAQLEYrobZm3vkTH8mXq5GNgvjc5/XEST1ilVHaxXUXfeDeFlqoETMtyqS4+3/h4ONX2i++ebZrvA=="], + "brace-expansion": ["brace-expansion@1.1.12", "", { "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" } }, "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg=="], "braces": ["braces@3.0.3", "", { "dependencies": { "fill-range": "^7.1.1" } }, "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA=="], @@ -523,6 +548,8 @@ "define-properties": ["define-properties@1.2.1", "", { "dependencies": { "define-data-property": "^1.0.1", "has-property-descriptors": "^1.0.0", "object-keys": "^1.1.1" } }, "sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg=="], + "defu": ["defu@6.1.6", "", {}, "sha512-f8mefEW4WIVg4LckePx3mALjQSPQgFlg9U8yaPdlsbdYcHQyj9n2zL2LJEA52smeYxOvmd/nB7TpMtHGMTHcug=="], + "detect-libc": ["detect-libc@2.1.2", "", {}, "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ=="], "detect-node-es": ["detect-node-es@1.1.0", "", {}, "sha512-ypdmJU/TbBby2Dxibuv7ZLW3Bs1QEmM7nHjEANfohJLvE0XVujisn1qPJcZxg+qDucsr+bP6fLD1rPS3AhJ7EQ=="], @@ -737,7 +764,7 @@ "jiti": ["jiti@2.6.1", "", { "bin": { "jiti": "lib/jiti-cli.mjs" } }, "sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ=="], - "jose": ["jose@6.1.3", "", {}, "sha512-0TpaTfihd4QMNwrz/ob2Bp7X04yuxJkjRGi4aKmOqwhov54i6u79oCv7T+C7lo70MKH6BesI3vscD1yb/yzKXQ=="], + "jose": ["jose@6.2.2", "", {}, "sha512-d7kPDd34KO/YnzaDOlikGpOurfF0ByC2sEV4cANCtdqLlTfBlw2p14O/5d/zv40gJPbIQxfES3nSx1/oYNyuZQ=="], "js-tokens": ["js-tokens@4.0.0", "", {}, "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ=="], @@ -755,6 +782,8 @@ "keyv": ["keyv@4.5.4", "", { "dependencies": { "json-buffer": "3.0.1" } }, "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw=="], + "kysely": ["kysely@0.28.15", "", {}, "sha512-r2clcf7HLWvDXaVUEvQymXJY4i3bSOIV3xsL/Upy3ZfSv5HeKsk9tsqbBptLvth5qHEIhxeHTA2jNLyQABkLBA=="], + "language-subtag-registry": ["language-subtag-registry@0.3.23", "", {}, "sha512-0K65Lea881pHotoGEa5gDlMxt3pctLi2RplBb7Ezh4rRdLEOtgi7n4EwK9lamnUCkKBqaeKRVebTq6BAxSkpXQ=="], "language-tags": ["language-tags@1.0.9", "", { "dependencies": { "language-subtag-registry": "^0.3.20" } }, "sha512-MbjN408fEndfiQXbFQ1vnd+1NoLDsnQW41410oQBXiyXDMYH5z505juWa4KUE1LqxRC7DgOgZDbKLxHIwm27hA=="], @@ -809,18 +838,16 @@ "nanoid": ["nanoid@5.1.6", "", { "bin": { "nanoid": "bin/nanoid.js" } }, "sha512-c7+7RQ+dMB5dPwwCp4ee1/iV/q2P6aK1mTZcfr1BTuVlyW9hJYiMPybJCcnBlQtuSmTIWNeazm/zqNoZSSElBg=="], + "nanostores": ["nanostores@1.2.0", "", {}, "sha512-F0wCzbsH80G7XXo0Jd9/AVQC7ouWY6idUCTnMwW5t/Rv9W8qmO6endavDwg7TNp5GbugwSukFMVZqzPSrSMndg=="], + "napi-postinstall": ["napi-postinstall@0.3.4", "", { "bin": { "napi-postinstall": "lib/cli.js" } }, "sha512-PHI5f1O0EP5xJ9gQmFGMS6IZcrVvTjpXjz7Na41gTE7eE2hK11lg04CECCYEEjdc17EV4DO+fkGEtt7TpTaTiQ=="], "natural-compare": ["natural-compare@1.4.0", "", {}, "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw=="], "next": ["next@15.4.10", "", { "dependencies": { "@next/env": "15.4.10", "@swc/helpers": "0.5.15", "caniuse-lite": "^1.0.30001579", "postcss": "8.4.31", "styled-jsx": "5.1.6" }, "optionalDependencies": { "@next/swc-darwin-arm64": "15.4.8", "@next/swc-darwin-x64": "15.4.8", "@next/swc-linux-arm64-gnu": "15.4.8", "@next/swc-linux-arm64-musl": "15.4.8", "@next/swc-linux-x64-gnu": "15.4.8", "@next/swc-linux-x64-musl": "15.4.8", "@next/swc-win32-arm64-msvc": "15.4.8", "@next/swc-win32-x64-msvc": "15.4.8", "sharp": "^0.34.3" }, "peerDependencies": { "@opentelemetry/api": "^1.1.0", "@playwright/test": "^1.51.1", "babel-plugin-react-compiler": "*", "react": "^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0", "react-dom": "^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0", "sass": "^1.3.0" }, "optionalPeers": ["@opentelemetry/api", "@playwright/test", "babel-plugin-react-compiler", "sass"], "bin": { "next": "dist/bin/next" } }, "sha512-itVlc79QjpKMFMRhP+kbGKaSG/gZM6RCvwhEbwmCNF06CdDiNaoHcbeg0PqkEa2GOcn8KJ0nnc7+yL7EjoYLHQ=="], - "next-auth": ["next-auth@5.0.0-beta.30", "", { "dependencies": { "@auth/core": "0.41.0" }, "peerDependencies": { "@simplewebauthn/browser": "^9.0.1", "@simplewebauthn/server": "^9.0.2", "next": "^14.0.0-0 || ^15.0.0 || ^16.0.0", "nodemailer": "^7.0.7", "react": "^18.2.0 || ^19.0.0" }, "optionalPeers": ["@simplewebauthn/browser", "@simplewebauthn/server", "nodemailer"] }, "sha512-+c51gquM3F6nMVmoAusRJ7RIoY0K4Ts9HCCwyy/BRoe4mp3msZpOzYMyb5LAYc1wSo74PMQkGDcaghIO7W6Xjg=="], - "next-themes": ["next-themes@0.4.6", "", { "peerDependencies": { "react": "^16.8 || ^17 || ^18 || ^19 || ^19.0.0-rc", "react-dom": "^16.8 || ^17 || ^18 || ^19 || ^19.0.0-rc" } }, "sha512-pZvgD5L0IEvX5/9GWyHMf3m8BKiVQwsCMHfoFosXtXBMnaS0ZnIJ9ST4b4NqLVKDEm8QBxoNNGNaBv2JNF6XNA=="], - "oauth4webapi": ["oauth4webapi@3.8.3", "", {}, "sha512-pQ5BsX3QRTgnt5HxgHwgunIRaDXBdkT23tf8dfzmtTIL2LTpdmxgbpbBm0VgFWAIDlezQvQCTgnVIUmHupXHxw=="], - "object-assign": ["object-assign@4.1.1", "", {}, "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg=="], "object-inspect": ["object-inspect@1.13.4", "", {}, "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew=="], @@ -887,10 +914,6 @@ "postgres-interval": ["postgres-interval@1.2.0", "", { "dependencies": { "xtend": "^4.0.0" } }, "sha512-9ZhXKM/rw350N1ovuWHbGxnGh/SNJ4cnxHiM0rxE4VN41wsg8P8zWn9hv/buK00RP4WvlOyr/RBDiptyxVbkZQ=="], - "preact": ["preact@10.24.3", "", {}, "sha512-Z2dPnBnMUfyQfSQ+GBdsGa16hz35YmLmtTLhM169uW944hYL6xzTYkJjC07j+Wosz733pMWx0fgON3JNw1jJQA=="], - - "preact-render-to-string": ["preact-render-to-string@6.5.11", "", { "peerDependencies": { "preact": ">=10" } }, "sha512-ubnauqoGczeGISiOh6RjX0/cdaF8v/oDXIjO85XALCQjwQP+SB4RDXXtvZ6yTYSjG+PC1QRP2AhPgCEsM2EvUw=="], - "prelude-ls": ["prelude-ls@1.2.1", "", {}, "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g=="], "prop-types": ["prop-types@15.8.1", "", { "dependencies": { "loose-envify": "^1.4.0", "object-assign": "^4.1.1", "react-is": "^16.13.1" } }, "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg=="], @@ -925,6 +948,8 @@ "reusify": ["reusify@1.1.0", "", {}, "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw=="], + "rou3": ["rou3@0.7.12", "", {}, "sha512-iFE4hLDuloSWcD7mjdCDhx2bKcIsYbtOTpfH5MHHLSKMOUyjqQXTeZVa289uuwEGEKFoE/BAPbhaU4B774nceg=="], + "run-parallel": ["run-parallel@1.2.0", "", { "dependencies": { "queue-microtask": "^1.2.2" } }, "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA=="], "safe-array-concat": ["safe-array-concat@1.1.3", "", { "dependencies": { "call-bind": "^1.0.8", "call-bound": "^1.0.2", "get-intrinsic": "^1.2.6", "has-symbols": "^1.1.0", "isarray": "^2.0.5" } }, "sha512-AURm5f0jYEOydBj7VQlVvDrjeFgthDdEF5H1dP+6mNpoXOMo1quQqJ4wvJDyRZ9+pO3kGWoOdmV08cSv2aJV6Q=="], @@ -937,6 +962,8 @@ "semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="], + "set-cookie-parser": ["set-cookie-parser@3.1.0", "", {}, "sha512-kjnC1DXBHcxaOaOXBHBeRtltsDG2nUiUni+jP92M9gYdW12rsmx92UsfpH7o5tDRs7I1ZZPSQJQGv3UaRfCiuw=="], + "set-function-length": ["set-function-length@1.2.2", "", { "dependencies": { "define-data-property": "^1.1.4", "es-errors": "^1.3.0", "function-bind": "^1.1.2", "get-intrinsic": "^1.2.4", "gopd": "^1.0.1", "has-property-descriptors": "^1.0.2" } }, "sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg=="], "set-function-name": ["set-function-name@2.0.2", "", { "dependencies": { "define-data-property": "^1.1.4", "es-errors": "^1.3.0", "functions-have-names": "^1.2.3", "has-property-descriptors": "^1.0.2" } }, "sha512-7PGFlmtwsEADb0WYyvCMa1t+yke6daIG4Wirafur5kcf+MhUnPms1UeR0CKQdTZD81yESwMHbtn+TR+dMviakQ=="], @@ -1053,6 +1080,8 @@ "yocto-queue": ["yocto-queue@0.1.0", "", {}, "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q=="], + "zod": ["zod@4.3.6", "", {}, "sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg=="], + "@esbuild-kit/core-utils/esbuild": ["esbuild@0.18.20", "", { "optionalDependencies": { "@esbuild/android-arm": "0.18.20", "@esbuild/android-arm64": "0.18.20", "@esbuild/android-x64": "0.18.20", "@esbuild/darwin-arm64": "0.18.20", "@esbuild/darwin-x64": "0.18.20", "@esbuild/freebsd-arm64": "0.18.20", "@esbuild/freebsd-x64": "0.18.20", "@esbuild/linux-arm": "0.18.20", "@esbuild/linux-arm64": "0.18.20", "@esbuild/linux-ia32": "0.18.20", "@esbuild/linux-loong64": "0.18.20", "@esbuild/linux-mips64el": "0.18.20", "@esbuild/linux-ppc64": "0.18.20", "@esbuild/linux-riscv64": "0.18.20", "@esbuild/linux-s390x": "0.18.20", "@esbuild/linux-x64": "0.18.20", "@esbuild/netbsd-x64": "0.18.20", "@esbuild/openbsd-x64": "0.18.20", "@esbuild/sunos-x64": "0.18.20", "@esbuild/win32-arm64": "0.18.20", "@esbuild/win32-ia32": "0.18.20", "@esbuild/win32-x64": "0.18.20" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-ceqxoedUrcayh7Y7ZX6NdbbDzGROiyVBgC4PriJThBKSVPWnnFHZAkfI1lJT8QFkOwH4qOS2SJkS4wvpGl8BpA=="], "@eslint-community/eslint-utils/eslint-visitor-keys": ["eslint-visitor-keys@3.4.3", "", {}, "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag=="], @@ -1103,8 +1132,6 @@ "next/postcss": ["postcss@8.4.31", "", { "dependencies": { "nanoid": "^3.3.6", "picocolors": "^1.0.0", "source-map-js": "^1.0.2" } }, "sha512-PS08Iboia9mts/2ygV3eLpY5ghnUcfLV/EXTOW1E2qYxJKGGBUtNjN76FYHnMs36RmARn41bC0AZmn+rR0OVpQ=="], - "next-auth/@auth/core": ["@auth/core@0.41.0", "", { "dependencies": { "@panva/hkdf": "^1.2.1", "jose": "^6.0.6", "oauth4webapi": "^3.3.0", "preact": "10.24.3", "preact-render-to-string": "6.5.11" }, "peerDependencies": { "@simplewebauthn/browser": "^9.0.1", "@simplewebauthn/server": "^9.0.2", "nodemailer": "^6.8.0" }, "optionalPeers": ["@simplewebauthn/browser", "@simplewebauthn/server", "nodemailer"] }, "sha512-Wd7mHPQ/8zy6Qj7f4T46vg3aoor8fskJm6g2Zyj064oQ3+p0xNZXAV60ww0hY+MbTesfu29kK14Zk5d5JTazXQ=="], - "postcss/nanoid": ["nanoid@3.3.11", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w=="], "sharp/semver": ["semver@7.7.3", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q=="], diff --git a/package.json b/package.json index 77f7495..b5b9ef9 100644 --- a/package.json +++ b/package.json @@ -9,13 +9,13 @@ "lint": "next lint" }, "dependencies": { - "@auth/drizzle-adapter": "^1.10.0", "@radix-ui/react-checkbox": "^1.3.3", "@radix-ui/react-dialog": "^1.1.15", "@radix-ui/react-dropdown-menu": "^2.1.16", "@radix-ui/react-label": "^2.1.7", "@radix-ui/react-select": "^2.2.6", "@radix-ui/react-slot": "^1.2.3", + "better-auth": "^1.6.0", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", "date-fns": "^4.1.0", @@ -26,7 +26,6 @@ "lucide-react": "^0.539.0", "nanoid": "^5.1.5", "next": "15.4.10", - "next-auth": "^5.0.0-beta.29", "next-themes": "^0.4.6", "pg": "^8.16.3", "postgres": "^3.4.7", From 3ab77cc21f8a70fdb45ea9ec7d44f67e9c933b54 Mon Sep 17 00:00:00 2001 From: Dmytro Stanchiev Date: Mon, 6 Apr 2026 22:40:41 -0400 Subject: [PATCH 04/25] refactor: update env vars for better-auth --- .env.production.example | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/.env.production.example b/.env.production.example index 07bfde2..01a1951 100644 --- a/.env.production.example +++ b/.env.production.example @@ -1,9 +1,8 @@ OPENROUTER_API_KEY= AUTH_AUTHENTIK_CLIENT_ID= -AUTH_AUTHENTIK_CLIENT_SECRET=notsosupersecret -AUTH_AUTHENTIK_ISSUER=https://example.com +AUTH_AUTHENTIK_CLIENT_SECRET=XXXXXXXXXXXXXXXX +AUTH_AUTHENTIK_ISSUER=XXXXXXXXXXXXXXXXXXX -NEXTAUTH_URL=https://example.com -AUTH_SECRET=supersecret -NEXTAUTH_SECRET=supersecret -DB_URL=postgres://:@:/ +BETTER_AUTH_URL=XXXXXXXXXXXXXXXXXXX +BETTER_AUTH_SECRET=XXXXXXXXXXX +DB_URL=XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX From febc57b240dfb92054639a8a713541b8b52ff977 Mon Sep 17 00:00:00 2001 From: Dmytro Stanchiev Date: Mon, 6 Apr 2026 22:41:00 -0400 Subject: [PATCH 05/25] refactor: update DB schema for better-auth conventions --- drizzle/0000_loose_catseye.sql | 57 +----- drizzle/0001_great_sentry.sql | 41 ++++ drizzle/meta/0001_snapshot.json | 328 ++++++++++++++++++++++++++++++++ drizzle/meta/_journal.json | 7 + src/db/schema.ts | 82 ++++---- 5 files changed, 414 insertions(+), 101 deletions(-) create mode 100644 drizzle/0001_great_sentry.sql create mode 100644 drizzle/meta/0001_snapshot.json diff --git a/drizzle/0000_loose_catseye.sql b/drizzle/0000_loose_catseye.sql index e7d7fd7..35e5681 100644 --- a/drizzle/0000_loose_catseye.sql +++ b/drizzle/0000_loose_catseye.sql @@ -1,56 +1 @@ --- Current sql file was generated after introspecting the database --- If you want to run this migration please uncomment this code before executing migrations -/* -CREATE TABLE "session" ( - "sessionToken" text PRIMARY KEY NOT NULL, - "userId" text NOT NULL, - "expires" timestamp NOT NULL -); ---> statement-breakpoint -CREATE TABLE "user" ( - "id" text PRIMARY KEY NOT NULL, - "name" text, - "email" text NOT NULL, - "emailVerified" timestamp, - "image" text -); ---> statement-breakpoint -CREATE TABLE "verificationToken" ( - "identifier" text NOT NULL, - "token" text NOT NULL, - "expires" timestamp NOT NULL, - CONSTRAINT "verificationToken_identifier_token_pk" PRIMARY KEY("identifier","token") -); ---> statement-breakpoint -CREATE TABLE "authenticator" ( - "credentialID" text NOT NULL, - "userId" text NOT NULL, - "providerAccountId" text NOT NULL, - "credentialPublicKey" text NOT NULL, - "counter" integer NOT NULL, - "credentialDeviceType" text NOT NULL, - "credentialBackedUp" boolean NOT NULL, - "transports" text, - CONSTRAINT "authenticator_userId_credentialID_pk" PRIMARY KEY("credentialID","userId"), - CONSTRAINT "authenticator_credentialID_unique" UNIQUE("credentialID") -); ---> statement-breakpoint -CREATE TABLE "account" ( - "userId" text NOT NULL, - "type" text NOT NULL, - "provider" text NOT NULL, - "providerAccountId" text NOT NULL, - "refresh_token" text, - "access_token" text, - "expires_at" text, - "token_type" text, - "scope" text, - "id_token" text, - "session_state" text, - CONSTRAINT "account_provider_providerAccountId_pk" PRIMARY KEY("provider","providerAccountId") -); ---> statement-breakpoint -ALTER TABLE "session" ADD CONSTRAINT "session_userId_user_id_fk" FOREIGN KEY ("userId") REFERENCES "public"."user"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint -ALTER TABLE "authenticator" ADD CONSTRAINT "authenticator_userId_user_id_fk" FOREIGN KEY ("userId") REFERENCES "public"."user"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint -ALTER TABLE "account" ADD CONSTRAINT "account_userId_user_id_fk" FOREIGN KEY ("userId") REFERENCES "public"."user"("id") ON DELETE cascade ON UPDATE no action; -*/ \ No newline at end of file +-- Baseline snapshot: tables already exist in the database diff --git a/drizzle/0001_great_sentry.sql b/drizzle/0001_great_sentry.sql new file mode 100644 index 0000000..1a0bf22 --- /dev/null +++ b/drizzle/0001_great_sentry.sql @@ -0,0 +1,41 @@ +ALTER TABLE "authenticator" DISABLE ROW LEVEL SECURITY;--> statement-breakpoint +DROP TABLE "authenticator" CASCADE;--> statement-breakpoint +ALTER TABLE "verificationToken" RENAME TO "verification";--> statement-breakpoint +ALTER TABLE "session" RENAME COLUMN "expires" TO "expiresAt";--> statement-breakpoint +ALTER TABLE "session" RENAME COLUMN "sessionToken" TO "token";--> statement-breakpoint +ALTER TABLE "verification" RENAME COLUMN "token" TO "value";--> statement-breakpoint +ALTER TABLE "verification" RENAME COLUMN "expires" TO "expiresAt";--> statement-breakpoint +ALTER TABLE "account" RENAME COLUMN "providerAccountId" TO "accountId";--> statement-breakpoint +ALTER TABLE "account" RENAME COLUMN "provider" TO "providerId";--> statement-breakpoint +ALTER TABLE "account" RENAME COLUMN "access_token" TO "accessToken";--> statement-breakpoint +ALTER TABLE "account" RENAME COLUMN "refresh_token" TO "refreshToken";--> statement-breakpoint +ALTER TABLE "account" RENAME COLUMN "expires_at" TO "accessTokenExpiresAt";--> statement-breakpoint +ALTER TABLE "account" RENAME COLUMN "id_token" TO "idToken";--> statement-breakpoint +ALTER TABLE "verification" DROP CONSTRAINT "verificationToken_identifier_token_pk";--> statement-breakpoint +ALTER TABLE "account" DROP CONSTRAINT "account_provider_providerAccountId_pk";--> statement-breakpoint +ALTER TABLE "user" ALTER COLUMN "emailVerified" SET DATA TYPE boolean USING ("emailVerified" IS NOT NULL);--> statement-breakpoint +ALTER TABLE "session" DROP CONSTRAINT "session_pkey";--> statement-breakpoint +ALTER TABLE "session" ADD COLUMN "id" text PRIMARY KEY NOT NULL DEFAULT gen_random_uuid();--> statement-breakpoint +ALTER TABLE "session" ADD COLUMN "createdAt" timestamp DEFAULT now();--> statement-breakpoint +ALTER TABLE "session" ADD COLUMN "updatedAt" timestamp DEFAULT now();--> statement-breakpoint +ALTER TABLE "session" ADD COLUMN "ipAddress" text;--> statement-breakpoint +ALTER TABLE "session" ADD COLUMN "userAgent" text;--> statement-breakpoint +ALTER TABLE "user" ADD COLUMN "createdAt" timestamp DEFAULT now();--> statement-breakpoint +ALTER TABLE "user" ADD COLUMN "updatedAt" timestamp DEFAULT now();--> statement-breakpoint +ALTER TABLE "verification" ADD COLUMN "id" text PRIMARY KEY NOT NULL DEFAULT gen_random_uuid();--> statement-breakpoint +ALTER TABLE "verification" ADD COLUMN "createdAt" timestamp DEFAULT now();--> statement-breakpoint +ALTER TABLE "verification" ADD COLUMN "updatedAt" timestamp DEFAULT now();--> statement-breakpoint +ALTER TABLE "account" ADD COLUMN "id" text PRIMARY KEY NOT NULL DEFAULT gen_random_uuid();--> statement-breakpoint +ALTER TABLE "account" ADD COLUMN "refreshTokenExpiresAt" timestamp;--> statement-breakpoint +ALTER TABLE "account" ADD COLUMN "password" text;--> statement-breakpoint +ALTER TABLE "account" ADD COLUMN "createdAt" timestamp DEFAULT now();--> statement-breakpoint +ALTER TABLE "account" ADD COLUMN "updatedAt" timestamp DEFAULT now();--> statement-breakpoint +ALTER TABLE "account" DROP COLUMN "type";--> statement-breakpoint +ALTER TABLE "account" DROP COLUMN "token_type";--> statement-breakpoint +ALTER TABLE "account" DROP COLUMN "session_state";--> statement-breakpoint +ALTER TABLE "session" ADD CONSTRAINT "session_token_unique" UNIQUE("token");--> statement-breakpoint +ALTER TABLE "user" ADD CONSTRAINT "user_email_unique" UNIQUE("email"); +-- Drop the uuid defaults so future inserts rely on app-provided values +ALTER TABLE "session" ALTER COLUMN "id" DROP DEFAULT;--> statement-breakpoint +ALTER TABLE "verification" ALTER COLUMN "id" DROP DEFAULT;--> statement-breakpoint +ALTER TABLE "account" ALTER COLUMN "id" DROP DEFAULT; diff --git a/drizzle/meta/0001_snapshot.json b/drizzle/meta/0001_snapshot.json new file mode 100644 index 0000000..ebaba7c --- /dev/null +++ b/drizzle/meta/0001_snapshot.json @@ -0,0 +1,328 @@ +{ + "id": "69e7666b-0b8c-4658-906d-993870a0b539", + "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 + }, + "accountId": { + "name": "accountId", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "providerId": { + "name": "providerId", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "userId": { + "name": "userId", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "accessToken": { + "name": "accessToken", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "refreshToken": { + "name": "refreshToken", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "accessTokenExpiresAt": { + "name": "accessTokenExpiresAt", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "refreshTokenExpiresAt": { + "name": "refreshTokenExpiresAt", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "scope": { + "name": "scope", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "idToken": { + "name": "idToken", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "password": { + "name": "password", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "createdAt": { + "name": "createdAt", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "default": "now()" + }, + "updatedAt": { + "name": "updatedAt", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "account_userId_user_id_fk": { + "name": "account_userId_user_id_fk", + "tableFrom": "account", + "tableTo": "user", + "columnsFrom": [ + "userId" + ], + "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 + }, + "expiresAt": { + "name": "expiresAt", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "token": { + "name": "token", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "createdAt": { + "name": "createdAt", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "default": "now()" + }, + "updatedAt": { + "name": "updatedAt", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "default": "now()" + }, + "ipAddress": { + "name": "ipAddress", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "userAgent": { + "name": "userAgent", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "userId": { + "name": "userId", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "session_userId_user_id_fk": { + "name": "session_userId_user_id_fk", + "tableFrom": "session", + "tableTo": "user", + "columnsFrom": [ + "userId" + ], + "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": false + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "emailVerified": { + "name": "emailVerified", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "image": { + "name": "image", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "createdAt": { + "name": "createdAt", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "default": "now()" + }, + "updatedAt": { + "name": "updatedAt", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "user_email_unique": { + "name": "user_email_unique", + "nullsNotDistinct": false, + "columns": [ + "email" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.verification": { + "name": "verification", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "identifier": { + "name": "identifier", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "value": { + "name": "value", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "expiresAt": { + "name": "expiresAt", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "createdAt": { + "name": "createdAt", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "default": "now()" + }, + "updatedAt": { + "name": "updatedAt", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + } + }, + "enums": {}, + "schemas": {}, + "sequences": {}, + "roles": {}, + "policies": {}, + "views": {}, + "_meta": { + "columns": {}, + "schemas": {}, + "tables": {} + } +} \ No newline at end of file diff --git a/drizzle/meta/_journal.json b/drizzle/meta/_journal.json index 612f104..353b4e2 100644 --- a/drizzle/meta/_journal.json +++ b/drizzle/meta/_journal.json @@ -8,6 +8,13 @@ "when": 1755586325384, "tag": "0000_loose_catseye", "breakpoints": true + }, + { + "idx": 1, + "version": "7", + "when": 1775526538601, + "tag": "0001_great_sentry", + "breakpoints": true } ] } \ No newline at end of file diff --git a/src/db/schema.ts b/src/db/schema.ts index 08dbac6..421df04 100644 --- a/src/db/schema.ts +++ b/src/db/schema.ts @@ -1,55 +1,47 @@ -import { pgTable, text, timestamp, integer, boolean, primaryKey } from 'drizzle-orm/pg-core'; +import { pgTable, text, timestamp, boolean } from 'drizzle-orm/pg-core'; -export const users = pgTable('user', { +export const user = pgTable('user', { id: text('id').primaryKey(), name: text('name'), - email: text('email').notNull(), - emailVerified: timestamp('emailVerified', { mode: 'string' }), + email: text('email').notNull().unique(), + emailVerified: boolean('emailVerified').default(false), image: text('image'), + createdAt: timestamp('createdAt').defaultNow(), + updatedAt: timestamp('updatedAt').defaultNow(), }); -export const accounts = pgTable('account', { - userId: text('userId').notNull().references(() => users.id, { onDelete: 'cascade' }), - type: text('type').notNull(), - provider: text('provider').notNull(), - providerAccountId: text('providerAccountId').notNull(), - refresh_token: text('refresh_token'), - access_token: text('access_token'), - expires_at: text('expires_at'), - token_type: text('token_type'), +export const session = pgTable('session', { + id: text('id').primaryKey(), + expiresAt: timestamp('expiresAt').notNull(), + token: text('token').notNull().unique(), + createdAt: timestamp('createdAt').defaultNow(), + updatedAt: timestamp('updatedAt').defaultNow(), + ipAddress: text('ipAddress'), + userAgent: text('userAgent'), + userId: text('userId').notNull().references(() => user.id, { onDelete: 'cascade' }), +}); + +export const account = pgTable('account', { + id: text('id').primaryKey(), + accountId: text('accountId').notNull(), + providerId: text('providerId').notNull(), + userId: text('userId').notNull().references(() => user.id, { onDelete: 'cascade' }), + accessToken: text('accessToken'), + refreshToken: text('refreshToken'), + accessTokenExpiresAt: timestamp('accessTokenExpiresAt'), + refreshTokenExpiresAt: timestamp('refreshTokenExpiresAt'), scope: text('scope'), - id_token: text('id_token'), - session_state: text('session_state'), -}, (account) => ({ - compoundKey: primaryKey({ columns: [account.provider, account.providerAccountId] }) -})); - -export const sessions = pgTable('session', { - sessionToken: text().primaryKey().notNull(), - userId: text().notNull().references(() => users.id, { onDelete: 'cascade' }), - expires: timestamp({ mode: 'string' }).notNull(), + idToken: text('idToken'), + password: text('password'), + createdAt: timestamp('createdAt').defaultNow(), + updatedAt: timestamp('updatedAt').defaultNow(), }); -export const verificationTokens = pgTable('verificationToken', { +export const verification = pgTable('verification', { + id: text('id').primaryKey(), identifier: text('identifier').notNull(), - token: text('token').notNull(), - expires: timestamp('expires', { mode: 'string' }).notNull(), -}, (vt) => ({ - compoundKey: primaryKey({ columns: [vt.identifier, vt.token] }) -})); - -export const authenticators = pgTable('authenticator', { - credentialID: text('credentialID').notNull().unique(), - userId: text('userId').notNull().references(() => users.id, { onDelete: 'cascade' }), - providerAccountId: text('providerAccountId').notNull(), - credentialPublicKey: text('credentialPublicKey').notNull(), - counter: integer('counter').notNull(), - credentialDeviceType: text('credentialDeviceType').notNull(), - credentialBackedUp: boolean('credentialBackedUp').notNull(), - transports: text('transports'), -}, (authenticator) => ({ - compositePK: primaryKey({ - columns: [authenticator.credentialID, authenticator.userId], - name: "authenticator_userId_credentialID_pk" - }) -})); \ No newline at end of file + value: text('value').notNull(), + expiresAt: timestamp('expiresAt').notNull(), + createdAt: timestamp('createdAt').defaultNow(), + updatedAt: timestamp('updatedAt').defaultNow(), +}); From 08a894577baaf83450e68b9eae41c8791d8bedbd Mon Sep 17 00:00:00 2001 From: Dmytro Stanchiev Date: Mon, 6 Apr 2026 22:41:11 -0400 Subject: [PATCH 06/25] refactor: replace next-auth with better-auth core and client --- src/app/api/auth/[...all]/route.ts | 4 ++ src/app/api/auth/[...nextauth]/route.ts | 2 - src/auth.ts | 53 ++++++++++--------------- src/lib/auth-client.ts | 5 +++ 4 files changed, 31 insertions(+), 33 deletions(-) create mode 100644 src/app/api/auth/[...all]/route.ts delete mode 100644 src/app/api/auth/[...nextauth]/route.ts create mode 100644 src/lib/auth-client.ts diff --git a/src/app/api/auth/[...all]/route.ts b/src/app/api/auth/[...all]/route.ts new file mode 100644 index 0000000..0b0883d --- /dev/null +++ b/src/app/api/auth/[...all]/route.ts @@ -0,0 +1,4 @@ +import { auth } from "@/auth"; +import { toNextJsHandler } from "better-auth/next-js"; + +export const { GET, POST } = toNextJsHandler(auth); diff --git a/src/app/api/auth/[...nextauth]/route.ts b/src/app/api/auth/[...nextauth]/route.ts deleted file mode 100644 index 7c62e2d..0000000 --- a/src/app/api/auth/[...nextauth]/route.ts +++ /dev/null @@ -1,2 +0,0 @@ -import { handlers } from "@/auth"; -export const { GET, POST } = handlers; diff --git a/src/auth.ts b/src/auth.ts index 46ec624..801afea 100644 --- a/src/auth.ts +++ b/src/auth.ts @@ -1,35 +1,26 @@ -import NextAuth, { NextAuthConfig, NextAuthResult } from "next-auth"; -import Authentik from "next-auth/providers/authentik"; -import type { Provider } from "next-auth/providers"; -import { DrizzleAdapter } from "@auth/drizzle-adapter"; +import { betterAuth } from "better-auth"; +import { drizzleAdapter } from "better-auth/adapters/drizzle"; +import { genericOAuth } from "better-auth/plugins"; import { db } from "@/db/index"; +import * as schema from "@/db/schema"; -const providers: Provider[] = [ - Authentik({ - clientId: process.env.AUTH_AUTHENTIK_CLIENT_ID, - clientSecret: process.env.AUTH_AUTHENTIK_CLIENT_SECRET, - issuer: process.env.AUTH_AUTHENTIK_ISSUER, +export const auth = betterAuth({ + baseURL: process.env.BETTER_AUTH_URL, + database: drizzleAdapter(db, { + provider: "pg", + schema, }), -]; - -export const providerMap = providers.map((provider) => { - if (typeof provider === "function") { - const providerData = provider(); - return { id: providerData.id, name: providerData.name }; - } else { - return { id: provider.id, name: provider.name }; - } + plugins: [ + genericOAuth({ + config: [ + { + providerId: "authentik", + clientId: process.env.AUTH_AUTHENTIK_CLIENT_ID!, + clientSecret: process.env.AUTH_AUTHENTIK_CLIENT_SECRET!, + discoveryUrl: `${process.env.AUTH_AUTHENTIK_ISSUER}/.well-known/openid-configuration`, + scopes: ["openid", "email", "profile"], + }, + ], + }), + ], }); - -const config = { - adapter: DrizzleAdapter(db), - providers, - pages: { - signIn: "/auth/signin", - signOut: "/auth/signout", - error: "/auth/error", - }, - trustHost: true, -} satisfies NextAuthConfig; -export const { handlers, signIn, signOut, auth }: NextAuthResult = - NextAuth(config); diff --git a/src/lib/auth-client.ts b/src/lib/auth-client.ts new file mode 100644 index 0000000..68f3bf1 --- /dev/null +++ b/src/lib/auth-client.ts @@ -0,0 +1,5 @@ +import { createAuthClient } from "better-auth/react"; + +export const authClient = createAuthClient(); + +export const { useSession, signIn, signOut } = authClient; From 490c601dc1b49752860db83d4389df98f54f13c7 Mon Sep 17 00:00:00 2001 From: Dmytro Stanchiev Date: Mon, 6 Apr 2026 22:41:25 -0400 Subject: [PATCH 07/25] refactor: remove next-auth SessionProvider wrapper --- src/components/SessionProvider.tsx | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/components/SessionProvider.tsx b/src/components/SessionProvider.tsx index 1ab533c..5e36b50 100644 --- a/src/components/SessionProvider.tsx +++ b/src/components/SessionProvider.tsx @@ -1,6 +1,5 @@ "use client" -import { SessionProvider } from "next-auth/react" import { ReactNode } from "react" interface Props { @@ -8,5 +7,5 @@ interface Props { } export default function AuthSessionProvider({ children }: Props) { - return {children} + return <>{children} } From d7d52ef1a813373fdc46353409064f660eb5ab9d Mon Sep 17 00:00:00 2001 From: Dmytro Stanchiev Date: Mon, 6 Apr 2026 22:41:37 -0400 Subject: [PATCH 08/25] refactor: migrate auth pages to better-auth client --- src/app/auth/signin/page.tsx | 45 ++++++++++++++++++++------------ src/app/auth/signout/page.tsx | 48 ++++++++++++++++++++--------------- src/components/sign-in.tsx | 8 +++--- 3 files changed, 61 insertions(+), 40 deletions(-) diff --git a/src/app/auth/signin/page.tsx b/src/app/auth/signin/page.tsx index a6791a3..3497e46 100644 --- a/src/app/auth/signin/page.tsx +++ b/src/app/auth/signin/page.tsx @@ -1,15 +1,35 @@ -import { signIn, auth } from "@/auth" +"use client" + +import { signIn, useSession } from "@/lib/auth-client" import { Button } from "@/components/ui/button" import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card" -import { redirect } from "next/navigation" import Link from "next/link" +import { useRouter } from "next/navigation" +import { useEffect } from "react" -export default async function SignInPage() { - const session = await auth() +export default function SignInPage() { + const { data: session, isPending } = useSession() + const router = useRouter() + + useEffect(() => { + if (session?.user) { + router.push("/") + } + }, [session, router]) + + const handleSignIn = async () => { + await signIn.social({ + provider: "authentik", + callbackURL: "/", + }) + } + + if (isPending) { + return null + } - // If already signed in, redirect to home if (session?.user) { - redirect("/") + return null } return ( @@ -22,16 +42,9 @@ export default async function SignInPage() { -
{ - "use server" - await signIn("authentik", { redirectTo: "/" }) - }} - > - -
+
diff --git a/src/app/auth/signout/page.tsx b/src/app/auth/signout/page.tsx index e676031..889d3e7 100644 --- a/src/app/auth/signout/page.tsx +++ b/src/app/auth/signout/page.tsx @@ -1,14 +1,29 @@ -import { signOut, auth } from "@/auth" +"use client" + +import { signOut, useSession } from "@/lib/auth-client" import { Button } from "@/components/ui/button" import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card" -import { redirect } from "next/navigation" import Link from "next/link" +import { useRouter } from "next/navigation" +import { useEffect } from "react" -export default async function SignOutPage() { - const session = await auth() - - if (!session) { - redirect("/") +export default function SignOutPage() { + const { data: session, isPending } = useSession() + const router = useRouter() + + useEffect(() => { + if (!session) { + router.push("/") + } + }, [session, router]) + + const handleSignOut = async () => { + await signOut() + router.push("/") + } + + if (isPending || !session) { + return null } return ( @@ -25,19 +40,12 @@ export default async function SignOutPage() {
Currently signed in as
{session.user?.name || session.user?.email}
- +
-
{ - "use server" - await signOut({ redirectTo: "/" }) - }} - > - -
- + + @@ -46,4 +54,4 @@ export default async function SignOutPage() {
) -} \ No newline at end of file +} diff --git a/src/components/sign-in.tsx b/src/components/sign-in.tsx index f1bff96..5bddf77 100644 --- a/src/components/sign-in.tsx +++ b/src/components/sign-in.tsx @@ -1,20 +1,20 @@ "use client" -import { signOut, useSession } from "next-auth/react" +import { signOut, useSession } from "@/lib/auth-client" import { Button } from "@/components/ui/button" import { useRouter } from "next/navigation" export default function SignIn() { - const { data: session, status } = useSession() + const { data: session, isPending } = useSession() const router = useRouter() const handleSignOut = async () => { - await signOut({ redirect: false }) + await signOut() router.push("/") router.refresh() } - if (status === "loading") { + if (isPending) { return
} From 15be2399c6b8568fd9e7161a14ed9127328c1eca Mon Sep 17 00:00:00 2001 From: Dmytro Stanchiev Date: Mon, 6 Apr 2026 22:41:57 -0400 Subject: [PATCH 09/25] refactor: migrate session usage to better-auth API --- src/app/api/ai-event/route.ts | 5 ++++- src/app/page.tsx | 8 ++++---- src/components/ai-toolbar.tsx | 15 ++++++--------- 3 files changed, 14 insertions(+), 14 deletions(-) diff --git a/src/app/api/ai-event/route.ts b/src/app/api/ai-event/route.ts index 715d29b..5afd938 100644 --- a/src/app/api/ai-event/route.ts +++ b/src/app/api/ai-event/route.ts @@ -1,8 +1,11 @@ import { NextResponse } from "next/server"; import { auth } from "@/auth"; +import { headers } from "next/headers"; export async function POST(request: Request) { - const session = await auth(); + const session = await auth.api.getSession({ + headers: await headers(), + }); if (!session?.user) { return NextResponse.json( diff --git a/src/app/page.tsx b/src/app/page.tsx index a832e3e..3fdf1bc 100644 --- a/src/app/page.tsx +++ b/src/app/page.tsx @@ -2,7 +2,7 @@ import { useEffect, useState } from 'react' import { nanoid } from 'nanoid' -import { useSession } from 'next-auth/react' +import { useSession } from '@/lib/auth-client' import { toast } from 'sonner' import { saveEvent as addEvent, deleteEvent, getEvents as getAllEvents, clearEvents, updateEvent } from '@/lib/events-db' @@ -44,7 +44,7 @@ export default function HomePage() { })() }, []) - const { data: session, status } = useSession() + const { data: session, isPending } = useSession() const resetForm = () => { setTitle('') @@ -256,8 +256,8 @@ export default function HomePage() { onImport={handleImport} > void aiLoading: boolean @@ -16,8 +15,8 @@ interface AIToolbarProps { } export const AIToolbar = ({ - session, - status, + isAuthenticated, + isPending, aiPrompt, setAiPrompt, aiLoading, @@ -28,17 +27,15 @@ export const AIToolbar = ({ }: AIToolbarProps) => { return ( <> - {/* AI Toolbar */} - {status === "loading" ? ( + {isPending ? (
Loading...
) : (
- {session?.user ? ( + {isAuthenticated ? (