Overhaul!

This commit is contained in:
Atridad Lahiji 2023-08-12 17:12:42 -06:00 committed by atridadl
parent c907729b44
commit 039fd81c4e
No known key found for this signature in database
28 changed files with 658 additions and 1462 deletions

View file

@ -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=""

View file

@ -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",
],
},
};

View file

@ -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
View file

@ -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

View file

@ -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])

View file

@ -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>
);
};

View file

@ -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 });
}
};

View file

@ -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;

View file

@ -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
View file

@ -0,0 +1,9 @@
import { authMiddleware } from "@clerk/nextjs";
export default authMiddleware({
publicRoutes: ["/", "/api/(.*)"],
});
export const config = {
matcher: ["/((?!.*\\..*|_next).*)", "/", "/(api|trpc)(.*)"],
};

View file

@ -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>
);
};

View file

@ -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>
</>
);
};

View file

@ -1,4 +0,0 @@
import NextAuth from "next-auth";
import { authOptions } from "~/server/auth";
export default NextAuth(authOptions);

View file

@ -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>

View file

@ -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>
</>
);
};

View file

@ -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>;
}
};

View file

@ -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()}

View 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",
};

View 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",
};

View file

@ -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,
});

View file

@ -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())

View file

@ -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)
);

View file

@ -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;
}),
});

View file

@ -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;
}),
});

View file

@ -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,
},
},
},
});

View file

@ -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);

View file

@ -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);
};

View file

@ -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),
});