Overhaul!
This commit is contained in:
parent
c907729b44
commit
039fd81c4e
28 changed files with 658 additions and 1462 deletions
|
@ -8,9 +8,9 @@ UPSTASH_REDIS_EXPIRY_SECONDS=""
|
|||
UPSTASH_RATELIMIT_REQUESTS=""
|
||||
UPSTASH_RATELIMIT_SECONDS=""
|
||||
|
||||
#Next Auth Core
|
||||
NEXTAUTH_SECRET=""
|
||||
NEXTAUTH_URL=""
|
||||
#Auth
|
||||
NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY=""
|
||||
CLERK_SECRET_KEY=""
|
||||
|
||||
# Next Auth Github Provider
|
||||
GITHUB_CLIENT_ID=""
|
||||
|
|
|
@ -10,7 +10,11 @@ const config = {
|
|||
defaultLocale: "en",
|
||||
},
|
||||
images: {
|
||||
domains: ["avatars.githubusercontent.com", "lh3.googleusercontent.com"],
|
||||
domains: [
|
||||
"avatars.githubusercontent.com",
|
||||
"lh3.googleusercontent.com",
|
||||
"img.clerk.com",
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
|
|
|
@ -1,11 +1,11 @@
|
|||
{
|
||||
"name": "sprintpadawan",
|
||||
"version": "1.2.7",
|
||||
"version": "2.0.0",
|
||||
"description": "Plan. Sprint. Repeat.",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"build": "next build",
|
||||
"serv": "NEXTAUTH_URL=http://localhost:3000 && next dev",
|
||||
"serv": "next dev",
|
||||
"dev": "pnpm serv",
|
||||
"postinstall": "prisma generate",
|
||||
"lint": "next lint",
|
||||
|
@ -14,6 +14,7 @@
|
|||
"dependencies": {
|
||||
"@ably-labs/react-hooks": "^2.1.1",
|
||||
"@auth/prisma-adapter": "^1.0.1",
|
||||
"@clerk/nextjs": "^4.23.2",
|
||||
"@prisma/client": "5.1.1",
|
||||
"@react-email/components": "^0.0.7",
|
||||
"@tanstack/react-query": "^4.32.6",
|
||||
|
@ -28,7 +29,6 @@
|
|||
"autoprefixer": "^10.4.14",
|
||||
"json2csv": "6.0.0-alpha.2",
|
||||
"next": "^13.4.13",
|
||||
"next-auth": "^4.22.5",
|
||||
"nextjs-cors": "^2.1.2",
|
||||
"postcss": "^8.4.27",
|
||||
"react": "18.2.0",
|
||||
|
|
358
pnpm-lock.yaml
generated
358
pnpm-lock.yaml
generated
|
@ -11,6 +11,9 @@ dependencies:
|
|||
'@auth/prisma-adapter':
|
||||
specifier: ^1.0.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':
|
||||
specifier: 5.1.1
|
||||
version: 5.1.1(prisma@5.1.1)
|
||||
|
@ -215,6 +218,83 @@ packages:
|
|||
dependencies:
|
||||
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):
|
||||
resolution: {integrity: sha512-v0BqORYamk1koxDon6femDGLWSL7P78vYTyOU5nFaALnmNALL+ktgdHvWbxzzBBJIKS7kv3XvM/DqNwiLcgFTA==}
|
||||
peerDependencies:
|
||||
|
@ -769,6 +849,32 @@ packages:
|
|||
resolution: {integrity: sha512-dhPeilub1NuIG0X5Kvhh9lH4iW3ZsHlnzwgwbOlgwQ2wG1IqFzsgHqmKPk3WzsdWAeaxKJxgM0+W433RmN45GA==}
|
||||
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):
|
||||
resolution: {integrity: sha512-fxcCeK5pMQGcgCqCrWsi+I2rpIbk0rAhdrN+ke7f34tIrgPwA68ensrpin+9+fZvuV2OtzHmuipwduSY6HswdA==}
|
||||
engines: {node: '>=16.13'}
|
||||
|
@ -1072,6 +1178,13 @@ packages:
|
|||
resolution: {integrity: sha512-r3VeA319/braYMBIzj+XLgLKQ9lJSVglvPvP9HUv4kr5w6Y5grQMxMcExhTiZWltE9bnSJHKtBBzHafOo7KC8A==}
|
||||
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:
|
||||
resolution: {integrity: sha512-IQ3EbTzGxIigb1I3qPZc1rWJnH0BmSKv5QYTalEwweFvyBDLSAe24zP0le/hyi7ecGfZVlIVAg4BZqb8WBwKqw==}
|
||||
dependencies:
|
||||
|
@ -1081,6 +1194,21 @@ packages:
|
|||
'@types/responselike': 1.0.0
|
||||
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:
|
||||
resolution: {integrity: sha512-sdPRb9K6iL5XZOmBubg8yiFp5yS/JdUDQsq5e6h95km91MCYMuvp7mh1fjPEYUhvHepKpZOjnEaMBR4PxjWDzg==}
|
||||
dependencies:
|
||||
|
@ -1092,10 +1220,32 @@ packages:
|
|||
resolution: {integrity: sha512-LG4opVs2ANWZ1TJoKc937iMmNstM/d0ae1vNbnBvBhqCSezgVUOzcLCqbI5elV8Vy6WKwKjaqR+zO9VKirBBCA==}
|
||||
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:
|
||||
resolution: {integrity: sha512-SZs7ekbP8CN0txVG2xVRH6EgKmEm31BOxA07vkFaETzZz1xh+cbt8BcI0slpymvwhx5dlFnQG2rTlPVQn+iRPQ==}
|
||||
dev: false
|
||||
|
||||
/@types/http-errors@2.0.1:
|
||||
resolution: {integrity: sha512-/K3ds8TRAfBvi5vfjuz8y6+GiAYBZ0x4tXv1Av6CWBWn0IlADc+ZX9pMq7oU0fNQPnBwIZl3rmeLp6SBApbxSQ==}
|
||||
dev: false
|
||||
|
||||
/@types/json-schema@7.0.12:
|
||||
resolution: {integrity: sha512-Hr5Jfhc9eYOQNPYO5WLDq/n4jqijdHNlDXjuAQkkt+mWdQR+XJToOHrsD4cPaMXpn6KO7y2+wM8AZEs8VpBLVA==}
|
||||
dev: true
|
||||
|
@ -1110,16 +1260,39 @@ packages:
|
|||
resolution: {integrity: sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ==}
|
||||
dev: true
|
||||
|
||||
/@types/keygrip@1.0.2:
|
||||
resolution: {integrity: sha512-GJhpTepz2udxGexqos8wgaBx4I/zWIDPh/KOGEwAqtuGDkOUJu5eFvwmdBX4AmB8Odsr+9pHCQqiAqDL/yKMKw==}
|
||||
dev: false
|
||||
|
||||
/@types/keyv@3.1.4:
|
||||
resolution: {integrity: sha512-BQ5aZNSCpj7D6K2ksrRCTmKRLEpnPvWDiLPfoGyhZ++8YtiK9d/3DBKPJgry359X/P1PfruyYwvnvwFjuEiEIg==}
|
||||
dependencies:
|
||||
'@types/node': 20.4.9
|
||||
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:
|
||||
resolution: {integrity: sha512-J8xLz7q2OFulZ2cyGTLE1TbbZcjpno7FaN6zdJNrgAdrJ+DZzh/uFR6YrTb4C+nXakvud8Q4+rbhoIWlYQbUFQ==}
|
||||
dev: false
|
||||
|
||||
/@types/node@16.18.6:
|
||||
resolution: {integrity: sha512-vmYJF0REqDyyU0gviezF/KHq/fYaUbFhkcNbQCuPGFQj6VTbXuHZoxs/Y7mutWe73C8AC6l9fFu8mSYiBAqkGA==}
|
||||
dev: false
|
||||
|
||||
/@types/node@20.4.9:
|
||||
resolution: {integrity: sha512-8e2HYcg7ohnTUbHk8focoklEQYvemQmu9M/f43DZVx43kHn0tE3BY/6gSDxS7k0SprtS0NHvj+L80cGLnoOUcQ==}
|
||||
|
||||
|
@ -1131,6 +1304,14 @@ packages:
|
|||
resolution: {integrity: sha512-JCB8C6SnDoQf0cNycqd/35A7MjcnK+ZTqE7judS6o7utxUCg6imJg3QK2qzHKszlTjcj2cn+NwMB2i96ubpj7w==}
|
||||
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:
|
||||
resolution: {integrity: sha512-WKNtmsLWJM/3D5mG4U84cysVY31ivmyw85dE84fOCk5Hx78wezB/XEjVPWl2JTZ5FkEeaTJf+VgUAUn3PE7Isw==}
|
||||
dependencies:
|
||||
|
@ -1153,6 +1334,21 @@ packages:
|
|||
resolution: {integrity: sha512-G8hZ6XJiHnuhQKR7ZmysCeJWE08o8T0AXtk5darsCaTVsYZhhgUrq53jizaR2FvsoeCwJhlmwTjkXBY5Pn/ZHw==}
|
||||
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):
|
||||
resolution: {integrity: sha512-IZYjYZ0ifGSLZbwMqIip/nOamFiWJ9AH+T/GYNZBWkVcyNQOFGtSMoWV7RvY4poYCMZ/4lHzNl796WOSNxmk8A==}
|
||||
engines: {node: ^16.0.0 || >=18.0.0}
|
||||
|
@ -1502,6 +1698,15 @@ packages:
|
|||
is-shared-array-buffer: 1.0.2
|
||||
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:
|
||||
resolution: {integrity: sha512-eBvWn1lvIApYMhzQMsu9ciLfkBY499mFZlNqG+/9WR7PVlroQw0vG30cOQQbaKz3sCEc44TAOu2ykzqXSNnwag==}
|
||||
dev: true
|
||||
|
@ -1702,6 +1907,20 @@ packages:
|
|||
resolution: {integrity: sha512-QOSvevhslijgYwRx6Rv7zKdMF8lbRmx+uQGx2+vDc+KI/eBnsy9kit5aj23AgGu3pa4t9AgwbnXWqS+iOY+2aA==}
|
||||
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:
|
||||
resolution: {integrity: sha512-0QHgqR+Jv4bxHMp8kZ1Kn8CH55OikjKJ6JmKkZYP1F3D7w+lnFXF70nG5eNfsZS89jadi5Ywy5UCSKLAglIRkg==}
|
||||
dev: false
|
||||
|
@ -1926,6 +2145,10 @@ packages:
|
|||
engines: {node: '>=4'}
|
||||
hasBin: true
|
||||
|
||||
/csstype@3.1.1:
|
||||
resolution: {integrity: sha512-DJR/VvkAvSZW9bTouZue2sSxDwdTN92uHjqeKVm+0dAqdfNykRzQ95tay8aXMBAAPpUiq4Qcug2L7neoRh2Egw==}
|
||||
dev: false
|
||||
|
||||
/csstype@3.1.2:
|
||||
resolution: {integrity: sha512-I7K1Uu0MBPzaFKg4nI5Q7Vs2t+3gWWW648spaF+Rg7pI9ds18Ugn+lvg4SHczUdKlHI5LWBXyqfS8+DufyBsgQ==}
|
||||
dev: true
|
||||
|
@ -1986,6 +2209,11 @@ packages:
|
|||
resolution: {integrity: sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==}
|
||||
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:
|
||||
resolution: {integrity: sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==}
|
||||
engines: {node: '>=0.10.0'}
|
||||
|
@ -2121,6 +2349,13 @@ packages:
|
|||
domhandler: 5.0.3
|
||||
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:
|
||||
resolution: {integrity: sha512-asLFVfWWtJ90ZyOUHMqk7/S2w2guQKxUI2itj3d92ADHhxUSbCMGi1f1cBcJ7xM1To+pE/Khbwo1yuNbMEPKeA==}
|
||||
dependencies:
|
||||
|
@ -2696,6 +2931,15 @@ packages:
|
|||
is-callable: 1.2.7
|
||||
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:
|
||||
resolution: {integrity: sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==}
|
||||
engines: {node: '>= 6'}
|
||||
|
@ -3344,6 +3588,11 @@ packages:
|
|||
nopt: 6.0.0
|
||||
dev: false
|
||||
|
||||
/js-cookie@3.0.1:
|
||||
resolution: {integrity: sha512-+0rgsUXZu4ncpPxRL+lNEptWMOWl9etvPHc/koSRp6MPwpRYAhmk0dUG00J4bxVV3r9uUzfo24wW0knS07SKSw==}
|
||||
engines: {node: '>=12'}
|
||||
dev: false
|
||||
|
||||
/js-tokens@4.0.0:
|
||||
resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==}
|
||||
|
||||
|
@ -3505,6 +3754,12 @@ packages:
|
|||
dependencies:
|
||||
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:
|
||||
resolution: {integrity: sha512-tqNXrS78oMOE73NMxK4EMLQsQowWf8jKooH9g7xPavRT706R6bkQJ6DY2Te7QukaZsulxa30wQ7bk0pm4XiHmA==}
|
||||
engines: {node: '>=8'}
|
||||
|
@ -3516,6 +3771,11 @@ packages:
|
|||
dependencies:
|
||||
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:
|
||||
resolution: {integrity: sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==}
|
||||
engines: {node: '>= 0.6'}
|
||||
|
@ -3723,6 +3983,13 @@ packages:
|
|||
next: 13.4.13(react-dom@18.2.0)(react@18.2.0)
|
||||
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:
|
||||
resolution: {integrity: sha512-iwXuFrMAcFVi/ZoZiqq8BzAdsLw9kxDfTC0HMyjXfSL/6CSDAGD5UmR7azrAgWV1zKYq7dUUMj4owusBWKLsiQ==}
|
||||
engines: {node: '>=10'}
|
||||
|
@ -3734,6 +4001,10 @@ packages:
|
|||
resolution: {integrity: sha512-+eawOlIgy680F0kBzPUNFhMZGtJ1YmqM6l4+Crf4IkImjYrO/mqPwRMh352g23uIaQKFItcQ64I7KMaJxHgAVA==}
|
||||
dev: false
|
||||
|
||||
/node-fetch-native@1.0.1:
|
||||
resolution: {integrity: sha512-VzW+TAk2wE4X9maiKMlT+GsPU4OMmR1U9CrHSmd3DFLn2IcZ9VJ6M6BBugGfYUnPCLSYxXdZy17M0BEJyhUTwg==}
|
||||
dev: false
|
||||
|
||||
/node-fetch@2.6.12:
|
||||
resolution: {integrity: sha512-C/fGU2E8ToujUivIO0H+tpQ6HWo4eEmchoPIoXtxCrVghxdKq+QOHqEZW7tuP3KlV3bC8FRMO5nMCC7Zm1VP6g==}
|
||||
engines: {node: 4.x || >=6.0.0}
|
||||
|
@ -4025,6 +4296,10 @@ packages:
|
|||
/path-parse@1.0.7:
|
||||
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:
|
||||
resolution: {integrity: sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==}
|
||||
engines: {node: '>=8'}
|
||||
|
@ -4307,6 +4582,17 @@ packages:
|
|||
engines: {node: '>=6'}
|
||||
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:
|
||||
resolution: {integrity: sha512-tDNIz22aBzCDxLtVH++VnTfzxlfeK5CbqohpSqpJgj1Wg/cQbStNAz3NuqCs5vV+pjBsK4x4pN9HlVh7rcYRiA==}
|
||||
engines: {node: '>=0.6'}
|
||||
|
@ -4321,6 +4607,11 @@ packages:
|
|||
resolution: {integrity: sha512-kJt5qhMxoszgU/62PLP1CJytzd2NKetjSRnyuj31fDd3Rlcz3fzlFdFLD1SItunPwyqEOkca6GbV612BWfaBag==}
|
||||
dev: false
|
||||
|
||||
/quick-lru@4.0.1:
|
||||
resolution: {integrity: sha512-ARhCpm70fzdcvNQfPoy49IaanKkTlRWF2JMzqhcJbhSFRZv7nPTvZJdcY7301IPmvW+/p0RgIWnQDLJxifsQ7g==}
|
||||
engines: {node: '>=8'}
|
||||
dev: false
|
||||
|
||||
/quick-lru@5.1.1:
|
||||
resolution: {integrity: sha512-WuyALRjWPDGtt/wzJiadO5AXY+8hZ80hVpe6MyivgraREW751X3SbhRvG3eLKOYN+8VEvqLcf3wdnt44Z4S4SA==}
|
||||
engines: {node: '>=10'}
|
||||
|
@ -4704,6 +4995,30 @@ packages:
|
|||
resolution: {integrity: sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==}
|
||||
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:
|
||||
resolution: {integrity: sha512-R0XvVJ9WusLiqTCEiGCmICCMplcCkIwwR11mOSD9CR5u+IXYdiseeEuXCVAjS54zqwkLcPNnmU4OeJ6tUrWhDw==}
|
||||
engines: {node: '>=0.10.0'}
|
||||
|
@ -4893,6 +5208,14 @@ packages:
|
|||
resolution: {integrity: sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==}
|
||||
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):
|
||||
resolution: {integrity: sha512-B6DLqJzc21x7wntlH/GsZwEXTBttVSl1FtCzC8WP4oBc/NKef7kaax5jeihkkCEWc831/5NDJ9gRNDK6NEioQQ==}
|
||||
engines: {node: '>=12.13.0'}
|
||||
|
@ -5016,12 +5339,28 @@ packages:
|
|||
any-promise: 1.3.0
|
||||
dev: true
|
||||
|
||||
/to-no-case@1.0.2:
|
||||
resolution: {integrity: sha512-Z3g735FxuZY8rodxV4gH7LxClE4H0hTIyHNIHdk+vpQxjLm0cwnKXq/OFVZ76SOQmto7txVcwSCwkU5kqp+FKg==}
|
||||
dev: false
|
||||
|
||||
/to-regex-range@5.0.1:
|
||||
resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==}
|
||||
engines: {node: '>=8.0'}
|
||||
dependencies:
|
||||
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:
|
||||
resolution: {integrity: sha512-zks18/TWT1iHO3v0vFp5qLKOG27m67ycq/Y7a7cTiRuUNlc4gf3HGnkRgMv0NyhnfTamtkYBJl+YeD1/j07gBQ==}
|
||||
dev: false
|
||||
|
@ -5086,6 +5425,10 @@ packages:
|
|||
strip-bom: 3.0.0
|
||||
dev: true
|
||||
|
||||
/tslib@2.4.1:
|
||||
resolution: {integrity: sha512-tGyy4dAjRIEwI7BzsB0lynWgOpfqjUdq91XXAlIWD2OwKBH7oCl/GZG/HT4BOHrTlPMOASlMQ7veyTqpmRcrNA==}
|
||||
dev: false
|
||||
|
||||
/tslib@2.6.1:
|
||||
resolution: {integrity: sha512-t0hLfiEKfMUoqhG+U1oid7Pva4bbDPHYfJNiB7BiIjRkj1pyC++4N3huJfqY6aRH6VTB0rvtzQwjM4K6qpfOig==}
|
||||
dev: false
|
||||
|
@ -5124,6 +5467,11 @@ packages:
|
|||
engines: {node: '>=8'}
|
||||
dev: false
|
||||
|
||||
/type-fest@2.19.0:
|
||||
resolution: {integrity: sha512-RAH822pAdBgcNMAfWnCBU3CFZcfZ/i1eZjwFU/dsLKumyuuP3niueg2UAukXYF0E2AAoc82ZSSf9J0WQBinzHA==}
|
||||
engines: {node: '>=12.20'}
|
||||
dev: false
|
||||
|
||||
/type-fest@3.13.0:
|
||||
resolution: {integrity: sha512-Gur3yQGM9qiLNs0KPP7LPgeRbio2QTt4xXouobMCarR0/wyW3F+F/+OWwshg3NG0Adon7uQfSZBpB46NfhoF1A==}
|
||||
engines: {node: '>=14.16'}
|
||||
|
@ -5291,6 +5639,16 @@ packages:
|
|||
defaults: 1.0.4
|
||||
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:
|
||||
resolution: {integrity: sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==}
|
||||
dev: false
|
||||
|
|
|
@ -8,62 +8,6 @@ datasource db {
|
|||
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 {
|
||||
id String @id @unique @default(cuid())
|
||||
createdAt DateTime @default(now())
|
||||
|
@ -74,7 +18,6 @@ model Room {
|
|||
votes Vote[]
|
||||
scale String
|
||||
logs Log[]
|
||||
owner User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
||||
|
||||
@@index([userId])
|
||||
}
|
||||
|
@ -85,7 +28,6 @@ model Vote {
|
|||
userId String
|
||||
roomId String
|
||||
value String
|
||||
owner User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
||||
room Room @relation(fields: [roomId], references: [id], onDelete: Cascade)
|
||||
|
||||
@@unique([userId, roomId])
|
||||
|
@ -101,7 +43,6 @@ model Log {
|
|||
votes Json
|
||||
roomName String
|
||||
storyName String
|
||||
owner User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
||||
room Room @relation(fields: [roomId], references: [id], onDelete: Cascade)
|
||||
|
||||
@@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 Link from "next/link";
|
||||
import { useRouter } from "next/router";
|
||||
|
@ -9,19 +9,22 @@ interface NavbarProps {
|
|||
}
|
||||
|
||||
const Navbar = ({ title }: NavbarProps) => {
|
||||
const { data: sessionData, status: sessionStatus } = useSession();
|
||||
const { isLoaded, isSignedIn } = useUser();
|
||||
const router = useRouter();
|
||||
|
||||
const navigationMenu = () => {
|
||||
if (sessionStatus === "authenticated" && router.pathname !== "/dashboard") {
|
||||
if (router.pathname !== "/dashboard" && isSignedIn) {
|
||||
return (
|
||||
<Link className="btn btn-secondary btn-outline mx-2" href="/dashboard">
|
||||
Dashboard
|
||||
</Link>
|
||||
);
|
||||
} else if (sessionStatus === "unauthenticated") {
|
||||
} else if (!isSignedIn) {
|
||||
return (
|
||||
<button className="btn btn-secondary" onClick={() => void signIn()}>
|
||||
<button
|
||||
className="btn btn-secondary"
|
||||
onClick={() => void router.push("/sign-in")}
|
||||
>
|
||||
Sign In
|
||||
</button>
|
||||
);
|
||||
|
@ -51,7 +54,7 @@ const Navbar = ({ title }: NavbarProps) => {
|
|||
</Link>
|
||||
</div>
|
||||
|
||||
{sessionStatus === "loading" ? (
|
||||
{!isLoaded ? (
|
||||
<div className="flex items-center justify-center">
|
||||
<span className="loading loading-dots loading-lg"></span>
|
||||
</div>
|
||||
|
@ -59,53 +62,7 @@ const Navbar = ({ title }: NavbarProps) => {
|
|||
navigationMenu()
|
||||
)}
|
||||
|
||||
{sessionData?.user.image && (
|
||||
<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>
|
||||
)}
|
||||
<UserButton afterSignOutUrl="/" />
|
||||
</nav>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -1,25 +1,24 @@
|
|||
import { useSession } from "next-auth/react";
|
||||
import Link from "next/link";
|
||||
|
||||
import { configureAbly, useChannel } from "@ably-labs/react-hooks";
|
||||
import { useState } from "react";
|
||||
import { IoEnterOutline, IoTrashBinOutline } from "react-icons/io5";
|
||||
import { env } from "~/env.mjs";
|
||||
import { api } from "~/utils/api";
|
||||
import { useUser } from "@clerk/nextjs";
|
||||
|
||||
const RoomList = () => {
|
||||
const { data: sessionData } = useSession();
|
||||
const { isSignedIn, user } = useUser();
|
||||
|
||||
configureAbly({
|
||||
key: env.NEXT_PUBLIC_ABLY_PUBLIC_KEY,
|
||||
clientId: sessionData?.user.id,
|
||||
clientId: user?.id,
|
||||
recover: (_, cb) => {
|
||||
cb(true);
|
||||
},
|
||||
});
|
||||
|
||||
const [] = useChannel(
|
||||
`${env.NEXT_PUBLIC_APP_ENV}-${sessionData ? sessionData.user.id : ""}`,
|
||||
`${env.NEXT_PUBLIC_APP_ENV}-${user?.id}`,
|
||||
() => void refetchRoomsFromDb()
|
||||
);
|
||||
|
||||
|
@ -27,7 +26,7 @@ const RoomList = () => {
|
|||
|
||||
const { data: roomsFromDb, refetch: refetchRoomsFromDb } =
|
||||
api.room.getAll.useQuery(undefined, {
|
||||
enabled: sessionData?.user !== undefined,
|
||||
enabled: isSignedIn,
|
||||
});
|
||||
|
||||
const createRoom = api.room.create.useMutation({});
|
||||
|
@ -43,7 +42,7 @@ const RoomList = () => {
|
|||
const deleteRoom = api.room.delete.useMutation({});
|
||||
|
||||
const deleteRoomHandler = (roomId: string) => {
|
||||
if (sessionData) {
|
||||
if (isSignedIn) {
|
||||
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_SECONDS: z.string(),
|
||||
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_SECRET: z.string(),
|
||||
GOOGLE_CLIENT_ID: z.string(),
|
||||
|
@ -31,6 +20,7 @@ const server = z.object({
|
|||
APP_ENV: z.string(),
|
||||
RESEND_API_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({
|
||||
NEXT_PUBLIC_ABLY_PUBLIC_KEY: 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_SECONDS: process.env.UPSTASH_RATELIMIT_SECONDS,
|
||||
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_SECRET: process.env.GITHUB_CLIENT_SECRET,
|
||||
GOOGLE_CLIENT_ID: process.env.GOOGLE_CLIENT_ID,
|
||||
|
@ -68,6 +57,9 @@ const processEnv = {
|
|||
NEXT_PUBLIC_APP_ENV: process.env.NEXT_PUBLIC_APP_ENV,
|
||||
RESEND_API_KEY: process.env.RESEND_API_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
|
||||
|
|
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 { ClerkProvider } from "@clerk/nextjs";
|
||||
|
||||
import { api } from "~/utils/api";
|
||||
|
||||
import { useRouter } from "next/router";
|
||||
import { useEffect, useState } from "react";
|
||||
import Footer from "~/components/Footer";
|
||||
import Navbar from "~/components/Navbar";
|
||||
import "~/styles/globals.css";
|
||||
|
||||
const MyApp: AppType<{ session: Session | null }> = ({
|
||||
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]);
|
||||
|
||||
const MyApp: AppType = ({ Component, pageProps }) => {
|
||||
return (
|
||||
<SessionProvider session={ session }>
|
||||
<ClerkProvider {...pageProps}>
|
||||
<div className="block h-[100%]">
|
||||
<Navbar title="Sprint Padawan" />
|
||||
<div className="flex flex-row items-center justify-center min-h-[calc(100%-114px)]">
|
||||
{ pageLoading ? (
|
||||
<span className="loading loading-dots loading-lg"></span>
|
||||
) : (
|
||||
<Component {...pageProps} />
|
||||
) }
|
||||
</div>
|
||||
<Footer />
|
||||
</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 { useSession } from "next-auth/react";
|
||||
import type { NextPage } from "next";
|
||||
import Head from "next/head";
|
||||
|
||||
import RoomList from "~/components/RoomList";
|
||||
|
@ -7,27 +6,8 @@ import RoomList from "~/components/RoomList";
|
|||
import Link from "next/link";
|
||||
import { useEffect, useState } from "react";
|
||||
import { FaShieldAlt } from "react-icons/fa";
|
||||
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 },
|
||||
};
|
||||
};
|
||||
import { useUser } from "@clerk/nextjs";
|
||||
|
||||
const Home: NextPage = () => {
|
||||
return (
|
||||
|
@ -46,23 +26,27 @@ const Home: NextPage = () => {
|
|||
export default Home;
|
||||
|
||||
const HomePageBody = () => {
|
||||
const { data: sessionData } = useSession();
|
||||
const { isLoaded, user } = useUser();
|
||||
const [joinRoomTextBox, setJoinRoomTextBox] = useState<string>("");
|
||||
const [tabIndex, setTabIndex] = useState<number>();
|
||||
|
||||
useEffect(() => {
|
||||
const tabIndexLocal = localStorage.getItem(`dashboardTabIndex`);
|
||||
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">
|
||||
Hi, {sessionData?.user.name}!{" "}
|
||||
{sessionData?.user.isAdmin && (
|
||||
Hi, {user?.fullName}!{" "}
|
||||
{(user?.publicMetadata.isAdmin as boolean | undefined) && (
|
||||
<FaShieldAlt className="inline-block text-primary" />
|
||||
)}
|
||||
{sessionData?.user.isVIP && (
|
||||
{(user?.publicMetadata.isVIP as boolean | undefined) && (
|
||||
<GiStarFormation className="inline-block text-secondary" />
|
||||
)}
|
||||
</h1>
|
||||
|
|
|
@ -1,6 +1,5 @@
|
|||
import { type NextPage } from "next";
|
||||
import Head from "next/head";
|
||||
import Stats from "~/components/Stats";
|
||||
|
||||
const Home: NextPage = () => {
|
||||
return (
|
||||
|
@ -55,13 +54,6 @@ const HomePageBody = () => {
|
|||
</ul>
|
||||
</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 Image from "next/image";
|
||||
import { useEffect, useState } from "react";
|
||||
import { EventTypes } from "~/utils/types";
|
||||
|
||||
import { useSession } from "next-auth/react";
|
||||
import { useRouter } from "next/router";
|
||||
import {
|
||||
IoCheckmarkCircleOutline,
|
||||
|
@ -17,10 +16,7 @@ import {
|
|||
IoSaveOutline,
|
||||
} from "react-icons/io5";
|
||||
import { GiStarFormation } from "react-icons/gi";
|
||||
import { z } from "zod";
|
||||
import { api } from "~/utils/api";
|
||||
import { getServerAuthSession } from "../../server/auth";
|
||||
|
||||
import { configureAbly, useChannel, usePresence } from "@ably-labs/react-hooks";
|
||||
import Link from "next/link";
|
||||
import { FaShieldAlt } from "react-icons/fa";
|
||||
|
@ -28,27 +24,10 @@ import { RiVipCrownFill } from "react-icons/ri";
|
|||
import { env } from "~/env.mjs";
|
||||
import { downloadCSV } from "~/utils/helpers";
|
||||
import type { PresenceItem } from "~/utils/types";
|
||||
|
||||
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 },
|
||||
};
|
||||
};
|
||||
import { useUser } from "@clerk/nextjs";
|
||||
|
||||
const Room: NextPage = () => {
|
||||
const { isSignedIn } = useUser();
|
||||
return (
|
||||
<>
|
||||
<Head>
|
||||
|
@ -57,7 +36,13 @@ const Room: NextPage = () => {
|
|||
<meta http-equiv="Cache-control" content="no-cache" />
|
||||
</Head>
|
||||
<div className="flex flex-col items-center justify-center text-center gap-2">
|
||||
{!isSignedIn ? (
|
||||
<div className="flex items-center justify-center">
|
||||
<span className="loading loading-dots loading-lg"></span>
|
||||
</div>
|
||||
) : (
|
||||
<RoomBody />
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
|
@ -66,9 +51,9 @@ const Room: NextPage = () => {
|
|||
export default Room;
|
||||
|
||||
const RoomBody = ({}) => {
|
||||
const { data: sessionData } = useSession();
|
||||
const { isSignedIn, user } = useUser();
|
||||
const { query } = useRouter();
|
||||
const roomId = z.string().parse(query.id);
|
||||
const roomId = query.id as string;
|
||||
|
||||
const [storyNameText, setStoryNameText] = useState<string>("");
|
||||
const [roomScale, setRoomScale] = useState<string>("");
|
||||
|
@ -85,7 +70,7 @@ const RoomBody = ({}) => {
|
|||
|
||||
configureAbly({
|
||||
key: env.NEXT_PUBLIC_ABLY_PUBLIC_KEY,
|
||||
clientId: sessionData?.user.id,
|
||||
clientId: user?.id,
|
||||
recover: (_, cb) => {
|
||||
cb(true);
|
||||
},
|
||||
|
@ -108,11 +93,11 @@ const RoomBody = ({}) => {
|
|||
const [presenceData] = usePresence<PresenceItem>(
|
||||
`${env.NEXT_PUBLIC_APP_ENV}-${roomId}`,
|
||||
{
|
||||
name: sessionData?.user.name || "",
|
||||
image: sessionData?.user.image || "",
|
||||
client_id: sessionData?.user.id || "",
|
||||
isAdmin: sessionData?.user.isAdmin || false,
|
||||
isVIP: sessionData?.user.isVIP || false,
|
||||
name: user?.fullName || "",
|
||||
image: user?.imageUrl || "",
|
||||
client_id: user?.id || "",
|
||||
isAdmin: (user?.publicMetadata.isAdmin as boolean | undefined) || false,
|
||||
isVIP: (user?.publicMetadata.isVIP as boolean | undefined) || false,
|
||||
}
|
||||
);
|
||||
|
||||
|
@ -129,19 +114,16 @@ const RoomBody = ({}) => {
|
|||
|
||||
// Init story name
|
||||
useEffect(() => {
|
||||
if (sessionData && roomFromDb) {
|
||||
if (isSignedIn && roomFromDb) {
|
||||
setStoryNameText(roomFromDb.storyName || "");
|
||||
setRoomScale(roomFromDb.scale || "ERROR");
|
||||
}
|
||||
}, [roomFromDb, roomId, sessionData]);
|
||||
}, [roomFromDb, roomId, isSignedIn, user]);
|
||||
|
||||
// Helper functions
|
||||
const getVoteForCurrentUser = () => {
|
||||
if (roomFromDb && sessionData) {
|
||||
return (
|
||||
votesFromDb &&
|
||||
votesFromDb.find((vote) => vote.userId === sessionData.user.id)
|
||||
);
|
||||
if (roomFromDb && isSignedIn) {
|
||||
return votesFromDb && votesFromDb.find((vote) => vote.userId === user.id);
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
|
@ -184,12 +166,12 @@ const RoomBody = ({}) => {
|
|||
.concat({
|
||||
id: "LATEST",
|
||||
createdAt: new Date(),
|
||||
userId: roomFromDb.owner.id,
|
||||
userId: roomFromDb.userId,
|
||||
roomId: roomFromDb.id,
|
||||
scale: roomScale,
|
||||
votes: votesFromDb.map((vote) => {
|
||||
return {
|
||||
name: vote.owner.name,
|
||||
name: vote.userId,
|
||||
value: vote.value,
|
||||
};
|
||||
}),
|
||||
|
@ -365,9 +347,7 @@ const RoomBody = ({}) => {
|
|||
</div>
|
||||
)}
|
||||
|
||||
{sessionData &&
|
||||
!!roomFromDb &&
|
||||
roomFromDb.userId === sessionData.user.id && (
|
||||
{isSignedIn && !!roomFromDb && roomFromDb.userId === user.id && (
|
||||
<>
|
||||
<div className="card card-compact bg-neutral shadow-xl mx-auto m-4">
|
||||
<div className="card-body flex flex-col flex-wrap">
|
||||
|
@ -452,8 +432,7 @@ const RoomBody = ({}) => {
|
|||
</div>
|
||||
|
||||
{votesFromDb &&
|
||||
(roomFromDb.logs.length > 0 ||
|
||||
votesFromDb.length > 0) && (
|
||||
(roomFromDb.logs.length > 0 || votesFromDb.length > 0) && (
|
||||
<div>
|
||||
<button
|
||||
onClick={() => downloadLogs()}
|
||||
|
|
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 { createTRPCRouter } from "~/server/api/trpc";
|
||||
import { sessionRouter } from "./routers/session";
|
||||
import { userRouter } from "./routers/user";
|
||||
import { voteRouter } from "./routers/vote";
|
||||
import { restRouter } from "./routers/rest";
|
||||
|
||||
|
@ -13,8 +11,6 @@ import { restRouter } from "./routers/rest";
|
|||
export const appRouter = createTRPCRouter({
|
||||
room: roomRouter,
|
||||
vote: voteRouter,
|
||||
user: userRouter,
|
||||
session: sessionRouter,
|
||||
rest: restRouter,
|
||||
});
|
||||
|
||||
|
|
|
@ -13,7 +13,7 @@ export const restRouter = createTRPCRouter({
|
|||
.query(async ({ ctx, input }) => {
|
||||
const isValidKey = await validateApiKey(input.key);
|
||||
if (isValidKey) {
|
||||
await ctx.prisma.verificationToken.findMany();
|
||||
await ctx.prisma.vote.findMany();
|
||||
return "Toasted the DB";
|
||||
} else {
|
||||
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
|
||||
.meta({ openapi: { method: "GET", path: "/rest/rooms/count" } })
|
||||
.input(z.void())
|
||||
|
|
|
@ -14,10 +14,9 @@ export const roomRouter = createTRPCRouter({
|
|||
})
|
||||
)
|
||||
.mutation(async ({ ctx, input }) => {
|
||||
if (ctx.session) {
|
||||
const room = await ctx.prisma.room.create({
|
||||
data: {
|
||||
userId: ctx.session.user.id,
|
||||
userId: ctx.auth.userId,
|
||||
roomName: input.name,
|
||||
storyName: "First Story!",
|
||||
scale: "0.5,1,2,3,5,8",
|
||||
|
@ -26,10 +25,11 @@ export const roomRouter = createTRPCRouter({
|
|||
});
|
||||
if (room) {
|
||||
await invalidateCache(`kv_roomcount`);
|
||||
await invalidateCache(`kv_roomlist_${ctx.session.user.id}`);
|
||||
console.log("PUBLISHED TO ", `kv_roomlist_${ctx.auth.userId}`);
|
||||
await invalidateCache(`kv_roomlist_${ctx.auth.userId}`);
|
||||
|
||||
await publishToChannel(
|
||||
`${ctx.session.user.id}`,
|
||||
`${ctx.auth.userId}`,
|
||||
EventTypes.ROOM_LIST_UPDATE,
|
||||
JSON.stringify(room)
|
||||
);
|
||||
|
@ -42,10 +42,6 @@ export const roomRouter = createTRPCRouter({
|
|||
}
|
||||
// happy path
|
||||
return !!room;
|
||||
}
|
||||
|
||||
// clinically depressed path
|
||||
return false;
|
||||
}),
|
||||
|
||||
// Get One
|
||||
|
@ -64,7 +60,6 @@ export const roomRouter = createTRPCRouter({
|
|||
storyName: true,
|
||||
visible: true,
|
||||
scale: true,
|
||||
owner: true,
|
||||
},
|
||||
});
|
||||
}),
|
||||
|
@ -77,14 +72,14 @@ export const roomRouter = createTRPCRouter({
|
|||
createdAt: Date;
|
||||
roomName: string;
|
||||
}[]
|
||||
>(`kv_roomlist_${ctx.session.user.id}`);
|
||||
>(`kv_roomlist_${ctx.auth.userId}`);
|
||||
|
||||
if (cachedResult) {
|
||||
return cachedResult;
|
||||
} else {
|
||||
const roomList = await ctx.prisma.room.findMany({
|
||||
where: {
|
||||
userId: ctx.session.user.id,
|
||||
userId: ctx.auth.userId,
|
||||
},
|
||||
select: {
|
||||
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;
|
||||
}
|
||||
|
@ -124,11 +119,7 @@ export const roomRouter = createTRPCRouter({
|
|||
scale: true,
|
||||
votes: {
|
||||
select: {
|
||||
owner: {
|
||||
select: {
|
||||
name: true,
|
||||
},
|
||||
},
|
||||
userId: true,
|
||||
value: true,
|
||||
},
|
||||
},
|
||||
|
@ -138,12 +129,12 @@ export const roomRouter = createTRPCRouter({
|
|||
oldRoom &&
|
||||
(await ctx.prisma.log.create({
|
||||
data: {
|
||||
userId: ctx.session.user.id,
|
||||
userId: ctx.auth.userId,
|
||||
roomId: input.roomId,
|
||||
scale: oldRoom.scale,
|
||||
votes: oldRoom.votes.map((vote) => {
|
||||
return {
|
||||
name: vote.owner.name,
|
||||
name: vote.userId,
|
||||
value: vote.value,
|
||||
};
|
||||
}),
|
||||
|
@ -168,7 +159,7 @@ export const roomRouter = createTRPCRouter({
|
|||
},
|
||||
data: {
|
||||
storyName: input.name,
|
||||
userId: ctx.session.user.id,
|
||||
userId: ctx.auth.userId,
|
||||
visible: input.visible,
|
||||
scale: [...new Set(input.scale.split(","))]
|
||||
.filter((item) => item !== "")
|
||||
|
@ -182,11 +173,6 @@ export const roomRouter = createTRPCRouter({
|
|||
scale: true,
|
||||
votes: {
|
||||
select: {
|
||||
owner: {
|
||||
select: {
|
||||
name: true,
|
||||
},
|
||||
},
|
||||
value: true,
|
||||
},
|
||||
},
|
||||
|
@ -217,10 +203,10 @@ export const roomRouter = createTRPCRouter({
|
|||
if (deletedRoom) {
|
||||
await invalidateCache(`kv_roomcount`);
|
||||
await invalidateCache(`kv_votecount`);
|
||||
await invalidateCache(`kv_roomlist_${ctx.session.user.id}`);
|
||||
await invalidateCache(`kv_roomlist_${ctx.auth.userId}`);
|
||||
|
||||
await publishToChannel(
|
||||
`${ctx.session.user.id}`,
|
||||
`${ctx.auth.userId}`,
|
||||
EventTypes.ROOM_LIST_UPDATE,
|
||||
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;
|
||||
createdAt: Date;
|
||||
userId: string;
|
||||
owner: {
|
||||
name: string | null;
|
||||
};
|
||||
roomId: string;
|
||||
}[]
|
||||
>(`kv_votes_${input.roomId}`);
|
||||
|
@ -34,11 +31,6 @@ export const voteRouter = createTRPCRouter({
|
|||
select: {
|
||||
id: true,
|
||||
createdAt: true,
|
||||
owner: {
|
||||
select: {
|
||||
name: true,
|
||||
},
|
||||
},
|
||||
room: true,
|
||||
roomId: true,
|
||||
userId: true,
|
||||
|
@ -58,17 +50,17 @@ export const voteRouter = createTRPCRouter({
|
|||
where: {
|
||||
userId_roomId: {
|
||||
roomId: input.roomId,
|
||||
userId: ctx.session.user.id,
|
||||
userId: ctx.auth.userId,
|
||||
},
|
||||
},
|
||||
create: {
|
||||
value: input.value,
|
||||
userId: ctx.session.user.id,
|
||||
userId: ctx.auth.userId,
|
||||
roomId: input.roomId,
|
||||
},
|
||||
update: {
|
||||
value: input.value,
|
||||
userId: ctx.session.user.id,
|
||||
userId: ctx.auth.userId,
|
||||
roomId: input.roomId,
|
||||
},
|
||||
select: {
|
||||
|
@ -76,11 +68,6 @@ export const voteRouter = createTRPCRouter({
|
|||
userId: true,
|
||||
roomId: true,
|
||||
id: true,
|
||||
owner: {
|
||||
select: {
|
||||
name: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
|
|
|
@ -1,81 +1,71 @@
|
|||
/**
|
||||
* YOU PROBABLY DON'T NEED TO EDIT THIS FILE, UNLESS:
|
||||
* 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).
|
||||
* 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)
|
||||
*
|
||||
* TL;DR - This is where all the tRPC server stuff is created and plugged in. The pieces you will
|
||||
* need to use are documented accordingly near the end.
|
||||
* tl;dr - this is where all the tRPC server stuff is created and plugged in.
|
||||
* The pieces you will need to use are documented accordingly near the end
|
||||
*/
|
||||
|
||||
/**
|
||||
* 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 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 "~/server/db";
|
||||
|
||||
type CreateContextOptions = {
|
||||
session: Session | null;
|
||||
ip: string | undefined;
|
||||
};
|
||||
import { prisma } from "../db";
|
||||
|
||||
interface AuthContext {
|
||||
auth: SignedInAuthObject | SignedOutAuthObject;
|
||||
}
|
||||
/**
|
||||
* This helper generates the "internals" for a tRPC context. If you need to use it, you can export
|
||||
* it from here.
|
||||
* This helper generates the "internals" for a tRPC context. If you need to use
|
||||
* it, you can export it from here
|
||||
*
|
||||
* Examples of things you may need it for:
|
||||
* - testing, so we don't have to mock Next.js' req/res
|
||||
* - tRPC's `createSSGHelpers`, where we don't have req/res
|
||||
*
|
||||
* - testing, so we dont have to mock Next.js' req/res
|
||||
* - trpc's `createSSGHelpers` where we don't have req/res
|
||||
* @see https://create.t3.gg/en/usage/trpc#-servertrpccontextts
|
||||
*/
|
||||
const createInnerTRPCContext = (opts: CreateContextOptions) => {
|
||||
const createInnerTRPCContext = ({ auth }: AuthContext) => {
|
||||
return {
|
||||
session: opts.session,
|
||||
ip: opts.ip,
|
||||
auth,
|
||||
prisma,
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* This is the actual context you will use in your router. It will be used to process every request
|
||||
* that goes through your tRPC endpoint.
|
||||
*
|
||||
* @see https://trpc.io/docs/context
|
||||
* This is the actual context you'll use in your router. It will be used to
|
||||
* process every request that goes through your tRPC endpoint
|
||||
* @link https://trpc.io/docs/context
|
||||
*/
|
||||
export const createTRPCContext = async (opts: CreateNextContextOptions) => {
|
||||
const { req, res } = opts;
|
||||
|
||||
// Get the session from the server using the getServerSession wrapper function
|
||||
const session = await getServerAuthSession({ req, res });
|
||||
|
||||
return createInnerTRPCContext({
|
||||
ip: req.socket.remoteAddress,
|
||||
session,
|
||||
});
|
||||
export const createTRPCContext = (opts: CreateNextContextOptions) => {
|
||||
return createInnerTRPCContext({ auth: getAuth(opts.req) });
|
||||
};
|
||||
|
||||
/**
|
||||
* 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 type { OpenApiMeta } from "trpc-openapi";
|
||||
import { Ratelimit } from "@upstash/ratelimit";
|
||||
import superjson from "superjson";
|
||||
import { env } from "~/env.mjs";
|
||||
import { Redis } from "@upstash/redis";
|
||||
import { OpenApiMeta } from "trpc-openapi";
|
||||
|
||||
const t = initTRPC
|
||||
.meta<OpenApiMeta>()
|
||||
.context<typeof createTRPCContext>()
|
||||
.meta<OpenApiMeta>()
|
||||
.create({
|
||||
transformer: superjson,
|
||||
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)
|
||||
*
|
||||
* These are the pieces you use to build your tRPC API. You should import these a lot in the
|
||||
* "/src/server/api/routers" directory.
|
||||
* These are the pieces you use to build your tRPC API. You should import these
|
||||
* 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
|
||||
*/
|
||||
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
|
||||
* guarantee that a user querying is authorized, but you can still access user session data if they
|
||||
* are logged in.
|
||||
* This is the base piece you use to build new queries and mutations on your
|
||||
* tRPC API. It does not guarantee that a user querying is authorized, but you
|
||||
* can still access user session data if they are logged in
|
||||
*/
|
||||
export const publicProcedure = t.procedure;
|
||||
|
||||
/** 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);
|
||||
export const protectedProcedure = t.procedure.use(isAuthed);
|
||||
|
|
|
@ -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) => {
|
||||
try {
|
||||
console.log("KEY: ", key);
|
||||
await redis.set(`${env.APP_ENV}_${key}`, value, {
|
||||
ex: Number(env.UPSTASH_REDIS_EXPIRY_SECONDS),
|
||||
});
|
||||
|
|
Loading…
Add table
Reference in a new issue