From 93225707a893010af7284a24dd75fb87b362ec4d Mon Sep 17 00:00:00 2001 From: Shouvik Ghosh Date: Sun, 1 Sep 2024 21:35:16 +0530 Subject: [PATCH 01/43] Basic login --- src/components/layout/footer.tsx | 14 ++++++++++++-- src/server/auth/prismaAdapter.ts | 2 +- 2 files changed, 13 insertions(+), 3 deletions(-) diff --git a/src/components/layout/footer.tsx b/src/components/layout/footer.tsx index 70f6856b..d48a1a59 100644 --- a/src/components/layout/footer.tsx +++ b/src/components/layout/footer.tsx @@ -1,9 +1,12 @@ -import { Box, Group, Stack, createStyles } from "@mantine/core"; +import { Box, Button, Group, Stack, createStyles } from "@mantine/core"; import Link from "next/link"; import { cmlDiscordInviteUrl } from "~/consts/cmlDiscordInviteUrl"; import { COMING_SOON_PATHNAME } from "~/consts/pathnames"; import { blackBackgroundColor } from "~/styles/layoutColors"; import { difficultyColors } from "~/styles/difficultyColors"; +import { getSession, signIn, signOut } from "next-auth/react"; +import { useEffect, useState } from "react"; +import type { Session } from "next-auth"; @@ -35,6 +38,11 @@ const useStyles = createStyles( export const Footer = () => { const { classes } = useStyles(); + const [session, setSession] = useState(null); + + useEffect(() => { + void getSession().then(session => setSession(session)); + }, []); return ( @@ -50,7 +58,9 @@ export const Footer = () => { align="start" spacing="1px" > - My Account + { !session && } + { session && {session.user.name} } + { session && } Settings Awaitable; const getCreateUser = (prisma: PrismaClient): CreateUser => { return (user: PrismaUser): Awaitable => { - const { id, ...rest } = user; //remove the discord id from the user object - a replacement is auto-generated by Prisma + const { id, email, emailVerified, ...rest } = user; //remove the discord id from the user object - a replacement is auto-generated by Prisma const trimmedUser = rest as TrimmedUser; From 0cf48a56a8408e73e6b2ed09996ebf10855f3317 Mon Sep 17 00:00:00 2001 From: Shouvik Ghosh Date: Mon, 2 Sep 2024 22:59:12 +0530 Subject: [PATCH 02/43] Add user claims --- .../migration.sql | 25 ++++ prisma/schema.prisma | 14 ++ src/components/layout/footer.tsx | 11 +- src/consts/pathnames.ts | 3 +- src/pages/claim-user.tsx | 122 ++++++++++++++++++ src/pages/index.tsx | 8 ++ src/server/api/routers/user.ts | 53 ++++++++ 7 files changed, 226 insertions(+), 10 deletions(-) create mode 100644 prisma/migrations/20240902061032_add_user_claims/migration.sql create mode 100644 src/pages/claim-user.tsx diff --git a/prisma/migrations/20240902061032_add_user_claims/migration.sql b/prisma/migrations/20240902061032_add_user_claims/migration.sql new file mode 100644 index 00000000..8add972f --- /dev/null +++ b/prisma/migrations/20240902061032_add_user_claims/migration.sql @@ -0,0 +1,25 @@ +/* + Warnings: + + - A unique constraint covering the columns `[sessionToken]` on the table `session` will be added. If there are existing duplicate values, this will fail. + +*/ +-- CreateTable +CREATE TABLE `user-claim` ( + `claimByUserId` VARCHAR(191) NOT NULL, + `claimForUserId` VARCHAR(191) NOT NULL, + `id` MEDIUMINT UNSIGNED NOT NULL AUTO_INCREMENT, + + INDEX `user-claim_claimByUserId_idx`(`claimByUserId`), + UNIQUE INDEX `user-claim_claimByUserId_claimForUserId_key`(`claimByUserId`, `claimForUserId`), + PRIMARY KEY (`id`) +) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; + +-- CreateIndex +CREATE UNIQUE INDEX `session_sessionToken_key` ON `session`(`sessionToken`); + +-- AddForeignKey +ALTER TABLE `user-claim` ADD CONSTRAINT `user-claim_claimByUserId_fkey` FOREIGN KEY (`claimByUserId`) REFERENCES `user`(`id`) ON DELETE CASCADE ON UPDATE RESTRICT; + +-- AddForeignKey +ALTER TABLE `user-claim` ADD CONSTRAINT `user-claim_claimForUserId_fkey` FOREIGN KEY (`claimForUserId`) REFERENCES `user`(`id`) ON DELETE CASCADE ON UPDATE RESTRICT; diff --git a/prisma/schema.prisma b/prisma/schema.prisma index e16b8831..a6904e42 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -580,6 +580,18 @@ model UsersToCompletedMaps { @@map("users-to-completed-maps") } +model UserClaim { + User_UserClaim_claimBy User @relation("UserClaim_claimBy", fields: [claimByUserId], references: [id], onDelete: Cascade, onUpdate: Restrict) + claimByUserId String + User_UserClaim_claimFor User @relation("UserClaim_claimFor", fields: [claimForUserId], references: [id], onDelete: Cascade, onUpdate: Restrict) + claimForUserId String + id Int @id @default(autoincrement()) @db.UnsignedMediumInt + + @@unique([claimByUserId, claimForUserId]) + @@index([claimByUserId]) + @@map("user-claim") +} + // Next auth models model User { id String @id @default(cuid()) @@ -616,6 +628,8 @@ model User { Map_NewWithMod_New_submittedByToUser Map_NewWithMod_New[] @relation("Map_NewWithMod_New_SubmittedByToUser") Map_NewSolo_mapperUserIdToUser Map_NewSolo[] @relation("Map_NewSolo_MapperUserIdToUser") Map_NewSolo_submittedByToUser Map_NewSolo[] @relation("Map_NewSolo_SubmittedByToUser") + UserClaim_UserClaim_claimBy UserClaim[] @relation("UserClaim_claimBy") + UserClaim_UserClaim_claimFor UserClaim[] @relation("UserClaim_claimFor") @@unique([discordUsername, discordDiscriminator]) @@map("user") diff --git a/src/components/layout/footer.tsx b/src/components/layout/footer.tsx index d48a1a59..762bdf41 100644 --- a/src/components/layout/footer.tsx +++ b/src/components/layout/footer.tsx @@ -4,9 +4,7 @@ import { cmlDiscordInviteUrl } from "~/consts/cmlDiscordInviteUrl"; import { COMING_SOON_PATHNAME } from "~/consts/pathnames"; import { blackBackgroundColor } from "~/styles/layoutColors"; import { difficultyColors } from "~/styles/difficultyColors"; -import { getSession, signIn, signOut } from "next-auth/react"; -import { useEffect, useState } from "react"; -import type { Session } from "next-auth"; +import { signIn, signOut, useSession } from "next-auth/react"; @@ -38,12 +36,7 @@ const useStyles = createStyles( export const Footer = () => { const { classes } = useStyles(); - const [session, setSession] = useState(null); - - useEffect(() => { - void getSession().then(session => setSession(session)); - }, []); - + const { data: session } = useSession(); return ( diff --git a/src/consts/pathnames.ts b/src/consts/pathnames.ts index 1eb76751..6223d0d4 100644 --- a/src/consts/pathnames.ts +++ b/src/consts/pathnames.ts @@ -1,3 +1,4 @@ export const MODS_PAGE_PATHNAME = "/mods"; export const FAQ_PAGE_PATHNAME = "/faq"; -export const COMING_SOON_PATHNAME = "/coming-soon"; \ No newline at end of file +export const COMING_SOON_PATHNAME = "/coming-soon"; +export const CLAIM_USER = "/claim-user"; \ No newline at end of file diff --git a/src/pages/claim-user.tsx b/src/pages/claim-user.tsx new file mode 100644 index 00000000..1ab65385 --- /dev/null +++ b/src/pages/claim-user.tsx @@ -0,0 +1,122 @@ +import { Button, createStyles, ScrollArea, Table } from "@mantine/core"; +import { type NextPage } from "next"; +import { useSession } from "next-auth/react"; +import Link from "next/link"; +import { Layout } from "~/components/layout/layout"; +import { cmlDiscordInviteUrl } from "~/consts/cmlDiscordInviteUrl"; +import { CLAIM_USER } from "~/consts/pathnames"; +import { pageContentHeightPixels } from "~/styles/pageContentHeightPixels"; +import { api } from "~/utils/api"; + +const useStyles = createStyles( + (theme) => ({ + scrollArea: { + height: `${pageContentHeightPixels}px`, + color: theme.white, + }, + discordLink: { + textDecoration: 'underline', + } + }), +); + +const ClaimUser : NextPage = () => { + const { status } = useSession(); + + const utils = api.useUtils(); + const unlinkedUsersQuery = api.user.getUnlinked.useQuery({}, { queryKey: ["user.getUnlinked", {}] }); + const unlinkedUsers = unlinkedUsersQuery.data ?? []; + const claimsQuery = api.user.getUserClaims.useQuery(undefined, { queryKey: ["user.getUserClaims", undefined] }); + const claims = claimsQuery.data ?? []; + const unclaimedUsers = unlinkedUsers.filter(user => !claims.find(claim => claim.claimForUserId === user.id)); + + const createUserClaimMutation = api.user.createUserClaim.useMutation({ + async onSuccess() { + await Promise.all([ + utils.user.getUnlinked.invalidate(), + utils.user.getUserClaims.invalidate() + ]); + } + }); + + const { classes } = useStyles(); + + if (status === 'unauthenticated') { + return ( + + Login to claim users. + + ); + } + + return ( + + +

Claim user

+

Claimed users

+

+ Contact us on Discord to get your claim verified. +

+ + + + + + + + + + + {claims.map(claim => ( + + + + + + + ))} + +
Claim IDUser IDUsernameDiscriminator
{claim.id}{claim.claimForUserId}{claim.User_UserClaim_claimFor.discordUsername}{claim.User_UserClaim_claimFor.discordDiscriminator}
+

Users available

+ + + + + + + + + + {unclaimedUsers.map(user => ( + + + + + + ))} + +
UsernameDiscriminator
{user.discordUsername}{user.discordDiscriminator} + +
+
+
+ ); +} + +export default ClaimUser; \ No newline at end of file diff --git a/src/pages/index.tsx b/src/pages/index.tsx index a4848c2f..aaaebe8f 100644 --- a/src/pages/index.tsx +++ b/src/pages/index.tsx @@ -1,5 +1,6 @@ import { createStyles, Flex, ScrollArea } from "@mantine/core"; import { type NextPage } from "next"; +import { useSession } from "next-auth/react"; import Image from "next/image"; import Link from "next/link"; import { Layout } from "~/components/layout/layout"; @@ -53,6 +54,7 @@ const Home: NextPage = () => { const { classes } = useStyles(); const height = 280; const width = height / 577 * 867; + const { status } = useSession(); return ( {

CML Public Beta

Welcome! The site is currently in early beta.

For now, mods can only be browsed.

+ { status === "authenticated" && ( +

+ If you submitted ratings via the google form, you can claim your old user + from here. +

+ ) }

Community Projects

Celeste Mountain Lego Idea

{ + return await ctx.prisma.user.findMany({ + where: { + accountStatus: 'Unlinked', + }, + select: { + id: true, + discordUsername: true, + discordDiscriminator: true, + }, + orderBy: getOrderObjectArray(input.selectors, input.directions), + }); + }), + + getUserClaims: loggedInProcedure + .query(async ({ ctx }) => { + return await ctx.prisma.userClaim.findMany({ + where: { + claimByUserId: ctx.user.id, + }, + select: { + id: true, + claimForUserId: true, + User_UserClaim_claimFor: { + select: { + discordUsername: true, + discordDiscriminator: true, + } + } + }, + orderBy: { + id: 'asc' + }, + }); + }), + + createUserClaim: loggedInProcedure + .input(z.object({ + forUserId: z.string() + })) + .mutation(async ({ ctx, input }) => { + const claim = await ctx.prisma.userClaim.create({ + data: { + claimByUserId: ctx.user.id, + claimForUserId: input.forUserId + }, + }); + + return claim; + }), + add: loggedInProcedure .input(userPostSchema) .mutation(async ({ ctx, input }) => { From c040eddc4124570884a73762e0d2cc6f089ce271 Mon Sep 17 00:00:00 2001 From: Shouvik Ghosh Date: Tue, 3 Sep 2024 22:02:31 +0530 Subject: [PATCH 03/43] Implement verification --- prisma/schema.prisma | 6 +- src/consts/pathnames.ts | 3 +- src/pages/claim-user.tsx | 23 +-- src/pages/verify-claim.tsx | 120 ++++++++++++++ src/server/api/routers/user.ts | 275 ++++++++++++++++++++++++++++++++- 5 files changed, 407 insertions(+), 20 deletions(-) create mode 100644 src/pages/verify-claim.tsx diff --git a/prisma/schema.prisma b/prisma/schema.prisma index a6904e42..7dc6a5d6 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -582,10 +582,10 @@ model UsersToCompletedMaps { model UserClaim { User_UserClaim_claimBy User @relation("UserClaim_claimBy", fields: [claimByUserId], references: [id], onDelete: Cascade, onUpdate: Restrict) - claimByUserId String + claimByUserId String User_UserClaim_claimFor User @relation("UserClaim_claimFor", fields: [claimForUserId], references: [id], onDelete: Cascade, onUpdate: Restrict) - claimForUserId String - id Int @id @default(autoincrement()) @db.UnsignedMediumInt + claimForUserId String + id Int @id @default(autoincrement()) @db.UnsignedMediumInt @@unique([claimByUserId, claimForUserId]) @@index([claimByUserId]) diff --git a/src/consts/pathnames.ts b/src/consts/pathnames.ts index 6223d0d4..740b94b5 100644 --- a/src/consts/pathnames.ts +++ b/src/consts/pathnames.ts @@ -1,4 +1,5 @@ export const MODS_PAGE_PATHNAME = "/mods"; export const FAQ_PAGE_PATHNAME = "/faq"; export const COMING_SOON_PATHNAME = "/coming-soon"; -export const CLAIM_USER = "/claim-user"; \ No newline at end of file +export const CLAIM_USER_PATHNAME = "/claim-user"; +export const VERIFY_CLAIM_PATHNAME = "/verify-claim"; \ No newline at end of file diff --git a/src/pages/claim-user.tsx b/src/pages/claim-user.tsx index 1ab65385..fa0e89fc 100644 --- a/src/pages/claim-user.tsx +++ b/src/pages/claim-user.tsx @@ -4,7 +4,7 @@ import { useSession } from "next-auth/react"; import Link from "next/link"; import { Layout } from "~/components/layout/layout"; import { cmlDiscordInviteUrl } from "~/consts/cmlDiscordInviteUrl"; -import { CLAIM_USER } from "~/consts/pathnames"; +import { CLAIM_USER_PATHNAME } from "~/consts/pathnames"; import { pageContentHeightPixels } from "~/styles/pageContentHeightPixels"; import { api } from "~/utils/api"; @@ -31,11 +31,8 @@ const ClaimUser : NextPage = () => { const unclaimedUsers = unlinkedUsers.filter(user => !claims.find(claim => claim.claimForUserId === user.id)); const createUserClaimMutation = api.user.createUserClaim.useMutation({ - async onSuccess() { - await Promise.all([ - utils.user.getUnlinked.invalidate(), - utils.user.getUserClaims.invalidate() - ]); + onSuccess() { + void utils.user.getUserClaims.invalidate(); } }); @@ -46,7 +43,7 @@ const ClaimUser : NextPage = () => { Login to claim users. @@ -57,7 +54,7 @@ const ClaimUser : NextPage = () => { { Claim ID User ID Username - Discriminator @@ -81,8 +77,7 @@ const ClaimUser : NextPage = () => { {claim.id} {claim.claimForUserId} - {claim.User_UserClaim_claimFor.discordUsername} - {claim.User_UserClaim_claimFor.discordDiscriminator} + {claim.User_UserClaim_claimFor.discordUsername}#{claim.User_UserClaim_claimFor.discordDiscriminator} ))} @@ -91,16 +86,14 @@ const ClaimUser : NextPage = () => { - - + {unclaimedUsers.map(user => ( - - + + {unclaimedUsers.map(unclaimedUser => ( + + + + + ))} + +
UsernameDiscriminatorUser
{user.discordUsername}{user.discordDiscriminator}{user.discordUsername}#{user.discordDiscriminator} + + )} + + +

User claims

+ + + + + + + + + + + {userClaims.map(claim => { + const byUser = `${claim.User_UserClaim_claimBy.discordUsername}#${claim.User_UserClaim_claimBy.discordDiscriminator}`; + const forUser = `${claim.User_UserClaim_claimFor.discordUsername}#${claim.User_UserClaim_claimFor.discordDiscriminator}`; + return ( + + + + + + + ) + })} + +
Claim IDByFor
{claim.id}{byUser}{forUser}
+
+ + ); +} + +export default VerifyClaim; \ No newline at end of file diff --git a/src/server/api/routers/user.ts b/src/server/api/routers/user.ts index 0aa3b701..bddcf7ea 100644 --- a/src/server/api/routers/user.ts +++ b/src/server/api/routers/user.ts @@ -203,7 +203,7 @@ export const userRouter = createTRPCRouter({ return users; }), - getUnlinked: publicProcedure + getUnlinked: loggedInProcedure .input(userOrderSchema) .query(async ({ ctx, input }) => { return await ctx.prisma.user.findMany({ @@ -240,6 +240,30 @@ export const userRouter = createTRPCRouter({ }, }); }), + + getAllUserClaims: adminProcedure + .query(async ({ ctx }) => { + return await ctx.prisma.userClaim.findMany({ + select: { + id: true, + User_UserClaim_claimBy: { + select: { + discordUsername: true, + discordDiscriminator: true, + } + }, + User_UserClaim_claimFor: { + select: { + discordUsername: true, + discordDiscriminator: true, + } + }, + }, + orderBy: { + id: 'asc', + } + }); + }), createUserClaim: loggedInProcedure .input(z.object({ @@ -255,6 +279,255 @@ export const userRouter = createTRPCRouter({ return claim; }), + + verifyUserClaim: adminProcedure + .input(z.object({ + id: z.number() + })) + .mutation(async ({ ctx, input }) => { + const claim = await ctx.prisma.userClaim.findFirst({ + where: { + id: input.id, + } + }); + if (!claim) { + throw new TRPCError({ + code: "NOT_FOUND", + message: `No user claim exists with id "${input.id}"`, + }); + } + + await ctx.prisma.publisher.updateMany({ + where: { + userId: { + equals: claim.claimForUserId, + } + }, + data: { + userId: claim.claimByUserId, + } + }); + await ctx.prisma.mod.updateMany({ + where: { + submittedBy: { + equals: claim.claimForUserId, + } + }, + data: { + submittedBy: claim.claimByUserId, + } + }); + await ctx.prisma.mod.updateMany({ + where: { + approvedBy: { + equals: claim.claimForUserId, + } + }, + data: { + approvedBy: claim.claimByUserId, + } + }); + await ctx.prisma.map.updateMany({ + where: { + mapperUserId: { + equals: claim.claimForUserId, + } + }, + data: { + mapperUserId: claim.claimByUserId, + } + }); + await ctx.prisma.map.updateMany({ + where: { + submittedBy: { + equals: claim.claimForUserId, + } + }, + data: { + submittedBy: claim.claimByUserId, + } + }); + await ctx.prisma.map.updateMany({ + where: { + approvedBy: { + equals: claim.claimForUserId, + } + }, + data: { + approvedBy: claim.claimByUserId, + } + }); + await ctx.prisma.mod_Archive.updateMany({ + where: { + submittedBy: { + equals: claim.claimForUserId, + } + }, + data: { + submittedBy: claim.claimByUserId, + } + }); + await ctx.prisma.mod_Archive.updateMany({ + where: { + approvedBy: { + equals: claim.claimForUserId, + } + }, + data: { + approvedBy: claim.claimByUserId, + } + }); + await ctx.prisma.map_Archive.updateMany({ + where: { + mapperUserId: { + equals: claim.claimForUserId, + } + }, + data: { + mapperUserId: claim.claimByUserId, + } + }); + await ctx.prisma.map_Archive.updateMany({ + where: { + submittedBy: { + equals: claim.claimForUserId, + } + }, + data: { + submittedBy: claim.claimByUserId, + } + }); + await ctx.prisma.map_Archive.updateMany({ + where: { + approvedBy: { + equals: claim.claimForUserId, + } + }, + data: { + approvedBy: claim.claimByUserId, + } + }); + await ctx.prisma.mod_Edit.updateMany({ + where: { + submittedBy: { + equals: claim.claimForUserId, + } + }, + data: { + submittedBy: claim.claimByUserId, + } + }); + await ctx.prisma.map_Edit.updateMany({ + where: { + mapperUserId: { + equals: claim.claimForUserId, + } + }, + data: { + mapperUserId: claim.claimByUserId, + } + }); + await ctx.prisma.map_Edit.updateMany({ + where: { + submittedBy: { + equals: claim.claimForUserId, + } + }, + data: { + submittedBy: claim.claimByUserId, + } + }); + await ctx.prisma.mod_New.updateMany({ + where: { + submittedBy: { + equals: claim.claimForUserId, + } + }, + data: { + submittedBy: claim.claimByUserId, + } + }); + await ctx.prisma.map_NewWithMod_New.updateMany({ + where: { + mapperUserId: { + equals: claim.claimForUserId, + } + }, + data: { + mapperUserId: claim.claimByUserId, + } + }); + await ctx.prisma.map_NewWithMod_New.updateMany({ + where: { + submittedBy: { + equals: claim.claimForUserId, + } + }, + data: { + submittedBy: claim.claimByUserId, + } + }); + await ctx.prisma.map_NewSolo.updateMany({ + where: { + mapperUserId: { + equals: claim.claimForUserId, + } + }, + data: { + mapperUserId: claim.claimByUserId, + } + }); + await ctx.prisma.map_NewSolo.updateMany({ + where: { + submittedBy: { + equals: claim.claimForUserId, + } + }, + data: { + submittedBy: claim.claimByUserId, + } + }); + await ctx.prisma.rating.updateMany({ + where: { + submittedBy: { + equals: claim.claimForUserId, + } + }, + data: { + submittedBy: claim.claimByUserId, + } + }); + await ctx.prisma.reviewCollection.updateMany({ + where: { + userId: { + equals: claim.claimForUserId, + } + }, + data: { + userId: claim.claimByUserId, + } + }); + await ctx.prisma.usersToCompletedMaps.updateMany({ + where: { + userId: { + equals: claim.claimForUserId, + } + }, + data: { + userId: claim.claimByUserId, + } + }); + await ctx.prisma.userClaim.delete({ + where: { + id: claim.id, + } + }); + await ctx.prisma.user.delete({ + where: { + id: claim.claimForUserId, + } + }); + }), add: loggedInProcedure .input(userPostSchema) From 3eea913ab12b6d9bebad91e4e749b7e0fc51f002 Mon Sep 17 00:00:00 2001 From: Shouvik Ghosh Date: Tue, 3 Sep 2024 22:14:59 +0530 Subject: [PATCH 04/43] Shift login to top right --- src/components/layout/footer.tsx | 7 +------ src/components/layout/header.tsx | 10 ++++++++-- 2 files changed, 9 insertions(+), 8 deletions(-) diff --git a/src/components/layout/footer.tsx b/src/components/layout/footer.tsx index 762bdf41..cf843187 100644 --- a/src/components/layout/footer.tsx +++ b/src/components/layout/footer.tsx @@ -1,10 +1,9 @@ -import { Box, Button, Group, Stack, createStyles } from "@mantine/core"; +import { Box, Group, Stack, createStyles } from "@mantine/core"; import Link from "next/link"; import { cmlDiscordInviteUrl } from "~/consts/cmlDiscordInviteUrl"; import { COMING_SOON_PATHNAME } from "~/consts/pathnames"; import { blackBackgroundColor } from "~/styles/layoutColors"; import { difficultyColors } from "~/styles/difficultyColors"; -import { signIn, signOut, useSession } from "next-auth/react"; @@ -36,7 +35,6 @@ const useStyles = createStyles( export const Footer = () => { const { classes } = useStyles(); - const { data: session } = useSession(); return ( @@ -51,9 +49,6 @@ export const Footer = () => { align="start" spacing="1px" > - { !session && } - { session && {session.user.name} } - { session && } Settings { const height = 115; const width = height / 694 * 774; + const { data: session } = useSession(); return (
@@ -43,7 +45,11 @@ export const Header = () => { alt="CML Logo" /> Celeste Mods List - + + { !session && } + { session && {session.user.name} } + { session && } +
); From 143071470e2302f7dbc015a4bfba1de023805c2c Mon Sep 17 00:00:00 2001 From: otobot1 Date: Fri, 13 Sep 2024 23:08:30 -0600 Subject: [PATCH 05/43] tweak page URLs combine user-claim related pages into single folder --- src/{pages/claim-user.tsx => claim-user/index.tsx} | 0 src/{pages/verify-claim.tsx => claim-user/verify.tsx} | 0 src/consts/pathnames.ts | 2 +- 3 files changed, 1 insertion(+), 1 deletion(-) rename src/{pages/claim-user.tsx => claim-user/index.tsx} (100%) rename src/{pages/verify-claim.tsx => claim-user/verify.tsx} (100%) diff --git a/src/pages/claim-user.tsx b/src/claim-user/index.tsx similarity index 100% rename from src/pages/claim-user.tsx rename to src/claim-user/index.tsx diff --git a/src/pages/verify-claim.tsx b/src/claim-user/verify.tsx similarity index 100% rename from src/pages/verify-claim.tsx rename to src/claim-user/verify.tsx diff --git a/src/consts/pathnames.ts b/src/consts/pathnames.ts index 740b94b5..d72160a2 100644 --- a/src/consts/pathnames.ts +++ b/src/consts/pathnames.ts @@ -2,4 +2,4 @@ export const MODS_PAGE_PATHNAME = "/mods"; export const FAQ_PAGE_PATHNAME = "/faq"; export const COMING_SOON_PATHNAME = "/coming-soon"; export const CLAIM_USER_PATHNAME = "/claim-user"; -export const VERIFY_CLAIM_PATHNAME = "/verify-claim"; \ No newline at end of file +export const VERIFY_CLAIM_PATHNAME = "/claim-user/verify"; \ No newline at end of file From d691dcbc76a052159d84b4fba281138846f138d4 Mon Sep 17 00:00:00 2001 From: otobot1 Date: Fri, 13 Sep 2024 23:14:56 -0600 Subject: [PATCH 06/43] tweak wording on index page make explicit that we don't want people duplicating their ratings --- src/pages/index.tsx | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/src/pages/index.tsx b/src/pages/index.tsx index aaaebe8f..0b4c06ed 100644 --- a/src/pages/index.tsx +++ b/src/pages/index.tsx @@ -79,12 +79,11 @@ const Home: NextPage = () => {

CML Public Beta

Welcome! The site is currently in early beta.

For now, mods can only be browsed.

- { status === "authenticated" && ( + {status === "authenticated" && (

- If you submitted ratings via the google form, you can claim your old user - from here. + If you submitted ratings via the google form, PLEASE CLAIM YOUR OLD USER instead of submitting duplicate ratings for the same maps!

- ) } + )}

Community Projects

Celeste Mountain Lego Idea

{ - + ); }; From 1cb0d26f5d80dda06070977a4a79a33abedb6b46 Mon Sep 17 00:00:00 2001 From: otobot1 Date: Sun, 15 Sep 2024 14:27:29 -0600 Subject: [PATCH 07/43] Update schema.prisma Tweak UserClaim column and relation names --- prisma/schema.prisma | 23 +++++++++++++---------- 1 file changed, 13 insertions(+), 10 deletions(-) diff --git a/prisma/schema.prisma b/prisma/schema.prisma index 7dc6a5d6..8518ec25 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -581,14 +581,16 @@ model UsersToCompletedMaps { } model UserClaim { - User_UserClaim_claimBy User @relation("UserClaim_claimBy", fields: [claimByUserId], references: [id], onDelete: Cascade, onUpdate: Restrict) - claimByUserId String - User_UserClaim_claimFor User @relation("UserClaim_claimFor", fields: [claimForUserId], references: [id], onDelete: Cascade, onUpdate: Restrict) - claimForUserId String - id Int @id @default(autoincrement()) @db.UnsignedMediumInt - - @@unique([claimByUserId, claimForUserId]) - @@index([claimByUserId]) + id Int @id @default(autoincrement()) @db.UnsignedMediumInt + User_claimedBy User @relation("UserClaim_ClaimedBy", fields: [claimedBy], references: [id], onDelete: Cascade, onUpdate: Restrict) + claimedBy String + User_claimedUser User @relation("UserClaim_ClaimedUserId", fields: [claimedUserId], references: [id], onDelete: Cascade, onUpdate: Restrict) + claimedUserId String + User_ApprovedBy User? @relation("UserClaim_ApprovedBy", fields: [approvedBy], references: [id], onDelete: Cascade, onUpdate: Restrict) + approvedBy String? + + @@unique([claimedBy, claimedUserId]) + @@index([claimedBy]) @@map("user-claim") } @@ -628,8 +630,9 @@ model User { Map_NewWithMod_New_submittedByToUser Map_NewWithMod_New[] @relation("Map_NewWithMod_New_SubmittedByToUser") Map_NewSolo_mapperUserIdToUser Map_NewSolo[] @relation("Map_NewSolo_MapperUserIdToUser") Map_NewSolo_submittedByToUser Map_NewSolo[] @relation("Map_NewSolo_SubmittedByToUser") - UserClaim_UserClaim_claimBy UserClaim[] @relation("UserClaim_claimBy") - UserClaim_UserClaim_claimFor UserClaim[] @relation("UserClaim_claimFor") + UserClaim_claimedByToUser UserClaim[] @relation("UserClaim_ClaimedBy") + UserClaim_claimedUserIdToUser UserClaim[] @relation("UserClaim_ClaimedUserId") + UserClaim_approvedByToUser UserClaim[] @relation("UserClaim_ApprovedBy") @@unique([discordUsername, discordDiscriminator]) @@map("user") From e40cf9928456d455f3ff6ac67099326e15d64bc9 Mon Sep 17 00:00:00 2001 From: otobot1 Date: Sun, 15 Sep 2024 14:28:46 -0600 Subject: [PATCH 08/43] refactor checkIsPrivileged to support arrays of targetUserIds --- src/server/api/utils/permissions.ts | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/src/server/api/utils/permissions.ts b/src/server/api/utils/permissions.ts index d8b00350..1a4559e7 100644 --- a/src/server/api/utils/permissions.ts +++ b/src/server/api/utils/permissions.ts @@ -10,7 +10,7 @@ export type Permission = typeof permissionArray[number]; const getPermissions = (array: T) => { return array; -} +}; export const ADMIN_PERMISSION_STRINGS = getPermissions(["Super_Admin", "Admin"] as const); @@ -48,15 +48,21 @@ export const checkPermissions = (validPermissionsArray: readonly Permission[], u return false; -} +}; /** * throws on failure */ -export const checkIsPrivileged = (validPermissionsArray: readonly Permission[], sessionUser: SessionUser, targetUserId: string): void => { - if (sessionUser.id === targetUserId) return; +export const checkIsPrivileged = (validPermissionsArray: readonly Permission[], sessionUser: SessionUser, targetUserIdOrArray: string | string[]): void => { + if (Array.isArray(targetUserIdOrArray)) { + for (const targetUserId of targetUserIdOrArray) { + if (sessionUser.id === targetUserId) return; + } + } else { + if (sessionUser.id === targetUserIdOrArray) return; + } const isPrivileged = checkPermissions(validPermissionsArray, sessionUser.permissions); @@ -64,4 +70,4 @@ export const checkIsPrivileged = (validPermissionsArray: readonly Permission[], if (!isPrivileged) throw new TRPCError({ code: "FORBIDDEN", }); -} \ No newline at end of file +}; \ No newline at end of file From 009e6d752af4870d844ede254302212f96d8313e Mon Sep 17 00:00:00 2001 From: otobot1 Date: Sun, 15 Sep 2024 14:33:58 -0600 Subject: [PATCH 09/43] remove unused variable --- src/server/api/routers/map_mod_publisher/map.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/server/api/routers/map_mod_publisher/map.ts b/src/server/api/routers/map_mod_publisher/map.ts index c0a583a8..eb05c78d 100644 --- a/src/server/api/routers/map_mod_publisher/map.ts +++ b/src/server/api/routers/map_mod_publisher/map.ts @@ -1244,7 +1244,7 @@ export const mapRouter = createTRPCRouter({ deleteMap_total: adminProcedure .input(mapIdSchema) .mutation(async ({ ctx, input }) => { - const mapFromId = await getMapById("Map", false, false, ctx.prisma, input.id); //check that id matches an existing map + await getMapById("Map", false, false, ctx.prisma, input.id); //check that id matches an existing map await ctx.prisma.map.delete({ where: { id: input.id } }); //the deletion should cascade to any maps, mapEdits, and mapArchives From 87415b07943bee5e4eefe9de34e854b15584302d Mon Sep 17 00:00:00 2001 From: otobot1 Date: Sun, 15 Sep 2024 14:50:24 -0600 Subject: [PATCH 10/43] fix up tech and techVideo routers --- src/server/api/routers/map_mod_publisher/map.ts | 2 +- src/server/api/routers/tech_techVideo/tech.ts | 5 ++++- src/server/api/routers/tech_techVideo/techVideo.ts | 5 ++--- 3 files changed, 7 insertions(+), 5 deletions(-) diff --git a/src/server/api/routers/map_mod_publisher/map.ts b/src/server/api/routers/map_mod_publisher/map.ts index eb05c78d..9407b6e2 100644 --- a/src/server/api/routers/map_mod_publisher/map.ts +++ b/src/server/api/routers/map_mod_publisher/map.ts @@ -12,7 +12,7 @@ import { getModById } from "./mod"; import { getPublisherById } from "./publisher"; import { difficultyIdSchema_NonObject } from "../difficulty"; import { lengthIdSchema_NonObject } from "../length"; -import { techIdSchema_NonObject } from "../tech_techVideo/techVideo"; +import { techIdSchema_NonObject } from "../tech_techVideo/tech"; import { IfElse, ArrayIncludes } from "../../../../utils/typeHelpers"; import { getCurrentTime } from "../../utils/getCurrentTime"; import { getCheckedTableNames } from "../../utils/getCheckedTableNames"; diff --git a/src/server/api/routers/tech_techVideo/tech.ts b/src/server/api/routers/tech_techVideo/tech.ts index 0769d770..826eea4d 100644 --- a/src/server/api/routers/tech_techVideo/tech.ts +++ b/src/server/api/routers/tech_techVideo/tech.ts @@ -6,7 +6,7 @@ import { Prisma, Tech } from "@prisma/client"; import { getCombinedSchema, getOrderObjectArray } from "~/server/api/utils/sortOrderHelpers"; import { getNonEmptyArray } from "~/utils/getNonEmptyArray"; import { INT_MAX_SIZES } from "~/consts/integerSizes"; -import { techVideoRouter, defaultTechVideoSelect, techVideoPostWithTechSchema, techIdSchema_NonObject } from "./techVideo"; +import { techVideoRouter, defaultTechVideoSelect, techVideoPostWithTechSchema } from "./techVideo"; @@ -31,6 +31,9 @@ const defaultTechSelect = Prisma.validator()({ +export const techIdSchema_NonObject = z.number().int().gte(1).lte(INT_MAX_SIZES.smallInt.unsigned); //this needs to be here to resolve webpack error in ~\src\pages\api\panel.ts + + const techNameSchema_NonObject = z.string().min(1).max(50); diff --git a/src/server/api/routers/tech_techVideo/techVideo.ts b/src/server/api/routers/tech_techVideo/techVideo.ts index c8cc39a1..ae666de7 100644 --- a/src/server/api/routers/tech_techVideo/techVideo.ts +++ b/src/server/api/routers/tech_techVideo/techVideo.ts @@ -6,6 +6,7 @@ import { Prisma, TechVideo } from "@prisma/client"; import { getCombinedSchema, getOrderObjectArray } from "~/server/api/utils/sortOrderHelpers"; import { getNonEmptyArray } from "~/utils/getNonEmptyArray"; import { INT_MAX_SIZES } from "~/consts/integerSizes"; +import { techIdSchema_NonObject } from "./tech"; @@ -29,8 +30,6 @@ export const techVideoPostWithTechSchema = z.object({ }).strict(); -export const techIdSchema_NonObject = z.number().int().gte(1).lte(INT_MAX_SIZES.smallInt.unsigned); //this needs to be here to resolve webpack error in ~\src\pages\api\panel.ts - const techIdSchema_forTechVideos = z.object({ techId: techIdSchema_NonObject, }).strict(); @@ -49,7 +48,7 @@ const techVideoOrderSchema = getCombinedSchema( const getTechVideoById = async (prisma: MyPrismaClient, id: number): Promise> => { - const techVideo: TechVideo | null = await prisma.difficulty.findUnique({ //having type declaration here AND in function signature is safer + const techVideo: TechVideo | null = await prisma.techVideo.findUnique({ //having type declaration here AND in function signature is safer where: { id: id }, select: defaultTechVideoSelect, }); From 6959303240c2280974db3250a84cf8b4b8bd9681 Mon Sep 17 00:00:00 2001 From: otobot1 Date: Sun, 15 Sep 2024 15:07:38 -0600 Subject: [PATCH 11/43] move userClaim pages into pages directory required by next.js pagesRouter --- src/{ => pages}/claim-user/index.tsx | 0 src/{ => pages}/claim-user/verify.tsx | 0 2 files changed, 0 insertions(+), 0 deletions(-) rename src/{ => pages}/claim-user/index.tsx (100%) rename src/{ => pages}/claim-user/verify.tsx (100%) diff --git a/src/claim-user/index.tsx b/src/pages/claim-user/index.tsx similarity index 100% rename from src/claim-user/index.tsx rename to src/pages/claim-user/index.tsx diff --git a/src/claim-user/verify.tsx b/src/pages/claim-user/verify.tsx similarity index 100% rename from src/claim-user/verify.tsx rename to src/pages/claim-user/verify.tsx From 2f8124460ab5611e724b41e508980af351696675 Mon Sep 17 00:00:00 2001 From: otobot1 Date: Sun, 15 Sep 2024 15:10:07 -0600 Subject: [PATCH 12/43] refactor userClaim api backend only. frontend tweaks forthcoming. -split userClaim endpoints into a subrouter -place the user and userClaim router files into a new folder -tweak userClaim api to make userClaims non-public, as they don't need to be public -tweak userClaim router for formatting and readability, and make it match existing routers more closely -wrap the userClaim verification db calls into a transaction --- src/components/layout/header.tsx | 6 +- src/server/api/root.ts | 2 +- .../api/routers/map_mod_publisher/map.ts | 28 +- .../routers/map_mod_publisher/publisher.ts | 2 +- src/server/api/routers/rating.ts | 19 +- .../reviewCollection.ts | 2 +- src/server/api/routers/user.ts | 576 ------------------ src/server/api/routers/user_userClaim/user.ts | 270 ++++++++ .../api/routers/user_userClaim/userClaim.ts | 347 +++++++++++ 9 files changed, 648 insertions(+), 604 deletions(-) delete mode 100644 src/server/api/routers/user.ts create mode 100644 src/server/api/routers/user_userClaim/user.ts create mode 100644 src/server/api/routers/user_userClaim/userClaim.ts diff --git a/src/components/layout/header.tsx b/src/components/layout/header.tsx index 0f147d83..9c6c69c5 100644 --- a/src/components/layout/header.tsx +++ b/src/components/layout/header.tsx @@ -46,9 +46,9 @@ export const Header = () => { /> Celeste Mods List - { !session && } - { session && {session.user.name} } - { session && } + {!session && } + {session && {session.user.name}} + {session && } diff --git a/src/server/api/root.ts b/src/server/api/root.ts index 2786295d..f5e15299 100644 --- a/src/server/api/root.ts +++ b/src/server/api/root.ts @@ -2,7 +2,7 @@ import { createTRPCRouter, publicProcedure } from "~/server/api/trpc"; import { difficultyRouter } from "./routers/difficulty"; import { techRouter } from "./routers/tech_techVideo/tech"; import { lengthRouter } from "./routers/length"; -import { userRouter } from "./routers/user"; +import { userRouter } from "./routers/user_userClaim/user"; import { publisherRouter } from "./routers/map_mod_publisher/publisher"; import { modRouter } from "./routers/map_mod_publisher/mod"; import { mapRouter } from "./routers/map_mod_publisher/map"; diff --git a/src/server/api/routers/map_mod_publisher/map.ts b/src/server/api/routers/map_mod_publisher/map.ts index 9407b6e2..a9f47d9f 100644 --- a/src/server/api/routers/map_mod_publisher/map.ts +++ b/src/server/api/routers/map_mod_publisher/map.ts @@ -6,7 +6,7 @@ import { Prisma, Map, MapSide, Map_NewWithMod_New, Map_Edit, Map_Archive, Map_Ne import { getCombinedSchema, getOrderObjectArray } from "~/server/api/utils/sortOrderHelpers"; import { getNonEmptyArray } from "~/utils/getNonEmptyArray"; import { INT_MAX_SIZES } from "~/consts/integerSizes"; -import { displayNameSchema_NonObject, getUserById, userIdSchema_NonObject } from "../user"; +import { displayNameSchema_NonObject, getUserById, userIdSchema_NonObject } from "../user_userClaim/user"; import { MODLIST_MODERATOR_PERMISSION_STRINGS, checkPermissions } from "../../utils/permissions"; import { getModById } from "./mod"; import { getPublisherById } from "./publisher"; @@ -326,35 +326,35 @@ export const getMapById = async< map = await prisma.map.findUnique({ where: whereObject, include: { MapToTechs: includeTechObject }, - }) as Union; + }) as Union; // this is safe because map is checked to be non-null in the next block. this cast is required to make TypeScript happy. break; } case "Map_Archive": { map = await prisma.map_Archive.findUnique({ where: whereObject as Prisma.Map_ArchiveWhereUniqueInput, include: { Map_ArchiveToTechs: includeTechObject }, - }) as Union; + }) as Union; // this is safe because map is checked to be non-null in the next block. this cast is required to make TypeScript happy. break; } case "Map_Edit": { map = await prisma.map_Edit.findUnique({ where: whereObject as Prisma.Map_EditWhereUniqueInput, include: { Map_EditToTechs: includeTechObject }, - }) as Union; + }) as Union; // this is safe because map is checked to be non-null in the next block. this cast is required to make TypeScript happy. break; } case "Map_NewWithMod_New": { map = await prisma.map_NewWithMod_New.findUnique({ where: whereObject as Prisma.Map_NewWithMod_NewWhereUniqueInput, include: { Map_NewWithMod_NewToTechs: includeTechObject }, - }) as Union; + }) as Union; // this is safe because map is checked to be non-null in the next block. this cast is required to make TypeScript happy. break; } case "Map_NewSolo": { map = await prisma.map_NewSolo.findUnique({ where: whereObject as Prisma.Map_NewSoloWhereUniqueInput, include: { Map_NewSoloToTechs: includeTechObject }, - }) as Union; + }) as Union; // this is safe because map is checked to be non-null in the next block. this cast is required to make TypeScript happy. break; } default: { @@ -368,35 +368,35 @@ export const getMapById = async< map = await prisma.map.findUnique({ where: whereObject, select: defaultMapSelect, - }) as Union; + }) as Union; // this is safe because map is checked to be non-null in the next block. this cast is required to make TypeScript happy. break; } case "Map_Archive": { map = await prisma.map_Archive.findUnique({ where: whereObject as Prisma.Map_ArchiveWhereUniqueInput, select: defaultMapArchiveSelect, - }) as Union; + }) as Union; // this is safe because map is checked to be non-null in the next block. this cast is required to make TypeScript happy. break; } case "Map_Edit": { map = await prisma.map_Edit.findUnique({ where: whereObject as Prisma.Map_EditWhereUniqueInput, select: defaultMapEditSelect, - }) as Union; + }) as Union; // this is safe because map is checked to be non-null in the next block. this cast is required to make TypeScript happy. break; } case "Map_NewWithMod_New": { map = await prisma.map_NewWithMod_New.findUnique({ where: whereObject as Prisma.Map_NewWithMod_NewWhereUniqueInput, select: defaultMapNewWithModNewSelect, - }) as Union; + }) as Union; // this is safe because map is checked to be non-null in the next block. this cast is required to make TypeScript happy. break; } case "Map_NewSolo": { map = await prisma.map_NewSolo.findUnique({ where: whereObject as Prisma.Map_NewSoloWhereUniqueInput, select: defaultMapNewSoloSelect, - }) as Union; + }) as Union; // this is safe because map is checked to be non-null in the next block. this cast is required to make TypeScript happy. break; } default: { @@ -802,7 +802,7 @@ export const mapRouter = createTRPCRouter({ let mapperNameString: string; if (nonNormalInput.mapperUserId) { - const userFromId = await getUserById(ctx.prisma, nonNormalInput.mapperUserId, undefined); + const userFromId = await getUserById(ctx.prisma, nonNormalInput.mapperUserId, undefined, undefined); mapperNameString = userFromId.name; } @@ -977,13 +977,13 @@ export const mapRouter = createTRPCRouter({ let mapperNameString: string; if (nonNormalInput.mapperUserId) { - const userFromId = await getUserById(ctx.prisma, nonNormalInput.mapperUserId, undefined); + const userFromId = await getUserById(ctx.prisma, nonNormalInput.mapperUserId, undefined, undefined); mapperUserId = userFromId.id; mapperNameString = userFromId.name; } else if (nonNormalInput.mapperUserId !== null && existingMap.mapperUserId) { - const userFromId = await getUserById(ctx.prisma, existingMap.mapperUserId, undefined); + const userFromId = await getUserById(ctx.prisma, existingMap.mapperUserId, undefined, undefined); mapperUserId = undefined; mapperNameString = userFromId.name; diff --git a/src/server/api/routers/map_mod_publisher/publisher.ts b/src/server/api/routers/map_mod_publisher/publisher.ts index 1042cea2..73954981 100644 --- a/src/server/api/routers/map_mod_publisher/publisher.ts +++ b/src/server/api/routers/map_mod_publisher/publisher.ts @@ -6,7 +6,7 @@ import { Prisma, Publisher } from "@prisma/client"; import { getCombinedSchema, getOrderObjectArray } from "~/server/api/utils/sortOrderHelpers"; import { getNonEmptyArray } from "~/utils/getNonEmptyArray"; import { INT_MAX_SIZES } from "~/consts/integerSizes"; -import { userIdSchema_NonObject } from "../user"; +import { userIdSchema_NonObject } from "../user_userClaim/user"; import axios from "axios"; diff --git a/src/server/api/routers/rating.ts b/src/server/api/routers/rating.ts index 2108c7ce..e60b7351 100644 --- a/src/server/api/routers/rating.ts +++ b/src/server/api/routers/rating.ts @@ -12,7 +12,7 @@ import { difficultyIdSchema_NonObject } from "./difficulty"; import { getCurrentTime } from "../utils/getCurrentTime"; import { ADMIN_PERMISSION_STRINGS, checkIsPrivileged } from "../utils/permissions"; import { getModById } from "./map_mod_publisher/mod"; -import { userIdSchema_NonObject } from "./user"; +import { userIdSchema_NonObject } from "./user_userClaim/user"; @@ -91,8 +91,7 @@ const getRatingById = async< } return undefined as ReturnType; - } - else { + } else { if (!rating) { throw new TRPCError({ code: "NOT_FOUND", @@ -393,7 +392,7 @@ export const ratingRouter = createTRPCRouter({ getById: adminProcedure .input(ratingIdSchema) .query(async ({ ctx, input }) => { - return await getRatingById(false, ctx.prisma, input.id); + return getRatingById(false, ctx.prisma, input.id); }), getByModId: adminProcedure @@ -403,7 +402,7 @@ export const ratingRouter = createTRPCRouter({ }).strict(), ) .query(async ({ ctx, input }) => { - return await ctx.prisma.rating.findMany({ where: { Map: { modId: input.modId } } }); + return ctx.prisma.rating.findMany({ where: { Map: { modId: input.modId } } }); }), getByMapId: adminProcedure @@ -413,17 +412,21 @@ export const ratingRouter = createTRPCRouter({ }).strict(), ) .query(async ({ ctx, input }) => { - return await ctx.prisma.rating.findMany({ where: { mapId: input.mapId } }); + return ctx.prisma.rating.findMany({ where: { mapId: input.mapId } }); }), - getByUserId: adminProcedure + getByUserId: loggedInProcedure .input( z.object({ userId: userIdSchema_NonObject, }).strict(), ) .query(async ({ ctx, input }) => { - return await ctx.prisma.rating.findMany({ where: { submittedBy: input.userId } }); + const ratingsFromId = await ctx.prisma.rating.findMany({ where: { submittedBy: input.userId } }); + + checkIsPrivileged(ADMIN_PERMISSION_STRINGS, ctx.user, input.userId); //check that user has permission to view this user's ratings + + return ctx.prisma.rating.findMany({ where: { submittedBy: input.userId } }); }), add: loggedInProcedure diff --git a/src/server/api/routers/review_reviewCollection_mapReview/reviewCollection.ts b/src/server/api/routers/review_reviewCollection_mapReview/reviewCollection.ts index 8cc8f613..d564251c 100644 --- a/src/server/api/routers/review_reviewCollection_mapReview/reviewCollection.ts +++ b/src/server/api/routers/review_reviewCollection_mapReview/reviewCollection.ts @@ -6,7 +6,7 @@ import { Prisma, ReviewCollection } from "@prisma/client"; import { getCombinedSchema, getOrderObjectArray } from "~/server/api/utils/sortOrderHelpers"; import { getNonEmptyArray } from "~/utils/getNonEmptyArray"; import { INT_MAX_SIZES } from "~/consts/integerSizes"; -import { userIdSchema_NonObject } from "../user"; +import { userIdSchema_NonObject } from "../user_userClaim/user"; import { ADMIN_PERMISSION_STRINGS, MODLIST_MODERATOR_PERMISSION_STRINGS, checkIsPrivileged } from "../../utils/permissions"; diff --git a/src/server/api/routers/user.ts b/src/server/api/routers/user.ts deleted file mode 100644 index bddcf7ea..00000000 --- a/src/server/api/routers/user.ts +++ /dev/null @@ -1,576 +0,0 @@ -import { z } from "zod"; -import { createTRPCRouter, publicProcedure, loggedInProcedure, adminProcedure } from "~/server/api/trpc"; -import { TRPCError } from "@trpc/server"; -import { MyPrismaClient } from "~/server/prisma"; -import { Prisma, User } from "@prisma/client"; -import { getCombinedSchema, getOrderObjectArray } from "~/server/api/utils/sortOrderHelpers"; -import { getNonEmptyArray } from "~/utils/getNonEmptyArray"; -import { ADMIN_PERMISSION_STRINGS, Permission, checkIsPrivileged, checkPermissions } from "../utils/permissions"; -import { selectIdObject } from "../utils/selectIdObject"; - - - - -type TrimmedUser = Pick> & { - Publisher: { id: number; }[]; - ReviewCollection: { id: number; }[]; -}; - - -type ExpandedUser = TrimmedUser & { - Account: { - providerAccountId: string; - }[]; - discordUsername: string; - discordDiscriminator: string; - permissions: string[]; - timeDeletedOrBanned?: number; -}; - - - - -const defaultPartialUserSelectObject = { - id: true, - name: true, - image: true, - displayDiscord: true, - showCompletedMaps: true, - accountStatus: true, - Publisher: selectIdObject, - ReviewCollection: selectIdObject, -}; - -const defaultPartialUserSelect = Prisma.validator()(defaultPartialUserSelectObject); - - -const discordUserSelectObject = { - Account: { - where: { - provider: "discord", //TODO: ensure this is the right casing - }, - select: { - providerAccountId: true, - }, - }, - discordUsername: true, - discordDiscriminator: true, -}; - -const discordUserSelect = Prisma.validator()(discordUserSelectObject); - - -const defaultFullUserSelect = Prisma.validator()({ - ...defaultPartialUserSelectObject, - ...discordUserSelectObject, - permissions: true, - timeDeletedOrBanned: true, -}); - - -/** - * @param permissions permission string array from sessionUser - * @param overwrite set to true to force return of defaultFullUserSelect. set to false to force return of defaultPartialUserSelect. leave undefined to use fallback logic. - */ -const getUserSelect = (permissions: Permission[] | undefined, overwrite?: boolean): typeof defaultPartialUserSelect | typeof defaultFullUserSelect => { //TODO: base check on admin OR relevant user - if (overwrite === true) return defaultFullUserSelect; - else if (overwrite === false) return defaultPartialUserSelect; - - if (checkPermissions(ADMIN_PERMISSION_STRINGS, permissions)) return defaultFullUserSelect; - else return defaultPartialUserSelect; -}; - - - - -export const displayNameSchema_NonObject = z.string().min(1).max(50); - - -export const userIdSchema_NonObject = z.string().cuid(); //TODO!: figure out if we need to add z.coerce before string() - -const userIdSchema = z.object({ - id: userIdSchema_NonObject, -}).strict(); - - -const userPostSchema = z.object({ - discordCode: z.string(), - displayName: displayNameSchema_NonObject, - displayDiscord: z.boolean(), - showCompletedMaps: z.boolean(), -}).strict(); - - -const userOrderSchema = getCombinedSchema( - getNonEmptyArray(Prisma.UserScalarFieldEnum), - ["name"], - ["asc"], -); - - - - -/** - * @param permissions permission string array from sessionUser - * @param overwrite set to true to force return of defaultFullUserSelect. set to false to force return of defaultPartialUserSelect. leave undefined to use fallback logic. - */ -export const getUserById = async ( - prisma: MyPrismaClient, - id: string, - permissions: Permission[] | undefined, - overwrite?: boolean, -): Promise< - TrimmedUser | - ExpandedUser -> => { - const user = await prisma.user.findUnique({ //having type declaration here AND in function signature is safer - where: { id: id }, - select: getUserSelect(permissions, overwrite), - }); - - if (!user) { - throw new TRPCError({ - code: "NOT_FOUND", - message: `No user exists with id "${id}"`, - }); - } - - return user; -}; - - - - -const undefinedSessionError = new TRPCError({ - code: "INTERNAL_SERVER_ERROR", - message: "Session is undefined when it should not be. Please contact an admin.", -}); - - - - -export const userRouter = createTRPCRouter({ - getAll: publicProcedure - .input(userOrderSchema) - .query(({ ctx, input }) => { - return ctx.prisma.user.findMany({ - select: getUserSelect(ctx.session?.user.permissions), - orderBy: getOrderObjectArray(input.selectors, input.directions), - }); - }), - - getMany: publicProcedure - .input( - z.object({ - pageSize: z.number().int().min(1).max(100).default(50), - pageNumber: z.number().int().min(1).default(1), - }).strict().merge(userOrderSchema), - ) - .query(async ({ ctx, input }) => { - const { pageSize, pageNumber } = input; - - const numToSkip = pageSize * (pageNumber - 1); - - const users = await ctx.prisma.user.findMany({ - skip: numToSkip, - take: pageSize, - select: getUserSelect(ctx.session?.user.permissions), - orderBy: getOrderObjectArray(input.selectors, input.directions), - }); - - return users; - }), - - getById: publicProcedure - .input(userIdSchema) - .query(async ({ ctx, input }) => { - return await getUserById(ctx.prisma, input.id, ctx.session?.user.permissions); - }), - - getByName: publicProcedure - .input( - z.object({ - query: displayNameSchema_NonObject, - }).strict().merge(userOrderSchema), - ) - .query(async ({ ctx, input }) => { - const users = await ctx.prisma.user.findMany({ - where: { name: { contains: input.query } }, - select: defaultPartialUserSelect, - orderBy: getOrderObjectArray(input.selectors, input.directions), - }); - - return users; - }), - - getUnlinked: loggedInProcedure - .input(userOrderSchema) - .query(async ({ ctx, input }) => { - return await ctx.prisma.user.findMany({ - where: { - accountStatus: 'Unlinked', - }, - select: { - id: true, - discordUsername: true, - discordDiscriminator: true, - }, - orderBy: getOrderObjectArray(input.selectors, input.directions), - }); - }), - - getUserClaims: loggedInProcedure - .query(async ({ ctx }) => { - return await ctx.prisma.userClaim.findMany({ - where: { - claimByUserId: ctx.user.id, - }, - select: { - id: true, - claimForUserId: true, - User_UserClaim_claimFor: { - select: { - discordUsername: true, - discordDiscriminator: true, - } - } - }, - orderBy: { - id: 'asc' - }, - }); - }), - - getAllUserClaims: adminProcedure - .query(async ({ ctx }) => { - return await ctx.prisma.userClaim.findMany({ - select: { - id: true, - User_UserClaim_claimBy: { - select: { - discordUsername: true, - discordDiscriminator: true, - } - }, - User_UserClaim_claimFor: { - select: { - discordUsername: true, - discordDiscriminator: true, - } - }, - }, - orderBy: { - id: 'asc', - } - }); - }), - - createUserClaim: loggedInProcedure - .input(z.object({ - forUserId: z.string() - })) - .mutation(async ({ ctx, input }) => { - const claim = await ctx.prisma.userClaim.create({ - data: { - claimByUserId: ctx.user.id, - claimForUserId: input.forUserId - }, - }); - - return claim; - }), - - verifyUserClaim: adminProcedure - .input(z.object({ - id: z.number() - })) - .mutation(async ({ ctx, input }) => { - const claim = await ctx.prisma.userClaim.findFirst({ - where: { - id: input.id, - } - }); - if (!claim) { - throw new TRPCError({ - code: "NOT_FOUND", - message: `No user claim exists with id "${input.id}"`, - }); - } - - await ctx.prisma.publisher.updateMany({ - where: { - userId: { - equals: claim.claimForUserId, - } - }, - data: { - userId: claim.claimByUserId, - } - }); - await ctx.prisma.mod.updateMany({ - where: { - submittedBy: { - equals: claim.claimForUserId, - } - }, - data: { - submittedBy: claim.claimByUserId, - } - }); - await ctx.prisma.mod.updateMany({ - where: { - approvedBy: { - equals: claim.claimForUserId, - } - }, - data: { - approvedBy: claim.claimByUserId, - } - }); - await ctx.prisma.map.updateMany({ - where: { - mapperUserId: { - equals: claim.claimForUserId, - } - }, - data: { - mapperUserId: claim.claimByUserId, - } - }); - await ctx.prisma.map.updateMany({ - where: { - submittedBy: { - equals: claim.claimForUserId, - } - }, - data: { - submittedBy: claim.claimByUserId, - } - }); - await ctx.prisma.map.updateMany({ - where: { - approvedBy: { - equals: claim.claimForUserId, - } - }, - data: { - approvedBy: claim.claimByUserId, - } - }); - await ctx.prisma.mod_Archive.updateMany({ - where: { - submittedBy: { - equals: claim.claimForUserId, - } - }, - data: { - submittedBy: claim.claimByUserId, - } - }); - await ctx.prisma.mod_Archive.updateMany({ - where: { - approvedBy: { - equals: claim.claimForUserId, - } - }, - data: { - approvedBy: claim.claimByUserId, - } - }); - await ctx.prisma.map_Archive.updateMany({ - where: { - mapperUserId: { - equals: claim.claimForUserId, - } - }, - data: { - mapperUserId: claim.claimByUserId, - } - }); - await ctx.prisma.map_Archive.updateMany({ - where: { - submittedBy: { - equals: claim.claimForUserId, - } - }, - data: { - submittedBy: claim.claimByUserId, - } - }); - await ctx.prisma.map_Archive.updateMany({ - where: { - approvedBy: { - equals: claim.claimForUserId, - } - }, - data: { - approvedBy: claim.claimByUserId, - } - }); - await ctx.prisma.mod_Edit.updateMany({ - where: { - submittedBy: { - equals: claim.claimForUserId, - } - }, - data: { - submittedBy: claim.claimByUserId, - } - }); - await ctx.prisma.map_Edit.updateMany({ - where: { - mapperUserId: { - equals: claim.claimForUserId, - } - }, - data: { - mapperUserId: claim.claimByUserId, - } - }); - await ctx.prisma.map_Edit.updateMany({ - where: { - submittedBy: { - equals: claim.claimForUserId, - } - }, - data: { - submittedBy: claim.claimByUserId, - } - }); - await ctx.prisma.mod_New.updateMany({ - where: { - submittedBy: { - equals: claim.claimForUserId, - } - }, - data: { - submittedBy: claim.claimByUserId, - } - }); - await ctx.prisma.map_NewWithMod_New.updateMany({ - where: { - mapperUserId: { - equals: claim.claimForUserId, - } - }, - data: { - mapperUserId: claim.claimByUserId, - } - }); - await ctx.prisma.map_NewWithMod_New.updateMany({ - where: { - submittedBy: { - equals: claim.claimForUserId, - } - }, - data: { - submittedBy: claim.claimByUserId, - } - }); - await ctx.prisma.map_NewSolo.updateMany({ - where: { - mapperUserId: { - equals: claim.claimForUserId, - } - }, - data: { - mapperUserId: claim.claimByUserId, - } - }); - await ctx.prisma.map_NewSolo.updateMany({ - where: { - submittedBy: { - equals: claim.claimForUserId, - } - }, - data: { - submittedBy: claim.claimByUserId, - } - }); - await ctx.prisma.rating.updateMany({ - where: { - submittedBy: { - equals: claim.claimForUserId, - } - }, - data: { - submittedBy: claim.claimByUserId, - } - }); - await ctx.prisma.reviewCollection.updateMany({ - where: { - userId: { - equals: claim.claimForUserId, - } - }, - data: { - userId: claim.claimByUserId, - } - }); - await ctx.prisma.usersToCompletedMaps.updateMany({ - where: { - userId: { - equals: claim.claimForUserId, - } - }, - data: { - userId: claim.claimByUserId, - } - }); - await ctx.prisma.userClaim.delete({ - where: { - id: claim.id, - } - }); - await ctx.prisma.user.delete({ - where: { - id: claim.claimForUserId, - } - }); - }), - - add: loggedInProcedure - .input(userPostSchema) - .mutation(async ({ ctx, input }) => { - //TODO: implement procedure - //enforce unique displayNames - - throw "not implemented"; - }), - - edit: loggedInProcedure - .input(userPostSchema.partial().merge(userIdSchema)) - .mutation(async ({ ctx, input }) => { - //TODO: implement procedure - //enforce unique displayNames - - throw "not implemented"; - }), - - delete: loggedInProcedure - .input(userIdSchema) - .mutation(async ({ ctx, input }) => { - if (!ctx.session?.user) throw undefinedSessionError; - - - checkIsPrivileged(ADMIN_PERMISSION_STRINGS, ctx.user, input.id); //check user has sufficient privileges - - await getUserById(ctx.prisma, input.id, ctx.user.permissions, true); //check that id matches an existing user //overwrite = true because checkIsPrivileged was called - - - await ctx.prisma.user.delete({ where: { id: input.id } }); - - - return true; - }), - - adminEdits: adminProcedure - .input(userPostSchema) - .mutation(async ({ ctx, input }) => { - //TODO: implement procedure - //cover things like banning users or changing permissions - //may split into multiple procedures - - throw "not implemented"; - }), -}); \ No newline at end of file diff --git a/src/server/api/routers/user_userClaim/user.ts b/src/server/api/routers/user_userClaim/user.ts new file mode 100644 index 00000000..0b037eed --- /dev/null +++ b/src/server/api/routers/user_userClaim/user.ts @@ -0,0 +1,270 @@ +import { z } from "zod"; +import { createTRPCRouter, publicProcedure, loggedInProcedure, adminProcedure } from "~/server/api/trpc"; +import { TRPCError } from "@trpc/server"; +import { MyPrismaClient } from "~/server/prisma"; +import { Prisma, User } from "@prisma/client"; +import { getCombinedSchema, getOrderObjectArray } from "~/server/api/utils/sortOrderHelpers"; +import { getNonEmptyArray } from "~/utils/getNonEmptyArray"; +import { ADMIN_PERMISSION_STRINGS, Permission, checkIsPrivileged, checkPermissions } from "../../utils/permissions"; +import { selectIdObject } from "../../utils/selectIdObject"; +import { userClaimRouter } from "./userClaim"; + + + + +type TrimmedUser = Pick> & { + Publisher: { id: number; }[]; + ReviewCollection: { id: number; }[]; +}; + + +type ExpandedUser = TrimmedUser & { + Account: { + providerAccountId: string; + }[]; + discordUsername: string; + discordDiscriminator: string; + permissions: string[]; + timeDeletedOrBanned?: number; +}; + + + + +const defaultPartialUserSelectObject = { + id: true, + name: true, + image: true, + displayDiscord: true, + showCompletedMaps: true, + accountStatus: true, + Publisher: selectIdObject, + ReviewCollection: selectIdObject, +}; + +const defaultPartialUserSelect = Prisma.validator()(defaultPartialUserSelectObject); + + +const discordUserSelectObject = { + Account: { + where: { + provider: "discord", //TODO: ensure this is the right casing + }, + select: { + providerAccountId: true, + }, + }, + discordUsername: true, + discordDiscriminator: true, +}; + +const discordUserSelect = Prisma.validator()(discordUserSelectObject); + + +const defaultFullUserSelect = Prisma.validator()({ + ...defaultPartialUserSelectObject, + ...discordUserSelectObject, + permissions: true, + timeDeletedOrBanned: true, +}); + + +/** + * @param permissions permission string array from sessionUser + * @param overwrite set to true to force return of defaultFullUserSelect. set to false to force return of defaultPartialUserSelect. leave undefined to use fallback logic. + */ +const getUserSelect = (permissions: Permission[] | undefined, overwrite?: boolean): typeof defaultPartialUserSelect | typeof defaultFullUserSelect => { //TODO: base check on admin OR relevant user + if (overwrite === true) return defaultFullUserSelect; + else if (overwrite === false) return defaultPartialUserSelect; + + if (checkPermissions(ADMIN_PERMISSION_STRINGS, permissions)) return defaultFullUserSelect; + else return defaultPartialUserSelect; +}; + + + + +export const displayNameSchema_NonObject = z.string().min(1).max(50); + + +export const userIdSchema_NonObject = z.string().cuid(); //TODO!: figure out if we need to add z.coerce before string() + +const userIdSchema = z.object({ + id: userIdSchema_NonObject, +}).strict(); + + +const userPostSchema = z.object({ + discordCode: z.string(), + displayName: displayNameSchema_NonObject, + displayDiscord: z.boolean(), + showCompletedMaps: z.boolean(), +}).strict(); + + +const userOrderSchema = getCombinedSchema( + getNonEmptyArray(Prisma.UserScalarFieldEnum), + ["name"], + ["asc"], +); + + + + +/** + * @param permissions permission string array from sessionUser + * @param overwrite set to true to force return of defaultFullUserSelect. set to false to force return of defaultPartialUserSelect. leave undefined to use fallback logic. + */ +export const getUserById = async ( + prisma: MyPrismaClient, + id: string, + permissions: Permission[] | undefined, + overwrite: boolean | undefined, +): Promise< + TrimmedUser | + ExpandedUser +> => { + const user = await prisma.user.findUnique({ //having type declaration here AND in function signature is safer + where: { id: id }, + select: getUserSelect(permissions, overwrite), + }); + + if (!user) { + throw new TRPCError({ + code: "NOT_FOUND", + message: `No user exists with id "${id}"`, + }); + } + + return user; +}; + + + + +const undefinedSessionError = new TRPCError({ + code: "INTERNAL_SERVER_ERROR", + message: "Session is undefined when it should not be. Please contact an admin.", +}); + + + + +export const userRouter = createTRPCRouter({ + getAll: publicProcedure + .input(userOrderSchema) + .query(({ ctx, input }) => { + return ctx.prisma.user.findMany({ + select: getUserSelect(ctx.session?.user.permissions), + orderBy: getOrderObjectArray(input.selectors, input.directions), + }); + }), + + getMany: publicProcedure + .input( + z.object({ + pageSize: z.number().int().min(1).max(100).default(50), + pageNumber: z.number().int().min(1).default(1), + }).strict().merge(userOrderSchema), + ) + .query(async ({ ctx, input }) => { + const { pageSize, pageNumber } = input; + + const numToSkip = pageSize * (pageNumber - 1); + + const users = await ctx.prisma.user.findMany({ + skip: numToSkip, + take: pageSize, + select: getUserSelect(ctx.session?.user.permissions), + orderBy: getOrderObjectArray(input.selectors, input.directions), + }); + + return users; + }), + + getById: publicProcedure + .input(userIdSchema) + .query(async ({ ctx, input }) => { + return await getUserById(ctx.prisma, input.id, ctx.session?.user.permissions, undefined); + }), + + getByName: publicProcedure + .input( + z.object({ + query: displayNameSchema_NonObject, + }).strict().merge(userOrderSchema), + ) + .query(async ({ ctx, input }) => { + const users = await ctx.prisma.user.findMany({ + where: { name: { contains: input.query } }, + select: defaultPartialUserSelect, + orderBy: getOrderObjectArray(input.selectors, input.directions), + }); + + return users; + }), + + getUnlinked: loggedInProcedure + .input(userOrderSchema) + .query(async ({ ctx, input }) => { + return await ctx.prisma.user.findMany({ + where: { + accountStatus: 'Unlinked', + }, + select: { + id: true, + discordUsername: true, + discordDiscriminator: true, + }, + orderBy: getOrderObjectArray(input.selectors, input.directions), + }); + }), + + add: loggedInProcedure + .input(userPostSchema) + .mutation(async ({ ctx, input }) => { + //TODO: implement procedure + //enforce unique displayNames + + throw "not implemented"; + }), + + edit: loggedInProcedure + .input(userPostSchema.partial().merge(userIdSchema)) + .mutation(async ({ ctx, input }) => { + //TODO: implement procedure + //enforce unique displayNames + + throw "not implemented"; + }), + + delete: loggedInProcedure + .input(userIdSchema) + .mutation(async ({ ctx, input }) => { //TODO!!!: mark the user as deleted here and update the other endpoints to not return deleted users. can be a follow-up issue. need to tidy up this whole router lol. + if (!ctx.session?.user) throw undefinedSessionError; + + + checkIsPrivileged(ADMIN_PERMISSION_STRINGS, ctx.user, input.id); //check user has sufficient privileges + + await getUserById(ctx.prisma, input.id, ctx.user.permissions, true); //check that id matches an existing user //overwrite = true because checkIsPrivileged was called + + + await ctx.prisma.user.delete({ where: { id: input.id } }); + + + return true; + }), + + adminEdits: adminProcedure + .input(userPostSchema) + .mutation(async ({ ctx, input }) => { + //TODO: implement procedure + //cover things like banning users or changing permissions + //may split into multiple procedures + + throw "not implemented"; + }), + + + userClaim: userClaimRouter, +}); \ No newline at end of file diff --git a/src/server/api/routers/user_userClaim/userClaim.ts b/src/server/api/routers/user_userClaim/userClaim.ts new file mode 100644 index 00000000..682487c9 --- /dev/null +++ b/src/server/api/routers/user_userClaim/userClaim.ts @@ -0,0 +1,347 @@ +import { z } from "zod"; +import { createTRPCRouter, adminProcedure, loggedInProcedure } from "~/server/api/trpc"; +import { TRPCError } from "@trpc/server"; +import { MyPrismaClient } from "~/server/prisma"; +import { Prisma, UserClaim } from "@prisma/client"; +import { getCombinedSchema, getOrderObjectArray } from "~/server/api/utils/sortOrderHelpers"; +import { getNonEmptyArray } from "~/utils/getNonEmptyArray"; +import { INT_MAX_SIZES } from "~/consts/integerSizes"; +import { userIdSchema_NonObject } from "./user"; +import { IfElse } from "~/utils/typeHelpers"; +import { ADMIN_PERMISSION_STRINGS, checkIsPrivileged, checkPermissions } from "../../utils/permissions"; + + + + +type TrimmedUserClaim = Omit & { + approvedBy: undefined; +}; + + + + +export const defaultUserClaimSelect = Prisma.validator()({ + id: true, + claimedBy: true, + claimedUserId: true, + approvedBy: true, +}); + + + + +const userClaimIdSchema = z.object({ + id: z.number().int().gte(1).lte(INT_MAX_SIZES.mediumInt.unsigned), +}).strict(); + + +const userIdSchema = z.object({ + userId: userIdSchema_NonObject, +}).strict(); + + +const userIdSchema_forUserClaimCreation = z.object({ + claimedUserId: userIdSchema_NonObject, +}).strict(); + + +const userClaimOrderSchema = getCombinedSchema( + getNonEmptyArray(Prisma.UserClaimScalarFieldEnum), + ["id", "claimedBy", "claimedUserId"], + ["asc"], +); + + + + +const getUserClaimById = async< + ReturnAll extends boolean, + Union extends IfElse, + ReturnType extends NonNullable +>( + returnAll: ReturnAll, + prisma: MyPrismaClient, + id: number +): Promise => { + let userClaim: Union | null; + + if (returnAll) { + userClaim = await prisma.userClaim.findUnique({ //having type declaration here AND in function signature is safer + where: { id: id }, + }) as Union; // this is safe because userClaim is checked to be non-null in the next block. this cast is required to make TypeScript happy. + + } else { + userClaim = await prisma.userClaim.findUnique({ //having type declaration here AND in function signature is safer + where: { id: id }, + select: defaultUserClaimSelect, + }) as Union; // this is safe because userClaim is checked to be non-null in the next block. this cast is required to make TypeScript happy. + } + + + if (!userClaim) { + throw new TRPCError({ + code: "NOT_FOUND", + message: `No userClaim exists with id "${id}"`, + }); + } + + + return userClaim as unknown as ReturnType; // this cast is required to make TypeScript happy. +}; + + + + +const getTrimmedUserClaim = (userClaim: UserClaim): TrimmedUserClaim => ({ + ...userClaim, + approvedBy: undefined, +}); + + + + +export const userClaimRouter = createTRPCRouter({ + getAll: adminProcedure + .input(userClaimOrderSchema) + .query(({ ctx, input }) => { + return ctx.prisma.userClaim.findMany({ + select: defaultUserClaimSelect, + orderBy: getOrderObjectArray(input.selectors, input.directions), + }); + }), + + getById: loggedInProcedure + .input(userClaimIdSchema) + .query(async ({ ctx, input }) => { + const userClaim = await getUserClaimById(true, ctx.prisma, input.id); + + + const isAdmin = checkPermissions(ADMIN_PERMISSION_STRINGS, ctx.user.permissions); + + if (isAdmin) { + return userClaim; + } + + + const isLinkedUser = ctx.user.id === userClaim.claimedBy || ctx.user.id === userClaim.claimedUserId; + + if (isLinkedUser) { + return getTrimmedUserClaim(userClaim); + } + }), + + getByClaimingUserId: loggedInProcedure + .input(userIdSchema.merge(userClaimOrderSchema)) + .query(async ({ ctx, input }) => { + const userClaims = await ctx.prisma.userClaim.findMany({ + where: { claimedBy: input.userId }, + select: defaultUserClaimSelect, + orderBy: getOrderObjectArray(input.selectors, input.directions), + }); + + + const isAdmin = checkPermissions(ADMIN_PERMISSION_STRINGS, ctx.user.permissions); + + if (isAdmin) { + return userClaims; + } + + + const isLinkedUser = ctx.user.id === input.userId; + + if (isLinkedUser) { + return userClaims.map(getTrimmedUserClaim); + } + }), + + getByClaimedUserId: adminProcedure + .input(userIdSchema.merge(userClaimOrderSchema)) + .query(async ({ ctx, input }) => { + const userClaims = await ctx.prisma.userClaim.findMany({ + where: { claimedUserId: input.userId }, + select: defaultUserClaimSelect, + orderBy: getOrderObjectArray(input.selectors, input.directions), + }); + + return userClaims; + }), + + add: loggedInProcedure + .input(userIdSchema_forUserClaimCreation) + .mutation(async ({ ctx, input }) => { + const isDuplicateClaim = await ctx.prisma.userClaim.findFirst({ + where: { + claimedBy: ctx.user.id, + claimedUserId: input.claimedUserId, + }, + }); + + if (isDuplicateClaim) { + throw new TRPCError({ + code: "FORBIDDEN", + message: `User claim already exists for user ${input.claimedUserId}`, + }); + } + + + const userClaim = await ctx.prisma.userClaim.create({ + data: { + // use the connection syntax so Prisma checks that foreign keys exist + User_claimedBy: { connect: { id: ctx.user.id } }, + User_claimedUser: { connect: { id: input.claimedUserId } }, + }, + select: defaultUserClaimSelect, + }); + + + return userClaim; + }), + + delete: loggedInProcedure + .input(userClaimIdSchema) + .mutation(async ({ ctx, input }) => { + const userClaimFromId = await getUserClaimById(false, ctx.prisma, input.id); //check that id matches an existing userClaim + + + checkIsPrivileged(ADMIN_PERMISSION_STRINGS, ctx.user, userClaimFromId.claimedBy); //check that the user has permission to delete this userClaim + + + await ctx.prisma.userClaim.delete({ where: { id: input.id } }); + + + return true; + }), + + verify: adminProcedure + .input(userClaimIdSchema) + .mutation(async ({ ctx, input }) => { + const claim = await ctx.prisma.userClaim.findUnique({ + where: { + id: input.id, + } + }); + + if (!claim) { + throw new TRPCError({ + code: "NOT_FOUND", + message: `No user claim exists with id "${input.id}"`, + }); + } + + + const updateUserId: Prisma.PublisherUpdateManyArgs = { + where: { + userId: { + equals: claim.claimedUserId, + }, + }, + data: { + userId: claim.claimedBy, // using the unchecked syntax instead of the connection syntax because updateMany doesn't support the connection syntax. this is safe, as the claiming userId is checked to be valid before this is used. + }, + }; + + const updateSubmittedBy: Prisma.ModUpdateManyArgs = { + where: { + submittedBy: { + equals: claim.claimedUserId, + }, + }, + data: { + submittedBy: claim.claimedBy, // using the unchecked syntax instead of the connection syntax because updateMany doesn't support the connection syntax. this is safe, as the claiming userId is checked to be valid before this is used. + }, + }; + + const updateApprovedBy: Prisma.ModUpdateManyArgs = { + where: { + approvedBy: { + equals: claim.claimedUserId, + }, + }, + data: { + approvedBy: claim.claimedBy, // using the unchecked syntax instead of the connection syntax because updateMany doesn't support the connection syntax. this is safe, as the claiming userId is checked to be valid before this is used. + }, + }; + + const updateMapperUserId: Prisma.MapUpdateManyArgs = { + where: { + mapperUserId: { + equals: claim.claimedUserId, + }, + }, + data: { + mapperUserId: claim.claimedBy, // using the unchecked syntax instead of the connection syntax because updateMany doesn't support the connection syntax. this is safe, as the claiming userId is checked to be valid before this is used. + }, + }; + + + //TODO!!!: make sure nothing was missed here + // A deeply nested request *may* work here, but I'm not sure. It would be more work and harder to read, but would probably be marginally faster. IMO, it's not worth bothering. + await ctx.prisma.$transaction( + async (transactionPrisma) => { // using an interactive transaction to allow verifying that the claiming user still exists before making changes + const claimingUser = await transactionPrisma.user.findUnique({ + where: { + id: claim.claimedBy, + } + }); + + if (!claimingUser) { + throw new TRPCError({ + code: "INTERNAL_SERVER_ERROR", + message: `Invalid userClaim. No user exists with id ${claim.claimedBy}, which is the claimedBy user of this userClaim. Please contact an admin.`, + }); + } + + + const promises = [ + transactionPrisma.publisher.updateMany(updateUserId), + + transactionPrisma.mod.updateMany(updateSubmittedBy), + transactionPrisma.mod.updateMany(updateApprovedBy), + + transactionPrisma.map.updateMany(updateMapperUserId), + transactionPrisma.map.updateMany(updateSubmittedBy as Prisma.MapUpdateManyArgs), + transactionPrisma.map.updateMany(updateApprovedBy as Prisma.MapUpdateManyArgs), + + transactionPrisma.mod_Archive.updateMany(updateSubmittedBy as Prisma.Mod_ArchiveUpdateManyArgs), + transactionPrisma.mod_Archive.updateMany(updateApprovedBy as Prisma.Mod_ArchiveUpdateManyArgs), + + transactionPrisma.map_Archive.updateMany(updateMapperUserId as Prisma.Map_ArchiveUpdateManyArgs), + transactionPrisma.map_Archive.updateMany(updateSubmittedBy as Prisma.Map_ArchiveUpdateManyArgs), + transactionPrisma.map_Archive.updateMany(updateApprovedBy as Prisma.Map_ArchiveUpdateManyArgs), + + transactionPrisma.mod_Edit.updateMany(updateSubmittedBy as Prisma.Mod_EditUpdateManyArgs), + transactionPrisma.map_Edit.updateMany(updateMapperUserId as Prisma.Map_EditUpdateManyArgs), + transactionPrisma.map_Edit.updateMany(updateSubmittedBy as Prisma.Map_EditUpdateManyArgs), + + transactionPrisma.mod_New.updateMany(updateSubmittedBy as Prisma.Mod_NewUpdateManyArgs), + + transactionPrisma.map_NewWithMod_New.updateMany(updateMapperUserId as Prisma.Map_NewWithMod_NewUpdateManyArgs), + transactionPrisma.map_NewWithMod_New.updateMany(updateSubmittedBy as Prisma.Map_NewWithMod_NewUpdateManyArgs), + + transactionPrisma.map_NewSolo.updateMany(updateMapperUserId as Prisma.Map_NewSoloUpdateManyArgs), + transactionPrisma.map_NewSolo.updateMany(updateSubmittedBy as Prisma.Map_NewSoloUpdateManyArgs), + + transactionPrisma.rating.updateMany(updateSubmittedBy as Prisma.RatingUpdateManyArgs), + + transactionPrisma.reviewCollection.updateMany(updateUserId as Prisma.ReviewCollectionUpdateManyArgs), + + transactionPrisma.usersToCompletedMaps.updateMany(updateUserId as Prisma.UsersToCompletedMapsUpdateManyArgs), + ]; + + await Promise.all(promises); + + + // delete the user after all the other updates have been made, so that a race condition where the delete partially cascades to updating values doesn't occur + // this delete should cascade to the userClaim as well (TODO!!!: confirm this and update this comment accordingly or add a deletion of the userClaim) + await ctx.prisma.user.delete({ + where: { + id: claim.claimedUserId, + } + }); + } + ); + + + return true; + }), +}); \ No newline at end of file From 34002f5fe613fd6a8b8a435f78c6721404863196 Mon Sep 17 00:00:00 2001 From: otobot1 Date: Sun, 15 Sep 2024 15:13:24 -0600 Subject: [PATCH 13/43] Update userClaim.ts -add check for claimedUser to verification endpoint --- .../api/routers/user_userClaim/userClaim.ts | 18 ++++++++++++++++-- 1 file changed, 16 insertions(+), 2 deletions(-) diff --git a/src/server/api/routers/user_userClaim/userClaim.ts b/src/server/api/routers/user_userClaim/userClaim.ts index 682487c9..85aafd9d 100644 --- a/src/server/api/routers/user_userClaim/userClaim.ts +++ b/src/server/api/routers/user_userClaim/userClaim.ts @@ -278,13 +278,13 @@ export const userClaimRouter = createTRPCRouter({ // A deeply nested request *may* work here, but I'm not sure. It would be more work and harder to read, but would probably be marginally faster. IMO, it's not worth bothering. await ctx.prisma.$transaction( async (transactionPrisma) => { // using an interactive transaction to allow verifying that the claiming user still exists before making changes - const claimingUser = await transactionPrisma.user.findUnique({ + const claimedByUser = await transactionPrisma.user.findUnique({ where: { id: claim.claimedBy, } }); - if (!claimingUser) { + if (!claimedByUser) { throw new TRPCError({ code: "INTERNAL_SERVER_ERROR", message: `Invalid userClaim. No user exists with id ${claim.claimedBy}, which is the claimedBy user of this userClaim. Please contact an admin.`, @@ -292,6 +292,20 @@ export const userClaimRouter = createTRPCRouter({ } + const claimedUser = await transactionPrisma.user.findUnique({ + where: { + id: claim.claimedUserId, + } + }); + + if (!claimedUser) { + throw new TRPCError({ + code: "INTERNAL_SERVER_ERROR", + message: `Invalid userClaim. No user exists with id ${claim.claimedUserId}, which is the claimed user of this userClaim. Please contact an admin.`, + }); + } + + const promises = [ transactionPrisma.publisher.updateMany(updateUserId), From 0aed77a375e160138c0d38e917d400fbf17349a9 Mon Sep 17 00:00:00 2001 From: otobot1 Date: Sun, 15 Sep 2024 16:14:53 -0600 Subject: [PATCH 14/43] create new baseline migration --- prisma/migrations/0_init/migration.sql | 23 ++++++++++++++++- .../migration.sql | 25 ------------------- 2 files changed, 22 insertions(+), 26 deletions(-) delete mode 100644 prisma/migrations/20240902061032_add_user_claims/migration.sql diff --git a/prisma/migrations/0_init/migration.sql b/prisma/migrations/0_init/migration.sql index 29f2f7ab..90ca617e 100644 --- a/prisma/migrations/0_init/migration.sql +++ b/prisma/migrations/0_init/migration.sql @@ -477,6 +477,18 @@ CREATE TABLE `users-to-completed-maps` ( PRIMARY KEY (`userId`, `mapId`) ) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; +-- CreateTable +CREATE TABLE `user-claim` ( + `id` MEDIUMINT UNSIGNED NOT NULL AUTO_INCREMENT, + `claimedBy` VARCHAR(191) NOT NULL, + `claimedUserId` VARCHAR(191) NOT NULL, + `approvedBy` VARCHAR(191) NULL, + + INDEX `user-claim_claimedBy_idx`(`claimedBy`), + UNIQUE INDEX `user-claim_claimedBy_claimedUserId_key`(`claimedBy`, `claimedUserId`), + PRIMARY KEY (`id`) +) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; + -- CreateTable CREATE TABLE `user` ( `id` VARCHAR(191) NOT NULL, @@ -520,6 +532,7 @@ CREATE TABLE `session` ( `userId` VARCHAR(191) NOT NULL, `expires` DATETIME(3) NOT NULL, + UNIQUE INDEX `session_sessionToken_key`(`sessionToken`), PRIMARY KEY (`id`) ) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; @@ -752,9 +765,17 @@ ALTER TABLE `users-to-completed-maps` ADD CONSTRAINT `users-to-completed-maps_us -- AddForeignKey ALTER TABLE `users-to-completed-maps` ADD CONSTRAINT `users-to-completed-maps_mapId_fkey` FOREIGN KEY (`mapId`) REFERENCES `map`(`id`) ON DELETE CASCADE ON UPDATE RESTRICT; +-- AddForeignKey +ALTER TABLE `user-claim` ADD CONSTRAINT `user-claim_claimedBy_fkey` FOREIGN KEY (`claimedBy`) REFERENCES `user`(`id`) ON DELETE CASCADE ON UPDATE RESTRICT; + +-- AddForeignKey +ALTER TABLE `user-claim` ADD CONSTRAINT `user-claim_claimedUserId_fkey` FOREIGN KEY (`claimedUserId`) REFERENCES `user`(`id`) ON DELETE CASCADE ON UPDATE RESTRICT; + +-- AddForeignKey +ALTER TABLE `user-claim` ADD CONSTRAINT `user-claim_approvedBy_fkey` FOREIGN KEY (`approvedBy`) REFERENCES `user`(`id`) ON DELETE CASCADE ON UPDATE RESTRICT; + -- AddForeignKey ALTER TABLE `account` ADD CONSTRAINT `account_userId_fkey` FOREIGN KEY (`userId`) REFERENCES `user`(`id`) ON DELETE CASCADE ON UPDATE RESTRICT; -- AddForeignKey ALTER TABLE `session` ADD CONSTRAINT `session_userId_fkey` FOREIGN KEY (`userId`) REFERENCES `user`(`id`) ON DELETE CASCADE ON UPDATE RESTRICT; - diff --git a/prisma/migrations/20240902061032_add_user_claims/migration.sql b/prisma/migrations/20240902061032_add_user_claims/migration.sql deleted file mode 100644 index 8add972f..00000000 --- a/prisma/migrations/20240902061032_add_user_claims/migration.sql +++ /dev/null @@ -1,25 +0,0 @@ -/* - Warnings: - - - A unique constraint covering the columns `[sessionToken]` on the table `session` will be added. If there are existing duplicate values, this will fail. - -*/ --- CreateTable -CREATE TABLE `user-claim` ( - `claimByUserId` VARCHAR(191) NOT NULL, - `claimForUserId` VARCHAR(191) NOT NULL, - `id` MEDIUMINT UNSIGNED NOT NULL AUTO_INCREMENT, - - INDEX `user-claim_claimByUserId_idx`(`claimByUserId`), - UNIQUE INDEX `user-claim_claimByUserId_claimForUserId_key`(`claimByUserId`, `claimForUserId`), - PRIMARY KEY (`id`) -) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; - --- CreateIndex -CREATE UNIQUE INDEX `session_sessionToken_key` ON `session`(`sessionToken`); - --- AddForeignKey -ALTER TABLE `user-claim` ADD CONSTRAINT `user-claim_claimByUserId_fkey` FOREIGN KEY (`claimByUserId`) REFERENCES `user`(`id`) ON DELETE CASCADE ON UPDATE RESTRICT; - --- AddForeignKey -ALTER TABLE `user-claim` ADD CONSTRAINT `user-claim_claimForUserId_fkey` FOREIGN KEY (`claimForUserId`) REFERENCES `user`(`id`) ON DELETE CASCADE ON UPDATE RESTRICT; From 3fb089d146d0475cf5edaa8c43b9e76027a12433 Mon Sep 17 00:00:00 2001 From: otobot1 Date: Fri, 27 Sep 2024 16:26:32 -0600 Subject: [PATCH 15/43] show "claim user" message to all users --- src/pages/index.tsx | 40 +++++++++++++++++++++++++++++++++------- 1 file changed, 33 insertions(+), 7 deletions(-) diff --git a/src/pages/index.tsx b/src/pages/index.tsx index 0b4c06ed..716ca3da 100644 --- a/src/pages/index.tsx +++ b/src/pages/index.tsx @@ -1,10 +1,10 @@ import { createStyles, Flex, ScrollArea } from "@mantine/core"; import { type NextPage } from "next"; -import { useSession } from "next-auth/react"; +import { signIn, useSession } from "next-auth/react"; import Image from "next/image"; import Link from "next/link"; import { Layout } from "~/components/layout/layout"; -import { MODS_PAGE_PATHNAME } from "~/consts/pathnames"; +import { CLAIM_USER_PATHNAME, MODS_PAGE_PATHNAME } from "~/consts/pathnames"; import { pageContentHeightPixels } from "~/styles/pageContentHeightPixels"; @@ -50,11 +50,39 @@ const useStyles = createStyles( +type ClaimUserLinkProps = { + isAuthenticated: boolean; + classes: ReturnType["classes"]; +}; + + +const ClaimUserLink = ({ + isAuthenticated, + classes, +}: ClaimUserLinkProps) => { + const linkText = "CLAIM YOUR OLD USER"; + + + if (isAuthenticated) { + return {linkText}; + } + + + return signIn("discord", { callbackUrl: CLAIM_USER_PATHNAME })}>{linkText}; +}; + + + + const Home: NextPage = () => { const { classes } = useStyles(); const height = 280; const width = height / 577 * 867; + + const { status } = useSession(); + const isAuthenticated = status === "authenticated"; + return ( {

CML Public Beta

Welcome! The site is currently in early beta.

For now, mods can only be browsed.

- {status === "authenticated" && ( -

- If you submitted ratings via the google form, PLEASE CLAIM YOUR OLD USER instead of submitting duplicate ratings for the same maps! -

- )} +

+ If you submitted ratings via the google form, PLEASE instead of submitting duplicate ratings for the same maps! +

Community Projects

Celeste Mountain Lego Idea

Date: Fri, 27 Sep 2024 17:46:53 -0600 Subject: [PATCH 16/43] Update /claim-user to work with API changes Also make minor tweaks to wording --- src/pages/claim-user/index.tsx | 178 +++++++++++------- .../api/routers/user_userClaim/userClaim.ts | 55 +++--- 2 files changed, 139 insertions(+), 94 deletions(-) diff --git a/src/pages/claim-user/index.tsx b/src/pages/claim-user/index.tsx index fa0e89fc..12582b0b 100644 --- a/src/pages/claim-user/index.tsx +++ b/src/pages/claim-user/index.tsx @@ -8,6 +8,15 @@ import { CLAIM_USER_PATHNAME } from "~/consts/pathnames"; import { pageContentHeightPixels } from "~/styles/pageContentHeightPixels"; import { api } from "~/utils/api"; + + + +const PAGE_TITLE = "Claim User"; +const PAGE_DESCRIPTION = "Submit a claim for a legacy user."; + + + + const useStyles = createStyles( (theme) => ({ scrollArea: { @@ -20,96 +29,131 @@ const useStyles = createStyles( }), ); -const ClaimUser : NextPage = () => { - const { status } = useSession(); - const utils = api.useUtils(); + + +const ClaimUser: NextPage = () => { + const { status, data: sessionData } = useSession(); + const userId = sessionData?.user.id ?? ""; + + + // Get all unlinked legacy users const unlinkedUsersQuery = api.user.getUnlinked.useQuery({}, { queryKey: ["user.getUnlinked", {}] }); const unlinkedUsers = unlinkedUsersQuery.data ?? []; - const claimsQuery = api.user.getUserClaims.useQuery(undefined, { queryKey: ["user.getUserClaims", undefined] }); - const claims = claimsQuery.data ?? []; - const unclaimedUsers = unlinkedUsers.filter(user => !claims.find(claim => claim.claimForUserId === user.id)); - const createUserClaimMutation = api.user.createUserClaim.useMutation({ + + // Get all claims made by the current user + const userClaimsQuery = api.user.userClaim.getByClaimingUserId.useQuery({ userId }, { queryKey: ["user.userClaim.getByClaimingUserId", { userId }] }); + const userClaims = userClaimsQuery.data ?? []; + + + // Separate legacy users based on if the current user has claimed them + type UnlinkedUser = typeof unlinkedUsers[number]; + const claimedUsers: UnlinkedUser[] = []; + const unclaimedUsers: UnlinkedUser[] = []; + + for (const unlinkedUser of unlinkedUsers) { + const matchingUserClaim = userClaims.find(claim => claim.claimedUserId === unlinkedUser.id); + + if (matchingUserClaim) { + claimedUsers.push(unlinkedUser); + } else { + unclaimedUsers.push(unlinkedUser); + } + } + + + const utils = api.useUtils(); + + const createUserClaimMutation = api.user.userClaim.add.useMutation({ onSuccess() { - void utils.user.getUserClaims.invalidate(); + void utils.user.userClaim.getByClaimingUserId.invalidate(); } }); + const { classes } = useStyles(); + if (status === 'unauthenticated') { return ( Login to claim users. - ); + ); } + return ( - -

Claim user

-

Claimed users

-

- Contact us on Discord to get your claim verified. -

- - - - - - - - - - {claims.map(claim => ( - - - - + +

{PAGE_TITLE}

+

Claimed Users

+

+ Contact us on Discord to get your claim verified. +

+
Claim IDUser IDUsername
{claim.id}{claim.claimForUserId}{claim.User_UserClaim_claimFor.discordUsername}#{claim.User_UserClaim_claimFor.discordDiscriminator}
+ + + + + - ))} - -
Claim IDUser IDUsername
-

Users available

- - - - - - - - - {unclaimedUsers.map(user => ( - - - + + + {userClaims.map(claim => ( + + + + + + ))} + +
User
{user.discordUsername}#{user.discordDiscriminator} - -
{claim.id}{claim.claimedUserId}{claim.User_claimedUser.discordUsername}#{claim.User_claimedUser.discordDiscriminator}
+

Unclaimed Users

+ + + + + - ))} - -
User
-
+ +
{unclaimedUser.discordUsername}#{unclaimedUser.discordDiscriminator} + +
+
- ); -} + ); +}; export default ClaimUser; \ No newline at end of file diff --git a/src/server/api/routers/user_userClaim/userClaim.ts b/src/server/api/routers/user_userClaim/userClaim.ts index 85aafd9d..e8b5f97e 100644 --- a/src/server/api/routers/user_userClaim/userClaim.ts +++ b/src/server/api/routers/user_userClaim/userClaim.ts @@ -2,18 +2,26 @@ import { z } from "zod"; import { createTRPCRouter, adminProcedure, loggedInProcedure } from "~/server/api/trpc"; import { TRPCError } from "@trpc/server"; import { MyPrismaClient } from "~/server/prisma"; -import { Prisma, UserClaim } from "@prisma/client"; +import { Prisma, UserClaim as PrismaUserClaim } from "@prisma/client"; import { getCombinedSchema, getOrderObjectArray } from "~/server/api/utils/sortOrderHelpers"; import { getNonEmptyArray } from "~/utils/getNonEmptyArray"; import { INT_MAX_SIZES } from "~/consts/integerSizes"; import { userIdSchema_NonObject } from "./user"; -import { IfElse } from "~/utils/typeHelpers"; import { ADMIN_PERMISSION_STRINGS, checkIsPrivileged, checkPermissions } from "../../utils/permissions"; -type TrimmedUserClaim = Omit & { +type ExpandedUserClaim = PrismaUserClaim & { + User_claimedUser: { + id: string; + discordUsername: string | null; + discordDiscriminator: string | null; + }; +}; + + +type TrimmedUserClaim = Omit & { approvedBy: undefined; }; @@ -25,6 +33,13 @@ export const defaultUserClaimSelect = Prisma.validator() claimedBy: true, claimedUserId: true, approvedBy: true, + User_claimedUser: { + select: { + id: true, + discordUsername: true, + discordDiscriminator: true, + }, + }, }); @@ -54,28 +69,14 @@ const userClaimOrderSchema = getCombinedSchema( -const getUserClaimById = async< - ReturnAll extends boolean, - Union extends IfElse, - ReturnType extends NonNullable ->( - returnAll: ReturnAll, +const getUserClaimById = async( prisma: MyPrismaClient, id: number -): Promise => { - let userClaim: Union | null; - - if (returnAll) { - userClaim = await prisma.userClaim.findUnique({ //having type declaration here AND in function signature is safer - where: { id: id }, - }) as Union; // this is safe because userClaim is checked to be non-null in the next block. this cast is required to make TypeScript happy. - - } else { - userClaim = await prisma.userClaim.findUnique({ //having type declaration here AND in function signature is safer - where: { id: id }, - select: defaultUserClaimSelect, - }) as Union; // this is safe because userClaim is checked to be non-null in the next block. this cast is required to make TypeScript happy. - } +): Promise => { + const userClaim = await prisma.userClaim.findUnique({ + where: { id: id }, + select: defaultUserClaimSelect, + }); if (!userClaim) { @@ -86,13 +87,13 @@ const getUserClaimById = async< } - return userClaim as unknown as ReturnType; // this cast is required to make TypeScript happy. + return userClaim; }; -const getTrimmedUserClaim = (userClaim: UserClaim): TrimmedUserClaim => ({ +const getTrimmedUserClaim = (userClaim: ExpandedUserClaim): TrimmedUserClaim => ({ ...userClaim, approvedBy: undefined, }); @@ -113,7 +114,7 @@ export const userClaimRouter = createTRPCRouter({ getById: loggedInProcedure .input(userClaimIdSchema) .query(async ({ ctx, input }) => { - const userClaim = await getUserClaimById(true, ctx.prisma, input.id); + const userClaim = await getUserClaimById(ctx.prisma, input.id); const isAdmin = checkPermissions(ADMIN_PERMISSION_STRINGS, ctx.user.permissions); @@ -200,7 +201,7 @@ export const userClaimRouter = createTRPCRouter({ delete: loggedInProcedure .input(userClaimIdSchema) .mutation(async ({ ctx, input }) => { - const userClaimFromId = await getUserClaimById(false, ctx.prisma, input.id); //check that id matches an existing userClaim + const userClaimFromId = await getUserClaimById(ctx.prisma, input.id); //check that id matches an existing userClaim checkIsPrivileged(ADMIN_PERMISSION_STRINGS, ctx.user, userClaimFromId.claimedBy); //check that the user has permission to delete this userClaim From c1b21e9e9a750509cafc3374401af989a67d2ea9 Mon Sep 17 00:00:00 2001 From: otobot1 Date: Fri, 27 Sep 2024 19:22:52 -0600 Subject: [PATCH 17/43] increase query invalidation specificity -update comment --- src/pages/claim-user/index.tsx | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/pages/claim-user/index.tsx b/src/pages/claim-user/index.tsx index 12582b0b..0bbb9b14 100644 --- a/src/pages/claim-user/index.tsx +++ b/src/pages/claim-user/index.tsx @@ -47,8 +47,9 @@ const ClaimUser: NextPage = () => { const userClaims = userClaimsQuery.data ?? []; - // Separate legacy users based on if the current user has claimed them + // Sort legacy users based on if the current user has claimed them type UnlinkedUser = typeof unlinkedUsers[number]; + const claimedUsers: UnlinkedUser[] = []; const unclaimedUsers: UnlinkedUser[] = []; @@ -67,7 +68,7 @@ const ClaimUser: NextPage = () => { const createUserClaimMutation = api.user.userClaim.add.useMutation({ onSuccess() { - void utils.user.userClaim.getByClaimingUserId.invalidate(); + utils.user.userClaim.getByClaimingUserId.invalidate({ userId }); } }); From a1877716154073a1e4d88736076ff14c04a4cd92 Mon Sep 17 00:00:00 2001 From: otobot1 Date: Fri, 27 Sep 2024 19:56:00 -0600 Subject: [PATCH 18/43] prefer import syntax: `type {...}` over `{type ...}` -i read somewhere that it's better and found it convincing at the time, but i can't recall why. i'll add a comment to this PR (#861) if i ever remember --- src/pages/claim-user/index.tsx | 2 +- src/pages/claim-user/verify.tsx | 2 +- src/pages/index.tsx | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/pages/claim-user/index.tsx b/src/pages/claim-user/index.tsx index 0bbb9b14..0af4da8c 100644 --- a/src/pages/claim-user/index.tsx +++ b/src/pages/claim-user/index.tsx @@ -1,5 +1,5 @@ import { Button, createStyles, ScrollArea, Table } from "@mantine/core"; -import { type NextPage } from "next"; +import type { NextPage } from "next"; import { useSession } from "next-auth/react"; import Link from "next/link"; import { Layout } from "~/components/layout/layout"; diff --git a/src/pages/claim-user/verify.tsx b/src/pages/claim-user/verify.tsx index 5ec297d8..a1199929 100644 --- a/src/pages/claim-user/verify.tsx +++ b/src/pages/claim-user/verify.tsx @@ -1,5 +1,5 @@ import { Button, createStyles, Modal, ScrollArea, Stack, Table } from "@mantine/core"; -import { type NextPage } from "next"; +import type { NextPage } from "next"; import { useSession } from "next-auth/react"; import { useState } from "react"; import { Layout } from "~/components/layout/layout"; diff --git a/src/pages/index.tsx b/src/pages/index.tsx index 716ca3da..0305e4df 100644 --- a/src/pages/index.tsx +++ b/src/pages/index.tsx @@ -1,5 +1,5 @@ import { createStyles, Flex, ScrollArea } from "@mantine/core"; -import { type NextPage } from "next"; +import type { NextPage } from "next"; import { signIn, useSession } from "next-auth/react"; import Image from "next/image"; import Link from "next/link"; From 119c1bcabcad2f1b9d080140a0fef95b301d500b Mon Sep 17 00:00:00 2001 From: otobot1 Date: Fri, 27 Sep 2024 19:59:44 -0600 Subject: [PATCH 19/43] refactoring, formatting, and referencing constants --- src/pages/claim-user/verify.tsx | 137 ++++++++++++++++------------ src/server/api/utils/permissions.ts | 1 - src/utils/permissions.ts | 20 ++++ 3 files changed, 100 insertions(+), 58 deletions(-) create mode 100644 src/utils/permissions.ts diff --git a/src/pages/claim-user/verify.tsx b/src/pages/claim-user/verify.tsx index a1199929..800d5885 100644 --- a/src/pages/claim-user/verify.tsx +++ b/src/pages/claim-user/verify.tsx @@ -4,9 +4,20 @@ import { useSession } from "next-auth/react"; import { useState } from "react"; import { Layout } from "~/components/layout/layout"; import { VERIFY_CLAIM_PATHNAME } from "~/consts/pathnames"; +import { ADMIN_PERMISSION_STRINGS } from "~/server/api/utils/permissions"; +import { isPermitted } from "~/utils/permissions"; import { pageContentHeightPixels } from "~/styles/pageContentHeightPixels"; import { api } from "~/utils/api"; + + + +const PAGE_TITLE = "Verify Claim"; +const PAGE_DESCRIPTION = "Verify legacy user claims."; + + + + const useStyles = createStyles( (theme) => ({ scrollArea: { @@ -16,58 +27,70 @@ const useStyles = createStyles( }), ); -const VerifyClaim : NextPage = () => { - const { data: session, status } = useSession(); - const utils = api.useUtils(); - const userClaimsQuery = api.user.getAllUserClaims.useQuery(); + + +const VerifyClaim: NextPage = () => { + const { status, data: sessionData } = useSession(); + + + const userClaimsQuery = api.user.userClaim.getAll.useQuery({}, { queryKey: ["user.userClaim.getAll", {}] }); const userClaims = userClaimsQuery.data ?? []; - const verifyUserClaimMutation = api.user.verifyUserClaim.useMutation({ + + const [claimToVerify, setClaimToVerify] = useState<{ + id: number, + claimedBy: string, + claimedUserId: string; + } | null>(null); + + + const utils = api.useUtils(); + + const verifyUserClaimMutation = api.user.userClaim.verify.useMutation({ async onSuccess() { await Promise.all([ - utils.user.getAllUserClaims.invalidate(), - utils.user.getUserClaims.invalidate() + utils.user.userClaim.invalidate() ]); + setClaimToVerify(null); } }); - const [claimToVerify, setClaimToVerify] = useState<{ id: number, byUser: string, forUser: string } | null>(null); const { classes } = useStyles(); - if (status === 'loading') { + if (status === "loading") { return ( <> - ); + ); } - else if (session === null || !(session.user.permissions.find(p => p === 'Admin' || p === 'Super_Admin'))) { + else if (sessionData === null || !isPermitted(sessionData.user.permissions, ADMIN_PERMISSION_STRINGS)) { return ( Login as an admin/superadmin to verify users. - ); + ); } return ( { setClaimToVerify(null); }} title="Confirmation" centered> - { claimToVerify && ( + {claimToVerify && (

Are you sure you want to verify claim {claimToVerify.id} by {claimToVerify.byUser} for {claimToVerify.forUser}? @@ -78,43 +101,43 @@ const VerifyClaim : NextPage = () => { )} - -

User claims

- - - - - - - - - - - {userClaims.map(claim => { - const byUser = `${claim.User_UserClaim_claimBy.discordUsername}#${claim.User_UserClaim_claimBy.discordDiscriminator}`; - const forUser = `${claim.User_UserClaim_claimFor.discordUsername}#${claim.User_UserClaim_claimFor.discordDiscriminator}`; - return ( - - - - - - - ) - })} - -
Claim IDByFor
{claim.id}{byUser}{forUser}
- + +

User claims

+ + + + + + + + + + + {userClaims.map(claim => { + const byUser = `${claim.User_UserClaim_claimBy.discordUsername}#${claim.User_UserClaim_claimBy.discordDiscriminator}`; + const forUser = `${claim.User_UserClaim_claimFor.discordUsername}#${claim.User_UserClaim_claimFor.discordDiscriminator}`; + return ( + + + + + + + ); + })} + +
Claim IDByFor
{claim.id}{byUser}{forUser}
+
- ); -} + ); +}; export default VerifyClaim; \ No newline at end of file diff --git a/src/server/api/utils/permissions.ts b/src/server/api/utils/permissions.ts index 1a4559e7..401be3d7 100644 --- a/src/server/api/utils/permissions.ts +++ b/src/server/api/utils/permissions.ts @@ -1,4 +1,3 @@ -import { Prisma } from "@prisma/client"; import { TRPCError } from "@trpc/server"; import { SessionUser } from "next-auth"; diff --git a/src/utils/permissions.ts b/src/utils/permissions.ts new file mode 100644 index 00000000..4669f8a5 --- /dev/null +++ b/src/utils/permissions.ts @@ -0,0 +1,20 @@ +import type { Permission } from "../server/api/utils/permissions"; + + + + +export const isPermitted = (userPermissions: Permission[], validPermissionsArray: readonly Permission[]): boolean => { + if (!userPermissions.length) return false; + + + for (const validPermission of validPermissionsArray) { + for (const userPermission of userPermissions) { + if (userPermission === validPermission) { + return true; + } + } + } + + + return false; +}; \ No newline at end of file From 39e1cfff79fb0efdf8a9b7dffbc6f3af89aa620e Mon Sep 17 00:00:00 2001 From: otobot1 Date: Fri, 27 Sep 2024 21:22:05 -0600 Subject: [PATCH 20/43] Update schema.prisma change all `onUpdate: Restrict` to `onUpdate: Cascade` --- prisma/schema.prisma | 156 +++++++++++++++++++++---------------------- 1 file changed, 78 insertions(+), 78 deletions(-) diff --git a/prisma/schema.prisma b/prisma/schema.prisma index 8518ec25..b4321b1e 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -26,7 +26,7 @@ model Difficulty { id Int @id @default(autoincrement()) @db.UnsignedSmallInt name String @db.VarChar(50) description String? @db.VarChar(100) - ParentDifficulty Difficulty? @relation("DifficultyToDifficulty", fields: [parentDifficultyId], references: [id], onDelete: Cascade, onUpdate: Restrict) + ParentDifficulty Difficulty? @relation("DifficultyToDifficulty", fields: [parentDifficultyId], references: [id], onDelete: Cascade, onUpdate: Cascade) parentDifficultyId Int? @default(0) @db.UnsignedSmallInt order Int @db.UnsignedTinyInt //1 is the easiest, higher numbers are harder ChildDifficulty Difficulty[] @relation("DifficultyToDifficulty") @@ -48,7 +48,7 @@ model Tech { id Int @id @default(autoincrement()) @db.UnsignedSmallInt name String @unique @db.VarChar(50) description String? @db.VarChar(150) - Difficulty Difficulty @relation(fields: [difficultyId], references: [id], onDelete: Restrict, onUpdate: Restrict) + Difficulty Difficulty @relation(fields: [difficultyId], references: [id], onDelete: Restrict, onUpdate: Cascade) difficultyId Int @db.UnsignedSmallInt //must be a parent difficulty TechVideo TechVideo[] MapToTechs MapToTechs[] @@ -63,7 +63,7 @@ model Tech { model TechVideo { id Int @id @default(autoincrement()) @db.UnsignedSmallInt - Tech Tech @relation(fields: [techId], references: [id], onDelete: Cascade, onUpdate: Restrict) + Tech Tech @relation(fields: [techId], references: [id], onDelete: Cascade, onUpdate: Cascade) techId Int @db.UnsignedSmallInt url String @db.VarChar(100) @@ -75,7 +75,7 @@ model Publisher { id Int @id @default(autoincrement()) @db.UnsignedSmallInt gamebananaId Int? @unique @db.UnsignedMediumInt name String @unique @db.VarChar(100) - User User? @relation(fields: [userId], references: [id], onDelete: SetNull, onUpdate: Restrict) + User User? @relation(fields: [userId], references: [id], onDelete: SetNull, onUpdate: Cascade) userId String? Mod Mod[] Mod_Archive Mod_Archive[] @@ -101,7 +101,7 @@ model Mod { id Int @id @default(autoincrement()) @db.UnsignedSmallInt type ModType @default(Normal) name String @db.VarChar(200) - Publisher Publisher @relation(fields: [publisherId], references: [id], onDelete: Cascade, onUpdate: Restrict) + Publisher Publisher @relation(fields: [publisherId], references: [id], onDelete: Cascade, onUpdate: Cascade) publisherId Int @db.UnsignedSmallInt contentWarning Boolean @default(false) notes String? @db.VarChar(500) @@ -109,10 +109,10 @@ model Mod { longDescription String? @db.VarChar(1500) gamebananaModId Int @unique @db.UnsignedMediumInt timeSubmitted Int - User_SubmittedBy User? @relation("Mod_SubmittedByToUser", fields: [submittedBy], references: [id], onDelete: SetNull, onUpdate: Restrict) + User_SubmittedBy User? @relation("Mod_SubmittedByToUser", fields: [submittedBy], references: [id], onDelete: SetNull, onUpdate: Cascade) submittedBy String? timeApproved Int - User_ApprovedBy User? @relation("Mod_ApprovedByToUser", fields: [approvedBy], references: [id], onDelete: SetNull, onUpdate: Restrict) + User_ApprovedBy User? @relation("Mod_ApprovedByToUser", fields: [approvedBy], references: [id], onDelete: SetNull, onUpdate: Cascade) approvedBy String? timeCreatedGamebanana Int Map Map[] @@ -130,9 +130,9 @@ model Mod { } model ModToTags { - Mod Mod @relation(fields: [modId], references: [id], onDelete: Cascade, onUpdate: Restrict) + Mod Mod @relation(fields: [modId], references: [id], onDelete: Cascade, onUpdate: Cascade) modId Int @db.UnsignedSmallInt - Tag Tag @relation(fields: [tagId], references: [id], onDelete: Cascade, onUpdate: Restrict) + Tag Tag @relation(fields: [tagId], references: [id], onDelete: Cascade, onUpdate: Cascade) tagId Int @db.UnsignedTinyInt @@id([modId, tagId]) @@ -142,15 +142,15 @@ model ModToTags { model Map { id Int @id @default(autoincrement()) @db.UnsignedMediumInt - Mod Mod @relation(fields: [modId], references: [id], onDelete: Cascade, onUpdate: Restrict) + Mod Mod @relation(fields: [modId], references: [id], onDelete: Cascade, onUpdate: Cascade) modId Int @db.UnsignedSmallInt - User_MapperUser User? @relation("Map_MapperUserIdToUser", fields: [mapperUserId], references: [id], onDelete: SetNull, onUpdate: Restrict) + User_MapperUser User? @relation("Map_MapperUserIdToUser", fields: [mapperUserId], references: [id], onDelete: SetNull, onUpdate: Cascade) mapperUserId String? mapperNameString String @db.VarChar(50) name String @db.VarChar(200) - Difficulty Difficulty @relation(fields: [canonicalDifficultyId], references: [id], onDelete: Restrict, onUpdate: Restrict) + Difficulty Difficulty @relation(fields: [canonicalDifficultyId], references: [id], onDelete: Restrict, onUpdate: Cascade) canonicalDifficultyId Int @db.UnsignedSmallInt //must be a parent difficulty - Length Length @relation(fields: [lengthId], references: [id], onDelete: Restrict, onUpdate: Restrict) + Length Length @relation(fields: [lengthId], references: [id], onDelete: Restrict, onUpdate: Cascade) lengthId Int @db.UnsignedTinyInt description String? @db.VarChar(500) notes String? @db.VarChar(500) @@ -159,10 +159,10 @@ model Map { overallRank Int? @db.UnsignedTinyInt mapRemovedFromModBool Boolean @default(false) timeSubmitted Int - User_SubmittedBy User? @relation("Map_SubmittedByToUser", fields: [submittedBy], references: [id], onDelete: SetNull, onUpdate: Restrict) + User_SubmittedBy User? @relation("Map_SubmittedByToUser", fields: [submittedBy], references: [id], onDelete: SetNull, onUpdate: Cascade) submittedBy String? timeApproved Int - User_ApprovedBy User? @relation("Map_ApprovedByToUser", fields: [approvedBy], references: [id], onDelete: SetNull, onUpdate: Restrict) + User_ApprovedBy User? @relation("Map_ApprovedByToUser", fields: [approvedBy], references: [id], onDelete: SetNull, onUpdate: Cascade) approvedBy String? MapToTechs MapToTechs[] Rating Rating[] @@ -183,9 +183,9 @@ model Map { } model MapToTechs { - Map Map @relation(fields: [mapId], references: [id], onDelete: Cascade, onUpdate: Restrict) + Map Map @relation(fields: [mapId], references: [id], onDelete: Cascade, onUpdate: Cascade) mapId Int @db.UnsignedMediumInt - Tech Tech @relation(fields: [techId], references: [id], onDelete: Cascade, onUpdate: Restrict) + Tech Tech @relation(fields: [techId], references: [id], onDelete: Cascade, onUpdate: Cascade) techId Int @db.UnsignedSmallInt fullClearOnlyBool Boolean @default(false) @@ -196,11 +196,11 @@ model MapToTechs { model Mod_Archive { id Int @id @default(autoincrement()) @db.UnsignedSmallInt - Mod Mod @relation(fields: [modId], references: [id], onDelete: Cascade, onUpdate: Restrict) + Mod Mod @relation(fields: [modId], references: [id], onDelete: Cascade, onUpdate: Cascade) modId Int @db.UnsignedSmallInt type ModType @default(Normal) name String @db.VarChar(200) - Publisher Publisher @relation(fields: [publisherId], references: [id], onDelete: Cascade, onUpdate: Restrict) + Publisher Publisher @relation(fields: [publisherId], references: [id], onDelete: Cascade, onUpdate: Cascade) publisherId Int @db.UnsignedSmallInt contentWarning Boolean @default(false) notes String? @db.VarChar(500) @@ -209,10 +209,10 @@ model Mod_Archive { gamebananaModId Int @db.UnsignedMediumInt timeCreatedGamebanana Int timeSubmitted Int - User_SubmittedBy User? @relation("Mod_Archive_SubmittedByToUser", fields: [submittedBy], references: [id], onDelete: SetNull, onUpdate: Restrict) + User_SubmittedBy User? @relation("Mod_Archive_SubmittedByToUser", fields: [submittedBy], references: [id], onDelete: SetNull, onUpdate: Cascade) submittedBy String? timeApproved Int - User_ApprovedBy User? @relation("Mod_Archive_ApprovedByToUser", fields: [approvedBy], references: [id], onDelete: SetNull, onUpdate: Restrict) + User_ApprovedBy User? @relation("Mod_Archive_ApprovedByToUser", fields: [approvedBy], references: [id], onDelete: SetNull, onUpdate: Cascade) approvedBy String? timeArchived Int Mod_ArchiveToTags Mod_ArchiveToTags[] @@ -225,9 +225,9 @@ model Mod_Archive { } model Mod_ArchiveToTags { - Mod_Archive Mod_Archive @relation(fields: [mod_ArchiveId], references: [id], onDelete: Cascade, onUpdate: Restrict) + Mod_Archive Mod_Archive @relation(fields: [mod_ArchiveId], references: [id], onDelete: Cascade, onUpdate: Cascade) mod_ArchiveId Int @db.UnsignedSmallInt - Tag Tag @relation(fields: [tagId], references: [id], onDelete: Cascade, onUpdate: Restrict) + Tag Tag @relation(fields: [tagId], references: [id], onDelete: Cascade, onUpdate: Cascade) tagId Int @db.UnsignedTinyInt @@id([mod_ArchiveId, tagId]) @@ -237,15 +237,15 @@ model Mod_ArchiveToTags { model Map_Archive { id Int @id @default(autoincrement()) @db.UnsignedMediumInt - Map Map @relation(fields: [mapId], references: [id], onDelete: Cascade, onUpdate: Restrict) + Map Map @relation(fields: [mapId], references: [id], onDelete: Cascade, onUpdate: Cascade) mapId Int @db.UnsignedMediumInt - User_MapperUser User? @relation("Map_Archive_MapperUserIdToUser", fields: [mapperUserId], references: [id], onDelete: SetNull, onUpdate: Restrict) + User_MapperUser User? @relation("Map_Archive_MapperUserIdToUser", fields: [mapperUserId], references: [id], onDelete: SetNull, onUpdate: Cascade) mapperUserId String? mapperNameString String @db.VarChar(50) name String @db.VarChar(200) - Difficulty Difficulty @relation(fields: [canonicalDifficultyId], references: [id], onDelete: Restrict, onUpdate: Restrict) + Difficulty Difficulty @relation(fields: [canonicalDifficultyId], references: [id], onDelete: Restrict, onUpdate: Cascade) canonicalDifficultyId Int @db.UnsignedSmallInt //must be a parent difficulty - Length Length @relation(fields: [lengthId], references: [id], onDelete: Restrict, onUpdate: Restrict) + Length Length @relation(fields: [lengthId], references: [id], onDelete: Restrict, onUpdate: Cascade) lengthId Int @db.UnsignedTinyInt description String? @db.VarChar(500) notes String? @db.VarChar(500) @@ -254,10 +254,10 @@ model Map_Archive { overallRank Int? @db.UnsignedTinyInt mapRemovedFromModBool Boolean @default(false) timeSubmitted Int - User_SubmittedBy User? @relation("Map_Archive_SubmittedByToUser", fields: [submittedBy], references: [id], onDelete: SetNull, onUpdate: Restrict) + User_SubmittedBy User? @relation("Map_Archive_SubmittedByToUser", fields: [submittedBy], references: [id], onDelete: SetNull, onUpdate: Cascade) submittedBy String? timeApproved Int - User_ApprovedBy User? @relation("Map_Archive_ApprovedByToUser", fields: [approvedBy], references: [id], onDelete: SetNull, onUpdate: Restrict) + User_ApprovedBy User? @relation("Map_Archive_ApprovedByToUser", fields: [approvedBy], references: [id], onDelete: SetNull, onUpdate: Cascade) approvedBy String? timeArchived Int Map_ArchiveToTechs Map_ArchiveToTechs[] @@ -272,9 +272,9 @@ model Map_Archive { } model Map_ArchiveToTechs { - Map_Archive Map_Archive @relation(fields: [map_ArchiveId], references: [id], onDelete: Cascade, onUpdate: Restrict) + Map_Archive Map_Archive @relation(fields: [map_ArchiveId], references: [id], onDelete: Cascade, onUpdate: Cascade) map_ArchiveId Int @db.UnsignedMediumInt - Tech Tech @relation(fields: [techId], references: [id], onDelete: Cascade, onUpdate: Restrict) + Tech Tech @relation(fields: [techId], references: [id], onDelete: Cascade, onUpdate: Cascade) techId Int @db.UnsignedSmallInt fullClearOnlyBool Boolean @default(false) @@ -285,11 +285,11 @@ model Map_ArchiveToTechs { model Mod_Edit { id Int @id @default(autoincrement()) @db.UnsignedSmallInt - Mod Mod @relation(fields: [modId], references: [id], onDelete: Cascade, onUpdate: Restrict) + Mod Mod @relation(fields: [modId], references: [id], onDelete: Cascade, onUpdate: Cascade) modId Int @db.UnsignedSmallInt type ModType @default(Normal) name String @db.VarChar(200) - Publisher Publisher @relation(fields: [publisherId], references: [id], onDelete: Cascade, onUpdate: Restrict) + Publisher Publisher @relation(fields: [publisherId], references: [id], onDelete: Cascade, onUpdate: Cascade) publisherId Int @db.UnsignedSmallInt contentWarning Boolean @default(false) notes String? @db.VarChar(500) @@ -298,7 +298,7 @@ model Mod_Edit { gamebananaModId Int @db.UnsignedMediumInt timeCreatedGamebanana Int timeSubmitted Int - User_SubmittedBy User? @relation(fields: [submittedBy], references: [id], onDelete: SetNull, onUpdate: Restrict) + User_SubmittedBy User? @relation(fields: [submittedBy], references: [id], onDelete: SetNull, onUpdate: Cascade) submittedBy String? Mod_EditToTags Mod_EditToTags[] Mod_NewToTags Mod_NewToTags[] @@ -310,9 +310,9 @@ model Mod_Edit { } model Mod_EditToTags { - Mod_Edit Mod_Edit @relation(fields: [mod_EditId], references: [id], onDelete: Cascade, onUpdate: Restrict) + Mod_Edit Mod_Edit @relation(fields: [mod_EditId], references: [id], onDelete: Cascade, onUpdate: Cascade) mod_EditId Int @db.UnsignedSmallInt - Tag Tag @relation(fields: [tagId], references: [id], onDelete: Cascade, onUpdate: Restrict) + Tag Tag @relation(fields: [tagId], references: [id], onDelete: Cascade, onUpdate: Cascade) tagId Int @db.UnsignedTinyInt @@id([mod_EditId, tagId]) @@ -322,15 +322,15 @@ model Mod_EditToTags { model Map_Edit { id Int @id @default(autoincrement()) @db.UnsignedMediumInt - Map Map @relation(fields: [mapId], references: [id], onDelete: Cascade, onUpdate: Restrict) + Map Map @relation(fields: [mapId], references: [id], onDelete: Cascade, onUpdate: Cascade) mapId Int @db.UnsignedMediumInt - User_MapperUser User? @relation("Map_Edit_MapperUserIdToUser", fields: [mapperUserId], references: [id], onDelete: SetNull, onUpdate: Restrict) + User_MapperUser User? @relation("Map_Edit_MapperUserIdToUser", fields: [mapperUserId], references: [id], onDelete: SetNull, onUpdate: Cascade) mapperUserId String? mapperNameString String @db.VarChar(50) name String @db.VarChar(200) - Difficulty Difficulty @relation(fields: [canonicalDifficultyId], references: [id], onDelete: Restrict, onUpdate: Restrict) + Difficulty Difficulty @relation(fields: [canonicalDifficultyId], references: [id], onDelete: Restrict, onUpdate: Cascade) canonicalDifficultyId Int @db.UnsignedSmallInt //must be a parent difficulty - Length Length @relation(fields: [lengthId], references: [id], onDelete: Restrict, onUpdate: Restrict) + Length Length @relation(fields: [lengthId], references: [id], onDelete: Restrict, onUpdate: Cascade) lengthId Int @db.UnsignedTinyInt description String? @db.VarChar(500) notes String? @db.VarChar(500) @@ -339,7 +339,7 @@ model Map_Edit { overallRank Int? @db.UnsignedTinyInt mapRemovedFromModBool Boolean @default(false) timeSubmitted Int - User_SubmittedBy User? @relation("Map_Edit_SubmittedByToUser", fields: [submittedBy], references: [id], onDelete: SetNull, onUpdate: Restrict) + User_SubmittedBy User? @relation("Map_Edit_SubmittedByToUser", fields: [submittedBy], references: [id], onDelete: SetNull, onUpdate: Cascade) submittedBy String? Map_EditToTechs Map_EditToTechs[] @@ -352,9 +352,9 @@ model Map_Edit { } model Map_EditToTechs { - Map_Edit Map_Edit @relation(fields: [map_editId], references: [id], onDelete: Cascade, onUpdate: Restrict) + Map_Edit Map_Edit @relation(fields: [map_editId], references: [id], onDelete: Cascade, onUpdate: Cascade) map_editId Int @db.UnsignedMediumInt - Tech Tech @relation(fields: [techId], references: [id], onDelete: Cascade, onUpdate: Restrict) + Tech Tech @relation(fields: [techId], references: [id], onDelete: Cascade, onUpdate: Cascade) techId Int @db.UnsignedSmallInt fullClearOnlyBool Boolean @default(false) @@ -367,7 +367,7 @@ model Mod_New { id Int @id @default(autoincrement()) @db.UnsignedSmallInt type ModType @default(Normal) name String @db.VarChar(200) - Publisher Publisher @relation(fields: [publisherId], references: [id], onDelete: Cascade, onUpdate: Restrict) + Publisher Publisher @relation(fields: [publisherId], references: [id], onDelete: Cascade, onUpdate: Cascade) publisherId Int @db.UnsignedSmallInt contentWarning Boolean @default(false) notes String? @db.VarChar(500) @@ -375,7 +375,7 @@ model Mod_New { longDescription String? @db.VarChar(1500) gamebananaModId Int @unique @db.UnsignedMediumInt timeSubmitted Int - User_SubmittedBy User? @relation(fields: [submittedBy], references: [id], onDelete: SetNull, onUpdate: Restrict) + User_SubmittedBy User? @relation(fields: [submittedBy], references: [id], onDelete: SetNull, onUpdate: Cascade) submittedBy String? timeCreatedGamebanana Int Map_NewWithMod_New Map_NewWithMod_New[] @@ -387,9 +387,9 @@ model Mod_New { } model Mod_NewToTags { - Mod_New Mod_Edit @relation(fields: [mod_NewId], references: [id], onDelete: Cascade, onUpdate: Restrict) + Mod_New Mod_Edit @relation(fields: [mod_NewId], references: [id], onDelete: Cascade, onUpdate: Cascade) mod_NewId Int @db.UnsignedSmallInt - Tag Tag @relation(fields: [tagId], references: [id], onDelete: Cascade, onUpdate: Restrict) + Tag Tag @relation(fields: [tagId], references: [id], onDelete: Cascade, onUpdate: Cascade) tagId Int @db.UnsignedTinyInt @@id([mod_NewId, tagId]) @@ -399,15 +399,15 @@ model Mod_NewToTags { model Map_NewWithMod_New { id Int @id @default(autoincrement()) @db.UnsignedMediumInt - Mod_New Mod_New @relation(fields: [mod_NewId], references: [id], onDelete: Cascade, onUpdate: Restrict) + Mod_New Mod_New @relation(fields: [mod_NewId], references: [id], onDelete: Cascade, onUpdate: Cascade) mod_NewId Int @db.UnsignedSmallInt - User_MapperUser User? @relation("Map_NewWithMod_New_MapperUserIdToUser", fields: [mapperUserId], references: [id], onDelete: SetNull, onUpdate: Restrict) + User_MapperUser User? @relation("Map_NewWithMod_New_MapperUserIdToUser", fields: [mapperUserId], references: [id], onDelete: SetNull, onUpdate: Cascade) mapperUserId String? mapperNameString String @db.VarChar(50) name String @db.VarChar(200) - Difficulty Difficulty @relation(fields: [canonicalDifficultyId], references: [id], onDelete: Restrict, onUpdate: Restrict) + Difficulty Difficulty @relation(fields: [canonicalDifficultyId], references: [id], onDelete: Restrict, onUpdate: Cascade) canonicalDifficultyId Int @db.UnsignedSmallInt //must be a parent difficulty - Length Length @relation(fields: [lengthId], references: [id], onDelete: Restrict, onUpdate: Restrict) + Length Length @relation(fields: [lengthId], references: [id], onDelete: Restrict, onUpdate: Cascade) lengthId Int @db.UnsignedTinyInt description String? @db.VarChar(500) notes String? @db.VarChar(500) @@ -416,7 +416,7 @@ model Map_NewWithMod_New { overallRank Int? @db.UnsignedTinyInt mapRemovedFromModBool Boolean @default(false) timeSubmitted Int - User_SubmittedBy User? @relation("Map_NewWithMod_New_SubmittedByToUser", fields: [submittedBy], references: [id], onDelete: SetNull, onUpdate: Restrict) + User_SubmittedBy User? @relation("Map_NewWithMod_New_SubmittedByToUser", fields: [submittedBy], references: [id], onDelete: SetNull, onUpdate: Cascade) submittedBy String? Map_NewWithMod_NewToTechs Map_NewWithMod_NewToTechs[] @@ -431,9 +431,9 @@ model Map_NewWithMod_New { } model Map_NewWithMod_NewToTechs { - Map_NewWithMod_New Map_NewWithMod_New @relation(fields: [map_NewWithMod_NewId], references: [id], onDelete: Cascade, onUpdate: Restrict) + Map_NewWithMod_New Map_NewWithMod_New @relation(fields: [map_NewWithMod_NewId], references: [id], onDelete: Cascade, onUpdate: Cascade) map_NewWithMod_NewId Int @db.UnsignedMediumInt - Tech Tech @relation(fields: [techId], references: [id], onDelete: Cascade, onUpdate: Restrict) + Tech Tech @relation(fields: [techId], references: [id], onDelete: Cascade, onUpdate: Cascade) techId Int @db.UnsignedSmallInt fullClearOnlyBool Boolean @default(false) @@ -444,15 +444,15 @@ model Map_NewWithMod_NewToTechs { model Map_NewSolo { id Int @id @default(autoincrement()) @db.UnsignedMediumInt - Mod Mod @relation(fields: [modId], references: [id], onDelete: Cascade, onUpdate: Restrict) + Mod Mod @relation(fields: [modId], references: [id], onDelete: Cascade, onUpdate: Cascade) modId Int @db.UnsignedSmallInt - User_MapperUser User? @relation("Map_NewSolo_MapperUserIdToUser", fields: [mapperUserId], references: [id], onDelete: SetNull, onUpdate: Restrict) + User_MapperUser User? @relation("Map_NewSolo_MapperUserIdToUser", fields: [mapperUserId], references: [id], onDelete: SetNull, onUpdate: Cascade) mapperUserId String? mapperNameString String @db.VarChar(50) name String @db.VarChar(200) - Difficulty Difficulty @relation(fields: [canonicalDifficultyId], references: [id], onDelete: Restrict, onUpdate: Restrict) + Difficulty Difficulty @relation(fields: [canonicalDifficultyId], references: [id], onDelete: Restrict, onUpdate: Cascade) canonicalDifficultyId Int @db.UnsignedSmallInt //must be a parent difficulty - Length Length @relation(fields: [lengthId], references: [id], onDelete: Restrict, onUpdate: Restrict) + Length Length @relation(fields: [lengthId], references: [id], onDelete: Restrict, onUpdate: Cascade) lengthId Int @db.UnsignedTinyInt description String? @db.VarChar(500) notes String? @db.VarChar(500) @@ -461,7 +461,7 @@ model Map_NewSolo { overallRank Int? @db.UnsignedTinyInt mapRemovedFromModBool Boolean @default(false) timeSubmitted Int - User_SubmittedBy User? @relation("Map_NewSolo_SubmittedByToUser", fields: [submittedBy], references: [id], onDelete: SetNull, onUpdate: Restrict) + User_SubmittedBy User? @relation("Map_NewSolo_SubmittedByToUser", fields: [submittedBy], references: [id], onDelete: SetNull, onUpdate: Cascade) submittedBy String? Map_NewSoloToTechs Map_NewSoloToTechs[] @@ -476,9 +476,9 @@ model Map_NewSolo { } model Map_NewSoloToTechs { - Map_NewSolo Map_NewSolo @relation(fields: [map_NewSoloId], references: [id], onDelete: Cascade, onUpdate: Restrict) + Map_NewSolo Map_NewSolo @relation(fields: [map_NewSoloId], references: [id], onDelete: Cascade, onUpdate: Cascade) map_NewSoloId Int @db.UnsignedMediumInt - Tech Tech @relation(fields: [techId], references: [id], onDelete: Cascade, onUpdate: Restrict) + Tech Tech @relation(fields: [techId], references: [id], onDelete: Cascade, onUpdate: Cascade) techId Int @db.UnsignedSmallInt fullClearOnlyBool Boolean @default(false) @@ -499,14 +499,14 @@ model Quality { model Rating { id Int @id @default(autoincrement()) @db.UnsignedInt - Map Map @relation(fields: [mapId], references: [id], onDelete: Cascade, onUpdate: Restrict) + Map Map @relation(fields: [mapId], references: [id], onDelete: Cascade, onUpdate: Cascade) mapId Int @db.UnsignedMediumInt - User_SubmittedBy User @relation(fields: [submittedBy], references: [id], onDelete: Cascade, onUpdate: Restrict) + User_SubmittedBy User @relation(fields: [submittedBy], references: [id], onDelete: Cascade, onUpdate: Cascade) submittedBy String timeSubmitted Int - Quality Quality? @relation(fields: [qualityId], references: [id], onDelete: Restrict, onUpdate: Restrict) + Quality Quality? @relation(fields: [qualityId], references: [id], onDelete: Restrict, onUpdate: Cascade) qualityId Int? @db.UnsignedTinyInt - Difficulty Difficulty? @relation(fields: [difficultyId], references: [id], onDelete: Restrict, onUpdate: Restrict) + Difficulty Difficulty? @relation(fields: [difficultyId], references: [id], onDelete: Restrict, onUpdate: Cascade) difficultyId Int? @db.UnsignedSmallInt //must be a child difficulty @@unique([mapId, submittedBy]) @@ -519,7 +519,7 @@ model Rating { model ReviewCollection { id Int @id @default(autoincrement()) @db.UnsignedSmallInt - User User @relation(fields: [userId], references: [id], onDelete: Cascade, onUpdate: Restrict) + User User @relation(fields: [userId], references: [id], onDelete: Cascade, onUpdate: Cascade) userId String name String @db.VarChar(100) description String @db.VarChar(500) @@ -532,9 +532,9 @@ model ReviewCollection { model Review { id Int @id @default(autoincrement()) @db.UnsignedMediumInt - Mod Mod @relation(fields: [modId], references: [id], onDelete: Cascade, onUpdate: Restrict) + Mod Mod @relation(fields: [modId], references: [id], onDelete: Cascade, onUpdate: Cascade) modId Int @db.UnsignedSmallInt - ReviewCollection ReviewCollection @relation(fields: [reviewCollectionId], references: [id], onDelete: Cascade, onUpdate: Restrict) + ReviewCollection ReviewCollection @relation(fields: [reviewCollectionId], references: [id], onDelete: Cascade, onUpdate: Cascade) reviewCollectionId Int @db.UnsignedSmallInt timeSubmitted Int likes String? @db.VarChar(1000) @@ -550,11 +550,11 @@ model Review { model MapReview { id Int @id @default(autoincrement()) @db.UnsignedInt - Review Review @relation(fields: [reviewId], references: [id], onDelete: Cascade, onUpdate: Restrict) + Review Review @relation(fields: [reviewId], references: [id], onDelete: Cascade, onUpdate: Cascade) reviewId Int @db.UnsignedMediumInt - Map Map @relation(fields: [mapId], references: [id], onDelete: Cascade, onUpdate: Restrict) + Map Map @relation(fields: [mapId], references: [id], onDelete: Cascade, onUpdate: Cascade) mapId Int @db.UnsignedMediumInt - Length Length @relation(fields: [lengthId], references: [id], onDelete: Restrict, onUpdate: Restrict) + Length Length @relation(fields: [lengthId], references: [id], onDelete: Restrict, onUpdate: Cascade) lengthId Int @db.UnsignedTinyInt timeSubmitted Int likes String? @db.VarChar(500) @@ -570,9 +570,9 @@ model MapReview { } model UsersToCompletedMaps { - User User @relation(fields: [userId], references: [id], onDelete: Cascade, onUpdate: Restrict) + User User @relation(fields: [userId], references: [id], onDelete: Cascade, onUpdate: Cascade) userId String - Map Map @relation(fields: [mapId], references: [id], onDelete: Cascade, onUpdate: Restrict) + Map Map @relation(fields: [mapId], references: [id], onDelete: Cascade, onUpdate: Cascade) mapId Int @db.UnsignedMediumInt @@id([userId, mapId]) @@ -582,11 +582,11 @@ model UsersToCompletedMaps { model UserClaim { id Int @id @default(autoincrement()) @db.UnsignedMediumInt - User_claimedBy User @relation("UserClaim_ClaimedBy", fields: [claimedBy], references: [id], onDelete: Cascade, onUpdate: Restrict) + User_claimedBy User @relation("UserClaim_ClaimedBy", fields: [claimedBy], references: [id], onDelete: Cascade, onUpdate: Cascade) claimedBy String - User_claimedUser User @relation("UserClaim_ClaimedUserId", fields: [claimedUserId], references: [id], onDelete: Cascade, onUpdate: Restrict) + User_claimedUser User @relation("UserClaim_ClaimedUserId", fields: [claimedUserId], references: [id], onDelete: Cascade, onUpdate: Cascade) claimedUserId String - User_ApprovedBy User? @relation("UserClaim_ApprovedBy", fields: [approvedBy], references: [id], onDelete: Cascade, onUpdate: Restrict) + User_ApprovedBy User? @relation("UserClaim_ApprovedBy", fields: [approvedBy], references: [id], onDelete: Cascade, onUpdate: Cascade) approvedBy String? @@unique([claimedBy, claimedUserId]) @@ -640,7 +640,7 @@ model User { model Account { id String @id @default(cuid()) - user User @relation(fields: [userId], references: [id], onDelete: Cascade, onUpdate: Restrict) + user User @relation(fields: [userId], references: [id], onDelete: Cascade, onUpdate: Cascade) userId String type String provider String @@ -660,7 +660,7 @@ model Account { model Session { id String @id @default(cuid()) sessionToken String @unique - user User @relation(fields: [userId], references: [id], onDelete: Cascade, onUpdate: Restrict) + user User @relation(fields: [userId], references: [id], onDelete: Cascade, onUpdate: Cascade) userId String expires DateTime From 2fea675fc6f36e9dfd76a13420f8c7c90796b9bd Mon Sep 17 00:00:00 2001 From: otobot1 Date: Fri, 27 Sep 2024 21:26:39 -0600 Subject: [PATCH 21/43] resolve webpack errors --- src/server/api/routers/map_mod_publisher/map.ts | 5 +++-- src/server/api/routers/map_mod_publisher/publisher.ts | 2 +- src/server/api/routers/rating.ts | 2 +- .../review_reviewCollection_mapReview/reviewCollection.ts | 2 +- src/server/api/routers/tech_techVideo/tech.ts | 5 +---- src/server/api/routers/tech_techVideo/techVideo.ts | 2 +- src/server/api/routers/user_userClaim/user.ts | 3 +-- src/server/api/routers/user_userClaim/userClaim.ts | 2 +- src/server/api/schemas/techIdSchema_NonObject.ts | 8 ++++++++ src/server/api/schemas/userIdSchema_NonObject.ts | 7 +++++++ 10 files changed, 25 insertions(+), 13 deletions(-) create mode 100644 src/server/api/schemas/techIdSchema_NonObject.ts create mode 100644 src/server/api/schemas/userIdSchema_NonObject.ts diff --git a/src/server/api/routers/map_mod_publisher/map.ts b/src/server/api/routers/map_mod_publisher/map.ts index a9f47d9f..21180e52 100644 --- a/src/server/api/routers/map_mod_publisher/map.ts +++ b/src/server/api/routers/map_mod_publisher/map.ts @@ -6,13 +6,14 @@ import { Prisma, Map, MapSide, Map_NewWithMod_New, Map_Edit, Map_Archive, Map_Ne import { getCombinedSchema, getOrderObjectArray } from "~/server/api/utils/sortOrderHelpers"; import { getNonEmptyArray } from "~/utils/getNonEmptyArray"; import { INT_MAX_SIZES } from "~/consts/integerSizes"; -import { displayNameSchema_NonObject, getUserById, userIdSchema_NonObject } from "../user_userClaim/user"; +import { displayNameSchema_NonObject, getUserById } from "../user_userClaim/user"; import { MODLIST_MODERATOR_PERMISSION_STRINGS, checkPermissions } from "../../utils/permissions"; import { getModById } from "./mod"; import { getPublisherById } from "./publisher"; import { difficultyIdSchema_NonObject } from "../difficulty"; import { lengthIdSchema_NonObject } from "../length"; -import { techIdSchema_NonObject } from "../tech_techVideo/tech"; +import { techIdSchema_NonObject } from "../../schemas/techIdSchema_NonObject"; +import { userIdSchema_NonObject } from "../../schemas/userIdSchema_NonObject"; import { IfElse, ArrayIncludes } from "../../../../utils/typeHelpers"; import { getCurrentTime } from "../../utils/getCurrentTime"; import { getCheckedTableNames } from "../../utils/getCheckedTableNames"; diff --git a/src/server/api/routers/map_mod_publisher/publisher.ts b/src/server/api/routers/map_mod_publisher/publisher.ts index 73954981..ad006639 100644 --- a/src/server/api/routers/map_mod_publisher/publisher.ts +++ b/src/server/api/routers/map_mod_publisher/publisher.ts @@ -6,7 +6,7 @@ import { Prisma, Publisher } from "@prisma/client"; import { getCombinedSchema, getOrderObjectArray } from "~/server/api/utils/sortOrderHelpers"; import { getNonEmptyArray } from "~/utils/getNonEmptyArray"; import { INT_MAX_SIZES } from "~/consts/integerSizes"; -import { userIdSchema_NonObject } from "../user_userClaim/user"; +import { userIdSchema_NonObject } from "../../schemas/userIdSchema_NonObject"; import axios from "axios"; diff --git a/src/server/api/routers/rating.ts b/src/server/api/routers/rating.ts index e60b7351..8ea52383 100644 --- a/src/server/api/routers/rating.ts +++ b/src/server/api/routers/rating.ts @@ -12,7 +12,7 @@ import { difficultyIdSchema_NonObject } from "./difficulty"; import { getCurrentTime } from "../utils/getCurrentTime"; import { ADMIN_PERMISSION_STRINGS, checkIsPrivileged } from "../utils/permissions"; import { getModById } from "./map_mod_publisher/mod"; -import { userIdSchema_NonObject } from "./user_userClaim/user"; +import { userIdSchema_NonObject } from "../schemas/userIdSchema_NonObject"; diff --git a/src/server/api/routers/review_reviewCollection_mapReview/reviewCollection.ts b/src/server/api/routers/review_reviewCollection_mapReview/reviewCollection.ts index d564251c..b620d133 100644 --- a/src/server/api/routers/review_reviewCollection_mapReview/reviewCollection.ts +++ b/src/server/api/routers/review_reviewCollection_mapReview/reviewCollection.ts @@ -6,7 +6,7 @@ import { Prisma, ReviewCollection } from "@prisma/client"; import { getCombinedSchema, getOrderObjectArray } from "~/server/api/utils/sortOrderHelpers"; import { getNonEmptyArray } from "~/utils/getNonEmptyArray"; import { INT_MAX_SIZES } from "~/consts/integerSizes"; -import { userIdSchema_NonObject } from "../user_userClaim/user"; +import { userIdSchema_NonObject } from "../../schemas/userIdSchema_NonObject"; import { ADMIN_PERMISSION_STRINGS, MODLIST_MODERATOR_PERMISSION_STRINGS, checkIsPrivileged } from "../../utils/permissions"; diff --git a/src/server/api/routers/tech_techVideo/tech.ts b/src/server/api/routers/tech_techVideo/tech.ts index 826eea4d..11c91bb0 100644 --- a/src/server/api/routers/tech_techVideo/tech.ts +++ b/src/server/api/routers/tech_techVideo/tech.ts @@ -5,8 +5,8 @@ import { MyPrismaClient } from "~/server/prisma"; import { Prisma, Tech } from "@prisma/client"; import { getCombinedSchema, getOrderObjectArray } from "~/server/api/utils/sortOrderHelpers"; import { getNonEmptyArray } from "~/utils/getNonEmptyArray"; -import { INT_MAX_SIZES } from "~/consts/integerSizes"; import { techVideoRouter, defaultTechVideoSelect, techVideoPostWithTechSchema } from "./techVideo"; +import { techIdSchema_NonObject } from "../../schemas/techIdSchema_NonObject"; @@ -31,9 +31,6 @@ const defaultTechSelect = Prisma.validator()({ -export const techIdSchema_NonObject = z.number().int().gte(1).lte(INT_MAX_SIZES.smallInt.unsigned); //this needs to be here to resolve webpack error in ~\src\pages\api\panel.ts - - const techNameSchema_NonObject = z.string().min(1).max(50); diff --git a/src/server/api/routers/tech_techVideo/techVideo.ts b/src/server/api/routers/tech_techVideo/techVideo.ts index ae666de7..3614e56e 100644 --- a/src/server/api/routers/tech_techVideo/techVideo.ts +++ b/src/server/api/routers/tech_techVideo/techVideo.ts @@ -6,7 +6,7 @@ import { Prisma, TechVideo } from "@prisma/client"; import { getCombinedSchema, getOrderObjectArray } from "~/server/api/utils/sortOrderHelpers"; import { getNonEmptyArray } from "~/utils/getNonEmptyArray"; import { INT_MAX_SIZES } from "~/consts/integerSizes"; -import { techIdSchema_NonObject } from "./tech"; +import { techIdSchema_NonObject } from "../../schemas/techIdSchema_NonObject"; diff --git a/src/server/api/routers/user_userClaim/user.ts b/src/server/api/routers/user_userClaim/user.ts index 0b037eed..a2b4786b 100644 --- a/src/server/api/routers/user_userClaim/user.ts +++ b/src/server/api/routers/user_userClaim/user.ts @@ -8,6 +8,7 @@ import { getNonEmptyArray } from "~/utils/getNonEmptyArray"; import { ADMIN_PERMISSION_STRINGS, Permission, checkIsPrivileged, checkPermissions } from "../../utils/permissions"; import { selectIdObject } from "../../utils/selectIdObject"; import { userClaimRouter } from "./userClaim"; +import { userIdSchema_NonObject } from "../../schemas/userIdSchema_NonObject"; @@ -87,8 +88,6 @@ const getUserSelect = (permissions: Permission[] | undefined, overwrite?: boolea export const displayNameSchema_NonObject = z.string().min(1).max(50); -export const userIdSchema_NonObject = z.string().cuid(); //TODO!: figure out if we need to add z.coerce before string() - const userIdSchema = z.object({ id: userIdSchema_NonObject, }).strict(); diff --git a/src/server/api/routers/user_userClaim/userClaim.ts b/src/server/api/routers/user_userClaim/userClaim.ts index e8b5f97e..ad31fcf4 100644 --- a/src/server/api/routers/user_userClaim/userClaim.ts +++ b/src/server/api/routers/user_userClaim/userClaim.ts @@ -6,7 +6,7 @@ import { Prisma, UserClaim as PrismaUserClaim } from "@prisma/client"; import { getCombinedSchema, getOrderObjectArray } from "~/server/api/utils/sortOrderHelpers"; import { getNonEmptyArray } from "~/utils/getNonEmptyArray"; import { INT_MAX_SIZES } from "~/consts/integerSizes"; -import { userIdSchema_NonObject } from "./user"; +import { userIdSchema_NonObject } from "../../schemas/userIdSchema_NonObject"; import { ADMIN_PERMISSION_STRINGS, checkIsPrivileged, checkPermissions } from "../../utils/permissions"; diff --git a/src/server/api/schemas/techIdSchema_NonObject.ts b/src/server/api/schemas/techIdSchema_NonObject.ts new file mode 100644 index 00000000..ea87b784 --- /dev/null +++ b/src/server/api/schemas/techIdSchema_NonObject.ts @@ -0,0 +1,8 @@ +import { z } from "zod"; +import { INT_MAX_SIZES } from "~/consts/integerSizes"; + + + + +// this needs to be here to resolve webpack errors in ~\src\pages\api\panel.ts and ~\src\server\api\routers\tech_techVideo\techVideo.ts +export const techIdSchema_NonObject = z.number().int().gte(1).lte(INT_MAX_SIZES.smallInt.unsigned); \ No newline at end of file diff --git a/src/server/api/schemas/userIdSchema_NonObject.ts b/src/server/api/schemas/userIdSchema_NonObject.ts new file mode 100644 index 00000000..42d548a9 --- /dev/null +++ b/src/server/api/schemas/userIdSchema_NonObject.ts @@ -0,0 +1,7 @@ +import { z } from 'zod'; + + + + +// this needs to be here to resolve webpack error in ~\src\server\api\routers\user_userClaim\userClaim.ts +export const userIdSchema_NonObject = z.string().cuid(); //TODO!: figure out if we need to add z.coerce before string() \ No newline at end of file From da682d949624eeeda9baebc35742379af6d276b3 Mon Sep 17 00:00:00 2001 From: otobot1 Date: Fri, 27 Sep 2024 22:19:09 -0600 Subject: [PATCH 22/43] work on getting userClaim pages to load no longer throwing React errors --- src/pages/claim-user/index.tsx | 118 ++++++------ src/pages/claim-user/verify.tsx | 168 ++++++++++-------- src/pages/index.tsx | 2 +- .../api/routers/user_userClaim/userClaim.ts | 36 ++-- 4 files changed, 180 insertions(+), 144 deletions(-) diff --git a/src/pages/claim-user/index.tsx b/src/pages/claim-user/index.tsx index 0af4da8c..50c6a4a4 100644 --- a/src/pages/claim-user/index.tsx +++ b/src/pages/claim-user/index.tsx @@ -1,4 +1,4 @@ -import { Button, createStyles, ScrollArea, Table } from "@mantine/core"; +import { Button, createStyles, LoadingOverlay, ScrollArea, Table } from "@mantine/core"; import type { NextPage } from "next"; import { useSession } from "next-auth/react"; import Link from "next/link"; @@ -76,7 +76,7 @@ const ClaimUser: NextPage = () => { const { classes } = useStyles(); - if (status === 'unauthenticated') { + if (status === "unauthenticated") { return ( { pageDescription={PAGE_DESCRIPTION} pathname={CLAIM_USER_PATHNAME} > - -

{PAGE_TITLE}

-

Claimed Users

-

- Contact us on Discord to get your claim verified. -

- - - - - - - - - - {userClaims.map(claim => ( - - - - + +

{PAGE_TITLE}

+

Claimed Users

+

+ Contact us on Discord to get your claim verified. +

+
Claim IDUser IDUsername
{claim.id}{claim.claimedUserId}{claim.User_claimedUser.discordUsername}#{claim.User_claimedUser.discordDiscriminator}
+ + + + + - ))} - -
Claim IDUser IDUsername
-

Unclaimed Users

- - - - - - - - - {unclaimedUsers.map(unclaimedUser => ( - - - + + + {userClaims.map( + (claim) => ( + + + + + + ) + )} + +
User
{unclaimedUser.discordUsername}#{unclaimedUser.discordDiscriminator} - -
{claim.id}{claim.claimedUserId}{claim.User_claimedUser.discordUsername}#{claim.User_claimedUser.discordDiscriminator}
+

Unclaimed Users

+ + + + + - ))} - -
User
-
+ + + {unclaimedUsers.map( + (unclaimedUser) => ( + + + {unclaimedUser.discordUsername}#{unclaimedUser.discordDiscriminator} + + + + + + ) + )} + + + +
); }; diff --git a/src/pages/claim-user/verify.tsx b/src/pages/claim-user/verify.tsx index 800d5885..32ab10ee 100644 --- a/src/pages/claim-user/verify.tsx +++ b/src/pages/claim-user/verify.tsx @@ -1,4 +1,4 @@ -import { Button, createStyles, Modal, ScrollArea, Stack, Table } from "@mantine/core"; +import { Button, createStyles, LoadingOverlay, Modal, ScrollArea, Stack, Table } from "@mantine/core"; import type { NextPage } from "next"; import { useSession } from "next-auth/react"; import { useState } from "react"; @@ -40,48 +40,32 @@ const VerifyClaim: NextPage = () => { const [claimToVerify, setClaimToVerify] = useState<{ id: number, - claimedBy: string, - claimedUserId: string; + claimingUser: string, + claimedUser: string; } | null>(null); const utils = api.useUtils(); - const verifyUserClaimMutation = api.user.userClaim.verify.useMutation({ - async onSuccess() { - await Promise.all([ - utils.user.userClaim.invalidate() - ]); + const onSuccess = async () => { + await Promise.all([ + utils.user.userClaim.invalidate() + ]); - setClaimToVerify(null); - } - }); + setClaimToVerify(null); + }; + + const verifyUserClaimMutation = api.user.userClaim.verify.useMutation({ onSuccess }); + const rejectUserClaimMutation = api.user.userClaim.delete.useMutation({ onSuccess }); const { classes } = useStyles(); - if (status === "loading") { - return ( - - <> - - ); - } - else if (sessionData === null || !isPermitted(sessionData.user.permissions, ADMIN_PERMISSION_STRINGS)) { - return ( - - Login as an admin/superadmin to verify users. - - ); - } + + const isLoading = status === "loading"; + + const isUserPermitted = !isLoading && sessionData !== null && isPermitted(sessionData.user.permissions, ADMIN_PERMISSION_STRINGS); + return ( { pageDescription={PAGE_DESCRIPTION} pathname={VERIFY_CLAIM_PATHNAME} > - { setClaimToVerify(null); }} title="Confirmation" centered> + { setClaimToVerify(null); }} + title="Verify Claim" + centered + > {claimToVerify && (

- Are you sure you want to verify claim {claimToVerify.id} by {claimToVerify.byUser} for {claimToVerify.forUser}? + Verify claim {claimToVerify.id}? {claimToVerify.claimingUser} is claiming {claimToVerify.claimedUser}

- + +
)}
- -

User claims

- - - - - - - - - - - {userClaims.map(claim => { - const byUser = `${claim.User_UserClaim_claimBy.discordUsername}#${claim.User_UserClaim_claimBy.discordDiscriminator}`; - const forUser = `${claim.User_UserClaim_claimFor.discordUsername}#${claim.User_UserClaim_claimFor.discordDiscriminator}`; - return ( - - - - - + + {isUserPermitted ? ( + +

User Claims

+
Claim IDByFor
{claim.id}{byUser}{forUser}
+ + + + + + - ); - })} - -
Claim IDByFor
-
+ + + {userClaims.map( + (claim) => { + const claimingUser = `${claim.User_claimedBy.discordUsername}#${claim.User_claimedBy.discordDiscriminator}`; + const claimedUser = `${claim.User_claimedUser.discordUsername}#${claim.User_claimedUser.discordDiscriminator}`; + + return ( + + {claim.id} + {claimingUser} + {claimedUser} + + + + + ); + } + )} + + + + ) : ( + +

You do not have permission to view this page.

+
+ )} +
); }; diff --git a/src/pages/index.tsx b/src/pages/index.tsx index 0305e4df..f2b6f021 100644 --- a/src/pages/index.tsx +++ b/src/pages/index.tsx @@ -68,7 +68,7 @@ const ClaimUserLink = ({ } - return signIn("discord", { callbackUrl: CLAIM_USER_PATHNAME })}>{linkText}; + return signIn("discord", { callbackUrl: CLAIM_USER_PATHNAME })}>{linkText}; //TODO!!!: Refactor this to use a Next.js Link component with an href so that logged-out users can see where they're going before they click the link }; diff --git a/src/server/api/routers/user_userClaim/userClaim.ts b/src/server/api/routers/user_userClaim/userClaim.ts index ad31fcf4..5f19f7ee 100644 --- a/src/server/api/routers/user_userClaim/userClaim.ts +++ b/src/server/api/routers/user_userClaim/userClaim.ts @@ -12,12 +12,16 @@ import { ADMIN_PERMISSION_STRINGS, checkIsPrivileged, checkPermissions } from ". +type RelatedUser = { + id: string; + discordUsername: string | null; + discordDiscriminator: string | null; +}; + + type ExpandedUserClaim = PrismaUserClaim & { - User_claimedUser: { - id: string; - discordUsername: string | null; - discordDiscriminator: string | null; - }; + User_claimedBy: RelatedUser; + User_claimedUser: RelatedUser; }; @@ -28,18 +32,22 @@ type TrimmedUserClaim = Omit & { -export const defaultUserClaimSelect = Prisma.validator()({ +const relatedUserSelectObject = { + id: true, + discordUsername: true, + discordDiscriminator: true, +} satisfies { [Key in keyof RelatedUser]: true }; // make this relationship explicit/type-safe //TODO!!!: do this throughout the api, or make a follow-up issue to do so + +const relatedUserSelect = Prisma.validator()(relatedUserSelectObject); + + +const defaultUserClaimSelect = Prisma.validator()({ id: true, claimedBy: true, claimedUserId: true, approvedBy: true, - User_claimedUser: { - select: { - id: true, - discordUsername: true, - discordDiscriminator: true, - }, - }, + User_claimedBy: { select: relatedUserSelect }, + User_claimedUser: { select: relatedUserSelect }, }); @@ -69,7 +77,7 @@ const userClaimOrderSchema = getCombinedSchema( -const getUserClaimById = async( +const getUserClaimById = async ( prisma: MyPrismaClient, id: number ): Promise => { From 881b72067d97b7b1cadf03e9fb77cadf4eafb5f3 Mon Sep 17 00:00:00 2001 From: otobot1 Date: Sat, 19 Oct 2024 21:24:28 -0600 Subject: [PATCH 23/43] get /claim-user working and tweak styling and wording --- src/pages/claim-user/index.tsx | 139 ++++++++++-------- .../api/schemas/userIdSchema_NonObject.ts | 4 +- 2 files changed, 76 insertions(+), 67 deletions(-) diff --git a/src/pages/claim-user/index.tsx b/src/pages/claim-user/index.tsx index 50c6a4a4..29615d13 100644 --- a/src/pages/claim-user/index.tsx +++ b/src/pages/claim-user/index.tsx @@ -1,4 +1,4 @@ -import { Button, createStyles, LoadingOverlay, ScrollArea, Table } from "@mantine/core"; +import { Button, createStyles, LoadingOverlay, ScrollArea, Table, Title } from "@mantine/core"; import type { NextPage } from "next"; import { useSession } from "next-auth/react"; import Link from "next/link"; @@ -6,12 +6,13 @@ import { Layout } from "~/components/layout/layout"; import { cmlDiscordInviteUrl } from "~/consts/cmlDiscordInviteUrl"; import { CLAIM_USER_PATHNAME } from "~/consts/pathnames"; import { pageContentHeightPixels } from "~/styles/pageContentHeightPixels"; +import { pageTitle } from "~/styles/pageTitle"; import { api } from "~/utils/api"; -const PAGE_TITLE = "Claim User"; +const PAGE_TITLE = "Claim Users"; const PAGE_DESCRIPTION = "Submit a claim for a legacy user."; @@ -19,13 +20,22 @@ const PAGE_DESCRIPTION = "Submit a claim for a legacy user."; const useStyles = createStyles( (theme) => ({ + pageTitle, scrollArea: { height: `${pageContentHeightPixels}px`, color: theme.white, }, discordLink: { - textDecoration: 'underline', - } + textDecoration: "underline", + }, + sectionTitle: { + marginTop: "25px", + marginBottom: "4px", + }, + sectionInfo: { + marginTop: "4px", + marginBottom: "4px", + }, }), ); @@ -95,68 +105,67 @@ const ClaimUser: NextPage = () => { pageDescription={PAGE_DESCRIPTION} pathname={CLAIM_USER_PATHNAME} > - - -

{PAGE_TITLE}

-

Claimed Users

-

- Contact us on Discord to get your claim verified. -

- - - - - - - - - - {userClaims.map( - (claim) => ( - - - - - - ) - )} - -
Claim IDUser IDUsername
{claim.id}{claim.claimedUserId}{claim.User_claimedUser.discordUsername}#{claim.User_claimedUser.discordDiscriminator}
-

Unclaimed Users

- - - - - - - - - {unclaimedUsers.map( - (unclaimedUser) => ( - - - - - ) - )} - -
User
- {unclaimedUser.discordUsername}#{unclaimedUser.discordDiscriminator} - - -
-
-
+ + {PAGE_TITLE} + Active Claims +

+ Contact us on Discord to get your claims verified. +

+ + + + + + + + + + {userClaims.map( + (claim) => ( + + + + + + ) + )} + +
Claim IDUser IDUsername
{claim.id}{claim.claimedUserId}{claim.User_claimedUser.discordUsername}#{claim.User_claimedUser.discordDiscriminator}
+ Unlinked Users + + + + + + + + + {unclaimedUsers.map( + (unclaimedUser) => ( + + + + + ) + )} + +
User
+ {unclaimedUser.discordUsername}#{unclaimedUser.discordDiscriminator} + + +
+
); }; diff --git a/src/server/api/schemas/userIdSchema_NonObject.ts b/src/server/api/schemas/userIdSchema_NonObject.ts index 42d548a9..d01bb05c 100644 --- a/src/server/api/schemas/userIdSchema_NonObject.ts +++ b/src/server/api/schemas/userIdSchema_NonObject.ts @@ -1,7 +1,7 @@ -import { z } from 'zod'; +import { z } from "zod"; // this needs to be here to resolve webpack error in ~\src\server\api\routers\user_userClaim\userClaim.ts -export const userIdSchema_NonObject = z.string().cuid(); //TODO!: figure out if we need to add z.coerce before string() \ No newline at end of file +export const userIdSchema_NonObject = z.string(); //TODO!!!: .cuid() doesn't work here, as sometimes a blank string is passed into trpc queries, which is not a valid cuid. Need to narrow this as much as possible without using .cuid(). \ No newline at end of file From 05781a54af2aef1f6034f391326e9f03b5307e20 Mon Sep 17 00:00:00 2001 From: otobot1 Date: Sat, 19 Oct 2024 21:50:57 -0600 Subject: [PATCH 24/43] get /claim-user/verify working --- src/pages/claim-user/index.tsx | 2 +- src/pages/claim-user/verify.tsx | 162 +++++++++++++++++--------------- 2 files changed, 87 insertions(+), 77 deletions(-) diff --git a/src/pages/claim-user/index.tsx b/src/pages/claim-user/index.tsx index 29615d13..bc84f416 100644 --- a/src/pages/claim-user/index.tsx +++ b/src/pages/claim-user/index.tsx @@ -155,7 +155,7 @@ const ClaimUser: NextPage = () => { diff --git a/src/pages/claim-user/verify.tsx b/src/pages/claim-user/verify.tsx index 32ab10ee..5ff2b2e6 100644 --- a/src/pages/claim-user/verify.tsx +++ b/src/pages/claim-user/verify.tsx @@ -1,4 +1,4 @@ -import { Button, createStyles, LoadingOverlay, Modal, ScrollArea, Stack, Table } from "@mantine/core"; +import { Button, createStyles, Group, LoadingOverlay, Modal, ScrollArea, Stack, Table, Title } from "@mantine/core"; import type { NextPage } from "next"; import { useSession } from "next-auth/react"; import { useState } from "react"; @@ -7,12 +7,13 @@ import { VERIFY_CLAIM_PATHNAME } from "~/consts/pathnames"; import { ADMIN_PERMISSION_STRINGS } from "~/server/api/utils/permissions"; import { isPermitted } from "~/utils/permissions"; import { pageContentHeightPixels } from "~/styles/pageContentHeightPixels"; +import { pageTitle } from "~/styles/pageTitle"; import { api } from "~/utils/api"; -const PAGE_TITLE = "Verify Claim"; +const PAGE_TITLE = "Verify Claims"; const PAGE_DESCRIPTION = "Verify legacy user claims."; @@ -20,6 +21,7 @@ const PAGE_DESCRIPTION = "Verify legacy user claims."; const useStyles = createStyles( (theme) => ({ + pageTitle, scrollArea: { height: `${pageContentHeightPixels}px`, color: theme.white, @@ -74,93 +76,101 @@ const VerifyClaim: NextPage = () => { pathname={VERIFY_CLAIM_PATHNAME} > { setClaimToVerify(null); }} title="Verify Claim" centered > {claimToVerify && ( - +

Verify claim {claimToVerify.id}? {claimToVerify.claimingUser} is claiming {claimToVerify.claimedUser}

- - + + + +
)}
- - {isUserPermitted ? ( - + + - <h1>User Claims</h1> - <Table> - <thead> - <tr> - <th>Claim ID</th> - <th>By</th> - <th>For</th> - <th></th> - </tr> - </thead> - <tbody> - {userClaims.map( - (claim) => { - const claimingUser = `${claim.User_claimedBy.discordUsername}#${claim.User_claimedBy.discordDiscriminator}`; - const claimedUser = `${claim.User_claimedUser.discordUsername}#${claim.User_claimedUser.discordDiscriminator}`; - - return ( - <tr key={claim.id}> - <td>{claim.id}</td> - <td>{claimingUser}</td> - <td>{claimedUser}</td> - <td> - <Button - onClick={ - () => { - setClaimToVerify({ - id: claim.id, - claimingUser, - claimedUser, - }); - } + {PAGE_TITLE} + + + + + + + + + + + + {userClaims.map( + (claim) => { + const claimingUser = `${claim.User_claimedBy.discordUsername}#${claim.User_claimedBy.discordDiscriminator}`; + const claimedUser = `${claim.User_claimedUser.discordUsername}#${claim.User_claimedUser.discordDiscriminator}`; + + return ( + + + + + - - ); - } - )} - -
Claim IDByFor
{claim.id}{claimingUser}{claimedUser} + -
-
- ) : ( - -

You do not have permission to view this page.

-
- )} -
- + } + > + Resolve + + + + ); + } + )} + + + + ) : ( + +

You do not have permission to view this page.

+
+ )} + ); }; From f97d6e438a444c6ea960c03020e79aaa6bc76196 Mon Sep 17 00:00:00 2001 From: otobot1 Date: Sat, 19 Oct 2024 21:51:32 -0600 Subject: [PATCH 25/43] rename /claim-user to /claim-users --- src/pages/{claim-user => claim-users}/index.tsx | 0 src/pages/{claim-user => claim-users}/verify.tsx | 0 2 files changed, 0 insertions(+), 0 deletions(-) rename src/pages/{claim-user => claim-users}/index.tsx (100%) rename src/pages/{claim-user => claim-users}/verify.tsx (100%) diff --git a/src/pages/claim-user/index.tsx b/src/pages/claim-users/index.tsx similarity index 100% rename from src/pages/claim-user/index.tsx rename to src/pages/claim-users/index.tsx diff --git a/src/pages/claim-user/verify.tsx b/src/pages/claim-users/verify.tsx similarity index 100% rename from src/pages/claim-user/verify.tsx rename to src/pages/claim-users/verify.tsx From 5ef636766ace14ebfa94cf9bcee826e8129342e8 Mon Sep 17 00:00:00 2001 From: otobot1 Date: Sat, 19 Oct 2024 21:56:40 -0600 Subject: [PATCH 26/43] fix double call of layout component --- src/pages/claim-users/verify.tsx | 137 ++++++++++++++++--------------- 1 file changed, 70 insertions(+), 67 deletions(-) diff --git a/src/pages/claim-users/verify.tsx b/src/pages/claim-users/verify.tsx index 5ff2b2e6..cae5c7db 100644 --- a/src/pages/claim-users/verify.tsx +++ b/src/pages/claim-users/verify.tsx @@ -61,14 +61,27 @@ const VerifyClaim: NextPage = () => { const rejectUserClaimMutation = api.user.userClaim.delete.useMutation({ onSuccess }); - const { classes } = useStyles(); - - const isLoading = status === "loading"; const isUserPermitted = !isLoading && sessionData !== null && isPermitted(sessionData.user.permissions, ADMIN_PERMISSION_STRINGS); + const { classes } = useStyles(); + + + if (!isUserPermitted) { + return ( + +

You do not have permission to view this page.

+
+ ); + } + + return ( { )} - {isUserPermitted ? ( - + + - <LoadingOverlay - visible={isLoading} - color="rgba(0, 0, 0, 0.5)" - /> - <Title - className={classes.pageTitle} - order={1} - > - {PAGE_TITLE} - - - - - - - - - - - - {userClaims.map( - (claim) => { - const claimingUser = `${claim.User_claimedBy.discordUsername}#${claim.User_claimedBy.discordDiscriminator}`; - const claimedUser = `${claim.User_claimedUser.discordUsername}#${claim.User_claimedUser.discordDiscriminator}`; - - return ( - - - - - + + ); + } + )} + +
Claim IDByFor
{claim.id}{claimingUser}{claimedUser} - +
+
); }; From 8e2e11955c33fc1b6db0217ad13d603dbdf800d3 Mon Sep 17 00:00:00 2001 From: otobot1 Date: Sat, 19 Oct 2024 21:57:42 -0600 Subject: [PATCH 27/43] update comment --- src/server/api/routers/user_userClaim/userClaim.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/server/api/routers/user_userClaim/userClaim.ts b/src/server/api/routers/user_userClaim/userClaim.ts index 5f19f7ee..10483aea 100644 --- a/src/server/api/routers/user_userClaim/userClaim.ts +++ b/src/server/api/routers/user_userClaim/userClaim.ts @@ -355,7 +355,7 @@ export const userClaimRouter = createTRPCRouter({ // delete the user after all the other updates have been made, so that a race condition where the delete partially cascades to updating values doesn't occur - // this delete should cascade to the userClaim as well (TODO!!!: confirm this and update this comment accordingly or add a deletion of the userClaim) + // the deletion automatically cascades to the userClaim await ctx.prisma.user.delete({ where: { id: claim.claimedUserId, From f5d09c29e6283fd56ed0caa2d72958c265156b46 Mon Sep 17 00:00:00 2001 From: otobot1 Date: Sat, 19 Oct 2024 22:10:12 -0600 Subject: [PATCH 28/43] update /claim-user pathname constants --- src/consts/pathnames.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/consts/pathnames.ts b/src/consts/pathnames.ts index d72160a2..c02c9d0a 100644 --- a/src/consts/pathnames.ts +++ b/src/consts/pathnames.ts @@ -1,5 +1,5 @@ export const MODS_PAGE_PATHNAME = "/mods"; export const FAQ_PAGE_PATHNAME = "/faq"; export const COMING_SOON_PATHNAME = "/coming-soon"; -export const CLAIM_USER_PATHNAME = "/claim-user"; -export const VERIFY_CLAIM_PATHNAME = "/claim-user/verify"; \ No newline at end of file +export const CLAIM_USER_PATHNAME = "/claim-users"; +export const VERIFY_CLAIM_PATHNAME = "/claim-users/verify"; \ No newline at end of file From 2b36ea7131576ac6d861b7fccab8fab2ab39302b Mon Sep 17 00:00:00 2001 From: otobot1 Date: Sat, 19 Oct 2024 22:11:17 -0600 Subject: [PATCH 29/43] add link to verify page from claim page only appears for admins --- src/pages/claim-users/index.tsx | 24 ++++++++++++++++++------ src/pages/claim-users/verify.tsx | 6 +++--- 2 files changed, 21 insertions(+), 9 deletions(-) diff --git a/src/pages/claim-users/index.tsx b/src/pages/claim-users/index.tsx index bc84f416..836234c9 100644 --- a/src/pages/claim-users/index.tsx +++ b/src/pages/claim-users/index.tsx @@ -4,10 +4,12 @@ import { useSession } from "next-auth/react"; import Link from "next/link"; import { Layout } from "~/components/layout/layout"; import { cmlDiscordInviteUrl } from "~/consts/cmlDiscordInviteUrl"; -import { CLAIM_USER_PATHNAME } from "~/consts/pathnames"; +import { CLAIM_USER_PATHNAME, VERIFY_CLAIM_PATHNAME } from "~/consts/pathnames"; +import { ADMIN_PERMISSION_STRINGS } from "~/server/api/utils/permissions"; import { pageContentHeightPixels } from "~/styles/pageContentHeightPixels"; import { pageTitle } from "~/styles/pageTitle"; import { api } from "~/utils/api"; +import { isPermitted } from "~/utils/permissions"; @@ -25,7 +27,7 @@ const useStyles = createStyles( height: `${pageContentHeightPixels}px`, color: theme.white, }, - discordLink: { + link: { textDecoration: "underline", }, sectionTitle: { @@ -43,7 +45,7 @@ const useStyles = createStyles( const ClaimUser: NextPage = () => { - const { status, data: sessionData } = useSession(); + const { status: sessionStatus, data: sessionData } = useSession(); const userId = sessionData?.user.id ?? ""; @@ -83,10 +85,15 @@ const ClaimUser: NextPage = () => { }); + const isLoading = sessionStatus === "loading"; + + const isAdmin = !isLoading && sessionData !== null && isPermitted(sessionData.user.permissions, ADMIN_PERMISSION_STRINGS); + + const { classes } = useStyles(); - if (status === "unauthenticated") { + if (sessionStatus === "unauthenticated") { return ( { className={classes.scrollArea} > {PAGE_TITLE} + {isAdmin && ( +

+ Click here to verify users. +

+ )} Active Claims

- Contact us on Discord to get your claims verified. + Contact us on Discord to get your claims verified.

diff --git a/src/pages/claim-users/verify.tsx b/src/pages/claim-users/verify.tsx index cae5c7db..a4ef6ed8 100644 --- a/src/pages/claim-users/verify.tsx +++ b/src/pages/claim-users/verify.tsx @@ -33,7 +33,7 @@ const useStyles = createStyles( const VerifyClaim: NextPage = () => { - const { status, data: sessionData } = useSession(); + const { status: sessionStatus, data: sessionData } = useSession(); const userClaimsQuery = api.user.userClaim.getAll.useQuery({}, { queryKey: ["user.userClaim.getAll", {}] }); @@ -61,7 +61,7 @@ const VerifyClaim: NextPage = () => { const rejectUserClaimMutation = api.user.userClaim.delete.useMutation({ onSuccess }); - const isLoading = status === "loading"; + const isLoading = sessionStatus === "loading"; const isUserPermitted = !isLoading && sessionData !== null && isPermitted(sessionData.user.permissions, ADMIN_PERMISSION_STRINGS); @@ -89,7 +89,7 @@ const VerifyClaim: NextPage = () => { pathname={VERIFY_CLAIM_PATHNAME} > { setClaimToVerify(null); }} title="Verify Claim" centered From 70e52aaf6f530c1da082bafac4e20ba4cf0c315c Mon Sep 17 00:00:00 2001 From: otobot1 Date: Sat, 19 Oct 2024 22:28:01 -0600 Subject: [PATCH 30/43] address type error --- src/server/auth/prismaAdapter.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/server/auth/prismaAdapter.ts b/src/server/auth/prismaAdapter.ts index d5b11096..c7e55bb6 100644 --- a/src/server/auth/prismaAdapter.ts +++ b/src/server/auth/prismaAdapter.ts @@ -11,7 +11,8 @@ type CreateUser = (user: PrismaUser) => Awaitable; const getCreateUser = (prisma: PrismaClient): CreateUser => { return (user: PrismaUser): Awaitable => { - const { id, email, emailVerified, ...rest } = user; //remove the discord id from the user object - a replacement is auto-generated by Prisma + //@ts-expect-error //required because the Discord Provider assumes the standard user type from NextAuth core and returns the email properties, but we don't want to store emails in the database so Prisma will fail to create the user as it isn't expecting those properties. fixing this will probably require a custom provider. //TODO!!!: create a follow-up issue to determine if we are receiving email info from Discord and discarding it or if we are not receiving it at all + const { id, email, emailVerified, ...rest } = user; //remove the discord id from the user object - a replacement is auto-generated by Prisma. also remove the undefined and undesired email properties const trimmedUser = rest as TrimmedUser; From f27308a9e070e7cb540dced0552ce03fc4e91334 Mon Sep 17 00:00:00 2001 From: otobot1 Date: Sun, 20 Oct 2024 16:41:58 -0600 Subject: [PATCH 31/43] tidy up types and adjust comments --- src/pages/claim-users/verify.tsx | 17 +++++++++++------ src/pages/index.tsx | 2 +- src/server/api/routers/user_userClaim/user.ts | 4 ++-- 3 files changed, 14 insertions(+), 9 deletions(-) diff --git a/src/pages/claim-users/verify.tsx b/src/pages/claim-users/verify.tsx index a4ef6ed8..4191e09e 100644 --- a/src/pages/claim-users/verify.tsx +++ b/src/pages/claim-users/verify.tsx @@ -32,6 +32,15 @@ const useStyles = createStyles( +type ClaimToVerify = { + id: number; + claimingUser: string; + claimedUser: string; +}; + + + + const VerifyClaim: NextPage = () => { const { status: sessionStatus, data: sessionData } = useSession(); @@ -40,11 +49,7 @@ const VerifyClaim: NextPage = () => { const userClaims = userClaimsQuery.data ?? []; - const [claimToVerify, setClaimToVerify] = useState<{ - id: number, - claimingUser: string, - claimedUser: string; - } | null>(null); + const [claimToVerify, setClaimToVerify] = useState(null); const utils = api.useUtils(); @@ -89,7 +94,7 @@ const VerifyClaim: NextPage = () => { pathname={VERIFY_CLAIM_PATHNAME} > { setClaimToVerify(null); }} title="Verify Claim" centered diff --git a/src/pages/index.tsx b/src/pages/index.tsx index f2b6f021..1ec03d4d 100644 --- a/src/pages/index.tsx +++ b/src/pages/index.tsx @@ -68,7 +68,7 @@ const ClaimUserLink = ({ } - return signIn("discord", { callbackUrl: CLAIM_USER_PATHNAME })}>{linkText}; //TODO!!!: Refactor this to use a Next.js Link component with an href so that logged-out users can see where they're going before they click the link + return signIn("discord", { callbackUrl: CLAIM_USER_PATHNAME })}>{linkText}; //TODO!!!: Refactor this to use a Next.js Link component with an href so that logged-out users can see where they're going before they click the link. This can be a followup issue. }; diff --git a/src/server/api/routers/user_userClaim/user.ts b/src/server/api/routers/user_userClaim/user.ts index a2b4786b..67605ed4 100644 --- a/src/server/api/routers/user_userClaim/user.ts +++ b/src/server/api/routers/user_userClaim/user.ts @@ -208,7 +208,7 @@ export const userRouter = createTRPCRouter({ .query(async ({ ctx, input }) => { return await ctx.prisma.user.findMany({ where: { - accountStatus: 'Unlinked', + accountStatus: "Unlinked", }, select: { id: true, @@ -239,7 +239,7 @@ export const userRouter = createTRPCRouter({ delete: loggedInProcedure .input(userIdSchema) - .mutation(async ({ ctx, input }) => { //TODO!!!: mark the user as deleted here and update the other endpoints to not return deleted users. can be a follow-up issue. need to tidy up this whole router lol. + .mutation(async ({ ctx, input }) => { if (!ctx.session?.user) throw undefinedSessionError; From c54eb601061d1560cd2d8647d044492e408612c5 Mon Sep 17 00:00:00 2001 From: otobot1 Date: Sun, 20 Oct 2024 16:58:03 -0600 Subject: [PATCH 32/43] Update schema.prisma --- prisma/schema.prisma | 19 ++++++++++--------- 1 file changed, 10 insertions(+), 9 deletions(-) diff --git a/prisma/schema.prisma b/prisma/schema.prisma index b4321b1e..dc62969a 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -298,7 +298,7 @@ model Mod_Edit { gamebananaModId Int @db.UnsignedMediumInt timeCreatedGamebanana Int timeSubmitted Int - User_SubmittedBy User? @relation(fields: [submittedBy], references: [id], onDelete: SetNull, onUpdate: Cascade) + User_SubmittedBy User? @relation("Mod_Edit_SubmittedByToUser", fields: [submittedBy], references: [id], onDelete: SetNull, onUpdate: Cascade) submittedBy String? Mod_EditToTags Mod_EditToTags[] Mod_NewToTags Mod_NewToTags[] @@ -375,7 +375,7 @@ model Mod_New { longDescription String? @db.VarChar(1500) gamebananaModId Int @unique @db.UnsignedMediumInt timeSubmitted Int - User_SubmittedBy User? @relation(fields: [submittedBy], references: [id], onDelete: SetNull, onUpdate: Cascade) + User_SubmittedBy User? @relation("Mod_New_SubmittedByToUser", fields: [submittedBy], references: [id], onDelete: SetNull, onUpdate: Cascade) submittedBy String? timeCreatedGamebanana Int Map_NewWithMod_New Map_NewWithMod_New[] @@ -581,12 +581,12 @@ model UsersToCompletedMaps { } model UserClaim { - id Int @id @default(autoincrement()) @db.UnsignedMediumInt - User_claimedBy User @relation("UserClaim_ClaimedBy", fields: [claimedBy], references: [id], onDelete: Cascade, onUpdate: Cascade) + id Int @id @default(autoincrement()) @db.UnsignedMediumInt + User_claimedBy User @relation("UserClaim_ClaimedBy", fields: [claimedBy], references: [id], onDelete: Cascade, onUpdate: Cascade) claimedBy String - User_claimedUser User @relation("UserClaim_ClaimedUserId", fields: [claimedUserId], references: [id], onDelete: Cascade, onUpdate: Cascade) + User_claimedUser User @relation("UserClaim_ClaimedUserId", fields: [claimedUserId], references: [id], onDelete: Cascade, onUpdate: Cascade) claimedUserId String - User_ApprovedBy User? @relation("UserClaim_ApprovedBy", fields: [approvedBy], references: [id], onDelete: Cascade, onUpdate: Cascade) + User_ApprovedBy User? @relation("UserClaim_ApprovedBy", fields: [approvedBy], references: [id], onDelete: Cascade, onUpdate: Cascade) approvedBy String? @@unique([claimedBy, claimedUserId]) @@ -599,7 +599,7 @@ model User { id String @id @default(cuid()) name String image String? - discordUsername String? @db.VarChar(32) + discordUsername String @db.VarChar(32) discordDiscriminator String? @db.VarChar(4) displayDiscord Boolean? showCompletedMaps Boolean @default(false) @@ -622,10 +622,10 @@ model User { Map_Archive_mapperUserIdToUser Map_Archive[] @relation("Map_Archive_MapperUserIdToUser") Map_Archive_submittedByToUser Map_Archive[] @relation("Map_Archive_SubmittedByToUser") Map_Archive_approvedByToUser Map_Archive[] @relation("Map_Archive_ApprovedByToUser") - Mod_Edit_submittedByToUser Mod_Edit[] + Mod_Edit_submittedByToUser Mod_Edit[] @relation("Mod_Edit_SubmittedByToUser") Map_Edit_mapperUserIdToUser Map_Edit[] @relation("Map_Edit_MapperUserIdToUser") Map_Edit_submittedByToUser Map_Edit[] @relation("Map_Edit_SubmittedByToUser") - Mod_New_submittedByToUser Mod_New[] + Mod_New_submittedByToUser Mod_New[] @relation("Mod_New_SubmittedByToUser") Map_NewWithMod_New_mapperUserIdToUser Map_NewWithMod_New[] @relation("Map_NewWithMod_New_MapperUserIdToUser") Map_NewWithMod_New_submittedByToUser Map_NewWithMod_New[] @relation("Map_NewWithMod_New_SubmittedByToUser") Map_NewSolo_mapperUserIdToUser Map_NewSolo[] @relation("Map_NewSolo_MapperUserIdToUser") @@ -690,6 +690,7 @@ enum User_AccountStatus { Deleted Banned Unlinked + Hidden // Used for the special CelesteModsList user that owns all of the mod submissions from the old spreadsheet } enum MapSide { From cecf35504b2d24641ba4cc40aa23d7e39f8f2e03 Mon Sep 17 00:00:00 2001 From: otobot1 Date: Sun, 20 Oct 2024 17:40:01 -0600 Subject: [PATCH 33/43] resolve remaining TODOs --- src/server/api/routers/user_userClaim/userClaim.ts | 2 +- src/server/api/schemas/userIdSchema_NonObject.ts | 2 +- src/server/auth/auth.ts | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/server/api/routers/user_userClaim/userClaim.ts b/src/server/api/routers/user_userClaim/userClaim.ts index 10483aea..d5323d2e 100644 --- a/src/server/api/routers/user_userClaim/userClaim.ts +++ b/src/server/api/routers/user_userClaim/userClaim.ts @@ -283,7 +283,6 @@ export const userClaimRouter = createTRPCRouter({ }; - //TODO!!!: make sure nothing was missed here // A deeply nested request *may* work here, but I'm not sure. It would be more work and harder to read, but would probably be marginally faster. IMO, it's not worth bothering. await ctx.prisma.$transaction( async (transactionPrisma) => { // using an interactive transaction to allow verifying that the claiming user still exists before making changes @@ -333,6 +332,7 @@ export const userClaimRouter = createTRPCRouter({ transactionPrisma.map_Archive.updateMany(updateApprovedBy as Prisma.Map_ArchiveUpdateManyArgs), transactionPrisma.mod_Edit.updateMany(updateSubmittedBy as Prisma.Mod_EditUpdateManyArgs), + transactionPrisma.map_Edit.updateMany(updateMapperUserId as Prisma.Map_EditUpdateManyArgs), transactionPrisma.map_Edit.updateMany(updateSubmittedBy as Prisma.Map_EditUpdateManyArgs), diff --git a/src/server/api/schemas/userIdSchema_NonObject.ts b/src/server/api/schemas/userIdSchema_NonObject.ts index d01bb05c..d4a75706 100644 --- a/src/server/api/schemas/userIdSchema_NonObject.ts +++ b/src/server/api/schemas/userIdSchema_NonObject.ts @@ -4,4 +4,4 @@ import { z } from "zod"; // this needs to be here to resolve webpack error in ~\src\server\api\routers\user_userClaim\userClaim.ts -export const userIdSchema_NonObject = z.string(); //TODO!!!: .cuid() doesn't work here, as sometimes a blank string is passed into trpc queries, which is not a valid cuid. Need to narrow this as much as possible without using .cuid(). \ No newline at end of file +export const userIdSchema_NonObject = z.string().min(0).max(100); \ No newline at end of file diff --git a/src/server/auth/auth.ts b/src/server/auth/auth.ts index 7ed5c2af..d6f68184 100644 --- a/src/server/auth/auth.ts +++ b/src/server/auth/auth.ts @@ -1,4 +1,4 @@ -import { type GetServerSidePropsContext } from "next"; +import type { GetServerSidePropsContext } from "next"; import { getServerSession, type NextAuthOptions, From fe926a9d508505497a0212c9d890db9e88137d4e Mon Sep 17 00:00:00 2001 From: otobot1 Date: Fri, 25 Oct 2024 17:51:12 -0600 Subject: [PATCH 34/43] remove remaining TODO!!! comments --- src/pages/index.tsx | 2 +- src/server/api/routers/user_userClaim/userClaim.ts | 2 +- src/server/auth/prismaAdapter.ts | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/pages/index.tsx b/src/pages/index.tsx index 1ec03d4d..0305e4df 100644 --- a/src/pages/index.tsx +++ b/src/pages/index.tsx @@ -68,7 +68,7 @@ const ClaimUserLink = ({ } - return signIn("discord", { callbackUrl: CLAIM_USER_PATHNAME })}>{linkText}; //TODO!!!: Refactor this to use a Next.js Link component with an href so that logged-out users can see where they're going before they click the link. This can be a followup issue. + return signIn("discord", { callbackUrl: CLAIM_USER_PATHNAME })}>{linkText}; }; diff --git a/src/server/api/routers/user_userClaim/userClaim.ts b/src/server/api/routers/user_userClaim/userClaim.ts index d5323d2e..41411b0d 100644 --- a/src/server/api/routers/user_userClaim/userClaim.ts +++ b/src/server/api/routers/user_userClaim/userClaim.ts @@ -36,7 +36,7 @@ const relatedUserSelectObject = { id: true, discordUsername: true, discordDiscriminator: true, -} satisfies { [Key in keyof RelatedUser]: true }; // make this relationship explicit/type-safe //TODO!!!: do this throughout the api, or make a follow-up issue to do so +} satisfies { [Key in keyof RelatedUser]: true }; // make this relationship explicit/type-safe const relatedUserSelect = Prisma.validator()(relatedUserSelectObject); diff --git a/src/server/auth/prismaAdapter.ts b/src/server/auth/prismaAdapter.ts index c7e55bb6..cb35bfbc 100644 --- a/src/server/auth/prismaAdapter.ts +++ b/src/server/auth/prismaAdapter.ts @@ -11,7 +11,7 @@ type CreateUser = (user: PrismaUser) => Awaitable; const getCreateUser = (prisma: PrismaClient): CreateUser => { return (user: PrismaUser): Awaitable => { - //@ts-expect-error //required because the Discord Provider assumes the standard user type from NextAuth core and returns the email properties, but we don't want to store emails in the database so Prisma will fail to create the user as it isn't expecting those properties. fixing this will probably require a custom provider. //TODO!!!: create a follow-up issue to determine if we are receiving email info from Discord and discarding it or if we are not receiving it at all + //@ts-expect-error //required because the Discord Provider assumes the standard user type from NextAuth core and returns the email properties, but we don't want to store emails in the database so Prisma will fail to create the user as it isn't expecting those properties. fixing this will probably require a custom provider. const { id, email, emailVerified, ...rest } = user; //remove the discord id from the user object - a replacement is auto-generated by Prisma. also remove the undefined and undesired email properties const trimmedUser = rest as TrimmedUser; From 0db568f9133c57aa306ba1b4fd9ef1bc04a6fe78 Mon Sep 17 00:00:00 2001 From: otobot1 Date: Sun, 5 Jan 2025 03:21:38 -0700 Subject: [PATCH 35/43] remove old settings button and rearrange remaining footer links --- src/components/layout/footer.tsx | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/src/components/layout/footer.tsx b/src/components/layout/footer.tsx index cf843187..fa4111cf 100644 --- a/src/components/layout/footer.tsx +++ b/src/components/layout/footer.tsx @@ -46,10 +46,10 @@ export const Footer = () => { position="apart" > - Settings + Cookie Policy { Join Our Discord Server! - Cookie Policy Privacy Policy From 77021eaedab6606a212550905c3edf28adaf9641 Mon Sep 17 00:00:00 2001 From: otobot1 Date: Sun, 5 Jan 2025 03:31:19 -0700 Subject: [PATCH 36/43] cleanup header JSX --- src/components/layout/header.tsx | 62 +++++++++++++++++++++++--------- 1 file changed, 46 insertions(+), 16 deletions(-) diff --git a/src/components/layout/header.tsx b/src/components/layout/header.tsx index 9c6c69c5..10438380 100644 --- a/src/components/layout/header.tsx +++ b/src/components/layout/header.tsx @@ -1,4 +1,4 @@ -import { Flex, createStyles, Title, Button } from "@mantine/core"; +import { Group, Stack, createStyles, Title, Button } from "@mantine/core"; import { signIn, signOut, useSession } from "next-auth/react"; import Image from "next/image"; import cmlLogo from "~/../public/images/logo/cml_logo.png"; @@ -36,21 +36,51 @@ export const Header = () => { return (
- - - Celeste Mods List - - {!session && } - {session && {session.user.name}} - {session && } - - + + + + + Celeste Mods List + + + {!session && ( + + )} + {session && ( + <> + + {session.user.name} + + + + )} + +
); }; \ No newline at end of file From c31764376a2a1b9dcfc516545b4f9a7cd0a14dbe Mon Sep 17 00:00:00 2001 From: otobot1 Date: Sun, 5 Jan 2025 04:26:57 -0700 Subject: [PATCH 37/43] add `as const` --- src/styles/layoutColors.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/styles/layoutColors.ts b/src/styles/layoutColors.ts index a372d360..4ab7b22e 100644 --- a/src/styles/layoutColors.ts +++ b/src/styles/layoutColors.ts @@ -1 +1 @@ -export const blackBackgroundColor = "rgb(25, 25, 25)"; // changed from "rgba(1, 1, 1, 0.9)" because the opacity causes problems when applying this color to the difficulty tab container. this is the same color though according to the table at the end of this article https://www.viget.com/articles/equating-color-and-transparency/#table \ No newline at end of file +export const blackBackgroundColor = "rgb(25, 25, 25)" as const; // changed from "rgba(1, 1, 1, 0.9)" because the opacity causes problems when applying this color to the difficulty tab container. this is the same color though according to the table at the end of this article https://www.viget.com/articles/equating-color-and-transparency/#table \ No newline at end of file From 31ef5ef1545b1561e04ab14032ce439bb37affd6 Mon Sep 17 00:00:00 2001 From: otobot1 Date: Sun, 5 Jan 2025 04:27:32 -0700 Subject: [PATCH 38/43] finish cleaning up header JSX and fixing CSS --- src/components/layout/header.tsx | 39 +++++++++++++++++++------------- 1 file changed, 23 insertions(+), 16 deletions(-) diff --git a/src/components/layout/header.tsx b/src/components/layout/header.tsx index 10438380..b6e35ab9 100644 --- a/src/components/layout/header.tsx +++ b/src/components/layout/header.tsx @@ -1,4 +1,4 @@ -import { Group, Stack, createStyles, Title, Button } from "@mantine/core"; +import { Group, Flex, createStyles, Title, Button } from "@mantine/core"; import { signIn, signOut, useSession } from "next-auth/react"; import Image from "next/image"; import cmlLogo from "~/../public/images/logo/cml_logo.png"; @@ -11,9 +11,10 @@ const useStyles = createStyles( (theme) => ({ header: { color: theme.white, + }, + group: { backgroundColor: blackBackgroundColor, - padding: "10px 45px", - alignItems: "center", + padding: "10px 45px", /*top and bottom | left and right*/ }, siteTitle: { fontSize: "45px", @@ -35,30 +36,36 @@ export const Header = () => { const { data: session } = useSession(); return ( -
- - + + - + Celeste Mods List - {!session && ( )} - + -
+ ); }; \ No newline at end of file From ba70659e8a99224f16647edae97c7d41fe5e1595 Mon Sep 17 00:00:00 2001 From: otobot1 Date: Sun, 5 Jan 2025 04:29:39 -0700 Subject: [PATCH 39/43] add more whitespace --- src/components/layout/footer.tsx | 24 +++++++++++++++++++----- 1 file changed, 19 insertions(+), 5 deletions(-) diff --git a/src/components/layout/footer.tsx b/src/components/layout/footer.tsx index fa4111cf..61cab31b 100644 --- a/src/components/layout/footer.tsx +++ b/src/components/layout/footer.tsx @@ -37,9 +37,15 @@ export const Footer = () => { const { classes } = useStyles(); return ( - -
-
+ +
+
{ align="center" spacing="1px" > - Cookie Policy + + Cookie Policy + { align="center" spacing="1px" > - Privacy Policy + + Privacy Policy +
From b7b63e03df8b3122fbf252a26c04e03e7f8af9f7 Mon Sep 17 00:00:00 2001 From: otobot1 Date: Sun, 5 Jan 2025 05:09:05 -0700 Subject: [PATCH 40/43] add new settings button --- .../filterPopovers/stringSearch.tsx | 2 +- src/components/layout/header.tsx | 151 +++++++++++++---- src/components/mods/maps/mapsTable.tsx | 156 +++++++++--------- src/styles/colorObjects.ts | 3 + src/styles/layoutColors.ts | 5 +- 5 files changed, 204 insertions(+), 113 deletions(-) create mode 100644 src/styles/colorObjects.ts diff --git a/src/components/filterPopovers/stringSearch.tsx b/src/components/filterPopovers/stringSearch.tsx index ee6033d9..1c8d5377 100644 --- a/src/components/filterPopovers/stringSearch.tsx +++ b/src/components/filterPopovers/stringSearch.tsx @@ -108,7 +108,7 @@ export const StringSearch = ({ value, setValue, iconProps, difficultyIndex }: St {...iconProps} size={iconProps?.size ?? 18 /*TODO!!: get this from MantineTheme*/} strokeWidth={iconProps?.strokeWidth ?? 1.5} - color={iconProps?.color ?? "white"} + color={iconProps?.color ?? "white" /*TODO!!: get this from MantineTheme*/} /> } diff --git a/src/components/layout/header.tsx b/src/components/layout/header.tsx index b6e35ab9..d618aed3 100644 --- a/src/components/layout/header.tsx +++ b/src/components/layout/header.tsx @@ -1,49 +1,104 @@ -import { Group, Flex, createStyles, Title, Button } from "@mantine/core"; +import Link from "next/link"; +import { Box, Group, Flex, createStyles, Title, Button, ActionIcon } from "@mantine/core"; import { signIn, signOut, useSession } from "next-auth/react"; import Image from "next/image"; import cmlLogo from "~/../public/images/logo/cml_logo.png"; -import { blackBackgroundColor } from "~/styles/layoutColors"; +import { Settings } from "tabler-icons-react"; +import { blackBackgroundColor, blackBackgroundHoverColor } from "~/styles/layoutColors"; +import { COMING_SOON_PATHNAME } from "~/consts/pathnames"; +import { colorObject } from "~/styles/colorObjects"; const useStyles = createStyles( - (theme) => ({ - header: { - color: theme.white, - }, - group: { - backgroundColor: blackBackgroundColor, - padding: "10px 45px", /*top and bottom | left and right*/ - }, - siteTitle: { - fontSize: "45px", - flexGrow: 1, - textAlign: "center", - margin: "0", - } - }) + (theme) => { + return ({ + header: { + color: theme.white, + }, + headerGroup: { + backgroundColor: blackBackgroundColor, + padding: "10px 45px", /*top and bottom | left and right*/ + }, + siteTitle: { + fontSize: "45px", + flexGrow: 1, + textAlign: "center", + margin: "0", + }, + iconLink: { + padding: "0 4px 0 0", /* top | right | bottom | left */ + }, + userSettingsGroup: { + padding: "4px 2px 4px 4px", /* top | right | bottom | left */ + columnGap: "5px", + borderRadius: "4px", + ":hover": { + backgroundColor: blackBackgroundHoverColor, + }, + "&&&&&": { + ":active": { + transform: "translateY(0.0625rem)", + }, + }, + }, + userName: { + ":active": { + transform: "none", + }, + }, + icon: colorObject(theme.white), + iconInLink: { + margin: "2px", + }, + iconNotInLink: { + padding: "4px 6px", /* top and bottom | left and right */ + }, + }); + }, +); + + + + +const USER_SETTINGS_ARIA_LABEL = "User settings"; + + +const SettingsIcon = ({ + className, +}: { + className: string; +}) => ( + ); export const Header = () => { - const { classes } = useStyles(); - const height = 115; - const width = height / 694 * 774; + const HEIGHT = 115; + const width = HEIGHT / 694 * 774; + const { data: session } = useSession(); + + const { classes, cx } = useStyles(); + + return (
{ @@ -63,22 +117,31 @@ export const Header = () => { >Celeste Mods List - {!session && ( - - )} {session && ( <> - - {session.user.name} - + + + + {session.user.name} + + + + )} + {!session && ( + <> + + + + + + + + )}
diff --git a/src/components/mods/maps/mapsTable.tsx b/src/components/mods/maps/mapsTable.tsx index 39fc11d5..09cbd556 100644 --- a/src/components/mods/maps/mapsTable.tsx +++ b/src/components/mods/maps/mapsTable.tsx @@ -1,7 +1,7 @@ import { type Dispatch, type SetStateAction, useEffect, useMemo, useState } from "react"; import Link from "next/link"; import { ActionIcon, Text, createStyles } from "@mantine/core"; -import { DataTable, type DataTableSortStatus } from "mantine-datatable"; +import { type DataTableSortStatus, DataTable } from "mantine-datatable"; import type { MapWithTechAndRatingInfo, Mod } from "~/components/mods/types"; import { CirclePlus } from "tabler-icons-react"; import { ModsTableTooltip } from "../modsTableTooltip"; @@ -11,6 +11,7 @@ import type { DifficultyColor } from "~/styles/difficultyColors"; import { getOrdinal } from "~/utils/getOrdinal"; import { COMING_SOON_PATHNAME } from "~/consts/pathnames"; import { truncateString } from "~/utils/truncateString"; +import { colorObject } from "~/styles/colorObjects"; @@ -25,92 +26,95 @@ const useStyles = createStyles( ( theme, { colors }: { colors: DifficultyColor; }, - ) => ({ - mapTable: { - // double ampersand to increase selectivity of class to ensure it overrides any other css - "&&": { - /* top | left and right | bottom */ - margin: `0 ${theme.spacing.sm} ${theme.spacing.xl}`, - backgroundColor: expandedModColors.default.backgroundColor, - }, - "&&&&&& table": { - borderSpacing: "0 8px", - // Border spacing adds space before the header, so we move the table up - transform: 'translate(0, -8px)', - "tbody": { - transform: 'translate(0, 3px)', + ) => { + return { + mapTable: { + // double ampersand to increase selectivity of class to ensure it overrides any other css + "&&": { + /* top | left and right | bottom */ + margin: `0 ${theme.spacing.sm} ${theme.spacing.xl}`, + backgroundColor: expandedModColors.default.backgroundColor, }, - }, - "&&&&&& thead": { - top: "0", - }, - "&&&& th": { - fontWeight: "bold", - border: "none", - backgroundColor: colors.primary.backgroundColor, - color: colors.primary.textColor, - // The down arrow appears blurry due to rotation, so we zoom in to fix that. - // https://stackoverflow.com/a/53556981 - ".mantine-Center-root": { - zoom: TABLE_HEADER_ARROW_ZOOM, + "&&&&&& table": { + borderSpacing: "0 8px", + // Border spacing adds space before the header, so we move the table up + transform: 'translate(0, -8px)', + "tbody": { + transform: 'translate(0, 3px)', + }, }, - "svg": { + "&&&&&& thead": { + top: "0", + }, + "&&&& th": { + fontWeight: "bold", + border: "none", + backgroundColor: colors.primary.backgroundColor, color: colors.primary.textColor, + // The down arrow appears blurry due to rotation, so we zoom in to fix that. + // https://stackoverflow.com/a/53556981 + ".mantine-Center-root": { + zoom: TABLE_HEADER_ARROW_ZOOM, + }, + "svg": { + color: colors.primary.textColor, + }, }, - }, - "&&&& th:hover": { - backgroundColor: colors.primaryHover.backgroundColor, - color: colors.primaryHover.textColor, - "svg": { + "&&&& th:hover": { + backgroundColor: colors.primaryHover.backgroundColor, color: colors.primaryHover.textColor, + "svg": { + color: colors.primaryHover.textColor, + }, }, }, - }, - leftColumnTitle: { - "&&": { - borderRadius: "20px 0 0 20px", + leftColumnTitle: { + "&&": { + borderRadius: "20px 0 0 20px", + }, }, - }, - rightColumnTitle: { - "&&": { - borderRadius: "0 20px 20px 0" + rightColumnTitle: { + "&&": { + borderRadius: "0 20px 20px 0" + }, }, - }, - columnCells: { - "&&&&": { - fontWeight: "bold", - backgroundColor: theme.white, - color: theme.black, - borderLeft: "none", - borderRight: "none", - borderTop: "2px solid", - borderBottom: "2px solid", - borderColor: colors.primary.backgroundColor, + columnCells: { + "&&&&": { + fontWeight: "bold", + backgroundColor: theme.white, + color: theme.black, + borderLeft: "none", + borderRight: "none", + borderTop: "2px solid", + borderBottom: "2px solid", + borderColor: colors.primary.backgroundColor, + }, }, - }, - leftColumnCells: { - "&&&&": { - fontWeight: "bold", - backgroundColor: theme.white, - color: theme.black, - borderRadius: "20px 0 0 20px", - border: "2px solid", - borderColor: colors.primary.backgroundColor, - borderRight: "none", + leftColumnCells: { + "&&&&": { + fontWeight: "bold", + backgroundColor: theme.white, + color: theme.black, + borderRadius: "20px 0 0 20px", + border: "2px solid", + borderColor: colors.primary.backgroundColor, + borderRight: "none", + }, }, - }, - rightColumnCells: { - "&&&&": { - fontWeight: "bold", - backgroundColor: theme.white, - color: theme.black, - borderRadius: "0 20px 20px 0", - border: "2px solid", - borderColor: colors.primary.backgroundColor, - borderLeft: "none", + rightColumnCells: { + "&&&&": { + fontWeight: "bold", + backgroundColor: theme.white, + color: theme.black, + borderRadius: "0 20px 20px 0", + border: "2px solid", + borderColor: colors.primary.backgroundColor, + borderLeft: "none", + }, }, - }, - }), + icon: colorObject(theme.black), + }; + }, ); @@ -417,7 +421,7 @@ export const MapsTable = ( aria-label="Rate this map" > diff --git a/src/styles/colorObjects.ts b/src/styles/colorObjects.ts new file mode 100644 index 00000000..0b73d0b9 --- /dev/null +++ b/src/styles/colorObjects.ts @@ -0,0 +1,3 @@ +export const colorObject = (color: string) => ({ + color, +}); \ No newline at end of file diff --git a/src/styles/layoutColors.ts b/src/styles/layoutColors.ts index 4ab7b22e..46135219 100644 --- a/src/styles/layoutColors.ts +++ b/src/styles/layoutColors.ts @@ -1 +1,4 @@ -export const blackBackgroundColor = "rgb(25, 25, 25)" as const; // changed from "rgba(1, 1, 1, 0.9)" because the opacity causes problems when applying this color to the difficulty tab container. this is the same color though according to the table at the end of this article https://www.viget.com/articles/equating-color-and-transparency/#table \ No newline at end of file +export const blackBackgroundColor = "rgb(25, 25, 25)" as const; // changed from "rgba(1, 1, 1, 0.9)" because the opacity causes problems when applying this color to the difficulty tab container. this is the same color though according to the table at the end of this article https://www.viget.com/articles/equating-color-and-transparency/#table + + +export const blackBackgroundHoverColor = "rgba(70, 78, 86, 0.3)" as const; \ No newline at end of file From dad1ec0d9a56c5ba57528811a8c09fe66cec7358 Mon Sep 17 00:00:00 2001 From: otobot1 Date: Sun, 5 Jan 2025 18:08:22 -0700 Subject: [PATCH 41/43] Update schema.prisma make user [discordUsername, discordDiscriminator] index non-unique to avoid errors when creating new accounts for legacy users --- prisma/schema.prisma | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/prisma/schema.prisma b/prisma/schema.prisma index dc62969a..bb3f48d7 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -634,7 +634,7 @@ model User { UserClaim_claimedUserIdToUser UserClaim[] @relation("UserClaim_ClaimedUserId") UserClaim_approvedByToUser UserClaim[] @relation("UserClaim_ApprovedBy") - @@unique([discordUsername, discordDiscriminator]) + @@index([discordUsername, discordDiscriminator]) @@map("user") } From 447295c0f905827e64ea92c24a48a419c21125e1 Mon Sep 17 00:00:00 2001 From: otobot1 Date: Sun, 5 Jan 2025 20:00:12 -0700 Subject: [PATCH 42/43] revert dad1ec0d9a56c5ba57528811a8c09fe66cec7358 --- prisma/schema.prisma | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/prisma/schema.prisma b/prisma/schema.prisma index bb3f48d7..dc62969a 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -634,7 +634,7 @@ model User { UserClaim_claimedUserIdToUser UserClaim[] @relation("UserClaim_ClaimedUserId") UserClaim_approvedByToUser UserClaim[] @relation("UserClaim_ApprovedBy") - @@index([discordUsername, discordDiscriminator]) + @@unique([discordUsername, discordDiscriminator]) @@map("user") } From 98453c774b2c15c1ce0e7675a31bbc1931101dd3 Mon Sep 17 00:00:00 2001 From: otobot1 Date: Sun, 5 Jan 2025 20:00:28 -0700 Subject: [PATCH 43/43] start troubleshooting verify user transaction --- src/server/api/routers/user_userClaim/userClaim.ts | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/src/server/api/routers/user_userClaim/userClaim.ts b/src/server/api/routers/user_userClaim/userClaim.ts index 41411b0d..196af322 100644 --- a/src/server/api/routers/user_userClaim/userClaim.ts +++ b/src/server/api/routers/user_userClaim/userClaim.ts @@ -12,6 +12,12 @@ import { ADMIN_PERMISSION_STRINGS, checkIsPrivileged, checkPermissions } from ". +const VERIFY_CLAIM_TRANSACTION_MAXWAIT_SECONDS = 5; +const VERIFY_CLAIM_TRANSACTION_TIMEOUT_SECONDS = 60; + + + + type RelatedUser = { id: string; discordUsername: string | null; @@ -361,7 +367,11 @@ export const userClaimRouter = createTRPCRouter({ id: claim.claimedUserId, } }); - } + }, + { + maxWait: VERIFY_CLAIM_TRANSACTION_MAXWAIT_SECONDS * 1000, + timeout: VERIFY_CLAIM_TRANSACTION_TIMEOUT_SECONDS * 1000, + }, );