refactor(db): migrate app pages and APIs from Prisma facade to Drizzle (4)
Co-authored-by: Cursor <[email protected]>
This commit is contained in:
1 parent
580972c0a0
commit
9aa4f331bf
40 files changed
+1259
-927
No files matched your search
@@ -1,9 +1,10 @@
|
||||
import { asc, eq, gte } from "drizzle-orm";
|
||||
import { notFound, redirect } from "next/navigation";
|
||||
import { getTranslations } from "next-intl/server";
|
||||
import { TicketQueueBanner } from "@/components/admin/ticket-queue-banner";
|
||||
import { getMinStaffRank } from "@/lib/admin/min-staff-rank";
|
||||
import { db, User, WebsiteTicket } from "@/lib/db";
|
||||
import { canAccess, getAdminContext, PERMS } from "@/lib/permissions";
|
||||
import { prisma } from "@/lib/prisma";
|
||||
import { AdminTicketDetail } from "./admin-ticket-detail";
|
||||
|
||||
export default async function AdminTicketDetailPage({
|
||||
@@ -21,11 +22,11 @@ export default async function AdminTicketDetailPage({
|
||||
const ticketId = Number(id);
|
||||
if (Number.isNaN(ticketId)) notFound();
|
||||
|
||||
const ticket = await prisma.websiteTicket.findUnique({
|
||||
where: { id: ticketId },
|
||||
include: {
|
||||
const ticket = await db.query.WebsiteTicket.findFirst({
|
||||
where: eq(WebsiteTicket.id, ticketId),
|
||||
with: {
|
||||
creator: {
|
||||
select: {
|
||||
columns: {
|
||||
id: true,
|
||||
username: true,
|
||||
look: true,
|
||||
@@ -35,12 +36,12 @@ export default async function AdminTicketDetailPage({
|
||||
accountCreated: true,
|
||||
},
|
||||
},
|
||||
assignee: { select: { id: true, username: true } },
|
||||
assignee: { columns: { id: true, username: true } },
|
||||
messages: {
|
||||
orderBy: { createdAt: "asc" },
|
||||
include: {
|
||||
orderBy: (m, { asc: a }) => [a(m.createdAt)],
|
||||
with: {
|
||||
user: {
|
||||
select: { id: true, username: true, look: true, rank: true },
|
||||
columns: { id: true, username: true, look: true, rank: true },
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -51,11 +52,11 @@ export default async function AdminTicketDetailPage({
|
||||
|
||||
// Staff assignees: min_staff_rank and above
|
||||
const minStaffRank = await getMinStaffRank();
|
||||
const staffMembers = await prisma.user.findMany({
|
||||
where: { rank: { gte: minStaffRank } },
|
||||
select: { id: true, username: true },
|
||||
orderBy: { username: "asc" },
|
||||
});
|
||||
const staffMembers = await db
|
||||
.select({ id: User.id, username: User.username })
|
||||
.from(User)
|
||||
.where(gte(User.rank, minStaffRank))
|
||||
.orderBy(asc(User.username));
|
||||
|
||||
const canEdit = canAccess(permissions, PERMS.TICKETS_EDIT, session.user.rank);
|
||||
|
||||
|
||||
+103
-59
@@ -1,12 +1,25 @@
|
||||
import {
|
||||
and,
|
||||
asc,
|
||||
count,
|
||||
desc,
|
||||
eq,
|
||||
like,
|
||||
ne,
|
||||
or,
|
||||
type SQL,
|
||||
sql,
|
||||
} from "drizzle-orm";
|
||||
import { alias } from "drizzle-orm/mysql-core";
|
||||
import { redirect } from "next/navigation";
|
||||
import { getTranslations } from "next-intl/server";
|
||||
import { StatusCard } from "@/components/admin/dashboard";
|
||||
import { TicketQueueBanner } from "@/components/admin/ticket-queue-banner";
|
||||
import { fetchTicketQueueOpenCounts } from "@/lib/admin/ticket-queue-counts";
|
||||
import { calcPagination, parseListParams } from "@/lib/admin-helpers";
|
||||
import { db, User, WebsiteTicket } from "@/lib/db";
|
||||
import { formatDate } from "@/lib/format-date";
|
||||
import { canAccess, getAdminContext, PERMS } from "@/lib/permissions";
|
||||
import { prisma } from "@/lib/prisma";
|
||||
import { type TicketRow, TicketsTable } from "./tickets-table";
|
||||
|
||||
export default async function AdminTicketsPage({
|
||||
@@ -24,92 +37,123 @@ export default async function AdminTicketsPage({
|
||||
const parsed = parseListParams(new URLSearchParams(raw));
|
||||
const statusFilter = raw.filter_status || raw.status || "active";
|
||||
|
||||
const conditions: any[] = [];
|
||||
const Creator = alias(User, "ticket_creator");
|
||||
const Assignee = alias(User, "ticket_assignee");
|
||||
|
||||
const conditions: SQL[] = [];
|
||||
|
||||
if (statusFilter === "active") {
|
||||
conditions.push({ status: { not: "closed" } });
|
||||
conditions.push(ne(WebsiteTicket.status, "closed"));
|
||||
} else if (statusFilter && statusFilter !== "all") {
|
||||
conditions.push({ status: statusFilter });
|
||||
conditions.push(eq(WebsiteTicket.status, statusFilter));
|
||||
}
|
||||
|
||||
if (parsed.search.trim()) {
|
||||
const q = parsed.search.trim();
|
||||
const asId = Number(q);
|
||||
conditions.push({
|
||||
OR: [
|
||||
{ subject: { contains: q } },
|
||||
{ category: { contains: q } },
|
||||
{ creator: { username: { contains: q } } },
|
||||
{ assignee: { username: { contains: q } } },
|
||||
...(Number.isFinite(asId) && asId > 0 ? [{ id: asId }] : []),
|
||||
],
|
||||
});
|
||||
const searchOr = [
|
||||
like(WebsiteTicket.subject, `%${q}%`),
|
||||
like(WebsiteTicket.category, `%${q}%`),
|
||||
like(Creator.username, `%${q}%`),
|
||||
like(Assignee.username, `%${q}%`),
|
||||
...(Number.isFinite(asId) && asId > 0
|
||||
? [eq(WebsiteTicket.id, asId)]
|
||||
: []),
|
||||
];
|
||||
conditions.push(or(...searchOr)!);
|
||||
}
|
||||
|
||||
const where: any =
|
||||
conditions.length === 0
|
||||
? {}
|
||||
: conditions.length === 1
|
||||
? conditions[0]
|
||||
: { AND: conditions };
|
||||
const where = conditions.length > 0 ? and(...conditions) : undefined;
|
||||
|
||||
const SORT_MAP: Record<string, any> = {
|
||||
subject: { subject: "asc" },
|
||||
status: { status: "asc" },
|
||||
priority: { priority: "asc" },
|
||||
assignee: { assigneeId: "asc" },
|
||||
date: { createdAt: "desc" },
|
||||
id: { id: "desc" },
|
||||
};
|
||||
const orderCol =
|
||||
parsed.sort === "subject"
|
||||
? WebsiteTicket.subject
|
||||
: parsed.sort === "status"
|
||||
? WebsiteTicket.status
|
||||
: parsed.sort === "priority"
|
||||
? WebsiteTicket.priority
|
||||
: parsed.sort === "assignee"
|
||||
? WebsiteTicket.assigneeId
|
||||
: parsed.sort === "id"
|
||||
? WebsiteTicket.id
|
||||
: WebsiteTicket.createdAt;
|
||||
const orderBy = parsed.order === "asc" ? asc(orderCol) : desc(orderCol);
|
||||
|
||||
const baseOrder = SORT_MAP[parsed.sort ?? "date"] ?? { createdAt: "desc" };
|
||||
const orderField = Object.keys(baseOrder)[0] as keyof typeof baseOrder;
|
||||
const orderBy = {
|
||||
[orderField]: parsed.order,
|
||||
} as any;
|
||||
|
||||
const [total, openCount, inProgressCount, waitingCount, closedCount, queues] =
|
||||
const [totalRows, openRows, inProgressRows, waitingRows, closedRows, queues] =
|
||||
await Promise.all([
|
||||
prisma.websiteTicket.count({ where }).catch(() => 0),
|
||||
prisma.websiteTicket.count({ where: { status: "open" } }).catch(() => 0),
|
||||
prisma.websiteTicket
|
||||
.count({ where: { status: "in_progress" } })
|
||||
.catch(() => 0),
|
||||
prisma.websiteTicket
|
||||
.count({ where: { status: "waiting" } })
|
||||
.catch(() => 0),
|
||||
prisma.websiteTicket
|
||||
.count({ where: { status: "closed" } })
|
||||
.catch(() => 0),
|
||||
db
|
||||
.select({ total: count() })
|
||||
.from(WebsiteTicket)
|
||||
.leftJoin(Creator, eq(WebsiteTicket.creatorId, Creator.id))
|
||||
.leftJoin(Assignee, eq(WebsiteTicket.assigneeId, Assignee.id))
|
||||
.where(where)
|
||||
.catch(() => [{ total: 0 }]),
|
||||
db
|
||||
.select({ total: count() })
|
||||
.from(WebsiteTicket)
|
||||
.where(eq(WebsiteTicket.status, "open"))
|
||||
.catch(() => [{ total: 0 }]),
|
||||
db
|
||||
.select({ total: count() })
|
||||
.from(WebsiteTicket)
|
||||
.where(eq(WebsiteTicket.status, "in_progress"))
|
||||
.catch(() => [{ total: 0 }]),
|
||||
db
|
||||
.select({ total: count() })
|
||||
.from(WebsiteTicket)
|
||||
.where(eq(WebsiteTicket.status, "waiting"))
|
||||
.catch(() => [{ total: 0 }]),
|
||||
db
|
||||
.select({ total: count() })
|
||||
.from(WebsiteTicket)
|
||||
.where(eq(WebsiteTicket.status, "closed"))
|
||||
.catch(() => [{ total: 0 }]),
|
||||
fetchTicketQueueOpenCounts(),
|
||||
]);
|
||||
|
||||
const total = Number(totalRows[0]?.total ?? 0);
|
||||
const openCount = Number(openRows[0]?.total ?? 0);
|
||||
const inProgressCount = Number(inProgressRows[0]?.total ?? 0);
|
||||
const waitingCount = Number(waitingRows[0]?.total ?? 0);
|
||||
const closedCount = Number(closedRows[0]?.total ?? 0);
|
||||
|
||||
const pagination = calcPagination(total, parsed.page, parsed.perPage);
|
||||
|
||||
const tickets = await prisma.websiteTicket
|
||||
.findMany({
|
||||
where,
|
||||
orderBy,
|
||||
skip: pagination.offset,
|
||||
take: pagination.perPage,
|
||||
include: {
|
||||
creator: { select: { id: true, username: true } },
|
||||
assignee: { select: { id: true, username: true } },
|
||||
_count: { select: { messages: true } },
|
||||
},
|
||||
const tickets = await db
|
||||
.select({
|
||||
id: WebsiteTicket.id,
|
||||
subject: WebsiteTicket.subject,
|
||||
creatorId: WebsiteTicket.creatorId,
|
||||
category: WebsiteTicket.category,
|
||||
status: WebsiteTicket.status,
|
||||
priority: WebsiteTicket.priority,
|
||||
createdAt: WebsiteTicket.createdAt,
|
||||
creatorUsername: Creator.username,
|
||||
assigneeUsername: Assignee.username,
|
||||
messageCount: sql<number>`(
|
||||
SELECT COUNT(*) FROM website_ticket_messages m
|
||||
WHERE m.ticket_id = ${WebsiteTicket.id}
|
||||
)`.mapWith(Number),
|
||||
})
|
||||
.from(WebsiteTicket)
|
||||
.leftJoin(Creator, eq(WebsiteTicket.creatorId, Creator.id))
|
||||
.leftJoin(Assignee, eq(WebsiteTicket.assigneeId, Assignee.id))
|
||||
.where(where)
|
||||
.orderBy(orderBy)
|
||||
.limit(pagination.perPage)
|
||||
.offset(pagination.offset)
|
||||
.catch(() => []);
|
||||
|
||||
const rows: TicketRow[] = tickets.map((ticket) => ({
|
||||
id: ticket.id,
|
||||
subject: ticket.subject,
|
||||
creator: ticket.creator.username,
|
||||
creator: ticket.creatorUsername ?? "—",
|
||||
creatorId: ticket.creatorId,
|
||||
category: ticket.category,
|
||||
messages: ticket._count.messages,
|
||||
messages: Number(ticket.messageCount ?? 0),
|
||||
status: ticket.status,
|
||||
priority: ticket.priority,
|
||||
assignee: ticket.assignee?.username ?? "—",
|
||||
assignee: ticket.assigneeUsername ?? "—",
|
||||
date: formatDate(ticket.createdAt, "date"),
|
||||
}));
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { asc } from "drizzle-orm";
|
||||
import { redirect } from "next/navigation";
|
||||
import { db, WebsiteTicketTemplate } from "@/lib/db";
|
||||
import { canAccess, getAdminContext, PERMS } from "@/lib/permissions";
|
||||
import { prisma } from "@/lib/prisma";
|
||||
import { TicketTemplatesManager } from "./templates-manager";
|
||||
|
||||
export default async function TicketTemplatesPage() {
|
||||
@@ -9,9 +10,10 @@ export default async function TicketTemplatesPage() {
|
||||
redirect("/admin/tickets");
|
||||
}
|
||||
|
||||
const templates = await prisma.websiteTicketTemplate.findMany({
|
||||
orderBy: { sortOrder: "asc" },
|
||||
});
|
||||
const templates = await db
|
||||
.select()
|
||||
.from(WebsiteTicketTemplate)
|
||||
.orderBy(asc(WebsiteTicketTemplate.sortOrder));
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
|
||||
@@ -1,10 +1,21 @@
|
||||
import {
|
||||
and,
|
||||
asc,
|
||||
count,
|
||||
desc,
|
||||
eq,
|
||||
inArray,
|
||||
like,
|
||||
or,
|
||||
type SQL,
|
||||
} from "drizzle-orm";
|
||||
import { redirect } from "next/navigation";
|
||||
import { getTranslations } from "next-intl/server";
|
||||
import { StatusCard } from "@/components/admin/dashboard";
|
||||
import { calcPagination, parseListParams } from "@/lib/admin-helpers";
|
||||
import { db, User, WebsitePaypalTransactions } from "@/lib/db";
|
||||
import { formatDate } from "@/lib/format-date";
|
||||
import { canAccess, getAdminContext, PERMS } from "@/lib/permissions";
|
||||
import { prisma } from "@/lib/prisma";
|
||||
import { type TransactionRow, TransactionsTable } from "./transactions-table";
|
||||
|
||||
const PAID_STATUS = ["completed", "paid", "success", "approved"] as const;
|
||||
@@ -33,15 +44,6 @@ function formatAmount(amount: number, currency: string): string {
|
||||
return `${value} ${currency}`;
|
||||
}
|
||||
|
||||
const SORT_MAP: Record<string, any> = {
|
||||
id: { id: "desc" },
|
||||
amount: { amount: "desc" },
|
||||
date: { createdAt: "desc" },
|
||||
status: { status: "desc" },
|
||||
package: { description: "desc" },
|
||||
payer: { userId: "desc" },
|
||||
};
|
||||
|
||||
export default async function AdminTransactions({
|
||||
searchParams,
|
||||
}: {
|
||||
@@ -57,14 +59,20 @@ export default async function AdminTransactions({
|
||||
const parsed = parseListParams(new URLSearchParams(raw));
|
||||
const statusFilter = raw.filter_status ?? "";
|
||||
|
||||
const where: any = {};
|
||||
const conditions: SQL[] = [];
|
||||
|
||||
if (statusFilter === "paid") {
|
||||
where.status = { in: [...PAID_STATUS] };
|
||||
conditions.push(
|
||||
inArray(WebsitePaypalTransactions.status, [...PAID_STATUS]),
|
||||
);
|
||||
} else if (statusFilter === "failed") {
|
||||
where.status = { in: [...FAILED_STATUS] };
|
||||
conditions.push(
|
||||
inArray(WebsitePaypalTransactions.status, [...FAILED_STATUS]),
|
||||
);
|
||||
} else if (statusFilter === "pending") {
|
||||
where.status = { in: [...PENDING_STATUS] };
|
||||
conditions.push(
|
||||
inArray(WebsitePaypalTransactions.status, [...PENDING_STATUS]),
|
||||
);
|
||||
}
|
||||
|
||||
if (parsed.search.trim()) {
|
||||
@@ -72,55 +80,67 @@ export default async function AdminTransactions({
|
||||
const asId = Number(q);
|
||||
let userIds: number[] = [];
|
||||
try {
|
||||
const users = await prisma.user.findMany({
|
||||
where: { username: { contains: q } },
|
||||
select: { id: true },
|
||||
take: 50,
|
||||
});
|
||||
const users = await db
|
||||
.select({ id: User.id })
|
||||
.from(User)
|
||||
.where(like(User.username, `%${q}%`))
|
||||
.limit(50);
|
||||
userIds = users.map((u) => u.id);
|
||||
} catch {
|
||||
userIds = [];
|
||||
}
|
||||
|
||||
where.AND = [
|
||||
...(where.status ? [{ status: where.status }] : []),
|
||||
{
|
||||
OR: [
|
||||
{ transactionId: { contains: q } },
|
||||
{ description: { contains: q } },
|
||||
...(Number.isFinite(asId) && asId > 0 ? [{ userId: asId }] : []),
|
||||
...(userIds.length ? [{ userId: { in: userIds } }] : []),
|
||||
],
|
||||
},
|
||||
];
|
||||
delete where.status;
|
||||
conditions.push(
|
||||
or(
|
||||
like(WebsitePaypalTransactions.transactionId, `%${q}%`),
|
||||
like(WebsitePaypalTransactions.description, `%${q}%`),
|
||||
...(Number.isFinite(asId) && asId > 0
|
||||
? [eq(WebsitePaypalTransactions.userId, asId)]
|
||||
: []),
|
||||
...(userIds.length
|
||||
? [inArray(WebsitePaypalTransactions.userId, userIds)]
|
||||
: []),
|
||||
)!,
|
||||
);
|
||||
}
|
||||
|
||||
const where = conditions.length > 0 ? and(...conditions) : undefined;
|
||||
|
||||
let total = 0;
|
||||
try {
|
||||
total = await prisma.websitePaypalTransactions.count({ where });
|
||||
const [totalRow] = await db
|
||||
.select({ total: count() })
|
||||
.from(WebsitePaypalTransactions)
|
||||
.where(where);
|
||||
total = Number(totalRow?.total ?? 0);
|
||||
} catch {
|
||||
total = 0;
|
||||
}
|
||||
|
||||
const pagination = calcPagination(total, parsed.page, parsed.perPage);
|
||||
const sortKey = parsed.sort ?? "id";
|
||||
const baseOrder = SORT_MAP[sortKey] ?? { id: "desc" as const };
|
||||
const orderField = Object.keys(baseOrder)[0] as keyof typeof baseOrder;
|
||||
const orderBy = {
|
||||
[orderField]: parsed.order,
|
||||
} as any;
|
||||
const orderCol =
|
||||
parsed.sort === "amount"
|
||||
? WebsitePaypalTransactions.amount
|
||||
: parsed.sort === "date"
|
||||
? WebsitePaypalTransactions.createdAt
|
||||
: parsed.sort === "status"
|
||||
? WebsitePaypalTransactions.status
|
||||
: parsed.sort === "package"
|
||||
? WebsitePaypalTransactions.description
|
||||
: parsed.sort === "payer"
|
||||
? WebsitePaypalTransactions.userId
|
||||
: WebsitePaypalTransactions.id;
|
||||
const orderBy = parsed.order === "asc" ? asc(orderCol) : desc(orderCol);
|
||||
|
||||
let transactions: Awaited<
|
||||
ReturnType<typeof prisma.websitePaypalTransactions.findMany>
|
||||
> = [];
|
||||
let transactions: (typeof WebsitePaypalTransactions.$inferSelect)[] = [];
|
||||
try {
|
||||
transactions = await prisma.websitePaypalTransactions.findMany({
|
||||
where,
|
||||
orderBy,
|
||||
skip: pagination.offset,
|
||||
take: pagination.perPage,
|
||||
});
|
||||
transactions = await db
|
||||
.select()
|
||||
.from(WebsitePaypalTransactions)
|
||||
.where(where)
|
||||
.orderBy(orderBy)
|
||||
.limit(pagination.perPage)
|
||||
.offset(pagination.offset);
|
||||
} catch {
|
||||
transactions = [];
|
||||
}
|
||||
@@ -129,10 +149,10 @@ export default async function AdminTransactions({
|
||||
let names = new Map<number, string>();
|
||||
if (userIds.length > 0) {
|
||||
try {
|
||||
const users = await prisma.user.findMany({
|
||||
where: { id: { in: userIds } },
|
||||
select: { id: true, username: true },
|
||||
});
|
||||
const users = await db
|
||||
.select({ id: User.id, username: User.username })
|
||||
.from(User)
|
||||
.where(inArray(User.id, userIds));
|
||||
names = new Map(users.map((u) => [u.id, u.username]));
|
||||
} catch {
|
||||
names = new Map();
|
||||
|
||||
@@ -1,20 +1,21 @@
|
||||
import { and, asc, count, like, or, type SQL } from "drizzle-orm";
|
||||
import { ExternalLink } from "lucide-react";
|
||||
import Link from "next/link";
|
||||
import { getTranslations } from "next-intl/server";
|
||||
import { EmulatorSettingsForm } from "@/components/admin/emulator-settings-form";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { calcPagination } from "@/lib/admin-helpers";
|
||||
import { prisma } from "@/lib/prisma";
|
||||
import { db, EmulatorSettings } from "@/lib/db";
|
||||
|
||||
const TRANSLATION_KEY_PATTERNS = [
|
||||
"commands.description.%",
|
||||
"commands.keys.%",
|
||||
"commands.cmd_%",
|
||||
"wordfilter.%",
|
||||
"pet.%message%",
|
||||
"matchmaking.%message%",
|
||||
"message.%",
|
||||
"notification.%",
|
||||
"commands.description.",
|
||||
"commands.keys.",
|
||||
"commands.cmd_",
|
||||
"wordfilter.",
|
||||
"pet.",
|
||||
"matchmaking.",
|
||||
"message.",
|
||||
"notification.",
|
||||
] as const;
|
||||
|
||||
export default async function EmulatorTranslationsPage({
|
||||
@@ -29,35 +30,39 @@ export default async function EmulatorTranslationsPage({
|
||||
const perPage = Math.min(Math.max(parseInt(sp.perPage || "50", 10), 10), 200);
|
||||
const page = Math.max(parseInt(sp.page || "1", 10), 1);
|
||||
|
||||
const patternFilters: any[] = TRANSLATION_KEY_PATTERNS.map((p) => ({
|
||||
key: { contains: p.replace(/%/g, "") },
|
||||
}));
|
||||
const patternFilters = or(
|
||||
...TRANSLATION_KEY_PATTERNS.map((p) => like(EmulatorSettings.key, `${p}%`)),
|
||||
)!;
|
||||
|
||||
const baseFilter: any = { OR: patternFilters };
|
||||
|
||||
const conditions: any[] = [baseFilter];
|
||||
if (group) conditions.push({ key: { startsWith: `${group}.` } });
|
||||
const conditions: SQL[] = [patternFilters];
|
||||
if (group) conditions.push(like(EmulatorSettings.key, `${group}.%`));
|
||||
if (search) {
|
||||
conditions.push({
|
||||
OR: [{ key: { contains: search } }, { value: { contains: search } }],
|
||||
});
|
||||
conditions.push(
|
||||
or(
|
||||
like(EmulatorSettings.key, `%${search}%`),
|
||||
like(EmulatorSettings.value, `%${search}%`),
|
||||
)!,
|
||||
);
|
||||
}
|
||||
const where: any = { AND: conditions };
|
||||
const where = and(...conditions);
|
||||
|
||||
const [settings, total, allKeys] = await Promise.all([
|
||||
prisma.emulatorSettings.findMany({
|
||||
where,
|
||||
orderBy: { key: "asc" },
|
||||
skip: (page - 1) * perPage,
|
||||
take: perPage,
|
||||
}),
|
||||
prisma.emulatorSettings.count({ where }),
|
||||
prisma.emulatorSettings.findMany({
|
||||
where: baseFilter,
|
||||
select: { key: true },
|
||||
}),
|
||||
const [settings, totalRows, allKeys] = await Promise.all([
|
||||
db
|
||||
.select()
|
||||
.from(EmulatorSettings)
|
||||
.where(where)
|
||||
.orderBy(asc(EmulatorSettings.key))
|
||||
.limit(perPage)
|
||||
.offset((page - 1) * perPage),
|
||||
db.select({ total: count() }).from(EmulatorSettings).where(where),
|
||||
db
|
||||
.select({ key: EmulatorSettings.key })
|
||||
.from(EmulatorSettings)
|
||||
.where(patternFilters),
|
||||
]);
|
||||
|
||||
const total = Number(totalRows[0]?.total ?? 0);
|
||||
|
||||
const groupCounts = new Map<string, number>();
|
||||
for (const { key } of allKeys) {
|
||||
const g = key.includes(".") ? key.split(".")[0] : "general";
|
||||
|
||||
@@ -1,6 +1,15 @@
|
||||
import { and, desc, eq, inArray, or } from "drizzle-orm";
|
||||
import { isBanActive, unixNow } from "@/lib/bans";
|
||||
import {
|
||||
AdminAuditLog,
|
||||
Ban,
|
||||
db,
|
||||
StaffActivities,
|
||||
SupportTickets,
|
||||
User,
|
||||
UsersSettings,
|
||||
} from "@/lib/db";
|
||||
import { formatDate } from "@/lib/format-date";
|
||||
import { prisma } from "@/lib/prisma";
|
||||
|
||||
export type SanctionEvent = {
|
||||
id: string;
|
||||
@@ -37,84 +46,89 @@ export async function loadUserSanctions(
|
||||
const now = unixNow();
|
||||
const events: SanctionEvent[] = [];
|
||||
|
||||
const [bans, audits, staffActs, cfh, settings] = await Promise.all([
|
||||
prisma.ban
|
||||
.findMany({
|
||||
where: { userId },
|
||||
orderBy: { timestamp: "desc" },
|
||||
take: 40,
|
||||
select: {
|
||||
id: true,
|
||||
banReason: true,
|
||||
type: true,
|
||||
timestamp: true,
|
||||
banExpire: true,
|
||||
userStaffId: true,
|
||||
},
|
||||
const [bans, audits, staffActs, cfh, settingsRows] = await Promise.all([
|
||||
db
|
||||
.select({
|
||||
id: Ban.id,
|
||||
banReason: Ban.banReason,
|
||||
type: Ban.type,
|
||||
timestamp: Ban.timestamp,
|
||||
banExpire: Ban.banExpire,
|
||||
userStaffId: Ban.userStaffId,
|
||||
})
|
||||
.from(Ban)
|
||||
.where(eq(Ban.userId, userId))
|
||||
.orderBy(desc(Ban.timestamp))
|
||||
.limit(40)
|
||||
.catch(() => []),
|
||||
prisma.adminAuditLog
|
||||
.findMany({
|
||||
where: {
|
||||
target: "User",
|
||||
targetId: userId,
|
||||
action: { in: [...AUDIT_ACTIONS] },
|
||||
},
|
||||
orderBy: { id: "desc" },
|
||||
take: 40,
|
||||
select: {
|
||||
id: true,
|
||||
action: true,
|
||||
details: true,
|
||||
createdAt: true,
|
||||
userId: true,
|
||||
user: { select: { username: true } },
|
||||
},
|
||||
db
|
||||
.select({
|
||||
id: AdminAuditLog.id,
|
||||
action: AdminAuditLog.action,
|
||||
details: AdminAuditLog.details,
|
||||
createdAt: AdminAuditLog.createdAt,
|
||||
userId: AdminAuditLog.userId,
|
||||
actorUsername: User.username,
|
||||
})
|
||||
.from(AdminAuditLog)
|
||||
.leftJoin(User, eq(AdminAuditLog.userId, User.id))
|
||||
.where(
|
||||
and(
|
||||
eq(AdminAuditLog.target, "User"),
|
||||
eq(AdminAuditLog.targetId, userId),
|
||||
inArray(AdminAuditLog.action, [...AUDIT_ACTIONS]),
|
||||
),
|
||||
)
|
||||
.orderBy(desc(AdminAuditLog.id))
|
||||
.limit(40)
|
||||
.catch(() => []),
|
||||
prisma.staffActivities
|
||||
.findMany({
|
||||
where: {
|
||||
targetType: "user",
|
||||
targetId: BigInt(userId),
|
||||
},
|
||||
orderBy: { id: "desc" },
|
||||
take: 40,
|
||||
select: {
|
||||
id: true,
|
||||
action: true,
|
||||
description: true,
|
||||
createdAt: true,
|
||||
userId: true,
|
||||
},
|
||||
db
|
||||
.select({
|
||||
id: StaffActivities.id,
|
||||
action: StaffActivities.action,
|
||||
description: StaffActivities.description,
|
||||
createdAt: StaffActivities.createdAt,
|
||||
userId: StaffActivities.userId,
|
||||
})
|
||||
.from(StaffActivities)
|
||||
.where(
|
||||
and(
|
||||
eq(StaffActivities.targetType, "user"),
|
||||
eq(StaffActivities.targetId, BigInt(userId)),
|
||||
),
|
||||
)
|
||||
.orderBy(desc(StaffActivities.id))
|
||||
.limit(40)
|
||||
.catch(() => []),
|
||||
prisma.supportTickets
|
||||
.findMany({
|
||||
where: {
|
||||
OR: [{ reportedId: userId }, { senderId: userId }],
|
||||
},
|
||||
orderBy: { id: "desc" },
|
||||
take: 20,
|
||||
select: {
|
||||
id: true,
|
||||
issue: true,
|
||||
state: true,
|
||||
timestamp: true,
|
||||
senderId: true,
|
||||
reportedId: true,
|
||||
modId: true,
|
||||
},
|
||||
db
|
||||
.select({
|
||||
id: SupportTickets.id,
|
||||
issue: SupportTickets.issue,
|
||||
state: SupportTickets.state,
|
||||
timestamp: SupportTickets.timestamp,
|
||||
senderId: SupportTickets.senderId,
|
||||
reportedId: SupportTickets.reportedId,
|
||||
modId: SupportTickets.modId,
|
||||
})
|
||||
.from(SupportTickets)
|
||||
.where(
|
||||
or(
|
||||
eq(SupportTickets.reportedId, userId),
|
||||
eq(SupportTickets.senderId, userId),
|
||||
),
|
||||
)
|
||||
.orderBy(desc(SupportTickets.id))
|
||||
.limit(20)
|
||||
.catch(() => []),
|
||||
db
|
||||
.select({ muteEndTimestamp: UsersSettings.muteEndTimestamp })
|
||||
.from(UsersSettings)
|
||||
.where(eq(UsersSettings.userId, userId))
|
||||
.limit(1)
|
||||
.catch(() => []),
|
||||
prisma.usersSettings
|
||||
.findUnique({
|
||||
where: { userId },
|
||||
select: { muteEndTimestamp: true },
|
||||
})
|
||||
.catch(() => null),
|
||||
]);
|
||||
|
||||
const settings = settingsRows[0] ?? null;
|
||||
const muteUntil = settings?.muteEndTimestamp ?? 0;
|
||||
if (muteUntil > now) {
|
||||
events.push({
|
||||
@@ -134,11 +148,10 @@ export async function loadUserSanctions(
|
||||
];
|
||||
const staffUsers =
|
||||
staffIds.length > 0
|
||||
? await prisma.user
|
||||
.findMany({
|
||||
where: { id: { in: staffIds } },
|
||||
select: { id: true, username: true },
|
||||
})
|
||||
? await db
|
||||
.select({ id: User.id, username: User.username })
|
||||
.from(User)
|
||||
.where(inArray(User.id, staffIds))
|
||||
.catch(() => [])
|
||||
: [];
|
||||
const staffName = new Map(staffUsers.map((u) => [u.id, u.username]));
|
||||
@@ -172,7 +185,7 @@ export async function loadUserSanctions(
|
||||
kind,
|
||||
label: a.action,
|
||||
detail: a.details?.slice(0, 200) || "—",
|
||||
actor: a.user?.username,
|
||||
actor: a.actorUsername ?? undefined,
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,18 @@
|
||||
import { asc, count, desc, eq, or, sql } from "drizzle-orm";
|
||||
import { notFound } from "next/navigation";
|
||||
import { prisma } from "@/lib/prisma";
|
||||
import {
|
||||
db,
|
||||
Rooms,
|
||||
RoomTradeLog,
|
||||
User,
|
||||
UsersBadges,
|
||||
UsersCurrency,
|
||||
} from "@/lib/db";
|
||||
|
||||
async function rawRows<T>(query: ReturnType<typeof sql>): Promise<T[]> {
|
||||
const [rows] = (await db.execute(query)) as unknown as [T[], unknown];
|
||||
return rows ?? [];
|
||||
}
|
||||
|
||||
export async function loadUserById(id: string) {
|
||||
const numericId = Number(id);
|
||||
@@ -7,9 +20,11 @@ export async function loadUserById(id: string) {
|
||||
Number.isSafeInteger(numericId) &&
|
||||
numericId > 0 &&
|
||||
String(numericId) === id;
|
||||
const user = await prisma.user.findUnique({
|
||||
where: isNumeric ? { id: numericId } : { username: id },
|
||||
});
|
||||
const [user] = await db
|
||||
.select()
|
||||
.from(User)
|
||||
.where(isNumeric ? eq(User.id, numericId) : eq(User.username, id))
|
||||
.limit(1);
|
||||
if (!user) notFound();
|
||||
|
||||
const [
|
||||
@@ -17,67 +32,76 @@ export async function loadUserById(id: string) {
|
||||
badges,
|
||||
bansReceived,
|
||||
permissionRows,
|
||||
roomCount,
|
||||
tradeCount,
|
||||
roomCountRows,
|
||||
tradeCountRows,
|
||||
userRooms,
|
||||
chatLogs,
|
||||
commandLogs,
|
||||
] = await Promise.all([
|
||||
prisma.usersCurrency.findMany({ where: { userId: user.id } }),
|
||||
prisma.usersBadges.findMany({
|
||||
where: { userId: user.id },
|
||||
orderBy: { slotId: "asc" },
|
||||
}),
|
||||
prisma.$queryRaw<
|
||||
Array<{
|
||||
id: number;
|
||||
banReason: string;
|
||||
type: string;
|
||||
timestamp: number;
|
||||
banExpire: number;
|
||||
staffName: string;
|
||||
}>
|
||||
>`
|
||||
db.select().from(UsersCurrency).where(eq(UsersCurrency.userId, user.id)),
|
||||
db
|
||||
.select()
|
||||
.from(UsersBadges)
|
||||
.where(eq(UsersBadges.userId, user.id))
|
||||
.orderBy(asc(UsersBadges.slotId)),
|
||||
rawRows<{
|
||||
id: number;
|
||||
banReason: string;
|
||||
type: string;
|
||||
timestamp: number;
|
||||
banExpire: number;
|
||||
staffName: string;
|
||||
}>(sql`
|
||||
SELECT b.id, b.ban_reason AS banReason, b.type, b.timestamp, b.ban_expire AS banExpire,
|
||||
COALESCE(s.username, 'Unknown') AS staffName
|
||||
FROM bans b LEFT JOIN users s ON s.id = b.user_staff_id
|
||||
WHERE b.user_id = ${user.id} ORDER BY b.id DESC LIMIT 5`,
|
||||
prisma.$queryRaw<Array<{ rankName: string }>>`
|
||||
SELECT COALESCE(rank_name, CONCAT('Rank ', id)) AS rankName FROM permissions WHERE id = ${user.rank} LIMIT 1`,
|
||||
prisma.rooms.count({ where: { ownerId: user.id } }),
|
||||
prisma.roomTradeLog.count({
|
||||
where: { OR: [{ userOneId: user.id }, { userTwoId: user.id }] },
|
||||
}),
|
||||
prisma.rooms.findMany({
|
||||
where: { ownerId: user.id },
|
||||
orderBy: { users: "desc" },
|
||||
take: 20,
|
||||
select: {
|
||||
id: true,
|
||||
name: true,
|
||||
state: true,
|
||||
users: true,
|
||||
usersMax: true,
|
||||
},
|
||||
}),
|
||||
prisma.$queryRaw<
|
||||
Array<{ id: number; roomId: number; timestamp: number; message: string }>
|
||||
>`
|
||||
WHERE b.user_id = ${user.id} ORDER BY b.id DESC LIMIT 5`),
|
||||
rawRows<{ rankName: string }>(sql`
|
||||
SELECT COALESCE(rank_name, CONCAT('Rank ', id)) AS rankName FROM permissions WHERE id = ${user.rank} LIMIT 1`),
|
||||
db.select({ total: count() }).from(Rooms).where(eq(Rooms.ownerId, user.id)),
|
||||
db
|
||||
.select({ total: count() })
|
||||
.from(RoomTradeLog)
|
||||
.where(
|
||||
or(
|
||||
eq(RoomTradeLog.userOneId, user.id),
|
||||
eq(RoomTradeLog.userTwoId, user.id),
|
||||
),
|
||||
),
|
||||
db
|
||||
.select({
|
||||
id: Rooms.id,
|
||||
name: Rooms.name,
|
||||
state: Rooms.state,
|
||||
users: Rooms.users,
|
||||
usersMax: Rooms.usersMax,
|
||||
})
|
||||
.from(Rooms)
|
||||
.where(eq(Rooms.ownerId, user.id))
|
||||
.orderBy(desc(Rooms.users))
|
||||
.limit(20),
|
||||
rawRows<{
|
||||
id: number;
|
||||
roomId: number;
|
||||
timestamp: number;
|
||||
message: string;
|
||||
}>(sql`
|
||||
SELECT ROW_NUMBER() OVER (ORDER BY timestamp DESC) AS id, room_id AS roomId, timestamp, message
|
||||
FROM chatlogs_room WHERE user_from_id = ${user.id} ORDER BY timestamp DESC LIMIT 20`,
|
||||
prisma.$queryRaw<
|
||||
Array<{
|
||||
id: number;
|
||||
timestamp: number;
|
||||
command: string;
|
||||
params: string;
|
||||
succes: string;
|
||||
}>
|
||||
>`
|
||||
FROM chatlogs_room WHERE user_from_id = ${user.id} ORDER BY timestamp DESC LIMIT 20`),
|
||||
rawRows<{
|
||||
id: number;
|
||||
timestamp: number;
|
||||
command: string;
|
||||
params: string;
|
||||
succes: string;
|
||||
}>(sql`
|
||||
SELECT ROW_NUMBER() OVER (ORDER BY timestamp DESC) AS id, timestamp, command, params, succes
|
||||
FROM commandlogs WHERE user_id = ${user.id} ORDER BY timestamp DESC LIMIT 10`,
|
||||
FROM commandlogs WHERE user_id = ${user.id} ORDER BY timestamp DESC LIMIT 10`),
|
||||
]);
|
||||
|
||||
const roomCount = Number(roomCountRows[0]?.total ?? 0);
|
||||
const tradeCount = Number(tradeCountRows[0]?.total ?? 0);
|
||||
|
||||
const diamonds =
|
||||
currencies.find((currency) => currency.type === 5)?.amount ?? 0;
|
||||
const duckets =
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import { sql } from "drizzle-orm";
|
||||
import { redirect } from "next/navigation";
|
||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
|
||||
import { db } from "@/lib/db";
|
||||
import { canAccess, getAdminContext, PERMS } from "@/lib/permissions";
|
||||
import { Prisma, prisma } from "@/lib/prisma";
|
||||
import { getWatchedUserIds } from "@/lib/services/watch";
|
||||
import { UserAccountInfo } from "../../_components/user-account-info";
|
||||
import { UserHero } from "../../_components/user-hero";
|
||||
@@ -28,10 +29,10 @@ export default async function UserEditPage({
|
||||
const watched = await getWatchedUserIds(session.user.id);
|
||||
|
||||
// Fetch rank list from permissions table for the rank dropdown
|
||||
const rawRanks: { id: number; rank_name: string }[] = await prisma.$queryRaw(
|
||||
Prisma.sql`SELECT id, rank_name FROM permissions ORDER BY id ASC`,
|
||||
);
|
||||
const ranks = rawRanks.map((r) => ({
|
||||
const [rawRanks] = (await db.execute(
|
||||
sql`SELECT id, rank_name FROM permissions ORDER BY id ASC`,
|
||||
)) as unknown as [{ id: number; rank_name: string }[], unknown];
|
||||
const ranks = (rawRanks ?? []).map((r) => ({
|
||||
id: Number(r.id),
|
||||
name: String(r.rank_name || `Rank ${r.id}`),
|
||||
}));
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { desc } from "drizzle-orm";
|
||||
import { redirect } from "next/navigation";
|
||||
import { db, WebsiteShopVouchers } from "@/lib/db";
|
||||
import { canAccess, getAdminContext, PERMS } from "@/lib/permissions";
|
||||
import { prisma } from "@/lib/prisma";
|
||||
import { type VoucherRow, VouchersClient } from "./vouchers-client";
|
||||
|
||||
export default async function AdminVouchers() {
|
||||
@@ -14,19 +15,19 @@ export default async function AdminVouchers() {
|
||||
|
||||
let rows: VoucherRow[] = [];
|
||||
try {
|
||||
const vouchers = await prisma.websiteShopVouchers.findMany({
|
||||
select: {
|
||||
id: true,
|
||||
code: true,
|
||||
amount: true,
|
||||
maxUses: true,
|
||||
useCount: true,
|
||||
expiresAt: true,
|
||||
createdAt: true,
|
||||
},
|
||||
orderBy: { id: "desc" },
|
||||
take: 200,
|
||||
});
|
||||
const vouchers = await db
|
||||
.select({
|
||||
id: WebsiteShopVouchers.id,
|
||||
code: WebsiteShopVouchers.code,
|
||||
amount: WebsiteShopVouchers.amount,
|
||||
maxUses: WebsiteShopVouchers.maxUses,
|
||||
useCount: WebsiteShopVouchers.useCount,
|
||||
expiresAt: WebsiteShopVouchers.expiresAt,
|
||||
createdAt: WebsiteShopVouchers.createdAt,
|
||||
})
|
||||
.from(WebsiteShopVouchers)
|
||||
.orderBy(desc(WebsiteShopVouchers.id))
|
||||
.limit(200);
|
||||
rows = vouchers.map((v) => {
|
||||
const expired = v.expiresAt ? v.expiresAt.getTime() <= now : false;
|
||||
const usedUp = v.maxUses > 0 && v.useCount >= v.maxUses;
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
import { asc } from "drizzle-orm";
|
||||
import { Filter } from "lucide-react";
|
||||
import { redirect } from "next/navigation";
|
||||
import { getTranslations } from "next-intl/server";
|
||||
import { AdminPageShell } from "@/components/admin/admin-page-shell";
|
||||
import { db, WebsiteWordfilter } from "@/lib/db";
|
||||
import { canAccess, getAdminContext, PERMS } from "@/lib/permissions";
|
||||
import { prisma } from "@/lib/prisma";
|
||||
import { WordFilterClient } from "./wordfilter-client";
|
||||
|
||||
export default async function AdminWordFilter() {
|
||||
@@ -21,11 +22,11 @@ export default async function AdminWordFilter() {
|
||||
|
||||
let words: { id: string; word: string }[] = [];
|
||||
try {
|
||||
const rows = await prisma.websiteWordfilter.findMany({
|
||||
select: { id: true, word: true },
|
||||
orderBy: { word: "asc" },
|
||||
take: 500,
|
||||
});
|
||||
const rows = await db
|
||||
.select({ id: WebsiteWordfilter.id, word: WebsiteWordfilter.word })
|
||||
.from(WebsiteWordfilter)
|
||||
.orderBy(asc(WebsiteWordfilter.word))
|
||||
.limit(500);
|
||||
words = rows.map((w) => ({ id: String(w.id), word: w.word }));
|
||||
} catch {
|
||||
words = [];
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { asc } from "drizzle-orm";
|
||||
import { redirect } from "next/navigation";
|
||||
import { getTranslations } from "next-intl/server";
|
||||
import {
|
||||
@@ -8,8 +9,8 @@ import {
|
||||
} from "@/actions/admin-writeable-boxes";
|
||||
import { StatusCard } from "@/components/admin/dashboard";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { db, WebsiteWriteableBoxes } from "@/lib/db";
|
||||
import { canAccess, getAdminContext, PERMS } from "@/lib/permissions";
|
||||
import { prisma } from "@/lib/prisma";
|
||||
|
||||
export const metadata = { title: "Writeable boxes" };
|
||||
|
||||
@@ -20,11 +21,11 @@ export default async function AdminWriteableBoxes() {
|
||||
}
|
||||
|
||||
const t = await getTranslations("pages.admin.writeableBoxes");
|
||||
const boxes = await prisma.websiteWriteableBoxes
|
||||
.findMany({
|
||||
orderBy: [{ position: "asc" }, { id: "asc" }],
|
||||
take: 500,
|
||||
})
|
||||
const boxes = await db
|
||||
.select()
|
||||
.from(WebsiteWriteableBoxes)
|
||||
.orderBy(asc(WebsiteWriteableBoxes.position), asc(WebsiteWriteableBoxes.id))
|
||||
.limit(500)
|
||||
.catch(() => []);
|
||||
|
||||
const activeCount = boxes.filter((b) => b.isActive).length;
|
||||
|
||||
Reference in new issue
Block a user