Repository: MathiasGruber/TheNinjaRPG
Pull request #477: :sparkles: Allow updating userIds across database
Author: login: MathiasGruber
Created: 2025-04-20T17:08:14Z | Merged: 2025-04-20T18:02:54Z

# Pull Request
Allow me to update the userId of a user across the database, effectively assigning a user to a new clerk user

## License

By making this pull request, I confirm that I have the right to waive copyright and related rights to my contribution, and agree that all copyright and related rights in my contributions are waived, and I acknowledge that the Studie-Tech ApS organization has the copyright to use and modify my contribution for perpetuity.


<!-- This is an auto-generated comment: release notes by coderabbit.ai -->

## Summary by CodeRabbit

- **New Features**
  - Added a button for privileged users to update the user ID of a profile, accessible only to specific staff.
  - Introduced a confirmation dialog with form validation and warnings for this irreversible action.
  - Successful updates display a notification and refresh the user data.

- **Bug Fixes**
  - Ensured user ID changes are reflected across all related data for consistency.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->


Diff:
diff --git a/app/src/layout/PublicUser.tsx b/app/src/layout/PublicUser.tsx
index 332ebc77a..fedc9a63e 100644
--- a/app/src/layout/PublicUser.tsx
+++ b/app/src/layout/PublicUser.tsx
@@ -50,6 +50,7 @@ import {
   Plus,
   PersonStanding,
   MessageCircle,
+  IdCard,
 } from "lucide-react";
 import { updateUserSchema } from "@/validators/user";
 import { canChangeUserRole } from "@/utils/permissions";
@@ -229,6 +230,15 @@ const PublicUserComponent: React.FC<PublicUserComponentProps> = (props) => {
     },
   });
 
+  const updateUserId = api.staff.updateUserId.useMutation({
+    onSuccess: async (data) => {
+      showMutationToast(data);
+      if (data.success) {
+        await utils.profile.getPublicUser.invalidate();
+      }
+    },
+  });
+
   const unstuckUser = api.staff.forceAwake.useMutation({
     onSuccess: async (data) => {
       showMutationToast(data);
@@ -331,10 +341,17 @@ const PublicUserComponent: React.FC<PublicUserComponentProps> = (props) => {
         topRightContent={
           <div className="flex flex-row gap-1">
             {userData?.username === "Terriator" && (
-              <CopyCheck
-                className="h-6 w-6 cursor-pointer hover:text-orange-500"
-                onClick={() => cloneUser.mutate({ userId: profile.userId })}
-              />
+              <>
+                <CopyCheck
+                  className="h-6 w-6 cursor-pointer hover:text-orange-500"
+                  onClick={() => cloneUser.mutate({ userId: profile.userId })}
+                />
+                <UpdateUserIdButton
+                  userId={profile.userId}
+                  username={profile.username}
+                  updateUserIdMutation={updateUserId}
+                />
+              </>
             )}
             {userData && !userData.isBanned && !userData.isSilenced && (
               <NewConversationPrompt
@@ -1173,6 +1190,65 @@ const EditUserComponent: React.FC<EditUserComponentProps> = ({ userId, profile }
   );
 };
 
+interface UpdateUserIdButtonProps {
+  userId: string;
+  username: string;
+  updateUserIdMutation: ReturnType<typeof api.staff.updateUserId.useMutation>;
+}
+
+const UpdateUserIdButton: React.FC<UpdateUserIdButtonProps> = ({
+  userId,
+  username,
+  updateUserIdMutation,
+}) => {
+  // Create form with zod schema
+  const userIdForm = useForm<{ newUserId: string }>({
+    defaultValues: {
+      newUserId: userId,
+    },
+  });
+
+  // Handle form submission
+  const handleUpdateUserId = userIdForm.handleSubmit((data) => {
+    updateUserIdMutation.mutate({
+      userId: userId,
+      newUserId: data.newUserId,
+    });
+  });
+
+  return (
+    <Confirm
+      title="Update User ID"
+      proceed_label="Update"
+      button={<IdCard className="h-6 w-6 cursor-pointer hover:text-orange-500" />}
+      onAccept={handleUpdateUserId}
+      isValid={userIdForm.formState.isValid}
+    >
+      <Form {...userIdForm}>
+        <form className="space-y-4">
+          <p>
+            This will update the user ID for {username}. This action cannot be undone
+            and may affect database relationships.
+          </p>
+          <FormField
+            control={userIdForm.control}
+            name="newUserId"
+            render={({ field }) => (
+              <FormItem>
+                <FormLabel>New User ID</FormLabel>
+                <FormControl>
+                  <Input {...field} />
+                </FormControl>
+                <FormMessage />
+              </FormItem>
+            )}
+          />
+        </form>
+      </Form>
+    </Confirm>
+  );
+};
+
 interface TrainingStatsComponentProps {
   userId: string;
 }
diff --git a/app/src/server/api/routers/staff.ts b/app/src/server/api/routers/staff.ts
index c890fd99f..1b604836b 100644
--- a/app/src/server/api/routers/staff.ts
+++ b/app/src/server/api/routers/staff.ts
@@ -2,7 +2,53 @@ import { createTRPCRouter, protectedProcedure } from "@/server/api/trpc";
 import { baseServerResponse, errorResponse } from "@/server/api/trpc";
 import { fetchBadge } from "@/routers/badge";
 import { eq, and } from "drizzle-orm";
-import { actionLog, userData, userItem, userJutsu, userBadge } from "@/drizzle/schema";
+import {
+  actionLog,
+  aiProfile,
+  automatedModeration,
+  bankTransfers,
+  bloodlineRolls,
+  captcha,
+  conceptImage,
+  conversation,
+  conversationComment,
+  damageSimulation,
+  forumPost,
+  forumThread,
+  historicalAvatar,
+  jutsuLoadout,
+  kageDefendedChallenges,
+  linkPromotion,
+  mpvpBattleUser,
+  notification,
+  paypalSubscription,
+  paypalTransaction,
+  poll,
+  pollOption,
+  questHistory,
+  reportLog,
+  ryoTrade,
+  supportReview,
+  trainingLog,
+  user2conversation,
+  userAttribute,
+  userBadge,
+  userBlackList,
+  userData,
+  userItem,
+  userJutsu,
+  userLikes,
+  userNindo,
+  userPollVote,
+  userReport,
+  userReportComment,
+  userRequest,
+  userReview,
+  userRewards,
+  userUpload,
+  userVote,
+  village,
+} from "@/drizzle/schema";
 import { fetchUser } from "@/routers/profile";
 import { getServerPusher, updateUserOnMap } from "@/libs/pusher";
 import { z } from "zod";
@@ -216,6 +262,272 @@ export const staffRouter = createTRPCRouter({
       }
       return { success: true, message: "User copied" };
     }),
+  // Update all occurances of a user ID in the database to another userId.
+  // VERY dangerous - used to e.g. link up unlinked accounts with new userIds from clerk
+  updateUserId: protectedProcedure
+    .input(z.object({ userId: z.string(), newUserId: z.string() }))
+    .output(baseServerResponse)
+    .mutation(async ({ ctx, input }) => {
+      // Query
+      const [user, fromUser, toUser] = await Promise.all([
+        fetchUser(ctx.drizzle, ctx.userId),
+        fetchUser(ctx.drizzle, input.userId),
+        ctx.drizzle.query.userData.findFirst({
+          where: eq(userData.userId, input.newUserId),
+        }),
+      ]);
+      // Guard
+      if (toUser) {
+        return { success: false, message: "UserId already exists" };
+      }
+      if (user.username !== "Terriator") {
+        return { success: false, message: "You are not Terriator" };
+      }
+      if (fromUser.role !== "USER") {
+        return { success: false, message: "Cannot change staff member's userId " };
+      }
+      // Mutate
+      await Promise.all([
+        ctx.drizzle
+          .update(userData)
+          .set({ userId: input.newUserId })
+          .where(eq(userData.userId, input.userId)),
+        ctx.drizzle
+          .update(aiProfile)
+          .set({ userId: input.newUserId })
+          .where(eq(aiProfile.userId, input.userId)),
+        ctx.drizzle
+          .update(userBlackList)
+          .set({ creatorUserId: input.newUserId })
+          .where(eq(userBlackList.creatorUserId, input.userId)),
+        ctx.drizzle
+          .update(userBlackList)
+          .set({ targetUserId: input.newUserId })
+          .where(eq(userBlackList.targetUserId, input.userId)),
+        ctx.drizzle
+          .update(bloodlineRolls)
+          .set({ userId: input.newUserId })
+          .where(eq(bloodlineRolls.userId, input.userId)),
+        ctx.drizzle
+          .update(captcha)
+          .set({ userId: input.newUserId })
+          .where(eq(captcha.userId, input.userId)),
+        ctx.drizzle
+          .update(mpvpBattleUser)
+          .set({ userId: input.newUserId })
+          .where(eq(mpvpBattleUser.userId, input.userId)),
+        ctx.drizzle
+          .update(conversation)
+          .set({ createdById: input.newUserId })
+          .where(eq(conversation.createdById, input.userId)),
+        ctx.drizzle
+          .update(user2conversation)
+          .set({ userId: input.newUserId })
+          .where(eq(user2conversation.userId, input.userId)),
+        ctx.drizzle
+          .update(conversationComment)
+          .set({ userId: input.newUserId })
+          .where(eq(conversationComment.userId, input.userId)),
+        ctx.drizzle
+          .update(damageSimulation)
+          .set({ userId: input.newUserId })
+          .where(eq(damageSimulation.userId, input.userId)),
+        ctx.drizzle
+          .update(forumPost)
+          .set({ userId: input.newUserId })
+          .where(eq(forumPost.userId, input.userId)),
+        ctx.drizzle
+          .update(forumThread)
+          .set({ userId: input.newUserId })
+          .where(eq(forumThread.userId, input.userId)),
+        ctx.drizzle
+          .update(historicalAvatar)
+          .set({ userId: input.newUserId })
+          .where(eq(historicalAvatar.userId, input.userId)),
+        ctx.drizzle
+          .update(jutsuLoadout)
+          .set({ userId: input.newUserId })
+          .where(eq(jutsuLoadout.userId, input.userId)),
+        ctx.drizzle
+          .update(notification)
+          .set({ userId: input.newUserId })
+          .where(eq(notification.userId, input.userId)),
+        ctx.drizzle
+          .update(paypalSubscription)
+          .set({ createdById: input.newUserId })
+          .where(eq(paypalSubscription.createdById, input.userId)),
+        ctx.drizzle
+          .update(paypalSubscription)
+          .set({ affectedUserId: input.newUserId })
+          .where(eq(paypalSubscription.affectedUserId, input.userId)),
+        ctx.drizzle
+          .update(paypalTransaction)
+          .set({ affectedUserId: input.newUserId })
+          .where(eq(paypalTransaction.affectedUserId, input.userId)),
+        ctx.drizzle
+          .update(paypalTransaction)
+          .set({ createdById: input.newUserId })
+          .where(eq(paypalTransaction.createdById, input.userId)),
+        ctx.drizzle
+          .update(ryoTrade)
+          .set({ creatorUserId: input.newUserId })
+          .where(eq(ryoTrade.creatorUserId, input.userId)),
+        ctx.drizzle
+          .update(ryoTrade)
+          .set({ purchaserUserId: input.newUserId })
+          .where(eq(ryoTrade.purchaserUserId, input.userId)),
+        ctx.drizzle
+          .update(ryoTrade)
+          .set({ allowedPurchaserId: input.newUserId })
+          .where(eq(ryoTrade.allowedPurchaserId, input.userId)),
+        ctx.drizzle
+          .update(reportLog)
+          .set({ targetUserId: input.newUserId })
+          .where(eq(reportLog.targetUserId, input.userId)),
+        ctx.drizzle
+          .update(reportLog)
+          .set({ staffUserId: input.newUserId })
+          .where(eq(reportLog.staffUserId, input.userId)),
+        ctx.drizzle
+          .update(actionLog)
+          .set({ userId: input.newUserId })
+          .where(eq(actionLog.userId, input.userId)),
+        ctx.drizzle
+          .update(trainingLog)
+          .set({ userId: input.newUserId })
+          .where(eq(trainingLog.userId, input.userId)),
+        ctx.drizzle
+          .update(userAttribute)
+          .set({ userId: input.newUserId })
+          .where(eq(userAttribute.userId, input.userId)),
+        ctx.drizzle
+          .update(userReview)
+          .set({ authorUserId: input.newUserId })
+          .where(eq(userReview.authorUserId, input.userId)),
+        ctx.drizzle
+          .update(userRewards)
+          .set({ awardedById: input.newUserId })
+          .where(eq(userRewards.awardedById, input.userId)),
+        ctx.drizzle
+          .update(userRewards)
+          .set({ receiverId: input.newUserId })
+          .where(eq(userRewards.receiverId, input.userId)),
+        ctx.drizzle
+          .update(userReview)
+          .set({ targetUserId: input.newUserId })
+          .where(eq(userReview.targetUserId, input.userId)),
+        ctx.drizzle
+          .update(userNindo)
+          .set({ userId: input.newUserId })
+          .where(eq(userNindo.userId, input.userId)),
+        ctx.drizzle
+          .update(userItem)
+          .set({ userId: input.newUserId })
+          .where(eq(userItem.userId, input.userId)),
+        ctx.drizzle
+          .update(userJutsu)
+          .set({ userId: input.newUserId })
+          .where(eq(userJutsu.userId, input.userId)),
+        ctx.drizzle
+          .update(userReport)
+          .set({ reporterUserId: input.newUserId })
+          .where(eq(userReport.reporterUserId, input.userId)),
+        ctx.drizzle
+          .update(userReport)
+          .set({ reportedUserId: input.newUserId })
+          .where(eq(userReport.reportedUserId, input.userId)),
+        ctx.drizzle
+          .update(userReportComment)
+          .set({ userId: input.newUserId })
+          .where(eq(userReportComment.userId, input.userId)),
+        ctx.drizzle
+          .update(bankTransfers)
+          .set({ senderId: input.newUserId })
+          .where(eq(bankTransfers.senderId, input.userId)),
+        ctx.drizzle
+          .update(bankTransfers)
+          .set({ receiverId: input.newUserId })
+          .where(eq(bankTransfers.receiverId, input.userId)),
+        ctx.drizzle
+          .update(automatedModeration)
+          .set({ userId: input.newUserId })
+          .where(eq(automatedModeration.userId, input.userId)),
+        ctx.drizzle
+          .update(supportReview)
+          .set({ userId: input.newUserId })
+          .where(eq(supportReview.userId, input.userId)),
+        ctx.drizzle
+          .update(kageDefendedChallenges)
+          .set({ userId: input.newUserId })
+          .where(eq(kageDefendedChallenges.userId, input.userId)),
+        ctx.drizzle
+          .update(kageDefendedChallenges)
+          .set({ kageId: input.newUserId })
+          .where(eq(kageDefendedChallenges.kageId, input.userId)),
+        ctx.drizzle
+          .update(questHistory)
+          .set({ userId: input.newUserId })
+          .where(eq(questHistory.userId, input.userId)),
+        ctx.drizzle
+          .update(userLikes)
+          .set({ userId: input.newUserId })
+          .where(eq(userLikes.userId, input.userId)),
+        ctx.drizzle
+          .update(conceptImage)
+          .set({ userId: input.newUserId })
+          .where(eq(conceptImage.userId, input.userId)),
+        ctx.drizzle
+          .update(userBadge)
+          .set({ userId: input.newUserId })
+          .where(eq(userBadge.userId, input.userId)),
+        ctx.drizzle
+          .update(userRequest)
+          .set({ senderId: input.newUserId })
+          .where(eq(userRequest.senderId, input.userId)),
+        ctx.drizzle
+          .update(userRequest)
+          .set({ receiverId: input.newUserId })
+          .where(eq(userRequest.receiverId, input.userId)),
+        ctx.drizzle
+          .update(linkPromotion)
+          .set({ userId: input.newUserId })
+          .where(eq(linkPromotion.userId, input.userId)),
+        ctx.drizzle
+          .update(linkPromotion)
+          .set({ reviewedBy: input.newUserId })
+          .where(eq(linkPromotion.reviewedBy, input.userId)),
+        ctx.drizzle
+          .update(userVote)
+          .set({ userId: input.newUserId })
+          .where(eq(userVote.userId, input.userId)),
+        ctx.drizzle
+          .update(poll)
+          .set({ createdByUserId: input.newUserId })
+          .where(eq(poll.createdByUserId, input.userId)),
+        ctx.drizzle
+          .update(pollOption)
+          .set({ targetUserId: input.newUserId })
+          .where(eq(pollOption.targetUserId, input.userId)),
+        ctx.drizzle
+          .update(pollOption)
+          .set({ createdByUserId: input.newUserId })
+          .where(eq(pollOption.createdByUserId, input.userId)),
+        ctx.drizzle
+          .update(village)
+          .set({ kageId: input.newUserId })
+          .where(eq(village.kageId, input.userId)),
+        ctx.drizzle
+          .update(userPollVote)
+          .set({ userId: input.newUserId })
+          .where(eq(userPollVote.userId, input.userId)),
+        ctx.drizzle
+          .update(userUpload)
+          .set({ userId: input.newUserId })
+          .where(eq(userUpload.userId, input.userId)),
+      ]);
+
+      return { success: true, message: "UserId updated" };
+    }),
 });
 
 export type staffRouter = inferRouterOutputs<typeof staffRouter>;


Reviews:
user: login: copilot-pull-request-reviewer[bot]
body: ## Pull Request Overview

This pull request introduces a new mutation endpoint to update a user’s ID across multiple tables in the database and integrates a corresponding UI component to trigger this update.  
- Adds the updateUserId mutation in the staff router with proper guard checks before performing updates.  
- Integrates an UpdateUserIdButton component into the PublicUser layout for administrators to trigger the mutation.

### Reviewed Changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated 1 comment.

| File                                             | Description                                                                                 |
| ------------------------------------------------ | ------------------------------------------------------------------------------------------- |
| app/src/server/api/routers/staff.ts              | Adds a bulk update mutation to change a user’s ID across many tables with guard conditions. |
| app/src/layout/PublicUser.tsx                    | Introduces the UpdateUserIdButton component and integrates the mutation into the UI.         |




state: COMMENTED
author_association: NONE
submitted_at: 2025-04-20T17:08:50Z
user: login: coderabbitai[bot]
body: **Actionable comments posted: 2**

<details>
<summary>📜 Review details</summary>

**Configuration used: CodeRabbit UI**
**Review profile: CHILL**
**Plan: Pro**

<details>
<summary>📥 Commits</summary>

Reviewing files that changed from the base of the PR and between e7cbe7cd4202339d6c36479e696b2f3871ea354e and b600039285bc14345cab4e30db502d39b6077d9f.

</details>

<details>
<summary>📒 Files selected for processing (2)</summary>

* `app/src/layout/PublicUser.tsx` (4 hunks)
* `app/src/server/api/routers/staff.ts` (2 hunks)

</details>

<details>
<summary>🧰 Additional context used</summary>

<details>
<summary>🧬 Code Graph Analysis (2)</summary>

<details>
<summary>app/src/layout/PublicUser.tsx (1)</summary><blockquote>

<details>
<summary>app/src/libs/toast.tsx (1)</summary>

* `showMutationToast` (12-45)

</details>

</blockquote></details>
<details>
<summary>app/src/server/api/routers/staff.ts (3)</summary><blockquote>

<details>
<summary>app/src/server/api/trpc.ts (2)</summary>

* `protectedProcedure` (169-171)
* `baseServerResponse` (183-186)

</details>
<details>
<summary>app/src/server/api/routers/profile.ts (1)</summary>

* `fetchUser` (1358-1366)

</details>
<details>
<summary>app/drizzle/schema.ts (45)</summary>

* `userData` (1325-1489)
* `aiProfile` (113-126)
* `userBlackList` (305-321)
* `bloodlineRolls` (375-398)
* `captcha` (408-426)
* `mpvpBattleUser` (528-544)
* `conversation` (660-681)
* `user2conversation` (688-707)
* `conversationComment` (720-746)
* `damageSimulation` (763-780)
* `forumPost` (806-824)
* `forumThread` (838-861)
* `historicalAvatar` (871-898)
* `jutsuLoadout` (1020-1035)
* `notification` (1037-1055)
* `paypalSubscription` (1064-1093)
* `paypalTransaction` (1106-1140)
* `ryoTrade` (1166-1185)
* `reportLog` (1194-1211)
* `actionLog` (1213-1235)
* `trainingLog` (1244-1266)
* `userAttribute` (1268-1284)
* `userReview` (1586-1605)
* `userRewards` (2371-2391)
* `userNindo` (1607-1623)
* `userItem` (1626-1649)
* `userJutsu` (1664-1691)
* `userReport` (1705-1740)
* `userReportComment` (1755-1774)
* `bankTransfers` (2254-2271)
* `automatedModeration` (1784-1816)
* `supportReview` (1843-1855)
* `kageDefendedChallenges` (2012-2032)
* `questHistory` (2117-2141)
* `userLikes` (2184-2200)
* `conceptImage` (2202-2236)
* `userBadge` (2306-2321)
* `userRequest` (2335-2357)
* `linkPromotion` (2432-2455)
* `userVote` (2471-2497)
* `poll` (2636-2658)
* `pollOption` (2672-2697)
* `village` (1857-1905)
* `userPollVote` (2718-2739)
* `userUpload` (2759-2777)

</details>

</blockquote></details>

</details>

</details>

<details>
<summary>⏰ Context from checks skipped due to timeout of 90000ms (3)</summary>

* GitHub Check: analyze
* GitHub Check: build-test
* GitHub Check: Analyze (javascript)

</details>

<details>
<summary>🔇 Additional comments (12)</summary><blockquote>

<details>
<summary>app/src/server/api/routers/staff.ts (6)</summary>

`5-51`: **New imports are added to support database table updates.**

Adding these imports is necessary to reference all tables needed for the comprehensive user ID update across the database. The list appears complete and matches the tables that will be updated in the mutation.

---

`265-267`: **Code comments effectively describe the purpose and risk.**

The comments clearly explain this is a dangerous operation used for linking unlinked accounts with new IDs from clerk. This helps future developers understand the purpose and significance of this mutation.

---

`268-270`: **Protected mutation with appropriate inputs and response type.**

The mutation is correctly set up as a protected procedure requiring authentication, with proper input validation using zod and the standardized response output type.

---

`272-278`: **User validation is thoroughly implemented.**

The code properly fetches the current user, the user to be updated, and checks if the target user ID already exists before proceeding.

---

`280-288`: **Security checks are properly implemented.**

The mutation has three important guards:
1. Ensures the new user ID doesn't already exist
2. Restricts the operation to the privileged user "Terriator"
3. Prevents staff user IDs from being changed

These are essential security measures for such a powerful operation.

---

`528-530`: **Successful completion returns appropriate response.**

The mutation returns a clear success message when the operation completes.

</details>
<details>
<summary>app/src/layout/PublicUser.tsx (6)</summary>

`53-53`: **Import IdCard icon for the new update button.**

The IdCard icon is an appropriate visual cue for the user ID update functionality.

---

`233-240`: **Mutation hook with proper invalidation.**

The updateUserId mutation hook is correctly implemented with success handling to show a toast notification and invalidate the public user cache, ensuring the UI reflects the updated state.

---

`349-354`: **Conditional rendering of the update button.**

The UpdateUserIdButton is correctly conditionally rendered only for the "Terriator" user, maintaining the privilege restriction established in the backend.

---

`1193-1197`: **Props interface is well-defined.**

The UpdateUserIdButtonProps interface clearly defines the required props for the component, including the user ID, username, and the mutation hook.

---

`1211-1218`: **Form submission handler is properly implemented.**

The form submission handler correctly calls the mutation with the current user ID and the new user ID from the form data.

---

`1219-1249`: **Component provides clear warnings about the operation.**

The component effectively uses a confirmation dialog and warns users about the irreversible nature of the action and its potential impact on database relationships.

</details>

</blockquote></details>

</details>

<!-- This is an auto-generated comment by CodeRabbit for review status -->
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-20T17:11:11Z

Review comments:
diff_hunk: @@ -216,6 +262,272 @@ export const staffRouter = createTRPCRouter({
       }
       return { success: true, message: "User copied" };
     }),
+  // Update all occurances of a user ID in the database to another userId.
+  // VERY dangerous - used to e.g. link up unlinked accounts with new userIds from clerk
+  updateUserId: protectedProcedure
+    .input(z.object({ userId: z.string(), newUserId: z.string() }))
+    .output(baseServerResponse)
+    .mutation(async ({ ctx, input }) => {
+      // Query
+      const [user, fromUser, toUser] = await Promise.all([
+        fetchUser(ctx.drizzle, ctx.userId),
+        fetchUser(ctx.drizzle, input.userId),
+        ctx.drizzle.query.userData.findFirst({
+          where: eq(userData.userId, input.newUserId),
+        }),
+      ]);
+      // Guard
+      if (toUser) {
+        return { success: false, message: "UserId already exists" };
+      }
+      if (user.username !== "Terriator") {
+        return { success: false, message: "You are not Terriator" };
+      }
+      if (fromUser.role !== "USER") {
+        return { success: false, message: "Cannot change staff member's userId " };
+      }
+      // Mutate
+      await Promise.all([
path: app/src/server/api/routers/staff.ts
user: login: Copilot
body: Consider wrapping the multiple update operations in a single database transaction to ensure atomicity. Without a transaction, if one update fails, the database may be left in an inconsistent state.
created_at: 2025-04-20T17:08:50Z
author_association: NONE
diff_hunk: @@ -216,6 +262,272 @@ export const staffRouter = createTRPCRouter({
       }
       return { success: true, message: "User copied" };
     }),
+  // Update all occurances of a user ID in the database to another userId.
+  // VERY dangerous - used to e.g. link up unlinked accounts with new userIds from clerk
+  updateUserId: protectedProcedure
+    .input(z.object({ userId: z.string(), newUserId: z.string() }))
+    .output(baseServerResponse)
+    .mutation(async ({ ctx, input }) => {
+      // Query
+      const [user, fromUser, toUser] = await Promise.all([
+        fetchUser(ctx.drizzle, ctx.userId),
+        fetchUser(ctx.drizzle, input.userId),
+        ctx.drizzle.query.userData.findFirst({
+          where: eq(userData.userId, input.newUserId),
+        }),
+      ]);
+      // Guard
+      if (toUser) {
+        return { success: false, message: "UserId already exists" };
+      }
+      if (user.username !== "Terriator") {
+        return { success: false, message: "You are not Terriator" };
+      }
+      if (fromUser.role !== "USER") {
+        return { success: false, message: "Cannot change staff member's userId " };
+      }
+      // Mutate
+      await Promise.all([
+        ctx.drizzle
+          .update(userData)
+          .set({ userId: input.newUserId })
+          .where(eq(userData.userId, input.userId)),
+        ctx.drizzle
+          .update(aiProfile)
+          .set({ userId: input.newUserId })
+          .where(eq(aiProfile.userId, input.userId)),
+        ctx.drizzle
+          .update(userBlackList)
+          .set({ creatorUserId: input.newUserId })
+          .where(eq(userBlackList.creatorUserId, input.userId)),
+        ctx.drizzle
+          .update(userBlackList)
+          .set({ targetUserId: input.newUserId })
+          .where(eq(userBlackList.targetUserId, input.userId)),
+        ctx.drizzle
+          .update(bloodlineRolls)
+          .set({ userId: input.newUserId })
+          .where(eq(bloodlineRolls.userId, input.userId)),
+        ctx.drizzle
+          .update(captcha)
+          .set({ userId: input.newUserId })
+          .where(eq(captcha.userId, input.userId)),
+        ctx.drizzle
+          .update(mpvpBattleUser)
+          .set({ userId: input.newUserId })
+          .where(eq(mpvpBattleUser.userId, input.userId)),
+        ctx.drizzle
+          .update(conversation)
+          .set({ createdById: input.newUserId })
+          .where(eq(conversation.createdById, input.userId)),
+        ctx.drizzle
+          .update(user2conversation)
+          .set({ userId: input.newUserId })
+          .where(eq(user2conversation.userId, input.userId)),
+        ctx.drizzle
+          .update(conversationComment)
+          .set({ userId: input.newUserId })
+          .where(eq(conversationComment.userId, input.userId)),
+        ctx.drizzle
+          .update(damageSimulation)
+          .set({ userId: input.newUserId })
+          .where(eq(damageSimulation.userId, input.userId)),
+        ctx.drizzle
+          .update(forumPost)
+          .set({ userId: input.newUserId })
+          .where(eq(forumPost.userId, input.userId)),
+        ctx.drizzle
+          .update(forumThread)
+          .set({ userId: input.newUserId })
+          .where(eq(forumThread.userId, input.userId)),
+        ctx.drizzle
+          .update(historicalAvatar)
+          .set({ userId: input.newUserId })
+          .where(eq(historicalAvatar.userId, input.userId)),
+        ctx.drizzle
+          .update(jutsuLoadout)
+          .set({ userId: input.newUserId })
+          .where(eq(jutsuLoadout.userId, input.userId)),
+        ctx.drizzle
+          .update(notification)
+          .set({ userId: input.newUserId })
+          .where(eq(notification.userId, input.userId)),
+        ctx.drizzle
+          .update(paypalSubscription)
+          .set({ createdById: input.newUserId })
+          .where(eq(paypalSubscription.createdById, input.userId)),
+        ctx.drizzle
+          .update(paypalSubscription)
+          .set({ affectedUserId: input.newUserId })
+          .where(eq(paypalSubscription.affectedUserId, input.userId)),
+        ctx.drizzle
+          .update(paypalTransaction)
+          .set({ affectedUserId: input.newUserId })
+          .where(eq(paypalTransaction.affectedUserId, input.userId)),
+        ctx.drizzle
+          .update(paypalTransaction)
+          .set({ createdById: input.newUserId })
+          .where(eq(paypalTransaction.createdById, input.userId)),
+        ctx.drizzle
+          .update(ryoTrade)
+          .set({ creatorUserId: input.newUserId })
+          .where(eq(ryoTrade.creatorUserId, input.userId)),
+        ctx.drizzle
+          .update(ryoTrade)
+          .set({ purchaserUserId: input.newUserId })
+          .where(eq(ryoTrade.purchaserUserId, input.userId)),
+        ctx.drizzle
+          .update(ryoTrade)
+          .set({ allowedPurchaserId: input.newUserId })
+          .where(eq(ryoTrade.allowedPurchaserId, input.userId)),
+        ctx.drizzle
+          .update(reportLog)
+          .set({ targetUserId: input.newUserId })
+          .where(eq(reportLog.targetUserId, input.userId)),
+        ctx.drizzle
+          .update(reportLog)
+          .set({ staffUserId: input.newUserId })
+          .where(eq(reportLog.staffUserId, input.userId)),
+        ctx.drizzle
+          .update(actionLog)
+          .set({ userId: input.newUserId })
+          .where(eq(actionLog.userId, input.userId)),
+        ctx.drizzle
+          .update(trainingLog)
+          .set({ userId: input.newUserId })
+          .where(eq(trainingLog.userId, input.userId)),
+        ctx.drizzle
+          .update(userAttribute)
+          .set({ userId: input.newUserId })
+          .where(eq(userAttribute.userId, input.userId)),
+        ctx.drizzle
+          .update(userReview)
+          .set({ authorUserId: input.newUserId })
+          .where(eq(userReview.authorUserId, input.userId)),
+        ctx.drizzle
+          .update(userRewards)
+          .set({ awardedById: input.newUserId })
+          .where(eq(userRewards.awardedById, input.userId)),
+        ctx.drizzle
+          .update(userRewards)
+          .set({ receiverId: input.newUserId })
+          .where(eq(userRewards.receiverId, input.userId)),
+        ctx.drizzle
+          .update(userReview)
+          .set({ targetUserId: input.newUserId })
+          .where(eq(userReview.targetUserId, input.userId)),
+        ctx.drizzle
+          .update(userNindo)
+          .set({ userId: input.newUserId })
+          .where(eq(userNindo.userId, input.userId)),
+        ctx.drizzle
+          .update(userItem)
+          .set({ userId: input.newUserId })
+          .where(eq(userItem.userId, input.userId)),
+        ctx.drizzle
+          .update(userJutsu)
+          .set({ userId: input.newUserId })
+          .where(eq(userJutsu.userId, input.userId)),
+        ctx.drizzle
+          .update(userReport)
+          .set({ reporterUserId: input.newUserId })
+          .where(eq(userReport.reporterUserId, input.userId)),
+        ctx.drizzle
+          .update(userReport)
+          .set({ reportedUserId: input.newUserId })
+          .where(eq(userReport.reportedUserId, input.userId)),
+        ctx.drizzle
+          .update(userReportComment)
+          .set({ userId: input.newUserId })
+          .where(eq(userReportComment.userId, input.userId)),
+        ctx.drizzle
+          .update(bankTransfers)
+          .set({ senderId: input.newUserId })
+          .where(eq(bankTransfers.senderId, input.userId)),
+        ctx.drizzle
+          .update(bankTransfers)
+          .set({ receiverId: input.newUserId })
+          .where(eq(bankTransfers.receiverId, input.userId)),
+        ctx.drizzle
+          .update(automatedModeration)
+          .set({ userId: input.newUserId })
+          .where(eq(automatedModeration.userId, input.userId)),
+        ctx.drizzle
+          .update(supportReview)
+          .set({ userId: input.newUserId })
+          .where(eq(supportReview.userId, input.userId)),
+        ctx.drizzle
+          .update(kageDefendedChallenges)
+          .set({ userId: input.newUserId })
+          .where(eq(kageDefendedChallenges.userId, input.userId)),
+        ctx.drizzle
+          .update(kageDefendedChallenges)
+          .set({ kageId: input.newUserId })
+          .where(eq(kageDefendedChallenges.kageId, input.userId)),
+        ctx.drizzle
+          .update(questHistory)
+          .set({ userId: input.newUserId })
+          .where(eq(questHistory.userId, input.userId)),
+        ctx.drizzle
+          .update(userLikes)
+          .set({ userId: input.newUserId })
+          .where(eq(userLikes.userId, input.userId)),
+        ctx.drizzle
+          .update(conceptImage)
+          .set({ userId: input.newUserId })
+          .where(eq(conceptImage.userId, input.userId)),
+        ctx.drizzle
+          .update(userBadge)
+          .set({ userId: input.newUserId })
+          .where(eq(userBadge.userId, input.userId)),
+        ctx.drizzle
+          .update(userRequest)
+          .set({ senderId: input.newUserId })
+          .where(eq(userRequest.senderId, input.userId)),
+        ctx.drizzle
+          .update(userRequest)
+          .set({ receiverId: input.newUserId })
+          .where(eq(userRequest.receiverId, input.userId)),
+        ctx.drizzle
+          .update(linkPromotion)
+          .set({ userId: input.newUserId })
+          .where(eq(linkPromotion.userId, input.userId)),
+        ctx.drizzle
+          .update(linkPromotion)
+          .set({ reviewedBy: input.newUserId })
+          .where(eq(linkPromotion.reviewedBy, input.userId)),
+        ctx.drizzle
+          .update(userVote)
+          .set({ userId: input.newUserId })
+          .where(eq(userVote.userId, input.userId)),
+        ctx.drizzle
+          .update(poll)
+          .set({ createdByUserId: input.newUserId })
+          .where(eq(poll.createdByUserId, input.userId)),
+        ctx.drizzle
+          .update(pollOption)
+          .set({ targetUserId: input.newUserId })
+          .where(eq(pollOption.targetUserId, input.userId)),
+        ctx.drizzle
+          .update(pollOption)
+          .set({ createdByUserId: input.newUserId })
+          .where(eq(pollOption.createdByUserId, input.userId)),
+        ctx.drizzle
+          .update(village)
+          .set({ kageId: input.newUserId })
+          .where(eq(village.kageId, input.userId)),
+        ctx.drizzle
+          .update(userPollVote)
+          .set({ userId: input.newUserId })
+          .where(eq(userPollVote.userId, input.userId)),
+        ctx.drizzle
+          .update(userUpload)
+          .set({ userId: input.newUserId })
+          .where(eq(userUpload.userId, input.userId)),
+      ]);
path: app/src/server/api/routers/staff.ts
user: login: coderabbitai[bot]
body: _🛠️ Refactor suggestion_

**Consider using a database transaction for better atomicity.**

The function updates multiple tables using Promise.all, which helps with concurrent execution. However, if any update fails, the database could be left in an inconsistent state with some references updated and others not.

Consider wrapping all updates in a database transaction to ensure atomicity:

```diff
- await Promise.all([
+ await ctx.drizzle.transaction(async (tx) => {
+   await Promise.all([
      // All your update operations here
+   ]);
+ });
```

This would ensure that either all updates succeed or none of them do, maintaining database integrity.


> Committable suggestion skipped: line range outside the PR's diff.

<!-- This is an auto-generated comment by CodeRabbit -->
created_at: 2025-04-20T17:11:10Z
author_association: CONTRIBUTOR
diff_hunk: @@ -1173,6 +1190,65 @@ const EditUserComponent: React.FC<EditUserComponentProps> = ({ userId, profile }
   );
 };
 
+interface UpdateUserIdButtonProps {
+  userId: string;
+  username: string;
+  updateUserIdMutation: ReturnType<typeof api.staff.updateUserId.useMutation>;
+}
+
+const UpdateUserIdButton: React.FC<UpdateUserIdButtonProps> = ({
+  userId,
+  username,
+  updateUserIdMutation,
+}) => {
+  // Create form with zod schema
+  const userIdForm = useForm<{ newUserId: string }>({
+    defaultValues: {
+      newUserId: userId,
+    },
+  });
path: app/src/layout/PublicUser.tsx
user: login: coderabbitai[bot]
body: _🛠️ Refactor suggestion_

**Add form validation for the new user ID.**

The form initializes with the current user ID as the default value but lacks specific validation rules for the new user ID. This could allow submission of invalid IDs that would fail at the backend.

Consider adding validation rules to ensure the new ID meets required format:

```diff
+ import { z } from "zod";
+ import { zodResolver } from "@hookform/resolvers/zod";

+ const updateUserIdSchema = z.object({
+   newUserId: z.string().min(5).max(50).regex(/^[a-zA-Z0-9_-]+$/, 
+     "ID must contain only letters, numbers, underscores, and hyphens")
+ });

  const userIdForm = useForm<{ newUserId: string }>({
+   resolver: zodResolver(updateUserIdSchema),
    defaultValues: {
      newUserId: userId,
    },
  });
```

<!-- This is an auto-generated comment by CodeRabbit -->
created_at: 2025-04-20T17:11:10Z
author_association: CONTRIBUTOR

PR comments:
user: login: vercel[bot]
created_at: 2025-04-20T17:08:18Z
body: [vc]: #qlDSmk7UKaYGBwle4IqFrw4bRW7khgFZ0sKZ/AV3S7o=:eyJpc01vbm9yZXBvIjp0cnVlLCJ0eXBlIjoiZ2l0aHViIiwicHJvamVjdHMiOlt7Im5hbWUiOiJ0bnIiLCJpbnNwZWN0b3JVcmwiOiJodHRwczovL3ZlcmNlbC5jb20vdGhlLW5pbmphLXJwZy90bnIvMlFLVFUyRkw4WUFnNXJocjZpZVdieEJnTDhwTiIsInByZXZpZXdVcmwiOiIiLCJuZXh0Q29tbWl0U3RhdHVzIjoiRkFJTEVEIiwibGl2ZUZlZWRiYWNrIjp7InJlc29sdmVkIjowLCJ1bnJlc29sdmVkIjowLCJ0b3RhbCI6MCwibGluayI6IiJ9LCJyb290RGlyZWN0b3J5IjoiYXBwIn1dfQ==
**The latest updates on your projects**. Learn more about [Vercel for Git ↗︎](https://vercel.link/github-learn-more)

| Name | Status | Preview | Comments | Updated (UTC) |
| :--- | :----- | :------ | :------- | :------ |
| **tnr** | ❌ Failed ([Inspect](https://vercel.com/the-ninja-rpg/tnr/2QKTU2FL8YAg5rhr6ieWbxBgL8pN)) |  |  | Apr 20, 2025 5:53pm |


author_association: NONE
user: login: coderabbitai[bot]
created_at: 2025-04-20T17:08:20Z
body: <!-- This is an auto-generated comment: summarize by coderabbit.ai -->
<!-- walkthrough_start -->

## Walkthrough

A new feature has been introduced allowing privileged users, specifically those with the username "Terriator", to update a user's ID through the application interface. This is accomplished by adding a new mutation to the staff API and a corresponding button and dialog in the public user profile UI. The backend mutation performs comprehensive updates across numerous database tables to replace the old user ID with the new one, ensuring data consistency. The frontend provides a confirmation dialog and form for entering the new user ID, with appropriate validation and user feedback.

## Changes

| File(s)                                         | Change Summary                                                                                                          |
|-------------------------------------------------|-------------------------------------------------------------------------------------------------------------------------|
| app/src/layout/PublicUser.tsx                   | Added `updateUserId` mutation hook; introduced `UpdateUserIdButton` component with dialog and form; conditional rendering for privileged users. |
| app/src/server/api/routers/staff.ts             | Added `updateUserId` protected mutation to `staffRouter`; performs extensive updates across many tables to change user ID. |

## Sequence Diagram(s)

```mermaid
sequenceDiagram
    participant Terriator as Privileged User
    participant UI as PublicUserComponent
    participant API as staff.updateUserId Mutation
    participant DB as Database

    Terriator->>UI: Clicks Update User ID Button
    UI->>Terriator: Shows confirmation dialog and form
    Terriator->>UI: Enters new user ID and submits
    UI->>API: Calls updateUserId mutation with old and new user IDs
    API->>DB: Checks guards (user exists, role, new ID unused)
    API->>DB: Updates userId in all related tables
    API-->>UI: Returns success response
    UI-->>Terriator: Shows success toast and updates UI
```

## Poem

> 🐇  
> A magic button for Terriator’s hand,  
> To change a user’s ID across the land.  
> With checks and warnings, a dialog appears,  
> Ensuring no data disappears.  
> The backend scurries, tables align,  
> All in a toast—“Success!”—so fine!  
> Hop, hop, hooray for the update design!

<!-- walkthrough_end -->
<!-- internal state start -->


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

<!-- internal state end -->
<!-- finishing_touch_checkbox_start -->

<details>
<summary>✨ Finishing Touches</summary>

- [ ] <!-- {"checkboxId": "7962f53c-55bc-4827-bfbf-6a18da830691"} --> 📝 Generate Docstrings

</details>

<!-- finishing_touch_checkbox_end -->
<!-- tips_start -->

---

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

<details>
<summary>❤️ Share</summary>

- [X](https://twitter.com/intent/tweet?text=I%20just%20used%20%40coderabbitai%20for%20my%20code%20review%2C%20and%20it%27s%20fantastic%21%20It%27s%20free%20for%20OSS%20and%20offers%20a%20free%20trial%20for%20the%20proprietary%20code.%20Check%20it%20out%3A&url=https%3A//coderabbit.ai)
- [Mastodon](https://mastodon.social/share?text=I%20just%20used%20%40coderabbitai%20for%20my%20code%20review%2C%20and%20it%27s%20fantastic%21%20It%27s%20free%20for%20OSS%20and%20offers%20a%20free%20trial%20for%20the%20proprietary%20code.%20Check%20it%20out%3A%20https%3A%2F%2Fcoderabbit.ai)
- [Reddit](https://www.reddit.com/submit?title=Great%20tool%20for%20code%20review%20-%20CodeRabbit&text=I%20just%20used%20CodeRabbit%20for%20my%20code%20review%2C%20and%20it%27s%20fantastic%21%20It%27s%20free%20for%20OSS%20and%20offers%20a%20free%20trial%20for%20proprietary%20code.%20Check%20it%20out%3A%20https%3A//coderabbit.ai)
- [LinkedIn](https://www.linkedin.com/sharing/share-offsite/?url=https%3A%2F%2Fcoderabbit.ai&mini=true&title=Great%20tool%20for%20code%20review%20-%20CodeRabbit&summary=I%20just%20used%20CodeRabbit%20for%20my%20code%20review%2C%20and%20it%27s%20fantastic%21%20It%27s%20free%20for%20OSS%20and%20offers%20a%20free%20trial%20for%20proprietary%20code)

</details>

<details>
<summary>🪧 Tips</summary>

### Chat

There are 3 ways to chat with [CodeRabbit](https://coderabbit.ai?utm_source=oss&utm_medium=github&utm_campaign=MathiasGruber/TheNinjaRPG&utm_content=477):

- Review comments: Directly reply to a review comment made by CodeRabbit. Example:
  - `I pushed a fix in commit <commit_id>, please review it.`
  - `Generate unit testing code for this file.`
  - `Open a follow-up GitHub issue for this discussion.`
- Files and specific lines of code (under the "Files changed" tab): Tag `@coderabbitai` in a new review comment at the desired location with your query. Examples:
  - `@coderabbitai generate unit testing code for this file.`
  -	`@coderabbitai modularize this function.`
- PR comments: Tag `@coderabbitai` in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
  - `@coderabbitai gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.`
  - `@coderabbitai read src/utils.ts and generate unit testing code.`
  - `@coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.`
  - `@coderabbitai help me debug CodeRabbit configuration file.`

Note: Be mindful of the bot's finite context window. It's strongly recommended to break down tasks such as reading entire modules into smaller chunks. For a focused discussion, use review comments to chat about specific files and their changes, instead of using the PR comments.

### CodeRabbit Commands (Invoked using PR comments)

- `@coderabbitai pause` to pause the reviews on a PR.
- `@coderabbitai resume` to resume the paused reviews.
- `@coderabbitai review` to trigger an incremental review. This is useful when automatic reviews are disabled for the repository.
- `@coderabbitai full review` to do a full review from scratch and review all the files again.
- `@coderabbitai summary` to regenerate the summary of the PR.
- `@coderabbitai generate docstrings` to [generate docstrings](https://docs.coderabbit.ai/finishing-touches/docstrings) for this PR.
- `@coderabbitai resolve` resolve all the CodeRabbit review comments.
- `@coderabbitai configuration` to show the current CodeRabbit configuration for the repository.
- `@coderabbitai help` to get help.

### Other keywords and placeholders

- Add `@coderabbitai ignore` anywhere in the PR description to prevent this PR from being reviewed.
- Add `@coderabbitai summary` to generate the high-level summary at a specific location in the PR description.
- Add `@coderabbitai` anywhere in the PR title to generate the title automatically.

### CodeRabbit Configuration File (`.coderabbit.yaml`)

- You can programmatically configure CodeRabbit by adding a `.coderabbit.yaml` file to the root of your repository.
- Please see the [configuration documentation](https://docs.coderabbit.ai/guides/configure-coderabbit) for more information.
- If your editor has YAML language server enabled, you can add the path at the top of this file to enable auto-completion and validation: `# yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json`

### Documentation and Community

- Visit our [Documentation](https://docs.coderabbit.ai) for detailed information on how to use CodeRabbit.
- Join our [Discord Community](http://discord.gg/coderabbit) to get help, request features, and share feedback.
- Follow us on [X/Twitter](https://twitter.com/coderabbitai) for updates and announcements.

</details>

<!-- tips_end -->
author_association: CONTRIBUTOR
user: login: github-actions[bot]
created_at: 2025-04-20T17:13:02Z
body: ## 📦 Next.js Bundle Analysis for tnr

This analysis was generated by the [Next.js Bundle Analysis action](https://github.com/hashicorp/nextjs-bundle-analysis). 🤖

This PR introduced no changes to the JavaScript bundle! 🙌<!-- __NEXTJS_BUNDLE_tnr -->
author_association: CONTRIBUTOR
user: login: theeneon
created_at: 2025-04-20T17:56:47Z
body: I think this might have broke Syndicate users https://github.com/MathiasGruber/TheNinjaRPG/issues/476
author_association: COLLABORATOR
user: login: MathiasGruber
created_at: 2025-04-20T18:04:39Z
body: > I think this might have broke Syndicate users #476

Not related to this as it wasn't deployed
author_association: COLLABORATOR
user: login: theeneon
created_at: 2025-04-20T18:05:28Z
body: > > I think this might have broke Syndicate users #476
> 
> Not related to this as it wasn't deployed

Ah Okay so something else broke it then 
author_association: COLLABORATOR