diff --git a/.env.example b/.env.example index caecbe6..982b485 100644 --- a/.env.example +++ b/.env.example @@ -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="" diff --git a/next.config.mjs b/next.config.mjs index ea8dac8..87738f0 100644 --- a/next.config.mjs +++ b/next.config.mjs @@ -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", + ], }, }; diff --git a/package.json b/package.json index 0c2747b..2308e8b 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index c7bcf0a..e28a527 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -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 diff --git a/prisma/schema.prisma b/prisma/schema.prisma index edcd219..b482d4f 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -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]) diff --git a/src/components/Navbar.tsx b/src/components/Navbar.tsx index eabc8e3..8524ddb 100644 --- a/src/components/Navbar.tsx +++ b/src/components/Navbar.tsx @@ -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 ( Dashboard ); - } else if (sessionStatus === "unauthenticated") { + } else if (!isSignedIn) { return ( - ); @@ -51,7 +54,7 @@ const Navbar = ({ title }: NavbarProps) => { - {sessionStatus === "loading" ? ( + {!isLoaded ? (
@@ -59,53 +62,7 @@ const Navbar = ({ title }: NavbarProps) => { navigationMenu() )} - {sessionData?.user.image && ( -
-
- - -
-
- )} + ); }; diff --git a/src/components/RoomList.tsx b/src/components/RoomList.tsx index bde86da..f59bf7a 100644 --- a/src/components/RoomList.tsx +++ b/src/components/RoomList.tsx @@ -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 }); } }; diff --git a/src/components/Stats.tsx b/src/components/Stats.tsx deleted file mode 100644 index 7dccda4..0000000 --- a/src/components/Stats.tsx +++ /dev/null @@ -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 ( -
-
-
Users
-
- {usersCountLoading || usersCountFetching ? ( - - ) : ( - <>{usersCount ? usersCount : "0"} - )} -
-
- -
-
Rooms
-
- {roomsCountLoading || roomsCountFetching ? ( - - ) : ( - <>{roomsCount ? roomsCount : "0"} - )} -
-
- -
-
Votes
-
- {votesCountLoading || votesCountFetching ? ( - - ) : ( - <>{votesCount ? votesCount : "0"} - )} -
-
-
- ); -}; - -export default Stats; diff --git a/src/env.mjs b/src/env.mjs index 6212cc3..57cf2d4 100644 --- a/src/env.mjs +++ b/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 diff --git a/src/middleware.ts b/src/middleware.ts new file mode 100644 index 0000000..807e65c --- /dev/null +++ b/src/middleware.ts @@ -0,0 +1,9 @@ +import { authMiddleware } from "@clerk/nextjs"; + +export default authMiddleware({ + publicRoutes: ["/", "/api/(.*)"], +}); + +export const config = { + matcher: ["/((?!.*\\..*|_next).*)", "/", "/(api|trpc)(.*)"], +}; diff --git a/src/pages/_app.tsx b/src/pages/_app.tsx index 0518f20..d8c87a0 100644 --- a/src/pages/_app.tsx +++ b/src/pages/_app.tsx @@ -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(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 ( - +
- { pageLoading ? ( - - ) : ( - - ) } +
-
+ ); }; diff --git a/src/pages/admin/index.tsx b/src/pages/admin/index.tsx deleted file mode 100644 index 9c71901..0000000 --- a/src/pages/admin/index.tsx +++ /dev/null @@ -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 ( - <> - - Sprint Padawan - Admin - - -
-
- -
-
- - ); -}; - -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 ( - <> -

Admin Panel

- - - - {usersFetching ? ( - - ) : ( -
- - - -
- )} - -
-
-

Users:

- - {usersLoading || usersFetching ? ( - - ) : ( -
- - {/* head */} - - - - - - - - - - - - {users - ?.sort((user1, user2) => - user2.createdAt > user1.createdAt ? 1 : -1 - ) - .map((user) => { - return ( - - - - - - - - - - ); - })} - -
IDNameCreated At# SessionsProvidersActions
- {user.id} - - {user.name} - - {user.createdAt.toLocaleDateString()} - - {user.sessions.length} - - {getProviders(user).includes("google") && ( - - )} - {getProviders(user).includes("github") && ( - - )} - - - - - -
-
- )} -
-
- - ); -}; diff --git a/src/pages/api/auth/[...nextauth].ts b/src/pages/api/auth/[...nextauth].ts deleted file mode 100644 index 8aefbb6..0000000 --- a/src/pages/api/auth/[...nextauth].ts +++ /dev/null @@ -1,4 +0,0 @@ -import NextAuth from "next-auth"; -import { authOptions } from "~/server/auth"; - -export default NextAuth(authOptions); diff --git a/src/pages/dashboard/index.tsx b/src/pages/dashboard/index.tsx index 06c34dd..2056b14 100644 --- a/src/pages/dashboard/index.tsx +++ b/src/pages/dashboard/index.tsx @@ -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(""); const [tabIndex, setTabIndex] = useState(); useEffect(() => { const tabIndexLocal = localStorage.getItem(`dashboardTabIndex`); setTabIndex(tabIndexLocal !== null ? Number(tabIndexLocal) : 0); - }, [tabIndex, sessionData]); + }, [tabIndex, user]); - return ( + return !isLoaded ? ( +
+ +
+ ) : ( <>

- Hi, {sessionData?.user.name}!{" "} - {sessionData?.user.isAdmin && ( + Hi, {user?.fullName}!{" "} + {(user?.publicMetadata.isAdmin as boolean | undefined) && ( )} - {sessionData?.user.isVIP && ( + {(user?.publicMetadata.isVIP as boolean | undefined) && ( )}

diff --git a/src/pages/index.tsx b/src/pages/index.tsx index 5c74764..485f66b 100644 --- a/src/pages/index.tsx +++ b/src/pages/index.tsx @@ -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 = () => { - -
-
-

Stats:

- -
-
); }; diff --git a/src/pages/profile/index.tsx b/src/pages/profile/index.tsx deleted file mode 100644 index bb864d6..0000000 --- a/src/pages/profile/index.tsx +++ /dev/null @@ -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 ( - <> - - Sprint Padawan - Profile - - -
-
- -
-
- - ); -}; - -export default Profile; - -const ProfileBody = () => { - const { data: sessionData } = useSession(); - const [nameText, setNameText] = useState(""); - 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 ( - <> - -
-
- - -

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

- -
- -
-
-
- -
-
-

Profile:

- {sessionData.user.image && ( - Profile picture. - )} - -
- {sessionData.user.isAdmin && ( -
- -
- )} - {sessionData.user.isVIP && ( -
- -
- )} -
- - {providersLoading ? ( -
- {" "} -
- ) : ( -
- - - -
- )} - - {sessionData.user.name && ( - setNameText(event.target.value)} - /> - )} - - {sessionData.user.email && ( - - )} - - - - {/* */} - - -
-
- - ); - } else { - return

Error getting login session!

; - } -}; diff --git a/src/pages/room/[id].tsx b/src/pages/room/[id].tsx index e161c4b..cce31ce 100644 --- a/src/pages/room/[id].tsx +++ b/src/pages/room/[id].tsx @@ -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 ( <> @@ -57,7 +36,13 @@ const Room: NextPage = () => {
- + {!isSignedIn ? ( +
+ +
+ ) : ( + + )}
); @@ -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(""); const [roomScale, setRoomScale] = useState(""); @@ -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( `${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,111 +347,108 @@ const RoomBody = ({}) => { )} - {sessionData && - !!roomFromDb && - roomFromDb.userId === sessionData.user.id && ( - <> -
-
-

Room Settings

+ {isSignedIn && !!roomFromDb && roomFromDb.userId === user.id && ( + <> +
+
+

Room Settings

- + - { - setRoomScale(event.target.value); - }} - /> + { + setRoomScale(event.target.value); + }} + /> - + - { - setStoryNameText(event.target.value); - }} - /> + { + setStoryNameText(event.target.value); + }} + /> -
-
- -
- -
- -
- - {votesFromDb && - (roomFromDb.logs.length > 0 || - votesFromDb.length > 0) && ( -
- -
+
+
+
+ +
+ +
+ + {votesFromDb && + (roomFromDb.logs.length > 0 || votesFromDb.length > 0) && ( +
+ +
+ )}
- - )} +
+ + )} ); // Room does not exist diff --git a/src/pages/sign-in/[[...index]].tsx b/src/pages/sign-in/[[...index]].tsx new file mode 100644 index 0000000..f84cb7f --- /dev/null +++ b/src/pages/sign-in/[[...index]].tsx @@ -0,0 +1,17 @@ +import { SignIn } from "@clerk/nextjs"; + +const SignInPage = () => ( +
+ +
+); + +export default SignInPage; + +const styles = { + width: "100vw", + height: "100vh", + display: "flex", + justifyContent: "center", + alignItems: "center", +}; diff --git a/src/pages/sign-up/[[...index]].tsx b/src/pages/sign-up/[[...index]].tsx new file mode 100644 index 0000000..ce6c5ea --- /dev/null +++ b/src/pages/sign-up/[[...index]].tsx @@ -0,0 +1,17 @@ +import { SignUp } from "@clerk/nextjs"; + +const SignUpPage = () => ( +
+ +
+); + +export default SignUpPage; + +const styles = { + width: "100vw", + height: "100vh", + display: "flex", + justifyContent: "center", + alignItems: "center", +}; diff --git a/src/server/api/root.ts b/src/server/api/root.ts index 7d5426a..4acf4e6 100644 --- a/src/server/api/root.ts +++ b/src/server/api/root.ts @@ -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, }); diff --git a/src/server/api/routers/rest.ts b/src/server/api/routers/rest.ts index 3d6ff86..0ca0edf 100644 --- a/src/server/api/routers/rest.ts +++ b/src/server/api/routers/rest.ts @@ -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(`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()) diff --git a/src/server/api/routers/room.ts b/src/server/api/routers/room.ts index 598b818..44d3189 100644 --- a/src/server/api/routers/room.ts +++ b/src/server/api/routers/room.ts @@ -14,38 +14,34 @@ export const roomRouter = createTRPCRouter({ }) ) .mutation(async ({ ctx, input }) => { - if (ctx.session) { - const room = await ctx.prisma.room.create({ - data: { - userId: ctx.session.user.id, - roomName: input.name, - storyName: "First Story!", - scale: "0.5,1,2,3,5,8", - visible: false, - }, - }); - if (room) { - await invalidateCache(`kv_roomcount`); - await invalidateCache(`kv_roomlist_${ctx.session.user.id}`); + const room = await ctx.prisma.room.create({ + data: { + userId: ctx.auth.userId, + roomName: input.name, + storyName: "First Story!", + scale: "0.5,1,2,3,5,8", + visible: false, + }, + }); + if (room) { + await invalidateCache(`kv_roomcount`); + console.log("PUBLISHED TO ", `kv_roomlist_${ctx.auth.userId}`); + await invalidateCache(`kv_roomlist_${ctx.auth.userId}`); - await publishToChannel( - `${ctx.session.user.id}`, - EventTypes.ROOM_LIST_UPDATE, - JSON.stringify(room) - ); + await publishToChannel( + `${ctx.auth.userId}`, + EventTypes.ROOM_LIST_UPDATE, + JSON.stringify(room) + ); - await publishToChannel( - `stats`, - EventTypes.STATS_UPDATE, - JSON.stringify(room) - ); - } - // happy path - return !!room; + await publishToChannel( + `stats`, + EventTypes.STATS_UPDATE, + JSON.stringify(room) + ); } - - // clinically depressed path - return false; + // happy path + return !!room; }), // 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) ); diff --git a/src/server/api/routers/session.ts b/src/server/api/routers/session.ts deleted file mode 100644 index 7db9918..0000000 --- a/src/server/api/routers/session.ts +++ /dev/null @@ -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; - }), -}); diff --git a/src/server/api/routers/user.ts b/src/server/api/routers/user.ts deleted file mode 100644 index 923d9eb..0000000 --- a/src/server/api/routers/user.ts +++ /dev/null @@ -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 ", - 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; - }), -}); diff --git a/src/server/api/routers/vote.ts b/src/server/api/routers/vote.ts index c3e524b..be20fe3 100644 --- a/src/server/api/routers/vote.ts +++ b/src/server/api/routers/vote.ts @@ -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, - }, - }, }, }); diff --git a/src/server/api/trpc.ts b/src/server/api/trpc.ts index 2bf95d0..8a49ad7 100644 --- a/src/server/api/trpc.ts +++ b/src/server/api/trpc.ts @@ -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() .context() + .meta() .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); diff --git a/src/server/auth.ts b/src/server/auth.ts deleted file mode 100644 index fc9cf5d..0000000 --- a/src/server/auth.ts +++ /dev/null @@ -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); -}; diff --git a/src/server/redis.ts b/src/server/redis.ts index f09bb20..c7bbe4e 100644 --- a/src/server/redis.ts +++ b/src/server/redis.ts @@ -8,6 +8,7 @@ export const redis = Redis.fromEnv({ export const setCache = async (key: string, value: T) => { try { + console.log("KEY: ", key); await redis.set(`${env.APP_ENV}_${key}`, value, { ex: Number(env.UPSTASH_REDIS_EXPIRY_SECONDS), });