From af2dd2c1188a29b876db9e1efb20ad609dce8280 Mon Sep 17 00:00:00 2001 From: LunaticFTW Date: Thu, 25 Sep 2025 01:25:38 +0800 Subject: [PATCH 01/16] feat: add authentication feature --- eslint.config.mjs | 32 +- package-lock.json | 1223 ++++++++++++++++- package.json | 22 +- prisma.config.ts | 7 + .../migration.sql | 203 +++ prisma/migrations/migration_lock.toml | 3 + prisma/schema.prisma | 246 ++-- prisma/seeds/index.ts | 18 + prisma/seeds/user.ts | 38 + src/app/api/auth/[...all]/route.ts | 4 + src/app/auth/login/_components/login-form.tsx | 196 +++ src/app/auth/login/page.tsx | 20 + .../auth/logout/_components/logout-alert.tsx | 47 + src/app/auth/logout/page.tsx | 20 + src/components/ui/alert-dialog.tsx | 157 +++ src/components/ui/alert.tsx | 66 + src/components/ui/button.tsx | 38 +- src/components/ui/checkbox.tsx | 29 + src/components/ui/input.tsx | 21 + src/components/ui/label.tsx | 21 + src/lib/auth-client.ts | 8 + src/lib/auth.ts | 74 + src/lib/authz.ts | 42 + src/lib/with-role.tsx | 29 + 24 files changed, 2429 insertions(+), 135 deletions(-) create mode 100644 prisma.config.ts create mode 100644 prisma/migrations/20250924144302_base_migration/migration.sql create mode 100644 prisma/migrations/migration_lock.toml create mode 100644 prisma/seeds/index.ts create mode 100644 prisma/seeds/user.ts create mode 100644 src/app/api/auth/[...all]/route.ts create mode 100644 src/app/auth/login/_components/login-form.tsx create mode 100644 src/app/auth/login/page.tsx create mode 100644 src/app/auth/logout/_components/logout-alert.tsx create mode 100644 src/app/auth/logout/page.tsx create mode 100644 src/components/ui/alert-dialog.tsx create mode 100644 src/components/ui/alert.tsx create mode 100644 src/components/ui/checkbox.tsx create mode 100644 src/components/ui/input.tsx create mode 100644 src/components/ui/label.tsx create mode 100644 src/lib/auth-client.ts create mode 100644 src/lib/auth.ts create mode 100644 src/lib/authz.ts create mode 100644 src/lib/with-role.tsx diff --git a/eslint.config.mjs b/eslint.config.mjs index 58d4ff9..9a02983 100644 --- a/eslint.config.mjs +++ b/eslint.config.mjs @@ -2,6 +2,7 @@ import { dirname } from 'path' import { fileURLToPath } from 'url' import { FlatCompat } from '@eslint/eslintrc' import eslintPluginPrettierRecommended from 'eslint-plugin-prettier/recommended' +import unicorn from 'eslint-plugin-unicorn' const __filename = fileURLToPath(import.meta.url) const __dirname = dirname(__filename) @@ -11,11 +12,40 @@ const compat = new FlatCompat({ }) const eslintConfig = [ - ...compat.extends('next/core-web-vitals', 'next/typescript', 'plugin:naming/recommended'), + ...compat.extends('next/core-web-vitals', 'next/typescript'), { ignores: ['node_modules/**', '.next/**', 'out/**', 'build/**', 'next-env.d.ts'], }, + { + plugins: { + unicorn, + }, + rules: { + 'unicorn/filename-case': [ + 'error', + { + case: 'kebabCase', + ignore: [ + // This rule applies to filenames, not directory names. + // The original regexes `/^\[.*\]$/` and `/^\(.*\)$/` didn't work because + // they don't account for file extensions (e.g., `.tsx`). + + // This updated regex correctly matches dynamic segment files + // like `[id].tsx` or `[...slug].tsx`. + /^\[.*\]\..+$/, + // This is for the uncommon case where a file is named like a route group, + // e.g., `(marketing).tsx`. + /^\(.*\)\..+$/, + + // This correctly ignores files that start with an underscore, + // such as `_app.tsx` or private utility files. + /^_/, + ], + }, + ], + }, + }, eslintPluginPrettierRecommended, ] diff --git a/package-lock.json b/package-lock.json index 1a5cefa..d47656c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,9 +9,14 @@ "version": "0.1.0", "dependencies": { "@prisma/client": "^6.16.2", + "@radix-ui/react-alert-dialog": "^1.1.15", "@radix-ui/react-avatar": "^1.1.10", + "@radix-ui/react-checkbox": "^1.3.3", + "@radix-ui/react-label": "^2.1.7", "@radix-ui/react-slot": "^1.2.3", "@tailwindcss/postcss": "^4.1.13", + "bcryptjs": "^3.0.2", + "better-auth": "^1.3.14", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", "lucide-react": "^0.544.0", @@ -30,14 +35,17 @@ "@types/react": "^19", "@types/react-dom": "^19", "@vitejs/plugin-react": "^5.0.3", + "dotenv-cli": "^10.0.0", "eslint": "^9", "eslint-config-next": "15.5.3", "eslint-config-prettier": "^10.1.8", "eslint-plugin-naming": "^0.1.10", "eslint-plugin-prettier": "^5.5.4", + "eslint-plugin-unicorn": "^61.0.2", "jsdom": "^27.0.0", "prettier": "3.6.2", "prisma": "^6.16.2", + "tsx": "^4.20.5", "tw-animate-css": "^1.3.8", "typescript": "^5", "vite-tsconfig-paths": "^5.1.4", @@ -436,6 +444,17 @@ "node": ">=6.9.0" } }, + "node_modules/@better-auth/utils": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/@better-auth/utils/-/utils-0.3.0.tgz", + "integrity": "sha512-W+Adw6ZA6mgvnSnhOki270rwJ42t4XzSK6YWGF//BbVXL6SwCLWfyzBc1lN2m/4RM28KubdBKQ4X5VMoLRNPQw==", + "license": "MIT" + }, + "node_modules/@better-fetch/fetch": { + "version": "1.1.18", + "resolved": "https://registry.npmjs.org/@better-fetch/fetch/-/fetch-1.1.18.tgz", + "integrity": "sha512-rEFOE1MYIsBmoMJtQbl32PGHHXuG2hDxvEd7rUHE0vCBoFQVSDqaVs9hkZEtHCxRoY+CljXKFCOuJ8uxqw1LcA==" + }, "node_modules/@csstools/color-helpers": { "version": "5.1.0", "resolved": "https://registry.npmjs.org/@csstools/color-helpers/-/color-helpers-5.1.0.tgz", @@ -1188,6 +1207,12 @@ "node": "^18.18.0 || ^20.9.0 || >=21.1.0" } }, + "node_modules/@hexagon/base64": { + "version": "1.1.28", + "resolved": "https://registry.npmjs.org/@hexagon/base64/-/base64-1.1.28.tgz", + "integrity": "sha512-lhqDEAvWixy3bZ+UOYbPwUbBkwBq5C1LAJ/xPC8Oi+lL54oyakv/npbA0aU2hgCsx/1NUd4IBvV03+aUBWxerw==", + "license": "MIT" + }, "node_modules/@humanfs/core": { "version": "0.19.1", "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.1.tgz", @@ -1725,6 +1750,12 @@ "@jridgewell/sourcemap-codec": "^1.4.14" } }, + "node_modules/@levischuck/tiny-cbor": { + "version": "0.2.11", + "resolved": "https://registry.npmjs.org/@levischuck/tiny-cbor/-/tiny-cbor-0.2.11.tgz", + "integrity": "sha512-llBRm4dT4Z89aRsm6u2oEZ8tfwL/2l6BwpZ7JcyieouniDECM5AqNgr/y08zalEIvW3RSK4upYyybDcmjXqAow==", + "license": "MIT" + }, "node_modules/@napi-rs/wasm-runtime": { "version": "0.2.12", "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-0.2.12.tgz", @@ -1881,6 +1912,30 @@ "node": ">= 10" } }, + "node_modules/@noble/ciphers": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/@noble/ciphers/-/ciphers-2.0.1.tgz", + "integrity": "sha512-xHK3XHPUW8DTAobU+G0XT+/w+JLM7/8k1UFdB5xg/zTFPnFCobhftzw8wl4Lw2aq/Rvir5pxfZV5fEazmeCJ2g==", + "license": "MIT", + "engines": { + "node": ">= 20.19.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@noble/hashes": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-2.0.1.tgz", + "integrity": "sha512-XlOlEbQcE9fmuXxrVTXCTlG2nlRXa9Rj3rr5Ue/+tX+nmkgbX720YHh0VR3hBF9xDvwnb8D2shVGOwNx+ulArw==", + "license": "MIT", + "engines": { + "node": ">= 20.19.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, "node_modules/@nodelib/fs.scandir": { "version": "2.1.5", "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", @@ -1929,6 +1984,162 @@ "node": ">=12.4.0" } }, + "node_modules/@peculiar/asn1-android": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/@peculiar/asn1-android/-/asn1-android-2.5.0.tgz", + "integrity": "sha512-t8A83hgghWQkcneRsgGs2ebAlRe54ns88p7ouv8PW2tzF1nAW4yHcL4uZKrFpIU+uszIRzTkcCuie37gpkId0A==", + "license": "MIT", + "dependencies": { + "@peculiar/asn1-schema": "^2.5.0", + "asn1js": "^3.0.6", + "tslib": "^2.8.1" + } + }, + "node_modules/@peculiar/asn1-cms": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/@peculiar/asn1-cms/-/asn1-cms-2.5.0.tgz", + "integrity": "sha512-p0SjJ3TuuleIvjPM4aYfvYw8Fk1Hn/zAVyPJZTtZ2eE9/MIer6/18ROxX6N/e6edVSfvuZBqhxAj3YgsmSjQ/A==", + "license": "MIT", + "dependencies": { + "@peculiar/asn1-schema": "^2.5.0", + "@peculiar/asn1-x509": "^2.5.0", + "@peculiar/asn1-x509-attr": "^2.5.0", + "asn1js": "^3.0.6", + "tslib": "^2.8.1" + } + }, + "node_modules/@peculiar/asn1-csr": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/@peculiar/asn1-csr/-/asn1-csr-2.5.0.tgz", + "integrity": "sha512-ioigvA6WSYN9h/YssMmmoIwgl3RvZlAYx4A/9jD2qaqXZwGcNlAxaw54eSx2QG1Yu7YyBC5Rku3nNoHrQ16YsQ==", + "license": "MIT", + "dependencies": { + "@peculiar/asn1-schema": "^2.5.0", + "@peculiar/asn1-x509": "^2.5.0", + "asn1js": "^3.0.6", + "tslib": "^2.8.1" + } + }, + "node_modules/@peculiar/asn1-ecc": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/@peculiar/asn1-ecc/-/asn1-ecc-2.5.0.tgz", + "integrity": "sha512-t4eYGNhXtLRxaP50h3sfO6aJebUCDGQACoeexcelL4roMFRRVgB20yBIu2LxsPh/tdW9I282gNgMOyg3ywg/mg==", + "license": "MIT", + "dependencies": { + "@peculiar/asn1-schema": "^2.5.0", + "@peculiar/asn1-x509": "^2.5.0", + "asn1js": "^3.0.6", + "tslib": "^2.8.1" + } + }, + "node_modules/@peculiar/asn1-pfx": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/@peculiar/asn1-pfx/-/asn1-pfx-2.5.0.tgz", + "integrity": "sha512-Vj0d0wxJZA+Ztqfb7W+/iu8Uasw6hhKtCdLKXLG/P3kEPIQpqGI4P4YXlROfl7gOCqFIbgsj1HzFIFwQ5s20ug==", + "license": "MIT", + "dependencies": { + "@peculiar/asn1-cms": "^2.5.0", + "@peculiar/asn1-pkcs8": "^2.5.0", + "@peculiar/asn1-rsa": "^2.5.0", + "@peculiar/asn1-schema": "^2.5.0", + "asn1js": "^3.0.6", + "tslib": "^2.8.1" + } + }, + "node_modules/@peculiar/asn1-pkcs8": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/@peculiar/asn1-pkcs8/-/asn1-pkcs8-2.5.0.tgz", + "integrity": "sha512-L7599HTI2SLlitlpEP8oAPaJgYssByI4eCwQq2C9eC90otFpm8MRn66PpbKviweAlhinWQ3ZjDD2KIVtx7PaVw==", + "license": "MIT", + "dependencies": { + "@peculiar/asn1-schema": "^2.5.0", + "@peculiar/asn1-x509": "^2.5.0", + "asn1js": "^3.0.6", + "tslib": "^2.8.1" + } + }, + "node_modules/@peculiar/asn1-pkcs9": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/@peculiar/asn1-pkcs9/-/asn1-pkcs9-2.5.0.tgz", + "integrity": "sha512-UgqSMBLNLR5TzEZ5ZzxR45Nk6VJrammxd60WMSkofyNzd3DQLSNycGWSK5Xg3UTYbXcDFyG8pA/7/y/ztVCa6A==", + "license": "MIT", + "dependencies": { + "@peculiar/asn1-cms": "^2.5.0", + "@peculiar/asn1-pfx": "^2.5.0", + "@peculiar/asn1-pkcs8": "^2.5.0", + "@peculiar/asn1-schema": "^2.5.0", + "@peculiar/asn1-x509": "^2.5.0", + "@peculiar/asn1-x509-attr": "^2.5.0", + "asn1js": "^3.0.6", + "tslib": "^2.8.1" + } + }, + "node_modules/@peculiar/asn1-rsa": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/@peculiar/asn1-rsa/-/asn1-rsa-2.5.0.tgz", + "integrity": "sha512-qMZ/vweiTHy9syrkkqWFvbT3eLoedvamcUdnnvwyyUNv5FgFXA3KP8td+ATibnlZ0EANW5PYRm8E6MJzEB/72Q==", + "license": "MIT", + "dependencies": { + "@peculiar/asn1-schema": "^2.5.0", + "@peculiar/asn1-x509": "^2.5.0", + "asn1js": "^3.0.6", + "tslib": "^2.8.1" + } + }, + "node_modules/@peculiar/asn1-schema": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/@peculiar/asn1-schema/-/asn1-schema-2.5.0.tgz", + "integrity": "sha512-YM/nFfskFJSlHqv59ed6dZlLZqtZQwjRVJ4bBAiWV08Oc+1rSd5lDZcBEx0lGDHfSoH3UziI2pXt2UM33KerPQ==", + "license": "MIT", + "dependencies": { + "asn1js": "^3.0.6", + "pvtsutils": "^1.3.6", + "tslib": "^2.8.1" + } + }, + "node_modules/@peculiar/asn1-x509": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/@peculiar/asn1-x509/-/asn1-x509-2.5.0.tgz", + "integrity": "sha512-CpwtMCTJvfvYTFMuiME5IH+8qmDe3yEWzKHe7OOADbGfq7ohxeLaXwQo0q4du3qs0AII3UbLCvb9NF/6q0oTKQ==", + "license": "MIT", + "dependencies": { + "@peculiar/asn1-schema": "^2.5.0", + "asn1js": "^3.0.6", + "pvtsutils": "^1.3.6", + "tslib": "^2.8.1" + } + }, + "node_modules/@peculiar/asn1-x509-attr": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/@peculiar/asn1-x509-attr/-/asn1-x509-attr-2.5.0.tgz", + "integrity": "sha512-9f0hPOxiJDoG/bfNLAFven+Bd4gwz/VzrCIIWc1025LEI4BXO0U5fOCTNDPbbp2ll+UzqKsZ3g61mpBp74gk9A==", + "license": "MIT", + "dependencies": { + "@peculiar/asn1-schema": "^2.5.0", + "@peculiar/asn1-x509": "^2.5.0", + "asn1js": "^3.0.6", + "tslib": "^2.8.1" + } + }, + "node_modules/@peculiar/x509": { + "version": "1.14.0", + "resolved": "https://registry.npmjs.org/@peculiar/x509/-/x509-1.14.0.tgz", + "integrity": "sha512-Yc4PDxN3OrxUPiXgU63c+ZRXKGE8YKF2McTciYhUHFtHVB0KMnjeFSU0qpztGhsp4P0uKix4+J2xEpIEDu8oXg==", + "license": "MIT", + "dependencies": { + "@peculiar/asn1-cms": "^2.5.0", + "@peculiar/asn1-csr": "^2.5.0", + "@peculiar/asn1-ecc": "^2.5.0", + "@peculiar/asn1-pkcs9": "^2.5.0", + "@peculiar/asn1-rsa": "^2.5.0", + "@peculiar/asn1-schema": "^2.5.0", + "@peculiar/asn1-x509": "^2.5.0", + "pvtsutils": "^1.3.6", + "reflect-metadata": "^0.2.2", + "tslib": "^2.8.1", + "tsyringe": "^4.10.0" + } + }, "node_modules/@pkgr/core": { "version": "0.2.9", "resolved": "https://registry.npmjs.org/@pkgr/core/-/core-0.2.9.tgz", @@ -2027,6 +2238,40 @@ "@prisma/debug": "6.16.2" } }, + "node_modules/@radix-ui/primitive": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/@radix-ui/primitive/-/primitive-1.1.3.tgz", + "integrity": "sha512-JTF99U/6XIjCBo0wqkU5sK10glYe27MRRsfwoiq5zzOEZLHU3A3KCMa5X/azekYRCJ0HlwI0crAXS/5dEHTzDg==", + "license": "MIT" + }, + "node_modules/@radix-ui/react-alert-dialog": { + "version": "1.1.15", + "resolved": "https://registry.npmjs.org/@radix-ui/react-alert-dialog/-/react-alert-dialog-1.1.15.tgz", + "integrity": "sha512-oTVLkEw5GpdRe29BqJ0LSDFWI3qu0vR1M0mUkOQWDIUnY/QIkLpgDMWuKxP94c2NAC2LGcgVhG1ImF3jkZ5wXw==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-dialog": "1.1.15", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-slot": "1.2.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, "node_modules/@radix-ui/react-avatar": { "version": "1.1.10", "resolved": "https://registry.npmjs.org/@radix-ui/react-avatar/-/react-avatar-1.1.10.tgz", @@ -2054,6 +2299,36 @@ } } }, + "node_modules/@radix-ui/react-checkbox": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-checkbox/-/react-checkbox-1.3.3.tgz", + "integrity": "sha512-wBbpv+NQftHDdG86Qc0pIyXk5IR3tM8Vd0nWLKDcX8nNn4nXFOFwsKuqw2okA/1D/mpaAkmuyndrPJTYDNZtFw==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-presence": "1.1.5", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-controllable-state": "1.2.2", + "@radix-ui/react-use-previous": "1.1.1", + "@radix-ui/react-use-size": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, "node_modules/@radix-ui/react-compose-refs": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/@radix-ui/react-compose-refs/-/react-compose-refs-1.1.2.tgz", @@ -2084,6 +2359,198 @@ } } }, + "node_modules/@radix-ui/react-dialog": { + "version": "1.1.15", + "resolved": "https://registry.npmjs.org/@radix-ui/react-dialog/-/react-dialog-1.1.15.tgz", + "integrity": "sha512-TCglVRtzlffRNxRMEyR36DGBLJpeusFcgMVD9PZEzAKnUs1lKCgX5u9BmC2Yg+LL9MgZDugFFs1Vl+Jp4t/PGw==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-dismissable-layer": "1.1.11", + "@radix-ui/react-focus-guards": "1.1.3", + "@radix-ui/react-focus-scope": "1.1.7", + "@radix-ui/react-id": "1.1.1", + "@radix-ui/react-portal": "1.1.9", + "@radix-ui/react-presence": "1.1.5", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-slot": "1.2.3", + "@radix-ui/react-use-controllable-state": "1.2.2", + "aria-hidden": "^1.2.4", + "react-remove-scroll": "^2.6.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-dismissable-layer": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/@radix-ui/react-dismissable-layer/-/react-dismissable-layer-1.1.11.tgz", + "integrity": "sha512-Nqcp+t5cTB8BinFkZgXiMJniQH0PsUt2k51FUhbdfeKvc4ACcG2uQniY/8+h1Yv6Kza4Q7lD7PQV0z0oicE0Mg==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-callback-ref": "1.1.1", + "@radix-ui/react-use-escape-keydown": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-focus-guards": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-focus-guards/-/react-focus-guards-1.1.3.tgz", + "integrity": "sha512-0rFg/Rj2Q62NCm62jZw0QX7a3sz6QCQU0LpZdNrJX8byRGaGVTqbrW9jAoIAHyMQqsNpeZ81YgSizOt5WXq0Pw==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-focus-scope": { + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/@radix-ui/react-focus-scope/-/react-focus-scope-1.1.7.tgz", + "integrity": "sha512-t2ODlkXBQyn7jkl6TNaw/MtVEVvIGelJDCG41Okq/KwUsJBwQ4XVZsHAVUkK4mBv3ewiAS3PGuUWuY2BoK4ZUw==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-callback-ref": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-id": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-id/-/react-id-1.1.1.tgz", + "integrity": "sha512-kGkGegYIdQsOb4XjsfM97rXsiHaBwco+hFI66oO4s9LU+PLAC5oJ7khdOVFxkhsmlbpUqDAvXw11CluXP+jkHg==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-use-layout-effect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-label": { + "version": "2.1.7", + "resolved": "https://registry.npmjs.org/@radix-ui/react-label/-/react-label-2.1.7.tgz", + "integrity": "sha512-YT1GqPSL8kJn20djelMX7/cTRp/Y9w5IZHvfxQTVHrOqa2yMl7i/UfMqKRU5V7mEyKTrUVgJXhNQPVCG8PBLoQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-primitive": "2.1.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-portal": { + "version": "1.1.9", + "resolved": "https://registry.npmjs.org/@radix-ui/react-portal/-/react-portal-1.1.9.tgz", + "integrity": "sha512-bpIxvq03if6UNwXZ+HTK71JLh4APvnXntDc6XOX8UVq4XQOVl7lwok0AvIl+b8zgCw3fSaVTZMpAPPagXbKmHQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-layout-effect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-presence": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@radix-ui/react-presence/-/react-presence-1.1.5.tgz", + "integrity": "sha512-/jfEwNDdQVBCNvjkGit4h6pMOzq8bHkopq458dPt2lMjx+eBQUohZNG9A7DtO/O5ukSbxuaNGXMjHicgwy6rQQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-use-layout-effect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, "node_modules/@radix-ui/react-primitive": { "version": "2.1.3", "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.1.3.tgz", @@ -2140,6 +2607,61 @@ } } }, + "node_modules/@radix-ui/react-use-controllable-state": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-controllable-state/-/react-use-controllable-state-1.2.2.tgz", + "integrity": "sha512-BjasUjixPFdS+NKkypcyyN5Pmg83Olst0+c6vGov0diwTEo6mgdqVR6hxcEgFuh4QrAs7Rc+9KuGJ9TVCj0Zzg==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-use-effect-event": "0.0.2", + "@radix-ui/react-use-layout-effect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-effect-event": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-effect-event/-/react-use-effect-event-0.0.2.tgz", + "integrity": "sha512-Qp8WbZOBe+blgpuUT+lw2xheLP8q0oatc9UpmiemEICxGvFLYmHm9QowVZGHtJlGbS6A6yJ3iViad/2cVjnOiA==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-use-layout-effect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-escape-keydown": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-escape-keydown/-/react-use-escape-keydown-1.1.1.tgz", + "integrity": "sha512-Il0+boE7w/XebUHyBjroE+DbByORGR9KKmITzbR7MyQ4akpORYP/ZmbhAr0DG7RmmBqoOnZdy2QlvajJ2QA59g==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-use-callback-ref": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, "node_modules/@radix-ui/react-use-is-hydrated": { "version": "0.1.0", "resolved": "https://registry.npmjs.org/@radix-ui/react-use-is-hydrated/-/react-use-is-hydrated-0.1.0.tgz", @@ -2173,6 +2695,39 @@ } } }, + "node_modules/@radix-ui/react-use-previous": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-previous/-/react-use-previous-1.1.1.tgz", + "integrity": "sha512-2dHfToCj/pzca2Ck724OZ5L0EVrr3eHRNsG/b3xQJLA2hZpVCS99bLAX+hm1IHXDEnzU6by5z/5MIY794/a8NQ==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-size": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-size/-/react-use-size-1.1.1.tgz", + "integrity": "sha512-ewrXRDTAqAXlkl6t/fkXWNAhFX9I+CkKlw6zjEwk86RSPKwZr3xpBRso655aqYafwtnbpHLj6toFzmd6xdVptQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-use-layout-effect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, "node_modules/@rolldown/pluginutils": { "version": "1.0.0-beta.35", "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.35.tgz", @@ -2502,6 +3057,31 @@ "dev": true, "license": "MIT" }, + "node_modules/@simplewebauthn/browser": { + "version": "13.2.0", + "resolved": "https://registry.npmjs.org/@simplewebauthn/browser/-/browser-13.2.0.tgz", + "integrity": "sha512-N3fuA1AAnTo5gCStYoIoiasPccC+xPLx2YU88Dv0GeAmPQTWHETlZQq5xZ0DgUq1H9loXMWQH5qqUjcI7BHJ1A==", + "license": "MIT" + }, + "node_modules/@simplewebauthn/server": { + "version": "13.2.1", + "resolved": "https://registry.npmjs.org/@simplewebauthn/server/-/server-13.2.1.tgz", + "integrity": "sha512-Inmfye5opZXe3HI0GaksqBnQiM7glcNySoG6DH1GgkO1Lh9dvuV4XSV9DK02DReUVX39HpcDob9nxHELjECoQw==", + "license": "MIT", + "dependencies": { + "@hexagon/base64": "^1.1.27", + "@levischuck/tiny-cbor": "^0.2.2", + "@peculiar/asn1-android": "^2.3.10", + "@peculiar/asn1-ecc": "^2.3.8", + "@peculiar/asn1-rsa": "^2.3.8", + "@peculiar/asn1-schema": "^2.3.8", + "@peculiar/asn1-x509": "^2.3.8", + "@peculiar/x509": "^1.13.0" + }, + "engines": { + "node": ">=20.0.0" + } + }, "node_modules/@standard-schema/spec": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.0.0.tgz", @@ -3743,6 +4323,18 @@ "dev": true, "license": "Python-2.0" }, + "node_modules/aria-hidden": { + "version": "1.2.6", + "resolved": "https://registry.npmjs.org/aria-hidden/-/aria-hidden-1.2.6.tgz", + "integrity": "sha512-ik3ZgC9dY/lYVVM++OISsaYDeg1tb0VtP5uL3ouh1koGOaUMDPpbFIei4JkFimWUFPn90sbMNMXQAIVOlnYKJA==", + "license": "MIT", + "dependencies": { + "tslib": "^2.0.0" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/aria-query": { "version": "5.3.2", "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.3.2.tgz", @@ -3913,6 +4505,20 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/asn1js": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/asn1js/-/asn1js-3.0.6.tgz", + "integrity": "sha512-UOCGPYbl0tv8+006qks/dTgV9ajs97X2p0FAbyS2iyCRrmLSRolDaHdp+v/CLgnzHc3fVB+CwYiUmei7ndFcgA==", + "license": "BSD-3-Clause", + "dependencies": { + "pvtsutils": "^1.3.6", + "pvutils": "^1.1.3", + "tslib": "^2.8.1" + }, + "engines": { + "node": ">=12.0.0" + } + }, "node_modules/assertion-error": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz", @@ -3993,6 +4599,83 @@ "baseline-browser-mapping": "dist/cli.js" } }, + "node_modules/bcryptjs": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/bcryptjs/-/bcryptjs-3.0.2.tgz", + "integrity": "sha512-k38b3XOZKv60C4E2hVsXTolJWfkGRMbILBIe2IBITXciy5bOsTKot5kDrf3ZfufQtQOUN5mXceUEpU1rTl9Uog==", + "license": "BSD-3-Clause", + "bin": { + "bcrypt": "bin/bcrypt" + } + }, + "node_modules/better-auth": { + "version": "1.3.14", + "resolved": "https://registry.npmjs.org/better-auth/-/better-auth-1.3.14.tgz", + "integrity": "sha512-2U2TKO4CmFFNxBagrRsgptdhZO9wpjhrSkEwsiB/DUnuFRxueY1pcBJA8xCrq1WnbqL/ktHh7UAEKb20Gb0FhA==", + "license": "MIT", + "dependencies": { + "@better-auth/utils": "0.3.0", + "@better-fetch/fetch": "^1.1.18", + "@noble/ciphers": "^2.0.0", + "@noble/hashes": "^2.0.0", + "@simplewebauthn/browser": "^13.1.2", + "@simplewebauthn/server": "^13.1.2", + "better-call": "1.0.19", + "defu": "^6.1.4", + "jose": "^6.1.0", + "kysely": "^0.28.5", + "nanostores": "^1.0.1", + "zod": "^4.1.5" + }, + "peerDependencies": { + "@lynx-js/react": "*", + "@sveltejs/kit": "^2.0.0", + "next": "^14.0.0 || ^15.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" + }, + "peerDependenciesMeta": { + "@lynx-js/react": { + "optional": true + }, + "@sveltejs/kit": { + "optional": true + }, + "next": { + "optional": true + }, + "react": { + "optional": true + }, + "react-dom": { + "optional": true + }, + "solid-js": { + "optional": true + }, + "svelte": { + "optional": true + }, + "vue": { + "optional": true + } + } + }, + "node_modules/better-call": { + "version": "1.0.19", + "resolved": "https://registry.npmjs.org/better-call/-/better-call-1.0.19.tgz", + "integrity": "sha512-sI3GcA1SCVa3H+CDHl8W8qzhlrckwXOTKhqq3OOPXjgn5aTOMIqGY34zLY/pHA6tRRMjTUC3lz5Mi7EbDA24Kw==", + "dependencies": { + "@better-auth/utils": "^0.3.0", + "@better-fetch/fetch": "^1.1.4", + "rou3": "^0.5.1", + "set-cookie-parser": "^2.7.1", + "uncrypto": "^0.1.3" + } + }, "node_modules/bidi-js": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/bidi-js/-/bidi-js-1.0.3.tgz", @@ -4061,6 +4744,19 @@ "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" } }, + "node_modules/builtin-modules": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/builtin-modules/-/builtin-modules-5.0.0.tgz", + "integrity": "sha512-bkXY9WsVpY7CvMhKSR6pZilZu9Ln5WDrKVBUXf2S443etkmEO4V58heTecXcUIsNsi4Rx8JUO4NfX1IcQl4deg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18.20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/c12": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/c12/-/c12-3.1.0.tgz", @@ -4214,6 +4910,13 @@ "url": "https://github.com/chalk/chalk?sponsor=1" } }, + "node_modules/change-case": { + "version": "5.4.4", + "resolved": "https://registry.npmjs.org/change-case/-/change-case-5.4.4.tgz", + "integrity": "sha512-HRQyTk2/YPEkt9TnUPbOpr64Uw3KOicFWPVBb+xiHvd6eBx/qPr9xqfBFDT8P2vWsvvz4jbEkfDe71W3VyNu2w==", + "dev": true, + "license": "MIT" + }, "node_modules/check-error": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/check-error/-/check-error-2.1.1.tgz", @@ -4249,6 +4952,22 @@ "node": ">=18" } }, + "node_modules/ci-info": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-4.3.0.tgz", + "integrity": "sha512-l+2bNRMiQgcfILUi33labAZYIWlH1kWDp+ecNo5iisRKrbm0xcRyCww71/YU0Fkw0mAFpz9bJayXPjey6vkmaQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/sibiraj-s" + } + ], + "license": "MIT", + "engines": { + "node": ">=8" + } + }, "node_modules/citty": { "version": "0.1.6", "resolved": "https://registry.npmjs.org/citty/-/citty-0.1.6.tgz", @@ -4271,6 +4990,29 @@ "url": "https://polar.sh/cva" } }, + "node_modules/clean-regexp": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/clean-regexp/-/clean-regexp-1.0.0.tgz", + "integrity": "sha512-GfisEZEJvzKrmGWkvfhgzcz/BllN1USeqD2V6tg14OAOgaCD2Z/PUEuxnAZ/nPvmaHRG7a8y77p1T/IRQ4D1Hw==", + "dev": true, + "license": "MIT", + "dependencies": { + "escape-string-regexp": "^1.0.5" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/clean-regexp/node_modules/escape-string-regexp": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", + "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.8.0" + } + }, "node_modules/client-only": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/client-only/-/client-only-0.0.1.tgz", @@ -4337,6 +5079,20 @@ "dev": true, "license": "MIT" }, + "node_modules/core-js-compat": { + "version": "3.45.1", + "resolved": "https://registry.npmjs.org/core-js-compat/-/core-js-compat-3.45.1.tgz", + "integrity": "sha512-tqTt5T4PzsMIZ430XGviK4vzYSoeNJ6CXODi6c/voxOT6IZqBht5/EKaSNnYiEjjRYxjVz7DQIsOsY0XNi8PIA==", + "dev": true, + "license": "MIT", + "dependencies": { + "browserslist": "^4.25.3" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/core-js" + } + }, "node_modules/cross-spawn": { "version": "7.0.6", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", @@ -4555,7 +5311,6 @@ "version": "6.1.4", "resolved": "https://registry.npmjs.org/defu/-/defu-6.1.4.tgz", "integrity": "sha512-mEQCMmwJu317oSz8CwdIOdwf3xMif1ttiM8LTufzc3g6kR+9Pe236twL8j3IYT1F7GfRgGcW6MWxzZjLIkuHIg==", - "devOptional": true, "license": "MIT" }, "node_modules/dequal": { @@ -4584,6 +5339,12 @@ "node": ">=8" } }, + "node_modules/detect-node-es": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/detect-node-es/-/detect-node-es-1.1.0.tgz", + "integrity": "sha512-ypdmJU/TbBby2Dxibuv7ZLW3Bs1QEmM7nHjEANfohJLvE0XVujisn1qPJcZxg+qDucsr+bP6fLD1rPS3AhJ7EQ==", + "license": "MIT" + }, "node_modules/doctrine": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-2.1.0.tgz", @@ -4597,19 +5358,64 @@ "node": ">=0.10.0" } }, - "node_modules/dom-accessibility-api": { - "version": "0.5.16", - "resolved": "https://registry.npmjs.org/dom-accessibility-api/-/dom-accessibility-api-0.5.16.tgz", - "integrity": "sha512-X7BJ2yElsnOJ30pZF4uIIDfBEVgF4XEBxL9Bxhy6dnrm5hkzqmsWHGTiHqRiITNhMyFLyAiWndIJP7Z1NTteDg==", + "node_modules/dom-accessibility-api": { + "version": "0.5.16", + "resolved": "https://registry.npmjs.org/dom-accessibility-api/-/dom-accessibility-api-0.5.16.tgz", + "integrity": "sha512-X7BJ2yElsnOJ30pZF4uIIDfBEVgF4XEBxL9Bxhy6dnrm5hkzqmsWHGTiHqRiITNhMyFLyAiWndIJP7Z1NTteDg==", + "dev": true, + "license": "MIT" + }, + "node_modules/dotenv": { + "version": "16.6.1", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.6.1.tgz", + "integrity": "sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow==", + "devOptional": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://dotenvx.com" + } + }, + "node_modules/dotenv-cli": { + "version": "10.0.0", + "resolved": "https://registry.npmjs.org/dotenv-cli/-/dotenv-cli-10.0.0.tgz", + "integrity": "sha512-lnOnttzfrzkRx2echxJHQRB6vOAMSCzzZg79IxpC00tU42wZPuZkQxNNrrwVAxaQZIIh001l4PxVlCrBxngBzA==", + "dev": true, + "license": "MIT", + "dependencies": { + "cross-spawn": "^7.0.6", + "dotenv": "^17.1.0", + "dotenv-expand": "^11.0.0", + "minimist": "^1.2.6" + }, + "bin": { + "dotenv": "cli.js" + } + }, + "node_modules/dotenv-cli/node_modules/dotenv": { + "version": "17.2.2", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-17.2.2.tgz", + "integrity": "sha512-Sf2LSQP+bOlhKWWyhFsn0UsfdK/kCWRv1iuA2gXAwt3dyNabr6QSj00I2V10pidqz69soatm9ZwZvpQMTIOd5Q==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://dotenvx.com" + } + }, + "node_modules/dotenv-expand": { + "version": "11.0.7", + "resolved": "https://registry.npmjs.org/dotenv-expand/-/dotenv-expand-11.0.7.tgz", + "integrity": "sha512-zIHwmZPRshsCdpMDyVsqGmgyP0yT8GAgXUnkdAoJisxvf33k7yO6OuoKmcTGuXPWSsm8Oh88nZicRLA9Y0rUeA==", "dev": true, - "license": "MIT" - }, - "node_modules/dotenv": { - "version": "16.6.1", - "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.6.1.tgz", - "integrity": "sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow==", - "devOptional": true, "license": "BSD-2-Clause", + "dependencies": { + "dotenv": "^16.4.5" + }, "engines": { "node": ">=12" }, @@ -5334,6 +6140,55 @@ "semver": "bin/semver.js" } }, + "node_modules/eslint-plugin-unicorn": { + "version": "61.0.2", + "resolved": "https://registry.npmjs.org/eslint-plugin-unicorn/-/eslint-plugin-unicorn-61.0.2.tgz", + "integrity": "sha512-zLihukvneYT7f74GNbVJXfWIiNQmkc/a9vYBTE4qPkQZswolWNdu+Wsp9sIXno1JOzdn6OUwLPd19ekXVkahRA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-validator-identifier": "^7.27.1", + "@eslint-community/eslint-utils": "^4.7.0", + "@eslint/plugin-kit": "^0.3.3", + "change-case": "^5.4.4", + "ci-info": "^4.3.0", + "clean-regexp": "^1.0.0", + "core-js-compat": "^3.44.0", + "esquery": "^1.6.0", + "find-up-simple": "^1.0.1", + "globals": "^16.3.0", + "indent-string": "^5.0.0", + "is-builtin-module": "^5.0.0", + "jsesc": "^3.1.0", + "pluralize": "^8.0.0", + "regexp-tree": "^0.1.27", + "regjsparser": "^0.12.0", + "semver": "^7.7.2", + "strip-indent": "^4.0.0" + }, + "engines": { + "node": "^20.10.0 || >=21.0.0" + }, + "funding": { + "url": "https://github.com/sindresorhus/eslint-plugin-unicorn?sponsor=1" + }, + "peerDependencies": { + "eslint": ">=9.29.0" + } + }, + "node_modules/eslint-plugin-unicorn/node_modules/globals": { + "version": "16.4.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-16.4.0.tgz", + "integrity": "sha512-ob/2LcVVaVGCYN+r14cnwnoDPUufjiYgSqRhiFD0Q1iI4Odora5RE8Iv1D24hAz5oMophRGkGz+yuvQmmUMnMw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/eslint-scope": { "version": "8.4.0", "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-8.4.0.tgz", @@ -5589,6 +6444,19 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/find-up-simple": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/find-up-simple/-/find-up-simple-1.0.1.tgz", + "integrity": "sha512-afd4O7zpqHeRyg4PfDQsXmlDe2PfdHtJt6Akt8jOWaApLOZk5JXs6VMR29lz03pRe9mpykrRCYIYxaJYcfpncQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/flat-cache": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-4.0.1.tgz", @@ -5717,6 +6585,15 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/get-nonce": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-nonce/-/get-nonce-1.0.1.tgz", + "integrity": "sha512-FJhYRoDaiatfEkUK8HKlicmu/3SGFD51q3itKDGoSTysQJBnfOcxU5GxnhE1E6soB76MbT0MBtnKJuXyAx+96Q==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/get-proto": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", @@ -6041,6 +6918,19 @@ "node": ">=0.8.19" } }, + "node_modules/indent-string": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-5.0.0.tgz", + "integrity": "sha512-m6FAo/spmsW2Ab2fU35JTYwtOKa2yAwXSwgjSv1TJzh4Mh7mC3lzAOVLBprb72XsTrgkEIsl7YrFNAiDiRhIGg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/internal-slot": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/internal-slot/-/internal-slot-1.1.0.tgz", @@ -6127,6 +7017,22 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/is-builtin-module": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/is-builtin-module/-/is-builtin-module-5.0.0.tgz", + "integrity": "sha512-f4RqJKBUe5rQkJ2eJEJBXSticB3hGbN9j0yxxMQFqIW89Jp9WYFtzfTcRlstDKVUTRzSOTLKRfO9vIztenwtxA==", + "dev": true, + "license": "MIT", + "dependencies": { + "builtin-modules": "^5.0.0" + }, + "engines": { + "node": ">=18.20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/is-bun-module": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/is-bun-module/-/is-bun-module-2.0.0.tgz", @@ -6505,6 +7411,15 @@ "jiti": "lib/jiti-cli.mjs" } }, + "node_modules/jose": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/jose/-/jose-6.1.0.tgz", + "integrity": "sha512-TTQJyoEoKcC1lscpVDCSsVgYzUDg/0Bt3WE//WiTPK6uOCQC2KZS4MpugbMWt/zyjkopgZoXhZuCi00gLudfUA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/panva" + } + }, "node_modules/js-tokens": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", @@ -6638,6 +7553,15 @@ "json-buffer": "3.0.1" } }, + "node_modules/kysely": { + "version": "0.28.7", + "resolved": "https://registry.npmjs.org/kysely/-/kysely-0.28.7.tgz", + "integrity": "sha512-u/cAuTL4DRIiO2/g4vNGRgklEKNIj5Q3CG7RoUB5DV5SfEC2hMvPxKi0GWPmnzwL2ryIeud2VTcEEmqzTzEPNw==", + "license": "MIT", + "engines": { + "node": ">=20.0.0" + } + }, "node_modules/language-subtag-registry": { "version": "0.3.23", "resolved": "https://registry.npmjs.org/language-subtag-registry/-/language-subtag-registry-0.3.23.tgz", @@ -7098,6 +8022,21 @@ "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" } }, + "node_modules/nanostores": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/nanostores/-/nanostores-1.0.1.tgz", + "integrity": "sha512-kNZ9xnoJYKg/AfxjrVL4SS0fKX++4awQReGqWnwTRHxeHGZ1FJFVgTqr/eMrNQdp0Tz7M7tG/TDaX8QfHDwVCw==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "engines": { + "node": "^20.0.0 || >=22.0.0" + } + }, "node_modules/napi-postinstall": { "version": "0.3.3", "resolved": "https://registry.npmjs.org/napi-postinstall/-/napi-postinstall-0.3.3.tgz", @@ -7541,6 +8480,16 @@ "pathe": "^2.0.3" } }, + "node_modules/pluralize": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/pluralize/-/pluralize-8.0.0.tgz", + "integrity": "sha512-Nc3IT5yHzflTfbjgqWcCPpo7DaKy4FnpB0l/zCAW0Tc7jxAiuqSxHasntB3D7887LSrA93kDJ9IXovxJYxyLCA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, "node_modules/possible-typed-array-names": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/possible-typed-array-names/-/possible-typed-array-names-1.1.0.tgz", @@ -7718,6 +8667,24 @@ ], "license": "MIT" }, + "node_modules/pvtsutils": { + "version": "1.3.6", + "resolved": "https://registry.npmjs.org/pvtsutils/-/pvtsutils-1.3.6.tgz", + "integrity": "sha512-PLgQXQ6H2FWCaeRak8vvk1GW462lMxB5s3Jm673N82zI4vqtVUPuZdffdZbPDFRoU8kAhItWFtPCWiPpp4/EDg==", + "license": "MIT", + "dependencies": { + "tslib": "^2.8.1" + } + }, + "node_modules/pvutils": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/pvutils/-/pvutils-1.1.3.tgz", + "integrity": "sha512-pMpnA0qRdFp32b1sJl1wOJNxZLQ2cbQx+k6tjNtZ8CpvVhNqEPRgivZ2WOUev2YMajecdH7ctUPDvEe87nariQ==", + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, "node_modules/queue-microtask": { "version": "1.2.3", "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", @@ -7788,6 +8755,75 @@ "node": ">=0.10.0" } }, + "node_modules/react-remove-scroll": { + "version": "2.7.1", + "resolved": "https://registry.npmjs.org/react-remove-scroll/-/react-remove-scroll-2.7.1.tgz", + "integrity": "sha512-HpMh8+oahmIdOuS5aFKKY6Pyog+FNaZV/XyJOq7b4YFwsFHe5yYfdbIalI4k3vU2nSDql7YskmUseHsRrJqIPA==", + "license": "MIT", + "dependencies": { + "react-remove-scroll-bar": "^2.3.7", + "react-style-singleton": "^2.2.3", + "tslib": "^2.1.0", + "use-callback-ref": "^1.3.3", + "use-sidecar": "^1.1.3" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/react-remove-scroll-bar": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/react-remove-scroll-bar/-/react-remove-scroll-bar-2.3.8.tgz", + "integrity": "sha512-9r+yi9+mgU33AKcj6IbT9oRCO78WriSj6t/cF8DWBZJ9aOGPOTEDvdUDz1FwKim7QXWwmHqtdHnRJfhAxEG46Q==", + "license": "MIT", + "dependencies": { + "react-style-singleton": "^2.2.2", + "tslib": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/react-style-singleton": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/react-style-singleton/-/react-style-singleton-2.2.3.tgz", + "integrity": "sha512-b6jSvxvVnyptAiLjbkWLE/lOnR4lfTtDAl+eUC7RZy+QQWc6wRzIV2CE6xBuMmDxc2qIihtDCZD5NPOFl7fRBQ==", + "license": "MIT", + "dependencies": { + "get-nonce": "^1.0.0", + "tslib": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, "node_modules/readdirp": { "version": "4.1.2", "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-4.1.2.tgz", @@ -7802,6 +8838,12 @@ "url": "https://paulmillr.com/funding/" } }, + "node_modules/reflect-metadata": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/reflect-metadata/-/reflect-metadata-0.2.2.tgz", + "integrity": "sha512-urBwgfrvVP/eAyXx4hluJivBKzuEbSQs9rKWCrCkbSxNv8mxPcUZKeuoF3Uy4mJl3Lwprp6yy5/39VWigZ4K6Q==", + "license": "Apache-2.0" + }, "node_modules/reflect.getprototypeof": { "version": "1.0.10", "resolved": "https://registry.npmjs.org/reflect.getprototypeof/-/reflect.getprototypeof-1.0.10.tgz", @@ -7825,6 +8867,16 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/regexp-tree": { + "version": "0.1.27", + "resolved": "https://registry.npmjs.org/regexp-tree/-/regexp-tree-0.1.27.tgz", + "integrity": "sha512-iETxpjK6YoRWJG5o6hXLwvjYAoW+FEZn9os0PD/b6AP6xQwsa/Y7lCVgIixBbUPMfhu+i2LtdeAqVTgGlQarfA==", + "dev": true, + "license": "MIT", + "bin": { + "regexp-tree": "bin/regexp-tree" + } + }, "node_modules/regexp.prototype.flags": { "version": "1.5.4", "resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.5.4.tgz", @@ -7846,6 +8898,32 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/regjsparser": { + "version": "0.12.0", + "resolved": "https://registry.npmjs.org/regjsparser/-/regjsparser-0.12.0.tgz", + "integrity": "sha512-cnE+y8bz4NhMjISKbgeVJtqNbtf5QpjZP+Bslo+UqkIt9QPnX9q095eiRRASJG1/tz6dlNr6Z5NsBiWYokp6EQ==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "jsesc": "~3.0.2" + }, + "bin": { + "regjsparser": "bin/parser" + } + }, + "node_modules/regjsparser/node_modules/jsesc": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.0.2.tgz", + "integrity": "sha512-xKqzzWXDttJuOcawBt4KnKHHIf5oQ/Cxax+0PWFG+DFDgHNAdi+TXECADI+RYiFUMmx8792xsMbbgXj4CwnP4g==", + "dev": true, + "license": "MIT", + "bin": { + "jsesc": "bin/jsesc" + }, + "engines": { + "node": ">=6" + } + }, "node_modules/require-from-string": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", @@ -7950,6 +9028,12 @@ "fsevents": "~2.3.2" } }, + "node_modules/rou3": { + "version": "0.5.1", + "resolved": "https://registry.npmjs.org/rou3/-/rou3-0.5.1.tgz", + "integrity": "sha512-OXMmJ3zRk2xeXFGfA3K+EOPHC5u7RDFG7lIOx0X1pdnhUkI8MdVrbV+sNsD80ElpUZ+MRHdyxPnFthq9VHs8uQ==", + "license": "MIT" + }, "node_modules/rrweb-cssom": { "version": "0.8.0", "resolved": "https://registry.npmjs.org/rrweb-cssom/-/rrweb-cssom-0.8.0.tgz", @@ -8075,6 +9159,12 @@ "node": ">=10" } }, + "node_modules/set-cookie-parser": { + "version": "2.7.1", + "resolved": "https://registry.npmjs.org/set-cookie-parser/-/set-cookie-parser-2.7.1.tgz", + "integrity": "sha512-IOc8uWeOZgnb3ptbCURJWNjWUPcO3ZnTTdzsurqERrP6nPyv+paC55vJM0LpOlT2ne+Ix+9+CRG1MNLlyZ4GjQ==", + "license": "MIT" + }, "node_modules/set-function-length": { "version": "1.2.2", "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz", @@ -8440,6 +9530,19 @@ "node": ">=4" } }, + "node_modules/strip-indent": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/strip-indent/-/strip-indent-4.1.0.tgz", + "integrity": "sha512-OA95x+JPmL7kc7zCu+e+TeYxEiaIyndRx0OrBcK2QPPH09oAndr2ALvymxWA+Lx1PYYvFUm4O63pRkdJAaW96w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/strip-json-comments": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", @@ -8794,6 +9897,44 @@ "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", "license": "0BSD" }, + "node_modules/tsx": { + "version": "4.20.5", + "resolved": "https://registry.npmjs.org/tsx/-/tsx-4.20.5.tgz", + "integrity": "sha512-+wKjMNU9w/EaQayHXb7WA7ZaHY6hN8WgfvHNQ3t1PnU91/7O8TcTnIhCDYTZwnt8JsO9IBqZ30Ln1r7pPF52Aw==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "~0.25.0", + "get-tsconfig": "^4.7.5" + }, + "bin": { + "tsx": "dist/cli.mjs" + }, + "engines": { + "node": ">=18.0.0" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + } + }, + "node_modules/tsyringe": { + "version": "4.10.0", + "resolved": "https://registry.npmjs.org/tsyringe/-/tsyringe-4.10.0.tgz", + "integrity": "sha512-axr3IdNuVIxnaK5XGEUFTu3YmAQ6lllgrvqfEoR16g/HGnYY/6We4oWENtAnzK6/LpJ2ur9PAb80RBt7/U4ugw==", + "license": "MIT", + "dependencies": { + "tslib": "^1.9.3" + }, + "engines": { + "node": ">= 6.0.0" + } + }, + "node_modules/tsyringe/node_modules/tslib": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", + "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==", + "license": "0BSD" + }, "node_modules/tw-animate-css": { "version": "1.3.8", "resolved": "https://registry.npmjs.org/tw-animate-css/-/tw-animate-css-1.3.8.tgz", @@ -8928,6 +10069,12 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/uncrypto": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/uncrypto/-/uncrypto-0.1.3.tgz", + "integrity": "sha512-Ql87qFHB3s/De2ClA9e0gsnS6zXG27SkTiSJwjCc9MebbfapQfuPzumMIUMi38ezPZVNFcHI9sUIepeQfw8J8Q==", + "license": "MIT" + }, "node_modules/undici-types": { "version": "6.21.0", "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", @@ -9011,6 +10158,49 @@ "punycode": "^2.1.0" } }, + "node_modules/use-callback-ref": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/use-callback-ref/-/use-callback-ref-1.3.3.tgz", + "integrity": "sha512-jQL3lRnocaFtu3V00JToYz/4QkNWswxijDaCVNZRiRTO3HQDLsdu1ZtmIUvV4yPp+rvWm5j0y0TG/S61cuijTg==", + "license": "MIT", + "dependencies": { + "tslib": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/use-sidecar": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/use-sidecar/-/use-sidecar-1.1.3.tgz", + "integrity": "sha512-Fedw0aZvkhynoPYlA5WXrMCAMm+nSWdZt6lzJQ7Ok8S6Q+VsHmHpRWndVRJ8Be0ZbkfPc5LRYH+5XrzXcEeLRQ==", + "license": "MIT", + "dependencies": { + "detect-node-es": "^1.1.0", + "tslib": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, "node_modules/use-sync-external-store": { "version": "1.5.0", "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.5.0.tgz", @@ -9514,6 +10704,15 @@ "funding": { "url": "https://github.com/sponsors/sindresorhus" } + }, + "node_modules/zod": { + "version": "4.1.11", + "resolved": "https://registry.npmjs.org/zod/-/zod-4.1.11.tgz", + "integrity": "sha512-WPsqwxITS2tzx1bzhIKsEs19ABD5vmCVa4xBo2tq/SrV4RNZtfws1EnCWQXM6yh8bD08a1idvkB5MZSBiZsjwg==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } } } } diff --git a/package.json b/package.json index 0dd24ec..98a9078 100644 --- a/package.json +++ b/package.json @@ -3,18 +3,31 @@ "version": "0.1.0", "private": true, "scripts": { - "dev": "next dev --turbopack", + "dev": "next dev", "build": "next build --turbopack", "start": "next start", "test": "vitest", "lint": "eslint", - "lint:fix": "eslint --fix" + "lint:fix": "eslint --fix", + "db:generate:dev": "dotenv -e .env.development -- prisma generate --schema src/prisma/schema.prisma", + "db:generate:prod": "dotenv -e .env.production -- prisma generate --schema src/prisma/schema.prisma", + "db:migrate:dev": "dotenv -e .env.development -- prisma migrate dev --schema src/prisma/schema.prisma", + "db:migrate:prod": "dotenv -e .env.production -- prisma migrate deploy --schema src/prisma/schema.prisma", + "db:push:dev": "dotenv -e .env.development -- prisma db push --schema src/prisma/schema.prisma", + "db:reset:dev": "dotenv -e .env.development -- prisma migrate reset --skip-seed --schema src/prisma/schema.prisma", + "db:seed:dev": "dotenv -e .env.development -- prisma db seed --schema src/prisma/schema.prisma", + "db:seed:prod": "dotenv -e .env.production -- prisma db seed --schema src/prisma/schema.prisma" }, "dependencies": { "@prisma/client": "^6.16.2", + "@radix-ui/react-alert-dialog": "^1.1.15", "@radix-ui/react-avatar": "^1.1.10", + "@radix-ui/react-checkbox": "^1.3.3", + "@radix-ui/react-label": "^2.1.7", "@radix-ui/react-slot": "^1.2.3", "@tailwindcss/postcss": "^4.1.13", + "bcryptjs": "^3.0.2", + "better-auth": "^1.3.14", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", "lucide-react": "^0.544.0", @@ -33,17 +46,20 @@ "@types/react": "^19", "@types/react-dom": "^19", "@vitejs/plugin-react": "^5.0.3", + "dotenv-cli": "^10.0.0", "eslint": "^9", "eslint-config-next": "15.5.3", "eslint-config-prettier": "^10.1.8", "eslint-plugin-naming": "^0.1.10", "eslint-plugin-prettier": "^5.5.4", + "eslint-plugin-unicorn": "^61.0.2", "jsdom": "^27.0.0", "prettier": "3.6.2", "prisma": "^6.16.2", + "tsx": "^4.20.5", "tw-animate-css": "^1.3.8", "typescript": "^5", "vite-tsconfig-paths": "^5.1.4", "vitest": "^3.2.4" } -} \ No newline at end of file +} diff --git a/prisma.config.ts b/prisma.config.ts new file mode 100644 index 0000000..608f244 --- /dev/null +++ b/prisma.config.ts @@ -0,0 +1,7 @@ +import { defineConfig } from 'prisma/config' + +export default defineConfig({ + migrations: { + seed: 'tsx prisma/seeds/index.ts', + }, +}) diff --git a/prisma/migrations/20250924144302_base_migration/migration.sql b/prisma/migrations/20250924144302_base_migration/migration.sql new file mode 100644 index 0000000..7859aee --- /dev/null +++ b/prisma/migrations/20250924144302_base_migration/migration.sql @@ -0,0 +1,203 @@ +-- CreateEnum +CREATE TYPE "public"."Role" AS ENUM ('USER', 'MODERATOR'); + +-- CreateTable +CREATE TABLE "public"."user" ( + "id" TEXT NOT NULL, + "name" TEXT NOT NULL, + "email" TEXT NOT NULL, + "avatarUrl" TEXT, + "role" "public"."Role" NOT NULL DEFAULT 'USER', + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" TIMESTAMP(3) NOT NULL, + "emailVerified" BOOLEAN NOT NULL DEFAULT false, + "image" TEXT, + "position" TEXT, + + CONSTRAINT "user_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "public"."account" ( + "id" TEXT NOT NULL, + "userId" TEXT NOT NULL, + "provider" TEXT NOT NULL, + "providerAccountId" TEXT NOT NULL, + "accessToken" TEXT, + "refreshToken" TEXT, + "expiresAt" INTEGER, + "accountId" TEXT NOT NULL, + "providerId" TEXT NOT NULL, + "idToken" TEXT, + "accessTokenExpiresAt" TIMESTAMP(3), + "refreshTokenExpiresAt" TIMESTAMP(3), + "scope" TEXT, + "password" TEXT, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" TIMESTAMP(3) NOT NULL, + + CONSTRAINT "account_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "public"."Post" ( + "id" TEXT NOT NULL, + "title" TEXT NOT NULL, + "description" TEXT NOT NULL, + "previewUrl" TEXT, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" TIMESTAMP(3) NOT NULL, + + CONSTRAINT "Post_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "public"."PostAuthor" ( + "id" TEXT NOT NULL, + "userId" TEXT NOT NULL, + "postId" TEXT NOT NULL, + + CONSTRAINT "PostAuthor_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "public"."Like" ( + "id" TEXT NOT NULL, + "userId" TEXT NOT NULL, + "postId" TEXT NOT NULL, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + + CONSTRAINT "Like_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "public"."Comment" ( + "id" TEXT NOT NULL, + "userId" TEXT NOT NULL, + "postId" TEXT NOT NULL, + "content" TEXT NOT NULL, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" TIMESTAMP(3) NOT NULL, + + CONSTRAINT "Comment_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "public"."Report" ( + "id" TEXT NOT NULL, + "userId" TEXT NOT NULL, + "postId" TEXT NOT NULL, + "reason" TEXT NOT NULL, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + + CONSTRAINT "Report_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "public"."Follow" ( + "id" TEXT NOT NULL, + "followerId" TEXT NOT NULL, + "followingId" TEXT NOT NULL, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + + CONSTRAINT "Follow_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "public"."LeaderboardSnapshot" ( + "id" TEXT NOT NULL, + "month" TEXT NOT NULL, + "userId" TEXT NOT NULL, + "postId" TEXT, + "score" INTEGER NOT NULL, + "rank" INTEGER NOT NULL, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + + CONSTRAINT "LeaderboardSnapshot_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "public"."session" ( + "id" TEXT NOT NULL, + "expiresAt" TIMESTAMP(3) NOT NULL, + "token" TEXT NOT NULL, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" TIMESTAMP(3) NOT NULL, + "ipAddress" TEXT, + "userAgent" TEXT, + "userId" TEXT NOT NULL, + + CONSTRAINT "session_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "public"."verification" ( + "id" TEXT NOT NULL, + "identifier" TEXT NOT NULL, + "value" TEXT NOT NULL, + "expiresAt" TIMESTAMP(3) NOT NULL, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + + CONSTRAINT "verification_pkey" PRIMARY KEY ("id") +); + +-- CreateIndex +CREATE UNIQUE INDEX "user_email_key" ON "public"."user"("email"); + +-- CreateIndex +CREATE UNIQUE INDEX "account_provider_providerAccountId_key" ON "public"."account"("provider", "providerAccountId"); + +-- CreateIndex +CREATE UNIQUE INDEX "PostAuthor_userId_postId_key" ON "public"."PostAuthor"("userId", "postId"); + +-- CreateIndex +CREATE UNIQUE INDEX "Like_userId_postId_key" ON "public"."Like"("userId", "postId"); + +-- CreateIndex +CREATE UNIQUE INDEX "Follow_followerId_followingId_key" ON "public"."Follow"("followerId", "followingId"); + +-- CreateIndex +CREATE UNIQUE INDEX "session_token_key" ON "public"."session"("token"); + +-- AddForeignKey +ALTER TABLE "public"."account" ADD CONSTRAINT "account_userId_fkey" FOREIGN KEY ("userId") REFERENCES "public"."user"("id") ON DELETE RESTRICT ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "public"."PostAuthor" ADD CONSTRAINT "PostAuthor_userId_fkey" FOREIGN KEY ("userId") REFERENCES "public"."user"("id") ON DELETE RESTRICT ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "public"."PostAuthor" ADD CONSTRAINT "PostAuthor_postId_fkey" FOREIGN KEY ("postId") REFERENCES "public"."Post"("id") ON DELETE RESTRICT ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "public"."Like" ADD CONSTRAINT "Like_userId_fkey" FOREIGN KEY ("userId") REFERENCES "public"."user"("id") ON DELETE RESTRICT ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "public"."Like" ADD CONSTRAINT "Like_postId_fkey" FOREIGN KEY ("postId") REFERENCES "public"."Post"("id") ON DELETE RESTRICT ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "public"."Comment" ADD CONSTRAINT "Comment_userId_fkey" FOREIGN KEY ("userId") REFERENCES "public"."user"("id") ON DELETE RESTRICT ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "public"."Comment" ADD CONSTRAINT "Comment_postId_fkey" FOREIGN KEY ("postId") REFERENCES "public"."Post"("id") ON DELETE RESTRICT ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "public"."Report" ADD CONSTRAINT "Report_userId_fkey" FOREIGN KEY ("userId") REFERENCES "public"."user"("id") ON DELETE RESTRICT ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "public"."Report" ADD CONSTRAINT "Report_postId_fkey" FOREIGN KEY ("postId") REFERENCES "public"."Post"("id") ON DELETE RESTRICT ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "public"."Follow" ADD CONSTRAINT "Follow_followerId_fkey" FOREIGN KEY ("followerId") REFERENCES "public"."user"("id") ON DELETE RESTRICT ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "public"."Follow" ADD CONSTRAINT "Follow_followingId_fkey" FOREIGN KEY ("followingId") REFERENCES "public"."user"("id") ON DELETE RESTRICT ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "public"."LeaderboardSnapshot" ADD CONSTRAINT "LeaderboardSnapshot_userId_fkey" FOREIGN KEY ("userId") REFERENCES "public"."user"("id") ON DELETE RESTRICT ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "public"."LeaderboardSnapshot" ADD CONSTRAINT "LeaderboardSnapshot_postId_fkey" FOREIGN KEY ("postId") REFERENCES "public"."Post"("id") ON DELETE SET NULL ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "public"."session" ADD CONSTRAINT "session_userId_fkey" FOREIGN KEY ("userId") REFERENCES "public"."user"("id") ON DELETE CASCADE ON UPDATE CASCADE; diff --git a/prisma/migrations/migration_lock.toml b/prisma/migrations/migration_lock.toml new file mode 100644 index 0000000..044d57c --- /dev/null +++ b/prisma/migrations/migration_lock.toml @@ -0,0 +1,3 @@ +# Please do not edit this file manually +# It should be added in your version-control system (e.g., Git) +provider = "postgresql" diff --git a/prisma/schema.prisma b/prisma/schema.prisma index 9ca7b90..929825c 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -1,132 +1,176 @@ -generator client { - provider = "prisma-client-js" + +generator client { + provider = "prisma-client-js" } -datasource db { - provider = "postgresql" - url = env("DATABASE_URL") +datasource db { + provider = "postgresql" + url = env("DATABASE_URL") } -model User { - id String @id @default(cuid()) - name String - email String @unique - avatarUrl String? - role Role @default(USER) - createdAt DateTime @default(now()) - updatedAt DateTime @updatedAt - - accounts Account[] - posts PostAuthor[] // posts authored (many-to-many) - likes Like[] - comments Comment[] - reports Report[] - followers Follow[] @relation("UserFollowers") - following Follow[] @relation("UserFollowing") - leaderboard LeaderboardSnapshot[] +model User { + id String @id @default(cuid()) + name String + email String @unique + avatarUrl String? + role Role @default(USER) + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + accounts Account[] + posts PostAuthor[] // posts authored (many-to-many) + likes Like[] + comments Comment[] + reports Report[] + followers Follow[] @relation("UserFollowers") + following Follow[] @relation("UserFollowing") + leaderboard LeaderboardSnapshot[] + emailVerified Boolean @default(false) + image String? + position String? + sessions Session[] + + @@map("user") } -model Account { - id String @id @default(cuid()) - userId String - provider String - providerAccountId String - accessToken String? - refreshToken String? - expiresAt Int? - - user User @relation(fields: [userId], references: [id]) - - @@unique([provider, providerAccountId]) +model Account { + id String @id @default(cuid()) + userId String + provider String + providerAccountId String + accessToken String? + refreshToken String? + expiresAt Int? + + user User @relation(fields: [userId], references: [id]) + + accountId String + providerId String + idToken String? + accessTokenExpiresAt DateTime? + refreshTokenExpiresAt DateTime? + scope String? + password String? + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + @@unique([provider, providerAccountId]) + @@map("account") } -model Post { - id String @id @default(cuid()) - title String - description String - previewUrl String? - createdAt DateTime @default(now()) - updatedAt DateTime @updatedAt - - authors PostAuthor[] - likes Like[] - comments Comment[] - reports Report[] - leaderboard LeaderboardSnapshot[] +model Post { + id String @id @default(cuid()) + title String + description String + previewUrl String? + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + authors PostAuthor[] + likes Like[] + comments Comment[] + reports Report[] + leaderboard LeaderboardSnapshot[] } -model PostAuthor { - id String @id @default(cuid()) - userId String - postId String +model PostAuthor { + id String @id @default(cuid()) + userId String + postId String - user User @relation(fields: [userId], references: [id]) - post Post @relation(fields: [postId], references: [id]) + user User @relation(fields: [userId], references: [id]) + post Post @relation(fields: [postId], references: [id]) - @@unique([userId, postId]) + @@unique([userId, postId]) } -model Like { - id String @id @default(cuid()) - userId String - postId String - createdAt DateTime @default(now()) +model Like { + id String @id @default(cuid()) + userId String + postId String + createdAt DateTime @default(now()) - user User @relation(fields: [userId], references: [id]) - post Post @relation(fields: [postId], references: [id]) + user User @relation(fields: [userId], references: [id]) + post Post @relation(fields: [postId], references: [id]) - @@unique([userId, postId]) + @@unique([userId, postId]) } -model Comment { - id String @id @default(cuid()) - userId String - postId String - content String - createdAt DateTime @default(now()) - updatedAt DateTime @updatedAt +model Comment { + id String @id @default(cuid()) + userId String + postId String + content String + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt - user User @relation(fields: [userId], references: [id]) - post Post @relation(fields: [postId], references: [id]) + user User @relation(fields: [userId], references: [id]) + post Post @relation(fields: [postId], references: [id]) } -model Report { - id String @id @default(cuid()) - userId String - postId String - reason String - createdAt DateTime @default(now()) +model Report { + id String @id @default(cuid()) + userId String + postId String + reason String + createdAt DateTime @default(now()) - user User @relation(fields: [userId], references: [id]) - post Post @relation(fields: [postId], references: [id]) + user User @relation(fields: [userId], references: [id]) + post Post @relation(fields: [postId], references: [id]) } -model Follow { - id String @id @default(cuid()) - followerId String - followingId String - createdAt DateTime @default(now()) +model Follow { + id String @id @default(cuid()) + followerId String + followingId String + createdAt DateTime @default(now()) - follower User @relation("UserFollowers", fields: [followerId], references: [id]) - following User @relation("UserFollowing", fields: [followingId], references: [id]) + follower User @relation("UserFollowers", fields: [followerId], references: [id]) + following User @relation("UserFollowing", fields: [followingId], references: [id]) - @@unique([followerId, followingId]) + @@unique([followerId, followingId]) } -model LeaderboardSnapshot { - id String @id @default(cuid()) - month String // e.g., "2025-09" - userId String - postId String? - score Int - rank Int - createdAt DateTime @default(now()) - - user User @relation(fields: [userId], references: [id]) - post Post? @relation(fields: [postId], references: [id]) +model LeaderboardSnapshot { + id String @id @default(cuid()) + month String // e.g., "2025-09" + userId String + postId String? + score Int + rank Int + createdAt DateTime @default(now()) + + user User @relation(fields: [userId], references: [id]) + post Post? @relation(fields: [postId], references: [id]) } -enum Role { - USER - MODERATOR +enum Role { + USER + MODERATOR +} + +model Session { + id String @id + expiresAt DateTime + token String + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + ipAddress String? + userAgent String? + userId String + user User @relation(fields: [userId], references: [id], onDelete: Cascade) + + @@unique([token]) + @@map("session") +} + +model Verification { + id String @id + identifier String + value String + expiresAt DateTime + createdAt DateTime @default(now()) + updatedAt DateTime @default(now()) @updatedAt + + @@map("verification") } diff --git a/prisma/seeds/index.ts b/prisma/seeds/index.ts new file mode 100644 index 0000000..852cb3e --- /dev/null +++ b/prisma/seeds/index.ts @@ -0,0 +1,18 @@ +import { seedUsers } from './user' +import prisma from '../prisma' + +async function main() { + console.log('🌱 Start seeding...') + await seedUsers() + // await seedPosts(); + console.log('✅ Seeding finished.') +} + +main() + .catch((e) => { + console.error(e) + process.exit(1) + }) + .finally(async () => { + await prisma.$disconnect() + }) diff --git a/prisma/seeds/user.ts b/prisma/seeds/user.ts new file mode 100644 index 0000000..38a5707 --- /dev/null +++ b/prisma/seeds/user.ts @@ -0,0 +1,38 @@ +'use server' + +import bcrypt from 'bcryptjs' +import { nanoid } from 'nanoid' +import prisma from '../prisma' + +export async function seedUsers() { + const userId = nanoid() + + const hashedPassword = await bcrypt.hash( + (process.env.DEFAULT_PASSWORD as string) || 'Password1', + 10 + ) + + await prisma.user.upsert({ + where: { email: 'hello@codeshowcase.dev' }, + update: {}, // kalau sudah ada, tidak perlu update + create: { + id: userId, + name: 'Super Admin', + email: 'hello@codeshowcase.dev', + image: process.env.DEFAULT_USER_IMAGE, + emailVerified: true, + role: 'MODERATOR', + accounts: { + create: [ + { + accountId: userId, + provider: 'email-password', + providerAccountId: userId, + providerId: 'credential', + password: hashedPassword, + }, + ], + }, + }, + }) +} diff --git a/src/app/api/auth/[...all]/route.ts b/src/app/api/auth/[...all]/route.ts new file mode 100644 index 0000000..a314e5f --- /dev/null +++ b/src/app/api/auth/[...all]/route.ts @@ -0,0 +1,4 @@ +import { toNextJsHandler } from 'better-auth/next-js' +import { auth } from '@/lib/auth' + +export const { POST, GET } = toNextJsHandler(auth) diff --git a/src/app/auth/login/_components/login-form.tsx b/src/app/auth/login/_components/login-form.tsx new file mode 100644 index 0000000..96f4b67 --- /dev/null +++ b/src/app/auth/login/_components/login-form.tsx @@ -0,0 +1,196 @@ +'use client' + +import { Button } from '@/components/ui/button' +import { + Card, + CardContent, + CardHeader, + CardTitle, + CardDescription, + CardFooter, +} from '@/components/ui/card' +import { Input } from '@/components/ui/input' +import { Label } from '@/components/ui/label' +import { Checkbox } from '@/components/ui/checkbox' +import { useState } from 'react' +import { Loader2, Key } from 'lucide-react' +import authClient from '@/lib/auth-client' +import Link from 'next/link' +import { cn } from '@/lib/utils' + +export function LoginForm() { + const [email, setEmail] = useState('') + const [password, setPassword] = useState('') + const [loading, setLoading] = useState(false) + const [rememberMe, setRememberMe] = useState(false) + + return ( + + + Sign In + + Enter your email below to login to your account + + + +
+
+ + { + setEmail(e.target.value) + }} + value={email} + /> +
+ +
+
+ + + Forgot your password? + +
+ + setPassword(e.target.value)} + /> +
+ +
+ { + setRememberMe(!rememberMe) + }} + /> + +
+ + + +
+ + +
+
+
+ +
+

+ built with{' '} + + better-auth. + +

+
+
+
+ ) +} diff --git a/src/app/auth/login/page.tsx b/src/app/auth/login/page.tsx new file mode 100644 index 0000000..52bfecf --- /dev/null +++ b/src/app/auth/login/page.tsx @@ -0,0 +1,20 @@ +import { redirect } from "next/navigation"; +import { auth } from "@/lib/auth"; +import { LoginForm } from "./_components/login-form"; +import { headers } from "next/headers"; + +export default async function LoginPage() { + const session = await auth.api.getSession({ + headers: await headers(), + }); + + if (session?.user) { + redirect("/dashboard"); + } + + return ( +
+ +
+ ) +} \ No newline at end of file diff --git a/src/app/auth/logout/_components/logout-alert.tsx b/src/app/auth/logout/_components/logout-alert.tsx new file mode 100644 index 0000000..2bfae8a --- /dev/null +++ b/src/app/auth/logout/_components/logout-alert.tsx @@ -0,0 +1,47 @@ +'use client' + +import { + AlertDialog, + AlertDialogAction, + AlertDialogCancel, + AlertDialogContent, + AlertDialogDescription, + AlertDialogFooter, + AlertDialogHeader, + AlertDialogTitle, +} from '@/components/ui/alert-dialog' +import authClient from '@/lib/auth-client' +import { useRouter } from 'next/navigation' +import { useState } from 'react' + +export function LogoutAlert() { + const router = useRouter() + const [isOpen, setIsOpen] = useState(true) + + const handleLogout = async () => { + await authClient.signOut({ + fetchOptions: { + onSuccess: () => { + router.push('/auth/login') + }, + }, + }) + } + + return ( + + + + Are you sure you want to log out? + + You will be securely logged out of your account. You can always log back in later. + + + + Cancel + Log Out + + + + ) +} diff --git a/src/app/auth/logout/page.tsx b/src/app/auth/logout/page.tsx new file mode 100644 index 0000000..c4c0f16 --- /dev/null +++ b/src/app/auth/logout/page.tsx @@ -0,0 +1,20 @@ +import { LogoutAlert } from '@/app/auth/logout/_components/logout-alert' +import { headers } from 'next/headers' +import { auth } from '@/lib/auth' +import { redirect } from 'next/navigation' + +export default async function LogoutPage() { + const session = await auth.api.getSession({ + headers: await headers(), + }) + + if (!session?.user) { + redirect('/') + } + + return ( +
+ +
+ ) +} diff --git a/src/components/ui/alert-dialog.tsx b/src/components/ui/alert-dialog.tsx new file mode 100644 index 0000000..0863e40 --- /dev/null +++ b/src/components/ui/alert-dialog.tsx @@ -0,0 +1,157 @@ +"use client" + +import * as React from "react" +import * as AlertDialogPrimitive from "@radix-ui/react-alert-dialog" + +import { cn } from "@/lib/utils" +import { buttonVariants } from "@/components/ui/button" + +function AlertDialog({ + ...props +}: React.ComponentProps) { + return +} + +function AlertDialogTrigger({ + ...props +}: React.ComponentProps) { + return ( + + ) +} + +function AlertDialogPortal({ + ...props +}: React.ComponentProps) { + return ( + + ) +} + +function AlertDialogOverlay({ + className, + ...props +}: React.ComponentProps) { + return ( + + ) +} + +function AlertDialogContent({ + className, + ...props +}: React.ComponentProps) { + return ( + + + + + ) +} + +function AlertDialogHeader({ + className, + ...props +}: React.ComponentProps<"div">) { + return ( +
+ ) +} + +function AlertDialogFooter({ + className, + ...props +}: React.ComponentProps<"div">) { + return ( +
+ ) +} + +function AlertDialogTitle({ + className, + ...props +}: React.ComponentProps) { + return ( + + ) +} + +function AlertDialogDescription({ + className, + ...props +}: React.ComponentProps) { + return ( + + ) +} + +function AlertDialogAction({ + className, + ...props +}: React.ComponentProps) { + return ( + + ) +} + +function AlertDialogCancel({ + className, + ...props +}: React.ComponentProps) { + return ( + + ) +} + +export { + AlertDialog, + AlertDialogPortal, + AlertDialogOverlay, + AlertDialogTrigger, + AlertDialogContent, + AlertDialogHeader, + AlertDialogFooter, + AlertDialogTitle, + AlertDialogDescription, + AlertDialogAction, + AlertDialogCancel, +} diff --git a/src/components/ui/alert.tsx b/src/components/ui/alert.tsx new file mode 100644 index 0000000..1421354 --- /dev/null +++ b/src/components/ui/alert.tsx @@ -0,0 +1,66 @@ +import * as React from "react" +import { cva, type VariantProps } from "class-variance-authority" + +import { cn } from "@/lib/utils" + +const alertVariants = cva( + "relative w-full rounded-lg border px-4 py-3 text-sm grid has-[>svg]:grid-cols-[calc(var(--spacing)*4)_1fr] grid-cols-[0_1fr] has-[>svg]:gap-x-3 gap-y-0.5 items-start [&>svg]:size-4 [&>svg]:translate-y-0.5 [&>svg]:text-current", + { + variants: { + variant: { + default: "bg-card text-card-foreground", + destructive: + "text-destructive bg-card [&>svg]:text-current *:data-[slot=alert-description]:text-destructive/90", + }, + }, + defaultVariants: { + variant: "default", + }, + } +) + +function Alert({ + className, + variant, + ...props +}: React.ComponentProps<"div"> & VariantProps) { + return ( +
+ ) +} + +function AlertTitle({ className, ...props }: React.ComponentProps<"div">) { + return ( +
+ ) +} + +function AlertDescription({ + className, + ...props +}: React.ComponentProps<"div">) { + return ( +
+ ) +} + +export { Alert, AlertTitle, AlertDescription } diff --git a/src/components/ui/button.tsx b/src/components/ui/button.tsx index a6f6db0..d96719c 100644 --- a/src/components/ui/button.tsx +++ b/src/components/ui/button.tsx @@ -1,33 +1,35 @@ -import * as React from 'react' -import { Slot } from '@radix-ui/react-slot' -import { cva, type VariantProps } from 'class-variance-authority' +import * as React from "react" +import { Slot } from "@radix-ui/react-slot" +import { cva, type VariantProps } from "class-variance-authority" -import { cn } from '@/lib/utils' +import { cn } from "@/lib/utils" const buttonVariants = cva( "inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-all disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 shrink-0 [&_svg]:shrink-0 outline-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive", { variants: { variant: { - default: 'bg-primary text-primary-foreground hover:bg-primary/90', + default: "bg-primary text-primary-foreground hover:bg-primary/90", destructive: - 'bg-destructive text-white hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60', + "bg-destructive text-white hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60", outline: - 'border bg-background shadow-xs hover:bg-accent hover:text-accent-foreground dark:bg-input/30 dark:border-input dark:hover:bg-input/50', - secondary: 'bg-secondary text-secondary-foreground hover:bg-secondary/80', - ghost: 'hover:bg-accent hover:text-accent-foreground dark:hover:bg-accent/50', - link: 'text-primary underline-offset-4 hover:underline', + "border bg-background shadow-xs hover:bg-accent hover:text-accent-foreground dark:bg-input/30 dark:border-input dark:hover:bg-input/50", + secondary: + "bg-secondary text-secondary-foreground hover:bg-secondary/80", + ghost: + "hover:bg-accent hover:text-accent-foreground dark:hover:bg-accent/50", + link: "text-primary underline-offset-4 hover:underline", }, size: { - default: 'h-9 px-4 py-2 has-[>svg]:px-3', - sm: 'h-8 rounded-md gap-1.5 px-3 has-[>svg]:px-2.5', - lg: 'h-10 rounded-md px-6 has-[>svg]:px-4', - icon: 'size-9', + default: "h-9 px-4 py-2 has-[>svg]:px-3", + sm: "h-8 rounded-md gap-1.5 px-3 has-[>svg]:px-2.5", + lg: "h-10 rounded-md px-6 has-[>svg]:px-4", + icon: "size-9", }, }, defaultVariants: { - variant: 'default', - size: 'default', + variant: "default", + size: "default", }, } ) @@ -38,11 +40,11 @@ function Button({ size, asChild = false, ...props -}: React.ComponentProps<'button'> & +}: React.ComponentProps<"button"> & VariantProps & { asChild?: boolean }) { - const Comp = asChild ? Slot : 'button' + const Comp = asChild ? Slot : "button" return ( ) { + return ( + + + + + + ) +} + +export { Checkbox } diff --git a/src/components/ui/input.tsx b/src/components/ui/input.tsx new file mode 100644 index 0000000..af2b4a2 --- /dev/null +++ b/src/components/ui/input.tsx @@ -0,0 +1,21 @@ +import * as React from 'react' + +import { cn } from '@/lib/utils' + +function Input({ className, type, ...props }: React.ComponentProps<'input'>) { + return ( + + ) +} + +export { Input } diff --git a/src/components/ui/label.tsx b/src/components/ui/label.tsx new file mode 100644 index 0000000..0f0939b --- /dev/null +++ b/src/components/ui/label.tsx @@ -0,0 +1,21 @@ +'use client' + +import * as React from 'react' +import * as LabelPrimitive from '@radix-ui/react-label' + +import { cn } from '@/lib/utils' + +function Label({ className, ...props }: React.ComponentProps) { + return ( + + ) +} + +export { Label } diff --git a/src/lib/auth-client.ts b/src/lib/auth-client.ts new file mode 100644 index 0000000..d1a3f92 --- /dev/null +++ b/src/lib/auth-client.ts @@ -0,0 +1,8 @@ +import { inferAdditionalFields } from 'better-auth/client/plugins' +import { createAuthClient } from 'better-auth/react' +import type { auth } from './auth' + +const authClient = createAuthClient({ + plugins: [inferAdditionalFields()], +}) +export default authClient diff --git a/src/lib/auth.ts b/src/lib/auth.ts new file mode 100644 index 0000000..baffbd4 --- /dev/null +++ b/src/lib/auth.ts @@ -0,0 +1,74 @@ +import bcrypt from 'bcryptjs' +import { betterAuth } from 'better-auth' +import { prismaAdapter } from 'better-auth/adapters/prisma' +import { openAPI } from 'better-auth/plugins' +import prisma from '@/../prisma/prisma' + +export const auth = betterAuth({ + plugins: [openAPI()], + disabledPaths: [ + // Select some un-used path from better-auth + // "/sign-in/social", + // "/verify-email", + // "/send-verification-email", + // "/change-email", + // "/update-user", + // "/delete-user", + // "/link-social", + // "/delete-user/callback", + // "/unlink-account", + // "/account-info", + ], + trustedOrigins: [ + // add your trusted origin, example: https://ngodestudio.my.id + 'localhost:3000', + 'localhost:3002', + ], + emailAndPassword: { + enabled: true, + autoSignIn: false, + password: { + hash: async (password) => { + return await bcrypt.hash(password, 10) + }, + verify: async ({ password, hash }) => { + return await bcrypt.compare(password, hash) + }, + }, + }, + socialProviders: { + google: { + clientId: process.env.GOOGLE_CLIENT_ID!, + clientSecret: process.env.GOOGLE_CLIENT_SECRET!, + }, + github: { + clientId: process.env.GITHUB_CLIENT_ID!, + clientSecret: process.env.GITHUB_CLIENT_SECRET!, + }, + }, + database: prismaAdapter(prisma, { + provider: 'postgresql', + }), + + user: { + additionalFields: { + role: { + type: 'string', + required: false, + defaultValue: 'VIEWER', + input: false, + }, + position: { + type: 'string', + required: false, + }, + }, + }, + + // Setup your rate limiting for auth api + rateLimit: { + enabled: true, + window: 60, // time window in seconds + max: 30, // max requests in the window + }, +}) diff --git a/src/lib/authz.ts b/src/lib/authz.ts new file mode 100644 index 0000000..dbc9de4 --- /dev/null +++ b/src/lib/authz.ts @@ -0,0 +1,42 @@ +"use server" + +import { headers } from "next/headers"; +import { NextResponse } from "next/server"; +import type { Role } from "@prisma/client"; +import { auth } from "@/lib/auth"; + +/** + * Running on server !!! + * Use this to protect your api route or server action with roles based + */ +export async function requireAuth(options?: { roles?: Role[] }) { + const session = await auth.api.getSession({ + headers: await headers(), + }); + + if (!session?.user) { + return { + error: NextResponse.json( + { + status: "failed", + message: "Unauthorized: Login required", + }, + { status: 401 }, + ), + }; + } + + if (options?.roles && !options.roles.includes(session.user.role as Role)) { + return { + error: NextResponse.json( + { + status: "failed", + message: "Forbidden: Insufficient role", + }, + { status: 403 }, + ), + }; + } + + return { session }; +} diff --git a/src/lib/with-role.tsx b/src/lib/with-role.tsx new file mode 100644 index 0000000..004770d --- /dev/null +++ b/src/lib/with-role.tsx @@ -0,0 +1,29 @@ +import { headers } from 'next/headers' +import { redirect } from 'next/navigation' +import { auth } from '@/lib/auth' +import { Role } from '@prisma/client' + +/** + * + * Running on server !!! + * Use this to protect your server component page, is the same as authz.ts but different + */ +export function withRole

(Component: React.ComponentType

, allowed: Role[]) { + const RoleProtectedPage = async (props: P) => { + const session = await auth.api.getSession({ + headers: await headers(), + }) + + if (!session?.user) { + redirect('/auth/login') + } + + if (!allowed.includes(session.user.role as Role)) { + redirect('/unauthorized') + } + + return + } + + return RoleProtectedPage +} From bbcdeaaa6461fd8b79ce1f55dae31bd2bcfd6335 Mon Sep 17 00:00:00 2001 From: LunaticFTW Date: Thu, 25 Sep 2025 13:19:35 +0800 Subject: [PATCH 02/16] migration: remove table that unrelated with authentication feature --- .../migration.sql | 122 ------------------ 1 file changed, 122 deletions(-) diff --git a/prisma/migrations/20250924144302_base_migration/migration.sql b/prisma/migrations/20250924144302_base_migration/migration.sql index 7859aee..c1ae765 100644 --- a/prisma/migrations/20250924144302_base_migration/migration.sql +++ b/prisma/migrations/20250924144302_base_migration/migration.sql @@ -39,83 +39,6 @@ CREATE TABLE "public"."account" ( CONSTRAINT "account_pkey" PRIMARY KEY ("id") ); --- CreateTable -CREATE TABLE "public"."Post" ( - "id" TEXT NOT NULL, - "title" TEXT NOT NULL, - "description" TEXT NOT NULL, - "previewUrl" TEXT, - "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, - "updatedAt" TIMESTAMP(3) NOT NULL, - - CONSTRAINT "Post_pkey" PRIMARY KEY ("id") -); - --- CreateTable -CREATE TABLE "public"."PostAuthor" ( - "id" TEXT NOT NULL, - "userId" TEXT NOT NULL, - "postId" TEXT NOT NULL, - - CONSTRAINT "PostAuthor_pkey" PRIMARY KEY ("id") -); - --- CreateTable -CREATE TABLE "public"."Like" ( - "id" TEXT NOT NULL, - "userId" TEXT NOT NULL, - "postId" TEXT NOT NULL, - "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, - - CONSTRAINT "Like_pkey" PRIMARY KEY ("id") -); - --- CreateTable -CREATE TABLE "public"."Comment" ( - "id" TEXT NOT NULL, - "userId" TEXT NOT NULL, - "postId" TEXT NOT NULL, - "content" TEXT NOT NULL, - "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, - "updatedAt" TIMESTAMP(3) NOT NULL, - - CONSTRAINT "Comment_pkey" PRIMARY KEY ("id") -); - --- CreateTable -CREATE TABLE "public"."Report" ( - "id" TEXT NOT NULL, - "userId" TEXT NOT NULL, - "postId" TEXT NOT NULL, - "reason" TEXT NOT NULL, - "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, - - CONSTRAINT "Report_pkey" PRIMARY KEY ("id") -); - --- CreateTable -CREATE TABLE "public"."Follow" ( - "id" TEXT NOT NULL, - "followerId" TEXT NOT NULL, - "followingId" TEXT NOT NULL, - "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, - - CONSTRAINT "Follow_pkey" PRIMARY KEY ("id") -); - --- CreateTable -CREATE TABLE "public"."LeaderboardSnapshot" ( - "id" TEXT NOT NULL, - "month" TEXT NOT NULL, - "userId" TEXT NOT NULL, - "postId" TEXT, - "score" INTEGER NOT NULL, - "rank" INTEGER NOT NULL, - "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, - - CONSTRAINT "LeaderboardSnapshot_pkey" PRIMARY KEY ("id") -); - -- CreateTable CREATE TABLE "public"."session" ( "id" TEXT NOT NULL, @@ -148,56 +71,11 @@ CREATE UNIQUE INDEX "user_email_key" ON "public"."user"("email"); -- CreateIndex CREATE UNIQUE INDEX "account_provider_providerAccountId_key" ON "public"."account"("provider", "providerAccountId"); --- CreateIndex -CREATE UNIQUE INDEX "PostAuthor_userId_postId_key" ON "public"."PostAuthor"("userId", "postId"); - --- CreateIndex -CREATE UNIQUE INDEX "Like_userId_postId_key" ON "public"."Like"("userId", "postId"); - --- CreateIndex -CREATE UNIQUE INDEX "Follow_followerId_followingId_key" ON "public"."Follow"("followerId", "followingId"); - -- CreateIndex CREATE UNIQUE INDEX "session_token_key" ON "public"."session"("token"); -- AddForeignKey ALTER TABLE "public"."account" ADD CONSTRAINT "account_userId_fkey" FOREIGN KEY ("userId") REFERENCES "public"."user"("id") ON DELETE RESTRICT ON UPDATE CASCADE; --- AddForeignKey -ALTER TABLE "public"."PostAuthor" ADD CONSTRAINT "PostAuthor_userId_fkey" FOREIGN KEY ("userId") REFERENCES "public"."user"("id") ON DELETE RESTRICT ON UPDATE CASCADE; - --- AddForeignKey -ALTER TABLE "public"."PostAuthor" ADD CONSTRAINT "PostAuthor_postId_fkey" FOREIGN KEY ("postId") REFERENCES "public"."Post"("id") ON DELETE RESTRICT ON UPDATE CASCADE; - --- AddForeignKey -ALTER TABLE "public"."Like" ADD CONSTRAINT "Like_userId_fkey" FOREIGN KEY ("userId") REFERENCES "public"."user"("id") ON DELETE RESTRICT ON UPDATE CASCADE; - --- AddForeignKey -ALTER TABLE "public"."Like" ADD CONSTRAINT "Like_postId_fkey" FOREIGN KEY ("postId") REFERENCES "public"."Post"("id") ON DELETE RESTRICT ON UPDATE CASCADE; - --- AddForeignKey -ALTER TABLE "public"."Comment" ADD CONSTRAINT "Comment_userId_fkey" FOREIGN KEY ("userId") REFERENCES "public"."user"("id") ON DELETE RESTRICT ON UPDATE CASCADE; - --- AddForeignKey -ALTER TABLE "public"."Comment" ADD CONSTRAINT "Comment_postId_fkey" FOREIGN KEY ("postId") REFERENCES "public"."Post"("id") ON DELETE RESTRICT ON UPDATE CASCADE; - --- AddForeignKey -ALTER TABLE "public"."Report" ADD CONSTRAINT "Report_userId_fkey" FOREIGN KEY ("userId") REFERENCES "public"."user"("id") ON DELETE RESTRICT ON UPDATE CASCADE; - --- AddForeignKey -ALTER TABLE "public"."Report" ADD CONSTRAINT "Report_postId_fkey" FOREIGN KEY ("postId") REFERENCES "public"."Post"("id") ON DELETE RESTRICT ON UPDATE CASCADE; - --- AddForeignKey -ALTER TABLE "public"."Follow" ADD CONSTRAINT "Follow_followerId_fkey" FOREIGN KEY ("followerId") REFERENCES "public"."user"("id") ON DELETE RESTRICT ON UPDATE CASCADE; - --- AddForeignKey -ALTER TABLE "public"."Follow" ADD CONSTRAINT "Follow_followingId_fkey" FOREIGN KEY ("followingId") REFERENCES "public"."user"("id") ON DELETE RESTRICT ON UPDATE CASCADE; - --- AddForeignKey -ALTER TABLE "public"."LeaderboardSnapshot" ADD CONSTRAINT "LeaderboardSnapshot_userId_fkey" FOREIGN KEY ("userId") REFERENCES "public"."user"("id") ON DELETE RESTRICT ON UPDATE CASCADE; - --- AddForeignKey -ALTER TABLE "public"."LeaderboardSnapshot" ADD CONSTRAINT "LeaderboardSnapshot_postId_fkey" FOREIGN KEY ("postId") REFERENCES "public"."Post"("id") ON DELETE SET NULL ON UPDATE CASCADE; - -- AddForeignKey ALTER TABLE "public"."session" ADD CONSTRAINT "session_userId_fkey" FOREIGN KEY ("userId") REFERENCES "public"."user"("id") ON DELETE CASCADE ON UPDATE CASCADE; From 6c6acc9aed0d11a877790a616653555c1b049072 Mon Sep 17 00:00:00 2001 From: LunaticFTW Date: Thu, 25 Sep 2025 15:32:28 +0800 Subject: [PATCH 03/16] lint: fix lint error --- prisma/seeds/user.ts | 6 +-- src/app/auth/login/page.tsx | 14 +++---- src/components/ui/alert-dialog.tsx | 60 +++++++++-------------------- src/components/ui/alert.tsx | 30 ++++++--------- src/components/ui/button.tsx | 38 +++++++++--------- src/lib/authz.ts | 62 +++++++++++++++--------------- 6 files changed, 90 insertions(+), 120 deletions(-) diff --git a/prisma/seeds/user.ts b/prisma/seeds/user.ts index 38a5707..e0cdc1d 100644 --- a/prisma/seeds/user.ts +++ b/prisma/seeds/user.ts @@ -8,9 +8,9 @@ export async function seedUsers() { const userId = nanoid() const hashedPassword = await bcrypt.hash( - (process.env.DEFAULT_PASSWORD as string) || 'Password1', - 10 - ) + (process.env.DEFAULT_PASSWORD as string) || 'Password1', + 10 + ) await prisma.user.upsert({ where: { email: 'hello@codeshowcase.dev' }, diff --git a/src/app/auth/login/page.tsx b/src/app/auth/login/page.tsx index 52bfecf..5a650ea 100644 --- a/src/app/auth/login/page.tsx +++ b/src/app/auth/login/page.tsx @@ -1,15 +1,15 @@ -import { redirect } from "next/navigation"; -import { auth } from "@/lib/auth"; -import { LoginForm } from "./_components/login-form"; -import { headers } from "next/headers"; +import { redirect } from 'next/navigation' +import { auth } from '@/lib/auth' +import { LoginForm } from './_components/login-form' +import { headers } from 'next/headers' export default async function LoginPage() { const session = await auth.api.getSession({ headers: await headers(), - }); + }) if (session?.user) { - redirect("/dashboard"); + redirect('/dashboard') } return ( @@ -17,4 +17,4 @@ export default async function LoginPage() {

) -} \ No newline at end of file +} diff --git a/src/components/ui/alert-dialog.tsx b/src/components/ui/alert-dialog.tsx index 0863e40..d98a1de 100644 --- a/src/components/ui/alert-dialog.tsx +++ b/src/components/ui/alert-dialog.tsx @@ -1,31 +1,23 @@ -"use client" +'use client' -import * as React from "react" -import * as AlertDialogPrimitive from "@radix-ui/react-alert-dialog" +import * as React from 'react' +import * as AlertDialogPrimitive from '@radix-ui/react-alert-dialog' -import { cn } from "@/lib/utils" -import { buttonVariants } from "@/components/ui/button" +import { cn } from '@/lib/utils' +import { buttonVariants } from '@/components/ui/button' -function AlertDialog({ - ...props -}: React.ComponentProps) { +function AlertDialog({ ...props }: React.ComponentProps) { return } function AlertDialogTrigger({ ...props }: React.ComponentProps) { - return ( - - ) + return } -function AlertDialogPortal({ - ...props -}: React.ComponentProps) { - return ( - - ) +function AlertDialogPortal({ ...props }: React.ComponentProps) { + return } function AlertDialogOverlay({ @@ -36,7 +28,7 @@ function AlertDialogOverlay({ ) { +function AlertDialogHeader({ className, ...props }: React.ComponentProps<'div'>) { return (
) } -function AlertDialogFooter({ - className, - ...props -}: React.ComponentProps<"div">) { +function AlertDialogFooter({ className, ...props }: React.ComponentProps<'div'>) { return (
) @@ -99,7 +82,7 @@ function AlertDialogTitle({ return ( ) @@ -112,7 +95,7 @@ function AlertDialogDescription({ return ( ) @@ -122,12 +105,7 @@ function AlertDialogAction({ className, ...props }: React.ComponentProps) { - return ( - - ) + return } function AlertDialogCancel({ @@ -136,7 +114,7 @@ function AlertDialogCancel({ }: React.ComponentProps) { return ( ) diff --git a/src/components/ui/alert.tsx b/src/components/ui/alert.tsx index 1421354..c87dc72 100644 --- a/src/components/ui/alert.tsx +++ b/src/components/ui/alert.tsx @@ -1,20 +1,20 @@ -import * as React from "react" -import { cva, type VariantProps } from "class-variance-authority" +import * as React from 'react' +import { cva, type VariantProps } from 'class-variance-authority' -import { cn } from "@/lib/utils" +import { cn } from '@/lib/utils' const alertVariants = cva( - "relative w-full rounded-lg border px-4 py-3 text-sm grid has-[>svg]:grid-cols-[calc(var(--spacing)*4)_1fr] grid-cols-[0_1fr] has-[>svg]:gap-x-3 gap-y-0.5 items-start [&>svg]:size-4 [&>svg]:translate-y-0.5 [&>svg]:text-current", + 'relative w-full rounded-lg border px-4 py-3 text-sm grid has-[>svg]:grid-cols-[calc(var(--spacing)*4)_1fr] grid-cols-[0_1fr] has-[>svg]:gap-x-3 gap-y-0.5 items-start [&>svg]:size-4 [&>svg]:translate-y-0.5 [&>svg]:text-current', { variants: { variant: { - default: "bg-card text-card-foreground", + default: 'bg-card text-card-foreground', destructive: - "text-destructive bg-card [&>svg]:text-current *:data-[slot=alert-description]:text-destructive/90", + 'text-destructive bg-card [&>svg]:text-current *:data-[slot=alert-description]:text-destructive/90', }, }, defaultVariants: { - variant: "default", + variant: 'default', }, } ) @@ -23,7 +23,7 @@ function Alert({ className, variant, ...props -}: React.ComponentProps<"div"> & VariantProps) { +}: React.ComponentProps<'div'> & VariantProps) { return (
) { +function AlertTitle({ className, ...props }: React.ComponentProps<'div'>) { return (
) } -function AlertDescription({ - className, - ...props -}: React.ComponentProps<"div">) { +function AlertDescription({ className, ...props }: React.ComponentProps<'div'>) { return (
svg]:px-3", - sm: "h-8 rounded-md gap-1.5 px-3 has-[>svg]:px-2.5", - lg: "h-10 rounded-md px-6 has-[>svg]:px-4", - icon: "size-9", + default: 'h-9 px-4 py-2 has-[>svg]:px-3', + sm: 'h-8 rounded-md gap-1.5 px-3 has-[>svg]:px-2.5', + lg: 'h-10 rounded-md px-6 has-[>svg]:px-4', + icon: 'size-9', }, }, defaultVariants: { - variant: "default", - size: "default", + variant: 'default', + size: 'default', }, } ) @@ -40,11 +38,11 @@ function Button({ size, asChild = false, ...props -}: React.ComponentProps<"button"> & +}: React.ComponentProps<'button'> & VariantProps & { asChild?: boolean }) { - const Comp = asChild ? Slot : "button" + const Comp = asChild ? Slot : 'button' return ( Date: Thu, 25 Sep 2025 16:44:46 +0800 Subject: [PATCH 04/16] Fix: Update Prisma schema path in npm scripts Changed the Prisma schema path in all related npm scripts from 'src/prisma/schema.prisma' to 'prisma/schema.prisma' to reflect the new schema location. --- package.json | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/package.json b/package.json index 98a9078..9959003 100644 --- a/package.json +++ b/package.json @@ -9,14 +9,14 @@ "test": "vitest", "lint": "eslint", "lint:fix": "eslint --fix", - "db:generate:dev": "dotenv -e .env.development -- prisma generate --schema src/prisma/schema.prisma", - "db:generate:prod": "dotenv -e .env.production -- prisma generate --schema src/prisma/schema.prisma", - "db:migrate:dev": "dotenv -e .env.development -- prisma migrate dev --schema src/prisma/schema.prisma", - "db:migrate:prod": "dotenv -e .env.production -- prisma migrate deploy --schema src/prisma/schema.prisma", - "db:push:dev": "dotenv -e .env.development -- prisma db push --schema src/prisma/schema.prisma", - "db:reset:dev": "dotenv -e .env.development -- prisma migrate reset --skip-seed --schema src/prisma/schema.prisma", - "db:seed:dev": "dotenv -e .env.development -- prisma db seed --schema src/prisma/schema.prisma", - "db:seed:prod": "dotenv -e .env.production -- prisma db seed --schema src/prisma/schema.prisma" + "db:generate:dev": "dotenv -e .env.development -- prisma generate --schema prisma/schema.prisma", + "db:generate:prod": "dotenv -e .env.production -- prisma generate --schema prisma/schema.prisma", + "db:migrate:dev": "dotenv -e .env.development -- prisma migrate dev --schema prisma/schema.prisma", + "db:migrate:prod": "dotenv -e .env.production -- prisma migrate deploy --schema prisma/schema.prisma", + "db:push:dev": "dotenv -e .env.development -- prisma db push --schema prisma/schema.prisma", + "db:reset:dev": "dotenv -e .env.development -- prisma migrate reset --skip-seed --schema prisma/schema.prisma", + "db:seed:dev": "dotenv -e .env.development -- prisma db seed --schema prisma/schema.prisma", + "db:seed:prod": "dotenv -e .env.production -- prisma db seed --schema prisma/schema.prisma" }, "dependencies": { "@prisma/client": "^6.16.2", From dce8983f715c4f893423c065f0308ee222344ae1 Mon Sep 17 00:00:00 2001 From: LunaticFTW Date: Thu, 25 Sep 2025 16:59:57 +0800 Subject: [PATCH 05/16] Update .env.example --- .env.example | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/.env.example b/.env.example index 4cc714a..e3d15eb 100644 --- a/.env.example +++ b/.env.example @@ -1 +1,3 @@ -DATABASE_URL= \ No newline at end of file +DATABASE_URL= +BETTER_AUTH_SECRET= +BETTER_AUTH_URL= \ No newline at end of file From c0c73a6cb0dacdab53fc309525514fdc96f5de16 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Zain=F0=9F=8D=95?= Date: Thu, 25 Sep 2025 22:26:52 +0800 Subject: [PATCH 06/16] Refines Prisma configuration and updates linting scripts Centralizes the Prisma schema path within `prisma.config.ts`, enabling the removal of redundant `--schema` arguments from database-related npm scripts. Integrates `prisma format` into the `lint` and `lint:fix` commands to ensure consistent code formatting. Removes the unused `eslint-plugin-naming` dependency and cleans up an unused `lucide-react` import and unutilized `ctx` parameters in authentication callbacks. --- package-lock.json | 14 ------------- package.json | 21 +++++++++---------- prisma.config.ts | 1 + src/app/auth/login/_components/login-form.tsx | 14 ++++++------- 4 files changed, 18 insertions(+), 32 deletions(-) diff --git a/package-lock.json b/package-lock.json index d47656c..2ff6cbe 100644 --- a/package-lock.json +++ b/package-lock.json @@ -39,7 +39,6 @@ "eslint": "^9", "eslint-config-next": "15.5.3", "eslint-config-prettier": "^10.1.8", - "eslint-plugin-naming": "^0.1.10", "eslint-plugin-prettier": "^5.5.4", "eslint-plugin-unicorn": "^61.0.2", "jsdom": "^27.0.0", @@ -6022,19 +6021,6 @@ "eslint": "^3 || ^4 || ^5 || ^6 || ^7 || ^8 || ^9" } }, - "node_modules/eslint-plugin-naming": { - "version": "0.1.10", - "resolved": "https://registry.npmjs.org/eslint-plugin-naming/-/eslint-plugin-naming-0.1.10.tgz", - "integrity": "sha512-J1OofB0yFlebUN3cpVkErOq+KyD9o19UDciTaiy+ne00oujl4W0amqVkwG8XctmUtWPPZcGPahz3wR12PABXkA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 16.16.0" - }, - "peerDependencies": { - "eslint": ">=7.28.0" - } - }, "node_modules/eslint-plugin-prettier": { "version": "5.5.4", "resolved": "https://registry.npmjs.org/eslint-plugin-prettier/-/eslint-plugin-prettier-5.5.4.tgz", diff --git a/package.json b/package.json index 9959003..4e49e72 100644 --- a/package.json +++ b/package.json @@ -7,16 +7,16 @@ "build": "next build --turbopack", "start": "next start", "test": "vitest", - "lint": "eslint", - "lint:fix": "eslint --fix", - "db:generate:dev": "dotenv -e .env.development -- prisma generate --schema prisma/schema.prisma", - "db:generate:prod": "dotenv -e .env.production -- prisma generate --schema prisma/schema.prisma", - "db:migrate:dev": "dotenv -e .env.development -- prisma migrate dev --schema prisma/schema.prisma", - "db:migrate:prod": "dotenv -e .env.production -- prisma migrate deploy --schema prisma/schema.prisma", - "db:push:dev": "dotenv -e .env.development -- prisma db push --schema prisma/schema.prisma", - "db:reset:dev": "dotenv -e .env.development -- prisma migrate reset --skip-seed --schema prisma/schema.prisma", - "db:seed:dev": "dotenv -e .env.development -- prisma db seed --schema prisma/schema.prisma", - "db:seed:prod": "dotenv -e .env.production -- prisma db seed --schema prisma/schema.prisma" + "lint": "prisma format && eslint", + "lint:fix": "prisma format && eslint --fix", + "db:generate:dev": "dotenv -e .env.development -- prisma generate", + "db:generate:prod": "dotenv -e .env.production -- prisma generate", + "db:migrate:dev": "dotenv -e .env.development -- prisma migrate dev", + "db:migrate:prod": "dotenv -e .env.production -- prisma migrate deploy", + "db:push:dev": "dotenv -e .env.development -- prisma db push", + "db:reset:dev": "dotenv -e .env.development -- prisma migrate reset --skip-seed", + "db:seed:dev": "dotenv -e .env.development -- prisma db seed", + "db:seed:prod": "dotenv -e .env.production -- prisma db seed" }, "dependencies": { "@prisma/client": "^6.16.2", @@ -50,7 +50,6 @@ "eslint": "^9", "eslint-config-next": "15.5.3", "eslint-config-prettier": "^10.1.8", - "eslint-plugin-naming": "^0.1.10", "eslint-plugin-prettier": "^5.5.4", "eslint-plugin-unicorn": "^61.0.2", "jsdom": "^27.0.0", diff --git a/prisma.config.ts b/prisma.config.ts index 608f244..6d750cd 100644 --- a/prisma.config.ts +++ b/prisma.config.ts @@ -4,4 +4,5 @@ export default defineConfig({ migrations: { seed: 'tsx prisma/seeds/index.ts', }, + schema: 'prisma/schema.prisma', }) diff --git a/src/app/auth/login/_components/login-form.tsx b/src/app/auth/login/_components/login-form.tsx index 96f4b67..dc8a035 100644 --- a/src/app/auth/login/_components/login-form.tsx +++ b/src/app/auth/login/_components/login-form.tsx @@ -13,7 +13,7 @@ import { Input } from '@/components/ui/input' import { Label } from '@/components/ui/label' import { Checkbox } from '@/components/ui/checkbox' import { useState } from 'react' -import { Loader2, Key } from 'lucide-react' +import { Loader2 } from 'lucide-react' import authClient from '@/lib/auth-client' import Link from 'next/link' import { cn } from '@/lib/utils' @@ -89,10 +89,10 @@ export function LoginForm() { callbackURL: '/feeds', }, { - onRequest: (ctx) => { + onRequest: () => { setLoading(true) }, - onResponse: (ctx) => { + onResponse: () => { setLoading(false) }, } @@ -114,10 +114,10 @@ export function LoginForm() { callbackURL: '/feeds', }, { - onRequest: (ctx) => { + onRequest: () => { setLoading(true) }, - onResponse: (ctx) => { + onResponse: () => { setLoading(false) }, } @@ -160,10 +160,10 @@ export function LoginForm() { callbackURL: '/feeds', }, { - onRequest: (ctx) => { + onRequest: () => { setLoading(true) }, - onResponse: (ctx) => { + onResponse: () => { setLoading(false) }, } From 65eecb0533375938fd8c2b177e9754077bb40890 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Zain=F0=9F=8D=95?= Date: Thu, 25 Sep 2025 22:27:20 +0800 Subject: [PATCH 07/16] Prepares schema for initial authentication feature Temporarily comments out models and fields unrelated to core authentication. This includes `Post`, `Like`, `Comment`, `Report`, `Follow`, and `LeaderboardSnapshot` models, along with their associated relations in the `User` model. This change streamlines the database schema to focus solely on the initial authentication functionality, enabling a clearer development path. These features will be uncommented and implemented as they are developed. Also renames the base migration file to accurately reflect its purpose in setting up the initial authentication tables. --- .../migration.sql | 0 prisma/schema.prisma | 329 +++++++++--------- 2 files changed, 169 insertions(+), 160 deletions(-) rename prisma/migrations/{20250924144302_base_migration => 20250925142454_initial_tables_for_auth_feature}/migration.sql (100%) diff --git a/prisma/migrations/20250924144302_base_migration/migration.sql b/prisma/migrations/20250925142454_initial_tables_for_auth_feature/migration.sql similarity index 100% rename from prisma/migrations/20250924144302_base_migration/migration.sql rename to prisma/migrations/20250925142454_initial_tables_for_auth_feature/migration.sql diff --git a/prisma/schema.prisma b/prisma/schema.prisma index 929825c..7026d2e 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -1,176 +1,185 @@ - generator client { provider = "prisma-client-js" -} - +} + datasource db { - provider = "postgresql" + provider = "postgresql" url = env("DATABASE_URL") -} - +} + model User { - id String @id @default(cuid()) - name String - email String @unique - avatarUrl String? - role Role @default(USER) - createdAt DateTime @default(now()) - updatedAt DateTime @updatedAt - - accounts Account[] - posts PostAuthor[] // posts authored (many-to-many) - likes Like[] - comments Comment[] - reports Report[] - followers Follow[] @relation("UserFollowers") - following Follow[] @relation("UserFollowing") - leaderboard LeaderboardSnapshot[] - emailVerified Boolean @default(false) - image String? - position String? - sessions Session[] - + id String @id @default(cuid()) + name String + email String @unique + avatarUrl String? + role Role @default(USER) + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + accounts Account[] + + // #### UNCOMMENT THIS WHEN RELATED FEATURE IS READY #### + // posts PostAuthor[] // posts authored (many-to-many) + // likes Like[] + // comments Comment[] + // reports Report[] + // followers Follow[] @relation("UserFollowers") + // following Follow[] @relation("UserFollowing") + // leaderboard LeaderboardSnapshot[] + + emailVerified Boolean @default(false) + image String? + position String? + sessions Session[] + @@map("user") -} - +} + model Account { - id String @id @default(cuid()) - userId String - provider String - providerAccountId String - accessToken String? - refreshToken String? - expiresAt Int? - - user User @relation(fields: [userId], references: [id]) - - accountId String - providerId String - idToken String? - accessTokenExpiresAt DateTime? - refreshTokenExpiresAt DateTime? - scope String? - password String? - createdAt DateTime @default(now()) - updatedAt DateTime @updatedAt - - @@unique([provider, providerAccountId]) + id String @id @default(cuid()) + userId String + provider String + providerAccountId String + accessToken String? + refreshToken String? + expiresAt Int? + + user User @relation(fields: [userId], references: [id]) + + accountId String + providerId String + idToken String? + accessTokenExpiresAt DateTime? + refreshTokenExpiresAt DateTime? + scope String? + password String? + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + @@unique([provider, providerAccountId]) @@map("account") -} - -model Post { - id String @id @default(cuid()) - title String - description String - previewUrl String? - createdAt DateTime @default(now()) - updatedAt DateTime @updatedAt - - authors PostAuthor[] - likes Like[] - comments Comment[] - reports Report[] - leaderboard LeaderboardSnapshot[] -} - -model PostAuthor { - id String @id @default(cuid()) - userId String - postId String - - user User @relation(fields: [userId], references: [id]) - post Post @relation(fields: [postId], references: [id]) - - @@unique([userId, postId]) -} - -model Like { - id String @id @default(cuid()) - userId String - postId String - createdAt DateTime @default(now()) - - user User @relation(fields: [userId], references: [id]) - post Post @relation(fields: [postId], references: [id]) - - @@unique([userId, postId]) -} - -model Comment { - id String @id @default(cuid()) - userId String - postId String - content String - createdAt DateTime @default(now()) - updatedAt DateTime @updatedAt - - user User @relation(fields: [userId], references: [id]) - post Post @relation(fields: [postId], references: [id]) -} - -model Report { - id String @id @default(cuid()) - userId String - postId String - reason String - createdAt DateTime @default(now()) - - user User @relation(fields: [userId], references: [id]) - post Post @relation(fields: [postId], references: [id]) -} - -model Follow { - id String @id @default(cuid()) - followerId String - followingId String - createdAt DateTime @default(now()) - - follower User @relation("UserFollowers", fields: [followerId], references: [id]) - following User @relation("UserFollowing", fields: [followingId], references: [id]) - - @@unique([followerId, followingId]) -} - -model LeaderboardSnapshot { - id String @id @default(cuid()) - month String // e.g., "2025-09" - userId String - postId String? - score Int - rank Int - createdAt DateTime @default(now()) - - user User @relation(fields: [userId], references: [id]) - post Post? @relation(fields: [postId], references: [id]) -} - +} + +// #### UNCOMMENT THIS WHEN RELATED FEATURE IS READY #### +// model Post { +// id String @id @default(cuid()) +// title String +// description String +// previewUrl String? +// createdAt DateTime @default(now()) +// updatedAt DateTime @updatedAt + +// authors PostAuthor[] +// likes Like[] +// comments Comment[] +// reports Report[] +// leaderboard LeaderboardSnapshot[] +// } + +// #### UNCOMMENT THIS WHEN RELATED FEATURE IS READY #### +// model PostAuthor { +// id String @id @default(cuid()) +// userId String +// postId String + +// user User @relation(fields: [userId], references: [id]) +// post Post @relation(fields: [postId], references: [id]) + +// @@unique([userId, postId]) +// } + +// #### UNCOMMENT THIS WHEN RELATED FEATURE IS READY #### +// model Like { +// id String @id @default(cuid()) +// userId String +// postId String +// createdAt DateTime @default(now()) + +// user User @relation(fields: [userId], references: [id]) +// post Post @relation(fields: [postId], references: [id]) + +// @@unique([userId, postId]) +// } + +// #### UNCOMMENT THIS WHEN RELATED FEATURE IS READY #### +// model Comment { +// id String @id @default(cuid()) +// userId String +// postId String +// content String +// createdAt DateTime @default(now()) +// updatedAt DateTime @updatedAt + +// user User @relation(fields: [userId], references: [id]) +// post Post @relation(fields: [postId], references: [id]) +// } + +// #### UNCOMMENT THIS WHEN RELATED FEATURE IS READY #### +// model Report { +// id String @id @default(cuid()) +// userId String +// postId String +// reason String +// createdAt DateTime @default(now()) + +// user User @relation(fields: [userId], references: [id]) +// post Post @relation(fields: [postId], references: [id]) +// } + +// #### UNCOMMENT THIS WHEN RELATED FEATURE IS READY #### +// model Follow { +// id String @id @default(cuid()) +// followerId String +// followingId String +// createdAt DateTime @default(now()) + +// follower User @relation("UserFollowers", fields: [followerId], references: [id]) +// following User @relation("UserFollowing", fields: [followingId], references: [id]) + +// @@unique([followerId, followingId]) +// } + +// #### UNCOMMENT THIS WHEN RELATED FEATURE IS READY #### +// model LeaderboardSnapshot { +// id String @id @default(cuid()) +// month String // e.g., "2025-09" +// userId String +// postId String? +// score Int +// rank Int +// createdAt DateTime @default(now()) + +// user User @relation(fields: [userId], references: [id]) +// post Post? @relation(fields: [postId], references: [id]) +// } + enum Role { - USER + USER MODERATOR -} +} model Session { - id String @id - expiresAt DateTime - token String - createdAt DateTime @default(now()) - updatedAt DateTime @updatedAt - ipAddress String? - userAgent String? - userId String - user User @relation(fields: [userId], references: [id], onDelete: Cascade) - - @@unique([token]) + id String @id + expiresAt DateTime + token String + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + ipAddress String? + userAgent String? + userId String + user User @relation(fields: [userId], references: [id], onDelete: Cascade) + + @@unique([token]) @@map("session") -} +} model Verification { - id String @id - identifier String - value String - expiresAt DateTime - createdAt DateTime @default(now()) - updatedAt DateTime @default(now()) @updatedAt - + id String @id + identifier String + value String + expiresAt DateTime + createdAt DateTime @default(now()) + updatedAt DateTime @default(now()) @updatedAt + @@map("verification") -} +} From fdf6cd5992913c48e4fc142785f82cdf79a3ca92 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Zain=F0=9F=8D=95?= Date: Thu, 25 Sep 2025 22:52:16 +0800 Subject: [PATCH 08/16] Implements social media authentication options. This commit adds support for authenticating with Google and GitHub in addition to the existing authentication mechanism. This enhancement allows users to login using their social media accounts, providing a more convenient and secure experience. The changes include adding new environment variables and registering the authentication routes. --- .env.example | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/.env.example b/.env.example index e3d15eb..1dc0481 100644 --- a/.env.example +++ b/.env.example @@ -1,3 +1,9 @@ DATABASE_URL= BETTER_AUTH_SECRET= -BETTER_AUTH_URL= \ No newline at end of file +BETTER_AUTH_URL= + +GOOGLE_CLIENT_ID= +GOOGLE_CLIENT_SECRET= + +GITHUB_CLIENT_ID= +GITHUB_CLIENT_SECRET= \ No newline at end of file From 9b44baa2a44d377af0c06fc4729154199dac7349 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Zain=F0=9F=8D=95?= Date: Thu, 25 Sep 2025 23:51:45 +0800 Subject: [PATCH 09/16] remove unnecessary columns --- .../migration.sql | 7 ------- prisma/schema.prisma | 17 ++++++----------- prisma/seeds/user.ts | 4 +--- src/lib/auth.ts | 15 --------------- 4 files changed, 7 insertions(+), 36 deletions(-) rename prisma/migrations/{20250925142454_initial_tables_for_auth_feature => 20250925154009_initial_tables_for_auth_feature}/migration.sql (90%) diff --git a/prisma/migrations/20250925142454_initial_tables_for_auth_feature/migration.sql b/prisma/migrations/20250925154009_initial_tables_for_auth_feature/migration.sql similarity index 90% rename from prisma/migrations/20250925142454_initial_tables_for_auth_feature/migration.sql rename to prisma/migrations/20250925154009_initial_tables_for_auth_feature/migration.sql index c1ae765..559a4f2 100644 --- a/prisma/migrations/20250925142454_initial_tables_for_auth_feature/migration.sql +++ b/prisma/migrations/20250925154009_initial_tables_for_auth_feature/migration.sql @@ -12,7 +12,6 @@ CREATE TABLE "public"."user" ( "updatedAt" TIMESTAMP(3) NOT NULL, "emailVerified" BOOLEAN NOT NULL DEFAULT false, "image" TEXT, - "position" TEXT, CONSTRAINT "user_pkey" PRIMARY KEY ("id") ); @@ -21,11 +20,8 @@ CREATE TABLE "public"."user" ( CREATE TABLE "public"."account" ( "id" TEXT NOT NULL, "userId" TEXT NOT NULL, - "provider" TEXT NOT NULL, - "providerAccountId" TEXT NOT NULL, "accessToken" TEXT, "refreshToken" TEXT, - "expiresAt" INTEGER, "accountId" TEXT NOT NULL, "providerId" TEXT NOT NULL, "idToken" TEXT, @@ -68,9 +64,6 @@ CREATE TABLE "public"."verification" ( -- CreateIndex CREATE UNIQUE INDEX "user_email_key" ON "public"."user"("email"); --- CreateIndex -CREATE UNIQUE INDEX "account_provider_providerAccountId_key" ON "public"."account"("provider", "providerAccountId"); - -- CreateIndex CREATE UNIQUE INDEX "session_token_key" ON "public"."session"("token"); diff --git a/prisma/schema.prisma b/prisma/schema.prisma index 7026d2e..178b53b 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -16,7 +16,7 @@ model User { createdAt DateTime @default(now()) updatedAt DateTime @updatedAt - accounts Account[] + accounts Account[] // #### UNCOMMENT THIS WHEN RELATED FEATURE IS READY #### // posts PostAuthor[] // posts authored (many-to-many) @@ -27,22 +27,18 @@ model User { // following Follow[] @relation("UserFollowing") // leaderboard LeaderboardSnapshot[] - emailVerified Boolean @default(false) + emailVerified Boolean @default(false) image String? - position String? sessions Session[] @@map("user") } model Account { - id String @id @default(cuid()) - userId String - provider String - providerAccountId String - accessToken String? - refreshToken String? - expiresAt Int? + id String @id @default(cuid()) + userId String + accessToken String? + refreshToken String? user User @relation(fields: [userId], references: [id]) @@ -56,7 +52,6 @@ model Account { createdAt DateTime @default(now()) updatedAt DateTime @updatedAt - @@unique([provider, providerAccountId]) @@map("account") } diff --git a/prisma/seeds/user.ts b/prisma/seeds/user.ts index e0cdc1d..24c5fec 100644 --- a/prisma/seeds/user.ts +++ b/prisma/seeds/user.ts @@ -26,9 +26,7 @@ export async function seedUsers() { create: [ { accountId: userId, - provider: 'email-password', - providerAccountId: userId, - providerId: 'credential', + providerId: 'email-password', password: hashedPassword, }, ], diff --git a/src/lib/auth.ts b/src/lib/auth.ts index baffbd4..3edd510 100644 --- a/src/lib/auth.ts +++ b/src/lib/auth.ts @@ -50,21 +50,6 @@ export const auth = betterAuth({ provider: 'postgresql', }), - user: { - additionalFields: { - role: { - type: 'string', - required: false, - defaultValue: 'VIEWER', - input: false, - }, - position: { - type: 'string', - required: false, - }, - }, - }, - // Setup your rate limiting for auth api rateLimit: { enabled: true, From 9750a99c51eca50eedc9f2a0b1e2b1ed57a12cdc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Zain=F0=9F=8D=95?= Date: Thu, 25 Sep 2025 23:56:03 +0800 Subject: [PATCH 10/16] fix lint error --- src/lib/auth.ts | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/src/lib/auth.ts b/src/lib/auth.ts index 3edd510..17de991 100644 --- a/src/lib/auth.ts +++ b/src/lib/auth.ts @@ -50,6 +50,16 @@ export const auth = betterAuth({ provider: 'postgresql', }), + user: { + additionalFields: { + role: { + type: 'string', + required: true, + input: false, + }, + }, + }, + // Setup your rate limiting for auth api rateLimit: { enabled: true, From 690c9a7c9f7a99718d5c7503812d29331771fc14 Mon Sep 17 00:00:00 2001 From: LunaticFTW Date: Mon, 29 Sep 2025 16:10:24 +0800 Subject: [PATCH 11/16] implement client login form error handling --- package-lock.json | 39 ++- package.json | 5 +- src/app/auth/login/_components/login-form.tsx | 280 +++++++++--------- src/components/ui/form.tsx | 152 ++++++++++ 4 files changed, 334 insertions(+), 142 deletions(-) create mode 100644 src/components/ui/form.tsx diff --git a/package-lock.json b/package-lock.json index 2ff6cbe..9188671 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8,6 +8,7 @@ "name": "code-showcase-studio", "version": "0.1.0", "dependencies": { + "@hookform/resolvers": "^5.2.2", "@prisma/client": "^6.16.2", "@radix-ui/react-alert-dialog": "^1.1.15", "@radix-ui/react-avatar": "^1.1.10", @@ -24,8 +25,10 @@ "postcss": "^8.5.6", "react": "19.1.0", "react-dom": "19.1.0", + "react-hook-form": "^7.63.0", "tailwind-merge": "^3.3.1", - "tailwindcss": "^4.1.13" + "tailwindcss": "^4.1.13", + "zod": "^4.1.11" }, "devDependencies": { "@eslint/eslintrc": "^3", @@ -1212,6 +1215,18 @@ "integrity": "sha512-lhqDEAvWixy3bZ+UOYbPwUbBkwBq5C1LAJ/xPC8Oi+lL54oyakv/npbA0aU2hgCsx/1NUd4IBvV03+aUBWxerw==", "license": "MIT" }, + "node_modules/@hookform/resolvers": { + "version": "5.2.2", + "resolved": "https://registry.npmjs.org/@hookform/resolvers/-/resolvers-5.2.2.tgz", + "integrity": "sha512-A/IxlMLShx3KjV/HeTcTfaMxdwy690+L/ZADoeaTltLx+CVuzkeVIPuybK3jrRfw7YZnmdKsVVHAlEPIAEUNlA==", + "license": "MIT", + "dependencies": { + "@standard-schema/utils": "^0.3.0" + }, + "peerDependencies": { + "react-hook-form": "^7.55.0" + } + }, "node_modules/@humanfs/core": { "version": "0.19.1", "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.1.tgz", @@ -3088,6 +3103,12 @@ "devOptional": true, "license": "MIT" }, + "node_modules/@standard-schema/utils": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/@standard-schema/utils/-/utils-0.3.0.tgz", + "integrity": "sha512-e7Mew686owMaPJVNNLs55PUvgz371nKgwsc4vxE49zsODpJEnxgxRo2y/OKrqueavXgZNMDVj3DdHFlaSAeU8g==", + "license": "MIT" + }, "node_modules/@swc/helpers": { "version": "0.5.15", "resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.15.tgz", @@ -8724,6 +8745,22 @@ "react": "^19.1.0" } }, + "node_modules/react-hook-form": { + "version": "7.63.0", + "resolved": "https://registry.npmjs.org/react-hook-form/-/react-hook-form-7.63.0.tgz", + "integrity": "sha512-ZwueDMvUeucovM2VjkCf7zIHcs1aAlDimZu2Hvel5C5907gUzMpm4xCrQXtRzCvsBqFjonB4m3x4LzCFI1ZKWA==", + "license": "MIT", + "engines": { + "node": ">=18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/react-hook-form" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17 || ^18 || ^19" + } + }, "node_modules/react-is": { "version": "16.13.1", "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", diff --git a/package.json b/package.json index 4e49e72..eb05c9b 100644 --- a/package.json +++ b/package.json @@ -19,6 +19,7 @@ "db:seed:prod": "dotenv -e .env.production -- prisma db seed" }, "dependencies": { + "@hookform/resolvers": "^5.2.2", "@prisma/client": "^6.16.2", "@radix-ui/react-alert-dialog": "^1.1.15", "@radix-ui/react-avatar": "^1.1.10", @@ -35,8 +36,10 @@ "postcss": "^8.5.6", "react": "19.1.0", "react-dom": "19.1.0", + "react-hook-form": "^7.63.0", "tailwind-merge": "^3.3.1", - "tailwindcss": "^4.1.13" + "tailwindcss": "^4.1.13", + "zod": "^4.1.11" }, "devDependencies": { "@eslint/eslintrc": "^3", diff --git a/src/app/auth/login/_components/login-form.tsx b/src/app/auth/login/_components/login-form.tsx index dc8a035..e9a0d63 100644 --- a/src/app/auth/login/_components/login-form.tsx +++ b/src/app/auth/login/_components/login-form.tsx @@ -1,5 +1,11 @@ 'use client' +import { useState } from 'react' +import Link from 'next/link' +import { z } from 'zod' +import { useForm } from 'react-hook-form' +import { zodResolver } from '@hookform/resolvers/zod' +import { Loader2 } from 'lucide-react' import { Button } from '@/components/ui/button' import { Card, @@ -12,17 +18,69 @@ import { import { Input } from '@/components/ui/input' import { Label } from '@/components/ui/label' import { Checkbox } from '@/components/ui/checkbox' -import { useState } from 'react' -import { Loader2 } from 'lucide-react' -import authClient from '@/lib/auth-client' -import Link from 'next/link' +import { + Form, + FormControl, + FormField, + FormItem, + FormLabel, + FormMessage, +} from '@/components/ui/form' import { cn } from '@/lib/utils' +import authClient from '@/lib/auth-client' + +const formSchema = z.object({ + email: z.string().email({ + message: 'Please enter a valid email address.', + }), + password: z.string().min(1, { + message: 'Password is required.', + }), +}) export function LoginForm() { - const [email, setEmail] = useState('') - const [password, setPassword] = useState('') const [loading, setLoading] = useState(false) const [rememberMe, setRememberMe] = useState(false) + // No longer need the serverError state: const [serverError, setServerError] = useState('') + + const form = useForm>({ + resolver: zodResolver(formSchema), + defaultValues: { + email: '', + password: '', + }, + }) + + async function onSubmit(values: z.infer) { + setLoading(true) + + await authClient.signIn.email( + { + email: values.email, + password: values.password, + rememberMe, + callbackURL: '/feeds', + }, + { + onResponse: () => { + setLoading(false) + }, + onError: (error) => { + setLoading(false) + const errorMessage = error.error.message || 'An unexpected error occurred.' + + // --- CHANGE IS HERE --- + // Instead of a general alert, set the error message directly on the input fields. + // This makes the UI feel more responsive and directs the user to the problem area. + form.setError('email', { type: 'server' }) // Mark field as invalid + form.setError('password', { + type: 'server', + message: errorMessage, // Show the server message under the password field + }) + }, + } + ) + } return ( @@ -33,152 +91,94 @@ export function LoginForm() { -
-
- - { - setEmail(e.target.value) - }} - value={email} + {/* The top-level error Alert has been removed */} +
+ + ( + + Email + + + + + + )} /> -
- -
-
- - - Forgot your password? - -
- setPassword(e.target.value)} + ( + +
+ Password + + Forgot your password? + +
+ + + + {/* Server error messages will now appear here */} +
+ )} /> -
-
- { - setRememberMe(!rememberMe) - }} - /> - -
+
+ setRememberMe(Boolean(checked))} + /> + +
+ + + +
+ {/* ... Social login buttons remain the same ... */} - -
- - -
+ + + + + + Sign in with Google +
diff --git a/src/components/ui/form.tsx b/src/components/ui/form.tsx new file mode 100644 index 0000000..da1c196 --- /dev/null +++ b/src/components/ui/form.tsx @@ -0,0 +1,152 @@ +'use client' + +import * as React from 'react' +import * as LabelPrimitive from '@radix-ui/react-label' +import { Slot } from '@radix-ui/react-slot' +import { + Controller, + FormProvider, + useFormContext, + useFormState, + type ControllerProps, + type FieldPath, + type FieldValues, +} from 'react-hook-form' + +import { cn } from '@/lib/utils' +import { Label } from '@/components/ui/label' + +const Form = FormProvider + +type FormFieldContextValue< + TFieldValues extends FieldValues = FieldValues, + TName extends FieldPath = FieldPath, +> = { + name: TName +} + +const FormFieldContext = React.createContext({} as FormFieldContextValue) + +const FormField = < + TFieldValues extends FieldValues = FieldValues, + TName extends FieldPath = FieldPath, +>({ + ...props +}: ControllerProps) => { + return ( + + + + ) +} + +const useFormField = () => { + const fieldContext = React.useContext(FormFieldContext) + const itemContext = React.useContext(FormItemContext) + const { getFieldState } = useFormContext() + const formState = useFormState({ name: fieldContext.name }) + const fieldState = getFieldState(fieldContext.name, formState) + + if (!fieldContext) { + throw new Error('useFormField should be used within ') + } + + const { id } = itemContext + + return { + id, + name: fieldContext.name, + formItemId: `${id}-form-item`, + formDescriptionId: `${id}-form-item-description`, + formMessageId: `${id}-form-item-message`, + ...fieldState, + } +} + +type FormItemContextValue = { + id: string +} + +const FormItemContext = React.createContext({} as FormItemContextValue) + +function FormItem({ className, ...props }: React.ComponentProps<'div'>) { + const id = React.useId() + + return ( + +
+ + ) +} + +function FormLabel({ className, ...props }: React.ComponentProps) { + const { error, formItemId } = useFormField() + + return ( +
+ +
-

- built with{' '} - - better-auth. - -

+ + Belum punya akun? Daftar di sini. +
diff --git a/src/features/landing/components/cta-section.tsx b/src/features/landing/components/cta-section.tsx index b8b1edc..be9a52d 100644 --- a/src/features/landing/components/cta-section.tsx +++ b/src/features/landing/components/cta-section.tsx @@ -25,7 +25,7 @@ export default function CTASection() {

- + - + diff --git a/src/features/landing/components/hero-section.tsx b/src/features/landing/components/hero-section.tsx index 0270aaa..ac4cb59 100644 --- a/src/features/landing/components/hero-section.tsx +++ b/src/features/landing/components/hero-section.tsx @@ -1,5 +1,6 @@ import { Button } from '@/components/ui/button' import { ArrowRight, Sparkles } from 'lucide-react' +import Link from 'next/link' export default function HeroSection() { return ( @@ -23,10 +24,12 @@ export default function HeroSection() {

- + + + {/*