mirror of
https://github.com/RayLabsHQ/gitea-mirror.git
synced 2025-12-06 03:26:44 +03:00
Added Better Auth
This commit is contained in:
@@ -11,6 +11,8 @@ DATABASE_URL=sqlite://data/gitea-mirror.db
|
|||||||
|
|
||||||
# Security
|
# Security
|
||||||
JWT_SECRET=change-this-to-a-secure-random-string-in-production
|
JWT_SECRET=change-this-to-a-secure-random-string-in-production
|
||||||
|
BETTER_AUTH_SECRET=change-this-to-a-secure-random-string-in-production
|
||||||
|
BETTER_AUTH_URL=http://localhost:3000
|
||||||
|
|
||||||
# Optional GitHub/Gitea Mirror Configuration (for docker-compose, can also be set via web UI)
|
# Optional GitHub/Gitea Mirror Configuration (for docker-compose, can also be set via web UI)
|
||||||
# Uncomment and set as needed. These are passed as environment variables to the container.
|
# Uncomment and set as needed. These are passed as environment variables to the container.
|
||||||
|
|||||||
47
bun.lock
47
bun.lock
@@ -33,6 +33,7 @@
|
|||||||
"@types/react-dom": "^19.1.6",
|
"@types/react-dom": "^19.1.6",
|
||||||
"astro": "5.11.0",
|
"astro": "5.11.0",
|
||||||
"bcryptjs": "^3.0.2",
|
"bcryptjs": "^3.0.2",
|
||||||
|
"better-auth": "^1.2.12",
|
||||||
"canvas-confetti": "^1.9.3",
|
"canvas-confetti": "^1.9.3",
|
||||||
"class-variance-authority": "^0.7.1",
|
"class-variance-authority": "^0.7.1",
|
||||||
"clsx": "^2.1.1",
|
"clsx": "^2.1.1",
|
||||||
@@ -135,6 +136,10 @@
|
|||||||
|
|
||||||
"@babel/types": ["@babel/types@7.27.3", "", { "dependencies": { "@babel/helper-string-parser": "^7.27.1", "@babel/helper-validator-identifier": "^7.27.1" } }, "sha512-Y1GkI4ktrtvmawoSq+4FCVHNryea6uR+qUQy0AGxLSsjCX0nVmkYQMBLHDkXZuo5hGx7eYdnIaslsdBFm7zbUw=="],
|
"@babel/types": ["@babel/types@7.27.3", "", { "dependencies": { "@babel/helper-string-parser": "^7.27.1", "@babel/helper-validator-identifier": "^7.27.1" } }, "sha512-Y1GkI4ktrtvmawoSq+4FCVHNryea6uR+qUQy0AGxLSsjCX0nVmkYQMBLHDkXZuo5hGx7eYdnIaslsdBFm7zbUw=="],
|
||||||
|
|
||||||
|
"@better-auth/utils": ["@better-auth/utils@0.2.5", "", { "dependencies": { "typescript": "^5.8.2", "uncrypto": "^0.1.3" } }, "sha512-uI2+/8h/zVsH8RrYdG8eUErbuGBk16rZKQfz8CjxQOyCE6v7BqFYEbFwvOkvl1KbUdxhqOnXp78+uE5h8qVEgQ=="],
|
||||||
|
|
||||||
|
"@better-fetch/fetch": ["@better-fetch/fetch@1.1.18", "", {}, "sha512-rEFOE1MYIsBmoMJtQbl32PGHHXuG2hDxvEd7rUHE0vCBoFQVSDqaVs9hkZEtHCxRoY+CljXKFCOuJ8uxqw1LcA=="],
|
||||||
|
|
||||||
"@capsizecss/unpack": ["@capsizecss/unpack@2.4.0", "", { "dependencies": { "blob-to-buffer": "^1.2.8", "cross-fetch": "^3.0.4", "fontkit": "^2.0.2" } }, "sha512-GrSU71meACqcmIUxPYOJvGKF0yryjN/L1aCuE9DViCTJI7bfkjgYDPD1zbNDcINJwSSP6UaBZY9GAbYDO7re0Q=="],
|
"@capsizecss/unpack": ["@capsizecss/unpack@2.4.0", "", { "dependencies": { "blob-to-buffer": "^1.2.8", "cross-fetch": "^3.0.4", "fontkit": "^2.0.2" } }, "sha512-GrSU71meACqcmIUxPYOJvGKF0yryjN/L1aCuE9DViCTJI7bfkjgYDPD1zbNDcINJwSSP6UaBZY9GAbYDO7re0Q=="],
|
||||||
|
|
||||||
"@csstools/color-helpers": ["@csstools/color-helpers@5.0.2", "", {}, "sha512-JqWH1vsgdGcw2RR6VliXXdA0/59LttzlU8UlRT/iUUsEeWfYq8I+K0yhihEUTTHLRm1EXvpsCx3083EU15ecsA=="],
|
"@csstools/color-helpers": ["@csstools/color-helpers@5.0.2", "", {}, "sha512-JqWH1vsgdGcw2RR6VliXXdA0/59LttzlU8UlRT/iUUsEeWfYq8I+K0yhihEUTTHLRm1EXvpsCx3083EU15ecsA=="],
|
||||||
@@ -227,6 +232,8 @@
|
|||||||
|
|
||||||
"@floating-ui/utils": ["@floating-ui/utils@0.2.9", "", {}, "sha512-MDWhGtE+eHw5JW7lq4qhc5yRLS11ERl1c7Z6Xd0a58DozHES6EnNNwUWbMiG4J9Cgj053Bhk8zvlhFYKVhULwg=="],
|
"@floating-ui/utils": ["@floating-ui/utils@0.2.9", "", {}, "sha512-MDWhGtE+eHw5JW7lq4qhc5yRLS11ERl1c7Z6Xd0a58DozHES6EnNNwUWbMiG4J9Cgj053Bhk8zvlhFYKVhULwg=="],
|
||||||
|
|
||||||
|
"@hexagon/base64": ["@hexagon/base64@1.1.28", "", {}, "sha512-lhqDEAvWixy3bZ+UOYbPwUbBkwBq5C1LAJ/xPC8Oi+lL54oyakv/npbA0aU2hgCsx/1NUd4IBvV03+aUBWxerw=="],
|
||||||
|
|
||||||
"@img/sharp-darwin-arm64": ["@img/sharp-darwin-arm64@0.33.5", "", { "optionalDependencies": { "@img/sharp-libvips-darwin-arm64": "1.0.4" }, "os": "darwin", "cpu": "arm64" }, "sha512-UT4p+iz/2H4twwAoLCqfA9UH5pI6DggwKEGuaPy7nCVQ8ZsiY5PIcrRvD1DzuY3qYL07NtIQcWnBSY/heikIFQ=="],
|
"@img/sharp-darwin-arm64": ["@img/sharp-darwin-arm64@0.33.5", "", { "optionalDependencies": { "@img/sharp-libvips-darwin-arm64": "1.0.4" }, "os": "darwin", "cpu": "arm64" }, "sha512-UT4p+iz/2H4twwAoLCqfA9UH5pI6DggwKEGuaPy7nCVQ8ZsiY5PIcrRvD1DzuY3qYL07NtIQcWnBSY/heikIFQ=="],
|
||||||
|
|
||||||
"@img/sharp-darwin-x64": ["@img/sharp-darwin-x64@0.33.5", "", { "optionalDependencies": { "@img/sharp-libvips-darwin-x64": "1.0.4" }, "os": "darwin", "cpu": "x64" }, "sha512-fyHac4jIc1ANYGRDxtiqelIbdWkIuQaI84Mv45KvGRRxSAa7o7d1ZKAOBaYbnepLC1WqxfpimdeWfvqqSGwR2Q=="],
|
"@img/sharp-darwin-x64": ["@img/sharp-darwin-x64@0.33.5", "", { "optionalDependencies": { "@img/sharp-libvips-darwin-x64": "1.0.4" }, "os": "darwin", "cpu": "x64" }, "sha512-fyHac4jIc1ANYGRDxtiqelIbdWkIuQaI84Mv45KvGRRxSAa7o7d1ZKAOBaYbnepLC1WqxfpimdeWfvqqSGwR2Q=="],
|
||||||
@@ -277,8 +284,14 @@
|
|||||||
|
|
||||||
"@jridgewell/trace-mapping": ["@jridgewell/trace-mapping@0.3.25", "", { "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", "@jridgewell/sourcemap-codec": "^1.4.14" } }, "sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ=="],
|
"@jridgewell/trace-mapping": ["@jridgewell/trace-mapping@0.3.25", "", { "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", "@jridgewell/sourcemap-codec": "^1.4.14" } }, "sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ=="],
|
||||||
|
|
||||||
|
"@levischuck/tiny-cbor": ["@levischuck/tiny-cbor@0.2.11", "", {}, "sha512-llBRm4dT4Z89aRsm6u2oEZ8tfwL/2l6BwpZ7JcyieouniDECM5AqNgr/y08zalEIvW3RSK4upYyybDcmjXqAow=="],
|
||||||
|
|
||||||
"@mdx-js/mdx": ["@mdx-js/mdx@3.1.0", "", { "dependencies": { "@types/estree": "^1.0.0", "@types/estree-jsx": "^1.0.0", "@types/hast": "^3.0.0", "@types/mdx": "^2.0.0", "collapse-white-space": "^2.0.0", "devlop": "^1.0.0", "estree-util-is-identifier-name": "^3.0.0", "estree-util-scope": "^1.0.0", "estree-walker": "^3.0.0", "hast-util-to-jsx-runtime": "^2.0.0", "markdown-extensions": "^2.0.0", "recma-build-jsx": "^1.0.0", "recma-jsx": "^1.0.0", "recma-stringify": "^1.0.0", "rehype-recma": "^1.0.0", "remark-mdx": "^3.0.0", "remark-parse": "^11.0.0", "remark-rehype": "^11.0.0", "source-map": "^0.7.0", "unified": "^11.0.0", "unist-util-position-from-estree": "^2.0.0", "unist-util-stringify-position": "^4.0.0", "unist-util-visit": "^5.0.0", "vfile": "^6.0.0" } }, "sha512-/QxEhPAvGwbQmy1Px8F899L5Uc2KZ6JtXwlCgJmjSTBedwOZkByYcBG4GceIGPXRDsmfxhHazuS+hlOShRLeDw=="],
|
"@mdx-js/mdx": ["@mdx-js/mdx@3.1.0", "", { "dependencies": { "@types/estree": "^1.0.0", "@types/estree-jsx": "^1.0.0", "@types/hast": "^3.0.0", "@types/mdx": "^2.0.0", "collapse-white-space": "^2.0.0", "devlop": "^1.0.0", "estree-util-is-identifier-name": "^3.0.0", "estree-util-scope": "^1.0.0", "estree-walker": "^3.0.0", "hast-util-to-jsx-runtime": "^2.0.0", "markdown-extensions": "^2.0.0", "recma-build-jsx": "^1.0.0", "recma-jsx": "^1.0.0", "recma-stringify": "^1.0.0", "rehype-recma": "^1.0.0", "remark-mdx": "^3.0.0", "remark-parse": "^11.0.0", "remark-rehype": "^11.0.0", "source-map": "^0.7.0", "unified": "^11.0.0", "unist-util-position-from-estree": "^2.0.0", "unist-util-stringify-position": "^4.0.0", "unist-util-visit": "^5.0.0", "vfile": "^6.0.0" } }, "sha512-/QxEhPAvGwbQmy1Px8F899L5Uc2KZ6JtXwlCgJmjSTBedwOZkByYcBG4GceIGPXRDsmfxhHazuS+hlOShRLeDw=="],
|
||||||
|
|
||||||
|
"@noble/ciphers": ["@noble/ciphers@0.6.0", "", {}, "sha512-mIbq/R9QXk5/cTfESb1OKtyFnk7oc1Om/8onA1158K9/OZUQFDEVy55jVTato+xmp3XX6F6Qh0zz0Nc1AxAlRQ=="],
|
||||||
|
|
||||||
|
"@noble/hashes": ["@noble/hashes@1.8.0", "", {}, "sha512-jCs9ldd7NwzpgXDIf6P3+NrHh9/sD6CQdxHyjQI+h/6rDNo88ypBxxz45UDuZHz9r3tNz7N/VInSVoVdtXEI4A=="],
|
||||||
|
|
||||||
"@nodelib/fs.scandir": ["@nodelib/fs.scandir@2.1.5", "", { "dependencies": { "@nodelib/fs.stat": "2.0.5", "run-parallel": "^1.1.9" } }, "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g=="],
|
"@nodelib/fs.scandir": ["@nodelib/fs.scandir@2.1.5", "", { "dependencies": { "@nodelib/fs.stat": "2.0.5", "run-parallel": "^1.1.9" } }, "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g=="],
|
||||||
|
|
||||||
"@nodelib/fs.stat": ["@nodelib/fs.stat@2.0.5", "", {}, "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A=="],
|
"@nodelib/fs.stat": ["@nodelib/fs.stat@2.0.5", "", {}, "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A=="],
|
||||||
@@ -311,6 +324,16 @@
|
|||||||
|
|
||||||
"@oslojs/encoding": ["@oslojs/encoding@1.1.0", "", {}, "sha512-70wQhgYmndg4GCPxPPxPGevRKqTIJ2Nh4OkiMWmDAVYsTQ+Ta7Sq+rPevXyXGdzr30/qZBnyOalCszoMxlyldQ=="],
|
"@oslojs/encoding": ["@oslojs/encoding@1.1.0", "", {}, "sha512-70wQhgYmndg4GCPxPPxPGevRKqTIJ2Nh4OkiMWmDAVYsTQ+Ta7Sq+rPevXyXGdzr30/qZBnyOalCszoMxlyldQ=="],
|
||||||
|
|
||||||
|
"@peculiar/asn1-android": ["@peculiar/asn1-android@2.3.16", "", { "dependencies": { "@peculiar/asn1-schema": "^2.3.15", "asn1js": "^3.0.5", "tslib": "^2.8.1" } }, "sha512-a1viIv3bIahXNssrOIkXZIlI2ePpZaNmR30d4aBL99mu2rO+mT9D6zBsp7H6eROWGtmwv0Ionp5olJurIo09dw=="],
|
||||||
|
|
||||||
|
"@peculiar/asn1-ecc": ["@peculiar/asn1-ecc@2.3.15", "", { "dependencies": { "@peculiar/asn1-schema": "^2.3.15", "@peculiar/asn1-x509": "^2.3.15", "asn1js": "^3.0.5", "tslib": "^2.8.1" } }, "sha512-/HtR91dvgog7z/WhCVdxZJ/jitJuIu8iTqiyWVgRE9Ac5imt2sT/E4obqIVGKQw7PIy+X6i8lVBoT6wC73XUgA=="],
|
||||||
|
|
||||||
|
"@peculiar/asn1-rsa": ["@peculiar/asn1-rsa@2.3.15", "", { "dependencies": { "@peculiar/asn1-schema": "^2.3.15", "@peculiar/asn1-x509": "^2.3.15", "asn1js": "^3.0.5", "tslib": "^2.8.1" } }, "sha512-p6hsanvPhexRtYSOHihLvUUgrJ8y0FtOM97N5UEpC+VifFYyZa0iZ5cXjTkZoDwxJ/TTJ1IJo3HVTB2JJTpXvg=="],
|
||||||
|
|
||||||
|
"@peculiar/asn1-schema": ["@peculiar/asn1-schema@2.3.15", "", { "dependencies": { "asn1js": "^3.0.5", "pvtsutils": "^1.3.6", "tslib": "^2.8.1" } }, "sha512-QPeD8UA8axQREpgR5UTAfu2mqQmm97oUqahDtNdBcfj3qAnoXzFdQW+aNf/tD2WVXF8Fhmftxoj0eMIT++gX2w=="],
|
||||||
|
|
||||||
|
"@peculiar/asn1-x509": ["@peculiar/asn1-x509@2.3.15", "", { "dependencies": { "@peculiar/asn1-schema": "^2.3.15", "asn1js": "^3.0.5", "pvtsutils": "^1.3.6", "tslib": "^2.8.1" } }, "sha512-0dK5xqTqSLaxv1FHXIcd4Q/BZNuopg+u1l23hT9rOmQ1g4dNtw0g/RnEi+TboB0gOwGtrWn269v27cMgchFIIg=="],
|
||||||
|
|
||||||
"@radix-ui/number": ["@radix-ui/number@1.1.1", "", {}, "sha512-MkKCwxlXTgz6CFoJx3pCwn07GKp36+aZyu/u2Ln2VrA5DcdyCZkASEDBTd8x5whTQQL5CiYf4prXKLcgQdv29g=="],
|
"@radix-ui/number": ["@radix-ui/number@1.1.1", "", {}, "sha512-MkKCwxlXTgz6CFoJx3pCwn07GKp36+aZyu/u2Ln2VrA5DcdyCZkASEDBTd8x5whTQQL5CiYf4prXKLcgQdv29g=="],
|
||||||
|
|
||||||
"@radix-ui/primitive": ["@radix-ui/primitive@1.1.2", "", {}, "sha512-XnbHrrprsNqZKQhStrSwgRUQzoCI1glLzdw79xiZPoofhGICeZRSQ3dIxAKH1gb3OHfNf4d6f+vAv3kil2eggA=="],
|
"@radix-ui/primitive": ["@radix-ui/primitive@1.1.2", "", {}, "sha512-XnbHrrprsNqZKQhStrSwgRUQzoCI1glLzdw79xiZPoofhGICeZRSQ3dIxAKH1gb3OHfNf4d6f+vAv3kil2eggA=="],
|
||||||
@@ -459,6 +482,10 @@
|
|||||||
|
|
||||||
"@shikijs/vscode-textmate": ["@shikijs/vscode-textmate@10.0.2", "", {}, "sha512-83yeghZ2xxin3Nj8z1NMd/NCuca+gsYXswywDy5bHvwlWL8tpTQmzGeUuHd9FC3E/SBEMvzJRwWEOz5gGes9Qg=="],
|
"@shikijs/vscode-textmate": ["@shikijs/vscode-textmate@10.0.2", "", {}, "sha512-83yeghZ2xxin3Nj8z1NMd/NCuca+gsYXswywDy5bHvwlWL8tpTQmzGeUuHd9FC3E/SBEMvzJRwWEOz5gGes9Qg=="],
|
||||||
|
|
||||||
|
"@simplewebauthn/browser": ["@simplewebauthn/browser@13.1.2", "", {}, "sha512-aZnW0KawAM83fSBUgglP5WofbrLbLyr7CoPqYr66Eppm7zO86YX6rrCjRB3hQKPrL7ATvY4FVXlykZ6w6FwYYw=="],
|
||||||
|
|
||||||
|
"@simplewebauthn/server": ["@simplewebauthn/server@13.1.2", "", { "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" } }, "sha512-VwoDfvLXSCaRiD+xCIuyslU0HLxVggeE5BL06+GbsP2l1fGf5op8e0c3ZtKoi+vSg1q4ikjtAghC23ze2Q3H9g=="],
|
||||||
|
|
||||||
"@swc/helpers": ["@swc/helpers@0.5.17", "", { "dependencies": { "tslib": "^2.8.0" } }, "sha512-5IKx/Y13RsYd+sauPb2x+U/xZikHjolzfuDgTAl/Tdf3Q8rslRvC19NKDLgAJQ6wsqADk10ntlv08nPFw/gO/A=="],
|
"@swc/helpers": ["@swc/helpers@0.5.17", "", { "dependencies": { "tslib": "^2.8.0" } }, "sha512-5IKx/Y13RsYd+sauPb2x+U/xZikHjolzfuDgTAl/Tdf3Q8rslRvC19NKDLgAJQ6wsqADk10ntlv08nPFw/gO/A=="],
|
||||||
|
|
||||||
"@tailwindcss/node": ["@tailwindcss/node@4.1.11", "", { "dependencies": { "@ampproject/remapping": "^2.3.0", "enhanced-resolve": "^5.18.1", "jiti": "^2.4.2", "lightningcss": "1.30.1", "magic-string": "^0.30.17", "source-map-js": "^1.2.1", "tailwindcss": "4.1.11" } }, "sha512-yzhzuGRmv5QyU9qLNg4GTlYI6STedBWRE7NjxP45CsFYYq9taI0zJXZBMqIC/c8fViNLhmrbpSFS57EoxUmD6Q=="],
|
"@tailwindcss/node": ["@tailwindcss/node@4.1.11", "", { "dependencies": { "@ampproject/remapping": "^2.3.0", "enhanced-resolve": "^5.18.1", "jiti": "^2.4.2", "lightningcss": "1.30.1", "magic-string": "^0.30.17", "source-map-js": "^1.2.1", "tailwindcss": "4.1.11" } }, "sha512-yzhzuGRmv5QyU9qLNg4GTlYI6STedBWRE7NjxP45CsFYYq9taI0zJXZBMqIC/c8fViNLhmrbpSFS57EoxUmD6Q=="],
|
||||||
@@ -607,6 +634,8 @@
|
|||||||
|
|
||||||
"array-iterate": ["array-iterate@2.0.1", "", {}, "sha512-I1jXZMjAgCMmxT4qxXfPXa6SthSoE8h6gkSI9BGGNv8mP8G/v0blc+qFnZu6K42vTOiuME596QaLO0TP3Lk0xg=="],
|
"array-iterate": ["array-iterate@2.0.1", "", {}, "sha512-I1jXZMjAgCMmxT4qxXfPXa6SthSoE8h6gkSI9BGGNv8mP8G/v0blc+qFnZu6K42vTOiuME596QaLO0TP3Lk0xg=="],
|
||||||
|
|
||||||
|
"asn1js": ["asn1js@3.0.6", "", { "dependencies": { "pvtsutils": "^1.3.6", "pvutils": "^1.1.3", "tslib": "^2.8.1" } }, "sha512-UOCGPYbl0tv8+006qks/dTgV9ajs97X2p0FAbyS2iyCRrmLSRolDaHdp+v/CLgnzHc3fVB+CwYiUmei7ndFcgA=="],
|
||||||
|
|
||||||
"assertion-error": ["assertion-error@2.0.1", "", {}, "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA=="],
|
"assertion-error": ["assertion-error@2.0.1", "", {}, "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA=="],
|
||||||
|
|
||||||
"astring": ["astring@1.9.0", "", { "bin": { "astring": "bin/astring" } }, "sha512-LElXdjswlqjWrPpJFg1Fx4wpkOCxj1TDHlSV4PlaRxHGWko024xICaa97ZkMfs6DRKlCguiAI+rbXv5GWwXIkg=="],
|
"astring": ["astring@1.9.0", "", { "bin": { "astring": "bin/astring" } }, "sha512-LElXdjswlqjWrPpJFg1Fx4wpkOCxj1TDHlSV4PlaRxHGWko024xICaa97ZkMfs6DRKlCguiAI+rbXv5GWwXIkg=="],
|
||||||
@@ -625,6 +654,10 @@
|
|||||||
|
|
||||||
"before-after-hook": ["before-after-hook@4.0.0", "", {}, "sha512-q6tR3RPqIB1pMiTRMFcZwuG5T8vwp+vUvEG0vuI6B+Rikh5BfPp2fQ82c925FOs+b0lcFQ8CFrL+KbilfZFhOQ=="],
|
"before-after-hook": ["before-after-hook@4.0.0", "", {}, "sha512-q6tR3RPqIB1pMiTRMFcZwuG5T8vwp+vUvEG0vuI6B+Rikh5BfPp2fQ82c925FOs+b0lcFQ8CFrL+KbilfZFhOQ=="],
|
||||||
|
|
||||||
|
"better-auth": ["better-auth@1.2.12", "", { "dependencies": { "@better-auth/utils": "0.2.5", "@better-fetch/fetch": "^1.1.18", "@noble/ciphers": "^0.6.0", "@noble/hashes": "^1.6.1", "@simplewebauthn/browser": "^13.0.0", "@simplewebauthn/server": "^13.0.0", "better-call": "^1.0.8", "defu": "^6.1.4", "jose": "^6.0.11", "kysely": "^0.28.2", "nanostores": "^0.11.3", "zod": "^3.24.1" } }, "sha512-YicCyjQ+lxb7YnnaCewrVOjj3nPVa0xcfrOJK7k5MLMX9Mt9UnJ8GYaVQNHOHLyVxl92qc3C758X1ihqAUzm4w=="],
|
||||||
|
|
||||||
|
"better-call": ["better-call@1.0.12", "", { "dependencies": { "@better-fetch/fetch": "^1.1.4", "rou3": "^0.5.1", "set-cookie-parser": "^2.7.1", "uncrypto": "^0.1.3" } }, "sha512-ssq5OfB9Ungv2M1WVrRnMBomB0qz1VKuhkY2WxjHaLtlsHoSe9EPolj1xf7xf8LY9o3vfk3Rx6rCWI4oVHeBRg=="],
|
||||||
|
|
||||||
"blob-to-buffer": ["blob-to-buffer@1.2.9", "", {}, "sha512-BF033y5fN6OCofD3vgHmNtwZWRcq9NLyyxyILx9hfMy1sXYy4ojFl765hJ2lP0YaN2fuxPaLO2Vzzoxy0FLFFA=="],
|
"blob-to-buffer": ["blob-to-buffer@1.2.9", "", {}, "sha512-BF033y5fN6OCofD3vgHmNtwZWRcq9NLyyxyILx9hfMy1sXYy4ojFl765hJ2lP0YaN2fuxPaLO2Vzzoxy0FLFFA=="],
|
||||||
|
|
||||||
"boxen": ["boxen@8.0.1", "", { "dependencies": { "ansi-align": "^3.0.1", "camelcase": "^8.0.0", "chalk": "^5.3.0", "cli-boxes": "^3.0.0", "string-width": "^7.2.0", "type-fest": "^4.21.0", "widest-line": "^5.0.0", "wrap-ansi": "^9.0.0" } }, "sha512-F3PH5k5juxom4xktynS7MoFY+NUWH5LC4CnH11YB8NPew+HLpmBLCybSAEyb2F+4pRXhuhWqFesoQd6DAyc2hw=="],
|
"boxen": ["boxen@8.0.1", "", { "dependencies": { "ansi-align": "^3.0.1", "camelcase": "^8.0.0", "chalk": "^5.3.0", "cli-boxes": "^3.0.0", "string-width": "^7.2.0", "type-fest": "^4.21.0", "widest-line": "^5.0.0", "wrap-ansi": "^9.0.0" } }, "sha512-F3PH5k5juxom4xktynS7MoFY+NUWH5LC4CnH11YB8NPew+HLpmBLCybSAEyb2F+4pRXhuhWqFesoQd6DAyc2hw=="],
|
||||||
@@ -939,6 +972,8 @@
|
|||||||
|
|
||||||
"jiti": ["jiti@2.4.2", "", { "bin": { "jiti": "lib/jiti-cli.mjs" } }, "sha512-rg9zJN+G4n2nfJl5MW3BMygZX56zKPNVEYYqq7adpmMh4Jn2QNEwhvQlFy6jPVdcod7txZtKHWnyZiA3a0zP7A=="],
|
"jiti": ["jiti@2.4.2", "", { "bin": { "jiti": "lib/jiti-cli.mjs" } }, "sha512-rg9zJN+G4n2nfJl5MW3BMygZX56zKPNVEYYqq7adpmMh4Jn2QNEwhvQlFy6jPVdcod7txZtKHWnyZiA3a0zP7A=="],
|
||||||
|
|
||||||
|
"jose": ["jose@6.0.11", "", {}, "sha512-QxG7EaliDARm1O1S8BGakqncGT9s25bKL1WSf6/oa17Tkqwi8D2ZNglqCF+DsYF88/rV66Q/Q2mFAy697E1DUg=="],
|
||||||
|
|
||||||
"js-tokens": ["js-tokens@4.0.0", "", {}, "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ=="],
|
"js-tokens": ["js-tokens@4.0.0", "", {}, "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ=="],
|
||||||
|
|
||||||
"js-yaml": ["js-yaml@4.1.0", "", { "dependencies": { "argparse": "^2.0.1" }, "bin": { "js-yaml": "bin/js-yaml.js" } }, "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA=="],
|
"js-yaml": ["js-yaml@4.1.0", "", { "dependencies": { "argparse": "^2.0.1" }, "bin": { "js-yaml": "bin/js-yaml.js" } }, "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA=="],
|
||||||
@@ -961,6 +996,8 @@
|
|||||||
|
|
||||||
"kleur": ["kleur@4.1.5", "", {}, "sha512-o+NO+8WrRiQEE4/7nwRJhN1HWpVmJm511pBHUxPLtp0BUISzlBplORYSmTclCnJvQq2tKu/sgl3xVpkc7ZWuQQ=="],
|
"kleur": ["kleur@4.1.5", "", {}, "sha512-o+NO+8WrRiQEE4/7nwRJhN1HWpVmJm511pBHUxPLtp0BUISzlBplORYSmTclCnJvQq2tKu/sgl3xVpkc7ZWuQQ=="],
|
||||||
|
|
||||||
|
"kysely": ["kysely@0.28.2", "", {}, "sha512-4YAVLoF0Sf0UTqlhgQMFU9iQECdah7n+13ANkiuVfRvlK+uI0Etbgd7bVP36dKlG+NXWbhGua8vnGt+sdhvT7A=="],
|
||||||
|
|
||||||
"lightningcss": ["lightningcss@1.30.1", "", { "dependencies": { "detect-libc": "^2.0.3" }, "optionalDependencies": { "lightningcss-darwin-arm64": "1.30.1", "lightningcss-darwin-x64": "1.30.1", "lightningcss-freebsd-x64": "1.30.1", "lightningcss-linux-arm-gnueabihf": "1.30.1", "lightningcss-linux-arm64-gnu": "1.30.1", "lightningcss-linux-arm64-musl": "1.30.1", "lightningcss-linux-x64-gnu": "1.30.1", "lightningcss-linux-x64-musl": "1.30.1", "lightningcss-win32-arm64-msvc": "1.30.1", "lightningcss-win32-x64-msvc": "1.30.1" } }, "sha512-xi6IyHML+c9+Q3W0S4fCQJOym42pyurFiJUHEcEyHS0CeKzia4yZDEsLlqOFykxOdHpNy0NmvVO31vcSqAxJCg=="],
|
"lightningcss": ["lightningcss@1.30.1", "", { "dependencies": { "detect-libc": "^2.0.3" }, "optionalDependencies": { "lightningcss-darwin-arm64": "1.30.1", "lightningcss-darwin-x64": "1.30.1", "lightningcss-freebsd-x64": "1.30.1", "lightningcss-linux-arm-gnueabihf": "1.30.1", "lightningcss-linux-arm64-gnu": "1.30.1", "lightningcss-linux-arm64-musl": "1.30.1", "lightningcss-linux-x64-gnu": "1.30.1", "lightningcss-linux-x64-musl": "1.30.1", "lightningcss-win32-arm64-msvc": "1.30.1", "lightningcss-win32-x64-msvc": "1.30.1" } }, "sha512-xi6IyHML+c9+Q3W0S4fCQJOym42pyurFiJUHEcEyHS0CeKzia4yZDEsLlqOFykxOdHpNy0NmvVO31vcSqAxJCg=="],
|
||||||
|
|
||||||
"lightningcss-darwin-arm64": ["lightningcss-darwin-arm64@1.30.1", "", { "os": "darwin", "cpu": "arm64" }, "sha512-c8JK7hyE65X1MHMN+Viq9n11RRC7hgin3HhYKhrMyaXflk5GVplZ60IxyoVtzILeKr+xAJwg6zK6sjTBJ0FKYQ=="],
|
"lightningcss-darwin-arm64": ["lightningcss-darwin-arm64@1.30.1", "", { "os": "darwin", "cpu": "arm64" }, "sha512-c8JK7hyE65X1MHMN+Viq9n11RRC7hgin3HhYKhrMyaXflk5GVplZ60IxyoVtzILeKr+xAJwg6zK6sjTBJ0FKYQ=="],
|
||||||
@@ -1147,6 +1184,8 @@
|
|||||||
|
|
||||||
"nanoid": ["nanoid@3.3.11", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w=="],
|
"nanoid": ["nanoid@3.3.11", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w=="],
|
||||||
|
|
||||||
|
"nanostores": ["nanostores@0.11.4", "", {}, "sha512-k1oiVNN4hDK8NcNERSZLQiMfRzEGtfnvZvdBvey3SQbgn8Dcrk0h1I6vpxApjb10PFUflZrgJ2WEZyJQ+5v7YQ=="],
|
||||||
|
|
||||||
"neotraverse": ["neotraverse@0.6.18", "", {}, "sha512-Z4SmBUweYa09+o6pG+eASabEpP6QkQ70yHj351pQoEXIs8uHbaU2DWVmzBANKgflPa47A50PtB2+NgRpQvr7vA=="],
|
"neotraverse": ["neotraverse@0.6.18", "", {}, "sha512-Z4SmBUweYa09+o6pG+eASabEpP6QkQ70yHj351pQoEXIs8uHbaU2DWVmzBANKgflPa47A50PtB2+NgRpQvr7vA=="],
|
||||||
|
|
||||||
"next-themes": ["next-themes@0.4.6", "", { "peerDependencies": { "react": "^16.8 || ^17 || ^18 || ^19 || ^19.0.0-rc", "react-dom": "^16.8 || ^17 || ^18 || ^19 || ^19.0.0-rc" } }, "sha512-pZvgD5L0IEvX5/9GWyHMf3m8BKiVQwsCMHfoFosXtXBMnaS0ZnIJ9ST4b4NqLVKDEm8QBxoNNGNaBv2JNF6XNA=="],
|
"next-themes": ["next-themes@0.4.6", "", { "peerDependencies": { "react": "^16.8 || ^17 || ^18 || ^19 || ^19.0.0-rc", "react-dom": "^16.8 || ^17 || ^18 || ^19 || ^19.0.0-rc" } }, "sha512-pZvgD5L0IEvX5/9GWyHMf3m8BKiVQwsCMHfoFosXtXBMnaS0ZnIJ9ST4b4NqLVKDEm8QBxoNNGNaBv2JNF6XNA=="],
|
||||||
@@ -1215,6 +1254,10 @@
|
|||||||
|
|
||||||
"punycode": ["punycode@2.3.1", "", {}, "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg=="],
|
"punycode": ["punycode@2.3.1", "", {}, "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg=="],
|
||||||
|
|
||||||
|
"pvtsutils": ["pvtsutils@1.3.6", "", { "dependencies": { "tslib": "^2.8.1" } }, "sha512-PLgQXQ6H2FWCaeRak8vvk1GW462lMxB5s3Jm673N82zI4vqtVUPuZdffdZbPDFRoU8kAhItWFtPCWiPpp4/EDg=="],
|
||||||
|
|
||||||
|
"pvutils": ["pvutils@1.1.3", "", {}, "sha512-pMpnA0qRdFp32b1sJl1wOJNxZLQ2cbQx+k6tjNtZ8CpvVhNqEPRgivZ2WOUev2YMajecdH7ctUPDvEe87nariQ=="],
|
||||||
|
|
||||||
"queue-microtask": ["queue-microtask@1.2.3", "", {}, "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A=="],
|
"queue-microtask": ["queue-microtask@1.2.3", "", {}, "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A=="],
|
||||||
|
|
||||||
"radix3": ["radix3@1.1.2", "", {}, "sha512-b484I/7b8rDEdSDKckSSBA8knMpcdsXudlE/LNL639wFoHKwLbEkQFZHWEYwDC0wa0FKUcCY+GAF73Z7wxNVFA=="],
|
"radix3": ["radix3@1.1.2", "", {}, "sha512-b484I/7b8rDEdSDKckSSBA8knMpcdsXudlE/LNL639wFoHKwLbEkQFZHWEYwDC0wa0FKUcCY+GAF73Z7wxNVFA=="],
|
||||||
@@ -1299,6 +1342,8 @@
|
|||||||
|
|
||||||
"rollup": ["rollup@4.41.1", "", { "dependencies": { "@types/estree": "1.0.7" }, "optionalDependencies": { "@rollup/rollup-android-arm-eabi": "4.41.1", "@rollup/rollup-android-arm64": "4.41.1", "@rollup/rollup-darwin-arm64": "4.41.1", "@rollup/rollup-darwin-x64": "4.41.1", "@rollup/rollup-freebsd-arm64": "4.41.1", "@rollup/rollup-freebsd-x64": "4.41.1", "@rollup/rollup-linux-arm-gnueabihf": "4.41.1", "@rollup/rollup-linux-arm-musleabihf": "4.41.1", "@rollup/rollup-linux-arm64-gnu": "4.41.1", "@rollup/rollup-linux-arm64-musl": "4.41.1", "@rollup/rollup-linux-loongarch64-gnu": "4.41.1", "@rollup/rollup-linux-powerpc64le-gnu": "4.41.1", "@rollup/rollup-linux-riscv64-gnu": "4.41.1", "@rollup/rollup-linux-riscv64-musl": "4.41.1", "@rollup/rollup-linux-s390x-gnu": "4.41.1", "@rollup/rollup-linux-x64-gnu": "4.41.1", "@rollup/rollup-linux-x64-musl": "4.41.1", "@rollup/rollup-win32-arm64-msvc": "4.41.1", "@rollup/rollup-win32-ia32-msvc": "4.41.1", "@rollup/rollup-win32-x64-msvc": "4.41.1", "fsevents": "~2.3.2" }, "bin": { "rollup": "dist/bin/rollup" } }, "sha512-cPmwD3FnFv8rKMBc1MxWCwVQFxwf1JEmSX3iQXrRVVG15zerAIXRjMFVWnd5Q5QvgKF7Aj+5ykXFhUl+QGnyOw=="],
|
"rollup": ["rollup@4.41.1", "", { "dependencies": { "@types/estree": "1.0.7" }, "optionalDependencies": { "@rollup/rollup-android-arm-eabi": "4.41.1", "@rollup/rollup-android-arm64": "4.41.1", "@rollup/rollup-darwin-arm64": "4.41.1", "@rollup/rollup-darwin-x64": "4.41.1", "@rollup/rollup-freebsd-arm64": "4.41.1", "@rollup/rollup-freebsd-x64": "4.41.1", "@rollup/rollup-linux-arm-gnueabihf": "4.41.1", "@rollup/rollup-linux-arm-musleabihf": "4.41.1", "@rollup/rollup-linux-arm64-gnu": "4.41.1", "@rollup/rollup-linux-arm64-musl": "4.41.1", "@rollup/rollup-linux-loongarch64-gnu": "4.41.1", "@rollup/rollup-linux-powerpc64le-gnu": "4.41.1", "@rollup/rollup-linux-riscv64-gnu": "4.41.1", "@rollup/rollup-linux-riscv64-musl": "4.41.1", "@rollup/rollup-linux-s390x-gnu": "4.41.1", "@rollup/rollup-linux-x64-gnu": "4.41.1", "@rollup/rollup-linux-x64-musl": "4.41.1", "@rollup/rollup-win32-arm64-msvc": "4.41.1", "@rollup/rollup-win32-ia32-msvc": "4.41.1", "@rollup/rollup-win32-x64-msvc": "4.41.1", "fsevents": "~2.3.2" }, "bin": { "rollup": "dist/bin/rollup" } }, "sha512-cPmwD3FnFv8rKMBc1MxWCwVQFxwf1JEmSX3iQXrRVVG15zerAIXRjMFVWnd5Q5QvgKF7Aj+5ykXFhUl+QGnyOw=="],
|
||||||
|
|
||||||
|
"rou3": ["rou3@0.5.1", "", {}, "sha512-OXMmJ3zRk2xeXFGfA3K+EOPHC5u7RDFG7lIOx0X1pdnhUkI8MdVrbV+sNsD80ElpUZ+MRHdyxPnFthq9VHs8uQ=="],
|
||||||
|
|
||||||
"rrweb-cssom": ["rrweb-cssom@0.8.0", "", {}, "sha512-guoltQEx+9aMf2gDZ0s62EcV8lsXR+0w8915TC3ITdn2YueuNjdAYh/levpU9nFaoChh9RUS5ZdQMrKfVEN9tw=="],
|
"rrweb-cssom": ["rrweb-cssom@0.8.0", "", {}, "sha512-guoltQEx+9aMf2gDZ0s62EcV8lsXR+0w8915TC3ITdn2YueuNjdAYh/levpU9nFaoChh9RUS5ZdQMrKfVEN9tw=="],
|
||||||
|
|
||||||
"run-parallel": ["run-parallel@1.2.0", "", { "dependencies": { "queue-microtask": "^1.2.2" } }, "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA=="],
|
"run-parallel": ["run-parallel@1.2.0", "", { "dependencies": { "queue-microtask": "^1.2.2" } }, "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA=="],
|
||||||
@@ -1317,6 +1362,8 @@
|
|||||||
|
|
||||||
"server-destroy": ["server-destroy@1.0.1", "", {}, "sha512-rb+9B5YBIEzYcD6x2VKidaa+cqYBJQKnU4oe4E3ANwRRN56yk/ua1YCJT1n21NTS8w6CcOclAKNP3PhdCXKYtQ=="],
|
"server-destroy": ["server-destroy@1.0.1", "", {}, "sha512-rb+9B5YBIEzYcD6x2VKidaa+cqYBJQKnU4oe4E3ANwRRN56yk/ua1YCJT1n21NTS8w6CcOclAKNP3PhdCXKYtQ=="],
|
||||||
|
|
||||||
|
"set-cookie-parser": ["set-cookie-parser@2.7.1", "", {}, "sha512-IOc8uWeOZgnb3ptbCURJWNjWUPcO3ZnTTdzsurqERrP6nPyv+paC55vJM0LpOlT2ne+Ix+9+CRG1MNLlyZ4GjQ=="],
|
||||||
|
|
||||||
"setprototypeof": ["setprototypeof@1.2.0", "", {}, "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw=="],
|
"setprototypeof": ["setprototypeof@1.2.0", "", {}, "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw=="],
|
||||||
|
|
||||||
"sharp": ["sharp@0.33.5", "", { "dependencies": { "color": "^4.2.3", "detect-libc": "^2.0.3", "semver": "^7.6.3" }, "optionalDependencies": { "@img/sharp-darwin-arm64": "0.33.5", "@img/sharp-darwin-x64": "0.33.5", "@img/sharp-libvips-darwin-arm64": "1.0.4", "@img/sharp-libvips-darwin-x64": "1.0.4", "@img/sharp-libvips-linux-arm": "1.0.5", "@img/sharp-libvips-linux-arm64": "1.0.4", "@img/sharp-libvips-linux-s390x": "1.0.4", "@img/sharp-libvips-linux-x64": "1.0.4", "@img/sharp-libvips-linuxmusl-arm64": "1.0.4", "@img/sharp-libvips-linuxmusl-x64": "1.0.4", "@img/sharp-linux-arm": "0.33.5", "@img/sharp-linux-arm64": "0.33.5", "@img/sharp-linux-s390x": "0.33.5", "@img/sharp-linux-x64": "0.33.5", "@img/sharp-linuxmusl-arm64": "0.33.5", "@img/sharp-linuxmusl-x64": "0.33.5", "@img/sharp-wasm32": "0.33.5", "@img/sharp-win32-ia32": "0.33.5", "@img/sharp-win32-x64": "0.33.5" } }, "sha512-haPVm1EkS9pgvHrQ/F3Xy+hgcuMV0Wm9vfIBSiwZ05k+xgb0PkBQpGsAA/oWdDobNaZTH5ppvHtzCFbnSEwHVw=="],
|
"sharp": ["sharp@0.33.5", "", { "dependencies": { "color": "^4.2.3", "detect-libc": "^2.0.3", "semver": "^7.6.3" }, "optionalDependencies": { "@img/sharp-darwin-arm64": "0.33.5", "@img/sharp-darwin-x64": "0.33.5", "@img/sharp-libvips-darwin-arm64": "1.0.4", "@img/sharp-libvips-darwin-x64": "1.0.4", "@img/sharp-libvips-linux-arm": "1.0.5", "@img/sharp-libvips-linux-arm64": "1.0.4", "@img/sharp-libvips-linux-s390x": "1.0.4", "@img/sharp-libvips-linux-x64": "1.0.4", "@img/sharp-libvips-linuxmusl-arm64": "1.0.4", "@img/sharp-libvips-linuxmusl-x64": "1.0.4", "@img/sharp-linux-arm": "0.33.5", "@img/sharp-linux-arm64": "0.33.5", "@img/sharp-linux-s390x": "0.33.5", "@img/sharp-linux-x64": "0.33.5", "@img/sharp-linuxmusl-arm64": "0.33.5", "@img/sharp-linuxmusl-x64": "0.33.5", "@img/sharp-wasm32": "0.33.5", "@img/sharp-win32-ia32": "0.33.5", "@img/sharp-win32-x64": "0.33.5" } }, "sha512-haPVm1EkS9pgvHrQ/F3Xy+hgcuMV0Wm9vfIBSiwZ05k+xgb0PkBQpGsAA/oWdDobNaZTH5ppvHtzCFbnSEwHVw=="],
|
||||||
|
|||||||
175
docs/BETTER_AUTH_MIGRATION.md
Normal file
175
docs/BETTER_AUTH_MIGRATION.md
Normal file
@@ -0,0 +1,175 @@
|
|||||||
|
# Better Auth Migration Guide
|
||||||
|
|
||||||
|
This document describes the migration from the legacy authentication system to Better Auth.
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
|
||||||
|
Gitea Mirror has been migrated to use Better Auth, a modern authentication library that provides:
|
||||||
|
- Built-in support for email/password authentication
|
||||||
|
- Session management with secure cookies
|
||||||
|
- Database adapter with Drizzle ORM
|
||||||
|
- Ready for OAuth2, OIDC, and SSO integrations
|
||||||
|
- Type-safe authentication throughout the application
|
||||||
|
|
||||||
|
## Key Changes
|
||||||
|
|
||||||
|
### 1. Database Schema
|
||||||
|
|
||||||
|
New tables added:
|
||||||
|
- `sessions` - User session management
|
||||||
|
- `accounts` - Authentication providers (credentials, OAuth, etc.)
|
||||||
|
- `verification_tokens` - Email verification and password reset tokens
|
||||||
|
|
||||||
|
Modified tables:
|
||||||
|
- `users` - Added `emailVerified` field
|
||||||
|
|
||||||
|
### 2. Authentication Flow
|
||||||
|
|
||||||
|
**Login:**
|
||||||
|
- Users now log in with email instead of username
|
||||||
|
- Endpoint: `/api/auth/sign-in/email`
|
||||||
|
- Session cookies are automatically managed
|
||||||
|
|
||||||
|
**Registration:**
|
||||||
|
- Users register with username, email, and password
|
||||||
|
- Username is stored as an additional field
|
||||||
|
- Endpoint: `/api/auth/sign-up/email`
|
||||||
|
|
||||||
|
### 3. API Routes
|
||||||
|
|
||||||
|
All auth routes are now handled by Better Auth's catch-all handler:
|
||||||
|
- `/api/auth/[...all].ts` handles all authentication endpoints
|
||||||
|
|
||||||
|
Legacy routes have been backed up to `/src/pages/api/auth/legacy-backup/`
|
||||||
|
|
||||||
|
### 4. Session Management
|
||||||
|
|
||||||
|
Sessions are now managed by Better Auth:
|
||||||
|
- Middleware automatically populates `context.locals.user` and `context.locals.session`
|
||||||
|
- Use `useAuth()` hook in React components for client-side auth
|
||||||
|
- Sessions expire after 30 days by default
|
||||||
|
|
||||||
|
## Future OIDC/SSO Configuration
|
||||||
|
|
||||||
|
The project is now ready for OIDC and SSO integrations. To enable:
|
||||||
|
|
||||||
|
### 1. Enable SSO Plugin
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// src/lib/auth.ts
|
||||||
|
import { sso } from "better-auth/plugins/sso";
|
||||||
|
|
||||||
|
export const auth = betterAuth({
|
||||||
|
// ... existing config
|
||||||
|
plugins: [
|
||||||
|
sso({
|
||||||
|
provisionUser: async (data) => {
|
||||||
|
// Custom user provisioning logic
|
||||||
|
return data;
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. Register OIDC Providers
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// Example: Register an OIDC provider
|
||||||
|
await authClient.sso.register({
|
||||||
|
issuer: "https://idp.example.com",
|
||||||
|
domain: "example.com",
|
||||||
|
clientId: "your-client-id",
|
||||||
|
clientSecret: "your-client-secret",
|
||||||
|
providerId: "example-provider",
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. Enable OIDC Provider Mode
|
||||||
|
|
||||||
|
To make Gitea Mirror act as an OIDC provider:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// src/lib/auth.ts
|
||||||
|
import { oidcProvider } from "better-auth/plugins/oidc";
|
||||||
|
|
||||||
|
export const auth = betterAuth({
|
||||||
|
// ... existing config
|
||||||
|
plugins: [
|
||||||
|
oidcProvider({
|
||||||
|
loginPage: "/signin",
|
||||||
|
consentPage: "/oauth/consent",
|
||||||
|
metadata: {
|
||||||
|
issuer: process.env.BETTER_AUTH_URL || "http://localhost:3000",
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4. Database Migration for SSO
|
||||||
|
|
||||||
|
When enabling SSO/OIDC, run migrations to add required tables:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Generate the schema
|
||||||
|
bun drizzle-kit generate
|
||||||
|
|
||||||
|
# Apply the migration
|
||||||
|
bun drizzle-kit migrate
|
||||||
|
```
|
||||||
|
|
||||||
|
New tables that will be added:
|
||||||
|
- `sso_providers` - SSO provider configurations
|
||||||
|
- `oauth_applications` - OAuth2 client applications
|
||||||
|
- `oauth_access_tokens` - OAuth2 access tokens
|
||||||
|
- `oauth_consents` - User consent records
|
||||||
|
|
||||||
|
## Environment Variables
|
||||||
|
|
||||||
|
Required environment variables:
|
||||||
|
|
||||||
|
```env
|
||||||
|
# Better Auth configuration
|
||||||
|
BETTER_AUTH_SECRET=your-secret-key
|
||||||
|
BETTER_AUTH_URL=http://localhost:3000
|
||||||
|
|
||||||
|
# Legacy (kept for compatibility)
|
||||||
|
JWT_SECRET=your-secret-key
|
||||||
|
```
|
||||||
|
|
||||||
|
## Migration Script
|
||||||
|
|
||||||
|
To migrate existing users to Better Auth:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
bun run migrate:better-auth
|
||||||
|
```
|
||||||
|
|
||||||
|
This script:
|
||||||
|
1. Creates credential accounts for existing users
|
||||||
|
2. Moves password hashes to the accounts table
|
||||||
|
3. Preserves user creation dates
|
||||||
|
|
||||||
|
## Troubleshooting
|
||||||
|
|
||||||
|
### Login Issues
|
||||||
|
- Ensure users log in with email, not username
|
||||||
|
- Check that BETTER_AUTH_SECRET is set
|
||||||
|
- Verify database migrations have been applied
|
||||||
|
|
||||||
|
### Session Issues
|
||||||
|
- Clear browser cookies if experiencing session problems
|
||||||
|
- Check middleware is properly configured
|
||||||
|
- Ensure auth routes are accessible at `/api/auth/*`
|
||||||
|
|
||||||
|
### Development Tips
|
||||||
|
- Use `bun db:studio` to inspect database tables
|
||||||
|
- Check `/api/auth/session` to verify current session
|
||||||
|
- Enable debug logging in Better Auth for troubleshooting
|
||||||
|
|
||||||
|
## Resources
|
||||||
|
|
||||||
|
- [Better Auth Documentation](https://better-auth.com)
|
||||||
|
- [Better Auth Astro Integration](https://better-auth.com/docs/integrations/astro)
|
||||||
|
- [Better Auth Plugins](https://better-auth.com/docs/plugins)
|
||||||
45
drizzle/0001_vengeful_whirlwind.sql
Normal file
45
drizzle/0001_vengeful_whirlwind.sql
Normal file
@@ -0,0 +1,45 @@
|
|||||||
|
CREATE TABLE `accounts` (
|
||||||
|
`id` text PRIMARY KEY NOT NULL,
|
||||||
|
`user_id` text NOT NULL,
|
||||||
|
`provider_id` text NOT NULL,
|
||||||
|
`provider_user_id` text NOT NULL,
|
||||||
|
`access_token` text,
|
||||||
|
`refresh_token` text,
|
||||||
|
`expires_at` integer,
|
||||||
|
`password` text,
|
||||||
|
`created_at` integer DEFAULT (unixepoch()) NOT NULL,
|
||||||
|
`updated_at` integer DEFAULT (unixepoch()) NOT NULL,
|
||||||
|
FOREIGN KEY (`user_id`) REFERENCES `users`(`id`) ON UPDATE no action ON DELETE no action
|
||||||
|
);
|
||||||
|
--> statement-breakpoint
|
||||||
|
CREATE INDEX `idx_accounts_user_id` ON `accounts` (`user_id`);--> statement-breakpoint
|
||||||
|
CREATE INDEX `idx_accounts_provider` ON `accounts` (`provider_id`,`provider_user_id`);--> statement-breakpoint
|
||||||
|
CREATE TABLE `sessions` (
|
||||||
|
`id` text PRIMARY KEY NOT NULL,
|
||||||
|
`token` text NOT NULL,
|
||||||
|
`user_id` text NOT NULL,
|
||||||
|
`expires_at` integer NOT NULL,
|
||||||
|
`ip_address` text,
|
||||||
|
`user_agent` text,
|
||||||
|
`created_at` integer DEFAULT (unixepoch()) NOT NULL,
|
||||||
|
`updated_at` integer DEFAULT (unixepoch()) NOT NULL,
|
||||||
|
FOREIGN KEY (`user_id`) REFERENCES `users`(`id`) ON UPDATE no action ON DELETE no action
|
||||||
|
);
|
||||||
|
--> statement-breakpoint
|
||||||
|
CREATE UNIQUE INDEX `sessions_token_unique` ON `sessions` (`token`);--> statement-breakpoint
|
||||||
|
CREATE INDEX `idx_sessions_user_id` ON `sessions` (`user_id`);--> statement-breakpoint
|
||||||
|
CREATE INDEX `idx_sessions_token` ON `sessions` (`token`);--> statement-breakpoint
|
||||||
|
CREATE INDEX `idx_sessions_expires_at` ON `sessions` (`expires_at`);--> statement-breakpoint
|
||||||
|
CREATE TABLE `verification_tokens` (
|
||||||
|
`id` text PRIMARY KEY NOT NULL,
|
||||||
|
`token` text NOT NULL,
|
||||||
|
`identifier` text NOT NULL,
|
||||||
|
`type` text NOT NULL,
|
||||||
|
`expires_at` integer NOT NULL,
|
||||||
|
`created_at` integer DEFAULT (unixepoch()) NOT NULL
|
||||||
|
);
|
||||||
|
--> statement-breakpoint
|
||||||
|
CREATE UNIQUE INDEX `verification_tokens_token_unique` ON `verification_tokens` (`token`);--> statement-breakpoint
|
||||||
|
CREATE INDEX `idx_verification_tokens_token` ON `verification_tokens` (`token`);--> statement-breakpoint
|
||||||
|
CREATE INDEX `idx_verification_tokens_identifier` ON `verification_tokens` (`identifier`);--> statement-breakpoint
|
||||||
|
ALTER TABLE `users` ADD `email_verified` integer DEFAULT false NOT NULL;
|
||||||
1261
drizzle/meta/0001_snapshot.json
Normal file
1261
drizzle/meta/0001_snapshot.json
Normal file
File diff suppressed because it is too large
Load Diff
@@ -8,6 +8,13 @@
|
|||||||
"when": 1752161775910,
|
"when": 1752161775910,
|
||||||
"tag": "0000_big_xorn",
|
"tag": "0000_big_xorn",
|
||||||
"breakpoints": true
|
"breakpoints": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idx": 1,
|
||||||
|
"version": "6",
|
||||||
|
"when": 1752166860985,
|
||||||
|
"tag": "0001_vengeful_whirlwind",
|
||||||
|
"breakpoints": true
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
9
env.d.ts
vendored
Normal file
9
env.d.ts
vendored
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
/// <reference path="./.astro/types.d.ts" />
|
||||||
|
/// <reference types="astro/client" />
|
||||||
|
|
||||||
|
declare namespace App {
|
||||||
|
interface Locals {
|
||||||
|
user: import("better-auth").User | null;
|
||||||
|
session: import("better-auth").Session | null;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -22,6 +22,7 @@
|
|||||||
"db:pull": "bun drizzle-kit pull",
|
"db:pull": "bun drizzle-kit pull",
|
||||||
"db:check": "bun drizzle-kit check",
|
"db:check": "bun drizzle-kit check",
|
||||||
"db:studio": "bun drizzle-kit studio",
|
"db:studio": "bun drizzle-kit studio",
|
||||||
|
"migrate:better-auth": "bun scripts/migrate-to-better-auth.ts",
|
||||||
"startup-recovery": "bun scripts/startup-recovery.ts",
|
"startup-recovery": "bun scripts/startup-recovery.ts",
|
||||||
"startup-recovery-force": "bun scripts/startup-recovery.ts --force",
|
"startup-recovery-force": "bun scripts/startup-recovery.ts --force",
|
||||||
"test-recovery": "bun scripts/test-recovery.ts",
|
"test-recovery": "bun scripts/test-recovery.ts",
|
||||||
@@ -66,6 +67,7 @@
|
|||||||
"@types/react-dom": "^19.1.6",
|
"@types/react-dom": "^19.1.6",
|
||||||
"astro": "5.11.0",
|
"astro": "5.11.0",
|
||||||
"bcryptjs": "^3.0.2",
|
"bcryptjs": "^3.0.2",
|
||||||
|
"better-auth": "^1.2.12",
|
||||||
"canvas-confetti": "^1.9.3",
|
"canvas-confetti": "^1.9.3",
|
||||||
"class-variance-authority": "^0.7.1",
|
"class-variance-authority": "^0.7.1",
|
||||||
"clsx": "^2.1.1",
|
"clsx": "^2.1.1",
|
||||||
|
|||||||
109
scripts/generate-better-auth-schema.ts
Normal file
109
scripts/generate-better-auth-schema.ts
Normal file
@@ -0,0 +1,109 @@
|
|||||||
|
#!/usr/bin/env bun
|
||||||
|
|
||||||
|
import { betterAuth } from "better-auth";
|
||||||
|
import { drizzleAdapter } from "better-auth/adapters/drizzle";
|
||||||
|
import Database from "bun:sqlite";
|
||||||
|
import { drizzle } from "drizzle-orm/bun-sqlite";
|
||||||
|
import { sqliteTable, text, integer } from "drizzle-orm/sqlite-core";
|
||||||
|
|
||||||
|
// Create a minimal auth instance just for schema generation
|
||||||
|
const tempDb = new Database(":memory:");
|
||||||
|
const db = drizzle({ client: tempDb });
|
||||||
|
|
||||||
|
// Minimal auth config for schema generation
|
||||||
|
const auth = betterAuth({
|
||||||
|
database: drizzleAdapter(db, {
|
||||||
|
provider: "sqlite",
|
||||||
|
usePlural: true,
|
||||||
|
}),
|
||||||
|
emailAndPassword: {
|
||||||
|
enabled: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
// Generate the schema
|
||||||
|
const schema = auth.$internal.schema;
|
||||||
|
|
||||||
|
console.log("Better Auth Tables Required:");
|
||||||
|
console.log("============================");
|
||||||
|
|
||||||
|
// Convert Better Auth schema to Drizzle schema definitions
|
||||||
|
const drizzleSchemaCode = `// Better Auth Tables - Generated Schema
|
||||||
|
import { sqliteTable, text, integer, index } from "drizzle-orm/sqlite-core";
|
||||||
|
import { sql } from "drizzle-orm";
|
||||||
|
|
||||||
|
// Sessions table
|
||||||
|
export const sessions = sqliteTable("sessions", {
|
||||||
|
id: text("id").primaryKey(),
|
||||||
|
token: text("token").notNull().unique(),
|
||||||
|
userId: text("user_id").notNull().references(() => users.id),
|
||||||
|
expiresAt: integer("expires_at", { mode: "timestamp" }).notNull(),
|
||||||
|
ipAddress: text("ip_address"),
|
||||||
|
userAgent: text("user_agent"),
|
||||||
|
createdAt: integer("created_at", { mode: "timestamp" })
|
||||||
|
.notNull()
|
||||||
|
.default(sql\`(unixepoch())\`),
|
||||||
|
updatedAt: integer("updated_at", { mode: "timestamp" })
|
||||||
|
.notNull()
|
||||||
|
.default(sql\`(unixepoch())\`),
|
||||||
|
}, (table) => {
|
||||||
|
return {
|
||||||
|
userIdIdx: index("idx_sessions_user_id").on(table.userId),
|
||||||
|
tokenIdx: index("idx_sessions_token").on(table.token),
|
||||||
|
expiresAtIdx: index("idx_sessions_expires_at").on(table.expiresAt),
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
// Accounts table (for OAuth providers and credentials)
|
||||||
|
export const accounts = sqliteTable("accounts", {
|
||||||
|
id: text("id").primaryKey(),
|
||||||
|
userId: text("user_id").notNull().references(() => users.id),
|
||||||
|
providerId: text("provider_id").notNull(),
|
||||||
|
providerUserId: text("provider_user_id").notNull(),
|
||||||
|
accessToken: text("access_token"),
|
||||||
|
refreshToken: text("refresh_token"),
|
||||||
|
expiresAt: integer("expires_at", { mode: "timestamp" }),
|
||||||
|
password: text("password"), // For credential provider
|
||||||
|
createdAt: integer("created_at", { mode: "timestamp" })
|
||||||
|
.notNull()
|
||||||
|
.default(sql\`(unixepoch())\`),
|
||||||
|
updatedAt: integer("updated_at", { mode: "timestamp" })
|
||||||
|
.notNull()
|
||||||
|
.default(sql\`(unixepoch())\`),
|
||||||
|
}, (table) => {
|
||||||
|
return {
|
||||||
|
userIdIdx: index("idx_accounts_user_id").on(table.userId),
|
||||||
|
providerIdx: index("idx_accounts_provider").on(table.providerId, table.providerUserId),
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
// Verification tokens table
|
||||||
|
export const verificationTokens = sqliteTable("verification_tokens", {
|
||||||
|
id: text("id").primaryKey(),
|
||||||
|
token: text("token").notNull().unique(),
|
||||||
|
identifier: text("identifier").notNull(),
|
||||||
|
type: text("type").notNull(), // email, password-reset, etc
|
||||||
|
expiresAt: integer("expires_at", { mode: "timestamp" }).notNull(),
|
||||||
|
createdAt: integer("created_at", { mode: "timestamp" })
|
||||||
|
.notNull()
|
||||||
|
.default(sql\`(unixepoch())\`),
|
||||||
|
}, (table) => {
|
||||||
|
return {
|
||||||
|
tokenIdx: index("idx_verification_tokens_token").on(table.token),
|
||||||
|
identifierIdx: index("idx_verification_tokens_identifier").on(table.identifier),
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
// Future: SSO and OIDC Provider tables will be added when we enable those plugins
|
||||||
|
`;
|
||||||
|
|
||||||
|
console.log(drizzleSchemaCode);
|
||||||
|
|
||||||
|
// Output information about the schema
|
||||||
|
console.log("\n\nSummary:");
|
||||||
|
console.log("=========");
|
||||||
|
console.log("- Better Auth will modify the existing 'users' table");
|
||||||
|
console.log("- New tables required: sessions, accounts, verification_tokens");
|
||||||
|
console.log("\nNote: The 'users' table needs emailVerified field added");
|
||||||
|
|
||||||
|
tempDb.close();
|
||||||
85
scripts/migrate-to-better-auth.ts
Normal file
85
scripts/migrate-to-better-auth.ts
Normal file
@@ -0,0 +1,85 @@
|
|||||||
|
#!/usr/bin/env bun
|
||||||
|
|
||||||
|
import { db, users, accounts } from "../src/lib/db";
|
||||||
|
import { eq } from "drizzle-orm";
|
||||||
|
import { v4 as uuidv4 } from "uuid";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Migrate existing users to Better Auth schema
|
||||||
|
*
|
||||||
|
* This script:
|
||||||
|
* 1. Moves existing password hashes from users table to accounts table
|
||||||
|
* 2. Updates user data to match Better Auth requirements
|
||||||
|
* 3. Creates credential accounts for existing users
|
||||||
|
*/
|
||||||
|
|
||||||
|
async function migrateUsers() {
|
||||||
|
console.log("🔄 Starting user migration to Better Auth...");
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Get all existing users
|
||||||
|
const existingUsers = await db.select().from(users);
|
||||||
|
|
||||||
|
if (existingUsers.length === 0) {
|
||||||
|
console.log("✅ No users to migrate");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(`Found ${existingUsers.length} users to migrate`);
|
||||||
|
|
||||||
|
for (const user of existingUsers) {
|
||||||
|
console.log(`\nMigrating user: ${user.username} (${user.email})`);
|
||||||
|
|
||||||
|
// Check if user already has a credential account
|
||||||
|
const existingAccount = await db
|
||||||
|
.select()
|
||||||
|
.from(accounts)
|
||||||
|
.where(
|
||||||
|
eq(accounts.userId, user.id) &&
|
||||||
|
eq(accounts.providerId, "credential")
|
||||||
|
)
|
||||||
|
.limit(1);
|
||||||
|
|
||||||
|
if (existingAccount.length > 0) {
|
||||||
|
console.log("✓ User already migrated");
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create credential account with existing password hash
|
||||||
|
await db.insert(accounts).values({
|
||||||
|
id: uuidv4(),
|
||||||
|
userId: user.id,
|
||||||
|
providerId: "credential",
|
||||||
|
providerUserId: user.email, // Use email as provider user ID
|
||||||
|
password: user.password, // Move existing password hash
|
||||||
|
createdAt: user.createdAt,
|
||||||
|
updatedAt: user.updatedAt,
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log("✓ Created credential account");
|
||||||
|
|
||||||
|
// Update user name field if it's null (Better Auth uses 'name' field)
|
||||||
|
// Note: Better Auth expects a 'name' field, but we're using username
|
||||||
|
// This is handled by our additional fields configuration
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log("\n✅ User migration completed successfully!");
|
||||||
|
|
||||||
|
// Summary
|
||||||
|
const migratedAccounts = await db
|
||||||
|
.select()
|
||||||
|
.from(accounts)
|
||||||
|
.where(eq(accounts.providerId, "credential"));
|
||||||
|
|
||||||
|
console.log(`\nMigration Summary:`);
|
||||||
|
console.log(`- Total users: ${existingUsers.length}`);
|
||||||
|
console.log(`- Migrated accounts: ${migratedAccounts.length}`);
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error("❌ Migration failed:", error);
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Run migration
|
||||||
|
migrateUsers();
|
||||||
@@ -4,6 +4,7 @@ import * as React from 'react';
|
|||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
import { Button } from '@/components/ui/button';
|
import { Button } from '@/components/ui/button';
|
||||||
import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from '@/components/ui/card';
|
import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from '@/components/ui/card';
|
||||||
|
import { useAuth } from '@/hooks/useAuth';
|
||||||
|
|
||||||
import { toast, Toaster } from 'sonner';
|
import { toast, Toaster } from 'sonner';
|
||||||
import { showErrorToast } from '@/lib/utils';
|
import { showErrorToast } from '@/lib/utils';
|
||||||
@@ -11,43 +12,29 @@ import { showErrorToast } from '@/lib/utils';
|
|||||||
|
|
||||||
export function LoginForm() {
|
export function LoginForm() {
|
||||||
const [isLoading, setIsLoading] = useState(false);
|
const [isLoading, setIsLoading] = useState(false);
|
||||||
|
const { login } = useAuth();
|
||||||
|
|
||||||
async function handleLogin(e: React.FormEvent<HTMLFormElement>) {
|
async function handleLogin(e: React.FormEvent<HTMLFormElement>) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
setIsLoading(true);
|
setIsLoading(true);
|
||||||
const form = e.currentTarget;
|
const form = e.currentTarget;
|
||||||
const formData = new FormData(form);
|
const formData = new FormData(form);
|
||||||
const username = formData.get('username') as string | null;
|
const email = formData.get('email') as string | null;
|
||||||
const password = formData.get('password') as string | null;
|
const password = formData.get('password') as string | null;
|
||||||
|
|
||||||
if (!username || !password) {
|
if (!email || !password) {
|
||||||
toast.error('Please enter both username and password');
|
toast.error('Please enter both email and password');
|
||||||
setIsLoading(false);
|
setIsLoading(false);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const loginData = { username, password };
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const response = await fetch('/api/auth/login', {
|
await login(email, password);
|
||||||
method: 'POST',
|
toast.success('Login successful!');
|
||||||
headers: {
|
// Small delay before redirecting to see the success message
|
||||||
'Content-Type': 'application/json',
|
setTimeout(() => {
|
||||||
},
|
window.location.href = '/dashboard';
|
||||||
body: JSON.stringify(loginData),
|
}, 1000);
|
||||||
});
|
|
||||||
|
|
||||||
const data = await response.json();
|
|
||||||
|
|
||||||
if (response.ok) {
|
|
||||||
toast.success('Login successful!');
|
|
||||||
// Small delay before redirecting to see the success message
|
|
||||||
setTimeout(() => {
|
|
||||||
window.location.href = '/';
|
|
||||||
}, 1000);
|
|
||||||
} else {
|
|
||||||
showErrorToast(data.error || 'Login failed. Please try again.', toast);
|
|
||||||
}
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
showErrorToast(error, toast);
|
showErrorToast(error, toast);
|
||||||
} finally {
|
} finally {
|
||||||
@@ -80,16 +67,16 @@ export function LoginForm() {
|
|||||||
<form id="login-form" onSubmit={handleLogin}>
|
<form id="login-form" onSubmit={handleLogin}>
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
<div>
|
<div>
|
||||||
<label htmlFor="username" className="block text-sm font-medium mb-1">
|
<label htmlFor="email" className="block text-sm font-medium mb-1">
|
||||||
Username
|
Email
|
||||||
</label>
|
</label>
|
||||||
<input
|
<input
|
||||||
id="username"
|
id="email"
|
||||||
name="username"
|
name="email"
|
||||||
type="text"
|
type="email"
|
||||||
required
|
required
|
||||||
className="w-full rounded-md border border-input bg-background px-3 py-2 text-sm shadow-sm transition-colors placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring"
|
className="w-full rounded-md border border-input bg-background px-3 py-2 text-sm shadow-sm transition-colors placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring"
|
||||||
placeholder="Enter your username"
|
placeholder="Enter your email"
|
||||||
disabled={isLoading}
|
disabled={isLoading}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -5,9 +5,11 @@ import { Button } from '@/components/ui/button';
|
|||||||
import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from '@/components/ui/card';
|
import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from '@/components/ui/card';
|
||||||
import { toast, Toaster } from 'sonner';
|
import { toast, Toaster } from 'sonner';
|
||||||
import { showErrorToast } from '@/lib/utils';
|
import { showErrorToast } from '@/lib/utils';
|
||||||
|
import { useAuth } from '@/hooks/useAuth';
|
||||||
|
|
||||||
export function SignupForm() {
|
export function SignupForm() {
|
||||||
const [isLoading, setIsLoading] = useState(false);
|
const [isLoading, setIsLoading] = useState(false);
|
||||||
|
const { register } = useAuth();
|
||||||
|
|
||||||
async function handleSignup(e: React.FormEvent<HTMLFormElement>) {
|
async function handleSignup(e: React.FormEvent<HTMLFormElement>) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
@@ -31,28 +33,13 @@ export function SignupForm() {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const signupData = { username, email, password };
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const response = await fetch('/api/auth/register', {
|
await register(username, email, password);
|
||||||
method: 'POST',
|
toast.success('Account created successfully! Redirecting to dashboard...');
|
||||||
headers: {
|
// Small delay before redirecting to see the success message
|
||||||
'Content-Type': 'application/json',
|
setTimeout(() => {
|
||||||
},
|
window.location.href = '/dashboard';
|
||||||
body: JSON.stringify(signupData),
|
}, 1500);
|
||||||
});
|
|
||||||
|
|
||||||
const data = await response.json();
|
|
||||||
|
|
||||||
if (response.ok) {
|
|
||||||
toast.success('Account created successfully! Redirecting to dashboard...');
|
|
||||||
// Small delay before redirecting to see the success message
|
|
||||||
setTimeout(() => {
|
|
||||||
window.location.href = '/';
|
|
||||||
}, 1500);
|
|
||||||
} else {
|
|
||||||
showErrorToast(data.error || 'Failed to create account. Please try again.', toast);
|
|
||||||
}
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
showErrorToast(error, toast);
|
showErrorToast(error, toast);
|
||||||
} finally {
|
} finally {
|
||||||
|
|||||||
147
src/hooks/useAuth-legacy.ts
Normal file
147
src/hooks/useAuth-legacy.ts
Normal file
@@ -0,0 +1,147 @@
|
|||||||
|
import * as React from "react";
|
||||||
|
import {
|
||||||
|
useState,
|
||||||
|
useEffect,
|
||||||
|
createContext,
|
||||||
|
useContext,
|
||||||
|
type Context,
|
||||||
|
} from "react";
|
||||||
|
import { authApi } from "@/lib/api";
|
||||||
|
import type { ExtendedUser } from "@/types/user";
|
||||||
|
|
||||||
|
interface AuthContextType {
|
||||||
|
user: ExtendedUser | null;
|
||||||
|
isLoading: boolean;
|
||||||
|
error: string | null;
|
||||||
|
login: (username: string, password: string) => Promise<void>;
|
||||||
|
register: (
|
||||||
|
username: string,
|
||||||
|
email: string,
|
||||||
|
password: string
|
||||||
|
) => Promise<void>;
|
||||||
|
logout: () => Promise<void>;
|
||||||
|
refreshUser: () => Promise<void>; // Added refreshUser function
|
||||||
|
}
|
||||||
|
|
||||||
|
const AuthContext: Context<AuthContextType | undefined> = createContext<
|
||||||
|
AuthContextType | undefined
|
||||||
|
>(undefined);
|
||||||
|
|
||||||
|
export function AuthProvider({ children }: { children: React.ReactNode }) {
|
||||||
|
const [user, setUser] = useState<ExtendedUser | null>(null);
|
||||||
|
const [isLoading, setIsLoading] = useState(true);
|
||||||
|
const [error, setError] = useState<string | null>(null);
|
||||||
|
|
||||||
|
// Function to refetch the user data
|
||||||
|
const refreshUser = async () => {
|
||||||
|
// not using loading state to keep the ui seamless and refresh the data in bg
|
||||||
|
// setIsLoading(true);
|
||||||
|
try {
|
||||||
|
const user = await authApi.getCurrentUser();
|
||||||
|
setUser(user);
|
||||||
|
} catch (err: any) {
|
||||||
|
setUser(null);
|
||||||
|
console.error("Failed to refresh user data", err);
|
||||||
|
} finally {
|
||||||
|
// setIsLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Automatically check the user status when the app loads
|
||||||
|
useEffect(() => {
|
||||||
|
const checkAuth = async () => {
|
||||||
|
try {
|
||||||
|
const user = await authApi.getCurrentUser();
|
||||||
|
|
||||||
|
console.log("User data fetched:", user);
|
||||||
|
|
||||||
|
setUser(user);
|
||||||
|
} catch (err: any) {
|
||||||
|
setUser(null);
|
||||||
|
|
||||||
|
// Redirect user based on error
|
||||||
|
if (err?.message === "No users found") {
|
||||||
|
window.location.href = "/signup";
|
||||||
|
} else {
|
||||||
|
window.location.href = "/login";
|
||||||
|
}
|
||||||
|
console.error("Auth check failed", err);
|
||||||
|
} finally {
|
||||||
|
setIsLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
checkAuth();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const login = async (username: string, password: string) => {
|
||||||
|
setIsLoading(true);
|
||||||
|
setError(null);
|
||||||
|
try {
|
||||||
|
const user = await authApi.login(username, password);
|
||||||
|
setUser(user);
|
||||||
|
} catch (err) {
|
||||||
|
setError(err instanceof Error ? err.message : "Login failed");
|
||||||
|
throw err;
|
||||||
|
} finally {
|
||||||
|
setIsLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const register = async (
|
||||||
|
username: string,
|
||||||
|
email: string,
|
||||||
|
password: string
|
||||||
|
) => {
|
||||||
|
setIsLoading(true);
|
||||||
|
setError(null);
|
||||||
|
try {
|
||||||
|
const user = await authApi.register(username, email, password);
|
||||||
|
setUser(user);
|
||||||
|
} catch (err) {
|
||||||
|
setError(err instanceof Error ? err.message : "Registration failed");
|
||||||
|
throw err;
|
||||||
|
} finally {
|
||||||
|
setIsLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const logout = async () => {
|
||||||
|
setIsLoading(true);
|
||||||
|
try {
|
||||||
|
await authApi.logout();
|
||||||
|
setUser(null);
|
||||||
|
window.location.href = "/login";
|
||||||
|
} catch (err) {
|
||||||
|
console.error("Logout error:", err);
|
||||||
|
} finally {
|
||||||
|
setIsLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Create the context value with the added refreshUser function
|
||||||
|
const contextValue = {
|
||||||
|
user,
|
||||||
|
isLoading,
|
||||||
|
error,
|
||||||
|
login,
|
||||||
|
register,
|
||||||
|
logout,
|
||||||
|
refreshUser,
|
||||||
|
};
|
||||||
|
|
||||||
|
// Return the provider with the context value
|
||||||
|
return React.createElement(
|
||||||
|
AuthContext.Provider,
|
||||||
|
{ value: contextValue },
|
||||||
|
children
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useAuth() {
|
||||||
|
const context = useContext(AuthContext);
|
||||||
|
if (context === undefined) {
|
||||||
|
throw new Error("useAuth must be used within an AuthProvider");
|
||||||
|
}
|
||||||
|
return context;
|
||||||
|
}
|
||||||
@@ -6,21 +6,22 @@ import {
|
|||||||
useContext,
|
useContext,
|
||||||
type Context,
|
type Context,
|
||||||
} from "react";
|
} from "react";
|
||||||
import { authApi } from "@/lib/api";
|
import { authClient, useSession as useBetterAuthSession } from "@/lib/auth-client";
|
||||||
import type { ExtendedUser } from "@/types/user";
|
import type { Session, AuthUser } from "@/lib/auth-client";
|
||||||
|
|
||||||
interface AuthContextType {
|
interface AuthContextType {
|
||||||
user: ExtendedUser | null;
|
user: AuthUser | null;
|
||||||
|
session: Session | null;
|
||||||
isLoading: boolean;
|
isLoading: boolean;
|
||||||
error: string | null;
|
error: string | null;
|
||||||
login: (username: string, password: string) => Promise<void>;
|
login: (email: string, password: string, username?: string) => Promise<void>;
|
||||||
register: (
|
register: (
|
||||||
username: string,
|
username: string,
|
||||||
email: string,
|
email: string,
|
||||||
password: string
|
password: string
|
||||||
) => Promise<void>;
|
) => Promise<void>;
|
||||||
logout: () => Promise<void>;
|
logout: () => Promise<void>;
|
||||||
refreshUser: () => Promise<void>; // Added refreshUser function
|
refreshUser: () => Promise<void>;
|
||||||
}
|
}
|
||||||
|
|
||||||
const AuthContext: Context<AuthContextType | undefined> = createContext<
|
const AuthContext: Context<AuthContextType | undefined> = createContext<
|
||||||
@@ -28,60 +29,53 @@ const AuthContext: Context<AuthContextType | undefined> = createContext<
|
|||||||
>(undefined);
|
>(undefined);
|
||||||
|
|
||||||
export function AuthProvider({ children }: { children: React.ReactNode }) {
|
export function AuthProvider({ children }: { children: React.ReactNode }) {
|
||||||
const [user, setUser] = useState<ExtendedUser | null>(null);
|
const betterAuthSession = useBetterAuthSession();
|
||||||
const [isLoading, setIsLoading] = useState(true);
|
|
||||||
const [error, setError] = useState<string | null>(null);
|
const [error, setError] = useState<string | null>(null);
|
||||||
|
const [isLoading, setIsLoading] = useState(false);
|
||||||
|
|
||||||
// Function to refetch the user data
|
// Derive user and session from Better Auth hook
|
||||||
const refreshUser = async () => {
|
const user = betterAuthSession.data?.user || null;
|
||||||
// not using loading state to keep the ui seamless and refresh the data in bg
|
const session = betterAuthSession.data || null;
|
||||||
// setIsLoading(true);
|
|
||||||
try {
|
|
||||||
const user = await authApi.getCurrentUser();
|
|
||||||
setUser(user);
|
|
||||||
} catch (err: any) {
|
|
||||||
setUser(null);
|
|
||||||
console.error("Failed to refresh user data", err);
|
|
||||||
} finally {
|
|
||||||
// setIsLoading(false);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// Automatically check the user status when the app loads
|
// Check if this is first load and redirect if needed
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const checkAuth = async () => {
|
const checkFirstUser = async () => {
|
||||||
try {
|
if (!betterAuthSession.isPending && !user) {
|
||||||
const user = await authApi.getCurrentUser();
|
try {
|
||||||
|
// Check if there are any users in the system
|
||||||
console.log("User data fetched:", user);
|
const response = await fetch("/api/auth/check-users");
|
||||||
|
if (response.status === 404) {
|
||||||
setUser(user);
|
// No users found, redirect to signup
|
||||||
} catch (err: any) {
|
window.location.href = "/signup";
|
||||||
setUser(null);
|
} else if (!window.location.pathname.includes("/login")) {
|
||||||
|
// User not authenticated, redirect to login
|
||||||
// Redirect user based on error
|
window.location.href = "/login";
|
||||||
if (err?.message === "No users found") {
|
}
|
||||||
window.location.href = "/signup";
|
} catch (err) {
|
||||||
} else {
|
console.error("Failed to check users:", err);
|
||||||
window.location.href = "/login";
|
|
||||||
}
|
}
|
||||||
console.error("Auth check failed", err);
|
|
||||||
} finally {
|
|
||||||
setIsLoading(false);
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
checkAuth();
|
checkFirstUser();
|
||||||
}, []);
|
}, [betterAuthSession.isPending, user]);
|
||||||
|
|
||||||
const login = async (username: string, password: string) => {
|
const login = async (email: string, password: string) => {
|
||||||
setIsLoading(true);
|
setIsLoading(true);
|
||||||
setError(null);
|
setError(null);
|
||||||
try {
|
try {
|
||||||
const user = await authApi.login(username, password);
|
const result = await authClient.signIn.email({
|
||||||
setUser(user);
|
email,
|
||||||
|
password,
|
||||||
|
callbackURL: "/dashboard",
|
||||||
|
});
|
||||||
|
|
||||||
|
if (result.error) {
|
||||||
|
throw new Error(result.error.message || "Login failed");
|
||||||
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
setError(err instanceof Error ? err.message : "Login failed");
|
const message = err instanceof Error ? err.message : "Login failed";
|
||||||
|
setError(message);
|
||||||
throw err;
|
throw err;
|
||||||
} finally {
|
} finally {
|
||||||
setIsLoading(false);
|
setIsLoading(false);
|
||||||
@@ -96,10 +90,20 @@ export function AuthProvider({ children }: { children: React.ReactNode }) {
|
|||||||
setIsLoading(true);
|
setIsLoading(true);
|
||||||
setError(null);
|
setError(null);
|
||||||
try {
|
try {
|
||||||
const user = await authApi.register(username, email, password);
|
const result = await authClient.signUp.email({
|
||||||
setUser(user);
|
email,
|
||||||
|
password,
|
||||||
|
name: username, // Better Auth uses 'name' field
|
||||||
|
username, // Also pass username as additional field
|
||||||
|
callbackURL: "/dashboard",
|
||||||
|
});
|
||||||
|
|
||||||
|
if (result.error) {
|
||||||
|
throw new Error(result.error.message || "Registration failed");
|
||||||
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
setError(err instanceof Error ? err.message : "Registration failed");
|
const message = err instanceof Error ? err.message : "Registration failed";
|
||||||
|
setError(message);
|
||||||
throw err;
|
throw err;
|
||||||
} finally {
|
} finally {
|
||||||
setIsLoading(false);
|
setIsLoading(false);
|
||||||
@@ -109,9 +113,13 @@ export function AuthProvider({ children }: { children: React.ReactNode }) {
|
|||||||
const logout = async () => {
|
const logout = async () => {
|
||||||
setIsLoading(true);
|
setIsLoading(true);
|
||||||
try {
|
try {
|
||||||
await authApi.logout();
|
await authClient.signOut({
|
||||||
setUser(null);
|
fetchOptions: {
|
||||||
window.location.href = "/login";
|
onSuccess: () => {
|
||||||
|
window.location.href = "/login";
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error("Logout error:", err);
|
console.error("Logout error:", err);
|
||||||
} finally {
|
} finally {
|
||||||
@@ -119,10 +127,17 @@ export function AuthProvider({ children }: { children: React.ReactNode }) {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// Create the context value with the added refreshUser function
|
const refreshUser = async () => {
|
||||||
|
// Better Auth automatically handles session refresh
|
||||||
|
// We can force a refetch if needed
|
||||||
|
await betterAuthSession.refetch();
|
||||||
|
};
|
||||||
|
|
||||||
|
// Create the context value
|
||||||
const contextValue = {
|
const contextValue = {
|
||||||
user,
|
user: user as AuthUser | null,
|
||||||
isLoading,
|
session,
|
||||||
|
isLoading: isLoading || betterAuthSession.isPending,
|
||||||
error,
|
error,
|
||||||
login,
|
login,
|
||||||
register,
|
register,
|
||||||
@@ -145,3 +160,6 @@ export function useAuth() {
|
|||||||
}
|
}
|
||||||
return context;
|
return context;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Export the Better Auth session hook for direct use when needed
|
||||||
|
export { useBetterAuthSession };
|
||||||
22
src/lib/auth-client.ts
Normal file
22
src/lib/auth-client.ts
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
import { createAuthClient } from "better-auth/react";
|
||||||
|
|
||||||
|
export const authClient = createAuthClient({
|
||||||
|
// The base URL is optional when running on the same domain
|
||||||
|
// Better Auth will use the current domain by default
|
||||||
|
});
|
||||||
|
|
||||||
|
// Export commonly used methods for convenience
|
||||||
|
export const {
|
||||||
|
signIn,
|
||||||
|
signUp,
|
||||||
|
signOut,
|
||||||
|
useSession,
|
||||||
|
sendVerificationEmail,
|
||||||
|
resetPassword,
|
||||||
|
requestPasswordReset,
|
||||||
|
getSession
|
||||||
|
} = authClient;
|
||||||
|
|
||||||
|
// Export types
|
||||||
|
export type Session = Awaited<ReturnType<typeof authClient.getSession>>["data"];
|
||||||
|
export type AuthUser = Session extends { user: infer U } ? U : never;
|
||||||
76
src/lib/auth-config.ts
Normal file
76
src/lib/auth-config.ts
Normal file
@@ -0,0 +1,76 @@
|
|||||||
|
import { betterAuth } from "better-auth";
|
||||||
|
import { drizzleAdapter } from "better-auth/adapters/drizzle";
|
||||||
|
import { sso, oidcProvider } from "better-auth/plugins";
|
||||||
|
import type { BunSQLiteDatabase } from "drizzle-orm/bun-sqlite";
|
||||||
|
|
||||||
|
// Generate or use existing JWT secret
|
||||||
|
const JWT_SECRET = process.env.JWT_SECRET || process.env.BETTER_AUTH_SECRET;
|
||||||
|
|
||||||
|
if (!JWT_SECRET) {
|
||||||
|
throw new Error("JWT_SECRET or BETTER_AUTH_SECRET environment variable is required");
|
||||||
|
}
|
||||||
|
|
||||||
|
// This function will be called with the actual database instance
|
||||||
|
export function createAuth(db: BunSQLiteDatabase) {
|
||||||
|
return betterAuth({
|
||||||
|
// Database configuration
|
||||||
|
database: drizzleAdapter(db, {
|
||||||
|
provider: "sqlite",
|
||||||
|
usePlural: true, // Our tables use plural names (users, not user)
|
||||||
|
}),
|
||||||
|
|
||||||
|
// Base URL configuration
|
||||||
|
baseURL: process.env.BETTER_AUTH_URL || "http://localhost:3000",
|
||||||
|
|
||||||
|
// Authentication methods
|
||||||
|
emailAndPassword: {
|
||||||
|
enabled: true,
|
||||||
|
requireEmailVerification: false, // We'll enable this later
|
||||||
|
sendResetPassword: async ({ user, url, token }, request) => {
|
||||||
|
// TODO: Implement email sending for password reset
|
||||||
|
console.log("Password reset requested for:", user.email);
|
||||||
|
console.log("Reset URL:", url);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
// Session configuration
|
||||||
|
session: {
|
||||||
|
cookieName: "better-auth-session",
|
||||||
|
updateSessionCookieAge: true,
|
||||||
|
expiresIn: 60 * 60 * 24 * 30, // 30 days
|
||||||
|
},
|
||||||
|
|
||||||
|
// User configuration
|
||||||
|
user: {
|
||||||
|
additionalFields: {
|
||||||
|
// We can add custom fields here if needed
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
// Plugins for future OIDC/SSO support
|
||||||
|
plugins: [
|
||||||
|
// SSO plugin for OIDC client support
|
||||||
|
sso({
|
||||||
|
provisionUser: async (data) => {
|
||||||
|
// Custom user provisioning logic for SSO users
|
||||||
|
console.log("Provisioning SSO user:", data);
|
||||||
|
return data;
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
|
||||||
|
// OIDC Provider plugin (for future use when we want to be an OIDC provider)
|
||||||
|
oidcProvider({
|
||||||
|
loginPage: "/signin",
|
||||||
|
consentPage: "/oauth/consent",
|
||||||
|
metadata: {
|
||||||
|
issuer: process.env.BETTER_AUTH_URL || "http://localhost:3000",
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
|
||||||
|
// Trusted origins for CORS
|
||||||
|
trustedOrigins: [
|
||||||
|
process.env.BETTER_AUTH_URL || "http://localhost:3000",
|
||||||
|
],
|
||||||
|
});
|
||||||
|
}
|
||||||
179
src/lib/auth-oidc-config.example.ts
Normal file
179
src/lib/auth-oidc-config.example.ts
Normal file
@@ -0,0 +1,179 @@
|
|||||||
|
/**
|
||||||
|
* Example OIDC/SSO Configuration for Better Auth
|
||||||
|
*
|
||||||
|
* This file demonstrates how to enable OIDC and SSO features in Gitea Mirror.
|
||||||
|
* To use: Copy this file to auth-oidc-config.ts and update the auth.ts import.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { betterAuth } from "better-auth";
|
||||||
|
import { drizzleAdapter } from "better-auth/adapters/drizzle";
|
||||||
|
import { sso } from "better-auth/plugins/sso";
|
||||||
|
import { oidcProvider } from "better-auth/plugins/oidc";
|
||||||
|
import type { BunSQLiteDatabase } from "drizzle-orm/bun-sqlite";
|
||||||
|
|
||||||
|
export function createAuthWithOIDC(db: BunSQLiteDatabase) {
|
||||||
|
return betterAuth({
|
||||||
|
// Database configuration
|
||||||
|
database: drizzleAdapter(db, {
|
||||||
|
provider: "sqlite",
|
||||||
|
usePlural: true,
|
||||||
|
}),
|
||||||
|
|
||||||
|
// Base configuration
|
||||||
|
baseURL: process.env.BETTER_AUTH_URL || "http://localhost:3000",
|
||||||
|
basePath: "/api/auth",
|
||||||
|
|
||||||
|
// Email/Password authentication
|
||||||
|
emailAndPassword: {
|
||||||
|
enabled: true,
|
||||||
|
requireEmailVerification: false,
|
||||||
|
},
|
||||||
|
|
||||||
|
// Session configuration
|
||||||
|
session: {
|
||||||
|
cookieName: "better-auth-session",
|
||||||
|
updateSessionCookieAge: true,
|
||||||
|
expiresIn: 60 * 60 * 24 * 30, // 30 days
|
||||||
|
},
|
||||||
|
|
||||||
|
// User configuration with additional fields
|
||||||
|
user: {
|
||||||
|
additionalFields: {
|
||||||
|
username: {
|
||||||
|
type: "string",
|
||||||
|
required: true,
|
||||||
|
defaultValue: "user",
|
||||||
|
input: true,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
// OAuth2 providers (examples)
|
||||||
|
socialProviders: {
|
||||||
|
github: {
|
||||||
|
enabled: !!process.env.GITHUB_OAUTH_CLIENT_ID,
|
||||||
|
clientId: process.env.GITHUB_OAUTH_CLIENT_ID!,
|
||||||
|
clientSecret: process.env.GITHUB_OAUTH_CLIENT_SECRET!,
|
||||||
|
},
|
||||||
|
google: {
|
||||||
|
enabled: !!process.env.GOOGLE_OAUTH_CLIENT_ID,
|
||||||
|
clientId: process.env.GOOGLE_OAUTH_CLIENT_ID!,
|
||||||
|
clientSecret: process.env.GOOGLE_OAUTH_CLIENT_SECRET!,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
// Plugins
|
||||||
|
plugins: [
|
||||||
|
// SSO Plugin - For OIDC/SAML client functionality
|
||||||
|
sso({
|
||||||
|
// Auto-provision users from SSO providers
|
||||||
|
provisionUser: async (data) => {
|
||||||
|
console.log("Provisioning SSO user:", data.email);
|
||||||
|
|
||||||
|
// Custom logic to set username from email
|
||||||
|
const username = data.email.split('@')[0];
|
||||||
|
|
||||||
|
return {
|
||||||
|
...data,
|
||||||
|
username,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
|
// Organization provisioning for enterprise SSO
|
||||||
|
organizationProvisioning: {
|
||||||
|
disabled: false,
|
||||||
|
defaultRole: "member",
|
||||||
|
getRole: async (user) => {
|
||||||
|
// Custom logic to determine user role
|
||||||
|
// For admin emails, grant admin role
|
||||||
|
if (user.email?.endsWith('@admin.example.com')) {
|
||||||
|
return 'admin';
|
||||||
|
}
|
||||||
|
return 'member';
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
|
||||||
|
// OIDC Provider Plugin - Makes Gitea Mirror an OIDC provider
|
||||||
|
oidcProvider({
|
||||||
|
// Login page for OIDC authentication flow
|
||||||
|
loginPage: "/login",
|
||||||
|
|
||||||
|
// Consent page for OAuth2 authorization
|
||||||
|
consentPage: "/oauth/consent",
|
||||||
|
|
||||||
|
// Allow dynamic client registration
|
||||||
|
allowDynamicClientRegistration: false,
|
||||||
|
|
||||||
|
// OIDC metadata configuration
|
||||||
|
metadata: {
|
||||||
|
issuer: process.env.BETTER_AUTH_URL || "http://localhost:3000",
|
||||||
|
authorization_endpoint: "/api/auth/oauth2/authorize",
|
||||||
|
token_endpoint: "/api/auth/oauth2/token",
|
||||||
|
userinfo_endpoint: "/api/auth/oauth2/userinfo",
|
||||||
|
jwks_uri: "/api/auth/jwks",
|
||||||
|
},
|
||||||
|
|
||||||
|
// Additional user info claims
|
||||||
|
getAdditionalUserInfoClaim: (user, scopes) => {
|
||||||
|
const claims: Record<string, any> = {};
|
||||||
|
|
||||||
|
// Add custom claims based on scopes
|
||||||
|
if (scopes.includes('profile')) {
|
||||||
|
claims.username = user.username;
|
||||||
|
claims.preferred_username = user.username;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (scopes.includes('gitea')) {
|
||||||
|
// Add Gitea-specific claims
|
||||||
|
claims.gitea_admin = false; // Customize based on your logic
|
||||||
|
claims.gitea_repos = []; // Could fetch user's repositories
|
||||||
|
}
|
||||||
|
|
||||||
|
return claims;
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
|
||||||
|
// Trusted origins for CORS
|
||||||
|
trustedOrigins: [
|
||||||
|
process.env.BETTER_AUTH_URL || "http://localhost:3000",
|
||||||
|
// Add your OIDC client domains here
|
||||||
|
],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Environment variables needed:
|
||||||
|
/*
|
||||||
|
# OAuth2 Providers (optional)
|
||||||
|
GITHUB_OAUTH_CLIENT_ID=your-github-client-id
|
||||||
|
GITHUB_OAUTH_CLIENT_SECRET=your-github-client-secret
|
||||||
|
GOOGLE_OAUTH_CLIENT_ID=your-google-client-id
|
||||||
|
GOOGLE_OAUTH_CLIENT_SECRET=your-google-client-secret
|
||||||
|
|
||||||
|
# SSO Configuration (when registering providers)
|
||||||
|
SSO_PROVIDER_ISSUER=https://idp.example.com
|
||||||
|
SSO_PROVIDER_CLIENT_ID=your-client-id
|
||||||
|
SSO_PROVIDER_CLIENT_SECRET=your-client-secret
|
||||||
|
*/
|
||||||
|
|
||||||
|
// Example: Registering an SSO provider programmatically
|
||||||
|
/*
|
||||||
|
import { authClient } from "./auth-client";
|
||||||
|
|
||||||
|
// Register corporate SSO
|
||||||
|
await authClient.sso.register({
|
||||||
|
issuer: "https://login.microsoftonline.com/tenant-id/v2.0",
|
||||||
|
domain: "company.com",
|
||||||
|
clientId: process.env.AZURE_CLIENT_ID!,
|
||||||
|
clientSecret: process.env.AZURE_CLIENT_SECRET!,
|
||||||
|
providerId: "azure-ad",
|
||||||
|
mapping: {
|
||||||
|
id: "sub",
|
||||||
|
email: "email",
|
||||||
|
emailVerified: "email_verified",
|
||||||
|
name: "name",
|
||||||
|
image: "picture",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
*/
|
||||||
64
src/lib/auth.ts
Normal file
64
src/lib/auth.ts
Normal file
@@ -0,0 +1,64 @@
|
|||||||
|
import { betterAuth } from "better-auth";
|
||||||
|
import { drizzleAdapter } from "better-auth/adapters/drizzle";
|
||||||
|
import { db } from "./db";
|
||||||
|
|
||||||
|
// Generate or use existing JWT secret
|
||||||
|
const JWT_SECRET = process.env.JWT_SECRET || process.env.BETTER_AUTH_SECRET;
|
||||||
|
|
||||||
|
if (!JWT_SECRET) {
|
||||||
|
throw new Error("JWT_SECRET or BETTER_AUTH_SECRET environment variable is required");
|
||||||
|
}
|
||||||
|
|
||||||
|
export const auth = betterAuth({
|
||||||
|
// Database configuration
|
||||||
|
database: drizzleAdapter(db, {
|
||||||
|
provider: "sqlite",
|
||||||
|
usePlural: true, // Our tables use plural names (users, not user)
|
||||||
|
}),
|
||||||
|
|
||||||
|
// Base URL configuration
|
||||||
|
baseURL: process.env.BETTER_AUTH_URL || "http://localhost:3000",
|
||||||
|
basePath: "/api/auth", // Specify the base path for auth endpoints
|
||||||
|
|
||||||
|
// Authentication methods
|
||||||
|
emailAndPassword: {
|
||||||
|
enabled: true,
|
||||||
|
requireEmailVerification: false, // We'll enable this later
|
||||||
|
sendResetPassword: async ({ user, url, token }, request) => {
|
||||||
|
// TODO: Implement email sending for password reset
|
||||||
|
console.log("Password reset requested for:", user.email);
|
||||||
|
console.log("Reset URL:", url);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
// Session configuration
|
||||||
|
session: {
|
||||||
|
cookieName: "better-auth-session",
|
||||||
|
updateSessionCookieAge: true,
|
||||||
|
expiresIn: 60 * 60 * 24 * 30, // 30 days
|
||||||
|
},
|
||||||
|
|
||||||
|
// User configuration
|
||||||
|
user: {
|
||||||
|
additionalFields: {
|
||||||
|
// Keep the username field from our existing schema
|
||||||
|
username: {
|
||||||
|
type: "string",
|
||||||
|
required: true,
|
||||||
|
defaultValue: "user", // Default for migration
|
||||||
|
input: true, // Allow in signup form
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
// TODO: Add plugins for SSO and OIDC support in the future
|
||||||
|
// plugins: [],
|
||||||
|
|
||||||
|
// Trusted origins for CORS
|
||||||
|
trustedOrigins: [
|
||||||
|
process.env.BETTER_AUTH_URL || "http://localhost:3000",
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
// Export type for use in other parts of the app
|
||||||
|
export type Auth = typeof auth;
|
||||||
@@ -23,14 +23,14 @@ let sqlite: Database;
|
|||||||
try {
|
try {
|
||||||
sqlite = new Database(dbPath);
|
sqlite = new Database(dbPath);
|
||||||
console.log("Successfully connected to SQLite database using Bun's native driver");
|
console.log("Successfully connected to SQLite database using Bun's native driver");
|
||||||
|
|
||||||
// Run Drizzle migrations if needed
|
|
||||||
runDrizzleMigrations();
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Error opening database:", error);
|
console.error("Error opening database:", error);
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Create drizzle instance with the SQLite client
|
||||||
|
export const db = drizzle({ client: sqlite });
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Run Drizzle migrations
|
* Run Drizzle migrations
|
||||||
*/
|
*/
|
||||||
@@ -57,8 +57,18 @@ function runDrizzleMigrations() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create drizzle instance with the SQLite client
|
// Run Drizzle migrations after db is initialized
|
||||||
export const db = drizzle({ client: sqlite });
|
runDrizzleMigrations();
|
||||||
|
|
||||||
// Export all table definitions from schema
|
// Export all table definitions from schema
|
||||||
export { users, events, configs, repositories, mirrorJobs, organizations } from "./schema";
|
export {
|
||||||
|
users,
|
||||||
|
events,
|
||||||
|
configs,
|
||||||
|
repositories,
|
||||||
|
mirrorJobs,
|
||||||
|
organizations,
|
||||||
|
sessions,
|
||||||
|
accounts,
|
||||||
|
verificationTokens
|
||||||
|
} from "./schema";
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ export const userSchema = z.object({
|
|||||||
username: z.string(),
|
username: z.string(),
|
||||||
password: z.string(),
|
password: z.string(),
|
||||||
email: z.string().email(),
|
email: z.string().email(),
|
||||||
|
emailVerified: z.boolean().default(false),
|
||||||
createdAt: z.coerce.date(),
|
createdAt: z.coerce.date(),
|
||||||
updatedAt: z.coerce.date(),
|
updatedAt: z.coerce.date(),
|
||||||
});
|
});
|
||||||
@@ -215,6 +216,7 @@ export const users = sqliteTable("users", {
|
|||||||
username: text("username").notNull(),
|
username: text("username").notNull(),
|
||||||
password: text("password").notNull(),
|
password: text("password").notNull(),
|
||||||
email: text("email").notNull(),
|
email: text("email").notNull(),
|
||||||
|
emailVerified: integer("email_verified", { mode: "boolean" }).notNull().default(false),
|
||||||
createdAt: integer("created_at", { mode: "timestamp" })
|
createdAt: integer("created_at", { mode: "timestamp" })
|
||||||
.notNull()
|
.notNull()
|
||||||
.default(sql`(unixepoch())`),
|
.default(sql`(unixepoch())`),
|
||||||
@@ -434,6 +436,70 @@ export const organizations = sqliteTable("organizations", {
|
|||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// ===== Better Auth Tables =====
|
||||||
|
|
||||||
|
// Sessions table
|
||||||
|
export const sessions = sqliteTable("sessions", {
|
||||||
|
id: text("id").primaryKey(),
|
||||||
|
token: text("token").notNull().unique(),
|
||||||
|
userId: text("user_id").notNull().references(() => users.id),
|
||||||
|
expiresAt: integer("expires_at", { mode: "timestamp" }).notNull(),
|
||||||
|
ipAddress: text("ip_address"),
|
||||||
|
userAgent: text("user_agent"),
|
||||||
|
createdAt: integer("created_at", { mode: "timestamp" })
|
||||||
|
.notNull()
|
||||||
|
.default(sql`(unixepoch())`),
|
||||||
|
updatedAt: integer("updated_at", { mode: "timestamp" })
|
||||||
|
.notNull()
|
||||||
|
.default(sql`(unixepoch())`),
|
||||||
|
}, (table) => {
|
||||||
|
return {
|
||||||
|
userIdIdx: index("idx_sessions_user_id").on(table.userId),
|
||||||
|
tokenIdx: index("idx_sessions_token").on(table.token),
|
||||||
|
expiresAtIdx: index("idx_sessions_expires_at").on(table.expiresAt),
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
// Accounts table (for OAuth providers and credentials)
|
||||||
|
export const accounts = sqliteTable("accounts", {
|
||||||
|
id: text("id").primaryKey(),
|
||||||
|
userId: text("user_id").notNull().references(() => users.id),
|
||||||
|
providerId: text("provider_id").notNull(),
|
||||||
|
providerUserId: text("provider_user_id").notNull(),
|
||||||
|
accessToken: text("access_token"),
|
||||||
|
refreshToken: text("refresh_token"),
|
||||||
|
expiresAt: integer("expires_at", { mode: "timestamp" }),
|
||||||
|
password: text("password"), // For credential provider
|
||||||
|
createdAt: integer("created_at", { mode: "timestamp" })
|
||||||
|
.notNull()
|
||||||
|
.default(sql`(unixepoch())`),
|
||||||
|
updatedAt: integer("updated_at", { mode: "timestamp" })
|
||||||
|
.notNull()
|
||||||
|
.default(sql`(unixepoch())`),
|
||||||
|
}, (table) => {
|
||||||
|
return {
|
||||||
|
userIdIdx: index("idx_accounts_user_id").on(table.userId),
|
||||||
|
providerIdx: index("idx_accounts_provider").on(table.providerId, table.providerUserId),
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
// Verification tokens table
|
||||||
|
export const verificationTokens = sqliteTable("verification_tokens", {
|
||||||
|
id: text("id").primaryKey(),
|
||||||
|
token: text("token").notNull().unique(),
|
||||||
|
identifier: text("identifier").notNull(),
|
||||||
|
type: text("type").notNull(), // email, password-reset, etc
|
||||||
|
expiresAt: integer("expires_at", { mode: "timestamp" }).notNull(),
|
||||||
|
createdAt: integer("created_at", { mode: "timestamp" })
|
||||||
|
.notNull()
|
||||||
|
.default(sql`(unixepoch())`),
|
||||||
|
}, (table) => {
|
||||||
|
return {
|
||||||
|
tokenIdx: index("idx_verification_tokens_token").on(table.token),
|
||||||
|
identifierIdx: index("idx_verification_tokens_identifier").on(table.identifier),
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
// Export type definitions
|
// Export type definitions
|
||||||
export type User = z.infer<typeof userSchema>;
|
export type User = z.infer<typeof userSchema>;
|
||||||
export type Config = z.infer<typeof configSchema>;
|
export type Config = z.infer<typeof configSchema>;
|
||||||
|
|||||||
58
src/lib/utils/auth-helpers.ts
Normal file
58
src/lib/utils/auth-helpers.ts
Normal file
@@ -0,0 +1,58 @@
|
|||||||
|
import type { APIRoute, APIContext } from "astro";
|
||||||
|
import { auth } from "@/lib/auth";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get authenticated user from request
|
||||||
|
* @param request - The request object from Astro API route
|
||||||
|
* @returns The authenticated user or null if not authenticated
|
||||||
|
*/
|
||||||
|
export async function getAuthenticatedUser(request: Request) {
|
||||||
|
try {
|
||||||
|
const session = await auth.api.getSession({
|
||||||
|
headers: request.headers,
|
||||||
|
});
|
||||||
|
|
||||||
|
return session ? session.user : null;
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error getting session:", error);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Require authentication for API routes
|
||||||
|
* Returns an error response if user is not authenticated
|
||||||
|
* @param context - The API context from Astro
|
||||||
|
* @returns Object with user if authenticated, or error response if not
|
||||||
|
*/
|
||||||
|
export async function requireAuth(context: APIContext) {
|
||||||
|
const user = await getAuthenticatedUser(context.request);
|
||||||
|
|
||||||
|
if (!user) {
|
||||||
|
return {
|
||||||
|
user: null,
|
||||||
|
response: new Response(
|
||||||
|
JSON.stringify({
|
||||||
|
success: false,
|
||||||
|
error: "Unauthorized - Please log in",
|
||||||
|
}),
|
||||||
|
{
|
||||||
|
status: 401,
|
||||||
|
headers: { "Content-Type": "application/json" },
|
||||||
|
}
|
||||||
|
),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return { user, response: null };
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get user ID from authenticated session
|
||||||
|
* @param request - The request object from Astro API route
|
||||||
|
* @returns The user ID or null if not authenticated
|
||||||
|
*/
|
||||||
|
export async function getAuthenticatedUserId(request: Request): Promise<string | null> {
|
||||||
|
const user = await getAuthenticatedUser(request);
|
||||||
|
return user?.id || null;
|
||||||
|
}
|
||||||
@@ -3,6 +3,7 @@ import { initializeRecovery, hasJobsNeedingRecovery, getRecoveryStatus } from '.
|
|||||||
import { startCleanupService, stopCleanupService } from './lib/cleanup-service';
|
import { startCleanupService, stopCleanupService } from './lib/cleanup-service';
|
||||||
import { initializeShutdownManager, registerShutdownCallback } from './lib/shutdown-manager';
|
import { initializeShutdownManager, registerShutdownCallback } from './lib/shutdown-manager';
|
||||||
import { setupSignalHandlers } from './lib/signal-handlers';
|
import { setupSignalHandlers } from './lib/signal-handlers';
|
||||||
|
import { auth } from './lib/auth';
|
||||||
|
|
||||||
// Flag to track if recovery has been initialized
|
// Flag to track if recovery has been initialized
|
||||||
let recoveryInitialized = false;
|
let recoveryInitialized = false;
|
||||||
@@ -11,6 +12,25 @@ let cleanupServiceStarted = false;
|
|||||||
let shutdownManagerInitialized = false;
|
let shutdownManagerInitialized = false;
|
||||||
|
|
||||||
export const onRequest = defineMiddleware(async (context, next) => {
|
export const onRequest = defineMiddleware(async (context, next) => {
|
||||||
|
// Handle Better Auth session
|
||||||
|
try {
|
||||||
|
const session = await auth.api.getSession({
|
||||||
|
headers: context.request.headers,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (session) {
|
||||||
|
context.locals.user = session.user;
|
||||||
|
context.locals.session = session.session;
|
||||||
|
} else {
|
||||||
|
context.locals.user = null;
|
||||||
|
context.locals.session = null;
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
// If there's an error getting the session, set to null
|
||||||
|
context.locals.user = null;
|
||||||
|
context.locals.session = null;
|
||||||
|
}
|
||||||
|
|
||||||
// Initialize shutdown manager and signal handlers first
|
// Initialize shutdown manager and signal handlers first
|
||||||
if (!shutdownManagerInitialized) {
|
if (!shutdownManagerInitialized) {
|
||||||
try {
|
try {
|
||||||
|
|||||||
10
src/pages/api/auth/[...all].ts
Normal file
10
src/pages/api/auth/[...all].ts
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
import { auth } from "@/lib/auth";
|
||||||
|
import type { APIRoute } from "astro";
|
||||||
|
|
||||||
|
export const ALL: APIRoute = async (ctx) => {
|
||||||
|
// If you want to use rate limiting, make sure to set the 'x-forwarded-for' header
|
||||||
|
// to the request headers from the context
|
||||||
|
// ctx.request.headers.set("x-forwarded-for", ctx.clientAddress);
|
||||||
|
|
||||||
|
return auth.handler(ctx.request);
|
||||||
|
};
|
||||||
30
src/pages/api/auth/check-users.ts
Normal file
30
src/pages/api/auth/check-users.ts
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
import type { APIRoute } from "astro";
|
||||||
|
import { db, users } from "@/lib/db";
|
||||||
|
import { sql } from "drizzle-orm";
|
||||||
|
|
||||||
|
export const GET: APIRoute = async () => {
|
||||||
|
try {
|
||||||
|
const userCountResult = await db
|
||||||
|
.select({ count: sql<number>`count(*)` })
|
||||||
|
.from(users);
|
||||||
|
|
||||||
|
const userCount = userCountResult[0].count;
|
||||||
|
|
||||||
|
if (userCount === 0) {
|
||||||
|
return new Response(JSON.stringify({ error: "No users found" }), {
|
||||||
|
status: 404,
|
||||||
|
headers: { "Content-Type": "application/json" },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return new Response(JSON.stringify({ userCount }), {
|
||||||
|
status: 200,
|
||||||
|
headers: { "Content-Type": "application/json" },
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
return new Response(JSON.stringify({ error: "Internal server error" }), {
|
||||||
|
status: 500,
|
||||||
|
headers: { "Content-Type": "application/json" },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
13
src/pages/api/auth/legacy-backup/README.md
Normal file
13
src/pages/api/auth/legacy-backup/README.md
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
# Legacy Auth Routes Backup
|
||||||
|
|
||||||
|
These files are the original authentication routes before migrating to Better Auth.
|
||||||
|
They are kept here as a reference during the migration process.
|
||||||
|
|
||||||
|
## Migration Notes
|
||||||
|
|
||||||
|
- `index.ts` - Handled user session validation and getting current user
|
||||||
|
- `login.ts` - Handled user login with email/password
|
||||||
|
- `logout.ts` - Handled user logout and session cleanup
|
||||||
|
- `register.ts` - Handled new user registration
|
||||||
|
|
||||||
|
All these endpoints are now handled by Better Auth through the catch-all route `[...all].ts`.
|
||||||
@@ -2,36 +2,17 @@ import type { APIRoute } from "astro";
|
|||||||
import { db, organizations } from "@/lib/db";
|
import { db, organizations } from "@/lib/db";
|
||||||
import { eq, and } from "drizzle-orm";
|
import { eq, and } from "drizzle-orm";
|
||||||
import { createSecureErrorResponse } from "@/lib/utils";
|
import { createSecureErrorResponse } from "@/lib/utils";
|
||||||
import jwt from "jsonwebtoken";
|
import { requireAuth } from "@/lib/utils/auth-helpers";
|
||||||
|
|
||||||
const JWT_SECRET = process.env.JWT_SECRET || "your-secret-key";
|
export const PATCH: APIRoute = async (context) => {
|
||||||
|
|
||||||
export const PATCH: APIRoute = async ({ request, params, cookies }) => {
|
|
||||||
try {
|
try {
|
||||||
// Get token from Authorization header or cookies
|
// Check authentication
|
||||||
const authHeader = request.headers.get("Authorization");
|
const { user, response } = await requireAuth(context);
|
||||||
const token = authHeader?.split(" ")[1] || cookies.get("token")?.value;
|
if (response) return response;
|
||||||
|
|
||||||
if (!token) {
|
const userId = user!.id;
|
||||||
return new Response(JSON.stringify({ error: "Unauthorized" }), {
|
|
||||||
status: 401,
|
|
||||||
headers: { "Content-Type": "application/json" },
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// Verify token and get user ID
|
const orgId = context.params.id;
|
||||||
let userId: string;
|
|
||||||
try {
|
|
||||||
const decoded = jwt.verify(token, JWT_SECRET) as { id: string };
|
|
||||||
userId = decoded.id;
|
|
||||||
} catch (error) {
|
|
||||||
return new Response(JSON.stringify({ error: "Invalid token" }), {
|
|
||||||
status: 401,
|
|
||||||
headers: { "Content-Type": "application/json" },
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
const orgId = params.id;
|
|
||||||
if (!orgId) {
|
if (!orgId) {
|
||||||
return new Response(JSON.stringify({ error: "Organization ID is required" }), {
|
return new Response(JSON.stringify({ error: "Organization ID is required" }), {
|
||||||
status: 400,
|
status: 400,
|
||||||
@@ -39,7 +20,7 @@ export const PATCH: APIRoute = async ({ request, params, cookies }) => {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
const body = await request.json();
|
const body = await context.request.json();
|
||||||
const { destinationOrg } = body;
|
const { destinationOrg } = body;
|
||||||
|
|
||||||
// Validate that the organization belongs to the user
|
// Validate that the organization belongs to the user
|
||||||
|
|||||||
@@ -2,36 +2,17 @@ import type { APIRoute } from "astro";
|
|||||||
import { db, repositories } from "@/lib/db";
|
import { db, repositories } from "@/lib/db";
|
||||||
import { eq, and } from "drizzle-orm";
|
import { eq, and } from "drizzle-orm";
|
||||||
import { createSecureErrorResponse } from "@/lib/utils";
|
import { createSecureErrorResponse } from "@/lib/utils";
|
||||||
import jwt from "jsonwebtoken";
|
import { requireAuth } from "@/lib/utils/auth-helpers";
|
||||||
|
|
||||||
const JWT_SECRET = process.env.JWT_SECRET || "your-secret-key";
|
export const PATCH: APIRoute = async (context) => {
|
||||||
|
|
||||||
export const PATCH: APIRoute = async ({ request, params, cookies }) => {
|
|
||||||
try {
|
try {
|
||||||
// Get token from Authorization header or cookies
|
// Check authentication
|
||||||
const authHeader = request.headers.get("Authorization");
|
const { user, response } = await requireAuth(context);
|
||||||
const token = authHeader?.split(" ")[1] || cookies.get("token")?.value;
|
if (response) return response;
|
||||||
|
|
||||||
if (!token) {
|
const userId = user!.id;
|
||||||
return new Response(JSON.stringify({ error: "Unauthorized" }), {
|
|
||||||
status: 401,
|
|
||||||
headers: { "Content-Type": "application/json" },
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// Verify token and get user ID
|
const repoId = context.params.id;
|
||||||
let userId: string;
|
|
||||||
try {
|
|
||||||
const decoded = jwt.verify(token, JWT_SECRET) as { id: string };
|
|
||||||
userId = decoded.id;
|
|
||||||
} catch (error) {
|
|
||||||
return new Response(JSON.stringify({ error: "Invalid token" }), {
|
|
||||||
status: 401,
|
|
||||||
headers: { "Content-Type": "application/json" },
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
const repoId = params.id;
|
|
||||||
if (!repoId) {
|
if (!repoId) {
|
||||||
return new Response(JSON.stringify({ error: "Repository ID is required" }), {
|
return new Response(JSON.stringify({ error: "Repository ID is required" }), {
|
||||||
status: 400,
|
status: 400,
|
||||||
@@ -39,7 +20,7 @@ export const PATCH: APIRoute = async ({ request, params, cookies }) => {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
const body = await request.json();
|
const body = await context.request.json();
|
||||||
const { destinationOrg } = body;
|
const { destinationOrg } = body;
|
||||||
|
|
||||||
// Validate that the repository belongs to the user
|
// Validate that the repository belongs to the user
|
||||||
|
|||||||
@@ -1,12 +1,13 @@
|
|||||||
---
|
---
|
||||||
import '../styles/global.css';
|
import '../styles/global.css';
|
||||||
import App from '@/components/layout/MainLayout';
|
import App from '@/components/layout/MainLayout';
|
||||||
import { db, repositories, mirrorJobs, client } from '@/lib/db';
|
import { db, repositories, mirrorJobs, users } from '@/lib/db';
|
||||||
|
import { sql } from 'drizzle-orm';
|
||||||
import ThemeScript from '@/components/theme/ThemeScript.astro';
|
import ThemeScript from '@/components/theme/ThemeScript.astro';
|
||||||
|
|
||||||
// Check if any users exist in the database
|
// Check if any users exist in the database
|
||||||
const userCountResult = await client.execute(`SELECT COUNT(*) as count FROM users`);
|
const userCountResult = await db.select({ count: sql<number>`count(*)` }).from(users);
|
||||||
const userCount = userCountResult.rows[0].count;
|
const userCount = userCountResult[0]?.count || 0;
|
||||||
|
|
||||||
// Redirect to signup if no users exist
|
// Redirect to signup if no users exist
|
||||||
if (userCount === 0) {
|
if (userCount === 0) {
|
||||||
|
|||||||
@@ -2,11 +2,14 @@
|
|||||||
import '../styles/global.css';
|
import '../styles/global.css';
|
||||||
import ThemeScript from '@/components/theme/ThemeScript.astro';
|
import ThemeScript from '@/components/theme/ThemeScript.astro';
|
||||||
import { LoginForm } from '@/components/auth/LoginForm';
|
import { LoginForm } from '@/components/auth/LoginForm';
|
||||||
import { client } from '../lib/db';
|
import { db, users } from '@/lib/db';
|
||||||
|
import { sql } from 'drizzle-orm';
|
||||||
|
|
||||||
// Check if any users exist in the database
|
// Check if any users exist in the database
|
||||||
const userCountResult = await client.execute(`SELECT COUNT(*) as count FROM users`);
|
const userCountResult = await db
|
||||||
const userCount = userCountResult.rows[0].count;
|
.select({ count: sql<number>`count(*)` })
|
||||||
|
.from(users);
|
||||||
|
const userCount = userCountResult[0].count;
|
||||||
|
|
||||||
// Redirect to signup if no users exist
|
// Redirect to signup if no users exist
|
||||||
if (userCount === 0) {
|
if (userCount === 0) {
|
||||||
|
|||||||
@@ -2,11 +2,14 @@
|
|||||||
import '../styles/global.css';
|
import '../styles/global.css';
|
||||||
import ThemeScript from '@/components/theme/ThemeScript.astro';
|
import ThemeScript from '@/components/theme/ThemeScript.astro';
|
||||||
import { SignupForm } from '@/components/auth/SignupForm';
|
import { SignupForm } from '@/components/auth/SignupForm';
|
||||||
import { client } from '../lib/db';
|
import { db, users } from '@/lib/db';
|
||||||
|
import { sql } from 'drizzle-orm';
|
||||||
|
|
||||||
// Check if any users exist in the database
|
// Check if any users exist in the database
|
||||||
const userCountResult = await client.execute(`SELECT COUNT(*) as count FROM users`);
|
const userCountResult = await db
|
||||||
const userCount = userCountResult.rows[0]?.count;
|
.select({ count: sql<number>`count(*)` })
|
||||||
|
.from(users);
|
||||||
|
const userCount = userCountResult[0]?.count;
|
||||||
|
|
||||||
// Redirect to login if users already exist
|
// Redirect to login if users already exist
|
||||||
if (userCount !== null && Number(userCount) > 0) {
|
if (userCount !== null && Number(userCount) > 0) {
|
||||||
|
|||||||
Reference in New Issue
Block a user