From 302f3cb3f5c2e103552f133a80f81198971efd47 Mon Sep 17 00:00:00 2001 From: Sean O'Connor Date: Wed, 14 Jan 2026 02:33:20 -0500 Subject: [PATCH] feat: add oidc support with authentik --- .env.example | 8 + bun.lock | 70 ++++-- package.json | 6 +- src/app/auth/signin/page.tsx | 34 +++ .../_components/appearance-settings.tsx | 13 -- .../settings/_components/mode-switcher.tsx | 39 ---- .../settings/_components/settings-content.tsx | 23 +- .../settings/_components/theme-selector.tsx | 62 ----- src/app/layout.tsx | 22 +- src/components/AuthRedirect.tsx | 1 + src/components/analytics/umami-script.tsx | 4 + src/components/layout/navbar.tsx | 1 + src/components/layout/sidebar.tsx | 1 + src/components/navigation/sidebar-trigger.tsx | 5 +- .../animation-preferences-provider.tsx | 17 +- .../providers/color-theme-provider.tsx | 217 ------------------ src/components/providers/theme-provider.tsx | 111 --------- .../theme/accent-color-switcher.tsx | 148 ------------ src/components/theme/theme-switcher.tsx | 24 -- src/components/ui/sonner.tsx | 5 +- src/env.js | 7 + src/lib/auth-client.ts | 58 ++++- src/lib/auth.ts | 26 ++- src/server/api/routers/settings.ts | 12 +- src/styles/globals.css | 50 ++-- 25 files changed, 252 insertions(+), 712 deletions(-) delete mode 100644 src/app/dashboard/settings/_components/appearance-settings.tsx delete mode 100644 src/app/dashboard/settings/_components/mode-switcher.tsx delete mode 100644 src/app/dashboard/settings/_components/theme-selector.tsx delete mode 100644 src/components/providers/color-theme-provider.tsx delete mode 100644 src/components/providers/theme-provider.tsx delete mode 100644 src/components/theme/accent-color-switcher.tsx delete mode 100644 src/components/theme/theme-switcher.tsx diff --git a/.env.example b/.env.example index f62e0a2..1079fd2 100644 --- a/.env.example +++ b/.env.example @@ -33,3 +33,11 @@ NEXT_PUBLIC_UMAMI_WEBSITE_ID="your-website-id-here" NEXT_PUBLIC_UMAMI_SCRIPT_URL="https://analytics.umami.is/script.js" # Build tweaks # SKIP_ENV_VALIDATION=1 + +# SSO / Authentik (Optional - only needed if using SSO authentication) +# Configure these if you want to enable Single Sign-On with Authentik OIDC +# The issuer should be your Authentik application's OAuth2 provider URL +# Example: https://auth.example.com/application/o/your-app-slug +AUTHENTIK_ISSUER="" +AUTHENTIK_CLIENT_ID="" +AUTHENTIK_CLIENT_SECRET="" \ No newline at end of file diff --git a/bun.lock b/bun.lock index a3dec39..1f43a7e 100644 --- a/bun.lock +++ b/bun.lock @@ -5,6 +5,7 @@ "": { "name": "beenvoice", "dependencies": { + "@better-auth/sso": "^1.4.12", "@dnd-kit/core": "^6.3.1", "@dnd-kit/modifiers": "^9.0.0", "@dnd-kit/sortable": "^10.0.0", @@ -39,7 +40,7 @@ "@trpc/react-query": "^11.7.2", "@trpc/server": "^11.7.2", "bcryptjs": "^3.0.3", - "better-auth": "^1.4.6", + "better-auth": "^1.4.12", "chrono-node": "^2.9.0", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", @@ -49,7 +50,6 @@ "framer-motion": "^12.23.26", "lucide-react": "^0.525.0", "next": "^16.0.10", - "next-themes": "^0.4.6", "pg": "^8.16.3", "react": "^19.2.3", "react-day-picker": "^9.12.0", @@ -79,7 +79,7 @@ "eslint-config-next": "^16.0.10", "eslint-plugin-drizzle": "^0.2.3", "postcss": "^8.5.6", - "prettier": "^3.7.4", + "prettier": "3.6.2", "prettier-plugin-tailwindcss": "^0.6.14", "tailwindcss": "^4.1.18", "tailwindcss-animate": "^1.0.7", @@ -98,6 +98,8 @@ "packages": { "@alloc/quick-lru": ["@alloc/quick-lru@5.2.0", "", {}, "sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw=="], + "@authenio/xml-encryption": ["@authenio/xml-encryption@2.0.2", "", { "dependencies": { "@xmldom/xmldom": "^0.8.6", "escape-html": "^1.0.3", "xpath": "0.0.32" } }, "sha512-cTlrKttbrRHEw3W+0/I609A2Matj5JQaRvfLtEIGZvlN0RaPi+3ANsMeqAyCAVlH/lUIW2tmtBlSMni74lcXeg=="], + "@babel/code-frame": ["@babel/code-frame@7.27.1", "", { "dependencies": { "@babel/helper-validator-identifier": "^7.27.1", "js-tokens": "^4.0.0", "picocolors": "^1.1.1" } }, "sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg=="], "@babel/compat-data": ["@babel/compat-data@7.28.5", "", {}, "sha512-6uFXyCayocRbqhZOB+6XcuZbkMNimwfVGFji8CTZnCzOHVGvDqzvitu1re2AU5LROliz7eQPhB8CpAMvnx9EjA=="], @@ -132,13 +134,15 @@ "@babel/types": ["@babel/types@7.28.5", "", { "dependencies": { "@babel/helper-string-parser": "^7.27.1", "@babel/helper-validator-identifier": "^7.28.5" } }, "sha512-qQ5m48eI/MFLQ5PxQj4PFaprjyCTLI37ElWMmNs0K8Lk3dVeOdNpB3ks8jc7yM5CDmVC73eMVk/trk3fgmrUpA=="], - "@better-auth/core": ["@better-auth/core@1.4.6", "", { "dependencies": { "@standard-schema/spec": "^1.0.0", "zod": "^4.1.12" }, "peerDependencies": { "@better-auth/utils": "0.3.0", "@better-fetch/fetch": "1.1.18", "better-call": "1.1.5", "jose": "^6.1.0", "kysely": "^0.28.5", "nanostores": "^1.0.1" } }, "sha512-cYjscr4wU5ZJPhk86JuUkecJT+LSYCFmUzYaitiLkizl+wCr1qdPFSEoAnRVZVTUEEoKpeS2XW69voBJ1NoB3g=="], + "@better-auth/core": ["@better-auth/core@1.4.12", "", { "dependencies": { "@standard-schema/spec": "^1.0.0", "zod": "^4.1.12" }, "peerDependencies": { "@better-auth/utils": "0.3.0", "@better-fetch/fetch": "1.1.21", "better-call": "1.1.7", "jose": "^6.1.0", "kysely": "^0.28.5", "nanostores": "^1.0.1" } }, "sha512-VfqZwMAEl9rnGx092BIZ2Q5z8rt7jjN2OAbvPqehufSKZGmh8JsdtZRBMl/CHQir9bwi2Ev0UF4+7TQp+DXEMg=="], - "@better-auth/telemetry": ["@better-auth/telemetry@1.4.6", "", { "dependencies": { "@better-auth/utils": "0.3.0", "@better-fetch/fetch": "1.1.18" }, "peerDependencies": { "@better-auth/core": "1.4.6" } }, "sha512-idc9MGJXxWA7zl2U9zsbdG6+2ZCeqWdPq1KeFSfyqGMFtI1VPQOx9YWLqNPOt31YnOX77ojZSraU2sb7IRdBMA=="], + "@better-auth/sso": ["@better-auth/sso@1.4.12", "", { "dependencies": { "@better-auth/utils": "0.3.0", "@better-fetch/fetch": "1.1.21", "fast-xml-parser": "^5.2.5", "jose": "^6.1.0", "samlify": "^2.10.1", "zod": "^4.1.12" }, "peerDependencies": { "better-auth": "1.4.12" } }, "sha512-sZ8kakq1/LLfJUbigR6EXcQQ+Q0VI8qtkBWOJihhcZ5Vn5CV7WYPm3jfSAPHe0mR5NtzydkvfSEkO2V3pdMk3g=="], + + "@better-auth/telemetry": ["@better-auth/telemetry@1.4.12", "", { "dependencies": { "@better-auth/utils": "0.3.0", "@better-fetch/fetch": "1.1.21" }, "peerDependencies": { "@better-auth/core": "1.4.12" } }, "sha512-4q504Og42PzkUbZjXDt+FyeYaS0WZmAlEOC3nbBCZDObTVCRUnGgJW52B2maJ7BCVvAQgBGLEeQmQzU5+63J0A=="], "@better-auth/utils": ["@better-auth/utils@0.3.0", "", {}, "sha512-W+Adw6ZA6mgvnSnhOki270rwJ42t4XzSK6YWGF//BbVXL6SwCLWfyzBc1lN2m/4RM28KubdBKQ4X5VMoLRNPQw=="], - "@better-fetch/fetch": ["@better-fetch/fetch@1.1.18", "", {}, "sha512-rEFOE1MYIsBmoMJtQbl32PGHHXuG2hDxvEd7rUHE0vCBoFQVSDqaVs9hkZEtHCxRoY+CljXKFCOuJ8uxqw1LcA=="], + "@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=="], @@ -686,6 +690,10 @@ "@unrs/resolver-binding-win32-x64-msvc": ["@unrs/resolver-binding-win32-x64-msvc@1.11.1", "", { "os": "win32", "cpu": "x64" }, "sha512-lrW200hZdbfRtztbygyaq/6jP6AKE8qQN2KvPcJ+x7wiD038YtnYtZ82IMNJ69GJibV7bwL3y9FgK+5w/pYt6g=="], + "@xmldom/is-dom-node": ["@xmldom/is-dom-node@1.0.1", "", {}, "sha512-CJDxIgE5I0FH+ttq/Fxy6nRpxP70+e2O048EPe85J2use3XKdatVM7dDVvFNjQudd9B49NPoZ+8PG49zj4Er8Q=="], + + "@xmldom/xmldom": ["@xmldom/xmldom@0.8.11", "", {}, "sha512-cQzWCtO6C8TQiYl1ruKNn2U6Ao4o4WBBcbL61yJl84x+j5sOWWFU9X7DpND8XZG3daDppSsigMdfAIl2upQBRw=="], + "abs-svg-path": ["abs-svg-path@0.1.1", "", {}, "sha512-d8XPSGjfyzlXC3Xx891DJRyZfqk5JU0BJrDQcsWomFIV1/BIzPW5HDH5iDdWpqWaav0YVIEzT1RHTwWr0FFshA=="], "acorn": ["acorn@8.15.0", "", { "bin": { "acorn": "bin/acorn" } }, "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg=="], @@ -718,6 +726,8 @@ "arraybuffer.prototype.slice": ["arraybuffer.prototype.slice@1.0.4", "", { "dependencies": { "array-buffer-byte-length": "^1.0.1", "call-bind": "^1.0.8", "define-properties": "^1.2.1", "es-abstract": "^1.23.5", "es-errors": "^1.3.0", "get-intrinsic": "^1.2.6", "is-array-buffer": "^3.0.4" } }, "sha512-BNoCY6SXXPQ7gF2opIP4GBE+Xw7U+pHMYKuzjgCN3GwiaIR09UUeKfheyIry77QtrCBlC0KK0q5/TER/tYh3PQ=="], + "asn1": ["asn1@0.2.6", "", { "dependencies": { "safer-buffer": "~2.1.0" } }, "sha512-ix/FxPn0MDjeyJ7i/yoHGFt/EX6LyNbxSEhPPXODPL+KB0VPk86UYfL0lMdy+KCnv+fmvIzySwaK5COwqVbWTQ=="], + "ast-types-flow": ["ast-types-flow@0.0.8", "", {}, "sha512-OH/2E5Fg20h2aPrbe+QL8JZQFko0YZaF+j4mnQ7BGhfavO7OpSLa8a0y9sBwomHdSbkhTS8TQNayBfnW5DwbvQ=="], "async-function": ["async-function@1.0.0", "", {}, "sha512-hsU18Ae8CDTR6Kgu9DYf0EbCr/a5iGL0rytQDobUcdpYOKokk8LEjVphnXkDkgpi0wYVsqrXuP0bZxJaTqdgoA=="], @@ -738,9 +748,9 @@ "bcryptjs": ["bcryptjs@3.0.3", "", { "bin": { "bcrypt": "bin/bcrypt" } }, "sha512-GlF5wPWnSa/X5LKM1o0wz0suXIINz1iHRLvTS+sLyi7XPbe5ycmYI3DlZqVGZZtDgl4DmasFg7gOB3JYbphV5g=="], - "better-auth": ["better-auth@1.4.6", "", { "dependencies": { "@better-auth/core": "1.4.6", "@better-auth/telemetry": "1.4.6", "@better-auth/utils": "0.3.0", "@better-fetch/fetch": "1.1.18", "@noble/ciphers": "^2.0.0", "@noble/hashes": "^2.0.0", "better-call": "1.1.5", "defu": "^6.1.4", "jose": "^6.1.0", "kysely": "^0.28.5", "ms": "4.0.0-nightly.202508271359", "nanostores": "^1.0.1", "zod": "^4.1.12" }, "peerDependencies": { "@lynx-js/react": "*", "@sveltejs/kit": "^2.0.0", "@tanstack/react-start": "^1.0.0", "next": "^14.0.0 || ^15.0.0 || ^16.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", "vue": "^3.0.0" }, "optionalPeers": ["@lynx-js/react", "@sveltejs/kit", "@tanstack/react-start", "next", "react", "react-dom", "solid-js", "svelte", "vue"] }, "sha512-5wEBzjolrQA26b4uT6FVVYICsE3SmE/MzrZtl8cb2a3TJtswpP8v3OVV5yTso+ef9z85swgZk0/qBzcULFWVtA=="], + "better-auth": ["better-auth@1.4.12", "", { "dependencies": { "@better-auth/core": "1.4.12", "@better-auth/telemetry": "1.4.12", "@better-auth/utils": "0.3.0", "@better-fetch/fetch": "1.1.21", "@noble/ciphers": "^2.0.0", "@noble/hashes": "^2.0.0", "better-call": "1.1.7", "defu": "^6.1.4", "jose": "^6.1.0", "kysely": "^0.28.5", "nanostores": "^1.0.1", "zod": "^4.1.12" }, "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", "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", "better-sqlite3", "drizzle-kit", "drizzle-orm", "mongodb", "mysql2", "next", "pg", "prisma", "react", "react-dom", "solid-js", "svelte", "vitest", "vue"] }, "sha512-FsFMnWgk+AGrxsIGbpWLCibgYcbm6uNhPHln3ohXFDXSRa0gk39Beuh54Q+x6ml2qYodF0snxf/tPtDpBI/JiA=="], - "better-call": ["better-call@1.1.5", "", { "dependencies": { "@better-auth/utils": "^0.3.0", "@better-fetch/fetch": "^1.1.4", "rou3": "^0.7.10", "set-cookie-parser": "^2.7.1" }, "peerDependencies": { "zod": "^4.0.0" }, "optionalPeers": ["zod"] }, "sha512-nQJ3S87v6wApbDwbZ++FrQiSiVxWvZdjaO+2v6lZJAG2WWggkB2CziUDjPciz3eAt9TqfRursIQMZIcpkBnvlw=="], + "better-call": ["better-call@1.1.7", "", { "dependencies": { "@better-auth/utils": "^0.3.0", "@better-fetch/fetch": "^1.1.4", "rou3": "^0.7.10", "set-cookie-parser": "^2.7.1" }, "peerDependencies": { "zod": "^4.0.0" }, "optionalPeers": ["zod"] }, "sha512-6gaJe1bBIEgVebQu/7q9saahVzvBsGaByEnE8aDVncZEDiJO7sdNB28ot9I6iXSbR25egGmmZ6aIURXyQHRraQ=="], "bidi-js": ["bidi-js@1.0.3", "", { "dependencies": { "require-from-string": "^2.0.2" } }, "sha512-RKshQI1R3YQ+n9YJz2QQ147P66ELpa1FQEg20Dk8oW9t2KgLbpDLLp9aGZ7y8WHSshDknG0bknqGw5/tyCs5tw=="], @@ -764,6 +774,8 @@ "callsites": ["callsites@3.1.0", "", {}, "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ=="], + "camelcase": ["camelcase@6.3.0", "", {}, "sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA=="], + "caniuse-lite": ["caniuse-lite@1.0.30001757", "", {}, "sha512-r0nnL/I28Zi/yjk1el6ilj27tKcdjLsNqAOZr0yVjWPrSQyHgKI2INaEWw21bAQSv2LXRt1XuCS/GomNpWOxsQ=="], "chalk": ["chalk@4.1.2", "", { "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" } }, "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA=="], @@ -906,6 +918,8 @@ "escalade": ["escalade@3.2.0", "", {}, "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA=="], + "escape-html": ["escape-html@1.0.3", "", {}, "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow=="], + "escape-string-regexp": ["escape-string-regexp@4.0.0", "", {}, "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA=="], "eslint": ["eslint@9.39.1", "", { "dependencies": { "@eslint-community/eslint-utils": "^4.8.0", "@eslint-community/regexpp": "^4.12.1", "@eslint/config-array": "^0.21.1", "@eslint/config-helpers": "^0.4.2", "@eslint/core": "^0.17.0", "@eslint/eslintrc": "^3.3.1", "@eslint/js": "9.39.1", "@eslint/plugin-kit": "^0.4.1", "@humanfs/node": "^0.16.6", "@humanwhocodes/module-importer": "^1.0.1", "@humanwhocodes/retry": "^0.4.2", "@types/estree": "^1.0.6", "ajv": "^6.12.4", "chalk": "^4.0.0", "cross-spawn": "^7.0.6", "debug": "^4.3.2", "escape-string-regexp": "^4.0.0", "eslint-scope": "^8.4.0", "eslint-visitor-keys": "^4.2.1", "espree": "^10.4.0", "esquery": "^1.5.0", "esutils": "^2.0.2", "fast-deep-equal": "^3.1.3", "file-entry-cache": "^8.0.0", "find-up": "^5.0.0", "glob-parent": "^6.0.2", "ignore": "^5.2.0", "imurmurhash": "^0.1.4", "is-glob": "^4.0.0", "json-stable-stringify-without-jsonify": "^1.0.1", "lodash.merge": "^4.6.2", "minimatch": "^3.1.2", "natural-compare": "^1.4.0", "optionator": "^0.9.3" }, "peerDependencies": { "jiti": "*" }, "optionalPeers": ["jiti"], "bin": { "eslint": "bin/eslint.js" } }, "sha512-BhHmn2yNOFA9H9JmmIVKJmd288g9hrVRDkdoIgRCRuSySRUHH7r/DI6aAXW9T1WwUuY3DFgrcaqB+deURBLR5g=="], @@ -956,6 +970,8 @@ "fast-levenshtein": ["fast-levenshtein@2.0.6", "", {}, "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw=="], + "fast-xml-parser": ["fast-xml-parser@5.3.3", "", { "dependencies": { "strnum": "^2.1.0" }, "bin": { "fxparser": "src/cli/cli.js" } }, "sha512-2O3dkPAAC6JavuMm8+4+pgTk+5hoAs+CjZ+sWcQLkX9+/tHRuTkQh/Oaifr8qDmZ8iEHb771Ea6G8CdwkrgvYA=="], + "fastq": ["fastq@1.19.1", "", { "dependencies": { "reusify": "^1.0.4" } }, "sha512-GwLTyxkCXjXbxqIhTsMI2Nui8huMPtnxg7krajPJAjnEG/iiOS7i+zCtWGZR9G0NBKbXKh6X9m9UIsYX/N6vvQ=="], "fdir": ["fdir@6.4.6", "", { "peerDependencies": { "picomatch": "^3 || ^4" }, "optionalPeers": ["picomatch"] }, "sha512-hiFoqpyZcfNm1yc4u8oWCf9A2c4D3QjCrks3zmoVKVxpQRzmPNar1hUJcBG2RQHvEVGDN+Jm81ZheVLAQMK6+w=="], @@ -1212,7 +1228,7 @@ "motion-utils": ["motion-utils@12.23.6", "", {}, "sha512-eAWoPgr4eFEOFfg2WjIsMoqJTW6Z8MTUCgn/GZ3VRpClWBdnbjryiA3ZSNLyxCTmCQx4RmYX6jX1iWHbenUPNQ=="], - "ms": ["ms@4.0.0-nightly.202508271359", "", {}, "sha512-WC/Eo7NzFrOV/RRrTaI0fxKVbNCzEy76j2VqNV8SxDf9D69gSE2Lh0QwYvDlhiYmheBYExAvEAxVf5NoN0cj2A=="], + "ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="], "nanoid": ["nanoid@3.3.11", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w=="], @@ -1224,10 +1240,12 @@ "next": ["next@16.0.10", "", { "dependencies": { "@next/env": "16.0.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": "16.0.10", "@next/swc-darwin-x64": "16.0.10", "@next/swc-linux-arm64-gnu": "16.0.10", "@next/swc-linux-arm64-musl": "16.0.10", "@next/swc-linux-x64-gnu": "16.0.10", "@next/swc-linux-x64-musl": "16.0.10", "@next/swc-win32-arm64-msvc": "16.0.10", "@next/swc-win32-x64-msvc": "16.0.10", "sharp": "^0.34.4" }, "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-RtWh5PUgI+vxlV3HdR+IfWA1UUHu0+Ram/JBO4vWB54cVPentCD0e+lxyAYEsDTqGGMg7qpjhKh6dc6aW7W/sA=="], - "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=="], + "node-forge": ["node-forge@1.3.3", "", {}, "sha512-rLvcdSyRCyouf6jcOIPe/BgwG/d7hKjzMKOas33/pHEr6gbq18IK9zV7DiPvzsz0oBJPme6qr6H6kGZuI9/DZg=="], "node-releases": ["node-releases@2.0.27", "", {}, "sha512-nmh3lCkYZ3grZvqcCH+fjmQ7X+H0OeZgP40OierEaAptX4XofMh5kwNbWh7lBduUzCcV/8kZ+NDLCwm2iorIlA=="], + "node-rsa": ["node-rsa@1.1.1", "", { "dependencies": { "asn1": "^0.2.4" } }, "sha512-Jd4cvbJMryN21r5HgxQOpMEqv+ooke/korixNNK3mGqfGJmy0M77WDDzo/05969+OkMy3XW1UuZsSmW9KQm7Fw=="], + "normalize-svg-path": ["normalize-svg-path@1.1.0", "", { "dependencies": { "svg-arc-to-cubic-bezier": "^3.0.0" } }, "sha512-r9KHKG2UUeB5LoTouwDzBy2VxXlHsiM6fyLQvnJa0S5hrhzqElH/CH7TUGhT1fVvIYBIKf3OpY4YJ4CK+iaqHg=="], "object-assign": ["object-assign@4.1.1", "", {}, "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg=="], @@ -1308,7 +1326,7 @@ "prelude-ls": ["prelude-ls@1.2.1", "", {}, "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g=="], - "prettier": ["prettier@3.7.4", "", { "bin": { "prettier": "bin/prettier.cjs" } }, "sha512-v6UNi1+3hSlVvv8fSaoUbggEM5VErKmmpGA7Pl3HF8V6uKY7rvClBOJlH6yNwQtfTueNkGVpOv/mtWL9L4bgRA=="], + "prettier": ["prettier@3.6.2", "", { "bin": { "prettier": "bin/prettier.cjs" } }, "sha512-I7AIg5boAr5R0FFtJ6rCfD+LFsWHp81dolrFD8S79U9tb8Az2nGrJncnMSnys+bpQJfRUzqs9hnA81OAA3hCuQ=="], "prettier-plugin-tailwindcss": ["prettier-plugin-tailwindcss@0.6.14", "", { "peerDependencies": { "@ianvs/prettier-plugin-sort-imports": "*", "@prettier/plugin-hermes": "*", "@prettier/plugin-oxc": "*", "@prettier/plugin-pug": "*", "@shopify/prettier-plugin-liquid": "*", "@trivago/prettier-plugin-sort-imports": "*", "@zackad/prettier-plugin-twig": "*", "prettier": "^3.0", "prettier-plugin-astro": "*", "prettier-plugin-css-order": "*", "prettier-plugin-import-sort": "*", "prettier-plugin-jsdoc": "*", "prettier-plugin-marko": "*", "prettier-plugin-multiline-arrays": "*", "prettier-plugin-organize-attributes": "*", "prettier-plugin-organize-imports": "*", "prettier-plugin-sort-imports": "*", "prettier-plugin-style-order": "*", "prettier-plugin-svelte": "*" }, "optionalPeers": ["@ianvs/prettier-plugin-sort-imports", "@prettier/plugin-hermes", "@prettier/plugin-oxc", "@prettier/plugin-pug", "@shopify/prettier-plugin-liquid", "@trivago/prettier-plugin-sort-imports", "@zackad/prettier-plugin-twig", "prettier-plugin-astro", "prettier-plugin-css-order", "prettier-plugin-import-sort", "prettier-plugin-jsdoc", "prettier-plugin-marko", "prettier-plugin-multiline-arrays", "prettier-plugin-organize-attributes", "prettier-plugin-organize-imports", "prettier-plugin-sort-imports", "prettier-plugin-style-order", "prettier-plugin-svelte"] }, "sha512-pi2e/+ZygeIqntN+vC573BcW5Cve8zUB0SSAGxqpB4f96boZF4M3phPVoOFCeypwkpRYdi7+jQ5YJJUwrkGUAg=="], @@ -1418,6 +1436,10 @@ "safe-regex-test": ["safe-regex-test@1.1.0", "", { "dependencies": { "call-bound": "^1.0.2", "es-errors": "^1.3.0", "is-regex": "^1.2.1" } }, "sha512-x/+Cz4YrimQxQccJf5mKEbIa1NzeCRNI5Ecl/ekmlYaampdNLPalVyIcCZNNH3MvmqBugV5TMYZXv0ljslUlaw=="], + "safer-buffer": ["safer-buffer@2.1.2", "", {}, "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg=="], + + "samlify": ["samlify@2.10.2", "", { "dependencies": { "@authenio/xml-encryption": "^2.0.2", "@xmldom/xmldom": "^0.8.6", "camelcase": "^6.2.0", "node-forge": "^1.3.0", "node-rsa": "^1.1.1", "pako": "^1.0.10", "uuid": "^8.3.2", "xml": "^1.0.1", "xml-crypto": "^6.1.2", "xml-escape": "^1.1.0", "xpath": "^0.0.32" } }, "sha512-y5s1cHwclqwP8h7K2Wj9SfP1q+1S9+jrs5OAegYTLAiuFi7nDvuKqbiXLmUTvYPMpzHcX94wTY2+D604jgTKvA=="], + "scheduler": ["scheduler@0.27.0", "", {}, "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q=="], "selderee": ["selderee@0.11.0", "", { "dependencies": { "parseley": "^0.12.0" } }, "sha512-5TF+l7p4+OsnP8BCCvSyZiSPc4x4//p5uPwK8TCnVPJYRmU2aYKMpOXvw8zM5a5JvuuCGN1jmsMwuU2W02ukfA=="], @@ -1484,6 +1506,8 @@ "strip-json-comments": ["strip-json-comments@3.1.1", "", {}, "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig=="], + "strnum": ["strnum@2.1.2", "", {}, "sha512-l63NF9y/cLROq/yqKXSLtcMeeyOfnSQlfMSlzFt/K73oIaD8DGaQWd7Z34X9GPiKqP5rbSh84Hl4bOlLcjiSrQ=="], + "styled-jsx": ["styled-jsx@5.1.6", "", { "dependencies": { "client-only": "0.0.1" }, "peerDependencies": { "react": ">= 16.8.0 || 17.x.x || ^18.0.0-0 || ^19.0.0-0" } }, "sha512-qSVyDTeMotdvQYoHWLNGwRFJHC+i+ZvdBRYosOFgC+Wg1vx4frN2/RG/NA7SYqqvKNLf39P2LSRA2pu6n0XYZA=="], "superjson": ["superjson@2.2.6", "", { "dependencies": { "copy-anything": "^4" } }, "sha512-H+ue8Zo4vJmV2nRjpx86P35lzwDT3nItnIsocgumgr0hHMQ+ZGq5vrERg9kJBo5AWGmxZDhzDo+WVIJqkB0cGA=="], @@ -1556,6 +1580,8 @@ "util-deprecate": ["util-deprecate@1.0.2", "", {}, "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw=="], + "uuid": ["uuid@8.3.2", "", { "bin": { "uuid": "dist/bin/uuid" } }, "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg=="], + "victory-vendor": ["victory-vendor@37.3.6", "", { "dependencies": { "@types/d3-array": "^3.0.3", "@types/d3-ease": "^3.0.0", "@types/d3-interpolate": "^3.0.1", "@types/d3-scale": "^4.0.2", "@types/d3-shape": "^3.1.0", "@types/d3-time": "^3.0.0", "@types/d3-timer": "^3.0.0", "d3-array": "^3.1.6", "d3-ease": "^3.0.1", "d3-interpolate": "^3.0.1", "d3-scale": "^4.0.2", "d3-shape": "^3.1.0", "d3-time": "^3.0.0", "d3-timer": "^3.0.1" } }, "sha512-SbPDPdDBYp+5MJHhBCAyI7wKM3d5ivekigc2Dk2s7pgbZ9wIgIBYGVw4zGHBml/qTFbexrofXW6Gu4noGxrOwQ=="], "vite-compatible-readable-stream": ["vite-compatible-readable-stream@3.6.1", "", { "dependencies": { "inherits": "^2.0.3", "string_decoder": "^1.1.1", "util-deprecate": "^1.0.1" } }, "sha512-t20zYkrSf868+j/p31cRIGN28Phrjm3nRSLR2fyc2tiWi4cZGVdv68yNlwnIINTkMTmPoMiSlc0OadaO7DXZaQ=="], @@ -1574,6 +1600,14 @@ "word-wrap": ["word-wrap@1.2.5", "", {}, "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA=="], + "xml": ["xml@1.0.1", "", {}, "sha512-huCv9IH9Tcf95zuYCsQraZtWnJvBtLVE0QHMOs8bWyZAFZNDcYjsPq1nEx8jKA9y+Beo9v+7OBPRisQTjinQMw=="], + + "xml-crypto": ["xml-crypto@6.1.2", "", { "dependencies": { "@xmldom/is-dom-node": "^1.0.1", "@xmldom/xmldom": "^0.8.10", "xpath": "^0.0.33" } }, "sha512-leBOVQdVi8FvPJrMYoum7Ici9qyxfE4kVi+AkpUoYCSXaQF4IlBm1cneTK9oAxR61LpYxTx7lNcsnBIeRpGW2w=="], + + "xml-escape": ["xml-escape@1.1.0", "", {}, "sha512-B/T4sDK8Z6aUh/qNr7mjKAwwncIljFuUP+DO/D5hloYFj+90O88z8Wf7oSucZTHxBAsC1/CTP4rtx/x1Uf72Mg=="], + + "xpath": ["xpath@0.0.32", "", {}, "sha512-rxMJhSIoiO8vXcWvSifKqhvV96GjiD5wYb8/QHdoRyQvraTpp4IEv944nhGausZZ3u7dhQXteZuZbaqfpB7uYw=="], + "xtend": ["xtend@4.0.2", "", {}, "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ=="], "yallist": ["yallist@3.1.1", "", {}, "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g=="], @@ -1594,6 +1628,8 @@ "@better-auth/core/zod": ["zod@4.1.13", "", {}, "sha512-AvvthqfqrAhNH9dnfmrfKzX5upOdjUVJYFqNSlkmGf64gRaTzlPwz99IHYnVs28qYAybvAlBV+H7pn0saFY4Ig=="], + "@better-auth/sso/zod": ["zod@4.1.13", "", {}, "sha512-AvvthqfqrAhNH9dnfmrfKzX5upOdjUVJYFqNSlkmGf64gRaTzlPwz99IHYnVs28qYAybvAlBV+H7pn0saFY4Ig=="], + "@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=="], @@ -1636,8 +1672,6 @@ "@radix-ui/react-tooltip/@radix-ui/react-slot": ["@radix-ui/react-slot@1.2.3", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A=="], - "@react-email/render/prettier": ["prettier@3.6.2", "", { "bin": { "prettier": "bin/prettier.cjs" } }, "sha512-I7AIg5boAr5R0FFtJ6rCfD+LFsWHp81dolrFD8S79U9tb8Az2nGrJncnMSnys+bpQJfRUzqs9hnA81OAA3hCuQ=="], - "@react-pdf/reconciler/scheduler": ["scheduler@0.25.0-rc-603e6108-20241029", "", {}, "sha512-pFwF6H1XrSdYYNLfOcGlM28/j8CGLu8IvdrxqhjWULe2bPcKiKW4CV+OWqR/9fT52mywx65l7ysNkjLKBda7eA=="], "@tailwindcss/oxide-wasm32-wasi/@emnapi/core": ["@emnapi/core@1.7.1", "", { "dependencies": { "@emnapi/wasi-threads": "1.1.0", "tslib": "^2.4.0" }, "bundled": true }, "sha512-o1uhUASyo921r2XtHYOHy7gdkGLge8ghBEQHMWmyJFoXlpU58kIrhhN3w26lpQb6dspetweapMn2CSNwQ8I4wg=="], @@ -1668,8 +1702,6 @@ "cross-spawn/which": ["which@2.0.2", "", { "dependencies": { "isexe": "^2.0.0" }, "bin": { "node-which": "./bin/node-which" } }, "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA=="], - "debug/ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="], - "eslint-import-resolver-node/debug": ["debug@3.2.7", "", { "dependencies": { "ms": "^2.1.1" } }, "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ=="], "eslint-module-utils/debug": ["debug@3.2.7", "", { "dependencies": { "ms": "^2.1.1" } }, "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ=="], @@ -1698,6 +1730,8 @@ "unicode-trie/pako": ["pako@0.2.9", "", {}, "sha512-NUcwaKxUxWrZLpDG+z/xZaCgQITkA/Dv4V/T6bw7VON6l1Xz/VnrBqrYjZQ12TamKHzITTfOEIYUj48y2KXImA=="], + "xml-crypto/xpath": ["xpath@0.0.33", "", {}, "sha512-NNXnzrkDrAzalLhIUc01jO2mOzXGXh1JwPgkihcLLzw98c0WgYDmmjSh1Kl3wzaxSVWMuA+fe0WTWOBDWCBmNA=="], + "@esbuild-kit/core-utils/esbuild/@esbuild/android-arm": ["@esbuild/android-arm@0.18.20", "", { "os": "android", "cpu": "arm" }, "sha512-fyi7TDI/ijKKNZTUJAQqiG5T7YjJXgnzkURqmGj13C6dCqckZBLdl4h7bkhHt/t0WP+zO9/zwroDvANaOqO5Sw=="], "@esbuild-kit/core-utils/esbuild/@esbuild/android-arm64": ["@esbuild/android-arm64@0.18.20", "", { "os": "android", "cpu": "arm64" }, "sha512-Nz4rJcchGDtENV0eMKUNa6L12zz2zBDXuhj/Vjh18zGqB44Bi7MBMSXjgunJgjRhCmKOjnPuZp4Mb6OKqtMHLQ=="], @@ -1749,11 +1783,5 @@ "@typescript-eslint/typescript-estree/tinyglobby/picomatch": ["picomatch@4.0.3", "", {}, "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q=="], "cross-spawn/which/isexe": ["isexe@2.0.0", "", {}, "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw=="], - - "eslint-import-resolver-node/debug/ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="], - - "eslint-module-utils/debug/ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="], - - "eslint-plugin-import/debug/ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="], } } diff --git a/package.json b/package.json index 83d6ce7..d237e08 100644 --- a/package.json +++ b/package.json @@ -24,6 +24,7 @@ "typecheck": "tsc --noEmit" }, "dependencies": { + "@better-auth/sso": "^1.4.12", "@dnd-kit/core": "^6.3.1", "@dnd-kit/modifiers": "^9.0.0", "@dnd-kit/sortable": "^10.0.0", @@ -58,7 +59,7 @@ "@trpc/react-query": "^11.7.2", "@trpc/server": "^11.7.2", "bcryptjs": "^3.0.3", - "better-auth": "^1.4.6", + "better-auth": "^1.4.12", "chrono-node": "^2.9.0", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", @@ -68,7 +69,6 @@ "framer-motion": "^12.23.26", "lucide-react": "^0.525.0", "next": "^16.0.10", - "next-themes": "^0.4.6", "pg": "^8.16.3", "react": "^19.2.3", "react-day-picker": "^9.12.0", @@ -98,7 +98,7 @@ "eslint-config-next": "^16.0.10", "eslint-plugin-drizzle": "^0.2.3", "postcss": "^8.5.6", - "prettier": "^3.7.4", + "prettier": "3.6.2", "prettier-plugin-tailwindcss": "^0.6.14", "tailwindcss": "^4.1.18", "tailwindcss-animate": "^1.0.7", diff --git a/src/app/auth/signin/page.tsx b/src/app/auth/signin/page.tsx index 21827d4..4d83f8e 100644 --- a/src/app/auth/signin/page.tsx +++ b/src/app/auth/signin/page.tsx @@ -17,6 +17,7 @@ import { Users, FileText, TrendingUp, + Shield, } from "lucide-react"; function SignInForm() { @@ -47,6 +48,15 @@ function SignInForm() { } } + async function handleSocialSignIn() { + setLoading(true); + await authClient.signIn.sso({ + providerId: "authentik", + callbackURL: callbackUrl, + }); + setLoading(false); + } + return (
{/* Blob Background */} @@ -129,6 +139,30 @@ function SignInForm() {

+
+ + +
+
+ +
+
+ + Or continue with + +
+
+
+
diff --git a/src/app/dashboard/settings/_components/appearance-settings.tsx b/src/app/dashboard/settings/_components/appearance-settings.tsx deleted file mode 100644 index df60518..0000000 --- a/src/app/dashboard/settings/_components/appearance-settings.tsx +++ /dev/null @@ -1,13 +0,0 @@ -"use client"; - -import { ModeSwitcher } from "./mode-switcher"; -import { ThemeSelector } from "./theme-selector"; - -export function AppearanceSettings() { - return ( -
- - -
- ); -} diff --git a/src/app/dashboard/settings/_components/mode-switcher.tsx b/src/app/dashboard/settings/_components/mode-switcher.tsx deleted file mode 100644 index f39ae89..0000000 --- a/src/app/dashboard/settings/_components/mode-switcher.tsx +++ /dev/null @@ -1,39 +0,0 @@ -"use client"; - -import { useTheme } from "~/components/providers/theme-provider"; -import { Sun, Moon, Laptop } from "lucide-react"; -import { Tabs, TabsList, TabsTrigger } from "~/components/ui/tabs"; - -export function ModeSwitcher() { - const { theme, setTheme } = useTheme(); - - return ( -
-
- -

- {theme === "system" - ? "Follows system preference" - : `Currently in ${theme} mode`} -

-
- setTheme(value as "light" | "dark" | "system")} - className="w-auto" - > - - - - - - - - - - - - -
- ); -} diff --git a/src/app/dashboard/settings/_components/settings-content.tsx b/src/app/dashboard/settings/_components/settings-content.tsx index a8cb4b9..749b165 100644 --- a/src/app/dashboard/settings/_components/settings-content.tsx +++ b/src/app/dashboard/settings/_components/settings-content.tsx @@ -62,12 +62,12 @@ import { Textarea } from "~/components/ui/textarea"; import { api } from "~/trpc/react"; import { Switch } from "~/components/ui/switch"; import { Slider } from "~/components/ui/slider"; -import { AppearanceSettings } from "./appearance-settings"; import { useAnimationPreferences } from "~/components/providers/animation-preferences-provider"; import { Tabs, TabsContent, TabsList, TabsTrigger } from "~/components/ui/tabs"; export function SettingsContent() { const { data: session } = authClient.useSession(); + // const session = { user: null } as any; const [name, setName] = useState(""); const [deleteConfirmText, setDeleteConfirmText] = useState(""); const [importData, setImportData] = useState(""); @@ -294,7 +294,10 @@ export function SettingsContent() { if (profile?.name && !name) { setName(profile.name); } - }, [profile?.name, name]); + if (session?.user) { + setName(session.user.name ?? ""); + } + }, [session, profile?.name, name]); // (Removed direct DOM mutation; provider handles applying preferences globally) @@ -490,21 +493,7 @@ export function SettingsContent() { - {/* Appearance Settings */} - - - - - Appearance - - - Customize the look and feel of the application - - - - - - + {/* Theme follows system preferences automatically via CSS media queries */} {/* Accessibility & Animation */} diff --git a/src/app/dashboard/settings/_components/theme-selector.tsx b/src/app/dashboard/settings/_components/theme-selector.tsx deleted file mode 100644 index 418677d..0000000 --- a/src/app/dashboard/settings/_components/theme-selector.tsx +++ /dev/null @@ -1,62 +0,0 @@ -"use client"; - -import * as React from "react"; -import { Check } from "lucide-react"; -import { cn } from "~/lib/utils"; -import { - Tooltip, - TooltipContent, - TooltipProvider, - TooltipTrigger, -} from "~/components/ui/tooltip"; -import { Button } from "~/components/ui/button"; -import { useColorTheme } from "~/components/providers/color-theme-provider"; - -const themes = [ - { name: "slate", hex: "#64748b" }, - { name: "blue", hex: "#3b82f6" }, - { name: "green", hex: "#22c55e" }, - { name: "rose", hex: "#be123c" }, - { name: "orange", hex: "#ea580c" }, -]; - -export function ThemeSelector() { - const { colorTheme, setColorTheme } = useColorTheme(); - - return ( -
- -

- Select a theme for the application. -

-
- - {themes.map((t) => ( - - - - - -

{t.name}

-
-
- ))} -
-
-
- ); -} diff --git a/src/app/layout.tsx b/src/app/layout.tsx index 3f35217..f548e29 100644 --- a/src/app/layout.tsx +++ b/src/app/layout.tsx @@ -6,8 +6,8 @@ import { Inter, Playfair_Display, Geist_Mono } from "next/font/google"; import { TRPCReactProvider } from "~/trpc/react"; import { Toaster } from "~/components/ui/sonner"; import { AnimationPreferencesProvider } from "~/components/providers/animation-preferences-provider"; -import { ThemeProvider } from "~/components/providers/theme-provider"; -import { ColorThemeProvider } from "~/components/providers/color-theme-provider"; + + import { UmamiScript } from "~/components/analytics/umami-script"; export const metadata: Metadata = { @@ -51,17 +51,13 @@ export default function RootLayout({
- - - -
- {children} -
-
- - -
-
+ +
+ {children} +
+
+ +
diff --git a/src/components/AuthRedirect.tsx b/src/components/AuthRedirect.tsx index 57e7f6d..1a69127 100644 --- a/src/components/AuthRedirect.tsx +++ b/src/components/AuthRedirect.tsx @@ -6,6 +6,7 @@ import { useEffect } from "react"; export function AuthRedirect() { const { data: session, isPending } = authClient.useSession(); + // const session = { user: null }; const isPending = false; const router = useRouter(); useEffect(() => { diff --git a/src/components/analytics/umami-script.tsx b/src/components/analytics/umami-script.tsx index 8ba8e3e..eb83c95 100644 --- a/src/components/analytics/umami-script.tsx +++ b/src/components/analytics/umami-script.tsx @@ -4,6 +4,10 @@ import Script from "next/script"; import { env } from "~/env"; export function UmamiScript() { + if (process.env.NODE_ENV === "development") { + return null; + } + if (!env.NEXT_PUBLIC_UMAMI_WEBSITE_ID || !env.NEXT_PUBLIC_UMAMI_SCRIPT_URL) { return null; } diff --git a/src/components/layout/navbar.tsx b/src/components/layout/navbar.tsx index 8d15d98..db0e614 100644 --- a/src/components/layout/navbar.tsx +++ b/src/components/layout/navbar.tsx @@ -10,6 +10,7 @@ import { useRouter } from "next/navigation"; export function Navbar() { const { data: session, isPending } = authClient.useSession(); + // const session = { user: null } as any; const isPending = false; const [isMobileNavOpen, setIsMobileNavOpen] = useState(false); const router = useRouter(); diff --git a/src/components/layout/sidebar.tsx b/src/components/layout/sidebar.tsx index dec51e8..7e2bb39 100644 --- a/src/components/layout/sidebar.tsx +++ b/src/components/layout/sidebar.tsx @@ -34,6 +34,7 @@ interface SidebarProps { export function Sidebar({ mobile, onClose }: SidebarProps) { const pathname = usePathname(); const { data: session, isPending } = authClient.useSession(); + // const session = { user: null } as any; const isPending = false; const { isCollapsed, toggleCollapse } = useSidebar(); // If mobile, always expanded diff --git a/src/components/navigation/sidebar-trigger.tsx b/src/components/navigation/sidebar-trigger.tsx index 91eb118..a46283f 100644 --- a/src/components/navigation/sidebar-trigger.tsx +++ b/src/components/navigation/sidebar-trigger.tsx @@ -16,6 +16,7 @@ interface SidebarTriggerProps { export function SidebarTrigger({ isOpen, onToggle }: SidebarTriggerProps) { const pathname = usePathname(); const { isPending } = authClient.useSession(); + // const isPending = false; return ( <> @@ -71,8 +72,8 @@ export function SidebarTrigger({ isOpen, onToggle }: SidebarTriggerProps) { pathname === link.href ? "page" : undefined } className={`flex items-center gap-3 px-3 py-2.5 text-sm font-medium transition-colors ${pathname === link.href - ? "bg-primary/10 text-primary" - : "text-foreground hover:bg-muted" + ? "bg-primary/10 text-primary" + : "text-foreground hover:bg-muted" }`} onClick={onToggle} > diff --git a/src/components/providers/animation-preferences-provider.tsx b/src/components/providers/animation-preferences-provider.tsx index 0e7fc6c..e7e6ff5 100644 --- a/src/components/providers/animation-preferences-provider.tsx +++ b/src/components/providers/animation-preferences-provider.tsx @@ -53,7 +53,6 @@ import React, { useState, } from "react"; import { api } from "~/trpc/react"; -import { authClient } from "~/lib/auth-client"; type AnimationPreferences = { prefersReducedMotion: boolean; @@ -175,15 +174,16 @@ export function AnimationPreferencesProvider({ autoSync = true, }: AnimationPreferencesProviderProps) { const updateMutation = api.settings.updateAnimationPreferences.useMutation(); - const { data: session } = authClient.useSession(); - const isAuthed = !!session?.user; - // Server query only when authenticated + + // Server query - tRPC will handle authentication internally + // The query will only succeed if the user is authenticated const { data: serverPrefs } = api.settings.getAnimationPreferences.useQuery( undefined, { - enabled: isAuthed, + enabled: true, // Let tRPC handle auth refetchOnWindowFocus: false, staleTime: 60_000, + retry: false, // Don't retry if not authenticated }, ); @@ -279,7 +279,7 @@ export function AnimationPreferencesProvider({ // Optionally sync to server const shouldSync = opts?.sync ?? autoSync; - if (shouldSync && isAuthed) { + if (shouldSync && serverPrefs) { // If serverPrefs exists, user is authenticated pendingSyncRef.current = { prefersReducedMotion: patch.prefersReducedMotion, animationSpeedMultiplier: patch.animationSpeedMultiplier, @@ -315,7 +315,7 @@ export function AnimationPreferencesProvider({ animationSpeedMultiplier, autoSync, updateMutation, - isAuthed, + serverPrefs, ], ); @@ -323,7 +323,7 @@ export function AnimationPreferencesProvider({ useEffect(() => { if (!isHydratedRef.current) return; if (serverHydratedRef.current) return; - if (!isAuthed || !serverPrefs) return; + if (!serverPrefs) return; // No server prefs means not authenticated or not loaded yet const localIsDefault = prefersReducedMotion === DEFAULT_PREFERS_REDUCED && @@ -348,7 +348,6 @@ export function AnimationPreferencesProvider({ performUpdate, prefersReducedMotion, animationSpeedMultiplier, - isAuthed, ]); const updatePreferences = useCallback< diff --git a/src/components/providers/color-theme-provider.tsx b/src/components/providers/color-theme-provider.tsx deleted file mode 100644 index 6b8beb1..0000000 --- a/src/components/providers/color-theme-provider.tsx +++ /dev/null @@ -1,217 +0,0 @@ -"use client"; - -import * as React from "react"; -import { useTheme } from "./theme-provider"; -import { generateAccentColors } from "~/lib/color-utils"; -import { api } from "~/trpc/react"; -import { authClient } from "~/lib/auth-client"; - -type ColorTheme = "slate" | "blue" | "green" | "rose" | "orange" | "custom"; - -interface ColorThemeContextType { - colorTheme: ColorTheme; - setColorTheme: (theme: ColorTheme, customColor?: string) => void; - customColor?: string; -} - -const ColorThemeContext = React.createContext< - ColorThemeContextType | undefined ->(undefined); - -export function useColorTheme() { - const context = React.useContext(ColorThemeContext); - if (!context) { - throw new Error("useColorTheme must be used within a ColorThemeProvider"); - } - return context; -} - -interface ColorThemeProviderProps { - children: React.ReactNode; - defaultColorTheme?: ColorTheme; -} - -export function ColorThemeProvider({ - children, - defaultColorTheme = "slate", -}: ColorThemeProviderProps) { - const [colorTheme, setColorThemeState] = - React.useState(defaultColorTheme); - const [customColor, setCustomColor] = React.useState(); - const { theme: modeTheme } = useTheme(); - - // Auth & DB Sync - const { data: session } = authClient.useSession(); - const { data: dbTheme } = api.settings.getTheme.useQuery(undefined, { - enabled: !!session?.user, - staleTime: Infinity, // Only fetch once on mount/auth - }); - - const updateThemeMutation = api.settings.updateTheme.useMutation(); - - - - const setColorTheme = React.useCallback( - (theme: ColorTheme, customColor?: string) => { - const root = document.documentElement; - const themes: ColorTheme[] = ["slate", "blue", "green", "rose", "orange"]; - - // Clear any existing custom styles - const customProps = [ - "--primary", - "--accent", - "--ring", - "--secondary", - "--muted", - ]; - customProps.forEach((prop) => { - if (root.style.getPropertyValue(prop)) { - root.style.removeProperty(prop); - } - }); - - // Remove all theme classes - root.classList.remove(...themes); - - if (theme === "custom" && customColor) { - try { - const colors = generateAccentColors(customColor); - const themeColors = modeTheme === "dark" ? colors.dark : colors.light; - - Object.entries(themeColors).forEach(([key, value]) => { - root.style.setProperty(key, value); - }); - - setColorThemeState("custom"); - setCustomColor(customColor); - - // Persist custom theme locally - const themeData = { - color: customColor, - timestamp: Date.now(), - colors: colors, - }; - localStorage.setItem("customThemeColor", JSON.stringify(themeData)); - localStorage.setItem("isCustomTheme", "true"); - localStorage.removeItem("color-theme"); - } catch (error) { - console.error("Failed to apply custom theme:", error); - // Fallback to default - setColorThemeState(defaultColorTheme); - setCustomColor(undefined); - root.classList.add(defaultColorTheme); - localStorage.setItem("color-theme", defaultColorTheme); - return; // Don't sync failed theme - } - } else { - // Apply preset color theme by setting the appropriate class - setColorThemeState(theme); - setCustomColor(undefined); - root.classList.add(theme); - - // Clear custom theme storage - localStorage.removeItem("customThemeColor"); - localStorage.removeItem("isCustomTheme"); - - // Persist preset theme locally - localStorage.setItem("color-theme", theme); - } - - // Sync to DB if authenticated - // We check session inside the callback or pass it as dependency - // But since this is a callback, we'll use the mutation directly if we can - // However, we need to avoid infinite loops if the DB update triggers a re-render - // The mutation is stable. - }, - [modeTheme, defaultColorTheme], - ); - - // Sync from DB when available - React.useEffect(() => { - if (dbTheme) { - setColorTheme(dbTheme.colorTheme, dbTheme.customColor); - } - }, [dbTheme, setColorTheme]); - - // Effect to trigger DB update when state changes (debounced or direct) - // We do this separately to avoid putting mutation in the setColorTheme callback dependencies if possible - // But actually, calling it in setColorTheme is better for direct user action. - // The issue is `setColorTheme` is called by the `useEffect` that syncs FROM DB. - // So we need to distinguish between "user set theme" and "synced from DB". - // For now, we'll just let it be. If the DB sync calls setColorTheme, it will update state. - // If we add a DB update call here, it might be redundant but harmless if the value is same. - // BETTER APPROACH: Only call mutation when user interacts. - // But `setColorTheme` is exposed to consumers. - // Let's wrap the exposed `setColorTheme` to include the DB call. - - const handleSetColorTheme = React.useCallback( - (theme: ColorTheme, customColor?: string) => { - setColorTheme(theme, customColor); - - // Optimistic update is already done by setColorTheme (local state) - // Now sync to DB - if (session?.user) { - updateThemeMutation.mutate({ - colorTheme: theme, - customColor: theme === "custom" ? customColor : undefined, - }); - } - }, - [setColorTheme, session?.user, updateThemeMutation] - ); - - // Load saved theme on mount (Local Storage Fallback) - React.useEffect(() => { - // If we have DB data, that takes precedence (handled by other effect) - // But initially or if offline/unauth, use local storage - if (dbTheme) return; - - try { - const isCustom = localStorage.getItem("isCustomTheme") === "true"; - const savedThemeData = localStorage.getItem("customThemeColor"); - const savedColorTheme = localStorage.getItem("color-theme") as ColorTheme | null; - - if (isCustom && savedThemeData) { - const themeData = JSON.parse(savedThemeData) as { - color: string; - colors: Record; - }; - if (themeData?.color && themeData.colors) { - setColorTheme("custom", themeData.color); - return; - } - } - - if (savedColorTheme) { - setColorTheme(savedColorTheme); - } else { - setColorTheme(defaultColorTheme); - } - } catch (error) { - console.error("Failed to load theme:", error); - setColorTheme(defaultColorTheme); - } - }, [setColorTheme, defaultColorTheme, dbTheme]); - - // Re-apply custom theme when mode changes - React.useEffect(() => { - if (colorTheme === "custom" && customColor) { - setColorTheme("custom", customColor); - } - }, [modeTheme, colorTheme, customColor, setColorTheme]); - - const value = React.useMemo( - () => ({ - colorTheme, - setColorTheme: handleSetColorTheme, // Expose the wrapper - customColor, - }), - [colorTheme, customColor, handleSetColorTheme], - ); - - return ( - - {children} - - ); -} diff --git a/src/components/providers/theme-provider.tsx b/src/components/providers/theme-provider.tsx deleted file mode 100644 index 062e730..0000000 --- a/src/components/providers/theme-provider.tsx +++ /dev/null @@ -1,111 +0,0 @@ -"use client"; - -import * as React from "react"; -import { api } from "~/trpc/react"; -import { authClient } from "~/lib/auth-client"; - -type Theme = "dark" | "light" | "system"; - -type ThemeProviderProps = { - children: React.ReactNode; - defaultTheme?: Theme; - storageKey?: string; -}; - -type ThemeProviderState = { - theme: Theme; - setTheme: (theme: Theme) => void; -}; - -const initialState: ThemeProviderState = { - theme: "system", - setTheme: () => null, -}; - -const ThemeProviderContext = React.createContext(initialState); - -export function ThemeProvider({ - children, - defaultTheme = "system", - storageKey = "theme", - ...props -}: ThemeProviderProps) { - const [theme, setTheme] = React.useState(defaultTheme); - - // Auth & DB Sync - const { data: session } = authClient.useSession(); - const { data: dbTheme } = api.settings.getTheme.useQuery(undefined, { - enabled: !!session?.user, - staleTime: Infinity, - }); - - const updateThemeMutation = api.settings.updateTheme.useMutation(); - - // Sync from DB - React.useEffect(() => { - if (dbTheme?.theme) { - setTheme(dbTheme.theme); - } - }, [dbTheme]); - - React.useEffect(() => { - const savedTheme = localStorage.getItem(storageKey) as Theme | null; - if (savedTheme && !dbTheme) { - setTheme(savedTheme); - } - }, [storageKey, dbTheme]); - - React.useEffect(() => { - const root = window.document.documentElement; - - root.classList.remove("light", "dark"); - - if (theme === "system") { - const media = window.matchMedia("(prefers-color-scheme: dark)"); - const systemTheme = media.matches ? "dark" : "light"; - - root.classList.add(systemTheme); - - const listener = (e: MediaQueryListEvent) => { - const newTheme = e.matches ? "dark" : "light"; - root.classList.remove("light", "dark"); - root.classList.add(newTheme); - }; - - media.addEventListener("change", listener); - return () => media.removeEventListener("change", listener); - } - - root.classList.add(theme); - }, [theme]); - - const value = React.useMemo( - () => ({ - theme, - setTheme: (newTheme: Theme) => { - localStorage.setItem(storageKey, newTheme); - setTheme(newTheme); - - if (session?.user) { - updateThemeMutation.mutate({ theme: newTheme }); - } - }, - }), - [theme, storageKey, session?.user, updateThemeMutation] - ); - - return ( - - {children} - - ); -} - -export const useTheme = () => { - const context = React.useContext(ThemeProviderContext); - - if (context === undefined) - throw new Error("useTheme must be used within a ThemeProvider"); - - return context; -}; diff --git a/src/components/theme/accent-color-switcher.tsx b/src/components/theme/accent-color-switcher.tsx deleted file mode 100644 index 911a0f0..0000000 --- a/src/components/theme/accent-color-switcher.tsx +++ /dev/null @@ -1,148 +0,0 @@ -"use client"; - -import * as React from "react"; -import { Check, Palette } from "lucide-react"; -import { cn } from "~/lib/utils"; -import { Button } from "~/components/ui/button"; -import { - Popover, - PopoverContent, - PopoverTrigger, -} from "~/components/ui/popover"; -import { Label } from "~/components/ui/label"; -import { useColorTheme } from "~/components/providers/color-theme-provider"; - -const presetColors = [ - { name: "Slate", hex: "#64748b" }, - { name: "Blue", hex: "#3b82f6" }, - { name: "Green", hex: "#22c55e" }, - { name: "Rose", hex: "#be123c" }, - { name: "Orange", hex: "#ea580c" }, - { name: "Purple", hex: "#8b5cf6" }, - { name: "Teal", hex: "#14b8a6" }, - { name: "Pink", hex: "#ec4899" }, -]; - -export function AccentColorSwitcher() { - const { - colorTheme, - setColorTheme, - customColor: savedCustomColor, - } = useColorTheme(); - const [customColorInput, setCustomColorInput] = React.useState(""); - const [isCustom, setIsCustom] = React.useState(colorTheme === "custom"); - - React.useEffect(() => { - setIsCustom(colorTheme === "custom"); - if (savedCustomColor) { - setCustomColorInput(savedCustomColor); - } - }, [colorTheme, savedCustomColor]); - - const handleColorChange = (color: { name: string; hex: string }) => { - // eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-argument - setColorTheme(color.name.toLowerCase() as any); - }; - - const handleCustomColorSubmit = (e: React.FormEvent) => { - e.preventDefault(); - if (/^#[0-9A-F]{6}$/i.test(customColorInput)) { - setColorTheme("custom", customColorInput); - } - }; - - const handleReset = () => { - setColorTheme("slate"); - setCustomColorInput(""); - }; - - return ( -
-
- -

- Choose an accent color for your theme or create a custom one. -

-
- -
-
- {presetColors.map((color) => ( - - ))} -
- - - - - - - -
- -
- setCustomColorInput(e.target.value)} - className="flex-1 rounded-md border px-3 py-2" - /> - setCustomColorInput(e.target.value)} - className="h-10 w-12 cursor-pointer rounded-md border" - /> -
-
- - -
-
- - {isCustom && ( -
-
- Custom Theme Active - -
- )} -
-
- ); -} diff --git a/src/components/theme/theme-switcher.tsx b/src/components/theme/theme-switcher.tsx deleted file mode 100644 index 3ba4fb9..0000000 --- a/src/components/theme/theme-switcher.tsx +++ /dev/null @@ -1,24 +0,0 @@ -"use client"; - -import * as React from "react"; -import { Moon, Sun } from "lucide-react"; -import { useTheme } from "~/components/providers/theme-provider"; - -import { Button } from "~/components/ui/button"; - -export function ThemeSwitcher() { - const { setTheme, theme } = useTheme(); - - const toggleMode = () => { - const newMode = theme === "dark" ? "light" : "dark"; - setTheme(newMode); - }; - - return ( - - ); -} diff --git a/src/components/ui/sonner.tsx b/src/components/ui/sonner.tsx index bed2595..4f70dce 100644 --- a/src/components/ui/sonner.tsx +++ b/src/components/ui/sonner.tsx @@ -1,14 +1,11 @@ "use client"; import { Toaster as Sonner, type ToasterProps } from "sonner"; -import { useTheme } from "~/components/providers/theme-provider"; const Toaster = ({ ...props }: ToasterProps) => { - const { theme = "system" } = useTheme(); - return ( createAuthClient({ baseURL: process.env.NEXT_PUBLIC_APP_URL, + plugins: [ssoClient()], +}); + +// Type for the full client including plugins +type AuthClientType = ReturnType; + +// Lazy initialization - only create the client when first accessed +let _client: AuthClientType | undefined; + +function getClient(): AuthClientType { + if (typeof window === "undefined") { + // During SSR, return a safe mock that won't crash + // The actual client will only be used in the browser + // @ts-ignore - SSR mock doesn't need full client implementation + return { + // @ts-ignore + useSession: () => ({ data: null, isPending: false, error: null }), + // @ts-ignore + signIn: { email: async () => { }, social: async () => { }, sso: async () => { } }, + // @ts-ignore + signOut: async () => { }, + // @ts-ignore + signUp: { email: async () => { } }, + }; + } + + if (!_client) { + _client = createAuthClient({ + baseURL: process.env.NEXT_PUBLIC_APP_URL, + plugins: [ssoClient()], + }); + } + + return _client; +} + +// Export a Proxy that lazy-loads the client +export const authClient = new Proxy({} as AuthClientType, { + get(_target, prop) { + const client = getClient(); + const value = client[prop as keyof AuthClientType]; + return typeof value === "function" ? value.bind(client) : value; + }, }); diff --git a/src/lib/auth.ts b/src/lib/auth.ts index 096a0bd..1027644 100644 --- a/src/lib/auth.ts +++ b/src/lib/auth.ts @@ -1,6 +1,7 @@ import { betterAuth } from "better-auth"; import { drizzleAdapter } from "better-auth/adapters/drizzle"; import { nextCookies } from "better-auth/next-js"; +import { sso } from "@better-auth/sso"; import { db } from "~/server/db"; import * as schema from "~/server/db/schema"; @@ -28,5 +29,28 @@ export const auth = betterAuth({ }, }, }, - plugins: [nextCookies()], + plugins: [ + nextCookies(), + sso({ + // Only configure default SSO if Authentik credentials are provided + defaultSSO: + process.env.AUTHENTIK_ISSUER && + process.env.AUTHENTIK_CLIENT_ID && + process.env.AUTHENTIK_CLIENT_SECRET + ? [ + { + providerId: "authentik", + domain: "beenvoice.soconnor.dev", + oidcConfig: { + issuer: process.env.AUTHENTIK_ISSUER, + clientId: process.env.AUTHENTIK_CLIENT_ID, + clientSecret: process.env.AUTHENTIK_CLIENT_SECRET, + discoveryEndpoint: `${process.env.AUTHENTIK_ISSUER}/.well-known/openid-configuration`, + pkce: true, + }, + }, + ] + : [], + }), + ], }); diff --git a/src/server/api/routers/settings.ts b/src/server/api/routers/settings.ts index 7688c89..99f0bf4 100644 --- a/src/server/api/routers/settings.ts +++ b/src/server/api/routers/settings.ts @@ -1,7 +1,7 @@ import { z } from "zod"; import { eq } from "drizzle-orm"; import bcrypt from "bcryptjs"; -import { createTRPCRouter, protectedProcedure } from "~/server/api/trpc"; +import { createTRPCRouter, protectedProcedure, publicProcedure } from "~/server/api/trpc"; import { users, clients, @@ -92,7 +92,15 @@ export const settingsRouter = createTRPCRouter({ }), // Get animation preferences - getAnimationPreferences: protectedProcedure.query(async ({ ctx }) => { + getAnimationPreferences: publicProcedure.query(async ({ ctx }) => { + // Return defaults if not authenticated + if (!ctx.session?.user?.id) { + return { + prefersReducedMotion: false, + animationSpeedMultiplier: 1, + }; + } + const user = await ctx.db.query.users.findFirst({ where: eq(users.id, ctx.session.user.id), columns: { diff --git a/src/styles/globals.css b/src/styles/globals.css index 80d7d26..964d096 100644 --- a/src/styles/globals.css +++ b/src/styles/globals.css @@ -34,30 +34,32 @@ /* 16px Global Radius */ } - .dark { - --background: 240 10% 3.9%; - /* #09090B */ - --foreground: 0 0% 98%; - /* #FAFAFA */ - --card: 240 10% 3.9%; - --card-foreground: 0 0% 98%; - --popover: 240 10% 3.9%; - --popover-foreground: 0 0% 98%; - --primary: 0 0% 98%; - --primary-foreground: 240 5.9% 10%; - --secondary: 240 3.7% 20%; - /* #27272A */ - --secondary-foreground: 0 0% 98%; - --muted: 240 3.7% 15.9%; - --muted-foreground: 240 5% 64.9%; - --accent: 240 3.7% 15.9%; - --accent-foreground: 0 0% 98%; - --destructive: 0 62.8% 30.6%; - --destructive-foreground: 0 0% 98%; - --border: 240 3.7% 15.9%; - /* #27272A */ - --input: 240 3.7% 15.9%; - --ring: 240 4.9% 83.9%; + @media (prefers-color-scheme: dark) { + :root { + --background: 240 10% 3.9%; + /* #09090B */ + --foreground: 0 0% 98%; + /* #FAFAFA */ + --card: 240 10% 3.9%; + --card-foreground: 0 0% 98%; + --popover: 240 10% 3.9%; + --popover-foreground: 0 0% 98%; + --primary: 0 0% 98%; + --primary-foreground: 240 5.9% 10%; + --secondary: 240 3.7% 20%; + /* #27272A */ + --secondary-foreground: 0 0% 98%; + --muted: 240 3.7% 15.9%; + --muted-foreground: 240 5% 64.9%; + --accent: 240 3.7% 15.9%; + --accent-foreground: 0 0% 98%; + --destructive: 0 62.8% 30.6%; + --destructive-foreground: 0 0% 98%; + --border: 240 3.7% 15.9%; + /* #27272A */ + --input: 240 3.7% 15.9%; + --ring: 240 4.9% 83.9%; + } } }