mirror of
https://github.com/Hestia-Homes/assessment-model.git
synced 2026-06-08 11:37:25 +00:00
working on sign in and onboarding
This commit is contained in:
parent
4bf3ec44f2
commit
7d51fcc4dc
14 changed files with 4348 additions and 131 deletions
165
package-lock.json
generated
165
package-lock.json
generated
|
|
@ -38,8 +38,7 @@
|
|||
"aws-sdk": "^2.1415.0",
|
||||
"class-variance-authority": "^0.6.1",
|
||||
"clsx": "^1.2.1",
|
||||
"drizzle-kit": "^0.31.4",
|
||||
"drizzle-orm": "^0.44.3",
|
||||
"drizzle-orm": "^0.44.5",
|
||||
"esbuild": "^0.25.8",
|
||||
"eslint-config-next": "13.4.3",
|
||||
"lucide-react": "^0.233.0",
|
||||
|
|
@ -47,6 +46,7 @@
|
|||
"next-auth": "^4.22.1",
|
||||
"next-axiom": "^1.9.2",
|
||||
"next-themes": "^0.3.0",
|
||||
"nodemailer": "^6.10.1",
|
||||
"pg": "^8.11.1",
|
||||
"postcss": "^8.5.6",
|
||||
"react": "18.3.1",
|
||||
|
|
@ -68,6 +68,7 @@
|
|||
"cypress": "^14.5.3",
|
||||
"cypress-social-logins": "^1.14.1",
|
||||
"dotenv": "^16.3.1",
|
||||
"drizzle-kit": "^0.31.5",
|
||||
"eslint": "^8.57.1",
|
||||
"prettier": "^3.6.2",
|
||||
"start-server-and-test": "^2.0.0"
|
||||
|
|
@ -85,6 +86,95 @@
|
|||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/@auth/core": {
|
||||
"version": "0.34.2",
|
||||
"resolved": "https://registry.npmjs.org/@auth/core/-/core-0.34.2.tgz",
|
||||
"integrity": "sha512-KywHKRgLiF3l7PLyL73fjLSIBe1YNcA6sMeew4yMP6cfCWGXZrkkXd32AjRi1hlJ9nvovUBGZHvbn+LijO6ZeQ==",
|
||||
"license": "ISC",
|
||||
"optional": true,
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@panva/hkdf": "^1.1.1",
|
||||
"@types/cookie": "0.6.0",
|
||||
"cookie": "0.6.0",
|
||||
"jose": "^5.1.3",
|
||||
"oauth4webapi": "^2.10.4",
|
||||
"preact": "10.11.3",
|
||||
"preact-render-to-string": "5.2.3"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@simplewebauthn/browser": "^9.0.1",
|
||||
"@simplewebauthn/server": "^9.0.2",
|
||||
"nodemailer": "^6.8.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@simplewebauthn/browser": {
|
||||
"optional": true
|
||||
},
|
||||
"@simplewebauthn/server": {
|
||||
"optional": true
|
||||
},
|
||||
"nodemailer": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@auth/core/node_modules/cookie": {
|
||||
"version": "0.6.0",
|
||||
"resolved": "https://registry.npmjs.org/cookie/-/cookie-0.6.0.tgz",
|
||||
"integrity": "sha512-U71cyTamuh1CRNCfpGY6to28lxvNwPG4Guz/EVjgf3Jmzv0vlDp1atT9eS5dDjMYHucpHbWns6Lwf3BKz6svdw==",
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">= 0.6"
|
||||
}
|
||||
},
|
||||
"node_modules/@auth/core/node_modules/jose": {
|
||||
"version": "5.10.0",
|
||||
"resolved": "https://registry.npmjs.org/jose/-/jose-5.10.0.tgz",
|
||||
"integrity": "sha512-s+3Al/p9g32Iq+oqXxkW//7jk2Vig6FF1CFqzVXoTUXt2qz89YWbL+OwS17NFYEvxC35n0FKeGO2LGYSxeM2Gg==",
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"peer": true,
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/panva"
|
||||
}
|
||||
},
|
||||
"node_modules/@auth/core/node_modules/preact": {
|
||||
"version": "10.11.3",
|
||||
"resolved": "https://registry.npmjs.org/preact/-/preact-10.11.3.tgz",
|
||||
"integrity": "sha512-eY93IVpod/zG3uMF22Unl8h9KkrcKIRs2EGar8hwLZZDU1lkjph303V9HZBwufh2s736U6VXuhD109LYqPoffg==",
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"peer": true,
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/preact"
|
||||
}
|
||||
},
|
||||
"node_modules/@auth/core/node_modules/preact-render-to-string": {
|
||||
"version": "5.2.3",
|
||||
"resolved": "https://registry.npmjs.org/preact-render-to-string/-/preact-render-to-string-5.2.3.tgz",
|
||||
"integrity": "sha512-aPDxUn5o3GhWdtJtW0svRC2SS/l8D9MAgo2+AWml+BhDImb27ALf04Q2d+AHqUUOc6RdSXFIBVa2gxzgMKgtZA==",
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"pretty-format": "^3.8.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"preact": ">=10"
|
||||
}
|
||||
},
|
||||
"node_modules/@auth/core/node_modules/pretty-format": {
|
||||
"version": "3.8.0",
|
||||
"resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-3.8.0.tgz",
|
||||
"integrity": "sha512-WuxUnVtlWL1OfZFQFuqvnvs6MiAGk9UNsBostyBOB0Is9wb5uRESevA6rnl/rkksXaGX3GzZhPup5d6Vp1nFew==",
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"peer": true
|
||||
},
|
||||
"node_modules/@aws-crypto/sha256-browser": {
|
||||
"version": "5.2.0",
|
||||
"resolved": "https://registry.npmjs.org/@aws-crypto/sha256-browser/-/sha256-browser-5.2.0.tgz",
|
||||
|
|
@ -826,6 +916,7 @@
|
|||
"version": "0.10.2",
|
||||
"resolved": "https://registry.npmjs.org/@drizzle-team/brocli/-/brocli-0.10.2.tgz",
|
||||
"integrity": "sha512-z33Il7l5dKjUgGULTqBsQBQwckHh5AbIuxhdsIxDDiZAzBOrZO6q9ogcWC65kU382AfynTfgNumVcNIjuIua6w==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0"
|
||||
},
|
||||
"node_modules/@emnapi/core": {
|
||||
|
|
@ -864,6 +955,7 @@
|
|||
"resolved": "https://registry.npmjs.org/@esbuild-kit/core-utils/-/core-utils-3.3.2.tgz",
|
||||
"integrity": "sha512-sPRAnw9CdSsRmEtnsl2WXWdyquogVpB3yZ3dgwJfe8zrOzTsV7cJvmwrKVa+0ma5BoiGJ+BoqkMvawbayKUsqQ==",
|
||||
"deprecated": "Merged into tsx: https://tsx.is",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"esbuild": "~0.18.20",
|
||||
|
|
@ -877,6 +969,7 @@
|
|||
"cpu": [
|
||||
"arm"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
|
|
@ -893,6 +986,7 @@
|
|||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
|
|
@ -909,6 +1003,7 @@
|
|||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
|
|
@ -925,6 +1020,7 @@
|
|||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
|
|
@ -941,6 +1037,7 @@
|
|||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
|
|
@ -957,6 +1054,7 @@
|
|||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
|
|
@ -973,6 +1071,7 @@
|
|||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
|
|
@ -989,6 +1088,7 @@
|
|||
"cpu": [
|
||||
"arm"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
|
|
@ -1005,6 +1105,7 @@
|
|||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
|
|
@ -1021,6 +1122,7 @@
|
|||
"cpu": [
|
||||
"ia32"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
|
|
@ -1037,6 +1139,7 @@
|
|||
"cpu": [
|
||||
"loong64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
|
|
@ -1053,6 +1156,7 @@
|
|||
"cpu": [
|
||||
"mips64el"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
|
|
@ -1069,6 +1173,7 @@
|
|||
"cpu": [
|
||||
"ppc64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
|
|
@ -1085,6 +1190,7 @@
|
|||
"cpu": [
|
||||
"riscv64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
|
|
@ -1101,6 +1207,7 @@
|
|||
"cpu": [
|
||||
"s390x"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
|
|
@ -1117,6 +1224,7 @@
|
|||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
|
|
@ -1133,6 +1241,7 @@
|
|||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
|
|
@ -1149,6 +1258,7 @@
|
|||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
|
|
@ -1165,6 +1275,7 @@
|
|||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
|
|
@ -1181,6 +1292,7 @@
|
|||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
|
|
@ -1197,6 +1309,7 @@
|
|||
"cpu": [
|
||||
"ia32"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
|
|
@ -1213,6 +1326,7 @@
|
|||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
|
|
@ -1226,6 +1340,7 @@
|
|||
"version": "0.18.20",
|
||||
"resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.18.20.tgz",
|
||||
"integrity": "sha512-ceqxoedUrcayh7Y7ZX6NdbbDzGROiyVBgC4PriJThBKSVPWnnFHZAkfI1lJT8QFkOwH4qOS2SJkS4wvpGl8BpA==",
|
||||
"dev": true,
|
||||
"hasInstallScript": true,
|
||||
"license": "MIT",
|
||||
"bin": {
|
||||
|
|
@ -1264,6 +1379,7 @@
|
|||
"resolved": "https://registry.npmjs.org/@esbuild-kit/esm-loader/-/esm-loader-2.6.5.tgz",
|
||||
"integrity": "sha512-FxEMIkJKnodyA1OaCUoEvbYRkoZlLZ4d/eXFu9Fh8CbBBgP5EmZxrfTRyN0qpXZ4vOvqnE5YdRdcrmUUXuU+dA==",
|
||||
"deprecated": "Merged into tsx: https://tsx.is",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@esbuild-kit/core-utils": "^3.3.2",
|
||||
|
|
@ -5470,6 +5586,14 @@
|
|||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/cookie": {
|
||||
"version": "0.6.0",
|
||||
"resolved": "https://registry.npmjs.org/@types/cookie/-/cookie-0.6.0.tgz",
|
||||
"integrity": "sha512-4Kh9a6B2bQciAhf7FSuMRRkUWecJgJu9nPnx3yzpsfXX/c50REIqpHY4C82bXP90qrLtXtkDxTZosYO3UpOwlA==",
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"peer": true
|
||||
},
|
||||
"node_modules/@types/d3-array": {
|
||||
"version": "3.2.1",
|
||||
"resolved": "https://registry.npmjs.org/@types/d3-array/-/d3-array-3.2.1.tgz",
|
||||
|
|
@ -6849,6 +6973,7 @@
|
|||
"version": "1.1.2",
|
||||
"resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz",
|
||||
"integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/cachedir": {
|
||||
|
|
@ -7879,9 +8004,10 @@
|
|||
}
|
||||
},
|
||||
"node_modules/drizzle-kit": {
|
||||
"version": "0.31.4",
|
||||
"resolved": "https://registry.npmjs.org/drizzle-kit/-/drizzle-kit-0.31.4.tgz",
|
||||
"integrity": "sha512-tCPWVZWZqWVx2XUsVpJRnH9Mx0ClVOf5YUHerZ5so1OKSlqww4zy1R5ksEdGRcO3tM3zj0PYN6V48TbQCL1RfA==",
|
||||
"version": "0.31.5",
|
||||
"resolved": "https://registry.npmjs.org/drizzle-kit/-/drizzle-kit-0.31.5.tgz",
|
||||
"integrity": "sha512-+CHgPFzuoTQTt7cOYCV6MOw2w8vqEn/ap1yv4bpZOWL03u7rlVRQhUY0WYT3rHsgVTXwYQDZaSUJSQrMBUKuWg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@drizzle-team/brocli": "^0.10.2",
|
||||
|
|
@ -7894,9 +8020,9 @@
|
|||
}
|
||||
},
|
||||
"node_modules/drizzle-orm": {
|
||||
"version": "0.44.4",
|
||||
"resolved": "https://registry.npmjs.org/drizzle-orm/-/drizzle-orm-0.44.4.tgz",
|
||||
"integrity": "sha512-ZyzKFpTC/Ut3fIqc2c0dPZ6nhchQXriTsqTNs4ayRgl6sZcFlMs9QZKPSHXK4bdOf41GHGWf+FrpcDDYwW+W6Q==",
|
||||
"version": "0.44.5",
|
||||
"resolved": "https://registry.npmjs.org/drizzle-orm/-/drizzle-orm-0.44.5.tgz",
|
||||
"integrity": "sha512-jBe37K7d8ZSKptdKfakQFdeljtu3P2Cbo7tJoJSVZADzIKOBo9IAJPOmMsH2bZl90bZgh8FQlD8BjxXA/zuBkQ==",
|
||||
"license": "Apache-2.0",
|
||||
"peerDependencies": {
|
||||
"@aws-sdk/client-rds-data": ">=3",
|
||||
|
|
@ -8333,6 +8459,7 @@
|
|||
"version": "3.6.0",
|
||||
"resolved": "https://registry.npmjs.org/esbuild-register/-/esbuild-register-3.6.0.tgz",
|
||||
"integrity": "sha512-H2/S7Pm8a9CL1uhp9OvjwrBh5Pvx0H8qVOxNu8Wed9Y7qv56MPtq+GGM8RJpq6glYJn9Wspr8uw7l55uyinNeg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"debug": "^4.3.4"
|
||||
|
|
@ -11150,6 +11277,15 @@
|
|||
"integrity": "sha512-xxOWJsBKtzAq7DY0J+DTzuz58K8e7sJbdgwkbMWQe8UYB6ekmsQ45q0M/tJDsGaZmbC+l7n57UV8Hl5tHxO9uw==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/nodemailer": {
|
||||
"version": "6.10.1",
|
||||
"resolved": "https://registry.npmjs.org/nodemailer/-/nodemailer-6.10.1.tgz",
|
||||
"integrity": "sha512-Z+iLaBGVaSjbIzQ4pX6XV41HrooLsQ10ZWPUehGmuantvzWoDVBnmsdUcOIDM1t+yPor5pDhVlDESgOMEGxhHA==",
|
||||
"license": "MIT-0",
|
||||
"engines": {
|
||||
"node": ">=6.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/normalize-path": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz",
|
||||
|
|
@ -11187,6 +11323,17 @@
|
|||
"integrity": "sha512-a5ERWK1kh38ExDEfoO6qUHJb32rd7aYmPHuyCu3Fta/cnICvYmgd2uhuKXvPD+PXB+gCEYYEaQdIRAjCOwAKNA==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/oauth4webapi": {
|
||||
"version": "2.17.0",
|
||||
"resolved": "https://registry.npmjs.org/oauth4webapi/-/oauth4webapi-2.17.0.tgz",
|
||||
"integrity": "sha512-lbC0Z7uzAFNFyzEYRIC+pkSVvDHJTbEW+dYlSBAlCYDe6RxUkJ26bClhk8ocBZip1wfI9uKTe0fm4Ib4RHn6uQ==",
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"peer": true,
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/panva"
|
||||
}
|
||||
},
|
||||
"node_modules/object-assign": {
|
||||
"version": "4.1.1",
|
||||
"resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz",
|
||||
|
|
@ -13044,6 +13191,7 @@
|
|||
"version": "0.6.1",
|
||||
"resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
|
||||
"integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==",
|
||||
"dev": true,
|
||||
"license": "BSD-3-Clause",
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
|
|
@ -13062,6 +13210,7 @@
|
|||
"version": "0.5.21",
|
||||
"resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz",
|
||||
"integrity": "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"buffer-from": "^1.0.0",
|
||||
|
|
|
|||
|
|
@ -44,8 +44,7 @@
|
|||
"aws-sdk": "^2.1415.0",
|
||||
"class-variance-authority": "^0.6.1",
|
||||
"clsx": "^1.2.1",
|
||||
"drizzle-kit": "^0.31.4",
|
||||
"drizzle-orm": "^0.44.3",
|
||||
"drizzle-orm": "^0.44.5",
|
||||
"esbuild": "^0.25.8",
|
||||
"eslint-config-next": "13.4.3",
|
||||
"lucide-react": "^0.233.0",
|
||||
|
|
@ -53,6 +52,7 @@
|
|||
"next-auth": "^4.22.1",
|
||||
"next-axiom": "^1.9.2",
|
||||
"next-themes": "^0.3.0",
|
||||
"nodemailer": "^6.10.1",
|
||||
"pg": "^8.11.1",
|
||||
"postcss": "^8.5.6",
|
||||
"react": "18.3.1",
|
||||
|
|
@ -74,6 +74,7 @@
|
|||
"cypress": "^14.5.3",
|
||||
"cypress-social-logins": "^1.14.1",
|
||||
"dotenv": "^16.3.1",
|
||||
"drizzle-kit": "^0.31.5",
|
||||
"eslint": "^8.57.1",
|
||||
"prettier": "^3.6.2",
|
||||
"start-server-and-test": "^2.0.0"
|
||||
|
|
|
|||
276
src/app/api/auth/[...nextauth]/DrizzleEmailAdapter.ts
Normal file
276
src/app/api/auth/[...nextauth]/DrizzleEmailAdapter.ts
Normal file
|
|
@ -0,0 +1,276 @@
|
|||
import { eq, and } from "drizzle-orm";
|
||||
import type {
|
||||
Adapter,
|
||||
AdapterUser,
|
||||
AdapterAccount,
|
||||
VerificationToken,
|
||||
} from "next-auth/adapters";
|
||||
|
||||
import {
|
||||
user as userTable,
|
||||
accounts as accountsTable,
|
||||
sessions as sessionsTable,
|
||||
verificationTokens as verificationTokensTable,
|
||||
} from "@/app/db/schema/users";
|
||||
|
||||
/**
|
||||
* Custom Drizzle adapter for NextAuth v4
|
||||
* ---------------------------------------
|
||||
* ✅ Works with bigint user IDs (no need for UUID migration)
|
||||
* ✅ Compatible with existing users table
|
||||
* ✅ Adds optional Database Session support
|
||||
*
|
||||
* By default, NextAuth uses JWT-based sessions (stateless).
|
||||
* The session functions here are only used if you enable:
|
||||
*
|
||||
* session: { strategy: "database" }
|
||||
*
|
||||
* Benefits of database sessions:
|
||||
* - Revocable sessions (logout from all devices)
|
||||
* - View active sessions per user
|
||||
* - Force re-login after role/permission changes
|
||||
* - Audit trail of login activity
|
||||
*/
|
||||
export default function DrizzleEmailAdapter(
|
||||
db: any,
|
||||
tables: {
|
||||
user: typeof userTable;
|
||||
accounts: typeof accountsTable;
|
||||
verificationTokens: typeof verificationTokensTable;
|
||||
sessions?: typeof sessionsTable;
|
||||
}
|
||||
): Adapter {
|
||||
const { user, accounts, verificationTokens, sessions } = tables;
|
||||
|
||||
//----------------------------------------------------------------------
|
||||
// Helpers
|
||||
//----------------------------------------------------------------------
|
||||
const normaliseEmail = (email: string) => email.trim().toLowerCase();
|
||||
|
||||
const toAdapterUser = (u: any): AdapterUser => ({
|
||||
id: String(u.id),
|
||||
dbId: String(u.id),
|
||||
email: u.email,
|
||||
name: u.firstName ?? null,
|
||||
image: u.image ?? null,
|
||||
emailVerified: u.emailVerified ?? null,
|
||||
});
|
||||
|
||||
//----------------------------------------------------------------------
|
||||
// Adapter methods
|
||||
//----------------------------------------------------------------------
|
||||
return {
|
||||
//------------------------------------------------------------------
|
||||
// USERS
|
||||
//------------------------------------------------------------------
|
||||
async createUser(
|
||||
newUser: Omit<AdapterUser, "id"> & { id?: string }
|
||||
): Promise<AdapterUser> {
|
||||
const [created] = await db
|
||||
.insert(user) // <-- now clearly the table
|
||||
.values({
|
||||
email: normaliseEmail(newUser.email!),
|
||||
firstName: newUser.name ?? null,
|
||||
image: newUser.image ?? null,
|
||||
emailVerified: newUser.emailVerified ?? null,
|
||||
})
|
||||
.returning();
|
||||
|
||||
return toAdapterUser(created);
|
||||
},
|
||||
|
||||
async getUser(id: string): Promise<AdapterUser | null> {
|
||||
const [found] = await db
|
||||
.select()
|
||||
.from(user)
|
||||
.where(eq(user.id, BigInt(id)));
|
||||
return found ? toAdapterUser(found) : null;
|
||||
},
|
||||
|
||||
async getUserByEmail(email: string): Promise<AdapterUser | null> {
|
||||
const [found] = await db
|
||||
.select()
|
||||
.from(user)
|
||||
.where(eq(user.email, normaliseEmail(email)));
|
||||
return found ? toAdapterUser(found) : null;
|
||||
},
|
||||
|
||||
async updateUser(
|
||||
u: Partial<AdapterUser> & Pick<AdapterUser, "id">
|
||||
): Promise<AdapterUser> {
|
||||
const [updated] = await db
|
||||
.update(user)
|
||||
.set({
|
||||
firstName: u.name ?? null,
|
||||
image: u.image ?? null,
|
||||
emailVerified: u.emailVerified ?? null,
|
||||
})
|
||||
.where(eq(user.id, BigInt(u.id)))
|
||||
.returning();
|
||||
|
||||
return toAdapterUser(updated);
|
||||
},
|
||||
async deleteUser(id: string): Promise<void> {
|
||||
await db.delete(user).where(eq(user.id, BigInt(id)));
|
||||
},
|
||||
|
||||
//------------------------------------------------------------------
|
||||
// ACCOUNTS (OAuth)
|
||||
//------------------------------------------------------------------
|
||||
async linkAccount(account: AdapterAccount): Promise<void> {
|
||||
try {
|
||||
await db.insert(accounts).values({
|
||||
userId: BigInt(account.userId),
|
||||
provider: account.provider,
|
||||
providerAccountId: account.providerAccountId,
|
||||
type: account.type,
|
||||
refresh_token: account.refresh_token ?? null,
|
||||
access_token: account.access_token ?? null,
|
||||
expires_at: account.expires_at ?? null,
|
||||
token_type: account.token_type ?? null,
|
||||
scope: account.scope ?? null,
|
||||
id_token: account.id_token ?? null,
|
||||
session_state: account.session_state ?? null,
|
||||
});
|
||||
} catch (err: any) {
|
||||
console.error("Error linking account:", err);
|
||||
throw err;
|
||||
}
|
||||
},
|
||||
|
||||
async unlinkAccount(params: {
|
||||
provider: string;
|
||||
providerAccountId: string;
|
||||
}): Promise<void> {
|
||||
await db
|
||||
.delete(accounts)
|
||||
.where(
|
||||
and(
|
||||
eq(accounts.provider, params.provider),
|
||||
eq(accounts.providerAccountId, params.providerAccountId)
|
||||
)
|
||||
);
|
||||
},
|
||||
|
||||
async getUserByAccount(params: {
|
||||
provider: string;
|
||||
providerAccountId: string;
|
||||
}): Promise<AdapterUser | null> {
|
||||
const [acc] = await db
|
||||
.select()
|
||||
.from(accounts)
|
||||
.where(
|
||||
and(
|
||||
eq(accounts.provider, params.provider),
|
||||
eq(accounts.providerAccountId, params.providerAccountId)
|
||||
)
|
||||
);
|
||||
|
||||
if (!acc) return null;
|
||||
|
||||
const [usr] = await db
|
||||
.select()
|
||||
.from(user)
|
||||
.where(eq(user.id, BigInt(acc.userId)));
|
||||
return usr ? toAdapterUser(usr) : null;
|
||||
},
|
||||
|
||||
//------------------------------------------------------------------
|
||||
// EMAIL VERIFICATION TOKENS
|
||||
//------------------------------------------------------------------
|
||||
async createVerificationToken(
|
||||
token: VerificationToken
|
||||
): Promise<VerificationToken> {
|
||||
const [created] = await db
|
||||
.insert(verificationTokens)
|
||||
.values({
|
||||
...token,
|
||||
expires: new Date(token.expires), // keep as Date
|
||||
})
|
||||
.returning();
|
||||
return created as VerificationToken;
|
||||
},
|
||||
|
||||
async useVerificationToken(params: {
|
||||
identifier: string;
|
||||
token: string;
|
||||
}): Promise<VerificationToken | null> {
|
||||
const [found] = await db
|
||||
.select()
|
||||
.from(verificationTokens)
|
||||
.where(
|
||||
and(
|
||||
eq(verificationTokens.identifier, params.identifier),
|
||||
eq(verificationTokens.token, params.token)
|
||||
)
|
||||
);
|
||||
|
||||
if (!found) return null;
|
||||
|
||||
await db
|
||||
.delete(verificationTokens)
|
||||
.where(
|
||||
and(
|
||||
eq(verificationTokens.identifier, params.identifier),
|
||||
eq(verificationTokens.token, params.token)
|
||||
)
|
||||
);
|
||||
|
||||
return found as VerificationToken;
|
||||
},
|
||||
|
||||
//------------------------------------------------------------------
|
||||
// SESSIONS (Optional – only used if session.strategy = "database")
|
||||
//------------------------------------------------------------------
|
||||
async createSession(session) {
|
||||
if (!sessions) return null;
|
||||
const [created] = await db
|
||||
.insert(sessions)
|
||||
.values({
|
||||
sessionToken: session.sessionToken,
|
||||
userId: BigInt(session.userId),
|
||||
expires: session.expires,
|
||||
})
|
||||
.returning();
|
||||
return created;
|
||||
},
|
||||
|
||||
async getSessionAndUser(sessionToken) {
|
||||
if (!sessions) return null;
|
||||
|
||||
const [session] = await db
|
||||
.select()
|
||||
.from(sessions)
|
||||
.where(eq(sessions.sessionToken, sessionToken));
|
||||
|
||||
if (!session) return null;
|
||||
|
||||
const [u] = await db
|
||||
.select()
|
||||
.from(user)
|
||||
.where(eq(user.id, BigInt(session.userId)));
|
||||
|
||||
if (!u) return null;
|
||||
|
||||
return {
|
||||
session,
|
||||
user: toAdapterUser(u),
|
||||
};
|
||||
},
|
||||
|
||||
async updateSession(session) {
|
||||
if (!sessions) return null;
|
||||
const [updated] = await db
|
||||
.update(sessions)
|
||||
.set({ expires: session.expires })
|
||||
.where(eq(sessions.sessionToken, session.sessionToken))
|
||||
.returning();
|
||||
return updated ?? null;
|
||||
},
|
||||
|
||||
async deleteSession(sessionToken) {
|
||||
if (!sessions) return;
|
||||
await db.delete(sessions).where(eq(sessions.sessionToken, sessionToken));
|
||||
},
|
||||
};
|
||||
}
|
||||
|
|
@ -1,29 +1,48 @@
|
|||
import NextAuth, { NextAuthOptions } from "next-auth";
|
||||
import GoogleProvider from "next-auth/providers/google";
|
||||
import AzureADB2CProvider from "next-auth/providers/azure-ad-b2c";
|
||||
import CredentialsProvider from "next-auth/providers/credentials";
|
||||
import EmailProvider from "next-auth/providers/email";
|
||||
import DrizzleEmailAdapter from "./DrizzleEmailAdapter";
|
||||
|
||||
import { db } from "@/app/db/db";
|
||||
import { user as userTable, User } from "@/app/db/schema/users";
|
||||
import {
|
||||
user as users,
|
||||
accounts,
|
||||
verificationTokens,
|
||||
} from "@/app/db/schema/users";
|
||||
import { eq } from "drizzle-orm";
|
||||
|
||||
const { GOOGLE_CLIENT_ID = "", GOOGLE_CLIENT_SECRET = "" } = process.env;
|
||||
// ------------------------------------------------------------------
|
||||
// Environment variables
|
||||
// ------------------------------------------------------------------
|
||||
const {
|
||||
GOOGLE_CLIENT_ID = "",
|
||||
GOOGLE_CLIENT_SECRET = "",
|
||||
AZURE_AD_B2C_TENANT_NAME = "",
|
||||
AZURE_AD_B2C_CLIENT_ID = "",
|
||||
AZURE_AD_B2C_CLIENT_SECRET = "",
|
||||
AZURE_AD_B2C_PRIMARY_USER_FLOW = "",
|
||||
EMAIL_SERVER_HOST = "",
|
||||
EMAIL_SERVER_PORT = "",
|
||||
EMAIL_SERVER_USER = "",
|
||||
EMAIL_SERVER_PASSWORD = "",
|
||||
EMAIL_FROM = "",
|
||||
} = process.env;
|
||||
|
||||
type OauthProvider = "google";
|
||||
|
||||
// TODO: handle token expiration
|
||||
// https://next-auth.js.org/v3/tutorials/refresh-token-rotation
|
||||
// propertly set options too
|
||||
// https://next-auth.js.org/configuration/options
|
||||
type OauthProvider = "google" | "azure-ad-b2c";
|
||||
|
||||
// ------------------------------------------------------------------
|
||||
// NextAuth configuration
|
||||
// ------------------------------------------------------------------
|
||||
export const AuthOptions: NextAuthOptions = {
|
||||
adapter: DrizzleEmailAdapter(db, {
|
||||
user: users,
|
||||
accounts,
|
||||
verificationTokens,
|
||||
}),
|
||||
|
||||
providers: [
|
||||
// ------------------ Google ------------------
|
||||
GoogleProvider({
|
||||
clientId: GOOGLE_CLIENT_ID,
|
||||
clientSecret: GOOGLE_CLIENT_SECRET,
|
||||
|
|
@ -35,6 +54,8 @@ export const AuthOptions: NextAuthOptions = {
|
|||
},
|
||||
},
|
||||
}),
|
||||
|
||||
// ------------------ Azure AD B2C ------------------
|
||||
AzureADB2CProvider({
|
||||
tenantId: AZURE_AD_B2C_TENANT_NAME,
|
||||
clientId: AZURE_AD_B2C_CLIENT_ID,
|
||||
|
|
@ -47,109 +68,124 @@ export const AuthOptions: NextAuthOptions = {
|
|||
},
|
||||
},
|
||||
}),
|
||||
CredentialsProvider({
|
||||
name: "Email Login",
|
||||
credentials: {
|
||||
email: {
|
||||
label: "Email",
|
||||
type: "email",
|
||||
|
||||
// ------------------ Email (SES Magic Link) ------------------
|
||||
EmailProvider({
|
||||
server: {
|
||||
host: EMAIL_SERVER_HOST,
|
||||
port: Number(EMAIL_SERVER_PORT),
|
||||
auth: {
|
||||
user: EMAIL_SERVER_USER,
|
||||
pass: EMAIL_SERVER_PASSWORD,
|
||||
},
|
||||
},
|
||||
async authorize(credentials, req) {
|
||||
if (!credentials || !credentials.email) {
|
||||
throw new Error("Email is required");
|
||||
}
|
||||
|
||||
const { email } = credentials;
|
||||
|
||||
// Query the database to find the user by email
|
||||
const dbUser = await db
|
||||
.select()
|
||||
.from(userTable)
|
||||
.where(eq(userTable.email, email));
|
||||
|
||||
// If the email exists, return the user object (no password check)
|
||||
if (dbUser.length === 1) {
|
||||
return {
|
||||
id: dbUser[0].id.toString(), // Convert bigint to string to avoid serialization issues
|
||||
email: dbUser[0].email,
|
||||
dbId: dbUser[0].id.toString(), // Ensure dbId is added and is a string
|
||||
};
|
||||
}
|
||||
|
||||
return null;
|
||||
},
|
||||
from: EMAIL_FROM,
|
||||
maxAge: 60 * 60, // magic link valid for 1 hour
|
||||
}),
|
||||
],
|
||||
|
||||
// ------------------------------------------------------------------
|
||||
// Pages
|
||||
// ------------------------------------------------------------------
|
||||
pages: {
|
||||
signIn: "/",
|
||||
signIn: "/", // your landing/login page
|
||||
verifyRequest: "/auth/verify-request",
|
||||
error: "/auth/error",
|
||||
},
|
||||
|
||||
// ------------------------------------------------------------------
|
||||
// Callbacks
|
||||
// ------------------------------------------------------------------
|
||||
callbacks: {
|
||||
/**
|
||||
* Sign in callback — ensures user exists and links OAuth provider if needed
|
||||
*/
|
||||
async signIn({ user, account }) {
|
||||
try {
|
||||
if (user === null || user.email === null) {
|
||||
return "/beta";
|
||||
}
|
||||
const dbUser: User[] = await db
|
||||
.select()
|
||||
.from(userTable)
|
||||
.where(eq(userTable.email, String(user.email)));
|
||||
if (!user?.email) return false;
|
||||
|
||||
if (dbUser.length > 1) {
|
||||
console.error(`Multiple users found with email ${user.email}`);
|
||||
const normalisedEmail = user.email.toLowerCase();
|
||||
|
||||
// Fetch the user (NextAuth will have created them already if new)
|
||||
const [dbUser] = await db
|
||||
.select()
|
||||
.from(users)
|
||||
.where(eq(users.email, normalisedEmail));
|
||||
|
||||
if (!dbUser) {
|
||||
console.warn(`User not found for ${normalisedEmail} after sign-in.`);
|
||||
return false;
|
||||
}
|
||||
|
||||
if (dbUser.length === 0 || account === null) {
|
||||
return "/beta";
|
||||
}
|
||||
|
||||
if (!dbUser[0].oauthId) {
|
||||
// We make a second query to populate the oauthId and oauthProvider
|
||||
console.log("Updating user with oauthId and oauthProvider");
|
||||
// Link OAuth ID if missing (helps for older accounts)
|
||||
if (account && !dbUser.oauthId) {
|
||||
const provider = account.provider as OauthProvider;
|
||||
|
||||
await db
|
||||
.update(userTable)
|
||||
.update(users)
|
||||
.set({ oauthId: user.id, oauthProvider: provider })
|
||||
.where(eq(userTable.email, String(user.email)));
|
||||
|
||||
console.log("Updated oauthId and oauthProvider");
|
||||
.where(eq(users.email, normalisedEmail));
|
||||
}
|
||||
|
||||
// Always update last login timestamp
|
||||
await db
|
||||
.update(users)
|
||||
.set({ lastLogin: new Date() })
|
||||
.where(eq(users.id, dbUser.id));
|
||||
|
||||
// Pass bigint ID into NextAuth session/jwt
|
||||
user.dbId = dbUser.id.toString();
|
||||
|
||||
// If the user isn't onboarded yet, redirect to onboarding
|
||||
if (!dbUser.onboarded) {
|
||||
return "/onboarding";
|
||||
}
|
||||
|
||||
// Set the user's ID from your database
|
||||
// Because bigint isn't serializable, we need to convert it to a string
|
||||
user.dbId = dbUser[0].id.toString();
|
||||
return true;
|
||||
} catch (error) {
|
||||
console.error("Error during sign-in: ", error);
|
||||
console.error("Error during sign-in:", error);
|
||||
return false;
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Persist dbId in the JWT so it’s available in sessions
|
||||
*/
|
||||
async jwt({ token, user }) {
|
||||
// This is executed whenever a JWT is created or refreshed.
|
||||
// `user` is the object returned from `signIn` callback and
|
||||
// is only available during sign in, which is why we need to
|
||||
// store the id in the token and then read it back into the session.
|
||||
if (user?.dbId) {
|
||||
token.dbId = user.dbId;
|
||||
}
|
||||
return token;
|
||||
},
|
||||
|
||||
/**
|
||||
* Attach dbId to session.user
|
||||
*/
|
||||
async session({ session, token }) {
|
||||
if (session?.user) {
|
||||
if (session.user && token.dbId) {
|
||||
session.user.dbId = token.dbId;
|
||||
}
|
||||
|
||||
return session;
|
||||
},
|
||||
|
||||
/**
|
||||
* Redirect users after login
|
||||
*/
|
||||
async redirect({ baseUrl }) {
|
||||
const redirectUrl = baseUrl + "/home";
|
||||
return redirectUrl;
|
||||
return `${baseUrl}/home`;
|
||||
},
|
||||
},
|
||||
|
||||
// ------------------------------------------------------------------
|
||||
// General options
|
||||
// ------------------------------------------------------------------
|
||||
session: {
|
||||
strategy: "jwt", // lightweight, avoids DB session writes
|
||||
},
|
||||
jwt: {
|
||||
maxAge: 60 * 60 * 24 * 30, // 30 days
|
||||
},
|
||||
debug: false,
|
||||
};
|
||||
|
||||
const handler = NextAuth(AuthOptions);
|
||||
|
||||
export { handler as GET, handler as POST };
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import { db } from "@/app/db/db";
|
||||
import { NextRequest, NextResponse } from "next/server";
|
||||
import { portfolio, portfolioUsers} from "@/app/db/schema/portfolio";
|
||||
import { portfolio, portfolioUsers } from "@/app/db/schema/portfolio";
|
||||
import { user } from "@/app/db/schema/users";
|
||||
import {
|
||||
recommendation,
|
||||
|
|
@ -18,7 +18,6 @@ import { eq, inArray, Name } from "drizzle-orm";
|
|||
import { z } from "zod";
|
||||
import { ROLE_OPTIONS } from "@/app/portfolio/[slug]/(portfolio)/settings/roles";
|
||||
|
||||
|
||||
// Get colloborators (users) that have access to the portfolio
|
||||
export async function GET(
|
||||
_req: NextRequest,
|
||||
|
|
@ -41,7 +40,7 @@ export async function GET(
|
|||
|
||||
// Explicitly normalize BigInts to strings
|
||||
const collaborators = rows.map((r) => ({
|
||||
portfolioUserId: r.portfolioUserId ? r.portfolioUserId.toString(): null,
|
||||
portfolioUserId: r.portfolioUserId ? r.portfolioUserId.toString() : null,
|
||||
userId: r.userId ? r.userId.toString() : null,
|
||||
role: r.role,
|
||||
name: r.name ?? null,
|
||||
|
|
@ -58,7 +57,6 @@ export async function GET(
|
|||
}
|
||||
}
|
||||
|
||||
|
||||
// PUT: update a collaborator’s role
|
||||
export async function PUT(
|
||||
req: NextRequest,
|
||||
|
|
@ -110,7 +108,7 @@ export async function POST(
|
|||
const bodySchema = z.object({
|
||||
email: z.string().email(),
|
||||
role: z.enum(ROLE_OPTIONS),
|
||||
name: z.string()
|
||||
name: z.string(),
|
||||
});
|
||||
|
||||
let body: z.infer<typeof bodySchema>;
|
||||
|
|
@ -138,10 +136,14 @@ export async function POST(
|
|||
// If you’re on Postgres, this is ideal:
|
||||
const inserted = await db
|
||||
.insert(user)
|
||||
.values({ email: body.email, firstName: body.name, oauthProvider: "credentials" })
|
||||
.values({
|
||||
email: body.email,
|
||||
firstName: body.name,
|
||||
oauthProvider: "credentials",
|
||||
})
|
||||
.onConflictDoNothing() // relies on a UNIQUE(email) constraint
|
||||
.returning({ id: user.id });
|
||||
|
||||
|
||||
if (inserted.length > 0) {
|
||||
createdUserId = inserted[0].id;
|
||||
} else {
|
||||
|
|
@ -202,4 +204,4 @@ export async function POST(
|
|||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
14
src/app/auth/verify-request/page.tsx
Normal file
14
src/app/auth/verify-request/page.tsx
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
export default function VerifyRequestPage() {
|
||||
return (
|
||||
<div className="flex h-screen items-center justify-center bg-gray-50">
|
||||
<div className="text-center">
|
||||
<h1 className="text-2xl font-semibold text-brandblue mb-2">
|
||||
Check your email
|
||||
</h1>
|
||||
<p className="text-gray-600">
|
||||
We’ve sent you a sign-in link. Click it to finish logging in.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
@ -1,3 +0,0 @@
|
|||
export default function Beta() {
|
||||
return <div>You do not have access to this application currently</div>;
|
||||
}
|
||||
|
|
@ -13,17 +13,20 @@ export default function EmailSignInButton({
|
|||
}) {
|
||||
const [email, setEmail] = useState("");
|
||||
const [error, setError] = useState(initialError);
|
||||
const [status, setStatus] = useState<"idle" | "sending" | "sent">("idle");
|
||||
|
||||
const handleSubmit = async (e: { preventDefault: () => void }) => {
|
||||
e.preventDefault();
|
||||
const res = await signIn("credentials", {
|
||||
email,
|
||||
});
|
||||
setStatus("sending");
|
||||
|
||||
const res = await signIn("email", { email, redirect: false });
|
||||
|
||||
if (res?.error) {
|
||||
setError("You are not a valid user.");
|
||||
setStatus("idle");
|
||||
} else {
|
||||
console.log("Login successful");
|
||||
setError(undefined);
|
||||
setStatus("sent");
|
||||
}
|
||||
};
|
||||
|
||||
|
|
@ -31,40 +34,42 @@ export default function EmailSignInButton({
|
|||
target: { value: SetStateAction<string> };
|
||||
}) => {
|
||||
setEmail(e.target.value);
|
||||
if (error) {
|
||||
setError(undefined); // Clear the error when the user starts typing
|
||||
}
|
||||
if (error) setError(undefined);
|
||||
};
|
||||
|
||||
// Sync initial error state with server-side error prop
|
||||
useEffect(() => {
|
||||
setError(initialError);
|
||||
}, [initialError]);
|
||||
// Keep server-side error synced
|
||||
useEffect(() => setError(initialError), [initialError]);
|
||||
|
||||
return (
|
||||
<form onSubmit={handleSubmit} className="w-full">
|
||||
{/* Wrapper to control width and layout */}
|
||||
<div className="flex items-center w-full space-x-1">
|
||||
{/* Email input field using shadcn input */}
|
||||
<Input
|
||||
type="email"
|
||||
value={email}
|
||||
onChange={handleEmailChange}
|
||||
placeholder="Enter your email"
|
||||
required
|
||||
className="flex-1 h-10 rounded-lg border-gray-300" // Full width input
|
||||
className="flex-1 h-10 rounded-lg border-gray-300"
|
||||
/>
|
||||
<Button
|
||||
type="submit"
|
||||
className="h-10 w-10 bg-brandblue text-white hover:bg-hoverblue rounded-lg flex items-center justify-center" // Fixed size button
|
||||
disabled={status === "sending"}
|
||||
className="h-10 w-10 bg-brandblue text-white hover:bg-hoverblue rounded-lg flex items-center justify-center"
|
||||
>
|
||||
<ChevronRightIcon className="h-5 w-5" />
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{/* Reserve space for the error message */}
|
||||
<div className="min-h-[3rem] text-center">
|
||||
{error && <p className="text-red-500">You are not a valid user.</p>}
|
||||
{error && <p className="text-red-500">{error}</p>}
|
||||
{status === "sent" && (
|
||||
<p className="text-green-600">
|
||||
A login link has been sent to your email.
|
||||
</p>
|
||||
)}
|
||||
{status === "sending" && (
|
||||
<p className="text-gray-500">Sending login link...</p>
|
||||
)}
|
||||
</div>
|
||||
</form>
|
||||
);
|
||||
35
src/app/db/migrations/0117_colossal_bastion.sql
Normal file
35
src/app/db/migrations/0117_colossal_bastion.sql
Normal file
|
|
@ -0,0 +1,35 @@
|
|||
CREATE TABLE "account" (
|
||||
"userId" bigint NOT NULL,
|
||||
"type" text NOT NULL,
|
||||
"provider" text NOT NULL,
|
||||
"providerAccountId" text NOT NULL,
|
||||
"refresh_token" text,
|
||||
"access_token" text,
|
||||
"expires_at" integer,
|
||||
"token_type" text,
|
||||
"scope" text,
|
||||
"id_token" text,
|
||||
"session_state" text,
|
||||
CONSTRAINT "account_provider_providerAccountId_pk" PRIMARY KEY("provider","providerAccountId")
|
||||
);
|
||||
--> statement-breakpoint
|
||||
CREATE TABLE "session" (
|
||||
"sessionToken" text PRIMARY KEY NOT NULL,
|
||||
"userId" bigint NOT NULL,
|
||||
"expires" timestamp NOT NULL
|
||||
);
|
||||
--> statement-breakpoint
|
||||
CREATE TABLE "verificationToken" (
|
||||
"identifier" text NOT NULL,
|
||||
"token" text NOT NULL,
|
||||
"expires" timestamp NOT NULL,
|
||||
CONSTRAINT "verificationToken_identifier_token_pk" PRIMARY KEY("identifier","token")
|
||||
);
|
||||
--> statement-breakpoint
|
||||
ALTER TABLE "user" ADD COLUMN "emailVerified" timestamp;--> statement-breakpoint
|
||||
ALTER TABLE "user" ADD COLUMN "image" text;--> statement-breakpoint
|
||||
ALTER TABLE "user" ADD COLUMN "onboarded" boolean DEFAULT false NOT NULL;--> statement-breakpoint
|
||||
ALTER TABLE "user" ADD COLUMN "last_login" timestamp;--> statement-breakpoint
|
||||
ALTER TABLE "account" ADD CONSTRAINT "account_userId_user_id_fk" FOREIGN KEY ("userId") REFERENCES "public"."user"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
|
||||
ALTER TABLE "session" ADD CONSTRAINT "session_userId_user_id_fk" FOREIGN KEY ("userId") REFERENCES "public"."user"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
|
||||
ALTER TABLE "user" ADD CONSTRAINT "user_email_unique" UNIQUE("email");
|
||||
3587
src/app/db/migrations/meta/0117_snapshot.json
Normal file
3587
src/app/db/migrations/meta/0117_snapshot.json
Normal file
File diff suppressed because it is too large
Load diff
|
|
@ -820,6 +820,13 @@
|
|||
"when": 1759069966418,
|
||||
"tag": "0116_spotty_leech",
|
||||
"breakpoints": true
|
||||
},
|
||||
{
|
||||
"idx": 117,
|
||||
"version": "7",
|
||||
"when": 1760191704756,
|
||||
"tag": "0117_colossal_bastion",
|
||||
"breakpoints": true
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
@ -1,27 +1,87 @@
|
|||
import { bigserial, text, timestamp, pgTable } from "drizzle-orm/pg-core";
|
||||
import {
|
||||
bigint,
|
||||
bigserial,
|
||||
text,
|
||||
timestamp,
|
||||
pgTable,
|
||||
primaryKey,
|
||||
integer,
|
||||
boolean,
|
||||
} from "drizzle-orm/pg-core";
|
||||
import { InferModel } from "drizzle-orm";
|
||||
|
||||
// -------------------------
|
||||
// USERS
|
||||
// -------------------------
|
||||
export const user = pgTable("user", {
|
||||
id: bigserial("id", { mode: "bigint" }).primaryKey(),
|
||||
firstName: text("firstName"),
|
||||
// At the moment, Drizzle doesn't support unique constraints
|
||||
email: text("email").notNull(),
|
||||
email: text("email").notNull().unique(),
|
||||
emailVerified: timestamp("emailVerified", { mode: "date" }),
|
||||
oauthId: text("oauth_id"),
|
||||
oauthProvider: text("oauth_provider").$type<"google" | "credentials">(),
|
||||
// role: text("role").$type<"admin" | "write" | "read">(),
|
||||
createdAt: timestamp("created_at", {
|
||||
precision: 6,
|
||||
withTimezone: true,
|
||||
})
|
||||
oauthProvider: text("oauth_provider").$type<
|
||||
"google" | "credentials" | "azure-ad-b2c"
|
||||
>(),
|
||||
image: text("image"),
|
||||
onboarded: boolean("onboarded").default(false).notNull(),
|
||||
lastLogin: timestamp("last_login", { mode: "date" }),
|
||||
createdAt: timestamp("created_at", { precision: 6, withTimezone: true })
|
||||
.defaultNow()
|
||||
.notNull(),
|
||||
updatedAt: timestamp("updated_at", {
|
||||
precision: 6,
|
||||
withTimezone: true,
|
||||
})
|
||||
updatedAt: timestamp("updated_at", { precision: 6, withTimezone: true })
|
||||
.defaultNow()
|
||||
.notNull(),
|
||||
});
|
||||
|
||||
// -------------------------
|
||||
// ACCOUNTS (OAuth providers)
|
||||
// -------------------------
|
||||
export const accounts = pgTable(
|
||||
"account",
|
||||
{
|
||||
userId: bigint("userId", { mode: "bigint" })
|
||||
.notNull()
|
||||
.references(() => user.id, { onDelete: "cascade" }),
|
||||
type: text("type").$type<"oauth" | "email" | "credentials">().notNull(),
|
||||
provider: text("provider").notNull(),
|
||||
providerAccountId: text("providerAccountId").notNull(),
|
||||
refresh_token: text("refresh_token"),
|
||||
access_token: text("access_token"),
|
||||
expires_at: integer("expires_at"),
|
||||
token_type: text("token_type"),
|
||||
scope: text("scope"),
|
||||
id_token: text("id_token"),
|
||||
session_state: text("session_state"),
|
||||
},
|
||||
(account) => [
|
||||
primaryKey({ columns: [account.provider, account.providerAccountId] }),
|
||||
]
|
||||
);
|
||||
|
||||
export const sessions = pgTable("session", {
|
||||
sessionToken: text("sessionToken").primaryKey(),
|
||||
userId: bigint("userId", { mode: "bigint" })
|
||||
.notNull()
|
||||
.references(() => user.id, { onDelete: "cascade" }),
|
||||
expires: timestamp("expires", { mode: "date" }).notNull(),
|
||||
});
|
||||
|
||||
export const verificationTokens = pgTable(
|
||||
"verificationToken",
|
||||
{
|
||||
identifier: text("identifier").notNull(),
|
||||
token: text("token").notNull(),
|
||||
expires: timestamp("expires", { mode: "date" }).notNull(),
|
||||
},
|
||||
(vt) => [primaryKey({ columns: [vt.identifier, vt.token] })]
|
||||
);
|
||||
|
||||
// -------------------------
|
||||
// Types
|
||||
// -------------------------
|
||||
export type User = InferModel<typeof user, "select">;
|
||||
export type NewUser = InferModel<typeof user, "insert">;
|
||||
|
||||
export type Account = InferModel<typeof accounts, "select">;
|
||||
export type Session = InferModel<typeof sessions, "select">;
|
||||
export type VerificationToken = InferModel<typeof verificationTokens, "select">;
|
||||
|
|
|
|||
50
src/app/onboarding/page.tsx
Normal file
50
src/app/onboarding/page.tsx
Normal file
|
|
@ -0,0 +1,50 @@
|
|||
"use client";
|
||||
|
||||
import { useRouter } from "next/navigation";
|
||||
import { useState } from "react";
|
||||
import { Button } from "@/app/shadcn_components/ui/button";
|
||||
import { Input } from "@/app/shadcn_components/ui/input";
|
||||
|
||||
export default function OnboardingPage() {
|
||||
const router = useRouter();
|
||||
const [name, setName] = useState("");
|
||||
|
||||
async function handleSubmit(e: React.FormEvent) {
|
||||
e.preventDefault();
|
||||
|
||||
await fetch("/api/user/onboard", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ name }),
|
||||
});
|
||||
|
||||
router.push("/home");
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex h-screen items-center justify-center bg-gray-50">
|
||||
<form
|
||||
onSubmit={handleSubmit}
|
||||
className="p-8 bg-white rounded-xl shadow-md w-full max-w-md space-y-4"
|
||||
>
|
||||
<h1 className="text-xl font-semibold text-brandblue">Welcome!</h1>
|
||||
<p className="text-gray-600">
|
||||
Let's complete your profile to get started.
|
||||
</p>
|
||||
|
||||
<Input
|
||||
value={name}
|
||||
onChange={(e) => setName(e.target.value)}
|
||||
placeholder="Your full name"
|
||||
required
|
||||
/>
|
||||
<Button
|
||||
type="submit"
|
||||
className="w-full bg-brandblue hover:bg-hoverblue"
|
||||
>
|
||||
Continue
|
||||
</Button>
|
||||
</form>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
@ -2,15 +2,13 @@ import { getServerSession } from "next-auth/next";
|
|||
import { AuthOptions } from "./api/auth/[...nextauth]/route";
|
||||
import GoogleSignInButton from "./components/signin/GoogleSignInButton";
|
||||
import MicrosoftSignInButton from "./components/signin/MicrosoftSignInButton";
|
||||
import EmailSignInButton from "./components/signin/CredentialsButton";
|
||||
import EmailSignInButton from "./components/signin/EmailSignInButton";
|
||||
import { redirect } from "next/navigation";
|
||||
import Image from "next/image";
|
||||
|
||||
export default async function Home(
|
||||
props: {
|
||||
searchParams: Promise<{ error?: string }>;
|
||||
}
|
||||
) {
|
||||
export default async function Home(props: {
|
||||
searchParams: Promise<{ error?: string }>;
|
||||
}) {
|
||||
const searchParams = await props.searchParams;
|
||||
const session = await getServerSession(AuthOptions);
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue