Overhaul!
This commit is contained in:
parent
b530ee3dbd
commit
1196cbc9d2
28 changed files with 658 additions and 1462 deletions
|
@ -8,9 +8,9 @@ UPSTASH_REDIS_EXPIRY_SECONDS=""
|
||||||
UPSTASH_RATELIMIT_REQUESTS=""
|
UPSTASH_RATELIMIT_REQUESTS=""
|
||||||
UPSTASH_RATELIMIT_SECONDS=""
|
UPSTASH_RATELIMIT_SECONDS=""
|
||||||
|
|
||||||
#Next Auth Core
|
#Auth
|
||||||
NEXTAUTH_SECRET=""
|
NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY=""
|
||||||
NEXTAUTH_URL=""
|
CLERK_SECRET_KEY=""
|
||||||
|
|
||||||
# Next Auth Github Provider
|
# Next Auth Github Provider
|
||||||
GITHUB_CLIENT_ID=""
|
GITHUB_CLIENT_ID=""
|
||||||
|
|
|
@ -10,7 +10,11 @@ const config = {
|
||||||
defaultLocale: "en",
|
defaultLocale: "en",
|
||||||
},
|
},
|
||||||
images: {
|
images: {
|
||||||
domains: ["avatars.githubusercontent.com", "lh3.googleusercontent.com"],
|
domains: [
|
||||||
|
"avatars.githubusercontent.com",
|
||||||
|
"lh3.googleusercontent.com",
|
||||||
|
"img.clerk.com",
|
||||||
|
],
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -1,11 +1,11 @@
|
||||||
{
|
{
|
||||||
"name": "sprintpadawan",
|
"name": "sprintpadawan",
|
||||||
"version": "1.2.7",
|
"version": "2.0.0",
|
||||||
"description": "Plan. Sprint. Repeat.",
|
"description": "Plan. Sprint. Repeat.",
|
||||||
"private": true,
|
"private": true,
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"build": "next build",
|
"build": "next build",
|
||||||
"serv": "NEXTAUTH_URL=http://localhost:3000 && next dev",
|
"serv": "next dev",
|
||||||
"dev": "pnpm serv",
|
"dev": "pnpm serv",
|
||||||
"postinstall": "prisma generate",
|
"postinstall": "prisma generate",
|
||||||
"lint": "next lint",
|
"lint": "next lint",
|
||||||
|
@ -14,6 +14,7 @@
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@ably-labs/react-hooks": "^2.1.1",
|
"@ably-labs/react-hooks": "^2.1.1",
|
||||||
"@auth/prisma-adapter": "^1.0.1",
|
"@auth/prisma-adapter": "^1.0.1",
|
||||||
|
"@clerk/nextjs": "^4.23.2",
|
||||||
"@prisma/client": "5.1.1",
|
"@prisma/client": "5.1.1",
|
||||||
"@react-email/components": "^0.0.7",
|
"@react-email/components": "^0.0.7",
|
||||||
"@tanstack/react-query": "^4.32.6",
|
"@tanstack/react-query": "^4.32.6",
|
||||||
|
@ -28,7 +29,6 @@
|
||||||
"autoprefixer": "^10.4.14",
|
"autoprefixer": "^10.4.14",
|
||||||
"json2csv": "6.0.0-alpha.2",
|
"json2csv": "6.0.0-alpha.2",
|
||||||
"next": "^13.4.13",
|
"next": "^13.4.13",
|
||||||
"next-auth": "^4.22.5",
|
|
||||||
"nextjs-cors": "^2.1.2",
|
"nextjs-cors": "^2.1.2",
|
||||||
"postcss": "^8.4.27",
|
"postcss": "^8.4.27",
|
||||||
"react": "18.2.0",
|
"react": "18.2.0",
|
||||||
|
|
358
pnpm-lock.yaml
generated
358
pnpm-lock.yaml
generated
|
@ -11,6 +11,9 @@ dependencies:
|
||||||
'@auth/prisma-adapter':
|
'@auth/prisma-adapter':
|
||||||
specifier: ^1.0.1
|
specifier: ^1.0.1
|
||||||
version: 1.0.1(@prisma/client@5.1.1)
|
version: 1.0.1(@prisma/client@5.1.1)
|
||||||
|
'@clerk/nextjs':
|
||||||
|
specifier: ^4.23.2
|
||||||
|
version: 4.23.2(next@13.4.13)(react-dom@18.2.0)(react@18.2.0)
|
||||||
'@prisma/client':
|
'@prisma/client':
|
||||||
specifier: 5.1.1
|
specifier: 5.1.1
|
||||||
version: 5.1.1(prisma@5.1.1)
|
version: 5.1.1(prisma@5.1.1)
|
||||||
|
@ -215,6 +218,83 @@ packages:
|
||||||
dependencies:
|
dependencies:
|
||||||
regenerator-runtime: 0.14.0
|
regenerator-runtime: 0.14.0
|
||||||
|
|
||||||
|
/@clerk/backend@0.27.0:
|
||||||
|
resolution: {integrity: sha512-Sj541JrpqAn1A/UwdyDBxFV3stq2A/Pe/8HdPTG3Cct6briPyavfi46O5s1+L3BSvUcKUY+UbM0+8VsoCNFi4w==}
|
||||||
|
engines: {node: '>=14'}
|
||||||
|
dependencies:
|
||||||
|
'@clerk/types': 3.49.0
|
||||||
|
'@peculiar/webcrypto': 1.4.1
|
||||||
|
'@types/node': 16.18.6
|
||||||
|
cookie: 0.5.0
|
||||||
|
deepmerge: 4.2.2
|
||||||
|
node-fetch-native: 1.0.1
|
||||||
|
snakecase-keys: 5.4.4
|
||||||
|
tslib: 2.4.1
|
||||||
|
dev: false
|
||||||
|
|
||||||
|
/@clerk/clerk-react@4.23.2(react@18.2.0):
|
||||||
|
resolution: {integrity: sha512-6MJa8ecr22qHhTfdkMMIJGctMBqj01fLJ4vmfZvr22tIkwkPXoeYJd5XcFKuSoO2dXc1eHD/F9i/HdCqGm68gw==}
|
||||||
|
engines: {node: '>=14'}
|
||||||
|
peerDependencies:
|
||||||
|
react: '>=16'
|
||||||
|
dependencies:
|
||||||
|
'@clerk/shared': 0.21.0(react@18.2.0)
|
||||||
|
'@clerk/types': 3.49.0
|
||||||
|
react: 18.2.0
|
||||||
|
tslib: 2.4.1
|
||||||
|
dev: false
|
||||||
|
|
||||||
|
/@clerk/clerk-sdk-node@4.12.2:
|
||||||
|
resolution: {integrity: sha512-7xYPsLSeGO5XoP0No/9m2dsCMezwtmiYGKOwWzt41ZzJNFlU0rfqYF3VOZEsbtQlc3ZXeU+67ItjoJYrf3kT6A==}
|
||||||
|
engines: {node: '>=14'}
|
||||||
|
dependencies:
|
||||||
|
'@clerk/backend': 0.27.0
|
||||||
|
'@clerk/types': 3.49.0
|
||||||
|
'@types/cookies': 0.7.7
|
||||||
|
'@types/express': 4.17.14
|
||||||
|
'@types/node-fetch': 2.6.2
|
||||||
|
camelcase-keys: 6.2.2
|
||||||
|
snakecase-keys: 3.2.1
|
||||||
|
tslib: 2.4.1
|
||||||
|
dev: false
|
||||||
|
|
||||||
|
/@clerk/nextjs@4.23.2(next@13.4.13)(react-dom@18.2.0)(react@18.2.0):
|
||||||
|
resolution: {integrity: sha512-99bSVu9r1E9MxybO/6mmPAufSKq4KU7SFeMVkylX7UF8sy5t/LE9cLHyc+9jitcCGgZNai9Om4sj1WIgkNOP8w==}
|
||||||
|
engines: {node: '>=14'}
|
||||||
|
peerDependencies:
|
||||||
|
next: '>=10'
|
||||||
|
react: ^17.0.2 || ^18.0.0-0
|
||||||
|
react-dom: ^17.0.2 || ^18.0.0-0
|
||||||
|
dependencies:
|
||||||
|
'@clerk/backend': 0.27.0
|
||||||
|
'@clerk/clerk-react': 4.23.2(react@18.2.0)
|
||||||
|
'@clerk/clerk-sdk-node': 4.12.2
|
||||||
|
'@clerk/types': 3.49.0
|
||||||
|
next: 13.4.13(react-dom@18.2.0)(react@18.2.0)
|
||||||
|
path-to-regexp: 6.2.1
|
||||||
|
react: 18.2.0
|
||||||
|
react-dom: 18.2.0(react@18.2.0)
|
||||||
|
tslib: 2.4.1
|
||||||
|
dev: false
|
||||||
|
|
||||||
|
/@clerk/shared@0.21.0(react@18.2.0):
|
||||||
|
resolution: {integrity: sha512-tkV2OAddFMPBHDjcMbtNNrV3NQD+hGKf2hn3TKv1mRJNZ2oR5Bfk8r0bkaqwoqxX8ndkbHLCa9gwR8SWO7GGow==}
|
||||||
|
peerDependencies:
|
||||||
|
react: '>=16'
|
||||||
|
dependencies:
|
||||||
|
glob-to-regexp: 0.4.1
|
||||||
|
js-cookie: 3.0.1
|
||||||
|
react: 18.2.0
|
||||||
|
swr: 1.3.0(react@18.2.0)
|
||||||
|
dev: false
|
||||||
|
|
||||||
|
/@clerk/types@3.49.0:
|
||||||
|
resolution: {integrity: sha512-vAx5R/iYfsgIaIDMiDr6ZKQnAneAmRrUVYz6KCtPG6/hnEAnRYhwXpEUi89e5G0BFmuUfSxe/N/Anfc1PNteXQ==}
|
||||||
|
engines: {node: '>=14'}
|
||||||
|
dependencies:
|
||||||
|
csstype: 3.1.1
|
||||||
|
dev: false
|
||||||
|
|
||||||
/@commander-js/extra-typings@9.4.1(commander@9.4.1):
|
/@commander-js/extra-typings@9.4.1(commander@9.4.1):
|
||||||
resolution: {integrity: sha512-v0BqORYamk1koxDon6femDGLWSL7P78vYTyOU5nFaALnmNALL+ktgdHvWbxzzBBJIKS7kv3XvM/DqNwiLcgFTA==}
|
resolution: {integrity: sha512-v0BqORYamk1koxDon6femDGLWSL7P78vYTyOU5nFaALnmNALL+ktgdHvWbxzzBBJIKS7kv3XvM/DqNwiLcgFTA==}
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
|
@ -769,6 +849,32 @@ packages:
|
||||||
resolution: {integrity: sha512-dhPeilub1NuIG0X5Kvhh9lH4iW3ZsHlnzwgwbOlgwQ2wG1IqFzsgHqmKPk3WzsdWAeaxKJxgM0+W433RmN45GA==}
|
resolution: {integrity: sha512-dhPeilub1NuIG0X5Kvhh9lH4iW3ZsHlnzwgwbOlgwQ2wG1IqFzsgHqmKPk3WzsdWAeaxKJxgM0+W433RmN45GA==}
|
||||||
dev: false
|
dev: false
|
||||||
|
|
||||||
|
/@peculiar/asn1-schema@2.3.6:
|
||||||
|
resolution: {integrity: sha512-izNRxPoaeJeg/AyH8hER6s+H7p4itk+03QCa4sbxI3lNdseQYCuxzgsuNK8bTXChtLTjpJz6NmXKA73qLa3rCA==}
|
||||||
|
dependencies:
|
||||||
|
asn1js: 3.0.5
|
||||||
|
pvtsutils: 1.3.3
|
||||||
|
tslib: 2.6.1
|
||||||
|
dev: false
|
||||||
|
|
||||||
|
/@peculiar/json-schema@1.1.12:
|
||||||
|
resolution: {integrity: sha512-coUfuoMeIB7B8/NMekxaDzLhaYmp0HZNPEjYRm9goRou8UZIC3z21s0sL9AWoCw4EG876QyO3kYrc61WNF9B/w==}
|
||||||
|
engines: {node: '>=8.0.0'}
|
||||||
|
dependencies:
|
||||||
|
tslib: 2.6.1
|
||||||
|
dev: false
|
||||||
|
|
||||||
|
/@peculiar/webcrypto@1.4.1:
|
||||||
|
resolution: {integrity: sha512-eK4C6WTNYxoI7JOabMoZICiyqRRtJB220bh0Mbj5RwRycleZf9BPyZoxsTvpP0FpmVS2aS13NKOuh5/tN3sIRw==}
|
||||||
|
engines: {node: '>=10.12.0'}
|
||||||
|
dependencies:
|
||||||
|
'@peculiar/asn1-schema': 2.3.6
|
||||||
|
'@peculiar/json-schema': 1.1.12
|
||||||
|
pvtsutils: 1.3.3
|
||||||
|
tslib: 2.6.1
|
||||||
|
webcrypto-core: 1.7.7
|
||||||
|
dev: false
|
||||||
|
|
||||||
/@prisma/client@5.1.1(prisma@5.1.1):
|
/@prisma/client@5.1.1(prisma@5.1.1):
|
||||||
resolution: {integrity: sha512-fxcCeK5pMQGcgCqCrWsi+I2rpIbk0rAhdrN+ke7f34tIrgPwA68ensrpin+9+fZvuV2OtzHmuipwduSY6HswdA==}
|
resolution: {integrity: sha512-fxcCeK5pMQGcgCqCrWsi+I2rpIbk0rAhdrN+ke7f34tIrgPwA68ensrpin+9+fZvuV2OtzHmuipwduSY6HswdA==}
|
||||||
engines: {node: '>=16.13'}
|
engines: {node: '>=16.13'}
|
||||||
|
@ -1072,6 +1178,13 @@ packages:
|
||||||
resolution: {integrity: sha512-r3VeA319/braYMBIzj+XLgLKQ9lJSVglvPvP9HUv4kr5w6Y5grQMxMcExhTiZWltE9bnSJHKtBBzHafOo7KC8A==}
|
resolution: {integrity: sha512-r3VeA319/braYMBIzj+XLgLKQ9lJSVglvPvP9HUv4kr5w6Y5grQMxMcExhTiZWltE9bnSJHKtBBzHafOo7KC8A==}
|
||||||
dev: false
|
dev: false
|
||||||
|
|
||||||
|
/@types/body-parser@1.19.2:
|
||||||
|
resolution: {integrity: sha512-ALYone6pm6QmwZoAgeyNksccT9Q4AWZQ6PvfwR37GT6r6FWUPguq6sUmNGSMV2Wr761oQoBxwGGa6DR5o1DC9g==}
|
||||||
|
dependencies:
|
||||||
|
'@types/connect': 3.4.35
|
||||||
|
'@types/node': 20.4.9
|
||||||
|
dev: false
|
||||||
|
|
||||||
/@types/cacheable-request@6.0.3:
|
/@types/cacheable-request@6.0.3:
|
||||||
resolution: {integrity: sha512-IQ3EbTzGxIigb1I3qPZc1rWJnH0BmSKv5QYTalEwweFvyBDLSAe24zP0le/hyi7ecGfZVlIVAg4BZqb8WBwKqw==}
|
resolution: {integrity: sha512-IQ3EbTzGxIigb1I3qPZc1rWJnH0BmSKv5QYTalEwweFvyBDLSAe24zP0le/hyi7ecGfZVlIVAg4BZqb8WBwKqw==}
|
||||||
dependencies:
|
dependencies:
|
||||||
|
@ -1081,6 +1194,21 @@ packages:
|
||||||
'@types/responselike': 1.0.0
|
'@types/responselike': 1.0.0
|
||||||
dev: false
|
dev: false
|
||||||
|
|
||||||
|
/@types/connect@3.4.35:
|
||||||
|
resolution: {integrity: sha512-cdeYyv4KWoEgpBISTxWvqYsVy444DOqehiF3fM3ne10AmJ62RSyNkUnxMJXHQWRQQX2eR94m5y1IZyDwBjV9FQ==}
|
||||||
|
dependencies:
|
||||||
|
'@types/node': 20.4.9
|
||||||
|
dev: false
|
||||||
|
|
||||||
|
/@types/cookies@0.7.7:
|
||||||
|
resolution: {integrity: sha512-h7BcvPUogWbKCzBR2lY4oqaZbO3jXZksexYJVFvkrFeLgbZjQkU4x8pRq6eg2MHXQhY0McQdqmmsxRWlVAHooA==}
|
||||||
|
dependencies:
|
||||||
|
'@types/connect': 3.4.35
|
||||||
|
'@types/express': 4.17.14
|
||||||
|
'@types/keygrip': 1.0.2
|
||||||
|
'@types/node': 20.4.9
|
||||||
|
dev: false
|
||||||
|
|
||||||
/@types/eslint@8.44.2:
|
/@types/eslint@8.44.2:
|
||||||
resolution: {integrity: sha512-sdPRb9K6iL5XZOmBubg8yiFp5yS/JdUDQsq5e6h95km91MCYMuvp7mh1fjPEYUhvHepKpZOjnEaMBR4PxjWDzg==}
|
resolution: {integrity: sha512-sdPRb9K6iL5XZOmBubg8yiFp5yS/JdUDQsq5e6h95km91MCYMuvp7mh1fjPEYUhvHepKpZOjnEaMBR4PxjWDzg==}
|
||||||
dependencies:
|
dependencies:
|
||||||
|
@ -1092,10 +1220,32 @@ packages:
|
||||||
resolution: {integrity: sha512-LG4opVs2ANWZ1TJoKc937iMmNstM/d0ae1vNbnBvBhqCSezgVUOzcLCqbI5elV8Vy6WKwKjaqR+zO9VKirBBCA==}
|
resolution: {integrity: sha512-LG4opVs2ANWZ1TJoKc937iMmNstM/d0ae1vNbnBvBhqCSezgVUOzcLCqbI5elV8Vy6WKwKjaqR+zO9VKirBBCA==}
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
|
/@types/express-serve-static-core@4.17.35:
|
||||||
|
resolution: {integrity: sha512-wALWQwrgiB2AWTT91CB62b6Yt0sNHpznUXeZEcnPU3DRdlDIz74x8Qg1UUYKSVFi+va5vKOLYRBI1bRKiLLKIg==}
|
||||||
|
dependencies:
|
||||||
|
'@types/node': 20.4.9
|
||||||
|
'@types/qs': 6.9.7
|
||||||
|
'@types/range-parser': 1.2.4
|
||||||
|
'@types/send': 0.17.1
|
||||||
|
dev: false
|
||||||
|
|
||||||
|
/@types/express@4.17.14:
|
||||||
|
resolution: {integrity: sha512-TEbt+vaPFQ+xpxFLFssxUDXj5cWCxZJjIcB7Yg0k0GMHGtgtQgpvx/MUQUeAkNbA9AAGrwkAsoeItdTgS7FMyg==}
|
||||||
|
dependencies:
|
||||||
|
'@types/body-parser': 1.19.2
|
||||||
|
'@types/express-serve-static-core': 4.17.35
|
||||||
|
'@types/qs': 6.9.7
|
||||||
|
'@types/serve-static': 1.15.2
|
||||||
|
dev: false
|
||||||
|
|
||||||
/@types/http-cache-semantics@4.0.1:
|
/@types/http-cache-semantics@4.0.1:
|
||||||
resolution: {integrity: sha512-SZs7ekbP8CN0txVG2xVRH6EgKmEm31BOxA07vkFaETzZz1xh+cbt8BcI0slpymvwhx5dlFnQG2rTlPVQn+iRPQ==}
|
resolution: {integrity: sha512-SZs7ekbP8CN0txVG2xVRH6EgKmEm31BOxA07vkFaETzZz1xh+cbt8BcI0slpymvwhx5dlFnQG2rTlPVQn+iRPQ==}
|
||||||
dev: false
|
dev: false
|
||||||
|
|
||||||
|
/@types/http-errors@2.0.1:
|
||||||
|
resolution: {integrity: sha512-/K3ds8TRAfBvi5vfjuz8y6+GiAYBZ0x4tXv1Av6CWBWn0IlADc+ZX9pMq7oU0fNQPnBwIZl3rmeLp6SBApbxSQ==}
|
||||||
|
dev: false
|
||||||
|
|
||||||
/@types/json-schema@7.0.12:
|
/@types/json-schema@7.0.12:
|
||||||
resolution: {integrity: sha512-Hr5Jfhc9eYOQNPYO5WLDq/n4jqijdHNlDXjuAQkkt+mWdQR+XJToOHrsD4cPaMXpn6KO7y2+wM8AZEs8VpBLVA==}
|
resolution: {integrity: sha512-Hr5Jfhc9eYOQNPYO5WLDq/n4jqijdHNlDXjuAQkkt+mWdQR+XJToOHrsD4cPaMXpn6KO7y2+wM8AZEs8VpBLVA==}
|
||||||
dev: true
|
dev: true
|
||||||
|
@ -1110,16 +1260,39 @@ packages:
|
||||||
resolution: {integrity: sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ==}
|
resolution: {integrity: sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ==}
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
|
/@types/keygrip@1.0.2:
|
||||||
|
resolution: {integrity: sha512-GJhpTepz2udxGexqos8wgaBx4I/zWIDPh/KOGEwAqtuGDkOUJu5eFvwmdBX4AmB8Odsr+9pHCQqiAqDL/yKMKw==}
|
||||||
|
dev: false
|
||||||
|
|
||||||
/@types/keyv@3.1.4:
|
/@types/keyv@3.1.4:
|
||||||
resolution: {integrity: sha512-BQ5aZNSCpj7D6K2ksrRCTmKRLEpnPvWDiLPfoGyhZ++8YtiK9d/3DBKPJgry359X/P1PfruyYwvnvwFjuEiEIg==}
|
resolution: {integrity: sha512-BQ5aZNSCpj7D6K2ksrRCTmKRLEpnPvWDiLPfoGyhZ++8YtiK9d/3DBKPJgry359X/P1PfruyYwvnvwFjuEiEIg==}
|
||||||
dependencies:
|
dependencies:
|
||||||
'@types/node': 20.4.9
|
'@types/node': 20.4.9
|
||||||
dev: false
|
dev: false
|
||||||
|
|
||||||
|
/@types/mime@1.3.2:
|
||||||
|
resolution: {integrity: sha512-YATxVxgRqNH6nHEIsvg6k2Boc1JHI9ZbH5iWFFv/MTkchz3b1ieGDa5T0a9RznNdI0KhVbdbWSN+KWWrQZRxTw==}
|
||||||
|
dev: false
|
||||||
|
|
||||||
|
/@types/mime@3.0.1:
|
||||||
|
resolution: {integrity: sha512-Y4XFY5VJAuw0FgAqPNd6NNoV44jbq9Bz2L7Rh/J6jLTiHBSBJa9fxqQIvkIld4GsoDOcCbvzOUAbLPsSKKg+uA==}
|
||||||
|
dev: false
|
||||||
|
|
||||||
|
/@types/node-fetch@2.6.2:
|
||||||
|
resolution: {integrity: sha512-DHqhlq5jeESLy19TYhLakJ07kNumXWjcDdxXsLUMJZ6ue8VZJj4kLPQVE/2mdHh3xZziNF1xppu5lwmS53HR+A==}
|
||||||
|
dependencies:
|
||||||
|
'@types/node': 20.4.9
|
||||||
|
form-data: 3.0.1
|
||||||
|
dev: false
|
||||||
|
|
||||||
/@types/node@12.20.55:
|
/@types/node@12.20.55:
|
||||||
resolution: {integrity: sha512-J8xLz7q2OFulZ2cyGTLE1TbbZcjpno7FaN6zdJNrgAdrJ+DZzh/uFR6YrTb4C+nXakvud8Q4+rbhoIWlYQbUFQ==}
|
resolution: {integrity: sha512-J8xLz7q2OFulZ2cyGTLE1TbbZcjpno7FaN6zdJNrgAdrJ+DZzh/uFR6YrTb4C+nXakvud8Q4+rbhoIWlYQbUFQ==}
|
||||||
dev: false
|
dev: false
|
||||||
|
|
||||||
|
/@types/node@16.18.6:
|
||||||
|
resolution: {integrity: sha512-vmYJF0REqDyyU0gviezF/KHq/fYaUbFhkcNbQCuPGFQj6VTbXuHZoxs/Y7mutWe73C8AC6l9fFu8mSYiBAqkGA==}
|
||||||
|
dev: false
|
||||||
|
|
||||||
/@types/node@20.4.9:
|
/@types/node@20.4.9:
|
||||||
resolution: {integrity: sha512-8e2HYcg7ohnTUbHk8focoklEQYvemQmu9M/f43DZVx43kHn0tE3BY/6gSDxS7k0SprtS0NHvj+L80cGLnoOUcQ==}
|
resolution: {integrity: sha512-8e2HYcg7ohnTUbHk8focoklEQYvemQmu9M/f43DZVx43kHn0tE3BY/6gSDxS7k0SprtS0NHvj+L80cGLnoOUcQ==}
|
||||||
|
|
||||||
|
@ -1131,6 +1304,14 @@ packages:
|
||||||
resolution: {integrity: sha512-JCB8C6SnDoQf0cNycqd/35A7MjcnK+ZTqE7judS6o7utxUCg6imJg3QK2qzHKszlTjcj2cn+NwMB2i96ubpj7w==}
|
resolution: {integrity: sha512-JCB8C6SnDoQf0cNycqd/35A7MjcnK+ZTqE7judS6o7utxUCg6imJg3QK2qzHKszlTjcj2cn+NwMB2i96ubpj7w==}
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
|
/@types/qs@6.9.7:
|
||||||
|
resolution: {integrity: sha512-FGa1F62FT09qcrueBA6qYTrJPVDzah9a+493+o2PCXsesWHIn27G98TsSMs3WPNbZIEj4+VJf6saSFpvD+3Zsw==}
|
||||||
|
dev: false
|
||||||
|
|
||||||
|
/@types/range-parser@1.2.4:
|
||||||
|
resolution: {integrity: sha512-EEhsLsD6UsDM1yFhAvy0Cjr6VwmpMWqFBCb9w07wVugF7w9nfajxLuVmngTIpgS6svCnm6Vaw+MZhoDCKnOfsw==}
|
||||||
|
dev: false
|
||||||
|
|
||||||
/@types/react@18.2.20:
|
/@types/react@18.2.20:
|
||||||
resolution: {integrity: sha512-WKNtmsLWJM/3D5mG4U84cysVY31ivmyw85dE84fOCk5Hx78wezB/XEjVPWl2JTZ5FkEeaTJf+VgUAUn3PE7Isw==}
|
resolution: {integrity: sha512-WKNtmsLWJM/3D5mG4U84cysVY31ivmyw85dE84fOCk5Hx78wezB/XEjVPWl2JTZ5FkEeaTJf+VgUAUn3PE7Isw==}
|
||||||
dependencies:
|
dependencies:
|
||||||
|
@ -1153,6 +1334,21 @@ packages:
|
||||||
resolution: {integrity: sha512-G8hZ6XJiHnuhQKR7ZmysCeJWE08o8T0AXtk5darsCaTVsYZhhgUrq53jizaR2FvsoeCwJhlmwTjkXBY5Pn/ZHw==}
|
resolution: {integrity: sha512-G8hZ6XJiHnuhQKR7ZmysCeJWE08o8T0AXtk5darsCaTVsYZhhgUrq53jizaR2FvsoeCwJhlmwTjkXBY5Pn/ZHw==}
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
|
/@types/send@0.17.1:
|
||||||
|
resolution: {integrity: sha512-Cwo8LE/0rnvX7kIIa3QHCkcuF21c05Ayb0ZfxPiv0W8VRiZiNW/WuRupHKpqqGVGf7SUA44QSOUKaEd9lIrd/Q==}
|
||||||
|
dependencies:
|
||||||
|
'@types/mime': 1.3.2
|
||||||
|
'@types/node': 20.4.9
|
||||||
|
dev: false
|
||||||
|
|
||||||
|
/@types/serve-static@1.15.2:
|
||||||
|
resolution: {integrity: sha512-J2LqtvFYCzaj8pVYKw8klQXrLLk7TBZmQ4ShlcdkELFKGwGMfevMLneMMRkMgZxotOD9wg497LpC7O8PcvAmfw==}
|
||||||
|
dependencies:
|
||||||
|
'@types/http-errors': 2.0.1
|
||||||
|
'@types/mime': 3.0.1
|
||||||
|
'@types/node': 20.4.9
|
||||||
|
dev: false
|
||||||
|
|
||||||
/@typescript-eslint/eslint-plugin@6.3.0(@typescript-eslint/parser@6.3.0)(eslint@8.46.0)(typescript@5.1.6):
|
/@typescript-eslint/eslint-plugin@6.3.0(@typescript-eslint/parser@6.3.0)(eslint@8.46.0)(typescript@5.1.6):
|
||||||
resolution: {integrity: sha512-IZYjYZ0ifGSLZbwMqIip/nOamFiWJ9AH+T/GYNZBWkVcyNQOFGtSMoWV7RvY4poYCMZ/4lHzNl796WOSNxmk8A==}
|
resolution: {integrity: sha512-IZYjYZ0ifGSLZbwMqIip/nOamFiWJ9AH+T/GYNZBWkVcyNQOFGtSMoWV7RvY4poYCMZ/4lHzNl796WOSNxmk8A==}
|
||||||
engines: {node: ^16.0.0 || >=18.0.0}
|
engines: {node: ^16.0.0 || >=18.0.0}
|
||||||
|
@ -1502,6 +1698,15 @@ packages:
|
||||||
is-shared-array-buffer: 1.0.2
|
is-shared-array-buffer: 1.0.2
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
|
/asn1js@3.0.5:
|
||||||
|
resolution: {integrity: sha512-FVnvrKJwpt9LP2lAMl8qZswRNm3T4q9CON+bxldk2iwk3FFpuwhx2FfinyitizWHsVYyaY+y5JzDR0rCMV5yTQ==}
|
||||||
|
engines: {node: '>=12.0.0'}
|
||||||
|
dependencies:
|
||||||
|
pvtsutils: 1.3.3
|
||||||
|
pvutils: 1.1.3
|
||||||
|
tslib: 2.6.1
|
||||||
|
dev: false
|
||||||
|
|
||||||
/ast-types-flow@0.0.7:
|
/ast-types-flow@0.0.7:
|
||||||
resolution: {integrity: sha512-eBvWn1lvIApYMhzQMsu9ciLfkBY499mFZlNqG+/9WR7PVlroQw0vG30cOQQbaKz3sCEc44TAOu2ykzqXSNnwag==}
|
resolution: {integrity: sha512-eBvWn1lvIApYMhzQMsu9ciLfkBY499mFZlNqG+/9WR7PVlroQw0vG30cOQQbaKz3sCEc44TAOu2ykzqXSNnwag==}
|
||||||
dev: true
|
dev: true
|
||||||
|
@ -1702,6 +1907,20 @@ packages:
|
||||||
resolution: {integrity: sha512-QOSvevhslijgYwRx6Rv7zKdMF8lbRmx+uQGx2+vDc+KI/eBnsy9kit5aj23AgGu3pa4t9AgwbnXWqS+iOY+2aA==}
|
resolution: {integrity: sha512-QOSvevhslijgYwRx6Rv7zKdMF8lbRmx+uQGx2+vDc+KI/eBnsy9kit5aj23AgGu3pa4t9AgwbnXWqS+iOY+2aA==}
|
||||||
engines: {node: '>= 6'}
|
engines: {node: '>= 6'}
|
||||||
|
|
||||||
|
/camelcase-keys@6.2.2:
|
||||||
|
resolution: {integrity: sha512-YrwaA0vEKazPBkn0ipTiMpSajYDSe+KjQfrjhcBMxJt/znbvlHd8Pw/Vamaz5EB4Wfhs3SUR3Z9mwRu/P3s3Yg==}
|
||||||
|
engines: {node: '>=8'}
|
||||||
|
dependencies:
|
||||||
|
camelcase: 5.3.1
|
||||||
|
map-obj: 4.3.0
|
||||||
|
quick-lru: 4.0.1
|
||||||
|
dev: false
|
||||||
|
|
||||||
|
/camelcase@5.3.1:
|
||||||
|
resolution: {integrity: sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==}
|
||||||
|
engines: {node: '>=6'}
|
||||||
|
dev: false
|
||||||
|
|
||||||
/caniuse-lite@1.0.30001519:
|
/caniuse-lite@1.0.30001519:
|
||||||
resolution: {integrity: sha512-0QHgqR+Jv4bxHMp8kZ1Kn8CH55OikjKJ6JmKkZYP1F3D7w+lnFXF70nG5eNfsZS89jadi5Ywy5UCSKLAglIRkg==}
|
resolution: {integrity: sha512-0QHgqR+Jv4bxHMp8kZ1Kn8CH55OikjKJ6JmKkZYP1F3D7w+lnFXF70nG5eNfsZS89jadi5Ywy5UCSKLAglIRkg==}
|
||||||
dev: false
|
dev: false
|
||||||
|
@ -1926,6 +2145,10 @@ packages:
|
||||||
engines: {node: '>=4'}
|
engines: {node: '>=4'}
|
||||||
hasBin: true
|
hasBin: true
|
||||||
|
|
||||||
|
/csstype@3.1.1:
|
||||||
|
resolution: {integrity: sha512-DJR/VvkAvSZW9bTouZue2sSxDwdTN92uHjqeKVm+0dAqdfNykRzQ95tay8aXMBAAPpUiq4Qcug2L7neoRh2Egw==}
|
||||||
|
dev: false
|
||||||
|
|
||||||
/csstype@3.1.2:
|
/csstype@3.1.2:
|
||||||
resolution: {integrity: sha512-I7K1Uu0MBPzaFKg4nI5Q7Vs2t+3gWWW648spaF+Rg7pI9ds18Ugn+lvg4SHczUdKlHI5LWBXyqfS8+DufyBsgQ==}
|
resolution: {integrity: sha512-I7K1Uu0MBPzaFKg4nI5Q7Vs2t+3gWWW648spaF+Rg7pI9ds18Ugn+lvg4SHczUdKlHI5LWBXyqfS8+DufyBsgQ==}
|
||||||
dev: true
|
dev: true
|
||||||
|
@ -1986,6 +2209,11 @@ packages:
|
||||||
resolution: {integrity: sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==}
|
resolution: {integrity: sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==}
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
|
/deepmerge@4.2.2:
|
||||||
|
resolution: {integrity: sha512-FJ3UgI4gIl+PHZm53knsuSFpE+nESMr7M4v9QcgB7S63Kj/6WqMiFQJpBBYz1Pt+66bZpP3Q7Lye0Oo9MPKEdg==}
|
||||||
|
engines: {node: '>=0.10.0'}
|
||||||
|
dev: false
|
||||||
|
|
||||||
/deepmerge@4.3.1:
|
/deepmerge@4.3.1:
|
||||||
resolution: {integrity: sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==}
|
resolution: {integrity: sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==}
|
||||||
engines: {node: '>=0.10.0'}
|
engines: {node: '>=0.10.0'}
|
||||||
|
@ -2121,6 +2349,13 @@ packages:
|
||||||
domhandler: 5.0.3
|
domhandler: 5.0.3
|
||||||
dev: false
|
dev: false
|
||||||
|
|
||||||
|
/dot-case@3.0.4:
|
||||||
|
resolution: {integrity: sha512-Kv5nKlh6yRrdrGvxeJ2e5y2eRUpkUosIW4A2AS38zwSz27zu7ufDwQPi5Jhs3XAlGNetl3bmnGhQsMtkKJnj3w==}
|
||||||
|
dependencies:
|
||||||
|
no-case: 3.0.4
|
||||||
|
tslib: 2.6.1
|
||||||
|
dev: false
|
||||||
|
|
||||||
/duplexer2@0.1.4:
|
/duplexer2@0.1.4:
|
||||||
resolution: {integrity: sha512-asLFVfWWtJ90ZyOUHMqk7/S2w2guQKxUI2itj3d92ADHhxUSbCMGi1f1cBcJ7xM1To+pE/Khbwo1yuNbMEPKeA==}
|
resolution: {integrity: sha512-asLFVfWWtJ90ZyOUHMqk7/S2w2guQKxUI2itj3d92ADHhxUSbCMGi1f1cBcJ7xM1To+pE/Khbwo1yuNbMEPKeA==}
|
||||||
dependencies:
|
dependencies:
|
||||||
|
@ -2696,6 +2931,15 @@ packages:
|
||||||
is-callable: 1.2.7
|
is-callable: 1.2.7
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
|
/form-data@3.0.1:
|
||||||
|
resolution: {integrity: sha512-RHkBKtLWUVwd7SqRIvCZMEvAMoGUp0XU+seQiZejj0COz3RI3hWP4sCv3gZWWLjJTd7rGwcsF5eKZGii0r/hbg==}
|
||||||
|
engines: {node: '>= 6'}
|
||||||
|
dependencies:
|
||||||
|
asynckit: 0.4.0
|
||||||
|
combined-stream: 1.0.8
|
||||||
|
mime-types: 2.1.35
|
||||||
|
dev: false
|
||||||
|
|
||||||
/form-data@4.0.0:
|
/form-data@4.0.0:
|
||||||
resolution: {integrity: sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==}
|
resolution: {integrity: sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==}
|
||||||
engines: {node: '>= 6'}
|
engines: {node: '>= 6'}
|
||||||
|
@ -3344,6 +3588,11 @@ packages:
|
||||||
nopt: 6.0.0
|
nopt: 6.0.0
|
||||||
dev: false
|
dev: false
|
||||||
|
|
||||||
|
/js-cookie@3.0.1:
|
||||||
|
resolution: {integrity: sha512-+0rgsUXZu4ncpPxRL+lNEptWMOWl9etvPHc/koSRp6MPwpRYAhmk0dUG00J4bxVV3r9uUzfo24wW0knS07SKSw==}
|
||||||
|
engines: {node: '>=12'}
|
||||||
|
dev: false
|
||||||
|
|
||||||
/js-tokens@4.0.0:
|
/js-tokens@4.0.0:
|
||||||
resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==}
|
resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==}
|
||||||
|
|
||||||
|
@ -3505,6 +3754,12 @@ packages:
|
||||||
dependencies:
|
dependencies:
|
||||||
js-tokens: 4.0.0
|
js-tokens: 4.0.0
|
||||||
|
|
||||||
|
/lower-case@2.0.2:
|
||||||
|
resolution: {integrity: sha512-7fm3l3NAF9WfN6W3JOmf5drwpVqX78JtoGJ3A6W0a6ZnldM41w2fV5D490psKFTpMds8TJse/eHLFFsNHHjHgg==}
|
||||||
|
dependencies:
|
||||||
|
tslib: 2.6.1
|
||||||
|
dev: false
|
||||||
|
|
||||||
/lowercase-keys@2.0.0:
|
/lowercase-keys@2.0.0:
|
||||||
resolution: {integrity: sha512-tqNXrS78oMOE73NMxK4EMLQsQowWf8jKooH9g7xPavRT706R6bkQJ6DY2Te7QukaZsulxa30wQ7bk0pm4XiHmA==}
|
resolution: {integrity: sha512-tqNXrS78oMOE73NMxK4EMLQsQowWf8jKooH9g7xPavRT706R6bkQJ6DY2Te7QukaZsulxa30wQ7bk0pm4XiHmA==}
|
||||||
engines: {node: '>=8'}
|
engines: {node: '>=8'}
|
||||||
|
@ -3516,6 +3771,11 @@ packages:
|
||||||
dependencies:
|
dependencies:
|
||||||
yallist: 4.0.0
|
yallist: 4.0.0
|
||||||
|
|
||||||
|
/map-obj@4.3.0:
|
||||||
|
resolution: {integrity: sha512-hdN1wVrZbb29eBGiGjJbeP8JbKjq1urkHJ/LIP/NY48MZ1QVXUsQBV1G1zvYFHn1XE06cwjBsOI2K3Ulnj1YXQ==}
|
||||||
|
engines: {node: '>=8'}
|
||||||
|
dev: false
|
||||||
|
|
||||||
/media-typer@0.3.0:
|
/media-typer@0.3.0:
|
||||||
resolution: {integrity: sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==}
|
resolution: {integrity: sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==}
|
||||||
engines: {node: '>= 0.6'}
|
engines: {node: '>= 0.6'}
|
||||||
|
@ -3723,6 +3983,13 @@ packages:
|
||||||
next: 13.4.13(react-dom@18.2.0)(react@18.2.0)
|
next: 13.4.13(react-dom@18.2.0)(react@18.2.0)
|
||||||
dev: false
|
dev: false
|
||||||
|
|
||||||
|
/no-case@3.0.4:
|
||||||
|
resolution: {integrity: sha512-fgAN3jGAh+RoxUGZHTSOLJIqUc2wmoBwGR4tbpNAKmmovFoWq0OdRkb0VkldReO2a2iBT/OEulG9XSUc10r3zg==}
|
||||||
|
dependencies:
|
||||||
|
lower-case: 2.0.2
|
||||||
|
tslib: 2.6.1
|
||||||
|
dev: false
|
||||||
|
|
||||||
/node-abi@3.45.0:
|
/node-abi@3.45.0:
|
||||||
resolution: {integrity: sha512-iwXuFrMAcFVi/ZoZiqq8BzAdsLw9kxDfTC0HMyjXfSL/6CSDAGD5UmR7azrAgWV1zKYq7dUUMj4owusBWKLsiQ==}
|
resolution: {integrity: sha512-iwXuFrMAcFVi/ZoZiqq8BzAdsLw9kxDfTC0HMyjXfSL/6CSDAGD5UmR7azrAgWV1zKYq7dUUMj4owusBWKLsiQ==}
|
||||||
engines: {node: '>=10'}
|
engines: {node: '>=10'}
|
||||||
|
@ -3734,6 +4001,10 @@ packages:
|
||||||
resolution: {integrity: sha512-+eawOlIgy680F0kBzPUNFhMZGtJ1YmqM6l4+Crf4IkImjYrO/mqPwRMh352g23uIaQKFItcQ64I7KMaJxHgAVA==}
|
resolution: {integrity: sha512-+eawOlIgy680F0kBzPUNFhMZGtJ1YmqM6l4+Crf4IkImjYrO/mqPwRMh352g23uIaQKFItcQ64I7KMaJxHgAVA==}
|
||||||
dev: false
|
dev: false
|
||||||
|
|
||||||
|
/node-fetch-native@1.0.1:
|
||||||
|
resolution: {integrity: sha512-VzW+TAk2wE4X9maiKMlT+GsPU4OMmR1U9CrHSmd3DFLn2IcZ9VJ6M6BBugGfYUnPCLSYxXdZy17M0BEJyhUTwg==}
|
||||||
|
dev: false
|
||||||
|
|
||||||
/node-fetch@2.6.12:
|
/node-fetch@2.6.12:
|
||||||
resolution: {integrity: sha512-C/fGU2E8ToujUivIO0H+tpQ6HWo4eEmchoPIoXtxCrVghxdKq+QOHqEZW7tuP3KlV3bC8FRMO5nMCC7Zm1VP6g==}
|
resolution: {integrity: sha512-C/fGU2E8ToujUivIO0H+tpQ6HWo4eEmchoPIoXtxCrVghxdKq+QOHqEZW7tuP3KlV3bC8FRMO5nMCC7Zm1VP6g==}
|
||||||
engines: {node: 4.x || >=6.0.0}
|
engines: {node: 4.x || >=6.0.0}
|
||||||
|
@ -4025,6 +4296,10 @@ packages:
|
||||||
/path-parse@1.0.7:
|
/path-parse@1.0.7:
|
||||||
resolution: {integrity: sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==}
|
resolution: {integrity: sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==}
|
||||||
|
|
||||||
|
/path-to-regexp@6.2.1:
|
||||||
|
resolution: {integrity: sha512-JLyh7xT1kizaEvcaXOQwOc2/Yhw6KZOvPf1S8401UyLk86CU79LN3vl7ztXGm/pZ+YjoyAJ4rxmHwbkBXJX+yw==}
|
||||||
|
dev: false
|
||||||
|
|
||||||
/path-type@4.0.0:
|
/path-type@4.0.0:
|
||||||
resolution: {integrity: sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==}
|
resolution: {integrity: sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==}
|
||||||
engines: {node: '>=8'}
|
engines: {node: '>=8'}
|
||||||
|
@ -4307,6 +4582,17 @@ packages:
|
||||||
engines: {node: '>=6'}
|
engines: {node: '>=6'}
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
|
/pvtsutils@1.3.3:
|
||||||
|
resolution: {integrity: sha512-6sAOMlXyrJ+8tRN5IAaYfuYZRp1C2uJ0SyDynEFxL+VY8kCRib9Lpj/+KPaNFpaQWr/iRik5nrzz6iaNlxgEGA==}
|
||||||
|
dependencies:
|
||||||
|
tslib: 2.6.1
|
||||||
|
dev: false
|
||||||
|
|
||||||
|
/pvutils@1.1.3:
|
||||||
|
resolution: {integrity: sha512-pMpnA0qRdFp32b1sJl1wOJNxZLQ2cbQx+k6tjNtZ8CpvVhNqEPRgivZ2WOUev2YMajecdH7ctUPDvEe87nariQ==}
|
||||||
|
engines: {node: '>=6.0.0'}
|
||||||
|
dev: false
|
||||||
|
|
||||||
/qs@6.11.2:
|
/qs@6.11.2:
|
||||||
resolution: {integrity: sha512-tDNIz22aBzCDxLtVH++VnTfzxlfeK5CbqohpSqpJgj1Wg/cQbStNAz3NuqCs5vV+pjBsK4x4pN9HlVh7rcYRiA==}
|
resolution: {integrity: sha512-tDNIz22aBzCDxLtVH++VnTfzxlfeK5CbqohpSqpJgj1Wg/cQbStNAz3NuqCs5vV+pjBsK4x4pN9HlVh7rcYRiA==}
|
||||||
engines: {node: '>=0.6'}
|
engines: {node: '>=0.6'}
|
||||||
|
@ -4321,6 +4607,11 @@ packages:
|
||||||
resolution: {integrity: sha512-kJt5qhMxoszgU/62PLP1CJytzd2NKetjSRnyuj31fDd3Rlcz3fzlFdFLD1SItunPwyqEOkca6GbV612BWfaBag==}
|
resolution: {integrity: sha512-kJt5qhMxoszgU/62PLP1CJytzd2NKetjSRnyuj31fDd3Rlcz3fzlFdFLD1SItunPwyqEOkca6GbV612BWfaBag==}
|
||||||
dev: false
|
dev: false
|
||||||
|
|
||||||
|
/quick-lru@4.0.1:
|
||||||
|
resolution: {integrity: sha512-ARhCpm70fzdcvNQfPoy49IaanKkTlRWF2JMzqhcJbhSFRZv7nPTvZJdcY7301IPmvW+/p0RgIWnQDLJxifsQ7g==}
|
||||||
|
engines: {node: '>=8'}
|
||||||
|
dev: false
|
||||||
|
|
||||||
/quick-lru@5.1.1:
|
/quick-lru@5.1.1:
|
||||||
resolution: {integrity: sha512-WuyALRjWPDGtt/wzJiadO5AXY+8hZ80hVpe6MyivgraREW751X3SbhRvG3eLKOYN+8VEvqLcf3wdnt44Z4S4SA==}
|
resolution: {integrity: sha512-WuyALRjWPDGtt/wzJiadO5AXY+8hZ80hVpe6MyivgraREW751X3SbhRvG3eLKOYN+8VEvqLcf3wdnt44Z4S4SA==}
|
||||||
engines: {node: '>=10'}
|
engines: {node: '>=10'}
|
||||||
|
@ -4704,6 +4995,30 @@ packages:
|
||||||
resolution: {integrity: sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==}
|
resolution: {integrity: sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==}
|
||||||
engines: {node: '>=8'}
|
engines: {node: '>=8'}
|
||||||
|
|
||||||
|
/snake-case@3.0.4:
|
||||||
|
resolution: {integrity: sha512-LAOh4z89bGQvl9pFfNF8V146i7o7/CqFPbqzYgP+yYzDIDeS9HaNFtXABamRW+AQzEVODcvE79ljJ+8a9YSdMg==}
|
||||||
|
dependencies:
|
||||||
|
dot-case: 3.0.4
|
||||||
|
tslib: 2.6.1
|
||||||
|
dev: false
|
||||||
|
|
||||||
|
/snakecase-keys@3.2.1:
|
||||||
|
resolution: {integrity: sha512-CjU5pyRfwOtaOITYv5C8DzpZ8XA/ieRsDpr93HI2r6e3YInC6moZpSQbmUtg8cTk58tq2x3jcG2gv+p1IZGmMA==}
|
||||||
|
engines: {node: '>=8'}
|
||||||
|
dependencies:
|
||||||
|
map-obj: 4.3.0
|
||||||
|
to-snake-case: 1.0.0
|
||||||
|
dev: false
|
||||||
|
|
||||||
|
/snakecase-keys@5.4.4:
|
||||||
|
resolution: {integrity: sha512-YTywJG93yxwHLgrYLZjlC75moVEX04LZM4FHfihjHe1FCXm+QaLOFfSf535aXOAd0ArVQMWUAe8ZPm4VtWyXaA==}
|
||||||
|
engines: {node: '>=12'}
|
||||||
|
dependencies:
|
||||||
|
map-obj: 4.3.0
|
||||||
|
snake-case: 3.0.4
|
||||||
|
type-fest: 2.19.0
|
||||||
|
dev: false
|
||||||
|
|
||||||
/source-map-js@1.0.2:
|
/source-map-js@1.0.2:
|
||||||
resolution: {integrity: sha512-R0XvVJ9WusLiqTCEiGCmICCMplcCkIwwR11mOSD9CR5u+IXYdiseeEuXCVAjS54zqwkLcPNnmU4OeJ6tUrWhDw==}
|
resolution: {integrity: sha512-R0XvVJ9WusLiqTCEiGCmICCMplcCkIwwR11mOSD9CR5u+IXYdiseeEuXCVAjS54zqwkLcPNnmU4OeJ6tUrWhDw==}
|
||||||
engines: {node: '>=0.10.0'}
|
engines: {node: '>=0.10.0'}
|
||||||
|
@ -4893,6 +5208,14 @@ packages:
|
||||||
resolution: {integrity: sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==}
|
resolution: {integrity: sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==}
|
||||||
engines: {node: '>= 0.4'}
|
engines: {node: '>= 0.4'}
|
||||||
|
|
||||||
|
/swr@1.3.0(react@18.2.0):
|
||||||
|
resolution: {integrity: sha512-dkghQrOl2ORX9HYrMDtPa7LTVHJjCTeZoB1dqTbnnEDlSvN8JEKpYIYurDfvbQFUUS8Cg8PceFVZNkW0KNNYPw==}
|
||||||
|
peerDependencies:
|
||||||
|
react: ^16.11.0 || ^17.0.0 || ^18.0.0
|
||||||
|
dependencies:
|
||||||
|
react: 18.2.0
|
||||||
|
dev: false
|
||||||
|
|
||||||
/tailwindcss@3.2.7(postcss@8.4.21):
|
/tailwindcss@3.2.7(postcss@8.4.21):
|
||||||
resolution: {integrity: sha512-B6DLqJzc21x7wntlH/GsZwEXTBttVSl1FtCzC8WP4oBc/NKef7kaax5jeihkkCEWc831/5NDJ9gRNDK6NEioQQ==}
|
resolution: {integrity: sha512-B6DLqJzc21x7wntlH/GsZwEXTBttVSl1FtCzC8WP4oBc/NKef7kaax5jeihkkCEWc831/5NDJ9gRNDK6NEioQQ==}
|
||||||
engines: {node: '>=12.13.0'}
|
engines: {node: '>=12.13.0'}
|
||||||
|
@ -5016,12 +5339,28 @@ packages:
|
||||||
any-promise: 1.3.0
|
any-promise: 1.3.0
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
|
/to-no-case@1.0.2:
|
||||||
|
resolution: {integrity: sha512-Z3g735FxuZY8rodxV4gH7LxClE4H0hTIyHNIHdk+vpQxjLm0cwnKXq/OFVZ76SOQmto7txVcwSCwkU5kqp+FKg==}
|
||||||
|
dev: false
|
||||||
|
|
||||||
/to-regex-range@5.0.1:
|
/to-regex-range@5.0.1:
|
||||||
resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==}
|
resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==}
|
||||||
engines: {node: '>=8.0'}
|
engines: {node: '>=8.0'}
|
||||||
dependencies:
|
dependencies:
|
||||||
is-number: 7.0.0
|
is-number: 7.0.0
|
||||||
|
|
||||||
|
/to-snake-case@1.0.0:
|
||||||
|
resolution: {integrity: sha512-joRpzBAk1Bhi2eGEYBjukEWHOe/IvclOkiJl3DtA91jV6NwQ3MwXA4FHYeqk8BNp/D8bmi9tcNbRu/SozP0jbQ==}
|
||||||
|
dependencies:
|
||||||
|
to-space-case: 1.0.0
|
||||||
|
dev: false
|
||||||
|
|
||||||
|
/to-space-case@1.0.0:
|
||||||
|
resolution: {integrity: sha512-rLdvwXZ39VOn1IxGL3V6ZstoTbwLRckQmn/U8ZDLuWwIXNpuZDhQ3AiRUlhTbOXFVE9C+dR51wM0CBDhk31VcA==}
|
||||||
|
dependencies:
|
||||||
|
to-no-case: 1.0.2
|
||||||
|
dev: false
|
||||||
|
|
||||||
/to-utf8@0.0.1:
|
/to-utf8@0.0.1:
|
||||||
resolution: {integrity: sha512-zks18/TWT1iHO3v0vFp5qLKOG27m67ycq/Y7a7cTiRuUNlc4gf3HGnkRgMv0NyhnfTamtkYBJl+YeD1/j07gBQ==}
|
resolution: {integrity: sha512-zks18/TWT1iHO3v0vFp5qLKOG27m67ycq/Y7a7cTiRuUNlc4gf3HGnkRgMv0NyhnfTamtkYBJl+YeD1/j07gBQ==}
|
||||||
dev: false
|
dev: false
|
||||||
|
@ -5086,6 +5425,10 @@ packages:
|
||||||
strip-bom: 3.0.0
|
strip-bom: 3.0.0
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
|
/tslib@2.4.1:
|
||||||
|
resolution: {integrity: sha512-tGyy4dAjRIEwI7BzsB0lynWgOpfqjUdq91XXAlIWD2OwKBH7oCl/GZG/HT4BOHrTlPMOASlMQ7veyTqpmRcrNA==}
|
||||||
|
dev: false
|
||||||
|
|
||||||
/tslib@2.6.1:
|
/tslib@2.6.1:
|
||||||
resolution: {integrity: sha512-t0hLfiEKfMUoqhG+U1oid7Pva4bbDPHYfJNiB7BiIjRkj1pyC++4N3huJfqY6aRH6VTB0rvtzQwjM4K6qpfOig==}
|
resolution: {integrity: sha512-t0hLfiEKfMUoqhG+U1oid7Pva4bbDPHYfJNiB7BiIjRkj1pyC++4N3huJfqY6aRH6VTB0rvtzQwjM4K6qpfOig==}
|
||||||
dev: false
|
dev: false
|
||||||
|
@ -5124,6 +5467,11 @@ packages:
|
||||||
engines: {node: '>=8'}
|
engines: {node: '>=8'}
|
||||||
dev: false
|
dev: false
|
||||||
|
|
||||||
|
/type-fest@2.19.0:
|
||||||
|
resolution: {integrity: sha512-RAH822pAdBgcNMAfWnCBU3CFZcfZ/i1eZjwFU/dsLKumyuuP3niueg2UAukXYF0E2AAoc82ZSSf9J0WQBinzHA==}
|
||||||
|
engines: {node: '>=12.20'}
|
||||||
|
dev: false
|
||||||
|
|
||||||
/type-fest@3.13.0:
|
/type-fest@3.13.0:
|
||||||
resolution: {integrity: sha512-Gur3yQGM9qiLNs0KPP7LPgeRbio2QTt4xXouobMCarR0/wyW3F+F/+OWwshg3NG0Adon7uQfSZBpB46NfhoF1A==}
|
resolution: {integrity: sha512-Gur3yQGM9qiLNs0KPP7LPgeRbio2QTt4xXouobMCarR0/wyW3F+F/+OWwshg3NG0Adon7uQfSZBpB46NfhoF1A==}
|
||||||
engines: {node: '>=14.16'}
|
engines: {node: '>=14.16'}
|
||||||
|
@ -5291,6 +5639,16 @@ packages:
|
||||||
defaults: 1.0.4
|
defaults: 1.0.4
|
||||||
dev: false
|
dev: false
|
||||||
|
|
||||||
|
/webcrypto-core@1.7.7:
|
||||||
|
resolution: {integrity: sha512-7FjigXNsBfopEj+5DV2nhNpfic2vumtjjgPmeDKk45z+MJwXKKfhPB7118Pfzrmh4jqOMST6Ch37iPAHoImg5g==}
|
||||||
|
dependencies:
|
||||||
|
'@peculiar/asn1-schema': 2.3.6
|
||||||
|
'@peculiar/json-schema': 1.1.12
|
||||||
|
asn1js: 3.0.5
|
||||||
|
pvtsutils: 1.3.3
|
||||||
|
tslib: 2.6.1
|
||||||
|
dev: false
|
||||||
|
|
||||||
/webidl-conversions@3.0.1:
|
/webidl-conversions@3.0.1:
|
||||||
resolution: {integrity: sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==}
|
resolution: {integrity: sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==}
|
||||||
dev: false
|
dev: false
|
||||||
|
|
|
@ -8,62 +8,6 @@ datasource db {
|
||||||
relationMode = "prisma"
|
relationMode = "prisma"
|
||||||
}
|
}
|
||||||
|
|
||||||
model Account {
|
|
||||||
id String @id @default(cuid())
|
|
||||||
createdAt DateTime @default(now())
|
|
||||||
userId String
|
|
||||||
type String
|
|
||||||
provider String
|
|
||||||
providerAccountId String
|
|
||||||
refresh_token String? @db.Text
|
|
||||||
access_token String? @db.Text
|
|
||||||
expires_at Int?
|
|
||||||
token_type String?
|
|
||||||
scope String?
|
|
||||||
id_token String? @db.Text
|
|
||||||
session_state String?
|
|
||||||
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
|
||||||
|
|
||||||
@@unique([provider, providerAccountId])
|
|
||||||
@@index([userId])
|
|
||||||
}
|
|
||||||
|
|
||||||
model Session {
|
|
||||||
id String @id @default(cuid())
|
|
||||||
createdAt DateTime @default(now())
|
|
||||||
sessionToken String @unique
|
|
||||||
userId String
|
|
||||||
expires DateTime
|
|
||||||
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
|
||||||
|
|
||||||
@@index([userId])
|
|
||||||
}
|
|
||||||
|
|
||||||
model User {
|
|
||||||
id String @id @default(cuid())
|
|
||||||
createdAt DateTime @default(now())
|
|
||||||
name String?
|
|
||||||
email String? @unique
|
|
||||||
emailVerified DateTime?
|
|
||||||
image String?
|
|
||||||
accounts Account[]
|
|
||||||
sessions Session[]
|
|
||||||
rooms Room[]
|
|
||||||
votes Vote[]
|
|
||||||
logs Log[]
|
|
||||||
isAdmin Boolean @default(false)
|
|
||||||
isVIP Boolean @default(false)
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
model VerificationToken {
|
|
||||||
identifier String
|
|
||||||
token String @unique
|
|
||||||
expires DateTime
|
|
||||||
|
|
||||||
@@unique([identifier, token])
|
|
||||||
}
|
|
||||||
|
|
||||||
model Room {
|
model Room {
|
||||||
id String @id @unique @default(cuid())
|
id String @id @unique @default(cuid())
|
||||||
createdAt DateTime @default(now())
|
createdAt DateTime @default(now())
|
||||||
|
@ -74,7 +18,6 @@ model Room {
|
||||||
votes Vote[]
|
votes Vote[]
|
||||||
scale String
|
scale String
|
||||||
logs Log[]
|
logs Log[]
|
||||||
owner User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
|
||||||
|
|
||||||
@@index([userId])
|
@@index([userId])
|
||||||
}
|
}
|
||||||
|
@ -85,7 +28,6 @@ model Vote {
|
||||||
userId String
|
userId String
|
||||||
roomId String
|
roomId String
|
||||||
value String
|
value String
|
||||||
owner User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
|
||||||
room Room @relation(fields: [roomId], references: [id], onDelete: Cascade)
|
room Room @relation(fields: [roomId], references: [id], onDelete: Cascade)
|
||||||
|
|
||||||
@@unique([userId, roomId])
|
@@unique([userId, roomId])
|
||||||
|
@ -101,7 +43,6 @@ model Log {
|
||||||
votes Json
|
votes Json
|
||||||
roomName String
|
roomName String
|
||||||
storyName String
|
storyName String
|
||||||
owner User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
|
||||||
room Room @relation(fields: [roomId], references: [id], onDelete: Cascade)
|
room Room @relation(fields: [roomId], references: [id], onDelete: Cascade)
|
||||||
|
|
||||||
@@index([userId])
|
@@index([userId])
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import { signIn, signOut, useSession } from "next-auth/react";
|
import { UserButton, useUser } from "@clerk/nextjs";
|
||||||
import Image from "next/image";
|
import Image from "next/image";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import { useRouter } from "next/router";
|
import { useRouter } from "next/router";
|
||||||
|
@ -9,19 +9,22 @@ interface NavbarProps {
|
||||||
}
|
}
|
||||||
|
|
||||||
const Navbar = ({ title }: NavbarProps) => {
|
const Navbar = ({ title }: NavbarProps) => {
|
||||||
const { data: sessionData, status: sessionStatus } = useSession();
|
const { isLoaded, isSignedIn } = useUser();
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
|
||||||
const navigationMenu = () => {
|
const navigationMenu = () => {
|
||||||
if (sessionStatus === "authenticated" && router.pathname !== "/dashboard") {
|
if (router.pathname !== "/dashboard" && isSignedIn) {
|
||||||
return (
|
return (
|
||||||
<Link className="btn btn-secondary btn-outline mx-2" href="/dashboard">
|
<Link className="btn btn-secondary btn-outline mx-2" href="/dashboard">
|
||||||
Dashboard
|
Dashboard
|
||||||
</Link>
|
</Link>
|
||||||
);
|
);
|
||||||
} else if (sessionStatus === "unauthenticated") {
|
} else if (!isSignedIn) {
|
||||||
return (
|
return (
|
||||||
<button className="btn btn-secondary" onClick={() => void signIn()}>
|
<button
|
||||||
|
className="btn btn-secondary"
|
||||||
|
onClick={() => void router.push("/sign-in")}
|
||||||
|
>
|
||||||
Sign In
|
Sign In
|
||||||
</button>
|
</button>
|
||||||
);
|
);
|
||||||
|
@ -51,7 +54,7 @@ const Navbar = ({ title }: NavbarProps) => {
|
||||||
</Link>
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{sessionStatus === "loading" ? (
|
{!isLoaded ? (
|
||||||
<div className="flex items-center justify-center">
|
<div className="flex items-center justify-center">
|
||||||
<span className="loading loading-dots loading-lg"></span>
|
<span className="loading loading-dots loading-lg"></span>
|
||||||
</div>
|
</div>
|
||||||
|
@ -59,53 +62,7 @@ const Navbar = ({ title }: NavbarProps) => {
|
||||||
navigationMenu()
|
navigationMenu()
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{sessionData?.user.image && (
|
<UserButton afterSignOutUrl="/" />
|
||||||
<div className="flex-none gap-2">
|
|
||||||
<div className="dropdown dropdown-end">
|
|
||||||
<label tabIndex={0} className="btn btn-ghost btn-circle avatar">
|
|
||||||
<div className="w-10 rounded-full">
|
|
||||||
<Image
|
|
||||||
src={sessionData.user.image}
|
|
||||||
alt="Profile picture."
|
|
||||||
height={32}
|
|
||||||
width={32}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</label>
|
|
||||||
<ul
|
|
||||||
tabIndex={0}
|
|
||||||
className="mt-3 p-2 shadow menu menu-compact dropdown-content bg-base-100 rounded-box z-50"
|
|
||||||
>
|
|
||||||
<li>
|
|
||||||
<Link
|
|
||||||
about="Profile Page"
|
|
||||||
href="/profile"
|
|
||||||
className="justify-between"
|
|
||||||
>
|
|
||||||
Profile
|
|
||||||
</Link>
|
|
||||||
</li>
|
|
||||||
{sessionData.user.isAdmin && (
|
|
||||||
<li>
|
|
||||||
<Link
|
|
||||||
about="Admin Page"
|
|
||||||
href="/admin"
|
|
||||||
className="justify-between"
|
|
||||||
>
|
|
||||||
Admin
|
|
||||||
</Link>
|
|
||||||
</li>
|
|
||||||
)}
|
|
||||||
<button
|
|
||||||
className="btn btn-secondary btn-sm text-center whitespace-nowrap"
|
|
||||||
onClick={() => void signOut({ callbackUrl: "/" })}
|
|
||||||
>
|
|
||||||
Sign Out
|
|
||||||
</button>
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</nav>
|
</nav>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
@ -1,25 +1,24 @@
|
||||||
import { useSession } from "next-auth/react";
|
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
|
|
||||||
import { configureAbly, useChannel } from "@ably-labs/react-hooks";
|
import { configureAbly, useChannel } from "@ably-labs/react-hooks";
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import { IoEnterOutline, IoTrashBinOutline } from "react-icons/io5";
|
import { IoEnterOutline, IoTrashBinOutline } from "react-icons/io5";
|
||||||
import { env } from "~/env.mjs";
|
import { env } from "~/env.mjs";
|
||||||
import { api } from "~/utils/api";
|
import { api } from "~/utils/api";
|
||||||
|
import { useUser } from "@clerk/nextjs";
|
||||||
|
|
||||||
const RoomList = () => {
|
const RoomList = () => {
|
||||||
const { data: sessionData } = useSession();
|
const { isSignedIn, user } = useUser();
|
||||||
|
|
||||||
configureAbly({
|
configureAbly({
|
||||||
key: env.NEXT_PUBLIC_ABLY_PUBLIC_KEY,
|
key: env.NEXT_PUBLIC_ABLY_PUBLIC_KEY,
|
||||||
clientId: sessionData?.user.id,
|
clientId: user?.id,
|
||||||
recover: (_, cb) => {
|
recover: (_, cb) => {
|
||||||
cb(true);
|
cb(true);
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const [] = useChannel(
|
const [] = useChannel(
|
||||||
`${env.NEXT_PUBLIC_APP_ENV}-${sessionData ? sessionData.user.id : ""}`,
|
`${env.NEXT_PUBLIC_APP_ENV}-${user?.id}`,
|
||||||
() => void refetchRoomsFromDb()
|
() => void refetchRoomsFromDb()
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@ -27,7 +26,7 @@ const RoomList = () => {
|
||||||
|
|
||||||
const { data: roomsFromDb, refetch: refetchRoomsFromDb } =
|
const { data: roomsFromDb, refetch: refetchRoomsFromDb } =
|
||||||
api.room.getAll.useQuery(undefined, {
|
api.room.getAll.useQuery(undefined, {
|
||||||
enabled: sessionData?.user !== undefined,
|
enabled: isSignedIn,
|
||||||
});
|
});
|
||||||
|
|
||||||
const createRoom = api.room.create.useMutation({});
|
const createRoom = api.room.create.useMutation({});
|
||||||
|
@ -43,7 +42,7 @@ const RoomList = () => {
|
||||||
const deleteRoom = api.room.delete.useMutation({});
|
const deleteRoom = api.room.delete.useMutation({});
|
||||||
|
|
||||||
const deleteRoomHandler = (roomId: string) => {
|
const deleteRoomHandler = (roomId: string) => {
|
||||||
if (sessionData) {
|
if (isSignedIn) {
|
||||||
deleteRoom.mutate({ id: roomId });
|
deleteRoom.mutate({ id: roomId });
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
|
@ -1,83 +0,0 @@
|
||||||
import { configureAbly, useChannel } from "@ably-labs/react-hooks";
|
|
||||||
import { env } from "~/env.mjs";
|
|
||||||
import { api } from "~/utils/api";
|
|
||||||
|
|
||||||
const Stats = () => {
|
|
||||||
configureAbly({
|
|
||||||
key: env.NEXT_PUBLIC_ABLY_PUBLIC_KEY,
|
|
||||||
recover: (_, cb) => {
|
|
||||||
cb(true);
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const [] = useChannel(
|
|
||||||
`${env.NEXT_PUBLIC_APP_ENV}-stats`,
|
|
||||||
() => void refetchData()
|
|
||||||
);
|
|
||||||
|
|
||||||
const {
|
|
||||||
data: usersCount,
|
|
||||||
isLoading: usersCountLoading,
|
|
||||||
isFetching: usersCountFetching,
|
|
||||||
refetch: refetchUsersCount,
|
|
||||||
} = api.rest.userCount.useQuery();
|
|
||||||
const {
|
|
||||||
data: roomsCount,
|
|
||||||
isLoading: roomsCountLoading,
|
|
||||||
isFetching: roomsCountFetching,
|
|
||||||
refetch: refetchRoomsCount,
|
|
||||||
} = api.rest.roomCount.useQuery();
|
|
||||||
const {
|
|
||||||
data: votesCount,
|
|
||||||
isLoading: votesCountLoading,
|
|
||||||
isFetching: votesCountFetching,
|
|
||||||
refetch: refetchVotesCount,
|
|
||||||
} = api.rest.voteCount.useQuery();
|
|
||||||
|
|
||||||
const refetchData = async () => {
|
|
||||||
await Promise.all([
|
|
||||||
refetchUsersCount(),
|
|
||||||
refetchRoomsCount(),
|
|
||||||
refetchVotesCount(),
|
|
||||||
]);
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="stats stats-horizontal shadow bg-neutral m-4">
|
|
||||||
<div className="stat">
|
|
||||||
<div className="stat-title">Users</div>
|
|
||||||
<div className="stat-value">
|
|
||||||
{usersCountLoading || usersCountFetching ? (
|
|
||||||
<span className="loading loading-infinity loading-lg"></span>
|
|
||||||
) : (
|
|
||||||
<>{usersCount ? usersCount : "0"}</>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="stat">
|
|
||||||
<div className="stat-title">Rooms</div>
|
|
||||||
<div className="stat-value">
|
|
||||||
{roomsCountLoading || roomsCountFetching ? (
|
|
||||||
<span className="loading loading-infinity loading-lg"></span>
|
|
||||||
) : (
|
|
||||||
<>{roomsCount ? roomsCount : "0"}</>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="stat">
|
|
||||||
<div className="stat-title">Votes</div>
|
|
||||||
<div className="stat-value">
|
|
||||||
{votesCountLoading || votesCountFetching ? (
|
|
||||||
<span className="loading loading-infinity loading-lg"></span>
|
|
||||||
) : (
|
|
||||||
<>{votesCount ? votesCount : "0"}</>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default Stats;
|
|
18
src/env.mjs
18
src/env.mjs
|
@ -12,17 +12,6 @@ const server = z.object({
|
||||||
UPSTASH_RATELIMIT_REQUESTS: z.string(),
|
UPSTASH_RATELIMIT_REQUESTS: z.string(),
|
||||||
UPSTASH_RATELIMIT_SECONDS: z.string(),
|
UPSTASH_RATELIMIT_SECONDS: z.string(),
|
||||||
NODE_ENV: z.enum(["development", "test", "production"]),
|
NODE_ENV: z.enum(["development", "test", "production"]),
|
||||||
NEXTAUTH_SECRET:
|
|
||||||
process.env.NODE_ENV === "production"
|
|
||||||
? z.string().min(1)
|
|
||||||
: z.string().min(1).optional(),
|
|
||||||
NEXTAUTH_URL: z.preprocess(
|
|
||||||
// This makes Vercel deployments not fail if you don't set NEXTAUTH_URL
|
|
||||||
// Since NextAuth.js automatically uses the VERCEL_URL if present.
|
|
||||||
(str) => process.env.VERCEL_URL ?? str,
|
|
||||||
// VERCEL_URL doesn't include `https` so it cant be validated as a URL
|
|
||||||
process.env.VERCEL ? z.string().min(1) : z.string().url()
|
|
||||||
),
|
|
||||||
GITHUB_CLIENT_ID: z.string(),
|
GITHUB_CLIENT_ID: z.string(),
|
||||||
GITHUB_CLIENT_SECRET: z.string(),
|
GITHUB_CLIENT_SECRET: z.string(),
|
||||||
GOOGLE_CLIENT_ID: z.string(),
|
GOOGLE_CLIENT_ID: z.string(),
|
||||||
|
@ -31,6 +20,7 @@ const server = z.object({
|
||||||
APP_ENV: z.string(),
|
APP_ENV: z.string(),
|
||||||
RESEND_API_KEY: z.string(),
|
RESEND_API_KEY: z.string(),
|
||||||
UNKEY_ROOT_KEY: z.string(),
|
UNKEY_ROOT_KEY: z.string(),
|
||||||
|
CLERK_SECRET_KEY: z.string(),
|
||||||
});
|
});
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -40,6 +30,7 @@ const server = z.object({
|
||||||
const client = z.object({
|
const client = z.object({
|
||||||
NEXT_PUBLIC_ABLY_PUBLIC_KEY: z.string(),
|
NEXT_PUBLIC_ABLY_PUBLIC_KEY: z.string(),
|
||||||
NEXT_PUBLIC_APP_ENV: z.string(),
|
NEXT_PUBLIC_APP_ENV: z.string(),
|
||||||
|
NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY: z.string(),
|
||||||
});
|
});
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -56,8 +47,6 @@ const processEnv = {
|
||||||
UPSTASH_RATELIMIT_REQUESTS: process.env.UPSTASH_RATELIMIT_REQUESTS,
|
UPSTASH_RATELIMIT_REQUESTS: process.env.UPSTASH_RATELIMIT_REQUESTS,
|
||||||
UPSTASH_RATELIMIT_SECONDS: process.env.UPSTASH_RATELIMIT_SECONDS,
|
UPSTASH_RATELIMIT_SECONDS: process.env.UPSTASH_RATELIMIT_SECONDS,
|
||||||
NODE_ENV: process.env.NODE_ENV,
|
NODE_ENV: process.env.NODE_ENV,
|
||||||
NEXTAUTH_SECRET: process.env.NEXTAUTH_SECRET,
|
|
||||||
NEXTAUTH_URL: process.env.NEXTAUTH_URL,
|
|
||||||
GITHUB_CLIENT_ID: process.env.GITHUB_CLIENT_ID,
|
GITHUB_CLIENT_ID: process.env.GITHUB_CLIENT_ID,
|
||||||
GITHUB_CLIENT_SECRET: process.env.GITHUB_CLIENT_SECRET,
|
GITHUB_CLIENT_SECRET: process.env.GITHUB_CLIENT_SECRET,
|
||||||
GOOGLE_CLIENT_ID: process.env.GOOGLE_CLIENT_ID,
|
GOOGLE_CLIENT_ID: process.env.GOOGLE_CLIENT_ID,
|
||||||
|
@ -68,6 +57,9 @@ const processEnv = {
|
||||||
NEXT_PUBLIC_APP_ENV: process.env.NEXT_PUBLIC_APP_ENV,
|
NEXT_PUBLIC_APP_ENV: process.env.NEXT_PUBLIC_APP_ENV,
|
||||||
RESEND_API_KEY: process.env.RESEND_API_KEY,
|
RESEND_API_KEY: process.env.RESEND_API_KEY,
|
||||||
UNKEY_ROOT_KEY: process.env.UNKEY_ROOT_KEY,
|
UNKEY_ROOT_KEY: process.env.UNKEY_ROOT_KEY,
|
||||||
|
NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY:
|
||||||
|
process.env.NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY,
|
||||||
|
CLERK_SECRET_KEY: process.env.CLERK_SECRET_KEY,
|
||||||
};
|
};
|
||||||
|
|
||||||
// Don't touch the part below
|
// Don't touch the part below
|
||||||
|
|
9
src/middleware.ts
Normal file
9
src/middleware.ts
Normal file
|
@ -0,0 +1,9 @@
|
||||||
|
import { authMiddleware } from "@clerk/nextjs";
|
||||||
|
|
||||||
|
export default authMiddleware({
|
||||||
|
publicRoutes: ["/", "/api/(.*)"],
|
||||||
|
});
|
||||||
|
|
||||||
|
export const config = {
|
||||||
|
matcher: ["/((?!.*\\..*|_next).*)", "/", "/(api|trpc)(.*)"],
|
||||||
|
};
|
|
@ -1,56 +1,23 @@
|
||||||
import { type Session } from "next-auth";
|
|
||||||
import { SessionProvider } from "next-auth/react";
|
|
||||||
import { type AppType } from "next/app";
|
import { type AppType } from "next/app";
|
||||||
|
import { ClerkProvider } from "@clerk/nextjs";
|
||||||
|
|
||||||
import { api } from "~/utils/api";
|
import { api } from "~/utils/api";
|
||||||
|
|
||||||
import { useRouter } from "next/router";
|
|
||||||
import { useEffect, useState } from "react";
|
|
||||||
import Footer from "~/components/Footer";
|
import Footer from "~/components/Footer";
|
||||||
import Navbar from "~/components/Navbar";
|
import Navbar from "~/components/Navbar";
|
||||||
import "~/styles/globals.css";
|
import "~/styles/globals.css";
|
||||||
|
|
||||||
const MyApp: AppType<{ session: Session | null }> = ({
|
const MyApp: AppType = ({ Component, pageProps }) => {
|
||||||
Component,
|
|
||||||
pageProps: { session, ...pageProps },
|
|
||||||
}) => {
|
|
||||||
const [pageLoading, setPageLoading] = useState<boolean>(false);
|
|
||||||
const router = useRouter();
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
router.events.on("routeChangeStart", () => {
|
|
||||||
setPageLoading(true);
|
|
||||||
});
|
|
||||||
|
|
||||||
router.events.on("routeChangeComplete", () => {
|
|
||||||
setPageLoading(false);
|
|
||||||
});
|
|
||||||
|
|
||||||
return () => {
|
|
||||||
router.events.off("routeChangeStart", () => {
|
|
||||||
setPageLoading(true);
|
|
||||||
});
|
|
||||||
|
|
||||||
router.events.off("routeChangeComplete", () => {
|
|
||||||
setPageLoading(false);
|
|
||||||
});
|
|
||||||
};
|
|
||||||
}, [router.events]);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<SessionProvider session={ session }>
|
<ClerkProvider {...pageProps}>
|
||||||
<div className="block h-[100%]">
|
<div className="block h-[100%]">
|
||||||
<Navbar title="Sprint Padawan" />
|
<Navbar title="Sprint Padawan" />
|
||||||
<div className="flex flex-row items-center justify-center min-h-[calc(100%-114px)]">
|
<div className="flex flex-row items-center justify-center min-h-[calc(100%-114px)]">
|
||||||
{ pageLoading ? (
|
<Component {...pageProps} />
|
||||||
<span className="loading loading-dots loading-lg"></span>
|
|
||||||
) : (
|
|
||||||
<Component { ...pageProps } />
|
|
||||||
) }
|
|
||||||
</div>
|
</div>
|
||||||
<Footer />
|
<Footer />
|
||||||
</div>
|
</div>
|
||||||
</SessionProvider>
|
</ClerkProvider>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -1,270 +0,0 @@
|
||||||
import { type GetServerSideProps, type NextPage } from "next";
|
|
||||||
import Head from "next/head";
|
|
||||||
|
|
||||||
import { AiOutlineClear } from "react-icons/ai";
|
|
||||||
import { FaShieldAlt } from "react-icons/fa";
|
|
||||||
import { IoTrashBinOutline } from "react-icons/io5";
|
|
||||||
import { SiGithub, SiGoogle } from "react-icons/si";
|
|
||||||
import { GiStarFormation } from "react-icons/gi";
|
|
||||||
import { api } from "~/utils/api";
|
|
||||||
import { getServerAuthSession } from "../../server/auth";
|
|
||||||
import Stats from "~/components/Stats";
|
|
||||||
|
|
||||||
export const getServerSideProps: GetServerSideProps = async (ctx) => {
|
|
||||||
const session = await getServerAuthSession(ctx);
|
|
||||||
|
|
||||||
// Redirect to login if not signed in
|
|
||||||
if (!session) {
|
|
||||||
return {
|
|
||||||
redirect: {
|
|
||||||
destination: `/api/auth/signin?callbackUrl=${ctx.resolvedUrl}`,
|
|
||||||
permanent: false,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!session.user.isAdmin) {
|
|
||||||
ctx.res.statusCode = 403;
|
|
||||||
return {
|
|
||||||
redirect: {
|
|
||||||
destination: "/",
|
|
||||||
permanent: false,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
}
|
|
||||||
// Return session if logged in
|
|
||||||
return {
|
|
||||||
props: { session },
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
const Admin: NextPage = () => {
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<Head>
|
|
||||||
<title>Sprint Padawan - Admin</title>
|
|
||||||
<meta name="description" content="Plan. Sprint. Repeat." />
|
|
||||||
</Head>
|
|
||||||
<div className="flex flex-col items-center justify-center text-center px-4 py-16 ">
|
|
||||||
<div className="flex flex-col items-center">
|
|
||||||
<AdminBody />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default Admin;
|
|
||||||
|
|
||||||
const AdminBody = () => {
|
|
||||||
const {
|
|
||||||
data: users,
|
|
||||||
isLoading: usersLoading,
|
|
||||||
isFetching: usersFetching,
|
|
||||||
refetch: refetchUsers,
|
|
||||||
} = api.user.getAll.useQuery();
|
|
||||||
|
|
||||||
const getProviders = (user: {
|
|
||||||
createdAt: Date;
|
|
||||||
accounts: {
|
|
||||||
provider: string;
|
|
||||||
}[];
|
|
||||||
sessions: {
|
|
||||||
id: string;
|
|
||||||
}[];
|
|
||||||
id: string;
|
|
||||||
isAdmin: boolean;
|
|
||||||
isVIP: boolean;
|
|
||||||
name: string | null;
|
|
||||||
email: string | null;
|
|
||||||
}) => {
|
|
||||||
return user.accounts.map((account) => {
|
|
||||||
return account.provider;
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
const deleteUserMutation = api.user.delete.useMutation({
|
|
||||||
onSuccess: async () => {
|
|
||||||
await refetchData();
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const clearSessionsByUserMutation = api.session.deleteAllByUserId.useMutation(
|
|
||||||
{
|
|
||||||
onSuccess: async () => {
|
|
||||||
await refetchData();
|
|
||||||
},
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
const clearSessionsMutation = api.session.deleteAll.useMutation({
|
|
||||||
onSuccess: async () => {
|
|
||||||
await refetchData();
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const setAdminMutation = api.user.setAdmin.useMutation({
|
|
||||||
onSuccess: async () => {
|
|
||||||
await refetchData();
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const setVIPMutation = api.user.setVIP.useMutation({
|
|
||||||
onSuccess: async () => {
|
|
||||||
await refetchData();
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const deleteUserHandler = async (userId: string) => {
|
|
||||||
await deleteUserMutation.mutateAsync({ userId });
|
|
||||||
};
|
|
||||||
|
|
||||||
const clearSessionsByUserHandler = async (userId: string) => {
|
|
||||||
await clearSessionsByUserMutation.mutateAsync({ userId });
|
|
||||||
};
|
|
||||||
|
|
||||||
const clearSessionsHandler = async () => {
|
|
||||||
await clearSessionsMutation.mutateAsync();
|
|
||||||
};
|
|
||||||
|
|
||||||
const setAdmin = async (userId: string, value: boolean) => {
|
|
||||||
await setAdminMutation.mutateAsync({ userId, value });
|
|
||||||
};
|
|
||||||
|
|
||||||
const setVIP = async (userId: string, value: boolean) => {
|
|
||||||
await setVIPMutation.mutateAsync({ userId, value });
|
|
||||||
};
|
|
||||||
|
|
||||||
const refetchData = async () => {
|
|
||||||
await Promise.all([refetchUsers()]);
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<h1 className="text-4xl font-bold">Admin Panel</h1>
|
|
||||||
|
|
||||||
<Stats />
|
|
||||||
|
|
||||||
{usersFetching ? (
|
|
||||||
<span className="loading loading-dots loading-lg"></span>
|
|
||||||
) : (
|
|
||||||
<div className="flex flex-row flex-wrap text-center items-center justify-center gap-2">
|
|
||||||
<button
|
|
||||||
className="btn btn-primary m-2"
|
|
||||||
onClick={() => void clearSessionsHandler()}
|
|
||||||
>
|
|
||||||
Delete All Sessions
|
|
||||||
</button>
|
|
||||||
|
|
||||||
<button
|
|
||||||
className="btn btn-primary"
|
|
||||||
onClick={() => void refetchData()}
|
|
||||||
>
|
|
||||||
Re-fetch
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<div className="card max-w-[80vw] bg-neutral shadow-xl m-4">
|
|
||||||
<div className="card-body">
|
|
||||||
<h2 className="card-title">Users:</h2>
|
|
||||||
|
|
||||||
{usersLoading || usersFetching ? (
|
|
||||||
<span className="loading loading-dots loading-lg"></span>
|
|
||||||
) : (
|
|
||||||
<div className="overflow-x-scroll">
|
|
||||||
<table className="table text-center">
|
|
||||||
{/* head */}
|
|
||||||
<thead>
|
|
||||||
<tr className="border-white">
|
|
||||||
<th>ID</th>
|
|
||||||
<th>Name</th>
|
|
||||||
<th>Created At</th>
|
|
||||||
<th># Sessions</th>
|
|
||||||
<th>Providers</th>
|
|
||||||
<th>Actions</th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody className="">
|
|
||||||
{users
|
|
||||||
?.sort((user1, user2) =>
|
|
||||||
user2.createdAt > user1.createdAt ? 1 : -1
|
|
||||||
)
|
|
||||||
.map((user) => {
|
|
||||||
return (
|
|
||||||
<tr key={user.id} className="hover">
|
|
||||||
<td className="max-w-[100px] break-words">
|
|
||||||
{user.id}
|
|
||||||
</td>
|
|
||||||
|
|
||||||
<td className="max-w-[100px] break-normal">
|
|
||||||
{user.name}
|
|
||||||
</td>
|
|
||||||
<td className="max-w-[100px] break-normal">
|
|
||||||
{user.createdAt.toLocaleDateString()}
|
|
||||||
</td>
|
|
||||||
<td className="max-w-[100px] break-normal">
|
|
||||||
{user.sessions.length}
|
|
||||||
</td>
|
|
||||||
<td className="max-w-[100px] break-normal">
|
|
||||||
{getProviders(user).includes("google") && (
|
|
||||||
<SiGoogle className="text-xl m-1 inline-block hover:text-secondary" />
|
|
||||||
)}
|
|
||||||
{getProviders(user).includes("github") && (
|
|
||||||
<SiGithub className="text-xl m-1 inline-block hover:text-secondary" />
|
|
||||||
)}
|
|
||||||
</td>
|
|
||||||
<td>
|
|
||||||
<button className="m-2">
|
|
||||||
{user.isAdmin ? (
|
|
||||||
<FaShieldAlt
|
|
||||||
className="text-xl inline-block text-primary"
|
|
||||||
onClick={() => void setAdmin(user.id, false)}
|
|
||||||
/>
|
|
||||||
) : (
|
|
||||||
<FaShieldAlt
|
|
||||||
className="text-xl inline-block"
|
|
||||||
onClick={() => void setAdmin(user.id, true)}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</button>
|
|
||||||
<button className="m-2">
|
|
||||||
{user.isVIP ? (
|
|
||||||
<GiStarFormation
|
|
||||||
className="text-xl inline-block text-secondary"
|
|
||||||
onClick={() => void setVIP(user.id, false)}
|
|
||||||
/>
|
|
||||||
) : (
|
|
||||||
<GiStarFormation
|
|
||||||
className="text-xl inline-block"
|
|
||||||
onClick={() => void setVIP(user.id, true)}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
className="m-2"
|
|
||||||
onClick={() =>
|
|
||||||
void clearSessionsByUserHandler(user.id)
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<AiOutlineClear className="text-xl inline-block hover:text-warning" />
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
className="m-2"
|
|
||||||
onClick={() => void deleteUserHandler(user.id)}
|
|
||||||
>
|
|
||||||
<IoTrashBinOutline className="text-xl inline-block hover:text-error" />
|
|
||||||
</button>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
};
|
|
|
@ -1,4 +0,0 @@
|
||||||
import NextAuth from "next-auth";
|
|
||||||
import { authOptions } from "~/server/auth";
|
|
||||||
|
|
||||||
export default NextAuth(authOptions);
|
|
|
@ -1,5 +1,4 @@
|
||||||
import type { GetServerSideProps, NextPage } from "next";
|
import type { NextPage } from "next";
|
||||||
import { useSession } from "next-auth/react";
|
|
||||||
import Head from "next/head";
|
import Head from "next/head";
|
||||||
|
|
||||||
import RoomList from "~/components/RoomList";
|
import RoomList from "~/components/RoomList";
|
||||||
|
@ -7,27 +6,8 @@ import RoomList from "~/components/RoomList";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import { FaShieldAlt } from "react-icons/fa";
|
import { FaShieldAlt } from "react-icons/fa";
|
||||||
import { getServerAuthSession } from "~/server/auth";
|
|
||||||
import { GiStarFormation } from "react-icons/gi";
|
import { GiStarFormation } from "react-icons/gi";
|
||||||
|
import { useUser } from "@clerk/nextjs";
|
||||||
export const getServerSideProps: GetServerSideProps = async (ctx) => {
|
|
||||||
const session = await getServerAuthSession(ctx);
|
|
||||||
|
|
||||||
// Redirect to login if not signed in
|
|
||||||
if (!session) {
|
|
||||||
return {
|
|
||||||
redirect: {
|
|
||||||
destination: `/api/auth/signin?callbackUrl=${ctx.resolvedUrl}`,
|
|
||||||
permanent: false,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
// Return session if logged in
|
|
||||||
return {
|
|
||||||
props: { session },
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
const Home: NextPage = () => {
|
const Home: NextPage = () => {
|
||||||
return (
|
return (
|
||||||
|
@ -46,23 +26,27 @@ const Home: NextPage = () => {
|
||||||
export default Home;
|
export default Home;
|
||||||
|
|
||||||
const HomePageBody = () => {
|
const HomePageBody = () => {
|
||||||
const { data: sessionData } = useSession();
|
const { isLoaded, user } = useUser();
|
||||||
const [joinRoomTextBox, setJoinRoomTextBox] = useState<string>("");
|
const [joinRoomTextBox, setJoinRoomTextBox] = useState<string>("");
|
||||||
const [tabIndex, setTabIndex] = useState<number>();
|
const [tabIndex, setTabIndex] = useState<number>();
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const tabIndexLocal = localStorage.getItem(`dashboardTabIndex`);
|
const tabIndexLocal = localStorage.getItem(`dashboardTabIndex`);
|
||||||
setTabIndex(tabIndexLocal !== null ? Number(tabIndexLocal) : 0);
|
setTabIndex(tabIndexLocal !== null ? Number(tabIndexLocal) : 0);
|
||||||
}, [tabIndex, sessionData]);
|
}, [tabIndex, user]);
|
||||||
|
|
||||||
return (
|
return !isLoaded ? (
|
||||||
|
<div className="flex items-center justify-center">
|
||||||
|
<span className="loading loading-dots loading-lg"></span>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
<>
|
<>
|
||||||
<h1 className="flex flex-row flex-wrap text-center justify-center items-center gap-1 text-4xl font-bold mx-auto">
|
<h1 className="flex flex-row flex-wrap text-center justify-center items-center gap-1 text-4xl font-bold mx-auto">
|
||||||
Hi, {sessionData?.user.name}!{" "}
|
Hi, {user?.fullName}!{" "}
|
||||||
{sessionData?.user.isAdmin && (
|
{(user?.publicMetadata.isAdmin as boolean | undefined) && (
|
||||||
<FaShieldAlt className="inline-block text-primary" />
|
<FaShieldAlt className="inline-block text-primary" />
|
||||||
)}
|
)}
|
||||||
{sessionData?.user.isVIP && (
|
{(user?.publicMetadata.isVIP as boolean | undefined) && (
|
||||||
<GiStarFormation className="inline-block text-secondary" />
|
<GiStarFormation className="inline-block text-secondary" />
|
||||||
)}
|
)}
|
||||||
</h1>
|
</h1>
|
||||||
|
|
|
@ -1,6 +1,5 @@
|
||||||
import { type NextPage } from "next";
|
import { type NextPage } from "next";
|
||||||
import Head from "next/head";
|
import Head from "next/head";
|
||||||
import Stats from "~/components/Stats";
|
|
||||||
|
|
||||||
const Home: NextPage = () => {
|
const Home: NextPage = () => {
|
||||||
return (
|
return (
|
||||||
|
@ -55,13 +54,6 @@ const HomePageBody = () => {
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="card card-compact bg-secondary text-black font-bold text-left">
|
|
||||||
<div className="card-body">
|
|
||||||
<h2 className="card-title">Stats:</h2>
|
|
||||||
<Stats />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
@ -1,205 +0,0 @@
|
||||||
import { type GetServerSideProps, type NextPage } from "next";
|
|
||||||
import Head from "next/head";
|
|
||||||
import Image from "next/image";
|
|
||||||
|
|
||||||
import { signIn, useSession } from "next-auth/react";
|
|
||||||
import { useRouter } from "next/router";
|
|
||||||
import { useEffect, useState } from "react";
|
|
||||||
import { FaShieldAlt } from "react-icons/fa";
|
|
||||||
import { SiGithub, SiGoogle } from "react-icons/si";
|
|
||||||
import { api } from "~/utils/api";
|
|
||||||
import { getServerAuthSession } from "../../server/auth";
|
|
||||||
import { GiStarFormation } from "react-icons/gi";
|
|
||||||
|
|
||||||
export const getServerSideProps: GetServerSideProps = async (ctx) => {
|
|
||||||
const session = await getServerAuthSession(ctx);
|
|
||||||
|
|
||||||
// Redirect to login if not signed in
|
|
||||||
if (!session) {
|
|
||||||
return {
|
|
||||||
redirect: {
|
|
||||||
destination: `/api/auth/signin?callbackUrl=${ctx.resolvedUrl}`,
|
|
||||||
permanent: false,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
// Return session if logged in
|
|
||||||
return {
|
|
||||||
props: { session },
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
const Profile: NextPage = () => {
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<Head>
|
|
||||||
<title>Sprint Padawan - Profile</title>
|
|
||||||
<meta name="description" content="Plan. Sprint. Repeat." />
|
|
||||||
</Head>
|
|
||||||
<div className="flex flex-col items-center justify-center text-center gap-12 px-4 py-16 ">
|
|
||||||
<div className="flex flex-col items-center">
|
|
||||||
<ProfileBody />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default Profile;
|
|
||||||
|
|
||||||
const ProfileBody = () => {
|
|
||||||
const { data: sessionData } = useSession();
|
|
||||||
const [nameText, setNameText] = useState<string>("");
|
|
||||||
const router = useRouter();
|
|
||||||
|
|
||||||
const { data: providers, isLoading: providersLoading } =
|
|
||||||
api.user.getProviders.useQuery();
|
|
||||||
|
|
||||||
const deleteUserMutation = api.user.delete.useMutation({});
|
|
||||||
const saveUserMutation = api.user.save.useMutation({});
|
|
||||||
|
|
||||||
const deleteCurrentUser = async () => {
|
|
||||||
await deleteUserMutation.mutateAsync();
|
|
||||||
(document.querySelector("#delete-user-modal") as HTMLInputElement).checked =
|
|
||||||
false;
|
|
||||||
router.reload();
|
|
||||||
};
|
|
||||||
|
|
||||||
const saveUser = async () => {
|
|
||||||
await saveUserMutation.mutateAsync({
|
|
||||||
name: nameText,
|
|
||||||
});
|
|
||||||
router.reload();
|
|
||||||
};
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
setNameText(sessionData?.user.name || "");
|
|
||||||
}, [sessionData]);
|
|
||||||
|
|
||||||
if (sessionData) {
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<input
|
|
||||||
type="checkbox"
|
|
||||||
id="delete-user-modal"
|
|
||||||
className="modal-toggle"
|
|
||||||
/>
|
|
||||||
<div className="modal modal-bottom sm:modal-middle">
|
|
||||||
<div className="modal-box flex-col flex text-center justify-center items-center">
|
|
||||||
<label
|
|
||||||
htmlFor="delete-user-modal"
|
|
||||||
className="btn btn-sm btn-circle absolute right-2 top-2"
|
|
||||||
>
|
|
||||||
✕
|
|
||||||
</label>
|
|
||||||
|
|
||||||
<h3 className="font-bold text-lg text-error">
|
|
||||||
This action will delete ALL data associated with your account. The
|
|
||||||
same GitHub Account can be used, but none of your existing data
|
|
||||||
will be available. If you are sure, please confirm below:
|
|
||||||
</h3>
|
|
||||||
|
|
||||||
<div className="modal-action">
|
|
||||||
<label
|
|
||||||
htmlFor="delete-user-modal"
|
|
||||||
className="btn btn-error"
|
|
||||||
onClick={() => void deleteCurrentUser()}
|
|
||||||
>
|
|
||||||
I am sure!
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="card w-90 bg-neutral shadow-xl">
|
|
||||||
<div className="card-body">
|
|
||||||
<h2 className="card-title">Profile:</h2>
|
|
||||||
{sessionData.user.image && (
|
|
||||||
<Image
|
|
||||||
className="mx-auto"
|
|
||||||
src={sessionData.user.image}
|
|
||||||
alt="Profile picture."
|
|
||||||
height={100}
|
|
||||||
width={100}
|
|
||||||
priority
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<div className="flex flex-row flex-wrap items-center text-center justify-center gap-4">
|
|
||||||
{sessionData.user.isAdmin && (
|
|
||||||
<div className="tooltip tooltip-primary" data-tip="Admin">
|
|
||||||
<FaShieldAlt className="text-xl text-primary" />
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
{sessionData.user.isVIP && (
|
|
||||||
<div className="tooltip tooltip-secondary" data-tip="VIP">
|
|
||||||
<GiStarFormation className="inline-block text-xl text-secondary" />
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{providersLoading ? (
|
|
||||||
<div className="mx-auto">
|
|
||||||
<span className="loading loading-dots loading-lg"></span>{" "}
|
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
<div className="mx-auto">
|
|
||||||
<button
|
|
||||||
className={`btn btn-square btn-outline mx-2`}
|
|
||||||
disabled={providers?.includes("github")}
|
|
||||||
onClick={() => void signIn("github")}
|
|
||||||
>
|
|
||||||
<SiGithub />
|
|
||||||
</button>
|
|
||||||
|
|
||||||
<button
|
|
||||||
className={`btn btn-square btn-outline mx-2`}
|
|
||||||
disabled={providers?.includes("google")}
|
|
||||||
onClick={() => void signIn("google")}
|
|
||||||
>
|
|
||||||
<SiGoogle />
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{sessionData.user.name && (
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
placeholder="Name"
|
|
||||||
className="input input-bordered"
|
|
||||||
value={nameText}
|
|
||||||
onChange={(event) => setNameText(event.target.value)}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{sessionData.user.email && (
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
placeholder="Email"
|
|
||||||
className="input input-bordered"
|
|
||||||
value={sessionData.user.email}
|
|
||||||
disabled
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<button
|
|
||||||
onClick={() => void saveUser()}
|
|
||||||
className="btn btn-secondary"
|
|
||||||
>
|
|
||||||
Save Account
|
|
||||||
</button>
|
|
||||||
|
|
||||||
{/* <button className="btn btn-error">Delete Account</button> */}
|
|
||||||
|
|
||||||
<label htmlFor="delete-user-modal" className="btn btn-error">
|
|
||||||
Delete Account
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
return <h1>Error getting login session!</h1>;
|
|
||||||
}
|
|
||||||
};
|
|
|
@ -1,10 +1,9 @@
|
||||||
import { type GetServerSideProps, type NextPage } from "next";
|
import { type NextPage } from "next";
|
||||||
import Head from "next/head";
|
import Head from "next/head";
|
||||||
import Image from "next/image";
|
import Image from "next/image";
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import { EventTypes } from "~/utils/types";
|
import { EventTypes } from "~/utils/types";
|
||||||
|
|
||||||
import { useSession } from "next-auth/react";
|
|
||||||
import { useRouter } from "next/router";
|
import { useRouter } from "next/router";
|
||||||
import {
|
import {
|
||||||
IoCheckmarkCircleOutline,
|
IoCheckmarkCircleOutline,
|
||||||
|
@ -17,10 +16,7 @@ import {
|
||||||
IoSaveOutline,
|
IoSaveOutline,
|
||||||
} from "react-icons/io5";
|
} from "react-icons/io5";
|
||||||
import { GiStarFormation } from "react-icons/gi";
|
import { GiStarFormation } from "react-icons/gi";
|
||||||
import { z } from "zod";
|
|
||||||
import { api } from "~/utils/api";
|
import { api } from "~/utils/api";
|
||||||
import { getServerAuthSession } from "../../server/auth";
|
|
||||||
|
|
||||||
import { configureAbly, useChannel, usePresence } from "@ably-labs/react-hooks";
|
import { configureAbly, useChannel, usePresence } from "@ably-labs/react-hooks";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import { FaShieldAlt } from "react-icons/fa";
|
import { FaShieldAlt } from "react-icons/fa";
|
||||||
|
@ -28,27 +24,10 @@ import { RiVipCrownFill } from "react-icons/ri";
|
||||||
import { env } from "~/env.mjs";
|
import { env } from "~/env.mjs";
|
||||||
import { downloadCSV } from "~/utils/helpers";
|
import { downloadCSV } from "~/utils/helpers";
|
||||||
import type { PresenceItem } from "~/utils/types";
|
import type { PresenceItem } from "~/utils/types";
|
||||||
|
import { useUser } from "@clerk/nextjs";
|
||||||
export const getServerSideProps: GetServerSideProps = async (ctx) => {
|
|
||||||
const session = await getServerAuthSession(ctx);
|
|
||||||
|
|
||||||
// Redirect to login if not signed in
|
|
||||||
if (!session) {
|
|
||||||
return {
|
|
||||||
redirect: {
|
|
||||||
destination: `/api/auth/signin?callbackUrl=${ctx.resolvedUrl}`,
|
|
||||||
permanent: false,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
// Return session if logged in
|
|
||||||
return {
|
|
||||||
props: { session },
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
const Room: NextPage = () => {
|
const Room: NextPage = () => {
|
||||||
|
const { isSignedIn } = useUser();
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Head>
|
<Head>
|
||||||
|
@ -57,7 +36,13 @@ const Room: NextPage = () => {
|
||||||
<meta http-equiv="Cache-control" content="no-cache" />
|
<meta http-equiv="Cache-control" content="no-cache" />
|
||||||
</Head>
|
</Head>
|
||||||
<div className="flex flex-col items-center justify-center text-center gap-2">
|
<div className="flex flex-col items-center justify-center text-center gap-2">
|
||||||
<RoomBody />
|
{!isSignedIn ? (
|
||||||
|
<div className="flex items-center justify-center">
|
||||||
|
<span className="loading loading-dots loading-lg"></span>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<RoomBody />
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
@ -66,9 +51,9 @@ const Room: NextPage = () => {
|
||||||
export default Room;
|
export default Room;
|
||||||
|
|
||||||
const RoomBody = ({}) => {
|
const RoomBody = ({}) => {
|
||||||
const { data: sessionData } = useSession();
|
const { isSignedIn, user } = useUser();
|
||||||
const { query } = useRouter();
|
const { query } = useRouter();
|
||||||
const roomId = z.string().parse(query.id);
|
const roomId = query.id as string;
|
||||||
|
|
||||||
const [storyNameText, setStoryNameText] = useState<string>("");
|
const [storyNameText, setStoryNameText] = useState<string>("");
|
||||||
const [roomScale, setRoomScale] = useState<string>("");
|
const [roomScale, setRoomScale] = useState<string>("");
|
||||||
|
@ -85,7 +70,7 @@ const RoomBody = ({}) => {
|
||||||
|
|
||||||
configureAbly({
|
configureAbly({
|
||||||
key: env.NEXT_PUBLIC_ABLY_PUBLIC_KEY,
|
key: env.NEXT_PUBLIC_ABLY_PUBLIC_KEY,
|
||||||
clientId: sessionData?.user.id,
|
clientId: user?.id,
|
||||||
recover: (_, cb) => {
|
recover: (_, cb) => {
|
||||||
cb(true);
|
cb(true);
|
||||||
},
|
},
|
||||||
|
@ -108,11 +93,11 @@ const RoomBody = ({}) => {
|
||||||
const [presenceData] = usePresence<PresenceItem>(
|
const [presenceData] = usePresence<PresenceItem>(
|
||||||
`${env.NEXT_PUBLIC_APP_ENV}-${roomId}`,
|
`${env.NEXT_PUBLIC_APP_ENV}-${roomId}`,
|
||||||
{
|
{
|
||||||
name: sessionData?.user.name || "",
|
name: user?.fullName || "",
|
||||||
image: sessionData?.user.image || "",
|
image: user?.imageUrl || "",
|
||||||
client_id: sessionData?.user.id || "",
|
client_id: user?.id || "",
|
||||||
isAdmin: sessionData?.user.isAdmin || false,
|
isAdmin: (user?.publicMetadata.isAdmin as boolean | undefined) || false,
|
||||||
isVIP: sessionData?.user.isVIP || false,
|
isVIP: (user?.publicMetadata.isVIP as boolean | undefined) || false,
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@ -129,19 +114,16 @@ const RoomBody = ({}) => {
|
||||||
|
|
||||||
// Init story name
|
// Init story name
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (sessionData && roomFromDb) {
|
if (isSignedIn && roomFromDb) {
|
||||||
setStoryNameText(roomFromDb.storyName || "");
|
setStoryNameText(roomFromDb.storyName || "");
|
||||||
setRoomScale(roomFromDb.scale || "ERROR");
|
setRoomScale(roomFromDb.scale || "ERROR");
|
||||||
}
|
}
|
||||||
}, [roomFromDb, roomId, sessionData]);
|
}, [roomFromDb, roomId, isSignedIn, user]);
|
||||||
|
|
||||||
// Helper functions
|
// Helper functions
|
||||||
const getVoteForCurrentUser = () => {
|
const getVoteForCurrentUser = () => {
|
||||||
if (roomFromDb && sessionData) {
|
if (roomFromDb && isSignedIn) {
|
||||||
return (
|
return votesFromDb && votesFromDb.find((vote) => vote.userId === user.id);
|
||||||
votesFromDb &&
|
|
||||||
votesFromDb.find((vote) => vote.userId === sessionData.user.id)
|
|
||||||
);
|
|
||||||
} else {
|
} else {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
@ -184,12 +166,12 @@ const RoomBody = ({}) => {
|
||||||
.concat({
|
.concat({
|
||||||
id: "LATEST",
|
id: "LATEST",
|
||||||
createdAt: new Date(),
|
createdAt: new Date(),
|
||||||
userId: roomFromDb.owner.id,
|
userId: roomFromDb.userId,
|
||||||
roomId: roomFromDb.id,
|
roomId: roomFromDb.id,
|
||||||
scale: roomScale,
|
scale: roomScale,
|
||||||
votes: votesFromDb.map((vote) => {
|
votes: votesFromDb.map((vote) => {
|
||||||
return {
|
return {
|
||||||
name: vote.owner.name,
|
name: vote.userId,
|
||||||
value: vote.value,
|
value: vote.value,
|
||||||
};
|
};
|
||||||
}),
|
}),
|
||||||
|
@ -365,111 +347,108 @@ const RoomBody = ({}) => {
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{sessionData &&
|
{isSignedIn && !!roomFromDb && roomFromDb.userId === user.id && (
|
||||||
!!roomFromDb &&
|
<>
|
||||||
roomFromDb.userId === sessionData.user.id && (
|
<div className="card card-compact bg-neutral shadow-xl mx-auto m-4">
|
||||||
<>
|
<div className="card-body flex flex-col flex-wrap">
|
||||||
<div className="card card-compact bg-neutral shadow-xl mx-auto m-4">
|
<h2 className="card-title mx-auto">Room Settings</h2>
|
||||||
<div className="card-body flex flex-col flex-wrap">
|
|
||||||
<h2 className="card-title mx-auto">Room Settings</h2>
|
|
||||||
|
|
||||||
<label className="label mx-auto">
|
<label className="label mx-auto">
|
||||||
{"Vote Scale (Comma Separated):"}{" "}
|
{"Vote Scale (Comma Separated):"}{" "}
|
||||||
</label>
|
</label>
|
||||||
|
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
placeholder="Scale (Comma Separated)"
|
placeholder="Scale (Comma Separated)"
|
||||||
className="input input-bordered m-auto"
|
className="input input-bordered m-auto"
|
||||||
value={roomScale}
|
value={roomScale}
|
||||||
onChange={(event) => {
|
onChange={(event) => {
|
||||||
setRoomScale(event.target.value);
|
setRoomScale(event.target.value);
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<label className="label mx-auto">{"Story Name:"} </label>
|
<label className="label mx-auto">{"Story Name:"} </label>
|
||||||
|
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
placeholder="Story Name"
|
placeholder="Story Name"
|
||||||
className="input input-bordered m-auto"
|
className="input input-bordered m-auto"
|
||||||
value={storyNameText}
|
value={storyNameText}
|
||||||
onChange={(event) => {
|
onChange={(event) => {
|
||||||
setStoryNameText(event.target.value);
|
setStoryNameText(event.target.value);
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<div className="flex flex-row flex-wrap text-center items-center justify-center gap-2">
|
<div className="flex flex-row flex-wrap text-center items-center justify-center gap-2">
|
||||||
<div>
|
<div>
|
||||||
<button
|
<button
|
||||||
onClick={() => saveRoom(!roomFromDb.visible, false)}
|
onClick={() => saveRoom(!roomFromDb.visible, false)}
|
||||||
className="btn btn-primary inline-flex"
|
className="btn btn-primary inline-flex"
|
||||||
>
|
>
|
||||||
{roomFromDb.visible ? (
|
{roomFromDb.visible ? (
|
||||||
<>
|
<>
|
||||||
<IoEyeOffOutline className="text-xl mr-1" />
|
<IoEyeOffOutline className="text-xl mr-1" />
|
||||||
Hide
|
Hide
|
||||||
</>
|
</>
|
||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
<IoEyeOutline className="text-xl mr-1" />
|
<IoEyeOutline className="text-xl mr-1" />
|
||||||
Show
|
Show
|
||||||
</>
|
</>
|
||||||
)}
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div>
|
|
||||||
<button
|
|
||||||
onClick={() =>
|
|
||||||
saveRoom(
|
|
||||||
false,
|
|
||||||
true,
|
|
||||||
roomFromDb.storyName === storyNameText ||
|
|
||||||
votesFromDb?.length === 0
|
|
||||||
? false
|
|
||||||
: true
|
|
||||||
)
|
|
||||||
}
|
|
||||||
className="btn btn-primary inline-flex"
|
|
||||||
disabled={
|
|
||||||
[...new Set(roomScale.split(","))].filter(
|
|
||||||
(item) => item !== ""
|
|
||||||
).length <= 1
|
|
||||||
}
|
|
||||||
>
|
|
||||||
{roomFromDb.storyName === storyNameText ||
|
|
||||||
votesFromDb?.length === 0 ? (
|
|
||||||
<>
|
|
||||||
<IoReloadOutline className="text-xl mr-1" /> Reset
|
|
||||||
</>
|
|
||||||
) : (
|
|
||||||
<>
|
|
||||||
<IoSaveOutline className="text-xl mr-1" /> Save
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{votesFromDb &&
|
|
||||||
(roomFromDb.logs.length > 0 ||
|
|
||||||
votesFromDb.length > 0) && (
|
|
||||||
<div>
|
|
||||||
<button
|
|
||||||
onClick={() => downloadLogs()}
|
|
||||||
className="btn btn-primary inline-flex hover:animate-pulse"
|
|
||||||
>
|
|
||||||
<>
|
|
||||||
<IoDownloadOutline className="text-xl" />
|
|
||||||
</>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
)}
|
)}
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<button
|
||||||
|
onClick={() =>
|
||||||
|
saveRoom(
|
||||||
|
false,
|
||||||
|
true,
|
||||||
|
roomFromDb.storyName === storyNameText ||
|
||||||
|
votesFromDb?.length === 0
|
||||||
|
? false
|
||||||
|
: true
|
||||||
|
)
|
||||||
|
}
|
||||||
|
className="btn btn-primary inline-flex"
|
||||||
|
disabled={
|
||||||
|
[...new Set(roomScale.split(","))].filter(
|
||||||
|
(item) => item !== ""
|
||||||
|
).length <= 1
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{roomFromDb.storyName === storyNameText ||
|
||||||
|
votesFromDb?.length === 0 ? (
|
||||||
|
<>
|
||||||
|
<IoReloadOutline className="text-xl mr-1" /> Reset
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<IoSaveOutline className="text-xl mr-1" /> Save
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{votesFromDb &&
|
||||||
|
(roomFromDb.logs.length > 0 || votesFromDb.length > 0) && (
|
||||||
|
<div>
|
||||||
|
<button
|
||||||
|
onClick={() => downloadLogs()}
|
||||||
|
className="btn btn-primary inline-flex hover:animate-pulse"
|
||||||
|
>
|
||||||
|
<>
|
||||||
|
<IoDownloadOutline className="text-xl" />
|
||||||
|
</>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</>
|
</div>
|
||||||
)}
|
</>
|
||||||
|
)}
|
||||||
</span>
|
</span>
|
||||||
);
|
);
|
||||||
// Room does not exist
|
// Room does not exist
|
||||||
|
|
17
src/pages/sign-in/[[...index]].tsx
Normal file
17
src/pages/sign-in/[[...index]].tsx
Normal file
|
@ -0,0 +1,17 @@
|
||||||
|
import { SignIn } from "@clerk/nextjs";
|
||||||
|
|
||||||
|
const SignInPage = () => (
|
||||||
|
<div style={styles}>
|
||||||
|
<SignIn path="/sign-in" routing="path" signUpUrl="/sign-up" />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
|
export default SignInPage;
|
||||||
|
|
||||||
|
const styles = {
|
||||||
|
width: "100vw",
|
||||||
|
height: "100vh",
|
||||||
|
display: "flex",
|
||||||
|
justifyContent: "center",
|
||||||
|
alignItems: "center",
|
||||||
|
};
|
17
src/pages/sign-up/[[...index]].tsx
Normal file
17
src/pages/sign-up/[[...index]].tsx
Normal file
|
@ -0,0 +1,17 @@
|
||||||
|
import { SignUp } from "@clerk/nextjs";
|
||||||
|
|
||||||
|
const SignUpPage = () => (
|
||||||
|
<div style={styles}>
|
||||||
|
<SignUp path="/sign-up" routing="path" signInUrl="/sign-in" />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
|
export default SignUpPage;
|
||||||
|
|
||||||
|
const styles = {
|
||||||
|
width: "100vw",
|
||||||
|
height: "100vh",
|
||||||
|
display: "flex",
|
||||||
|
justifyContent: "center",
|
||||||
|
alignItems: "center",
|
||||||
|
};
|
|
@ -1,7 +1,5 @@
|
||||||
import { roomRouter } from "~/server/api/routers/room";
|
import { roomRouter } from "~/server/api/routers/room";
|
||||||
import { createTRPCRouter } from "~/server/api/trpc";
|
import { createTRPCRouter } from "~/server/api/trpc";
|
||||||
import { sessionRouter } from "./routers/session";
|
|
||||||
import { userRouter } from "./routers/user";
|
|
||||||
import { voteRouter } from "./routers/vote";
|
import { voteRouter } from "./routers/vote";
|
||||||
import { restRouter } from "./routers/rest";
|
import { restRouter } from "./routers/rest";
|
||||||
|
|
||||||
|
@ -13,8 +11,6 @@ import { restRouter } from "./routers/rest";
|
||||||
export const appRouter = createTRPCRouter({
|
export const appRouter = createTRPCRouter({
|
||||||
room: roomRouter,
|
room: roomRouter,
|
||||||
vote: voteRouter,
|
vote: voteRouter,
|
||||||
user: userRouter,
|
|
||||||
session: sessionRouter,
|
|
||||||
rest: restRouter,
|
rest: restRouter,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
@ -13,7 +13,7 @@ export const restRouter = createTRPCRouter({
|
||||||
.query(async ({ ctx, input }) => {
|
.query(async ({ ctx, input }) => {
|
||||||
const isValidKey = await validateApiKey(input.key);
|
const isValidKey = await validateApiKey(input.key);
|
||||||
if (isValidKey) {
|
if (isValidKey) {
|
||||||
await ctx.prisma.verificationToken.findMany();
|
await ctx.prisma.vote.findMany();
|
||||||
return "Toasted the DB";
|
return "Toasted the DB";
|
||||||
} else {
|
} else {
|
||||||
throw new TRPCError({ code: "UNAUTHORIZED" });
|
throw new TRPCError({ code: "UNAUTHORIZED" });
|
||||||
|
@ -38,24 +38,6 @@ export const restRouter = createTRPCRouter({
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
|
|
||||||
userCount: publicProcedure
|
|
||||||
.meta({ openapi: { method: "GET", path: "/rest/users/count" } })
|
|
||||||
.input(z.void())
|
|
||||||
.output(z.number())
|
|
||||||
.query(async ({ ctx }) => {
|
|
||||||
const cachedResult = await fetchCache<number>(`kv_usercount`);
|
|
||||||
|
|
||||||
if (cachedResult) {
|
|
||||||
return cachedResult;
|
|
||||||
} else {
|
|
||||||
const usersCount = await ctx.prisma.user.count();
|
|
||||||
|
|
||||||
await setCache(`kv_usercount`, usersCount);
|
|
||||||
|
|
||||||
return usersCount;
|
|
||||||
}
|
|
||||||
}),
|
|
||||||
|
|
||||||
roomCount: publicProcedure
|
roomCount: publicProcedure
|
||||||
.meta({ openapi: { method: "GET", path: "/rest/rooms/count" } })
|
.meta({ openapi: { method: "GET", path: "/rest/rooms/count" } })
|
||||||
.input(z.void())
|
.input(z.void())
|
||||||
|
|
|
@ -14,38 +14,34 @@ export const roomRouter = createTRPCRouter({
|
||||||
})
|
})
|
||||||
)
|
)
|
||||||
.mutation(async ({ ctx, input }) => {
|
.mutation(async ({ ctx, input }) => {
|
||||||
if (ctx.session) {
|
const room = await ctx.prisma.room.create({
|
||||||
const room = await ctx.prisma.room.create({
|
data: {
|
||||||
data: {
|
userId: ctx.auth.userId,
|
||||||
userId: ctx.session.user.id,
|
roomName: input.name,
|
||||||
roomName: input.name,
|
storyName: "First Story!",
|
||||||
storyName: "First Story!",
|
scale: "0.5,1,2,3,5,8",
|
||||||
scale: "0.5,1,2,3,5,8",
|
visible: false,
|
||||||
visible: false,
|
},
|
||||||
},
|
});
|
||||||
});
|
if (room) {
|
||||||
if (room) {
|
await invalidateCache(`kv_roomcount`);
|
||||||
await invalidateCache(`kv_roomcount`);
|
console.log("PUBLISHED TO ", `kv_roomlist_${ctx.auth.userId}`);
|
||||||
await invalidateCache(`kv_roomlist_${ctx.session.user.id}`);
|
await invalidateCache(`kv_roomlist_${ctx.auth.userId}`);
|
||||||
|
|
||||||
await publishToChannel(
|
await publishToChannel(
|
||||||
`${ctx.session.user.id}`,
|
`${ctx.auth.userId}`,
|
||||||
EventTypes.ROOM_LIST_UPDATE,
|
EventTypes.ROOM_LIST_UPDATE,
|
||||||
JSON.stringify(room)
|
JSON.stringify(room)
|
||||||
);
|
);
|
||||||
|
|
||||||
await publishToChannel(
|
await publishToChannel(
|
||||||
`stats`,
|
`stats`,
|
||||||
EventTypes.STATS_UPDATE,
|
EventTypes.STATS_UPDATE,
|
||||||
JSON.stringify(room)
|
JSON.stringify(room)
|
||||||
);
|
);
|
||||||
}
|
|
||||||
// happy path
|
|
||||||
return !!room;
|
|
||||||
}
|
}
|
||||||
|
// happy path
|
||||||
// clinically depressed path
|
return !!room;
|
||||||
return false;
|
|
||||||
}),
|
}),
|
||||||
|
|
||||||
// Get One
|
// Get One
|
||||||
|
@ -64,7 +60,6 @@ export const roomRouter = createTRPCRouter({
|
||||||
storyName: true,
|
storyName: true,
|
||||||
visible: true,
|
visible: true,
|
||||||
scale: true,
|
scale: true,
|
||||||
owner: true,
|
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
}),
|
}),
|
||||||
|
@ -77,14 +72,14 @@ export const roomRouter = createTRPCRouter({
|
||||||
createdAt: Date;
|
createdAt: Date;
|
||||||
roomName: string;
|
roomName: string;
|
||||||
}[]
|
}[]
|
||||||
>(`kv_roomlist_${ctx.session.user.id}`);
|
>(`kv_roomlist_${ctx.auth.userId}`);
|
||||||
|
|
||||||
if (cachedResult) {
|
if (cachedResult) {
|
||||||
return cachedResult;
|
return cachedResult;
|
||||||
} else {
|
} else {
|
||||||
const roomList = await ctx.prisma.room.findMany({
|
const roomList = await ctx.prisma.room.findMany({
|
||||||
where: {
|
where: {
|
||||||
userId: ctx.session.user.id,
|
userId: ctx.auth.userId,
|
||||||
},
|
},
|
||||||
select: {
|
select: {
|
||||||
id: true,
|
id: true,
|
||||||
|
@ -93,7 +88,7 @@ export const roomRouter = createTRPCRouter({
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
await setCache(`kv_roomlist_${ctx.session.user.id}`, roomList);
|
await setCache(`kv_roomlist_${ctx.auth.userId}`, roomList);
|
||||||
|
|
||||||
return roomList;
|
return roomList;
|
||||||
}
|
}
|
||||||
|
@ -124,11 +119,7 @@ export const roomRouter = createTRPCRouter({
|
||||||
scale: true,
|
scale: true,
|
||||||
votes: {
|
votes: {
|
||||||
select: {
|
select: {
|
||||||
owner: {
|
userId: true,
|
||||||
select: {
|
|
||||||
name: true,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
value: true,
|
value: true,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
@ -138,12 +129,12 @@ export const roomRouter = createTRPCRouter({
|
||||||
oldRoom &&
|
oldRoom &&
|
||||||
(await ctx.prisma.log.create({
|
(await ctx.prisma.log.create({
|
||||||
data: {
|
data: {
|
||||||
userId: ctx.session.user.id,
|
userId: ctx.auth.userId,
|
||||||
roomId: input.roomId,
|
roomId: input.roomId,
|
||||||
scale: oldRoom.scale,
|
scale: oldRoom.scale,
|
||||||
votes: oldRoom.votes.map((vote) => {
|
votes: oldRoom.votes.map((vote) => {
|
||||||
return {
|
return {
|
||||||
name: vote.owner.name,
|
name: vote.userId,
|
||||||
value: vote.value,
|
value: vote.value,
|
||||||
};
|
};
|
||||||
}),
|
}),
|
||||||
|
@ -168,7 +159,7 @@ export const roomRouter = createTRPCRouter({
|
||||||
},
|
},
|
||||||
data: {
|
data: {
|
||||||
storyName: input.name,
|
storyName: input.name,
|
||||||
userId: ctx.session.user.id,
|
userId: ctx.auth.userId,
|
||||||
visible: input.visible,
|
visible: input.visible,
|
||||||
scale: [...new Set(input.scale.split(","))]
|
scale: [...new Set(input.scale.split(","))]
|
||||||
.filter((item) => item !== "")
|
.filter((item) => item !== "")
|
||||||
|
@ -182,11 +173,6 @@ export const roomRouter = createTRPCRouter({
|
||||||
scale: true,
|
scale: true,
|
||||||
votes: {
|
votes: {
|
||||||
select: {
|
select: {
|
||||||
owner: {
|
|
||||||
select: {
|
|
||||||
name: true,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
value: true,
|
value: true,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
@ -217,10 +203,10 @@ export const roomRouter = createTRPCRouter({
|
||||||
if (deletedRoom) {
|
if (deletedRoom) {
|
||||||
await invalidateCache(`kv_roomcount`);
|
await invalidateCache(`kv_roomcount`);
|
||||||
await invalidateCache(`kv_votecount`);
|
await invalidateCache(`kv_votecount`);
|
||||||
await invalidateCache(`kv_roomlist_${ctx.session.user.id}`);
|
await invalidateCache(`kv_roomlist_${ctx.auth.userId}`);
|
||||||
|
|
||||||
await publishToChannel(
|
await publishToChannel(
|
||||||
`${ctx.session.user.id}`,
|
`${ctx.auth.userId}`,
|
||||||
EventTypes.ROOM_LIST_UPDATE,
|
EventTypes.ROOM_LIST_UPDATE,
|
||||||
JSON.stringify(deletedRoom)
|
JSON.stringify(deletedRoom)
|
||||||
);
|
);
|
||||||
|
|
|
@ -1,34 +0,0 @@
|
||||||
import { z } from "zod";
|
|
||||||
import { adminProcedure, createTRPCRouter } from "~/server/api/trpc";
|
|
||||||
import { invalidateCache } from "~/server/redis";
|
|
||||||
|
|
||||||
export const sessionRouter = createTRPCRouter({
|
|
||||||
deleteAllByUserId: adminProcedure
|
|
||||||
.input(
|
|
||||||
z.object({
|
|
||||||
userId: z.string(),
|
|
||||||
})
|
|
||||||
)
|
|
||||||
.mutation(async ({ ctx, input }) => {
|
|
||||||
const sessions = await ctx.prisma.session.deleteMany({
|
|
||||||
where: {
|
|
||||||
userId: input.userId,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!!sessions) {
|
|
||||||
await invalidateCache(`kv_userlist_admin`);
|
|
||||||
}
|
|
||||||
|
|
||||||
return !!sessions;
|
|
||||||
}),
|
|
||||||
deleteAll: adminProcedure.mutation(async ({ ctx }) => {
|
|
||||||
const sessions = await ctx.prisma.session.deleteMany();
|
|
||||||
|
|
||||||
if (!!sessions) {
|
|
||||||
await invalidateCache(`kv_userlist_admin`);
|
|
||||||
}
|
|
||||||
|
|
||||||
return !!sessions;
|
|
||||||
}),
|
|
||||||
});
|
|
|
@ -1,194 +0,0 @@
|
||||||
import type { User } from "@prisma/client";
|
|
||||||
import { Resend } from "resend";
|
|
||||||
import { z } from "zod";
|
|
||||||
import { Goodbye } from "~/components/templates/Goodbye";
|
|
||||||
import { env } from "~/env.mjs";
|
|
||||||
import { publishToChannel } from "~/server/ably";
|
|
||||||
import {
|
|
||||||
adminProcedure,
|
|
||||||
createTRPCRouter,
|
|
||||||
protectedProcedure,
|
|
||||||
} from "~/server/api/trpc";
|
|
||||||
|
|
||||||
import { fetchCache, invalidateCache, setCache } from "~/server/redis";
|
|
||||||
import { EventTypes } from "~/utils/types";
|
|
||||||
|
|
||||||
const resend = new Resend(process.env.RESEND_API_KEY);
|
|
||||||
|
|
||||||
export const userRouter = createTRPCRouter({
|
|
||||||
getProviders: protectedProcedure.query(async ({ ctx }) => {
|
|
||||||
const providers = await ctx.prisma.user.findUnique({
|
|
||||||
where: {
|
|
||||||
id: ctx.session.user.id,
|
|
||||||
},
|
|
||||||
select: {
|
|
||||||
accounts: {
|
|
||||||
select: {
|
|
||||||
provider: true,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
return providers?.accounts.map((account) => {
|
|
||||||
return account.provider;
|
|
||||||
});
|
|
||||||
}),
|
|
||||||
getAll: protectedProcedure.query(async ({ ctx }) => {
|
|
||||||
const cachedResult = await fetchCache<
|
|
||||||
{
|
|
||||||
accounts: {
|
|
||||||
provider: string;
|
|
||||||
}[];
|
|
||||||
sessions: {
|
|
||||||
id: string;
|
|
||||||
}[];
|
|
||||||
id: string;
|
|
||||||
createdAt: Date;
|
|
||||||
isAdmin: boolean;
|
|
||||||
isVIP: boolean;
|
|
||||||
name: string | null;
|
|
||||||
email: string | null;
|
|
||||||
}[]
|
|
||||||
>(`kv_userlist_admin`);
|
|
||||||
|
|
||||||
if (cachedResult) {
|
|
||||||
return cachedResult.map((user) => {
|
|
||||||
return {
|
|
||||||
...user,
|
|
||||||
createdAt: new Date(user.createdAt),
|
|
||||||
};
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
const users = await ctx.prisma.user.findMany({
|
|
||||||
select: {
|
|
||||||
id: true,
|
|
||||||
name: true,
|
|
||||||
isAdmin: true,
|
|
||||||
isVIP: true,
|
|
||||||
createdAt: true,
|
|
||||||
email: true,
|
|
||||||
sessions: {
|
|
||||||
select: {
|
|
||||||
id: true,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
accounts: {
|
|
||||||
select: {
|
|
||||||
provider: true,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
await setCache(`${env.APP_ENV}_kv_userlist_admin`, users);
|
|
||||||
|
|
||||||
return users;
|
|
||||||
}
|
|
||||||
}),
|
|
||||||
delete: protectedProcedure
|
|
||||||
.input(
|
|
||||||
z
|
|
||||||
.object({
|
|
||||||
userId: z.string(),
|
|
||||||
})
|
|
||||||
.optional()
|
|
||||||
)
|
|
||||||
.mutation(async ({ ctx, input }) => {
|
|
||||||
let user: User;
|
|
||||||
if (input?.userId && ctx.session.user.isAdmin) {
|
|
||||||
user = await ctx.prisma.user.delete({
|
|
||||||
where: {
|
|
||||||
id: input.userId,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
user = await ctx.prisma.user.delete({
|
|
||||||
where: {
|
|
||||||
id: ctx.session.user.id,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!!user && user.name && user.email) {
|
|
||||||
await resend.emails.send({
|
|
||||||
from: "Sprint Padawan <no-reply@sprintpadawan.dev>",
|
|
||||||
to: user.email,
|
|
||||||
subject: "Sorry to see you go... 😭",
|
|
||||||
react: Goodbye({ name: user.name }),
|
|
||||||
});
|
|
||||||
|
|
||||||
await invalidateCache(`kv_usercount`);
|
|
||||||
await invalidateCache(`kv_userlist_admin`);
|
|
||||||
|
|
||||||
await publishToChannel(
|
|
||||||
`stats`,
|
|
||||||
EventTypes.STATS_UPDATE,
|
|
||||||
JSON.stringify(user)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return !!user;
|
|
||||||
}),
|
|
||||||
save: protectedProcedure
|
|
||||||
.input(
|
|
||||||
z.object({
|
|
||||||
name: z.string(),
|
|
||||||
})
|
|
||||||
)
|
|
||||||
.mutation(async ({ ctx, input }) => {
|
|
||||||
const user = await ctx.prisma.user.update({
|
|
||||||
where: {
|
|
||||||
id: ctx.session.user.id,
|
|
||||||
},
|
|
||||||
data: {
|
|
||||||
name: input.name,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
return !!user;
|
|
||||||
}),
|
|
||||||
setAdmin: adminProcedure
|
|
||||||
.input(
|
|
||||||
z.object({
|
|
||||||
userId: z.string(),
|
|
||||||
value: z.boolean(),
|
|
||||||
})
|
|
||||||
)
|
|
||||||
.mutation(async ({ ctx, input }) => {
|
|
||||||
const user = await ctx.prisma.user.update({
|
|
||||||
where: {
|
|
||||||
id: input.userId,
|
|
||||||
},
|
|
||||||
data: {
|
|
||||||
isAdmin: input.value,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
await invalidateCache(`kv_userlist_admin`);
|
|
||||||
|
|
||||||
return !!user;
|
|
||||||
}),
|
|
||||||
|
|
||||||
setVIP: adminProcedure
|
|
||||||
.input(
|
|
||||||
z.object({
|
|
||||||
userId: z.string(),
|
|
||||||
value: z.boolean(),
|
|
||||||
})
|
|
||||||
)
|
|
||||||
.mutation(async ({ ctx, input }) => {
|
|
||||||
const user = await ctx.prisma.user.update({
|
|
||||||
where: {
|
|
||||||
id: input.userId,
|
|
||||||
},
|
|
||||||
data: {
|
|
||||||
isVIP: input.value,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
await invalidateCache(`kv_userlist_admin`);
|
|
||||||
|
|
||||||
return !!user;
|
|
||||||
}),
|
|
||||||
});
|
|
|
@ -17,9 +17,6 @@ export const voteRouter = createTRPCRouter({
|
||||||
id: string;
|
id: string;
|
||||||
createdAt: Date;
|
createdAt: Date;
|
||||||
userId: string;
|
userId: string;
|
||||||
owner: {
|
|
||||||
name: string | null;
|
|
||||||
};
|
|
||||||
roomId: string;
|
roomId: string;
|
||||||
}[]
|
}[]
|
||||||
>(`kv_votes_${input.roomId}`);
|
>(`kv_votes_${input.roomId}`);
|
||||||
|
@ -34,11 +31,6 @@ export const voteRouter = createTRPCRouter({
|
||||||
select: {
|
select: {
|
||||||
id: true,
|
id: true,
|
||||||
createdAt: true,
|
createdAt: true,
|
||||||
owner: {
|
|
||||||
select: {
|
|
||||||
name: true,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
room: true,
|
room: true,
|
||||||
roomId: true,
|
roomId: true,
|
||||||
userId: true,
|
userId: true,
|
||||||
|
@ -58,17 +50,17 @@ export const voteRouter = createTRPCRouter({
|
||||||
where: {
|
where: {
|
||||||
userId_roomId: {
|
userId_roomId: {
|
||||||
roomId: input.roomId,
|
roomId: input.roomId,
|
||||||
userId: ctx.session.user.id,
|
userId: ctx.auth.userId,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
create: {
|
create: {
|
||||||
value: input.value,
|
value: input.value,
|
||||||
userId: ctx.session.user.id,
|
userId: ctx.auth.userId,
|
||||||
roomId: input.roomId,
|
roomId: input.roomId,
|
||||||
},
|
},
|
||||||
update: {
|
update: {
|
||||||
value: input.value,
|
value: input.value,
|
||||||
userId: ctx.session.user.id,
|
userId: ctx.auth.userId,
|
||||||
roomId: input.roomId,
|
roomId: input.roomId,
|
||||||
},
|
},
|
||||||
select: {
|
select: {
|
||||||
|
@ -76,11 +68,6 @@ export const voteRouter = createTRPCRouter({
|
||||||
userId: true,
|
userId: true,
|
||||||
roomId: true,
|
roomId: true,
|
||||||
id: true,
|
id: true,
|
||||||
owner: {
|
|
||||||
select: {
|
|
||||||
name: true,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
@ -1,81 +1,71 @@
|
||||||
/**
|
/**
|
||||||
* YOU PROBABLY DON'T NEED TO EDIT THIS FILE, UNLESS:
|
* YOU PROBABLY DON'T NEED TO EDIT THIS FILE, UNLESS:
|
||||||
* 1. You want to modify request context (see Part 1).
|
* 1. You want to modify request context (see Part 1)
|
||||||
* 2. You want to create a new middleware or type of procedure (see Part 3).
|
* 2. You want to create a new middleware or type of procedure (see Part 3)
|
||||||
*
|
*
|
||||||
* TL;DR - This is where all the tRPC server stuff is created and plugged in. The pieces you will
|
* tl;dr - this is where all the tRPC server stuff is created and plugged in.
|
||||||
* need to use are documented accordingly near the end.
|
* The pieces you will need to use are documented accordingly near the end
|
||||||
*/
|
*/
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 1. CONTEXT
|
* 1. CONTEXT
|
||||||
*
|
*
|
||||||
* This section defines the "contexts" that are available in the backend API.
|
* This section defines the "contexts" that are available in the backend API
|
||||||
|
*
|
||||||
|
* These allow you to access things like the database, the session, etc, when
|
||||||
|
* processing a request
|
||||||
*
|
*
|
||||||
* These allow you to access things when processing a request, like the database, the session, etc.
|
|
||||||
*/
|
*/
|
||||||
import { type CreateNextContextOptions } from "@trpc/server/adapters/next";
|
import { type CreateNextContextOptions } from "@trpc/server/adapters/next";
|
||||||
import { type Session } from "next-auth";
|
import { getAuth } from "@clerk/nextjs/server";
|
||||||
|
import type {
|
||||||
|
SignedInAuthObject,
|
||||||
|
SignedOutAuthObject,
|
||||||
|
} from "@clerk/nextjs/api";
|
||||||
|
|
||||||
import { getServerAuthSession } from "~/server/auth";
|
import { prisma } from "../db";
|
||||||
import { prisma } from "~/server/db";
|
|
||||||
|
|
||||||
type CreateContextOptions = {
|
|
||||||
session: Session | null;
|
|
||||||
ip: string | undefined;
|
|
||||||
};
|
|
||||||
|
|
||||||
|
interface AuthContext {
|
||||||
|
auth: SignedInAuthObject | SignedOutAuthObject;
|
||||||
|
}
|
||||||
/**
|
/**
|
||||||
* This helper generates the "internals" for a tRPC context. If you need to use it, you can export
|
* This helper generates the "internals" for a tRPC context. If you need to use
|
||||||
* it from here.
|
* it, you can export it from here
|
||||||
*
|
*
|
||||||
* Examples of things you may need it for:
|
* Examples of things you may need it for:
|
||||||
* - testing, so we don't have to mock Next.js' req/res
|
* - testing, so we dont have to mock Next.js' req/res
|
||||||
* - tRPC's `createSSGHelpers`, where we don't have req/res
|
* - trpc's `createSSGHelpers` where we don't have req/res
|
||||||
*
|
|
||||||
* @see https://create.t3.gg/en/usage/trpc#-servertrpccontextts
|
* @see https://create.t3.gg/en/usage/trpc#-servertrpccontextts
|
||||||
*/
|
*/
|
||||||
const createInnerTRPCContext = (opts: CreateContextOptions) => {
|
const createInnerTRPCContext = ({ auth }: AuthContext) => {
|
||||||
return {
|
return {
|
||||||
session: opts.session,
|
auth,
|
||||||
ip: opts.ip,
|
|
||||||
prisma,
|
prisma,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* This is the actual context you will use in your router. It will be used to process every request
|
* This is the actual context you'll use in your router. It will be used to
|
||||||
* that goes through your tRPC endpoint.
|
* process every request that goes through your tRPC endpoint
|
||||||
*
|
* @link https://trpc.io/docs/context
|
||||||
* @see https://trpc.io/docs/context
|
|
||||||
*/
|
*/
|
||||||
export const createTRPCContext = async (opts: CreateNextContextOptions) => {
|
export const createTRPCContext = (opts: CreateNextContextOptions) => {
|
||||||
const { req, res } = opts;
|
return createInnerTRPCContext({ auth: getAuth(opts.req) });
|
||||||
|
|
||||||
// Get the session from the server using the getServerSession wrapper function
|
|
||||||
const session = await getServerAuthSession({ req, res });
|
|
||||||
|
|
||||||
return createInnerTRPCContext({
|
|
||||||
ip: req.socket.remoteAddress,
|
|
||||||
session,
|
|
||||||
});
|
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 2. INITIALIZATION
|
* 2. INITIALIZATION
|
||||||
*
|
*
|
||||||
* This is where the tRPC API is initialized, connecting the context and transformer.
|
* This is where the trpc api is initialized, connecting the context and
|
||||||
|
* transformer
|
||||||
*/
|
*/
|
||||||
import { initTRPC, TRPCError } from "@trpc/server";
|
import { initTRPC, TRPCError } from "@trpc/server";
|
||||||
import type { OpenApiMeta } from "trpc-openapi";
|
|
||||||
import { Ratelimit } from "@upstash/ratelimit";
|
|
||||||
import superjson from "superjson";
|
import superjson from "superjson";
|
||||||
import { env } from "~/env.mjs";
|
import { OpenApiMeta } from "trpc-openapi";
|
||||||
import { Redis } from "@upstash/redis";
|
|
||||||
|
|
||||||
const t = initTRPC
|
const t = initTRPC
|
||||||
.meta<OpenApiMeta>()
|
|
||||||
.context<typeof createTRPCContext>()
|
.context<typeof createTRPCContext>()
|
||||||
|
.meta<OpenApiMeta>()
|
||||||
.create({
|
.create({
|
||||||
transformer: superjson,
|
transformer: superjson,
|
||||||
errorFormatter({ shape }) {
|
errorFormatter({ shape }) {
|
||||||
|
@ -83,91 +73,37 @@ const t = initTRPC
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// check if the user is signed in, otherwise through a UNAUTHORIZED CODE
|
||||||
|
const isAuthed = t.middleware(({ next, ctx }) => {
|
||||||
|
if (!ctx.auth.userId) {
|
||||||
|
throw new TRPCError({ code: "UNAUTHORIZED" });
|
||||||
|
}
|
||||||
|
|
||||||
|
return next({
|
||||||
|
ctx: {
|
||||||
|
auth: ctx.auth,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
});
|
||||||
/**
|
/**
|
||||||
* 3. ROUTER & PROCEDURE (THE IMPORTANT BIT)
|
* 3. ROUTER & PROCEDURE (THE IMPORTANT BIT)
|
||||||
*
|
*
|
||||||
* These are the pieces you use to build your tRPC API. You should import these a lot in the
|
* These are the pieces you use to build your tRPC API. You should import these
|
||||||
* "/src/server/api/routers" directory.
|
* a lot in the /src/server/api/routers folder
|
||||||
*/
|
*/
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* This is how you create new routers and sub-routers in your tRPC API.
|
* This is how you create new routers and subrouters in your tRPC API
|
||||||
*
|
|
||||||
* @see https://trpc.io/docs/router
|
* @see https://trpc.io/docs/router
|
||||||
*/
|
*/
|
||||||
export const createTRPCRouter = t.router;
|
export const createTRPCRouter = t.router;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Public (unauthenticated) procedure
|
* Public (unauthed) procedure
|
||||||
*
|
*
|
||||||
* This is the base piece you use to build new queries and mutations on your tRPC API. It does not
|
* This is the base piece you use to build new queries and mutations on your
|
||||||
* guarantee that a user querying is authorized, but you can still access user session data if they
|
* tRPC API. It does not guarantee that a user querying is authorized, but you
|
||||||
* are logged in.
|
* can still access user session data if they are logged in
|
||||||
*/
|
*/
|
||||||
export const publicProcedure = t.procedure;
|
export const publicProcedure = t.procedure;
|
||||||
|
export const protectedProcedure = t.procedure.use(isAuthed);
|
||||||
/** Reusable middleware that enforces users are logged in before running the procedure. */
|
|
||||||
const enforceAuthSession = t.middleware(async ({ ctx, next }) => {
|
|
||||||
// Auth
|
|
||||||
if (!ctx.session || !ctx.session.user) {
|
|
||||||
throw new TRPCError({ code: "UNAUTHORIZED" });
|
|
||||||
}
|
|
||||||
|
|
||||||
const rateLimit = new Ratelimit({
|
|
||||||
redis: Redis.fromEnv(),
|
|
||||||
limiter: Ratelimit.slidingWindow(
|
|
||||||
Number(env.UPSTASH_RATELIMIT_REQUESTS),
|
|
||||||
`${Number(env.UPSTASH_RATELIMIT_SECONDS)}s`
|
|
||||||
),
|
|
||||||
analytics: true,
|
|
||||||
});
|
|
||||||
|
|
||||||
const { success } = await rateLimit.limit(
|
|
||||||
`${env.APP_ENV}_${ctx.session?.user.id}`
|
|
||||||
);
|
|
||||||
|
|
||||||
if (!success) throw new TRPCError({ code: "TOO_MANY_REQUESTS" });
|
|
||||||
|
|
||||||
return next({
|
|
||||||
ctx: {
|
|
||||||
session: { ...ctx.session, user: ctx.session.user },
|
|
||||||
},
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
const enforceAdminRole = t.middleware(async ({ ctx, next }) => {
|
|
||||||
if (!ctx.session || !ctx.session.user || !ctx.session?.user.isAdmin)
|
|
||||||
throw new TRPCError({ code: "UNAUTHORIZED" });
|
|
||||||
|
|
||||||
return next({
|
|
||||||
ctx: {
|
|
||||||
session: { ...ctx.session, user: ctx.session.user },
|
|
||||||
},
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
// const enforceApiToken = t.middleware(async ({ ctx, next, path }) => {
|
|
||||||
// const res = await unkey.keys.verify({
|
|
||||||
// key: ""
|
|
||||||
// })
|
|
||||||
// if (!ctx.session || !ctx.session.user || !ctx.session?.user.isAdmin)
|
|
||||||
// throw new TRPCError({ code: "UNAUTHORIZED" });
|
|
||||||
|
|
||||||
// return next({
|
|
||||||
// ctx: {
|
|
||||||
// session: { ...ctx.session, user: ctx.session.user },
|
|
||||||
// },
|
|
||||||
// });
|
|
||||||
// });
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Protected (authenticated) procedure
|
|
||||||
*
|
|
||||||
* If you want a query or mutation to ONLY be accessible to logged in users, use this. It verifies
|
|
||||||
* the session is valid and guarantees `ctx.session.user` is not null.
|
|
||||||
*
|
|
||||||
* @see https://trpc.io/docs/procedures
|
|
||||||
*/
|
|
||||||
export const protectedProcedure = t.procedure.use(enforceAuthSession);
|
|
||||||
|
|
||||||
export const adminProcedure = t.procedure.use(enforceAdminRole);
|
|
||||||
|
|
|
@ -1,118 +0,0 @@
|
||||||
import { PrismaAdapter } from "@auth/prisma-adapter";
|
|
||||||
import { type GetServerSidePropsContext } from "next";
|
|
||||||
import {
|
|
||||||
getServerSession,
|
|
||||||
type DefaultSession,
|
|
||||||
type NextAuthOptions,
|
|
||||||
} from "next-auth";
|
|
||||||
import GithubProvider from "next-auth/providers/github";
|
|
||||||
import GoogleProvider from "next-auth/providers/google";
|
|
||||||
import { Resend } from "resend";
|
|
||||||
import { env } from "~/env.mjs";
|
|
||||||
import { prisma } from "~/server/db";
|
|
||||||
import { Welcome } from "../components/templates/Welcome";
|
|
||||||
import { invalidateCache } from "./redis";
|
|
||||||
import { publishToChannel } from "./ably";
|
|
||||||
import { EventTypes } from "~/utils/types";
|
|
||||||
|
|
||||||
const resend = new Resend(process.env.RESEND_API_KEY);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Module augmentation for `next-auth` types. Allows us to add custom properties to the `session`
|
|
||||||
* object and keep type safety.
|
|
||||||
*
|
|
||||||
* @see https://next-auth.js.org/getting-started/typescript#module-augmentation
|
|
||||||
*/
|
|
||||||
declare module "next-auth" {
|
|
||||||
interface Session extends DefaultSession {
|
|
||||||
user: {
|
|
||||||
id: string;
|
|
||||||
isAdmin: boolean;
|
|
||||||
isVIP: boolean;
|
|
||||||
} & DefaultSession["user"];
|
|
||||||
}
|
|
||||||
|
|
||||||
interface User {
|
|
||||||
isAdmin: boolean;
|
|
||||||
isVIP: boolean;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Options for NextAuth.js used to configure adapters, providers, callbacks, etc.
|
|
||||||
*
|
|
||||||
* @see https://next-auth.js.org/configuration/options
|
|
||||||
*/
|
|
||||||
export const authOptions: NextAuthOptions = {
|
|
||||||
callbacks: {
|
|
||||||
session({ session, user }) {
|
|
||||||
if (session.user) {
|
|
||||||
session.user.id = user.id;
|
|
||||||
session.user.isAdmin = user.isAdmin;
|
|
||||||
session.user.isVIP = user.isVIP;
|
|
||||||
}
|
|
||||||
return session;
|
|
||||||
},
|
|
||||||
},
|
|
||||||
events: {
|
|
||||||
async createUser({ user }) {
|
|
||||||
if (user && user.name && user.email) {
|
|
||||||
await resend.sendEmail({
|
|
||||||
from: "no-reply@sprintpadawan.dev",
|
|
||||||
to: user.email,
|
|
||||||
subject: "🎉 Welcome to Sprint Padawan! 🎉",
|
|
||||||
//@ts-ignore: IDK why this doesn't work...
|
|
||||||
|
|
||||||
react: Welcome({ name: user.name }),
|
|
||||||
});
|
|
||||||
await invalidateCache(`kv_userlist_admin`);
|
|
||||||
await invalidateCache(`kv_usercount`);
|
|
||||||
|
|
||||||
await publishToChannel(
|
|
||||||
`stats`,
|
|
||||||
EventTypes.STATS_UPDATE,
|
|
||||||
JSON.stringify(user)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
async signIn({}) {
|
|
||||||
await invalidateCache(`kv_userlist_admin`);
|
|
||||||
},
|
|
||||||
async signOut() {
|
|
||||||
await invalidateCache(`kv_userlist_admin`);
|
|
||||||
},
|
|
||||||
},
|
|
||||||
// @ts-ignore This adapter should work...
|
|
||||||
adapter: PrismaAdapter(prisma),
|
|
||||||
providers: [
|
|
||||||
GithubProvider({
|
|
||||||
clientId: env.GITHUB_CLIENT_ID,
|
|
||||||
clientSecret: env.GITHUB_CLIENT_SECRET,
|
|
||||||
}),
|
|
||||||
GoogleProvider({
|
|
||||||
clientId: env.GOOGLE_CLIENT_ID,
|
|
||||||
clientSecret: env.GOOGLE_CLIENT_SECRET,
|
|
||||||
}),
|
|
||||||
/**
|
|
||||||
* ...add more providers here.
|
|
||||||
*
|
|
||||||
* Most other providers require a bit more work than the Discord provider. For example, the
|
|
||||||
* GitHub provider requires you to add the `refresh_token_expires_in` field to the Account
|
|
||||||
* model. Refer to the NextAuth.js docs for the provider you want to use. Example:
|
|
||||||
*
|
|
||||||
* @see https://next-auth.js.org/providers/github
|
|
||||||
*/
|
|
||||||
],
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Wrapper for `getServerSession` so that you don't need to import the `authOptions` in every file.
|
|
||||||
*
|
|
||||||
* @see https://next-auth.js.org/configuration/nextjs
|
|
||||||
*/
|
|
||||||
export const getServerAuthSession = (ctx: {
|
|
||||||
req: GetServerSidePropsContext["req"];
|
|
||||||
res: GetServerSidePropsContext["res"];
|
|
||||||
}) => {
|
|
||||||
return getServerSession(ctx.req, ctx.res, authOptions);
|
|
||||||
};
|
|
|
@ -8,6 +8,7 @@ export const redis = Redis.fromEnv({
|
||||||
|
|
||||||
export const setCache = async <T>(key: string, value: T) => {
|
export const setCache = async <T>(key: string, value: T) => {
|
||||||
try {
|
try {
|
||||||
|
console.log("KEY: ", key);
|
||||||
await redis.set(`${env.APP_ENV}_${key}`, value, {
|
await redis.set(`${env.APP_ENV}_${key}`, value, {
|
||||||
ex: Number(env.UPSTASH_REDIS_EXPIRY_SECONDS),
|
ex: Number(env.UPSTASH_REDIS_EXPIRY_SECONDS),
|
||||||
});
|
});
|
||||||
|
|
Loading…
Add table
Reference in a new issue