auth done
This commit is contained in:
parent
aba29922c7
commit
429cdb7094
|
|
@ -11,5 +11,3 @@ const nextConfig: NextConfig = {
|
|||
};
|
||||
|
||||
export default nextConfig;
|
||||
|
||||
import('@opennextjs/cloudflare').then(m => m.initOpenNextCloudflareForDev());
|
||||
|
|
|
|||
328
package-lock.json
generated
328
package-lock.json
generated
|
|
@ -9,8 +9,10 @@
|
|||
"version": "0.1.0",
|
||||
"dependencies": {
|
||||
"next": "16.2.1",
|
||||
"next-auth": "^4.24.11",
|
||||
"react": "19.2.4",
|
||||
"react-dom": "19.2.4"
|
||||
"react-dom": "19.2.4",
|
||||
"zustand": "^5.0.8"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@opennextjs/aws": "^3.9.16",
|
||||
|
|
@ -102,9 +104,6 @@
|
|||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"libc": [
|
||||
"glibc"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
|
|
@ -122,9 +121,6 @@
|
|||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"libc": [
|
||||
"musl"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
|
|
@ -142,9 +138,6 @@
|
|||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"libc": [
|
||||
"glibc"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
|
|
@ -162,9 +155,6 @@
|
|||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"libc": [
|
||||
"musl"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
|
|
@ -1635,6 +1625,14 @@
|
|||
"node": ">=6.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@babel/runtime": {
|
||||
"version": "7.29.2",
|
||||
"resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.29.2.tgz",
|
||||
"integrity": "sha512-JiDShH45zKHWyGe4ZNVRrCjBz8Nh9TMmZG1kh4QTK8hCBTWBi8Da+i7s1fJw7/lYpM4ccepSNfqzZ/QvABBi5g==",
|
||||
"engines": {
|
||||
"node": ">=6.9.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@babel/template": {
|
||||
"version": "7.28.6",
|
||||
"resolved": "https://registry.npmjs.org/@babel/template/-/template-7.28.6.tgz",
|
||||
|
|
@ -1931,6 +1929,7 @@
|
|||
"version": "1.9.1",
|
||||
"resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.9.1.tgz",
|
||||
"integrity": "sha512-VYi5+ZVLhpgK4hQ0TAjiQiZ6ol0oe4mBx7mVv7IflsiEp0OWoVsp/+f9Vc1hOhE0TtkORVrI1GvzyreqpgWtkA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
|
|
@ -2603,6 +2602,7 @@
|
|||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
|
|
@ -2625,6 +2625,7 @@
|
|||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
|
|
@ -2647,6 +2648,7 @@
|
|||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "LGPL-3.0-or-later",
|
||||
"optional": true,
|
||||
"os": [
|
||||
|
|
@ -2663,6 +2665,7 @@
|
|||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "LGPL-3.0-or-later",
|
||||
"optional": true,
|
||||
"os": [
|
||||
|
|
@ -2679,9 +2682,7 @@
|
|||
"cpu": [
|
||||
"arm"
|
||||
],
|
||||
"libc": [
|
||||
"glibc"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "LGPL-3.0-or-later",
|
||||
"optional": true,
|
||||
"os": [
|
||||
|
|
@ -2698,9 +2699,7 @@
|
|||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"libc": [
|
||||
"glibc"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "LGPL-3.0-or-later",
|
||||
"optional": true,
|
||||
"os": [
|
||||
|
|
@ -2717,9 +2716,7 @@
|
|||
"cpu": [
|
||||
"ppc64"
|
||||
],
|
||||
"libc": [
|
||||
"glibc"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "LGPL-3.0-or-later",
|
||||
"optional": true,
|
||||
"os": [
|
||||
|
|
@ -2736,9 +2733,7 @@
|
|||
"cpu": [
|
||||
"riscv64"
|
||||
],
|
||||
"libc": [
|
||||
"glibc"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "LGPL-3.0-or-later",
|
||||
"optional": true,
|
||||
"os": [
|
||||
|
|
@ -2755,9 +2750,7 @@
|
|||
"cpu": [
|
||||
"s390x"
|
||||
],
|
||||
"libc": [
|
||||
"glibc"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "LGPL-3.0-or-later",
|
||||
"optional": true,
|
||||
"os": [
|
||||
|
|
@ -2774,9 +2767,7 @@
|
|||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"libc": [
|
||||
"glibc"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "LGPL-3.0-or-later",
|
||||
"optional": true,
|
||||
"os": [
|
||||
|
|
@ -2793,9 +2784,7 @@
|
|||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"libc": [
|
||||
"musl"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "LGPL-3.0-or-later",
|
||||
"optional": true,
|
||||
"os": [
|
||||
|
|
@ -2812,9 +2801,7 @@
|
|||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"libc": [
|
||||
"musl"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "LGPL-3.0-or-later",
|
||||
"optional": true,
|
||||
"os": [
|
||||
|
|
@ -2831,9 +2818,7 @@
|
|||
"cpu": [
|
||||
"arm"
|
||||
],
|
||||
"libc": [
|
||||
"glibc"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
|
|
@ -2856,9 +2841,7 @@
|
|||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"libc": [
|
||||
"glibc"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
|
|
@ -2881,9 +2864,7 @@
|
|||
"cpu": [
|
||||
"ppc64"
|
||||
],
|
||||
"libc": [
|
||||
"glibc"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
|
|
@ -2906,9 +2887,7 @@
|
|||
"cpu": [
|
||||
"riscv64"
|
||||
],
|
||||
"libc": [
|
||||
"glibc"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
|
|
@ -2931,9 +2910,7 @@
|
|||
"cpu": [
|
||||
"s390x"
|
||||
],
|
||||
"libc": [
|
||||
"glibc"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
|
|
@ -2956,9 +2933,7 @@
|
|||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"libc": [
|
||||
"glibc"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
|
|
@ -2981,9 +2956,7 @@
|
|||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"libc": [
|
||||
"musl"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
|
|
@ -3006,9 +2979,7 @@
|
|||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"libc": [
|
||||
"musl"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
|
|
@ -3031,6 +3002,7 @@
|
|||
"cpu": [
|
||||
"wasm32"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "Apache-2.0 AND LGPL-3.0-or-later AND MIT",
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
|
|
@ -3050,6 +3022,7 @@
|
|||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "Apache-2.0 AND LGPL-3.0-or-later",
|
||||
"optional": true,
|
||||
"os": [
|
||||
|
|
@ -3069,6 +3042,7 @@
|
|||
"cpu": [
|
||||
"ia32"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "Apache-2.0 AND LGPL-3.0-or-later",
|
||||
"optional": true,
|
||||
"os": [
|
||||
|
|
@ -3088,6 +3062,7 @@
|
|||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "Apache-2.0 AND LGPL-3.0-or-later",
|
||||
"optional": true,
|
||||
"os": [
|
||||
|
|
@ -3239,9 +3214,6 @@
|
|||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"libc": [
|
||||
"glibc"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
|
|
@ -3258,9 +3230,6 @@
|
|||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"libc": [
|
||||
"musl"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
|
|
@ -3277,9 +3246,6 @@
|
|||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"libc": [
|
||||
"glibc"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
|
|
@ -3296,9 +3262,6 @@
|
|||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"libc": [
|
||||
"musl"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
|
|
@ -3632,6 +3595,14 @@
|
|||
"wrangler": "^4.65.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@panva/hkdf": {
|
||||
"version": "1.2.1",
|
||||
"resolved": "https://registry.npmjs.org/@panva/hkdf/-/hkdf-1.2.1.tgz",
|
||||
"integrity": "sha512-6oclG6Y3PiDFcoyk8srjLfVKyMfVCKJ27JwNPViuXziFpmdz+MZnZN/aKY0JGXgYuO/VghU0jcOAZgWXZ1Dmrw==",
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/panva"
|
||||
}
|
||||
},
|
||||
"node_modules/@poppinss/colors": {
|
||||
"version": "4.1.6",
|
||||
"resolved": "https://registry.npmjs.org/@poppinss/colors/-/colors-4.1.6.tgz",
|
||||
|
|
@ -4627,9 +4598,6 @@
|
|||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"libc": [
|
||||
"glibc"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
|
|
@ -4647,9 +4615,6 @@
|
|||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"libc": [
|
||||
"musl"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
|
|
@ -4667,9 +4632,6 @@
|
|||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"libc": [
|
||||
"glibc"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
|
|
@ -4687,9 +4649,6 @@
|
|||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"libc": [
|
||||
"musl"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
|
|
@ -4841,7 +4800,7 @@
|
|||
"version": "19.2.14",
|
||||
"resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.14.tgz",
|
||||
"integrity": "sha512-ilcTH/UniCkMdtexkoCN0bI7pMcJDvmQFPvuPvmEaYA/NSfFTAgdUSLAoVjaRJm7+6PvcM+q1zYOwS4wTYMF9w==",
|
||||
"dev": true,
|
||||
"devOptional": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"csstype": "^3.2.2"
|
||||
|
|
@ -5258,9 +5217,6 @@
|
|||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"libc": [
|
||||
"glibc"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
|
|
@ -5275,9 +5231,6 @@
|
|||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"libc": [
|
||||
"musl"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
|
|
@ -5292,9 +5245,6 @@
|
|||
"ppc64"
|
||||
],
|
||||
"dev": true,
|
||||
"libc": [
|
||||
"glibc"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
|
|
@ -5309,9 +5259,6 @@
|
|||
"riscv64"
|
||||
],
|
||||
"dev": true,
|
||||
"libc": [
|
||||
"glibc"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
|
|
@ -5326,9 +5273,6 @@
|
|||
"riscv64"
|
||||
],
|
||||
"dev": true,
|
||||
"libc": [
|
||||
"musl"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
|
|
@ -5343,9 +5287,6 @@
|
|||
"s390x"
|
||||
],
|
||||
"dev": true,
|
||||
"libc": [
|
||||
"glibc"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
|
|
@ -5360,9 +5301,6 @@
|
|||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"libc": [
|
||||
"glibc"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
|
|
@ -5377,9 +5315,6 @@
|
|||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"libc": [
|
||||
"musl"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
|
|
@ -6263,7 +6198,7 @@
|
|||
"version": "3.2.3",
|
||||
"resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz",
|
||||
"integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==",
|
||||
"dev": true,
|
||||
"devOptional": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/damerau-levenshtein": {
|
||||
|
|
@ -8717,6 +8652,14 @@
|
|||
"jiti": "lib/jiti-cli.mjs"
|
||||
}
|
||||
},
|
||||
"node_modules/jose": {
|
||||
"version": "4.15.9",
|
||||
"resolved": "https://registry.npmjs.org/jose/-/jose-4.15.9.tgz",
|
||||
"integrity": "sha512-1vUQX+IdDMVPj4k8kOxgUqlcK518yluMuGZwqlr44FS1ppZB/5GWh4rZG89erpOBOJjU/OBsnCVFfapsRz6nEA==",
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/panva"
|
||||
}
|
||||
},
|
||||
"node_modules/js-tokens": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
|
||||
|
|
@ -8997,9 +8940,6 @@
|
|||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"libc": [
|
||||
"glibc"
|
||||
],
|
||||
"license": "MPL-2.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
|
|
@ -9021,9 +8961,6 @@
|
|||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"libc": [
|
||||
"musl"
|
||||
],
|
||||
"license": "MPL-2.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
|
|
@ -9045,9 +8982,6 @@
|
|||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"libc": [
|
||||
"glibc"
|
||||
],
|
||||
"license": "MPL-2.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
|
|
@ -9069,9 +9003,6 @@
|
|||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"libc": [
|
||||
"musl"
|
||||
],
|
||||
"license": "MPL-2.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
|
|
@ -9472,6 +9403,45 @@
|
|||
}
|
||||
}
|
||||
},
|
||||
"node_modules/next-auth": {
|
||||
"version": "4.24.13",
|
||||
"resolved": "https://registry.npmjs.org/next-auth/-/next-auth-4.24.13.tgz",
|
||||
"integrity": "sha512-sgObCfcfL7BzIK76SS5TnQtc3yo2Oifp/yIpfv6fMfeBOiBJkDWF3A2y9+yqnmJ4JKc2C+nMjSjmgDeTwgN1rQ==",
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.20.13",
|
||||
"@panva/hkdf": "^1.0.2",
|
||||
"cookie": "^0.7.0",
|
||||
"jose": "^4.15.5",
|
||||
"oauth": "^0.9.15",
|
||||
"openid-client": "^5.4.0",
|
||||
"preact": "^10.6.3",
|
||||
"preact-render-to-string": "^5.1.19",
|
||||
"uuid": "^8.3.2"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@auth/core": "0.34.3",
|
||||
"next": "^12.2.5 || ^13 || ^14 || ^15 || ^16",
|
||||
"nodemailer": "^7.0.7",
|
||||
"react": "^17.0.2 || ^18 || ^19",
|
||||
"react-dom": "^17.0.2 || ^18 || ^19"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@auth/core": {
|
||||
"optional": true
|
||||
},
|
||||
"nodemailer": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/next-auth/node_modules/cookie": {
|
||||
"version": "0.7.2",
|
||||
"resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz",
|
||||
"integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==",
|
||||
"engines": {
|
||||
"node": ">= 0.6"
|
||||
}
|
||||
},
|
||||
"node_modules/next/node_modules/postcss": {
|
||||
"version": "8.4.31",
|
||||
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.31.tgz",
|
||||
|
|
@ -9581,6 +9551,11 @@
|
|||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/oauth": {
|
||||
"version": "0.9.15",
|
||||
"resolved": "https://registry.npmjs.org/oauth/-/oauth-0.9.15.tgz",
|
||||
"integrity": "sha512-a5ERWK1kh38ExDEfoO6qUHJb32rd7aYmPHuyCu3Fta/cnICvYmgd2uhuKXvPD+PXB+gCEYYEaQdIRAjCOwAKNA=="
|
||||
},
|
||||
"node_modules/object-assign": {
|
||||
"version": "4.1.1",
|
||||
"resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz",
|
||||
|
|
@ -9591,6 +9566,14 @@
|
|||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/object-hash": {
|
||||
"version": "2.2.0",
|
||||
"resolved": "https://registry.npmjs.org/object-hash/-/object-hash-2.2.0.tgz",
|
||||
"integrity": "sha512-gScRMn0bS5fH+IuwyIFgnh9zBdo4DV+6GhygmWM9HyNJSgS0hScp1f5vjtm7oIIOiT9trXrShAkLFSc2IqKNgw==",
|
||||
"engines": {
|
||||
"node": ">= 6"
|
||||
}
|
||||
},
|
||||
"node_modules/object-inspect": {
|
||||
"version": "1.13.4",
|
||||
"resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz",
|
||||
|
|
@ -9721,6 +9704,14 @@
|
|||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/oidc-token-hash": {
|
||||
"version": "5.2.0",
|
||||
"resolved": "https://registry.npmjs.org/oidc-token-hash/-/oidc-token-hash-5.2.0.tgz",
|
||||
"integrity": "sha512-6gj2m8cJZ+iSW8bm0FXdGF0YhIQbKrfP4yWTNzxc31U6MOjfEmB1rHvlYvxI1B7t7BCi1F2vYTT6YhtQRG4hxw==",
|
||||
"engines": {
|
||||
"node": "^10.13.0 || >=12.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/on-finished": {
|
||||
"version": "2.4.1",
|
||||
"resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz",
|
||||
|
|
@ -9760,6 +9751,36 @@
|
|||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/openid-client": {
|
||||
"version": "5.7.1",
|
||||
"resolved": "https://registry.npmjs.org/openid-client/-/openid-client-5.7.1.tgz",
|
||||
"integrity": "sha512-jDBPgSVfTnkIh71Hg9pRvtJc6wTwqjRkN88+gCFtYWrlP4Yx2Dsrow8uPi3qLr/aeymPF3o2+dS+wOpglK04ew==",
|
||||
"dependencies": {
|
||||
"jose": "^4.15.9",
|
||||
"lru-cache": "^6.0.0",
|
||||
"object-hash": "^2.2.0",
|
||||
"oidc-token-hash": "^5.0.3"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/panva"
|
||||
}
|
||||
},
|
||||
"node_modules/openid-client/node_modules/lru-cache": {
|
||||
"version": "6.0.0",
|
||||
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz",
|
||||
"integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==",
|
||||
"dependencies": {
|
||||
"yallist": "^4.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
}
|
||||
},
|
||||
"node_modules/openid-client/node_modules/yallist": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz",
|
||||
"integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A=="
|
||||
},
|
||||
"node_modules/optionator": {
|
||||
"version": "0.9.4",
|
||||
"resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz",
|
||||
|
|
@ -10000,6 +10021,26 @@
|
|||
"node": "^10 || ^12 || >=14"
|
||||
}
|
||||
},
|
||||
"node_modules/preact": {
|
||||
"version": "10.29.1",
|
||||
"resolved": "https://registry.npmjs.org/preact/-/preact-10.29.1.tgz",
|
||||
"integrity": "sha512-gQCLc/vWroE8lIpleXtdJhTFDogTdZG9AjMUpVkDf2iTCNwYNWA+u16dL41TqUDJO4gm2IgrcMv3uTpjd4Pwmg==",
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/preact"
|
||||
}
|
||||
},
|
||||
"node_modules/preact-render-to-string": {
|
||||
"version": "5.2.6",
|
||||
"resolved": "https://registry.npmjs.org/preact-render-to-string/-/preact-render-to-string-5.2.6.tgz",
|
||||
"integrity": "sha512-JyhErpYOvBV1hEPwIxc/fHWXPfnEGdRKxc8gFdAZ7XV4tlzyzG847XAyEZqoDnynP88akM4eaHcSOzNcLWFguw==",
|
||||
"dependencies": {
|
||||
"pretty-format": "^3.8.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"preact": ">=10"
|
||||
}
|
||||
},
|
||||
"node_modules/prelude-ls": {
|
||||
"version": "1.2.1",
|
||||
"resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz",
|
||||
|
|
@ -10010,6 +10051,11 @@
|
|||
"node": ">= 0.8.0"
|
||||
}
|
||||
},
|
||||
"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=="
|
||||
},
|
||||
"node_modules/prop-types": {
|
||||
"version": "15.8.1",
|
||||
"resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz",
|
||||
|
|
@ -11426,6 +11472,14 @@
|
|||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/uuid": {
|
||||
"version": "8.3.2",
|
||||
"resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz",
|
||||
"integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==",
|
||||
"bin": {
|
||||
"uuid": "dist/bin/uuid"
|
||||
}
|
||||
},
|
||||
"node_modules/vary": {
|
||||
"version": "1.1.2",
|
||||
"resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz",
|
||||
|
|
@ -12312,6 +12366,34 @@
|
|||
"peerDependencies": {
|
||||
"zod": "^3.25.0 || ^4.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/zustand": {
|
||||
"version": "5.0.12",
|
||||
"resolved": "https://registry.npmjs.org/zustand/-/zustand-5.0.12.tgz",
|
||||
"integrity": "sha512-i77ae3aZq4dhMlRhJVCYgMLKuSiZAaUPAct2AksxQ+gOtimhGMdXljRT21P5BNpeT4kXlLIckvkPM029OljD7g==",
|
||||
"engines": {
|
||||
"node": ">=12.20.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@types/react": ">=18.0.0",
|
||||
"immer": ">=9.0.6",
|
||||
"react": ">=18.0.0",
|
||||
"use-sync-external-store": ">=1.2.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/react": {
|
||||
"optional": true
|
||||
},
|
||||
"immer": {
|
||||
"optional": true
|
||||
},
|
||||
"react": {
|
||||
"optional": true
|
||||
},
|
||||
"use-sync-external-store": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -15,8 +15,10 @@
|
|||
},
|
||||
"dependencies": {
|
||||
"next": "16.2.1",
|
||||
"next-auth": "^4.24.11",
|
||||
"react": "19.2.4",
|
||||
"react-dom": "19.2.4"
|
||||
"react-dom": "19.2.4",
|
||||
"zustand": "^5.0.8"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@opennextjs/aws": "^3.9.16",
|
||||
|
|
|
|||
6
src/app/api/auth/[...nextauth]/route.ts
Normal file
6
src/app/api/auth/[...nextauth]/route.ts
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
import NextAuth from "next-auth";
|
||||
import { authOptions } from "@/lib/auth-options";
|
||||
|
||||
const handler = NextAuth(authOptions);
|
||||
|
||||
export { handler as GET, handler as POST };
|
||||
|
|
@ -1,16 +1,15 @@
|
|||
"use client";
|
||||
|
||||
import { SessionProvider } from "next-auth/react";
|
||||
import { StoreHydration } from "@/components/StoreHydration";
|
||||
import { AuthProvider } from "@/context/AuthContext";
|
||||
import { BookingProvider } from "@/context/BookingContext";
|
||||
import { CurrencyProvider } from "@/context/CurrencyContext";
|
||||
import type { ReactNode } from "react";
|
||||
|
||||
export function Providers({ children }: { children: ReactNode }) {
|
||||
return (
|
||||
<CurrencyProvider>
|
||||
<AuthProvider>
|
||||
<BookingProvider>{children}</BookingProvider>
|
||||
</AuthProvider>
|
||||
</CurrencyProvider>
|
||||
<SessionProvider refetchOnWindowFocus>
|
||||
<StoreHydration />
|
||||
<AuthProvider>{children}</AuthProvider>
|
||||
</SessionProvider>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,166 +4,111 @@ import {
|
|||
createContext,
|
||||
useCallback,
|
||||
useContext,
|
||||
useEffect,
|
||||
useMemo,
|
||||
useState,
|
||||
type ReactNode,
|
||||
} from "react";
|
||||
import { DEMO_BOOKING_REFS } from "@/lib/mocks/guestData";
|
||||
import { signIn, signOut, useSession } from "next-auth/react";
|
||||
import { getHotelPropertyId, getPublicApiUrl } from "@/lib/env";
|
||||
import { useGuestUiStore } from "@/stores/guest-ui-store";
|
||||
import { useOrdersStore } from "@/stores/orders-store";
|
||||
import type { OrderRecord } from "@/types/guest-order";
|
||||
|
||||
const STORAGE_SESSION = "shitaye_session_v1";
|
||||
const STORAGE_ORDERS = "shitaye_orders_v1";
|
||||
|
||||
export type OrderCategory = "room-service" | "laundry" | "gym" | "spa";
|
||||
|
||||
export type OrderRecord = {
|
||||
id: string;
|
||||
category: OrderCategory;
|
||||
title: string;
|
||||
detail: string;
|
||||
totalUsd: number;
|
||||
placedAt: string;
|
||||
status: "pending" | "confirmed" | "completed";
|
||||
};
|
||||
export type { OrderCategory, OrderRecord } from "@/types/guest-order";
|
||||
|
||||
export type MemberSession = {
|
||||
kind: "member";
|
||||
accessToken: string;
|
||||
email: string;
|
||||
displayName: string;
|
||||
propertyId?: string;
|
||||
points: number;
|
||||
tier: "Gold" | "Silver";
|
||||
/** How they signed in — for display only */
|
||||
authMethod: "otp" | "password" | "google" | "apple" | "facebook";
|
||||
authMethod: "otp" | "password" | "google";
|
||||
bookingCode?: string | null;
|
||||
bookingId?: string | null;
|
||||
role?: string;
|
||||
};
|
||||
|
||||
export type BookingRefSession = {
|
||||
kind: "bookingRef";
|
||||
bookingRef: string;
|
||||
guestName: string;
|
||||
roomLabel: string;
|
||||
checkOut: string;
|
||||
};
|
||||
|
||||
export type GuestSession = MemberSession | BookingRefSession;
|
||||
export type GuestSession = MemberSession;
|
||||
|
||||
type AuthContextValue = {
|
||||
session: GuestSession | null;
|
||||
orders: OrderRecord[];
|
||||
isHydrated: boolean;
|
||||
/** Demo OTP is always 123456 */
|
||||
accessToken: string | null;
|
||||
requestOtp: (email: string) => Promise<{ ok: boolean; message: string }>;
|
||||
verifyOtp: (email: string, code: string) => Promise<{ ok: boolean; message: string }>;
|
||||
loginPassword: (email: string, password: string) => Promise<{ ok: boolean; message: string }>;
|
||||
loginSocial: (provider: "google" | "apple" | "facebook") => void;
|
||||
loginBookingRef: (ref: string) => { ok: boolean; message: string };
|
||||
logout: () => void;
|
||||
loginGoogle: () => Promise<void>;
|
||||
loginBookingRef: (ref: string) => Promise<{ ok: boolean; message: string }>;
|
||||
logout: () => Promise<void>;
|
||||
addOrder: (o: Omit<OrderRecord, "id" | "placedAt" | "status"> & { status?: OrderRecord["status"] }) => void;
|
||||
awardPoints: (points: number) => void;
|
||||
setOrders: (orders: OrderRecord[] | ((prev: OrderRecord[]) => OrderRecord[])) => void;
|
||||
};
|
||||
|
||||
const AuthContext = createContext<AuthContextValue | null>(null);
|
||||
|
||||
function loadOrders(): OrderRecord[] {
|
||||
if (typeof window === "undefined") return [];
|
||||
try {
|
||||
const raw = localStorage.getItem(STORAGE_ORDERS);
|
||||
if (!raw) return seedOrders();
|
||||
const parsed = JSON.parse(raw) as OrderRecord[];
|
||||
return Array.isArray(parsed) ? parsed : seedOrders();
|
||||
} catch {
|
||||
return seedOrders();
|
||||
}
|
||||
}
|
||||
|
||||
function seedOrders(): OrderRecord[] {
|
||||
return [
|
||||
{
|
||||
id: "seed-rs-1",
|
||||
category: "room-service",
|
||||
title: "Room service · American breakfast ×2",
|
||||
detail: "Delivered 07:15 · Room charge",
|
||||
totalUsd: 36,
|
||||
placedAt: new Date(Date.now() - 86400000 * 2).toISOString(),
|
||||
status: "completed",
|
||||
},
|
||||
{
|
||||
id: "seed-l-1",
|
||||
category: "laundry",
|
||||
title: "Laundry · Express + 3 shirts",
|
||||
detail: "Returned same evening",
|
||||
totalUsd: 27,
|
||||
placedAt: new Date(Date.now() - 86400000).toISOString(),
|
||||
status: "completed",
|
||||
},
|
||||
{
|
||||
id: "seed-sp-1",
|
||||
category: "spa",
|
||||
title: "Spa · Signature Swedish 60 min",
|
||||
detail: "Apr 4 · 15:00",
|
||||
totalUsd: 85,
|
||||
placedAt: new Date(Date.now() - 86400000 * 3).toISOString(),
|
||||
status: "confirmed",
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
function loadSession(): GuestSession | null {
|
||||
if (typeof window === "undefined") return null;
|
||||
try {
|
||||
const raw = localStorage.getItem(STORAGE_SESSION);
|
||||
if (!raw) return null;
|
||||
return JSON.parse(raw) as GuestSession;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
function persistSession(s: GuestSession | null) {
|
||||
if (typeof window === "undefined") return;
|
||||
if (s) localStorage.setItem(STORAGE_SESSION, JSON.stringify(s));
|
||||
else localStorage.removeItem(STORAGE_SESSION);
|
||||
}
|
||||
|
||||
function persistOrders(orders: OrderRecord[]) {
|
||||
if (typeof window === "undefined") return;
|
||||
localStorage.setItem(STORAGE_ORDERS, JSON.stringify(orders));
|
||||
}
|
||||
|
||||
export function AuthProvider({ children }: { children: ReactNode }) {
|
||||
const [session, setSession] = useState<GuestSession | null>(null);
|
||||
const [orders, setOrders] = useState<OrderRecord[]>([]);
|
||||
const [isHydrated, setIsHydrated] = useState(false);
|
||||
const { data, status } = useSession();
|
||||
const orders = useOrdersStore((s) => s.orders);
|
||||
const localBonusPoints = useGuestUiStore((s) => s.localBonusPoints);
|
||||
|
||||
useEffect(() => {
|
||||
setSession(loadSession());
|
||||
setOrders(loadOrders());
|
||||
setIsHydrated(true);
|
||||
}, []);
|
||||
const isHydrated = status !== "loading";
|
||||
|
||||
const guestSession = useMemo((): GuestSession | null => {
|
||||
if (status !== "authenticated" || !data?.accessToken) return null;
|
||||
const email = data.user?.email ?? "";
|
||||
const displayName = data.user?.name ?? email.split("@")[0] ?? "Guest";
|
||||
return {
|
||||
kind: "member",
|
||||
accessToken: data.accessToken,
|
||||
email,
|
||||
displayName: displayName.charAt(0).toUpperCase() + displayName.slice(1),
|
||||
propertyId: data.propertyId ?? getHotelPropertyId(),
|
||||
points: localBonusPoints,
|
||||
authMethod: data.authMethod ?? "password",
|
||||
bookingCode: data.bookingCode ?? null,
|
||||
bookingId: data.bookingId ?? null,
|
||||
role: data.role,
|
||||
};
|
||||
}, [status, data, localBonusPoints]);
|
||||
|
||||
const requestOtp = useCallback(async (email: string) => {
|
||||
if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email)) {
|
||||
return { ok: false, message: "Enter a valid email address." };
|
||||
}
|
||||
return { ok: true, message: "Demo code sent. Use OTP 123456 to continue." };
|
||||
const propertyId = getHotelPropertyId();
|
||||
if (!propertyId) {
|
||||
return { ok: false, message: "Hotel is not configured (missing NEXT_PUBLIC_HOTEL_PROPERTY_ID)." };
|
||||
}
|
||||
try {
|
||||
const base = getPublicApiUrl();
|
||||
const res = await fetch(`${base}/auth/hotel-guest/send-otp`, {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ propertyId, email: email.trim().toLowerCase() }),
|
||||
});
|
||||
const body = (await res.json().catch(() => ({}))) as { message?: string };
|
||||
if (!res.ok) {
|
||||
return {
|
||||
ok: false,
|
||||
message: typeof body.message === "string" ? body.message : "Could not send code.",
|
||||
};
|
||||
}
|
||||
return { ok: true, message: "Check your email for the one-time code." };
|
||||
} catch {
|
||||
return { ok: false, message: "Could not send code. Try again." };
|
||||
}
|
||||
}, []);
|
||||
|
||||
const verifyOtp = useCallback(async (email: string, code: string) => {
|
||||
const trimmed = code.replace(/\s/g, "");
|
||||
if (trimmed !== "123456") {
|
||||
return { ok: false, message: "Invalid code. Demo OTP is 123456." };
|
||||
const r = await signIn("hotel-otp", {
|
||||
email: email.trim().toLowerCase(),
|
||||
otp: code.replace(/\s/g, ""),
|
||||
redirect: false,
|
||||
});
|
||||
if (r?.error) {
|
||||
return { ok: false, message: "Invalid or expired code." };
|
||||
}
|
||||
const local = email.split("@")[0] ?? "Guest";
|
||||
const name = local.charAt(0).toUpperCase() + local.slice(1);
|
||||
const next: MemberSession = {
|
||||
kind: "member",
|
||||
email: email.toLowerCase(),
|
||||
displayName: name,
|
||||
points: 2400,
|
||||
tier: "Gold",
|
||||
authMethod: "otp",
|
||||
};
|
||||
setSession(next);
|
||||
persistSession(next);
|
||||
return { ok: true, message: "Signed in." };
|
||||
}, []);
|
||||
|
||||
|
|
@ -171,69 +116,47 @@ export function AuthProvider({ children }: { children: ReactNode }) {
|
|||
if (!email || !password) {
|
||||
return { ok: false, message: "Email and password required." };
|
||||
}
|
||||
if (password !== "shitaye" && password !== "demo123") {
|
||||
return {
|
||||
ok: false,
|
||||
message: "Incorrect password. Try demo password: shitaye",
|
||||
};
|
||||
const r = await signIn("credentials", {
|
||||
identifier: email.trim().toLowerCase(),
|
||||
password,
|
||||
redirect: false,
|
||||
});
|
||||
if (r?.error) {
|
||||
return { ok: false, message: "Invalid email or password." };
|
||||
}
|
||||
const local = email.split("@")[0] ?? "Guest";
|
||||
const name = local.charAt(0).toUpperCase() + local.slice(1);
|
||||
const next: MemberSession = {
|
||||
kind: "member",
|
||||
email: email.toLowerCase(),
|
||||
displayName: name,
|
||||
points: 2400,
|
||||
tier: "Gold",
|
||||
authMethod: "password",
|
||||
};
|
||||
setSession(next);
|
||||
persistSession(next);
|
||||
return { ok: true, message: "Signed in." };
|
||||
}, []);
|
||||
|
||||
const loginSocial = useCallback((provider: "google" | "apple" | "facebook") => {
|
||||
const names: Record<typeof provider, string> = {
|
||||
google: "Google Guest",
|
||||
apple: "Apple Guest",
|
||||
facebook: "Facebook Guest",
|
||||
};
|
||||
const next: MemberSession = {
|
||||
kind: "member",
|
||||
email: `guest.${provider}@shitaye.demo`,
|
||||
displayName: names[provider],
|
||||
points: 2100,
|
||||
tier: "Silver",
|
||||
authMethod: provider,
|
||||
};
|
||||
setSession(next);
|
||||
persistSession(next);
|
||||
const loginGoogle = useCallback(async () => {
|
||||
await signIn("google", { callbackUrl: "/", redirect: true });
|
||||
}, []);
|
||||
|
||||
const loginBookingRef = useCallback((ref: string) => {
|
||||
const key = ref.trim().toUpperCase();
|
||||
const row = DEMO_BOOKING_REFS[key];
|
||||
if (!row) {
|
||||
const loginBookingRef = useCallback(async (ref: string) => {
|
||||
const propertyId = getHotelPropertyId();
|
||||
if (!propertyId) {
|
||||
return {
|
||||
ok: false,
|
||||
message: "Reference not found. Try SHITAYE-2026-DEMO or GUEST-1234.",
|
||||
message: "Hotel is not configured (missing NEXT_PUBLIC_HOTEL_PROPERTY_ID).",
|
||||
};
|
||||
}
|
||||
const next: BookingRefSession = {
|
||||
kind: "bookingRef",
|
||||
bookingRef: key,
|
||||
guestName: row.guestName,
|
||||
roomLabel: row.room,
|
||||
checkOut: row.checkOut,
|
||||
};
|
||||
setSession(next);
|
||||
persistSession(next);
|
||||
return { ok: true, message: "Linked to your stay." };
|
||||
const r = await signIn("booking-code", {
|
||||
bookingCode: ref.trim(),
|
||||
propertyId,
|
||||
redirect: false,
|
||||
});
|
||||
if (r?.error) {
|
||||
return { ok: false, message: "Invalid booking code or account not linked." };
|
||||
}
|
||||
return { ok: true, message: "Signed in with your booking." };
|
||||
}, []);
|
||||
|
||||
const logout = useCallback(() => {
|
||||
setSession(null);
|
||||
persistSession(null);
|
||||
const logout = useCallback(async () => {
|
||||
useGuestUiStore.getState().resetLocalBonus();
|
||||
await signOut({ callbackUrl: "/" });
|
||||
}, []);
|
||||
|
||||
const setOrders = useCallback((next: OrderRecord[] | ((prev: OrderRecord[]) => OrderRecord[])) => {
|
||||
useOrdersStore.getState().setOrders(next);
|
||||
}, []);
|
||||
|
||||
const addOrder = useCallback(
|
||||
|
|
@ -248,59 +171,49 @@ export function AuthProvider({ children }: { children: ReactNode }) {
|
|||
placedAt: new Date().toISOString(),
|
||||
status: o.status ?? "pending",
|
||||
};
|
||||
setOrders((prev) => {
|
||||
const next = [rec, ...prev];
|
||||
persistOrders(next);
|
||||
return next;
|
||||
});
|
||||
if (session?.kind === "member") {
|
||||
useOrdersStore.getState().pushOrder(rec);
|
||||
if (guestSession?.kind === "member") {
|
||||
const bonus = Math.min(150, Math.round(o.totalUsd * 2));
|
||||
setSession((s) => {
|
||||
if (!s || s.kind !== "member") return s;
|
||||
const u = { ...s, points: s.points + bonus };
|
||||
persistSession(u);
|
||||
return u;
|
||||
});
|
||||
useGuestUiStore.getState().addLocalBonus(bonus);
|
||||
}
|
||||
},
|
||||
[session],
|
||||
[guestSession],
|
||||
);
|
||||
|
||||
const awardPoints = useCallback((points: number) => {
|
||||
setSession((s) => {
|
||||
if (!s || s.kind !== "member") return s;
|
||||
const u = { ...s, points: s.points + points };
|
||||
persistSession(u);
|
||||
return u;
|
||||
});
|
||||
useGuestUiStore.getState().addLocalBonus(points);
|
||||
}, []);
|
||||
|
||||
const value = useMemo<AuthContextValue>(
|
||||
() => ({
|
||||
session,
|
||||
session: guestSession,
|
||||
orders,
|
||||
isHydrated,
|
||||
accessToken: data?.accessToken ?? null,
|
||||
requestOtp,
|
||||
verifyOtp,
|
||||
loginPassword,
|
||||
loginSocial,
|
||||
loginGoogle,
|
||||
loginBookingRef,
|
||||
logout,
|
||||
addOrder,
|
||||
awardPoints,
|
||||
setOrders,
|
||||
}),
|
||||
[
|
||||
session,
|
||||
guestSession,
|
||||
orders,
|
||||
isHydrated,
|
||||
data?.accessToken,
|
||||
requestOtp,
|
||||
verifyOtp,
|
||||
loginPassword,
|
||||
loginSocial,
|
||||
loginGoogle,
|
||||
loginBookingRef,
|
||||
logout,
|
||||
addOrder,
|
||||
awardPoints,
|
||||
setOrders,
|
||||
],
|
||||
);
|
||||
|
||||
|
|
|
|||
45
src/lib/api-client.ts
Normal file
45
src/lib/api-client.ts
Normal file
|
|
@ -0,0 +1,45 @@
|
|||
import { getPublicApiUrl } from "@/lib/env";
|
||||
|
||||
export class ApiError extends Error {
|
||||
constructor(
|
||||
message: string,
|
||||
public status: number,
|
||||
public body?: unknown,
|
||||
) {
|
||||
super(message);
|
||||
this.name = "ApiError";
|
||||
}
|
||||
}
|
||||
|
||||
export async function apiFetch<T = unknown>(
|
||||
path: string,
|
||||
init: RequestInit & { accessToken?: string } = {},
|
||||
): Promise<T> {
|
||||
const base = getPublicApiUrl();
|
||||
const url = path.startsWith("http") ? path : `${base}${path.startsWith("/") ? "" : "/"}${path}`;
|
||||
const headers = new Headers(init.headers);
|
||||
if (init.body && !headers.has("Content-Type")) {
|
||||
headers.set("Content-Type", "application/json");
|
||||
}
|
||||
if (init.accessToken) {
|
||||
headers.set("Authorization", `Bearer ${init.accessToken}`);
|
||||
}
|
||||
const res = await fetch(url, { ...init, headers });
|
||||
const text = await res.text();
|
||||
let data: unknown = null;
|
||||
if (text) {
|
||||
try {
|
||||
data = JSON.parse(text) as unknown;
|
||||
} catch {
|
||||
data = text;
|
||||
}
|
||||
}
|
||||
if (!res.ok) {
|
||||
const msg =
|
||||
typeof data === "object" && data !== null && "message" in data
|
||||
? String((data as { message: unknown }).message)
|
||||
: res.statusText;
|
||||
throw new ApiError(msg || `HTTP ${res.status}`, res.status, data);
|
||||
}
|
||||
return data as T;
|
||||
}
|
||||
213
src/lib/auth-options.ts
Normal file
213
src/lib/auth-options.ts
Normal file
|
|
@ -0,0 +1,213 @@
|
|||
import type { NextAuthOptions } from "next-auth";
|
||||
import CredentialsProvider from "next-auth/providers/credentials";
|
||||
import GoogleProvider from "next-auth/providers/google";
|
||||
import { getPublicApiUrl, getHotelPropertyId } from "@/lib/env";
|
||||
|
||||
async function postJson<T>(path: string, body: Record<string, unknown>): Promise<T> {
|
||||
const api = getPublicApiUrl();
|
||||
const res = await fetch(`${api}${path.startsWith("/") ? path : `/${path}`}`, {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify(body),
|
||||
});
|
||||
const data = (await res.json().catch(() => ({}))) as T & { message?: string };
|
||||
if (!res.ok) {
|
||||
throw new Error(
|
||||
typeof data === "object" && data && "message" in data && typeof data.message === "string"
|
||||
? data.message
|
||||
: `Auth failed (${res.status})`,
|
||||
);
|
||||
}
|
||||
return data as T;
|
||||
}
|
||||
|
||||
type LoginPayload = {
|
||||
access_token: string;
|
||||
user: {
|
||||
id: string;
|
||||
email: string | null;
|
||||
name: string | null;
|
||||
role: string;
|
||||
propertyId?: string;
|
||||
};
|
||||
};
|
||||
|
||||
type AuthMethod = "otp" | "password" | "google";
|
||||
|
||||
type BookingCodePayload = LoginPayload & {
|
||||
booking?: {
|
||||
id: string;
|
||||
bookingCode: string | null;
|
||||
checkIn: string;
|
||||
checkOut: string;
|
||||
status: string;
|
||||
};
|
||||
};
|
||||
|
||||
const providers: NextAuthOptions["providers"] = [
|
||||
CredentialsProvider({
|
||||
id: "credentials",
|
||||
name: "Email & password",
|
||||
credentials: {
|
||||
identifier: { label: "Email", type: "text" },
|
||||
password: { label: "Password", type: "password" },
|
||||
},
|
||||
async authorize(credentials) {
|
||||
if (!credentials?.identifier || !credentials?.password) return null;
|
||||
try {
|
||||
const data = await postJson<LoginPayload>("/auth/login", {
|
||||
identifier: credentials.identifier,
|
||||
password: credentials.password,
|
||||
});
|
||||
return {
|
||||
id: data.user.id,
|
||||
email: data.user.email ?? undefined,
|
||||
name: data.user.name ?? undefined,
|
||||
accessToken: data.access_token,
|
||||
role: data.user.role,
|
||||
propertyId: data.user.propertyId,
|
||||
authMethod: "password" as AuthMethod,
|
||||
};
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
},
|
||||
}),
|
||||
CredentialsProvider({
|
||||
id: "hotel-otp",
|
||||
name: "Hotel email OTP",
|
||||
credentials: {
|
||||
email: { label: "Email", type: "text" },
|
||||
otp: { label: "Code", type: "text" },
|
||||
},
|
||||
async authorize(credentials) {
|
||||
if (!credentials?.email || !credentials?.otp) return null;
|
||||
try {
|
||||
const data = await postJson<LoginPayload>("/auth/hotel-user/login-email-otp", {
|
||||
email: credentials.email,
|
||||
otp: credentials.otp,
|
||||
});
|
||||
return {
|
||||
id: data.user.id,
|
||||
email: data.user.email ?? undefined,
|
||||
name: data.user.name ?? undefined,
|
||||
accessToken: data.access_token,
|
||||
role: data.user.role,
|
||||
propertyId: data.user.propertyId,
|
||||
authMethod: "otp" as AuthMethod,
|
||||
};
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
},
|
||||
}),
|
||||
CredentialsProvider({
|
||||
id: "booking-code",
|
||||
name: "Booking code",
|
||||
credentials: {
|
||||
bookingCode: { label: "Booking code", type: "text" },
|
||||
propertyId: { label: "Property", type: "text" },
|
||||
},
|
||||
async authorize(credentials) {
|
||||
const propertyId = credentials?.propertyId?.trim() || getHotelPropertyId();
|
||||
const bookingCode = credentials?.bookingCode?.trim();
|
||||
if (!propertyId || !bookingCode) return null;
|
||||
try {
|
||||
const data = await postJson<BookingCodePayload>("/auth/hotel-guest/login-booking-code", {
|
||||
propertyId,
|
||||
bookingCode,
|
||||
});
|
||||
return {
|
||||
id: data.user.id,
|
||||
email: data.user.email ?? undefined,
|
||||
name: data.user.name ?? undefined,
|
||||
accessToken: data.access_token,
|
||||
role: data.user.role,
|
||||
propertyId: data.user.propertyId ?? propertyId,
|
||||
bookingCode: data.booking?.bookingCode ?? bookingCode,
|
||||
bookingId: data.booking?.id ?? null,
|
||||
};
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
},
|
||||
}),
|
||||
];
|
||||
|
||||
if (process.env.GOOGLE_CLIENT_ID?.trim() && process.env.GOOGLE_CLIENT_SECRET?.trim()) {
|
||||
providers.push(
|
||||
GoogleProvider({
|
||||
clientId: process.env.GOOGLE_CLIENT_ID,
|
||||
clientSecret: process.env.GOOGLE_CLIENT_SECRET,
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
export const authOptions: NextAuthOptions = {
|
||||
providers,
|
||||
callbacks: {
|
||||
async jwt({ token, user, account, profile }) {
|
||||
if (account?.provider === "google" && profile && "email" in profile && profile.email) {
|
||||
try {
|
||||
const data = await postJson<LoginPayload>("/auth/google", {
|
||||
email: profile.email,
|
||||
name: profile.name,
|
||||
googleId: profile.sub,
|
||||
role: "CUSTOMER",
|
||||
});
|
||||
token.accessToken = data.access_token;
|
||||
token.sub = data.user.id;
|
||||
token.email = data.user.email ?? undefined;
|
||||
token.name = data.user.name ?? undefined;
|
||||
token.role = data.user.role;
|
||||
token.propertyId = data.user.propertyId;
|
||||
token.authMethod = "google";
|
||||
token.error = undefined;
|
||||
} catch {
|
||||
token.error = "GoogleSignInFailed";
|
||||
}
|
||||
return token;
|
||||
}
|
||||
|
||||
if (user) {
|
||||
const u = user as {
|
||||
accessToken?: string;
|
||||
role?: string;
|
||||
propertyId?: string;
|
||||
bookingCode?: string | null;
|
||||
bookingId?: string | null;
|
||||
authMethod?: AuthMethod;
|
||||
};
|
||||
token.accessToken = u.accessToken;
|
||||
token.role = u.role;
|
||||
token.propertyId = u.propertyId;
|
||||
token.bookingCode = u.bookingCode ?? undefined;
|
||||
token.bookingId = u.bookingId ?? undefined;
|
||||
token.authMethod = u.authMethod ?? token.authMethod ?? "password";
|
||||
}
|
||||
return token;
|
||||
},
|
||||
async session({ session, token }) {
|
||||
if (session.user) {
|
||||
session.user.email = (token.email as string) ?? session.user.email;
|
||||
session.user.name = (token.name as string) ?? session.user.name;
|
||||
}
|
||||
session.accessToken = token.accessToken as string | undefined;
|
||||
session.role = token.role as string | undefined;
|
||||
session.propertyId = (token.propertyId as string | undefined) ?? getHotelPropertyId();
|
||||
session.bookingCode = token.bookingCode ?? null;
|
||||
session.bookingId = token.bookingId ?? null;
|
||||
session.authMethod = (token.authMethod as AuthMethod | undefined) ?? "password";
|
||||
session.error = token.error as string | undefined;
|
||||
return session;
|
||||
},
|
||||
},
|
||||
pages: {
|
||||
signIn: "/login",
|
||||
},
|
||||
session: {
|
||||
strategy: "jwt",
|
||||
maxAge: 60 * 60 * 24 * 7,
|
||||
},
|
||||
secret: process.env.NEXTAUTH_SECRET,
|
||||
};
|
||||
11
src/lib/env.ts
Normal file
11
src/lib/env.ts
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
/** API origin including /api prefix, e.g. http://localhost:7777/api */
|
||||
export function getPublicApiUrl(): string {
|
||||
const u = process.env.NEXT_PUBLIC_API_URL?.trim();
|
||||
if (u) return u.replace(/\/$/, "");
|
||||
return "http://localhost:7777/api";
|
||||
}
|
||||
|
||||
export function getHotelPropertyId(): string | undefined {
|
||||
const id = process.env.NEXT_PUBLIC_HOTEL_PROPERTY_ID?.trim();
|
||||
return id || undefined;
|
||||
}
|
||||
26
src/types/next-auth.d.ts
vendored
Normal file
26
src/types/next-auth.d.ts
vendored
Normal file
|
|
@ -0,0 +1,26 @@
|
|||
import type { DefaultSession } from "next-auth";
|
||||
|
||||
declare module "next-auth" {
|
||||
interface Session extends DefaultSession {
|
||||
accessToken?: string;
|
||||
role?: string;
|
||||
propertyId?: string;
|
||||
/** Set when signing in with booking code */
|
||||
bookingCode?: string | null;
|
||||
bookingId?: string | null;
|
||||
authMethod?: "otp" | "password" | "google";
|
||||
error?: string;
|
||||
}
|
||||
}
|
||||
|
||||
declare module "next-auth/jwt" {
|
||||
interface JWT {
|
||||
accessToken?: string;
|
||||
role?: string;
|
||||
propertyId?: string;
|
||||
bookingCode?: string | null;
|
||||
bookingId?: string | null;
|
||||
authMethod?: "otp" | "password" | "google";
|
||||
error?: string;
|
||||
}
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user