diff --git a/prisma/migrations/20241105162607_relate_user_to_legacyuser/migration.sql b/prisma/migrations/20241105162607_relate_user_to_legacyuser/migration.sql new file mode 100644 index 00000000..e2ada3b2 --- /dev/null +++ b/prisma/migrations/20241105162607_relate_user_to_legacyuser/migration.sql @@ -0,0 +1,16 @@ +/* + Warnings: + + - You are about to drop the column `alreadyMigrated` on the `LegacyUser` table. All the data in the column will be lost. + - A unique constraint covering the columns `[newAccountId]` on the table `LegacyUser` will be added. If there are existing duplicate values, this will fail. + +*/ +-- AlterTable +ALTER TABLE "LegacyUser" DROP COLUMN "alreadyMigrated", +ADD COLUMN "newAccountId" INTEGER; + +-- CreateIndex +CREATE UNIQUE INDEX "LegacyUser_newAccountId_key" ON "LegacyUser"("newAccountId"); + +-- AddForeignKey +ALTER TABLE "LegacyUser" ADD CONSTRAINT "LegacyUser_newAccountId_fkey" FOREIGN KEY ("newAccountId") REFERENCES "User"("id") ON DELETE SET NULL ON UPDATE CASCADE; diff --git a/prisma/schema.prisma b/prisma/schema.prisma index 22c6399e..5e8873f5 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -25,6 +25,7 @@ model User { Transactions Transaction[] Deposit Deposit[] WishLike WishLike[] + LegacyUser LegacyUser? @@index([id]) } @@ -161,10 +162,11 @@ model WishLike { } model LegacyUser { - id Int @unique - name String - balance Decimal - alreadyMigrated Boolean @default(false) + id Int @unique + name String + balance Decimal + newAccount User? @relation(fields: [newAccountId], references: [id]) + newAccountId Int? @unique } enum WishStatus { diff --git a/src/app/(loggedin)/account/AccountMigrationDialog.tsx b/src/app/(loggedin)/account/AccountMigrationDialog.tsx new file mode 100644 index 00000000..a5413cda --- /dev/null +++ b/src/app/(loggedin)/account/AccountMigrationDialog.tsx @@ -0,0 +1,67 @@ +import { useActionState, useEffect, useRef, useState } from "react"; +import toast from "react-hot-toast"; + +import { AnimatedPopup, PopupRefActions } from "@/components/ui/AnimatedPopup"; +import { FatButton } from "@/components/ui/Buttons/FatButton"; +import { LineButton } from "@/components/ui/Buttons/LineButton"; +import { DialogTitleWithButton } from "@/components/ui/DialogTitleWithButton"; +import { MigrationCombobox } from "@/components/ui/MigrationCombobox"; +import { migrateAccountAction } from "@/server/actions/account/migration"; + +export const AccountMigrationDialog = () => { + const [step, setStep] = useState(0); + const [result, formAction, isPending] = useActionState(migrateAccountAction, { + success: false, + error: undefined, + }); + + useEffect(() => { + if (result?.error) { + toast.error(result.error); + } else if (result?.success) { + toast.success("Account migrated successfully!"); + closeModal(); + } + }, [result]); + const popupRef = useRef(undefined); + const closeModal = () => { + popupRef?.current?.closeContainer(); + setStep(0); + }; + + const setupButton = ( + + ); + + return ( + +
+ +

+ Have an old Namukilke account you want to migrate? Use the form below + to move your old account's funds to this one.
+

+

+ Note that you can only migrate one account. +

+
+ + + +
+
+ ); +}; diff --git a/src/app/(loggedin)/account/page.tsx b/src/app/(loggedin)/account/page.tsx index 03b3b6a1..94bd5b09 100644 --- a/src/app/(loggedin)/account/page.tsx +++ b/src/app/(loggedin)/account/page.tsx @@ -15,7 +15,12 @@ import { RfidSetupDialog } from "@/components/ui/RfidSetupDialog"; import { SectionTitle } from "@/components/ui/SectionTitle"; import { getCurrentUserBalance } from "@/server/actions/account/getBalance"; import { logoutAction } from "@/server/actions/auth/logout"; -import { getCurrentUser } from "@/server/db/queries/account"; +import { + getCurrentUser, + getCurrentUserMigrationStatus, +} from "@/server/db/queries/account"; + +import { AccountMigrationDialog } from "./AccountMigrationDialog"; const AccountPage = () => { const pathName = usePathname(); @@ -23,6 +28,7 @@ const AccountPage = () => { null, ); const [userBalance, setUserBalance] = useState(null); + const [userMigrated, setUserMigrated] = useState(true); useEffect(() => { const checkNfcConnection = async () => { const user = await getCurrentUser(); @@ -38,6 +44,9 @@ const AccountPage = () => { getCurrentUserBalance().then((balance) => { setUserBalance(formatCurrency(balance)); }); + getCurrentUserMigrationStatus().then((migrated) => { + setUserMigrated(migrated); + }); }, []); return (
@@ -70,6 +79,7 @@ const AccountPage = () => { + {!userMigrated && } void; +} + +export const DialogTitleWithButton = ({ title, onButtonClick }: Props) => { + return ( +
+

{title}

+ {/* biome-ignore lint/a11y/useKeyWithClickEvents: */} +
+ +
+
+ ); +}; diff --git a/src/components/ui/MigrationCombobox.tsx b/src/components/ui/MigrationCombobox.tsx index 6e65623a..f3ab5929 100644 --- a/src/components/ui/MigrationCombobox.tsx +++ b/src/components/ui/MigrationCombobox.tsx @@ -40,7 +40,7 @@ export const MigrationCombobox = () => { ? accountList : accountList.filter( (account) => - account.name.includes(query) || + account.name.toLowerCase().includes(query.toLowerCase()) || account.id.toString().includes(query), ); diff --git a/src/server/actions/account/migration.ts b/src/server/actions/account/migration.ts index f1bbe702..28a92ce7 100644 --- a/src/server/actions/account/migration.ts +++ b/src/server/actions/account/migration.ts @@ -1,12 +1,18 @@ "use server"; +import { revalidatePath } from "next/cache"; + +import { getSession } from "@/auth/ironsession"; import { ClientLegacyUser } from "@/common/types"; import { db } from "@/server/db/prisma"; +import { newDeposit } from "@/server/db/queries/deposit"; +import { ValueError } from "@/server/exceptions/exception"; +import { Prisma, PrismaClient } from "@prisma/client"; export const getUnmigratedAccounts = async (): Promise => { const users = await db.legacyUser.findMany({ where: { - alreadyMigrated: false, + newAccountId: null, }, }); return users.map((user) => ({ @@ -15,3 +21,54 @@ export const getUnmigratedAccounts = async (): Promise => { balance: user.balance.toNumber(), })); }; + +export const migrateAccountAction = async ( + prevState: any, + formData: FormData, +) => { + try { + const session = await getSession(); + const currentUserId = session?.user?.userId; + if (!currentUserId) { + throw new Error("Unauthorized"); + } + const formUserId = formData.get("legacyAccountId") as string; + const legacyId = parseInt(formUserId); + if (!legacyId) { + throw new ValueError({ + cause: "invalid_value", + message: "Couldn't find legacy user to migrate", + }); + } + + await db.$transaction(async (tx) => { + const legacyUser = await tx.legacyUser.update({ + where: { + id: legacyId, + }, + data: { + newAccountId: currentUserId, + }, + }); + if (!legacyUser) { + throw new ValueError({ + cause: "invalid_value", + message: "Couldn't find legacy user to migrate", + }); + } + const legacyBalance = legacyUser.balance.toNumber(); + await newDeposit(tx as PrismaClient, currentUserId, legacyBalance); + }); + revalidatePath("/account"); + return { success: true }; + } catch (error: any) { + if (error instanceof Prisma.PrismaClientKnownRequestError) { + if (error.code === "P2002") { + return { + error: "You've already migrated a legacy account!", + }; + } + } + return { error: error?.message || "Unknown error occurred" }; + } +}; diff --git a/src/server/db/queries/account.ts b/src/server/db/queries/account.ts index a26779a8..14093ea9 100644 --- a/src/server/db/queries/account.ts +++ b/src/server/db/queries/account.ts @@ -32,6 +32,16 @@ export const createAccount = async ({ accountCredentials; const pinHash = await createPincodeHash(pinCode); + const newUser = await client.user.create({ + data: { + firstName, + lastName, + userName, + role, + pinHash, + }, + }); + let balance = 0; if (legacyAccountId) { @@ -41,7 +51,11 @@ export const createAccount = async ({ ); if (legacyUser) { balance = legacyUser.balance.toNumber(); - await migrateLegacyUser(client as PrismaClient, legacyAccountId); + await migrateLegacyUser( + client as PrismaClient, + legacyAccountId, + newUser.id, + ); } else { throw new ValueError({ message: `Legacy user with id ${legacyAccountId} was not found`, @@ -50,20 +64,13 @@ export const createAccount = async ({ } } - return client.user.create({ + await client.userBalance.create({ data: { - firstName, - lastName, - userName, - role, - pinHash, - Balances: { - create: { - balance, - }, - }, + userId: newUser.id, + balance, }, }); + return newUser; }; export const getAllUsers = async () => { @@ -189,13 +196,29 @@ export const getLegacyUserById = async (db: PrismaClient, legacyId: number) => { }); }; -export const migrateLegacyUser = async (db: PrismaClient, legacyId: number) => { +export const migrateLegacyUser = async ( + db: PrismaClient, + legacyId: number, + newUserId: number, +) => { await db.legacyUser.update({ where: { id: legacyId, }, data: { - alreadyMigrated: true, + newAccountId: newUserId, + }, + }); +}; + +export const getCurrentUserMigrationStatus = async () => { + const user = await getCurrentUser(); + if (!user.ok) return false; + const legacyUser = await db.legacyUser.findFirst({ + where: { + newAccountId: user.user.id, }, }); + console.log("got legacy user", legacyUser); + return !!legacyUser; };