refactor(db): migrate staff and app actions from Prisma facade to Drizzle
Co-authored-by: Cursor <[email protected]>
This commit is contained in:
1 parent
9854719cfd
commit
ed9c23c702
85 files changed
+2612
-1832
No files matched your search
+120
-77
@@ -1,11 +1,19 @@
|
||||
"use server";
|
||||
|
||||
import crypto from "node:crypto";
|
||||
import { and, eq } from "drizzle-orm";
|
||||
import { z } from "zod";
|
||||
import { invalidateLoginCache } from "@/lib/auth";
|
||||
import { hashPassword } from "@/lib/auth/password";
|
||||
import {
|
||||
Ban,
|
||||
db,
|
||||
User,
|
||||
UsersBadges,
|
||||
UsersCurrency,
|
||||
UsersSettings,
|
||||
} from "@/lib/db";
|
||||
import { PERMS } from "@/lib/permissions";
|
||||
import { prisma } from "@/lib/prisma";
|
||||
import { adminAction } from "@/lib/safe-action";
|
||||
import { ActionError, actionOk } from "@/lib/safe-action-shared";
|
||||
import { logAudit } from "@/lib/services/audit";
|
||||
@@ -21,6 +29,27 @@ import {
|
||||
const DEFAULT_LOOK =
|
||||
"hr-115-42.hd-195-19.ch-3030-82.lg-275-1408.fa-1201.ca-1804-64";
|
||||
|
||||
function isDuplicateKey(err: unknown): boolean {
|
||||
if (!err || typeof err !== "object") return false;
|
||||
const e = err as { code?: string | number; errno?: number };
|
||||
return e.code === "P2002" || e.code === "ER_DUP_ENTRY" || e.errno === 1062;
|
||||
}
|
||||
|
||||
function duplicateField(err: unknown): "username" | "mail" | null {
|
||||
if (!isDuplicateKey(err)) return null;
|
||||
const e = err as {
|
||||
message?: string;
|
||||
meta?: { target?: string[] };
|
||||
};
|
||||
const target = e.meta?.target ?? [];
|
||||
if (target.includes("username")) return "username";
|
||||
if (target.includes("mail")) return "mail";
|
||||
const msg = e.message ?? "";
|
||||
if (msg.includes("username")) return "username";
|
||||
if (msg.includes("mail")) return "mail";
|
||||
return null;
|
||||
}
|
||||
|
||||
export const createUser = adminAction(
|
||||
{ permission: PERMS.USERS_EDIT, schema: createUserSchema },
|
||||
async (ctx) => {
|
||||
@@ -34,32 +63,29 @@ export const createUser = adminAction(
|
||||
const now = Math.floor(Date.now() / 1000);
|
||||
|
||||
try {
|
||||
const user = await prisma.$transaction(async (tx) => {
|
||||
const created = await tx.user.create({
|
||||
data: {
|
||||
username,
|
||||
mail,
|
||||
password: hashedPassword,
|
||||
rank,
|
||||
motto: motto || "I'm new here!",
|
||||
look: DEFAULT_LOOK,
|
||||
credits: 5000,
|
||||
pixels: 5000,
|
||||
accountCreated: now,
|
||||
ipRegister: "0.0.0.0",
|
||||
ipCurrent: "0.0.0.0",
|
||||
},
|
||||
const user = await db.transaction(async (tx) => {
|
||||
const [result] = await tx.insert(User).values({
|
||||
username,
|
||||
mail,
|
||||
password: hashedPassword,
|
||||
rank,
|
||||
motto: motto || "I'm new here!",
|
||||
look: DEFAULT_LOOK,
|
||||
credits: 5000,
|
||||
pixels: 5000,
|
||||
accountCreated: now,
|
||||
ipRegister: "0.0.0.0",
|
||||
ipCurrent: "0.0.0.0",
|
||||
});
|
||||
const id = Number(result.insertId);
|
||||
|
||||
await tx.usersSettings.create({ data: { userId: created.id } });
|
||||
await tx.usersCurrency.createMany({
|
||||
data: [
|
||||
{ userId: created.id, type: 0, amount: 5000 },
|
||||
{ userId: created.id, type: 5, amount: 5000 },
|
||||
],
|
||||
});
|
||||
await tx.insert(UsersSettings).values({ userId: id });
|
||||
await tx.insert(UsersCurrency).values([
|
||||
{ userId: id, type: 0, amount: 5000 },
|
||||
{ userId: id, type: 5, amount: 5000 },
|
||||
]);
|
||||
|
||||
return created;
|
||||
return { id, username };
|
||||
});
|
||||
|
||||
logAudit({
|
||||
@@ -80,21 +106,11 @@ export const createUser = adminAction(
|
||||
|
||||
return actionOk({ id: user.id, username: user.username });
|
||||
} catch (err) {
|
||||
if (
|
||||
err &&
|
||||
typeof err === "object" &&
|
||||
"code" in err &&
|
||||
err.code === "P2002"
|
||||
) {
|
||||
const target =
|
||||
((err as { meta?: { target?: string[] } }).meta
|
||||
?.target as string[]) ?? [];
|
||||
if (target.includes("username"))
|
||||
throw new ActionError("Username already taken");
|
||||
if (target.includes("mail"))
|
||||
throw new ActionError("Email already registered");
|
||||
const field = duplicateField(err);
|
||||
if (field === "username") throw new ActionError("Username already taken");
|
||||
if (field === "mail") throw new ActionError("Email already registered");
|
||||
if (isDuplicateKey(err))
|
||||
throw new ActionError("Username or email already in use");
|
||||
}
|
||||
throw err;
|
||||
}
|
||||
},
|
||||
@@ -119,22 +135,32 @@ export const updateUser = adminAction(
|
||||
throw new ActionError("Cannot assign rank equal or higher than your own");
|
||||
}
|
||||
|
||||
await prisma.user.update({ where: { id }, data: userData });
|
||||
const patch = Object.fromEntries(
|
||||
Object.entries(userData).filter(([, v]) => v !== undefined),
|
||||
) as Partial<{
|
||||
username: string;
|
||||
mail: string;
|
||||
rank: number;
|
||||
motto: string;
|
||||
credits: number;
|
||||
pixels: number;
|
||||
}>;
|
||||
if (Object.keys(patch).length > 0) {
|
||||
await db.update(User).set(patch).where(eq(User.id, id));
|
||||
}
|
||||
invalidateLoginCache(targetUser.username);
|
||||
|
||||
if (diamonds !== undefined) {
|
||||
await prisma.usersCurrency.upsert({
|
||||
where: { userId_type: { userId: id, type: 5 } },
|
||||
update: { amount: diamonds },
|
||||
create: { userId: id, type: 5, amount: diamonds },
|
||||
});
|
||||
await db
|
||||
.insert(UsersCurrency)
|
||||
.values({ userId: id, type: 5, amount: diamonds })
|
||||
.onDuplicateKeyUpdate({ set: { amount: diamonds } });
|
||||
}
|
||||
if (duckets !== undefined) {
|
||||
await prisma.usersCurrency.upsert({
|
||||
where: { userId_type: { userId: id, type: 0 } },
|
||||
update: { amount: duckets },
|
||||
create: { userId: id, type: 0, amount: duckets },
|
||||
});
|
||||
await db
|
||||
.insert(UsersCurrency)
|
||||
.values({ userId: id, type: 0, amount: duckets })
|
||||
.onDuplicateKeyUpdate({ set: { amount: duckets } });
|
||||
}
|
||||
|
||||
logAudit({
|
||||
@@ -173,17 +199,15 @@ export const banUser = adminAction(
|
||||
const now = Math.floor(Date.now() / 1000);
|
||||
const banExpire = duration > 0 ? now + duration * 3600 : 0;
|
||||
|
||||
await prisma.ban.create({
|
||||
data: {
|
||||
userId,
|
||||
userStaffId: ctx.session.user.id,
|
||||
timestamp: now,
|
||||
banExpire,
|
||||
banReason: reason,
|
||||
type: type || "account",
|
||||
ip: ip || "",
|
||||
machineId: "",
|
||||
},
|
||||
await db.insert(Ban).values({
|
||||
userId,
|
||||
userStaffId: ctx.session.user.id,
|
||||
timestamp: now,
|
||||
banExpire,
|
||||
banReason: reason,
|
||||
type: type || "account",
|
||||
ip: ip || "",
|
||||
machineId: "",
|
||||
});
|
||||
|
||||
await rcon.disconnectUser(userId);
|
||||
@@ -216,7 +240,7 @@ export const unbanUser = adminAction(
|
||||
|
||||
const targetUser = await guardRank(userId, ctx.session.user.rank);
|
||||
|
||||
await prisma.ban.deleteMany({ where: { userId } });
|
||||
await db.delete(Ban).where(eq(Ban.userId, userId));
|
||||
|
||||
logAudit({
|
||||
userId: ctx.session.user.id,
|
||||
@@ -242,12 +266,19 @@ export const giveBadge = adminAction(
|
||||
|
||||
await guardRank(userId, ctx.session.user.rank);
|
||||
|
||||
const existing = await prisma.usersBadges.findFirst({
|
||||
where: { userId, badgeCode },
|
||||
});
|
||||
const [existing] = await db
|
||||
.select({ id: UsersBadges.id })
|
||||
.from(UsersBadges)
|
||||
.where(
|
||||
and(
|
||||
eq(UsersBadges.userId, userId),
|
||||
eq(UsersBadges.badgeCode, badgeCode),
|
||||
),
|
||||
)
|
||||
.limit(1);
|
||||
if (existing) throw new ActionError("Badge already assigned");
|
||||
|
||||
await prisma.usersBadges.create({ data: { userId, badgeCode } });
|
||||
await db.insert(UsersBadges).values({ userId, badgeCode });
|
||||
await rcon.giveBadge(userId, badgeCode);
|
||||
|
||||
return actionOk();
|
||||
@@ -268,12 +299,19 @@ export const removeBadge = adminAction(
|
||||
|
||||
await guardRank(userId, ctx.session.user.rank);
|
||||
|
||||
const existing = await prisma.usersBadges.findFirst({
|
||||
where: { userId, badgeCode },
|
||||
});
|
||||
const [existing] = await db
|
||||
.select({ id: UsersBadges.id })
|
||||
.from(UsersBadges)
|
||||
.where(
|
||||
and(
|
||||
eq(UsersBadges.userId, userId),
|
||||
eq(UsersBadges.badgeCode, badgeCode),
|
||||
),
|
||||
)
|
||||
.limit(1);
|
||||
if (!existing) throw new ActionError("Badge not found");
|
||||
|
||||
await prisma.usersBadges.delete({ where: { id: existing.id } });
|
||||
await db.delete(UsersBadges).where(eq(UsersBadges.id, existing.id));
|
||||
await rcon.removeBadge(userId, badgeCode);
|
||||
|
||||
return actionOk();
|
||||
@@ -283,10 +321,15 @@ export const removeBadge = adminAction(
|
||||
// ── Rank guard helper ───────────────────────────────────────────────
|
||||
|
||||
async function guardRank(targetUserId: number, sessionRank: number) {
|
||||
const target = await prisma.user.findUnique({
|
||||
where: { id: targetUserId },
|
||||
select: { username: true, rank: true, mail: true },
|
||||
});
|
||||
const [target] = await db
|
||||
.select({
|
||||
username: User.username,
|
||||
rank: User.rank,
|
||||
mail: User.mail,
|
||||
})
|
||||
.from(User)
|
||||
.where(eq(User.id, targetUserId))
|
||||
.limit(1);
|
||||
if (!target) throw new ActionError("User not found");
|
||||
if (target.rank >= sessionRank && sessionRank < 7) {
|
||||
throw new ActionError("Cannot modify user with equal or higher rank");
|
||||
@@ -311,10 +354,10 @@ export const resetPassword = adminAction(
|
||||
.slice(0, 16);
|
||||
const hashed = await hashPassword(newPassword);
|
||||
|
||||
await prisma.user.update({
|
||||
where: { id: ctx.data.userId },
|
||||
data: { password: hashed },
|
||||
});
|
||||
await db
|
||||
.update(User)
|
||||
.set({ password: hashed })
|
||||
.where(eq(User.id, ctx.data.userId));
|
||||
invalidateLoginCache(target.username);
|
||||
|
||||
logAudit({
|
||||
|
||||
Reference in new issue
Block a user