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 { notFound, redirect } from "next/navigation";
|
||||||
import { getTranslations } from "next-intl/server";
|
import { getTranslations } from "next-intl/server";
|
||||||
import { TicketQueueBanner } from "@/components/admin/ticket-queue-banner";
|
import { TicketQueueBanner } from "@/components/admin/ticket-queue-banner";
|
||||||
import { getMinStaffRank } from "@/lib/admin/min-staff-rank";
|
import { getMinStaffRank } from "@/lib/admin/min-staff-rank";
|
||||||
|
import { db, User, WebsiteTicket } from "@/lib/db";
|
||||||
import { canAccess, getAdminContext, PERMS } from "@/lib/permissions";
|
import { canAccess, getAdminContext, PERMS } from "@/lib/permissions";
|
||||||
import { prisma } from "@/lib/prisma";
|
|
||||||
import { AdminTicketDetail } from "./admin-ticket-detail";
|
import { AdminTicketDetail } from "./admin-ticket-detail";
|
||||||
|
|
||||||
export default async function AdminTicketDetailPage({
|
export default async function AdminTicketDetailPage({
|
||||||
@@ -21,11 +22,11 @@ export default async function AdminTicketDetailPage({
|
|||||||
const ticketId = Number(id);
|
const ticketId = Number(id);
|
||||||
if (Number.isNaN(ticketId)) notFound();
|
if (Number.isNaN(ticketId)) notFound();
|
||||||
|
|
||||||
const ticket = await prisma.websiteTicket.findUnique({
|
const ticket = await db.query.WebsiteTicket.findFirst({
|
||||||
where: { id: ticketId },
|
where: eq(WebsiteTicket.id, ticketId),
|
||||||
include: {
|
with: {
|
||||||
creator: {
|
creator: {
|
||||||
select: {
|
columns: {
|
||||||
id: true,
|
id: true,
|
||||||
username: true,
|
username: true,
|
||||||
look: true,
|
look: true,
|
||||||
@@ -35,12 +36,12 @@ export default async function AdminTicketDetailPage({
|
|||||||
accountCreated: true,
|
accountCreated: true,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
assignee: { select: { id: true, username: true } },
|
assignee: { columns: { id: true, username: true } },
|
||||||
messages: {
|
messages: {
|
||||||
orderBy: { createdAt: "asc" },
|
orderBy: (m, { asc: a }) => [a(m.createdAt)],
|
||||||
include: {
|
with: {
|
||||||
user: {
|
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
|
// Staff assignees: min_staff_rank and above
|
||||||
const minStaffRank = await getMinStaffRank();
|
const minStaffRank = await getMinStaffRank();
|
||||||
const staffMembers = await prisma.user.findMany({
|
const staffMembers = await db
|
||||||
where: { rank: { gte: minStaffRank } },
|
.select({ id: User.id, username: User.username })
|
||||||
select: { id: true, username: true },
|
.from(User)
|
||||||
orderBy: { username: "asc" },
|
.where(gte(User.rank, minStaffRank))
|
||||||
});
|
.orderBy(asc(User.username));
|
||||||
|
|
||||||
const canEdit = canAccess(permissions, PERMS.TICKETS_EDIT, session.user.rank);
|
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 { redirect } from "next/navigation";
|
||||||
import { getTranslations } from "next-intl/server";
|
import { getTranslations } from "next-intl/server";
|
||||||
import { StatusCard } from "@/components/admin/dashboard";
|
import { StatusCard } from "@/components/admin/dashboard";
|
||||||
import { TicketQueueBanner } from "@/components/admin/ticket-queue-banner";
|
import { TicketQueueBanner } from "@/components/admin/ticket-queue-banner";
|
||||||
import { fetchTicketQueueOpenCounts } from "@/lib/admin/ticket-queue-counts";
|
import { fetchTicketQueueOpenCounts } from "@/lib/admin/ticket-queue-counts";
|
||||||
import { calcPagination, parseListParams } from "@/lib/admin-helpers";
|
import { calcPagination, parseListParams } from "@/lib/admin-helpers";
|
||||||
|
import { db, User, WebsiteTicket } from "@/lib/db";
|
||||||
import { formatDate } from "@/lib/format-date";
|
import { formatDate } from "@/lib/format-date";
|
||||||
import { canAccess, getAdminContext, PERMS } from "@/lib/permissions";
|
import { canAccess, getAdminContext, PERMS } from "@/lib/permissions";
|
||||||
import { prisma } from "@/lib/prisma";
|
|
||||||
import { type TicketRow, TicketsTable } from "./tickets-table";
|
import { type TicketRow, TicketsTable } from "./tickets-table";
|
||||||
|
|
||||||
export default async function AdminTicketsPage({
|
export default async function AdminTicketsPage({
|
||||||
@@ -24,92 +37,123 @@ export default async function AdminTicketsPage({
|
|||||||
const parsed = parseListParams(new URLSearchParams(raw));
|
const parsed = parseListParams(new URLSearchParams(raw));
|
||||||
const statusFilter = raw.filter_status || raw.status || "active";
|
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") {
|
if (statusFilter === "active") {
|
||||||
conditions.push({ status: { not: "closed" } });
|
conditions.push(ne(WebsiteTicket.status, "closed"));
|
||||||
} else if (statusFilter && statusFilter !== "all") {
|
} else if (statusFilter && statusFilter !== "all") {
|
||||||
conditions.push({ status: statusFilter });
|
conditions.push(eq(WebsiteTicket.status, statusFilter));
|
||||||
}
|
}
|
||||||
|
|
||||||
if (parsed.search.trim()) {
|
if (parsed.search.trim()) {
|
||||||
const q = parsed.search.trim();
|
const q = parsed.search.trim();
|
||||||
const asId = Number(q);
|
const asId = Number(q);
|
||||||
conditions.push({
|
const searchOr = [
|
||||||
OR: [
|
like(WebsiteTicket.subject, `%${q}%`),
|
||||||
{ subject: { contains: q } },
|
like(WebsiteTicket.category, `%${q}%`),
|
||||||
{ category: { contains: q } },
|
like(Creator.username, `%${q}%`),
|
||||||
{ creator: { username: { contains: q } } },
|
like(Assignee.username, `%${q}%`),
|
||||||
{ assignee: { username: { contains: q } } },
|
...(Number.isFinite(asId) && asId > 0
|
||||||
...(Number.isFinite(asId) && asId > 0 ? [{ id: asId }] : []),
|
? [eq(WebsiteTicket.id, asId)]
|
||||||
],
|
: []),
|
||||||
});
|
];
|
||||||
|
conditions.push(or(...searchOr)!);
|
||||||
}
|
}
|
||||||
|
|
||||||
const where: any =
|
const where = conditions.length > 0 ? and(...conditions) : undefined;
|
||||||
conditions.length === 0
|
|
||||||
? {}
|
|
||||||
: conditions.length === 1
|
|
||||||
? conditions[0]
|
|
||||||
: { AND: conditions };
|
|
||||||
|
|
||||||
const SORT_MAP: Record<string, any> = {
|
const orderCol =
|
||||||
subject: { subject: "asc" },
|
parsed.sort === "subject"
|
||||||
status: { status: "asc" },
|
? WebsiteTicket.subject
|
||||||
priority: { priority: "asc" },
|
: parsed.sort === "status"
|
||||||
assignee: { assigneeId: "asc" },
|
? WebsiteTicket.status
|
||||||
date: { createdAt: "desc" },
|
: parsed.sort === "priority"
|
||||||
id: { id: "desc" },
|
? 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 [totalRows, openRows, inProgressRows, waitingRows, closedRows, queues] =
|
||||||
const orderField = Object.keys(baseOrder)[0] as keyof typeof baseOrder;
|
|
||||||
const orderBy = {
|
|
||||||
[orderField]: parsed.order,
|
|
||||||
} as any;
|
|
||||||
|
|
||||||
const [total, openCount, inProgressCount, waitingCount, closedCount, queues] =
|
|
||||||
await Promise.all([
|
await Promise.all([
|
||||||
prisma.websiteTicket.count({ where }).catch(() => 0),
|
db
|
||||||
prisma.websiteTicket.count({ where: { status: "open" } }).catch(() => 0),
|
.select({ total: count() })
|
||||||
prisma.websiteTicket
|
.from(WebsiteTicket)
|
||||||
.count({ where: { status: "in_progress" } })
|
.leftJoin(Creator, eq(WebsiteTicket.creatorId, Creator.id))
|
||||||
.catch(() => 0),
|
.leftJoin(Assignee, eq(WebsiteTicket.assigneeId, Assignee.id))
|
||||||
prisma.websiteTicket
|
.where(where)
|
||||||
.count({ where: { status: "waiting" } })
|
.catch(() => [{ total: 0 }]),
|
||||||
.catch(() => 0),
|
db
|
||||||
prisma.websiteTicket
|
.select({ total: count() })
|
||||||
.count({ where: { status: "closed" } })
|
.from(WebsiteTicket)
|
||||||
.catch(() => 0),
|
.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(),
|
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 pagination = calcPagination(total, parsed.page, parsed.perPage);
|
||||||
|
|
||||||
const tickets = await prisma.websiteTicket
|
const tickets = await db
|
||||||
.findMany({
|
.select({
|
||||||
where,
|
id: WebsiteTicket.id,
|
||||||
orderBy,
|
subject: WebsiteTicket.subject,
|
||||||
skip: pagination.offset,
|
creatorId: WebsiteTicket.creatorId,
|
||||||
take: pagination.perPage,
|
category: WebsiteTicket.category,
|
||||||
include: {
|
status: WebsiteTicket.status,
|
||||||
creator: { select: { id: true, username: true } },
|
priority: WebsiteTicket.priority,
|
||||||
assignee: { select: { id: true, username: true } },
|
createdAt: WebsiteTicket.createdAt,
|
||||||
_count: { select: { messages: true } },
|
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(() => []);
|
.catch(() => []);
|
||||||
|
|
||||||
const rows: TicketRow[] = tickets.map((ticket) => ({
|
const rows: TicketRow[] = tickets.map((ticket) => ({
|
||||||
id: ticket.id,
|
id: ticket.id,
|
||||||
subject: ticket.subject,
|
subject: ticket.subject,
|
||||||
creator: ticket.creator.username,
|
creator: ticket.creatorUsername ?? "—",
|
||||||
creatorId: ticket.creatorId,
|
creatorId: ticket.creatorId,
|
||||||
category: ticket.category,
|
category: ticket.category,
|
||||||
messages: ticket._count.messages,
|
messages: Number(ticket.messageCount ?? 0),
|
||||||
status: ticket.status,
|
status: ticket.status,
|
||||||
priority: ticket.priority,
|
priority: ticket.priority,
|
||||||
assignee: ticket.assignee?.username ?? "—",
|
assignee: ticket.assigneeUsername ?? "—",
|
||||||
date: formatDate(ticket.createdAt, "date"),
|
date: formatDate(ticket.createdAt, "date"),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
|
import { asc } from "drizzle-orm";
|
||||||
import { redirect } from "next/navigation";
|
import { redirect } from "next/navigation";
|
||||||
|
import { db, WebsiteTicketTemplate } from "@/lib/db";
|
||||||
import { canAccess, getAdminContext, PERMS } from "@/lib/permissions";
|
import { canAccess, getAdminContext, PERMS } from "@/lib/permissions";
|
||||||
import { prisma } from "@/lib/prisma";
|
|
||||||
import { TicketTemplatesManager } from "./templates-manager";
|
import { TicketTemplatesManager } from "./templates-manager";
|
||||||
|
|
||||||
export default async function TicketTemplatesPage() {
|
export default async function TicketTemplatesPage() {
|
||||||
@@ -9,9 +10,10 @@ export default async function TicketTemplatesPage() {
|
|||||||
redirect("/admin/tickets");
|
redirect("/admin/tickets");
|
||||||
}
|
}
|
||||||
|
|
||||||
const templates = await prisma.websiteTicketTemplate.findMany({
|
const templates = await db
|
||||||
orderBy: { sortOrder: "asc" },
|
.select()
|
||||||
});
|
.from(WebsiteTicketTemplate)
|
||||||
|
.orderBy(asc(WebsiteTicketTemplate.sortOrder));
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="space-y-6">
|
<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 { redirect } from "next/navigation";
|
||||||
import { getTranslations } from "next-intl/server";
|
import { getTranslations } from "next-intl/server";
|
||||||
import { StatusCard } from "@/components/admin/dashboard";
|
import { StatusCard } from "@/components/admin/dashboard";
|
||||||
import { calcPagination, parseListParams } from "@/lib/admin-helpers";
|
import { calcPagination, parseListParams } from "@/lib/admin-helpers";
|
||||||
|
import { db, User, WebsitePaypalTransactions } from "@/lib/db";
|
||||||
import { formatDate } from "@/lib/format-date";
|
import { formatDate } from "@/lib/format-date";
|
||||||
import { canAccess, getAdminContext, PERMS } from "@/lib/permissions";
|
import { canAccess, getAdminContext, PERMS } from "@/lib/permissions";
|
||||||
import { prisma } from "@/lib/prisma";
|
|
||||||
import { type TransactionRow, TransactionsTable } from "./transactions-table";
|
import { type TransactionRow, TransactionsTable } from "./transactions-table";
|
||||||
|
|
||||||
const PAID_STATUS = ["completed", "paid", "success", "approved"] as const;
|
const PAID_STATUS = ["completed", "paid", "success", "approved"] as const;
|
||||||
@@ -33,15 +44,6 @@ function formatAmount(amount: number, currency: string): string {
|
|||||||
return `${value} ${currency}`;
|
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({
|
export default async function AdminTransactions({
|
||||||
searchParams,
|
searchParams,
|
||||||
}: {
|
}: {
|
||||||
@@ -57,14 +59,20 @@ export default async function AdminTransactions({
|
|||||||
const parsed = parseListParams(new URLSearchParams(raw));
|
const parsed = parseListParams(new URLSearchParams(raw));
|
||||||
const statusFilter = raw.filter_status ?? "";
|
const statusFilter = raw.filter_status ?? "";
|
||||||
|
|
||||||
const where: any = {};
|
const conditions: SQL[] = [];
|
||||||
|
|
||||||
if (statusFilter === "paid") {
|
if (statusFilter === "paid") {
|
||||||
where.status = { in: [...PAID_STATUS] };
|
conditions.push(
|
||||||
|
inArray(WebsitePaypalTransactions.status, [...PAID_STATUS]),
|
||||||
|
);
|
||||||
} else if (statusFilter === "failed") {
|
} else if (statusFilter === "failed") {
|
||||||
where.status = { in: [...FAILED_STATUS] };
|
conditions.push(
|
||||||
|
inArray(WebsitePaypalTransactions.status, [...FAILED_STATUS]),
|
||||||
|
);
|
||||||
} else if (statusFilter === "pending") {
|
} else if (statusFilter === "pending") {
|
||||||
where.status = { in: [...PENDING_STATUS] };
|
conditions.push(
|
||||||
|
inArray(WebsitePaypalTransactions.status, [...PENDING_STATUS]),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (parsed.search.trim()) {
|
if (parsed.search.trim()) {
|
||||||
@@ -72,55 +80,67 @@ export default async function AdminTransactions({
|
|||||||
const asId = Number(q);
|
const asId = Number(q);
|
||||||
let userIds: number[] = [];
|
let userIds: number[] = [];
|
||||||
try {
|
try {
|
||||||
const users = await prisma.user.findMany({
|
const users = await db
|
||||||
where: { username: { contains: q } },
|
.select({ id: User.id })
|
||||||
select: { id: true },
|
.from(User)
|
||||||
take: 50,
|
.where(like(User.username, `%${q}%`))
|
||||||
});
|
.limit(50);
|
||||||
userIds = users.map((u) => u.id);
|
userIds = users.map((u) => u.id);
|
||||||
} catch {
|
} catch {
|
||||||
userIds = [];
|
userIds = [];
|
||||||
}
|
}
|
||||||
|
|
||||||
where.AND = [
|
conditions.push(
|
||||||
...(where.status ? [{ status: where.status }] : []),
|
or(
|
||||||
{
|
like(WebsitePaypalTransactions.transactionId, `%${q}%`),
|
||||||
OR: [
|
like(WebsitePaypalTransactions.description, `%${q}%`),
|
||||||
{ transactionId: { contains: q } },
|
...(Number.isFinite(asId) && asId > 0
|
||||||
{ description: { contains: q } },
|
? [eq(WebsitePaypalTransactions.userId, asId)]
|
||||||
...(Number.isFinite(asId) && asId > 0 ? [{ userId: asId }] : []),
|
: []),
|
||||||
...(userIds.length ? [{ userId: { in: userIds } }] : []),
|
...(userIds.length
|
||||||
],
|
? [inArray(WebsitePaypalTransactions.userId, userIds)]
|
||||||
},
|
: []),
|
||||||
];
|
)!,
|
||||||
delete where.status;
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const where = conditions.length > 0 ? and(...conditions) : undefined;
|
||||||
|
|
||||||
let total = 0;
|
let total = 0;
|
||||||
try {
|
try {
|
||||||
total = await prisma.websitePaypalTransactions.count({ where });
|
const [totalRow] = await db
|
||||||
|
.select({ total: count() })
|
||||||
|
.from(WebsitePaypalTransactions)
|
||||||
|
.where(where);
|
||||||
|
total = Number(totalRow?.total ?? 0);
|
||||||
} catch {
|
} catch {
|
||||||
total = 0;
|
total = 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
const pagination = calcPagination(total, parsed.page, parsed.perPage);
|
const pagination = calcPagination(total, parsed.page, parsed.perPage);
|
||||||
const sortKey = parsed.sort ?? "id";
|
const orderCol =
|
||||||
const baseOrder = SORT_MAP[sortKey] ?? { id: "desc" as const };
|
parsed.sort === "amount"
|
||||||
const orderField = Object.keys(baseOrder)[0] as keyof typeof baseOrder;
|
? WebsitePaypalTransactions.amount
|
||||||
const orderBy = {
|
: parsed.sort === "date"
|
||||||
[orderField]: parsed.order,
|
? WebsitePaypalTransactions.createdAt
|
||||||
} as any;
|
: 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<
|
let transactions: (typeof WebsitePaypalTransactions.$inferSelect)[] = [];
|
||||||
ReturnType<typeof prisma.websitePaypalTransactions.findMany>
|
|
||||||
> = [];
|
|
||||||
try {
|
try {
|
||||||
transactions = await prisma.websitePaypalTransactions.findMany({
|
transactions = await db
|
||||||
where,
|
.select()
|
||||||
orderBy,
|
.from(WebsitePaypalTransactions)
|
||||||
skip: pagination.offset,
|
.where(where)
|
||||||
take: pagination.perPage,
|
.orderBy(orderBy)
|
||||||
});
|
.limit(pagination.perPage)
|
||||||
|
.offset(pagination.offset);
|
||||||
} catch {
|
} catch {
|
||||||
transactions = [];
|
transactions = [];
|
||||||
}
|
}
|
||||||
@@ -129,10 +149,10 @@ export default async function AdminTransactions({
|
|||||||
let names = new Map<number, string>();
|
let names = new Map<number, string>();
|
||||||
if (userIds.length > 0) {
|
if (userIds.length > 0) {
|
||||||
try {
|
try {
|
||||||
const users = await prisma.user.findMany({
|
const users = await db
|
||||||
where: { id: { in: userIds } },
|
.select({ id: User.id, username: User.username })
|
||||||
select: { id: true, username: true },
|
.from(User)
|
||||||
});
|
.where(inArray(User.id, userIds));
|
||||||
names = new Map(users.map((u) => [u.id, u.username]));
|
names = new Map(users.map((u) => [u.id, u.username]));
|
||||||
} catch {
|
} catch {
|
||||||
names = new Map();
|
names = new Map();
|
||||||
|
|||||||
@@ -1,20 +1,21 @@
|
|||||||
|
import { and, asc, count, like, or, type SQL } from "drizzle-orm";
|
||||||
import { ExternalLink } from "lucide-react";
|
import { ExternalLink } from "lucide-react";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import { getTranslations } from "next-intl/server";
|
import { getTranslations } from "next-intl/server";
|
||||||
import { EmulatorSettingsForm } from "@/components/admin/emulator-settings-form";
|
import { EmulatorSettingsForm } from "@/components/admin/emulator-settings-form";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import { calcPagination } from "@/lib/admin-helpers";
|
import { calcPagination } from "@/lib/admin-helpers";
|
||||||
import { prisma } from "@/lib/prisma";
|
import { db, EmulatorSettings } from "@/lib/db";
|
||||||
|
|
||||||
const TRANSLATION_KEY_PATTERNS = [
|
const TRANSLATION_KEY_PATTERNS = [
|
||||||
"commands.description.%",
|
"commands.description.",
|
||||||
"commands.keys.%",
|
"commands.keys.",
|
||||||
"commands.cmd_%",
|
"commands.cmd_",
|
||||||
"wordfilter.%",
|
"wordfilter.",
|
||||||
"pet.%message%",
|
"pet.",
|
||||||
"matchmaking.%message%",
|
"matchmaking.",
|
||||||
"message.%",
|
"message.",
|
||||||
"notification.%",
|
"notification.",
|
||||||
] as const;
|
] as const;
|
||||||
|
|
||||||
export default async function EmulatorTranslationsPage({
|
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 perPage = Math.min(Math.max(parseInt(sp.perPage || "50", 10), 10), 200);
|
||||||
const page = Math.max(parseInt(sp.page || "1", 10), 1);
|
const page = Math.max(parseInt(sp.page || "1", 10), 1);
|
||||||
|
|
||||||
const patternFilters: any[] = TRANSLATION_KEY_PATTERNS.map((p) => ({
|
const patternFilters = or(
|
||||||
key: { contains: p.replace(/%/g, "") },
|
...TRANSLATION_KEY_PATTERNS.map((p) => like(EmulatorSettings.key, `${p}%`)),
|
||||||
}));
|
)!;
|
||||||
|
|
||||||
const baseFilter: any = { OR: patternFilters };
|
const conditions: SQL[] = [patternFilters];
|
||||||
|
if (group) conditions.push(like(EmulatorSettings.key, `${group}.%`));
|
||||||
const conditions: any[] = [baseFilter];
|
|
||||||
if (group) conditions.push({ key: { startsWith: `${group}.` } });
|
|
||||||
if (search) {
|
if (search) {
|
||||||
conditions.push({
|
conditions.push(
|
||||||
OR: [{ key: { contains: search } }, { value: { contains: search } }],
|
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([
|
const [settings, totalRows, allKeys] = await Promise.all([
|
||||||
prisma.emulatorSettings.findMany({
|
db
|
||||||
where,
|
.select()
|
||||||
orderBy: { key: "asc" },
|
.from(EmulatorSettings)
|
||||||
skip: (page - 1) * perPage,
|
.where(where)
|
||||||
take: perPage,
|
.orderBy(asc(EmulatorSettings.key))
|
||||||
}),
|
.limit(perPage)
|
||||||
prisma.emulatorSettings.count({ where }),
|
.offset((page - 1) * perPage),
|
||||||
prisma.emulatorSettings.findMany({
|
db.select({ total: count() }).from(EmulatorSettings).where(where),
|
||||||
where: baseFilter,
|
db
|
||||||
select: { key: true },
|
.select({ key: EmulatorSettings.key })
|
||||||
}),
|
.from(EmulatorSettings)
|
||||||
|
.where(patternFilters),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
const total = Number(totalRows[0]?.total ?? 0);
|
||||||
|
|
||||||
const groupCounts = new Map<string, number>();
|
const groupCounts = new Map<string, number>();
|
||||||
for (const { key } of allKeys) {
|
for (const { key } of allKeys) {
|
||||||
const g = key.includes(".") ? key.split(".")[0] : "general";
|
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 { isBanActive, unixNow } from "@/lib/bans";
|
||||||
|
import {
|
||||||
|
AdminAuditLog,
|
||||||
|
Ban,
|
||||||
|
db,
|
||||||
|
StaffActivities,
|
||||||
|
SupportTickets,
|
||||||
|
User,
|
||||||
|
UsersSettings,
|
||||||
|
} from "@/lib/db";
|
||||||
import { formatDate } from "@/lib/format-date";
|
import { formatDate } from "@/lib/format-date";
|
||||||
import { prisma } from "@/lib/prisma";
|
|
||||||
|
|
||||||
export type SanctionEvent = {
|
export type SanctionEvent = {
|
||||||
id: string;
|
id: string;
|
||||||
@@ -37,84 +46,89 @@ export async function loadUserSanctions(
|
|||||||
const now = unixNow();
|
const now = unixNow();
|
||||||
const events: SanctionEvent[] = [];
|
const events: SanctionEvent[] = [];
|
||||||
|
|
||||||
const [bans, audits, staffActs, cfh, settings] = await Promise.all([
|
const [bans, audits, staffActs, cfh, settingsRows] = await Promise.all([
|
||||||
prisma.ban
|
db
|
||||||
.findMany({
|
.select({
|
||||||
where: { userId },
|
id: Ban.id,
|
||||||
orderBy: { timestamp: "desc" },
|
banReason: Ban.banReason,
|
||||||
take: 40,
|
type: Ban.type,
|
||||||
select: {
|
timestamp: Ban.timestamp,
|
||||||
id: true,
|
banExpire: Ban.banExpire,
|
||||||
banReason: true,
|
userStaffId: Ban.userStaffId,
|
||||||
type: true,
|
|
||||||
timestamp: true,
|
|
||||||
banExpire: true,
|
|
||||||
userStaffId: true,
|
|
||||||
},
|
|
||||||
})
|
})
|
||||||
|
.from(Ban)
|
||||||
|
.where(eq(Ban.userId, userId))
|
||||||
|
.orderBy(desc(Ban.timestamp))
|
||||||
|
.limit(40)
|
||||||
.catch(() => []),
|
.catch(() => []),
|
||||||
prisma.adminAuditLog
|
db
|
||||||
.findMany({
|
.select({
|
||||||
where: {
|
id: AdminAuditLog.id,
|
||||||
target: "User",
|
action: AdminAuditLog.action,
|
||||||
targetId: userId,
|
details: AdminAuditLog.details,
|
||||||
action: { in: [...AUDIT_ACTIONS] },
|
createdAt: AdminAuditLog.createdAt,
|
||||||
},
|
userId: AdminAuditLog.userId,
|
||||||
orderBy: { id: "desc" },
|
actorUsername: User.username,
|
||||||
take: 40,
|
|
||||||
select: {
|
|
||||||
id: true,
|
|
||||||
action: true,
|
|
||||||
details: true,
|
|
||||||
createdAt: true,
|
|
||||||
userId: true,
|
|
||||||
user: { select: { username: true } },
|
|
||||||
},
|
|
||||||
})
|
})
|
||||||
|
.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(() => []),
|
.catch(() => []),
|
||||||
prisma.staffActivities
|
db
|
||||||
.findMany({
|
.select({
|
||||||
where: {
|
id: StaffActivities.id,
|
||||||
targetType: "user",
|
action: StaffActivities.action,
|
||||||
targetId: BigInt(userId),
|
description: StaffActivities.description,
|
||||||
},
|
createdAt: StaffActivities.createdAt,
|
||||||
orderBy: { id: "desc" },
|
userId: StaffActivities.userId,
|
||||||
take: 40,
|
|
||||||
select: {
|
|
||||||
id: true,
|
|
||||||
action: true,
|
|
||||||
description: true,
|
|
||||||
createdAt: true,
|
|
||||||
userId: true,
|
|
||||||
},
|
|
||||||
})
|
})
|
||||||
|
.from(StaffActivities)
|
||||||
|
.where(
|
||||||
|
and(
|
||||||
|
eq(StaffActivities.targetType, "user"),
|
||||||
|
eq(StaffActivities.targetId, BigInt(userId)),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.orderBy(desc(StaffActivities.id))
|
||||||
|
.limit(40)
|
||||||
.catch(() => []),
|
.catch(() => []),
|
||||||
prisma.supportTickets
|
db
|
||||||
.findMany({
|
.select({
|
||||||
where: {
|
id: SupportTickets.id,
|
||||||
OR: [{ reportedId: userId }, { senderId: userId }],
|
issue: SupportTickets.issue,
|
||||||
},
|
state: SupportTickets.state,
|
||||||
orderBy: { id: "desc" },
|
timestamp: SupportTickets.timestamp,
|
||||||
take: 20,
|
senderId: SupportTickets.senderId,
|
||||||
select: {
|
reportedId: SupportTickets.reportedId,
|
||||||
id: true,
|
modId: SupportTickets.modId,
|
||||||
issue: true,
|
|
||||||
state: true,
|
|
||||||
timestamp: true,
|
|
||||||
senderId: true,
|
|
||||||
reportedId: true,
|
|
||||||
modId: true,
|
|
||||||
},
|
|
||||||
})
|
})
|
||||||
|
.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(() => []),
|
.catch(() => []),
|
||||||
prisma.usersSettings
|
|
||||||
.findUnique({
|
|
||||||
where: { userId },
|
|
||||||
select: { muteEndTimestamp: true },
|
|
||||||
})
|
|
||||||
.catch(() => null),
|
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
const settings = settingsRows[0] ?? null;
|
||||||
const muteUntil = settings?.muteEndTimestamp ?? 0;
|
const muteUntil = settings?.muteEndTimestamp ?? 0;
|
||||||
if (muteUntil > now) {
|
if (muteUntil > now) {
|
||||||
events.push({
|
events.push({
|
||||||
@@ -134,11 +148,10 @@ export async function loadUserSanctions(
|
|||||||
];
|
];
|
||||||
const staffUsers =
|
const staffUsers =
|
||||||
staffIds.length > 0
|
staffIds.length > 0
|
||||||
? await prisma.user
|
? await db
|
||||||
.findMany({
|
.select({ id: User.id, username: User.username })
|
||||||
where: { id: { in: staffIds } },
|
.from(User)
|
||||||
select: { id: true, username: true },
|
.where(inArray(User.id, staffIds))
|
||||||
})
|
|
||||||
.catch(() => [])
|
.catch(() => [])
|
||||||
: [];
|
: [];
|
||||||
const staffName = new Map(staffUsers.map((u) => [u.id, u.username]));
|
const staffName = new Map(staffUsers.map((u) => [u.id, u.username]));
|
||||||
@@ -172,7 +185,7 @@ export async function loadUserSanctions(
|
|||||||
kind,
|
kind,
|
||||||
label: a.action,
|
label: a.action,
|
||||||
detail: a.details?.slice(0, 200) || "—",
|
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 { 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) {
|
export async function loadUserById(id: string) {
|
||||||
const numericId = Number(id);
|
const numericId = Number(id);
|
||||||
@@ -7,9 +20,11 @@ export async function loadUserById(id: string) {
|
|||||||
Number.isSafeInteger(numericId) &&
|
Number.isSafeInteger(numericId) &&
|
||||||
numericId > 0 &&
|
numericId > 0 &&
|
||||||
String(numericId) === id;
|
String(numericId) === id;
|
||||||
const user = await prisma.user.findUnique({
|
const [user] = await db
|
||||||
where: isNumeric ? { id: numericId } : { username: id },
|
.select()
|
||||||
});
|
.from(User)
|
||||||
|
.where(isNumeric ? eq(User.id, numericId) : eq(User.username, id))
|
||||||
|
.limit(1);
|
||||||
if (!user) notFound();
|
if (!user) notFound();
|
||||||
|
|
||||||
const [
|
const [
|
||||||
@@ -17,67 +32,76 @@ export async function loadUserById(id: string) {
|
|||||||
badges,
|
badges,
|
||||||
bansReceived,
|
bansReceived,
|
||||||
permissionRows,
|
permissionRows,
|
||||||
roomCount,
|
roomCountRows,
|
||||||
tradeCount,
|
tradeCountRows,
|
||||||
userRooms,
|
userRooms,
|
||||||
chatLogs,
|
chatLogs,
|
||||||
commandLogs,
|
commandLogs,
|
||||||
] = await Promise.all([
|
] = await Promise.all([
|
||||||
prisma.usersCurrency.findMany({ where: { userId: user.id } }),
|
db.select().from(UsersCurrency).where(eq(UsersCurrency.userId, user.id)),
|
||||||
prisma.usersBadges.findMany({
|
db
|
||||||
where: { userId: user.id },
|
.select()
|
||||||
orderBy: { slotId: "asc" },
|
.from(UsersBadges)
|
||||||
}),
|
.where(eq(UsersBadges.userId, user.id))
|
||||||
prisma.$queryRaw<
|
.orderBy(asc(UsersBadges.slotId)),
|
||||||
Array<{
|
rawRows<{
|
||||||
id: number;
|
id: number;
|
||||||
banReason: string;
|
banReason: string;
|
||||||
type: string;
|
type: string;
|
||||||
timestamp: number;
|
timestamp: number;
|
||||||
banExpire: number;
|
banExpire: number;
|
||||||
staffName: string;
|
staffName: string;
|
||||||
}>
|
}>(sql`
|
||||||
>`
|
|
||||||
SELECT b.id, b.ban_reason AS banReason, b.type, b.timestamp, b.ban_expire AS banExpire,
|
SELECT b.id, b.ban_reason AS banReason, b.type, b.timestamp, b.ban_expire AS banExpire,
|
||||||
COALESCE(s.username, 'Unknown') AS staffName
|
COALESCE(s.username, 'Unknown') AS staffName
|
||||||
FROM bans b LEFT JOIN users s ON s.id = b.user_staff_id
|
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`,
|
WHERE b.user_id = ${user.id} ORDER BY b.id DESC LIMIT 5`),
|
||||||
prisma.$queryRaw<Array<{ rankName: string }>>`
|
rawRows<{ rankName: string }>(sql`
|
||||||
SELECT COALESCE(rank_name, CONCAT('Rank ', id)) AS rankName FROM permissions WHERE id = ${user.rank} LIMIT 1`,
|
SELECT COALESCE(rank_name, CONCAT('Rank ', id)) AS rankName FROM permissions WHERE id = ${user.rank} LIMIT 1`),
|
||||||
prisma.rooms.count({ where: { ownerId: user.id } }),
|
db.select({ total: count() }).from(Rooms).where(eq(Rooms.ownerId, user.id)),
|
||||||
prisma.roomTradeLog.count({
|
db
|
||||||
where: { OR: [{ userOneId: user.id }, { userTwoId: user.id }] },
|
.select({ total: count() })
|
||||||
}),
|
.from(RoomTradeLog)
|
||||||
prisma.rooms.findMany({
|
.where(
|
||||||
where: { ownerId: user.id },
|
or(
|
||||||
orderBy: { users: "desc" },
|
eq(RoomTradeLog.userOneId, user.id),
|
||||||
take: 20,
|
eq(RoomTradeLog.userTwoId, user.id),
|
||||||
select: {
|
),
|
||||||
id: true,
|
),
|
||||||
name: true,
|
db
|
||||||
state: true,
|
.select({
|
||||||
users: true,
|
id: Rooms.id,
|
||||||
usersMax: true,
|
name: Rooms.name,
|
||||||
},
|
state: Rooms.state,
|
||||||
}),
|
users: Rooms.users,
|
||||||
prisma.$queryRaw<
|
usersMax: Rooms.usersMax,
|
||||||
Array<{ id: number; roomId: number; timestamp: number; message: string }>
|
})
|
||||||
>`
|
.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
|
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`,
|
FROM chatlogs_room WHERE user_from_id = ${user.id} ORDER BY timestamp DESC LIMIT 20`),
|
||||||
prisma.$queryRaw<
|
rawRows<{
|
||||||
Array<{
|
id: number;
|
||||||
id: number;
|
timestamp: number;
|
||||||
timestamp: number;
|
command: string;
|
||||||
command: string;
|
params: string;
|
||||||
params: string;
|
succes: string;
|
||||||
succes: string;
|
}>(sql`
|
||||||
}>
|
|
||||||
>`
|
|
||||||
SELECT ROW_NUMBER() OVER (ORDER BY timestamp DESC) AS id, timestamp, command, params, succes
|
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 =
|
const diamonds =
|
||||||
currencies.find((currency) => currency.type === 5)?.amount ?? 0;
|
currencies.find((currency) => currency.type === 5)?.amount ?? 0;
|
||||||
const duckets =
|
const duckets =
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
|
import { sql } from "drizzle-orm";
|
||||||
import { redirect } from "next/navigation";
|
import { redirect } from "next/navigation";
|
||||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
|
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
|
||||||
|
import { db } from "@/lib/db";
|
||||||
import { canAccess, getAdminContext, PERMS } from "@/lib/permissions";
|
import { canAccess, getAdminContext, PERMS } from "@/lib/permissions";
|
||||||
import { Prisma, prisma } from "@/lib/prisma";
|
|
||||||
import { getWatchedUserIds } from "@/lib/services/watch";
|
import { getWatchedUserIds } from "@/lib/services/watch";
|
||||||
import { UserAccountInfo } from "../../_components/user-account-info";
|
import { UserAccountInfo } from "../../_components/user-account-info";
|
||||||
import { UserHero } from "../../_components/user-hero";
|
import { UserHero } from "../../_components/user-hero";
|
||||||
@@ -28,10 +29,10 @@ export default async function UserEditPage({
|
|||||||
const watched = await getWatchedUserIds(session.user.id);
|
const watched = await getWatchedUserIds(session.user.id);
|
||||||
|
|
||||||
// Fetch rank list from permissions table for the rank dropdown
|
// Fetch rank list from permissions table for the rank dropdown
|
||||||
const rawRanks: { id: number; rank_name: string }[] = await prisma.$queryRaw(
|
const [rawRanks] = (await db.execute(
|
||||||
Prisma.sql`SELECT id, rank_name FROM permissions ORDER BY id ASC`,
|
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) => ({
|
const ranks = (rawRanks ?? []).map((r) => ({
|
||||||
id: Number(r.id),
|
id: Number(r.id),
|
||||||
name: String(r.rank_name || `Rank ${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 { redirect } from "next/navigation";
|
||||||
|
import { db, WebsiteShopVouchers } from "@/lib/db";
|
||||||
import { canAccess, getAdminContext, PERMS } from "@/lib/permissions";
|
import { canAccess, getAdminContext, PERMS } from "@/lib/permissions";
|
||||||
import { prisma } from "@/lib/prisma";
|
|
||||||
import { type VoucherRow, VouchersClient } from "./vouchers-client";
|
import { type VoucherRow, VouchersClient } from "./vouchers-client";
|
||||||
|
|
||||||
export default async function AdminVouchers() {
|
export default async function AdminVouchers() {
|
||||||
@@ -14,19 +15,19 @@ export default async function AdminVouchers() {
|
|||||||
|
|
||||||
let rows: VoucherRow[] = [];
|
let rows: VoucherRow[] = [];
|
||||||
try {
|
try {
|
||||||
const vouchers = await prisma.websiteShopVouchers.findMany({
|
const vouchers = await db
|
||||||
select: {
|
.select({
|
||||||
id: true,
|
id: WebsiteShopVouchers.id,
|
||||||
code: true,
|
code: WebsiteShopVouchers.code,
|
||||||
amount: true,
|
amount: WebsiteShopVouchers.amount,
|
||||||
maxUses: true,
|
maxUses: WebsiteShopVouchers.maxUses,
|
||||||
useCount: true,
|
useCount: WebsiteShopVouchers.useCount,
|
||||||
expiresAt: true,
|
expiresAt: WebsiteShopVouchers.expiresAt,
|
||||||
createdAt: true,
|
createdAt: WebsiteShopVouchers.createdAt,
|
||||||
},
|
})
|
||||||
orderBy: { id: "desc" },
|
.from(WebsiteShopVouchers)
|
||||||
take: 200,
|
.orderBy(desc(WebsiteShopVouchers.id))
|
||||||
});
|
.limit(200);
|
||||||
rows = vouchers.map((v) => {
|
rows = vouchers.map((v) => {
|
||||||
const expired = v.expiresAt ? v.expiresAt.getTime() <= now : false;
|
const expired = v.expiresAt ? v.expiresAt.getTime() <= now : false;
|
||||||
const usedUp = v.maxUses > 0 && v.useCount >= v.maxUses;
|
const usedUp = v.maxUses > 0 && v.useCount >= v.maxUses;
|
||||||
|
|||||||
@@ -1,9 +1,10 @@
|
|||||||
|
import { asc } from "drizzle-orm";
|
||||||
import { Filter } from "lucide-react";
|
import { Filter } from "lucide-react";
|
||||||
import { redirect } from "next/navigation";
|
import { redirect } from "next/navigation";
|
||||||
import { getTranslations } from "next-intl/server";
|
import { getTranslations } from "next-intl/server";
|
||||||
import { AdminPageShell } from "@/components/admin/admin-page-shell";
|
import { AdminPageShell } from "@/components/admin/admin-page-shell";
|
||||||
|
import { db, WebsiteWordfilter } from "@/lib/db";
|
||||||
import { canAccess, getAdminContext, PERMS } from "@/lib/permissions";
|
import { canAccess, getAdminContext, PERMS } from "@/lib/permissions";
|
||||||
import { prisma } from "@/lib/prisma";
|
|
||||||
import { WordFilterClient } from "./wordfilter-client";
|
import { WordFilterClient } from "./wordfilter-client";
|
||||||
|
|
||||||
export default async function AdminWordFilter() {
|
export default async function AdminWordFilter() {
|
||||||
@@ -21,11 +22,11 @@ export default async function AdminWordFilter() {
|
|||||||
|
|
||||||
let words: { id: string; word: string }[] = [];
|
let words: { id: string; word: string }[] = [];
|
||||||
try {
|
try {
|
||||||
const rows = await prisma.websiteWordfilter.findMany({
|
const rows = await db
|
||||||
select: { id: true, word: true },
|
.select({ id: WebsiteWordfilter.id, word: WebsiteWordfilter.word })
|
||||||
orderBy: { word: "asc" },
|
.from(WebsiteWordfilter)
|
||||||
take: 500,
|
.orderBy(asc(WebsiteWordfilter.word))
|
||||||
});
|
.limit(500);
|
||||||
words = rows.map((w) => ({ id: String(w.id), word: w.word }));
|
words = rows.map((w) => ({ id: String(w.id), word: w.word }));
|
||||||
} catch {
|
} catch {
|
||||||
words = [];
|
words = [];
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import { asc } from "drizzle-orm";
|
||||||
import { redirect } from "next/navigation";
|
import { redirect } from "next/navigation";
|
||||||
import { getTranslations } from "next-intl/server";
|
import { getTranslations } from "next-intl/server";
|
||||||
import {
|
import {
|
||||||
@@ -8,8 +9,8 @@ import {
|
|||||||
} from "@/actions/admin-writeable-boxes";
|
} from "@/actions/admin-writeable-boxes";
|
||||||
import { StatusCard } from "@/components/admin/dashboard";
|
import { StatusCard } from "@/components/admin/dashboard";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
|
import { db, WebsiteWriteableBoxes } from "@/lib/db";
|
||||||
import { canAccess, getAdminContext, PERMS } from "@/lib/permissions";
|
import { canAccess, getAdminContext, PERMS } from "@/lib/permissions";
|
||||||
import { prisma } from "@/lib/prisma";
|
|
||||||
|
|
||||||
export const metadata = { title: "Writeable boxes" };
|
export const metadata = { title: "Writeable boxes" };
|
||||||
|
|
||||||
@@ -20,11 +21,11 @@ export default async function AdminWriteableBoxes() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const t = await getTranslations("pages.admin.writeableBoxes");
|
const t = await getTranslations("pages.admin.writeableBoxes");
|
||||||
const boxes = await prisma.websiteWriteableBoxes
|
const boxes = await db
|
||||||
.findMany({
|
.select()
|
||||||
orderBy: [{ position: "asc" }, { id: "asc" }],
|
.from(WebsiteWriteableBoxes)
|
||||||
take: 500,
|
.orderBy(asc(WebsiteWriteableBoxes.position), asc(WebsiteWriteableBoxes.id))
|
||||||
})
|
.limit(500)
|
||||||
.catch(() => []);
|
.catch(() => []);
|
||||||
|
|
||||||
const activeCount = boxes.filter((b) => b.isActive).length;
|
const activeCount = boxes.filter((b) => b.isActive).length;
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
|
import { sql } from "drizzle-orm";
|
||||||
import { withAdmin } from "@/lib/api-handler";
|
import { withAdmin } from "@/lib/api-handler";
|
||||||
import { apiError } from "@/lib/api-response";
|
import { apiError } from "@/lib/api-response";
|
||||||
|
import { db } from "@/lib/db";
|
||||||
import { PERMS } from "@/lib/permission-slugs";
|
import { PERMS } from "@/lib/permission-slugs";
|
||||||
import { prisma } from "@/lib/prisma";
|
|
||||||
|
|
||||||
const VALID_REPORTS = new Set(["registrations", "online-by-hour", "economy"]);
|
const VALID_REPORTS = new Set(["registrations", "online-by-hour", "economy"]);
|
||||||
|
|
||||||
@@ -19,13 +20,13 @@ function csvRow(values: unknown[]): string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function streamRegistrations(): Promise<ReadableStream<Uint8Array>> {
|
async function streamRegistrations(): Promise<ReadableStream<Uint8Array>> {
|
||||||
const rows = await prisma.$queryRaw<{ day: string; cnt: bigint }[]>`
|
const [rows] = (await db.execute(sql`
|
||||||
SELECT FROM_UNIXTIME(account_created, '%Y-%m-%d') AS day, COUNT(*) AS cnt
|
SELECT FROM_UNIXTIME(account_created, '%Y-%m-%d') AS day, COUNT(*) AS cnt
|
||||||
FROM users
|
FROM users
|
||||||
WHERE account_created > UNIX_TIMESTAMP(NOW() - INTERVAL 90 DAY)
|
WHERE account_created > UNIX_TIMESTAMP(NOW() - INTERVAL 90 DAY)
|
||||||
GROUP BY day
|
GROUP BY day
|
||||||
ORDER BY day ASC
|
ORDER BY day ASC
|
||||||
`;
|
`)) as unknown as [{ day: string; cnt: bigint }[], unknown];
|
||||||
const encoder = new TextEncoder();
|
const encoder = new TextEncoder();
|
||||||
return new ReadableStream({
|
return new ReadableStream({
|
||||||
start(controller) {
|
start(controller) {
|
||||||
@@ -39,13 +40,13 @@ async function streamRegistrations(): Promise<ReadableStream<Uint8Array>> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function streamOnlineByHour(): Promise<ReadableStream<Uint8Array>> {
|
async function streamOnlineByHour(): Promise<ReadableStream<Uint8Array>> {
|
||||||
const rows = await prisma.$queryRaw<{ hour: number; cnt: bigint }[]>`
|
const [rows] = (await db.execute(sql`
|
||||||
SELECT HOUR(FROM_UNIXTIME(last_online)) AS hour, COUNT(*) AS cnt
|
SELECT HOUR(FROM_UNIXTIME(last_online)) AS hour, COUNT(*) AS cnt
|
||||||
FROM users
|
FROM users
|
||||||
WHERE last_online > UNIX_TIMESTAMP(NOW() - INTERVAL 30 DAY)
|
WHERE last_online > UNIX_TIMESTAMP(NOW() - INTERVAL 30 DAY)
|
||||||
GROUP BY hour
|
GROUP BY hour
|
||||||
ORDER BY hour ASC
|
ORDER BY hour ASC
|
||||||
`;
|
`)) as unknown as [{ hour: number; cnt: bigint }[], unknown];
|
||||||
const encoder = new TextEncoder();
|
const encoder = new TextEncoder();
|
||||||
return new ReadableStream({
|
return new ReadableStream({
|
||||||
start(controller) {
|
start(controller) {
|
||||||
@@ -61,11 +62,13 @@ async function streamOnlineByHour(): Promise<ReadableStream<Uint8Array>> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function streamEconomy(): Promise<ReadableStream<Uint8Array>> {
|
async function streamEconomy(): Promise<ReadableStream<Uint8Array>> {
|
||||||
const [totals] = await prisma.$queryRaw<
|
const [rows] = (await db.execute(sql`
|
||||||
{ total_credits: bigint; total_pixels: bigint }[]
|
|
||||||
>`
|
|
||||||
SELECT SUM(credits) AS total_credits, SUM(pixels) AS total_pixels FROM users
|
SELECT SUM(credits) AS total_credits, SUM(pixels) AS total_pixels FROM users
|
||||||
`;
|
`)) as unknown as [
|
||||||
|
{ total_credits: bigint; total_pixels: bigint }[],
|
||||||
|
unknown,
|
||||||
|
];
|
||||||
|
const totals = rows[0];
|
||||||
const encoder = new TextEncoder();
|
const encoder = new TextEncoder();
|
||||||
return new ReadableStream({
|
return new ReadableStream({
|
||||||
start(controller) {
|
start(controller) {
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
|
import { asc, count, eq, inArray, like, or } from "drizzle-orm";
|
||||||
import { withAdmin } from "@/lib/api-handler";
|
import { withAdmin } from "@/lib/api-handler";
|
||||||
import { apiError, apiOk } from "@/lib/api-response";
|
import { apiError, apiOk } from "@/lib/api-response";
|
||||||
|
import { CatalogItemsBc, CatalogPages, CatalogPagesBc, db } from "@/lib/db";
|
||||||
import { PERMS } from "@/lib/permissions";
|
import { PERMS } from "@/lib/permissions";
|
||||||
import { prisma } from "@/lib/prisma";
|
|
||||||
import {
|
import {
|
||||||
deletePage,
|
deletePage,
|
||||||
getAncestors,
|
getAncestors,
|
||||||
@@ -26,10 +27,16 @@ type PageRow = {
|
|||||||
|
|
||||||
async function getBcTreeFlat(): Promise<TreeNode[]> {
|
async function getBcTreeFlat(): Promise<TreeNode[]> {
|
||||||
const [allPages, itemCounts] = await Promise.all([
|
const [allPages, itemCounts] = await Promise.all([
|
||||||
prisma.catalogPagesBc.findMany({ orderBy: { orderNum: "asc" } }),
|
db.select().from(CatalogPagesBc).orderBy(asc(CatalogPagesBc.orderNum)),
|
||||||
prisma.catalogItemsBc.groupBy({ by: ["pageId"], _count: true }),
|
db
|
||||||
|
.select({
|
||||||
|
pageId: CatalogItemsBc.pageId,
|
||||||
|
total: count(),
|
||||||
|
})
|
||||||
|
.from(CatalogItemsBc)
|
||||||
|
.groupBy(CatalogItemsBc.pageId),
|
||||||
]);
|
]);
|
||||||
const itemCountMap = new Map(itemCounts.map((c) => [c.pageId, c._count]));
|
const itemCountMap = new Map(itemCounts.map((c) => [c.pageId, c.total]));
|
||||||
const childCountMap = new Map<number, number>();
|
const childCountMap = new Map<number, number>();
|
||||||
for (const p of allPages) {
|
for (const p of allPages) {
|
||||||
childCountMap.set(p.parentId, (childCountMap.get(p.parentId) ?? 0) + 1);
|
childCountMap.set(p.parentId, (childCountMap.get(p.parentId) ?? 0) + 1);
|
||||||
@@ -91,37 +98,49 @@ async function getChildren(
|
|||||||
): Promise<TreeNode[]> {
|
): Promise<TreeNode[]> {
|
||||||
// Roots in Habbo DBs are usually parent_id = -1, sometimes 0.
|
// Roots in Habbo DBs are usually parent_id = -1, sometimes 0.
|
||||||
const parentFilter =
|
const parentFilter =
|
||||||
parentId <= 0 ? { OR: [{ parentId: -1 }, { parentId: 0 }] } : { parentId };
|
parentId <= 0
|
||||||
|
? or(eq(CatalogPages.parentId, -1), eq(CatalogPages.parentId, 0))
|
||||||
|
: eq(CatalogPages.parentId, parentId);
|
||||||
|
const parentFilterBc =
|
||||||
|
parentId <= 0
|
||||||
|
? or(eq(CatalogPagesBc.parentId, -1), eq(CatalogPagesBc.parentId, 0))
|
||||||
|
: eq(CatalogPagesBc.parentId, parentId);
|
||||||
|
|
||||||
if (isBc) {
|
if (isBc) {
|
||||||
const pages = await prisma.catalogPagesBc.findMany({
|
const pages = await db
|
||||||
where: parentFilter,
|
.select()
|
||||||
orderBy: { orderNum: "asc" },
|
.from(CatalogPagesBc)
|
||||||
});
|
.where(parentFilterBc)
|
||||||
|
.orderBy(asc(CatalogPagesBc.orderNum));
|
||||||
const ids = pages.map((p) => toInt(p.id));
|
const ids = pages.map((p) => toInt(p.id));
|
||||||
const [childCounts, itemCounts] = await Promise.all([
|
const [childCounts, itemCounts] = await Promise.all([
|
||||||
ids.length
|
ids.length
|
||||||
? prisma.catalogPagesBc.groupBy({
|
? db
|
||||||
by: ["parentId"],
|
.select({
|
||||||
where: { parentId: { in: ids } },
|
parentId: CatalogPagesBc.parentId,
|
||||||
_count: true,
|
total: count(),
|
||||||
})
|
})
|
||||||
|
.from(CatalogPagesBc)
|
||||||
|
.where(inArray(CatalogPagesBc.parentId, ids))
|
||||||
|
.groupBy(CatalogPagesBc.parentId)
|
||||||
: Promise.resolve([]),
|
: Promise.resolve([]),
|
||||||
ids.length
|
ids.length
|
||||||
? prisma.catalogItemsBc
|
? db
|
||||||
.groupBy({
|
.select({
|
||||||
by: ["pageId"],
|
pageId: CatalogItemsBc.pageId,
|
||||||
where: { pageId: { in: ids } },
|
total: count(),
|
||||||
_count: true,
|
|
||||||
})
|
})
|
||||||
|
.from(CatalogItemsBc)
|
||||||
|
.where(inArray(CatalogItemsBc.pageId, ids))
|
||||||
|
.groupBy(CatalogItemsBc.pageId)
|
||||||
.catch(() => [])
|
.catch(() => [])
|
||||||
: Promise.resolve([]),
|
: Promise.resolve([]),
|
||||||
]);
|
]);
|
||||||
const childMap = Object.fromEntries(
|
const childMap = Object.fromEntries(
|
||||||
childCounts.map((c) => [toInt(c.parentId), c._count]),
|
childCounts.map((c) => [toInt(c.parentId), c.total]),
|
||||||
);
|
);
|
||||||
const itemMap = Object.fromEntries(
|
const itemMap = Object.fromEntries(
|
||||||
itemCounts.map((c) => [toInt(c.pageId), c._count]),
|
itemCounts.map((c) => [toInt(c.pageId), c.total]),
|
||||||
);
|
);
|
||||||
return pages.map((p) => {
|
return pages.map((p) => {
|
||||||
const id = toInt(p.id);
|
const id = toInt(p.id);
|
||||||
@@ -143,23 +162,27 @@ async function getChildren(
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
const pages = await prisma.catalogPages.findMany({
|
const pages = await db
|
||||||
where: parentFilter,
|
.select()
|
||||||
orderBy: { orderNum: "asc" },
|
.from(CatalogPages)
|
||||||
});
|
.where(parentFilter)
|
||||||
|
.orderBy(asc(CatalogPages.orderNum));
|
||||||
const ids = pages.map((p) => toInt(p.id));
|
const ids = pages.map((p) => toInt(p.id));
|
||||||
const [childCounts, itemMap] = await Promise.all([
|
const [childCounts, itemMap] = await Promise.all([
|
||||||
ids.length
|
ids.length
|
||||||
? prisma.catalogPages.groupBy({
|
? db
|
||||||
by: ["parentId"],
|
.select({
|
||||||
where: { parentId: { in: ids } },
|
parentId: CatalogPages.parentId,
|
||||||
_count: true,
|
total: count(),
|
||||||
})
|
})
|
||||||
|
.from(CatalogPages)
|
||||||
|
.where(inArray(CatalogPages.parentId, ids))
|
||||||
|
.groupBy(CatalogPages.parentId)
|
||||||
: Promise.resolve([]),
|
: Promise.resolve([]),
|
||||||
getCatalogItemCounts(ids),
|
getCatalogItemCounts(ids),
|
||||||
]);
|
]);
|
||||||
const childMap = Object.fromEntries(
|
const childMap = Object.fromEntries(
|
||||||
childCounts.map((c) => [toInt(c.parentId), c._count]),
|
childCounts.map((c) => [toInt(c.parentId), c.total]),
|
||||||
);
|
);
|
||||||
return pages.map((p) => {
|
return pages.map((p) => {
|
||||||
const id = toInt(p.id);
|
const id = toInt(p.id);
|
||||||
@@ -184,19 +207,17 @@ async function getChildren(
|
|||||||
async function searchPages(q: string, isBc: boolean): Promise<TreeNode[]> {
|
async function searchPages(q: string, isBc: boolean): Promise<TreeNode[]> {
|
||||||
const needle = q.trim();
|
const needle = q.trim();
|
||||||
const idExact = Number.parseInt(needle, 10);
|
const idExact = Number.parseInt(needle, 10);
|
||||||
const idFilter =
|
const idOk = Number.isFinite(idExact) && String(idExact) === needle;
|
||||||
Number.isFinite(idExact) && String(idExact) === needle
|
|
||||||
? [{ id: idExact }]
|
|
||||||
: [];
|
|
||||||
|
|
||||||
if (isBc) {
|
if (isBc) {
|
||||||
const pages = await prisma.catalogPagesBc.findMany({
|
const conditions = [like(CatalogPagesBc.caption, `%${needle}%`)];
|
||||||
where: {
|
if (idOk) conditions.push(eq(CatalogPagesBc.id, idExact));
|
||||||
OR: [{ caption: { contains: needle } }, ...idFilter],
|
const pages = await db
|
||||||
},
|
.select()
|
||||||
orderBy: { orderNum: "asc" },
|
.from(CatalogPagesBc)
|
||||||
take: 50,
|
.where(or(...conditions))
|
||||||
});
|
.orderBy(asc(CatalogPagesBc.orderNum))
|
||||||
|
.limit(50);
|
||||||
return pages.map((p) =>
|
return pages.map((p) =>
|
||||||
toTreeNode(
|
toTreeNode(
|
||||||
{
|
{
|
||||||
@@ -216,26 +237,30 @@ async function searchPages(q: string, isBc: boolean): Promise<TreeNode[]> {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const pages = await prisma.catalogPages.findMany({
|
const conditions = [like(CatalogPages.caption, `%${needle}%`)];
|
||||||
where: {
|
if (idOk) conditions.push(eq(CatalogPages.id, idExact));
|
||||||
OR: [{ caption: { contains: needle } }, ...idFilter],
|
const pages = await db
|
||||||
},
|
.select()
|
||||||
orderBy: { orderNum: "asc" },
|
.from(CatalogPages)
|
||||||
take: 50,
|
.where(or(...conditions))
|
||||||
});
|
.orderBy(asc(CatalogPages.orderNum))
|
||||||
|
.limit(50);
|
||||||
const ids = pages.map((p) => toInt(p.id));
|
const ids = pages.map((p) => toInt(p.id));
|
||||||
const [childCounts, itemMap] = await Promise.all([
|
const [childCounts, itemMap] = await Promise.all([
|
||||||
ids.length
|
ids.length
|
||||||
? prisma.catalogPages.groupBy({
|
? db
|
||||||
by: ["parentId"],
|
.select({
|
||||||
where: { parentId: { in: ids } },
|
parentId: CatalogPages.parentId,
|
||||||
_count: true,
|
total: count(),
|
||||||
})
|
})
|
||||||
|
.from(CatalogPages)
|
||||||
|
.where(inArray(CatalogPages.parentId, ids))
|
||||||
|
.groupBy(CatalogPages.parentId)
|
||||||
: Promise.resolve([]),
|
: Promise.resolve([]),
|
||||||
getCatalogItemCounts(ids),
|
getCatalogItemCounts(ids),
|
||||||
]);
|
]);
|
||||||
const childMap = Object.fromEntries(
|
const childMap = Object.fromEntries(
|
||||||
childCounts.map((c) => [toInt(c.parentId), c._count]),
|
childCounts.map((c) => [toInt(c.parentId), c.total]),
|
||||||
);
|
);
|
||||||
return pages.map((p) => {
|
return pages.map((p) => {
|
||||||
const id = toInt(p.id);
|
const id = toInt(p.id);
|
||||||
@@ -273,11 +298,19 @@ export const GET = withAdmin(
|
|||||||
const id = Number(pageIdRaw);
|
const id = Number(pageIdRaw);
|
||||||
if (!Number.isFinite(id) || id <= 0) return apiError("Invalid pageId");
|
if (!Number.isFinite(id) || id <= 0) return apiError("Invalid pageId");
|
||||||
if (isBc) {
|
if (isBc) {
|
||||||
const page = await prisma.catalogPagesBc.findUnique({ where: { id } });
|
const [page] = await db
|
||||||
|
.select()
|
||||||
|
.from(CatalogPagesBc)
|
||||||
|
.where(eq(CatalogPagesBc.id, id))
|
||||||
|
.limit(1);
|
||||||
if (!page) return apiError("Page not found", 404);
|
if (!page) return apiError("Page not found", 404);
|
||||||
return apiOk({ page });
|
return apiOk({ page });
|
||||||
}
|
}
|
||||||
const page = await prisma.catalogPages.findUnique({ where: { id } });
|
const [page] = await db
|
||||||
|
.select()
|
||||||
|
.from(CatalogPages)
|
||||||
|
.where(eq(CatalogPages.id, id))
|
||||||
|
.limit(1);
|
||||||
if (!page) return apiError("Page not found", 404);
|
if (!page) return apiError("Page not found", 404);
|
||||||
return apiOk({ page });
|
return apiOk({ page });
|
||||||
}
|
}
|
||||||
@@ -290,10 +323,15 @@ export const GET = withAdmin(
|
|||||||
const chain: Array<{ id: number; caption: string }> = [];
|
const chain: Array<{ id: number; caption: string }> = [];
|
||||||
let currentId = id;
|
let currentId = id;
|
||||||
for (let i = 0; i < 20; i++) {
|
for (let i = 0; i < 20; i++) {
|
||||||
const page = await prisma.catalogPagesBc.findUnique({
|
const [page] = await db
|
||||||
where: { id: currentId },
|
.select({
|
||||||
select: { id: true, caption: true, parentId: true },
|
id: CatalogPagesBc.id,
|
||||||
});
|
caption: CatalogPagesBc.caption,
|
||||||
|
parentId: CatalogPagesBc.parentId,
|
||||||
|
})
|
||||||
|
.from(CatalogPagesBc)
|
||||||
|
.where(eq(CatalogPagesBc.id, currentId))
|
||||||
|
.limit(1);
|
||||||
if (!page) break;
|
if (!page) break;
|
||||||
chain.unshift({ id: page.id, caption: page.caption });
|
chain.unshift({ id: page.id, caption: page.caption });
|
||||||
if (page.parentId <= 0) break;
|
if (page.parentId <= 0) break;
|
||||||
@@ -340,29 +378,37 @@ export const PATCH = withAdmin(
|
|||||||
}
|
}
|
||||||
const isBc = body.catalog === "bc";
|
const isBc = body.catalog === "bc";
|
||||||
if (isBc) {
|
if (isBc) {
|
||||||
const page = await prisma.catalogPagesBc.findUnique({
|
const [page] = await db
|
||||||
where: { id: pageId },
|
.select({
|
||||||
select: { enabled: true, visible: true },
|
enabled: CatalogPagesBc.enabled,
|
||||||
});
|
visible: CatalogPagesBc.visible,
|
||||||
|
})
|
||||||
|
.from(CatalogPagesBc)
|
||||||
|
.where(eq(CatalogPagesBc.id, pageId))
|
||||||
|
.limit(1);
|
||||||
if (!page) return apiError("Page not found", 404);
|
if (!page) return apiError("Page not found", 404);
|
||||||
const current = field === "enabled" ? page.enabled : page.visible;
|
const current = field === "enabled" ? page.enabled : page.visible;
|
||||||
await prisma.catalogPagesBc.update({
|
await db
|
||||||
where: { id: pageId },
|
.update(CatalogPagesBc)
|
||||||
data: { [field]: current === "1" ? "0" : "1" },
|
.set({ [field]: current === "1" ? "0" : "1" })
|
||||||
});
|
.where(eq(CatalogPagesBc.id, pageId));
|
||||||
await rcon.updateCatalog();
|
await rcon.updateCatalog();
|
||||||
return apiOk({});
|
return apiOk({});
|
||||||
}
|
}
|
||||||
const page = await prisma.catalogPages.findUnique({
|
const [page] = await db
|
||||||
where: { id: pageId },
|
.select({
|
||||||
select: { enabled: true, visible: true },
|
enabled: CatalogPages.enabled,
|
||||||
});
|
visible: CatalogPages.visible,
|
||||||
|
})
|
||||||
|
.from(CatalogPages)
|
||||||
|
.where(eq(CatalogPages.id, pageId))
|
||||||
|
.limit(1);
|
||||||
if (!page) return apiError("Page not found", 404);
|
if (!page) return apiError("Page not found", 404);
|
||||||
const current = field === "enabled" ? page.enabled : page.visible;
|
const current = field === "enabled" ? page.enabled : page.visible;
|
||||||
await prisma.catalogPages.update({
|
await db
|
||||||
where: { id: pageId },
|
.update(CatalogPages)
|
||||||
data: { [field]: current === "1" ? "0" : "1" },
|
.set({ [field]: current === "1" ? "0" : "1" })
|
||||||
});
|
.where(eq(CatalogPages.id, pageId));
|
||||||
await rcon.updateCatalog();
|
await rcon.updateCatalog();
|
||||||
return apiOk({});
|
return apiOk({});
|
||||||
},
|
},
|
||||||
@@ -379,43 +425,46 @@ export const DELETE = withAdmin(
|
|||||||
return apiError("Invalid pageId");
|
return apiError("Invalid pageId");
|
||||||
}
|
}
|
||||||
if (isBc) {
|
if (isBc) {
|
||||||
const page = await prisma.catalogPagesBc.findUnique({
|
const [page] = await db
|
||||||
where: { id: pageId },
|
.select({ parentId: CatalogPagesBc.parentId })
|
||||||
select: { parentId: true },
|
.from(CatalogPagesBc)
|
||||||
});
|
.where(eq(CatalogPagesBc.id, pageId))
|
||||||
|
.limit(1);
|
||||||
if (!page) return apiError("Page not found", 404);
|
if (!page) return apiError("Page not found", 404);
|
||||||
if (mode === "reparent") {
|
if (mode === "reparent") {
|
||||||
await prisma.$transaction([
|
await db.transaction(async (tx) => {
|
||||||
prisma.catalogPagesBc.updateMany({
|
await tx
|
||||||
where: { parentId: pageId },
|
.update(CatalogPagesBc)
|
||||||
data: { parentId: page.parentId },
|
.set({ parentId: page.parentId })
|
||||||
}),
|
.where(eq(CatalogPagesBc.parentId, pageId));
|
||||||
prisma.catalogItemsBc.deleteMany({ where: { pageId } }),
|
await tx
|
||||||
prisma.catalogPagesBc.delete({ where: { id: pageId } }),
|
.delete(CatalogItemsBc)
|
||||||
]);
|
.where(eq(CatalogItemsBc.pageId, pageId));
|
||||||
|
await tx.delete(CatalogPagesBc).where(eq(CatalogPagesBc.id, pageId));
|
||||||
|
});
|
||||||
} else {
|
} else {
|
||||||
// Deep cascade: collect all descendants
|
// Deep cascade: collect all descendants
|
||||||
const toDelete: number[] = [pageId];
|
const toDelete: number[] = [pageId];
|
||||||
const queue: number[] = [pageId];
|
const queue: number[] = [pageId];
|
||||||
while (queue.length > 0) {
|
while (queue.length > 0) {
|
||||||
const children = await prisma.catalogPagesBc.findMany({
|
const children = await db
|
||||||
where: { parentId: { in: queue } },
|
.select({ id: CatalogPagesBc.id })
|
||||||
select: { id: true },
|
.from(CatalogPagesBc)
|
||||||
});
|
.where(inArray(CatalogPagesBc.parentId, queue));
|
||||||
queue.length = 0;
|
queue.length = 0;
|
||||||
for (const child of children) {
|
for (const child of children) {
|
||||||
toDelete.push(child.id);
|
toDelete.push(child.id);
|
||||||
queue.push(child.id);
|
queue.push(child.id);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
await prisma.$transaction([
|
await db.transaction(async (tx) => {
|
||||||
prisma.catalogItemsBc.deleteMany({
|
await tx
|
||||||
where: { pageId: { in: toDelete } },
|
.delete(CatalogItemsBc)
|
||||||
}),
|
.where(inArray(CatalogItemsBc.pageId, toDelete));
|
||||||
prisma.catalogPagesBc.deleteMany({
|
await tx
|
||||||
where: { id: { in: toDelete } },
|
.delete(CatalogPagesBc)
|
||||||
}),
|
.where(inArray(CatalogPagesBc.id, toDelete));
|
||||||
]);
|
});
|
||||||
}
|
}
|
||||||
await rcon.updateCatalog();
|
await rcon.updateCatalog();
|
||||||
return apiOk({});
|
return apiOk({});
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
|
import { asc, eq, like, or } from "drizzle-orm";
|
||||||
import { withAdmin } from "@/lib/api-handler";
|
import { withAdmin } from "@/lib/api-handler";
|
||||||
import { apiOk } from "@/lib/api-response";
|
import { apiOk } from "@/lib/api-response";
|
||||||
|
import { db, ItemsBase } from "@/lib/db";
|
||||||
import { PERMS } from "@/lib/permissions";
|
import { PERMS } from "@/lib/permissions";
|
||||||
import { prisma } from "@/lib/prisma";
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Search items_base for Quick Add / catalog tooling.
|
* Search items_base for Quick Add / catalog tooling.
|
||||||
@@ -27,26 +28,26 @@ export const GET = withAdmin(
|
|||||||
}
|
}
|
||||||
|
|
||||||
const idExact = Number.parseInt(q, 10);
|
const idExact = Number.parseInt(q, 10);
|
||||||
const rows = await prisma.itemsBase.findMany({
|
const conditions = [
|
||||||
where: {
|
like(ItemsBase.itemName, `%${q}%`),
|
||||||
OR: [
|
like(ItemsBase.publicName, `%${q}%`),
|
||||||
{ itemName: { contains: q } },
|
];
|
||||||
{ publicName: { contains: q } },
|
if (Number.isFinite(idExact) && String(idExact) === q) {
|
||||||
...(Number.isFinite(idExact) && String(idExact) === q
|
conditions.push(eq(ItemsBase.id, idExact));
|
||||||
? [{ id: idExact }]
|
}
|
||||||
: []),
|
|
||||||
],
|
const rows = await db
|
||||||
},
|
.select({
|
||||||
select: {
|
id: ItemsBase.id,
|
||||||
id: true,
|
itemName: ItemsBase.itemName,
|
||||||
itemName: true,
|
publicName: ItemsBase.publicName,
|
||||||
publicName: true,
|
type: ItemsBase.type,
|
||||||
type: true,
|
spriteId: ItemsBase.spriteId,
|
||||||
spriteId: true,
|
})
|
||||||
},
|
.from(ItemsBase)
|
||||||
take: limit,
|
.where(or(...conditions))
|
||||||
orderBy: { id: "asc" },
|
.orderBy(asc(ItemsBase.id))
|
||||||
});
|
.limit(limit);
|
||||||
|
|
||||||
return apiOk({
|
return apiOk({
|
||||||
results: rows.map((r) => ({
|
results: rows.map((r) => ({
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
|
import { inArray } from "drizzle-orm";
|
||||||
import { withAdmin } from "@/lib/api-handler";
|
import { withAdmin } from "@/lib/api-handler";
|
||||||
|
import { db, ItemsBase } from "@/lib/db";
|
||||||
import { PERMS } from "@/lib/permissions";
|
import { PERMS } from "@/lib/permissions";
|
||||||
import { prisma } from "@/lib/prisma";
|
|
||||||
import { logAudit } from "@/lib/services/audit";
|
import { logAudit } from "@/lib/services/audit";
|
||||||
import {
|
import {
|
||||||
cloneSingleFurni,
|
cloneSingleFurni,
|
||||||
@@ -51,11 +52,10 @@ export const POST = withAdmin(
|
|||||||
const classnames = entries.map((e) => e.classname);
|
const classnames = entries.map((e) => e.classname);
|
||||||
if (classnames.length === 0) continue;
|
if (classnames.length === 0) continue;
|
||||||
|
|
||||||
const placeholders = classnames.map(() => "?").join(",");
|
const rows = await db
|
||||||
const rows = await prisma.$queryRawUnsafe<Array<{ item_name: string }>>(
|
.select({ item_name: ItemsBase.itemName })
|
||||||
`SELECT item_name FROM items_base WHERE item_name IN (${placeholders})`,
|
.from(ItemsBase)
|
||||||
...classnames,
|
.where(inArray(ItemsBase.itemName, classnames));
|
||||||
);
|
|
||||||
const have = new Set(rows.map((r) => r.item_name));
|
const have = new Set(rows.map((r) => r.item_name));
|
||||||
|
|
||||||
for (const entry of entries) {
|
for (const entry of entries) {
|
||||||
|
|||||||
@@ -1,9 +1,10 @@
|
|||||||
import { existsSync, promises as fs } from "node:fs";
|
import { existsSync, promises as fs } from "node:fs";
|
||||||
import path from "node:path";
|
import path from "node:path";
|
||||||
|
import { eq } from "drizzle-orm";
|
||||||
import { withAdmin } from "@/lib/api-handler";
|
import { withAdmin } from "@/lib/api-handler";
|
||||||
import { apiError } from "@/lib/api-response";
|
import { apiError } from "@/lib/api-response";
|
||||||
|
import { db, ItemsBase } from "@/lib/db";
|
||||||
import { PERMS } from "@/lib/permissions";
|
import { PERMS } from "@/lib/permissions";
|
||||||
import { prisma } from "@/lib/prisma";
|
|
||||||
import { getFurniAssetDirs } from "@/lib/services/furni-asset-dirs";
|
import { getFurniAssetDirs } from "@/lib/services/furni-asset-dirs";
|
||||||
import {
|
import {
|
||||||
appendFurniEntriesBatch,
|
appendFurniEntriesBatch,
|
||||||
@@ -120,15 +121,16 @@ export const POST = withAdmin(
|
|||||||
// Try spriteId as fallback revision
|
// Try spriteId as fallback revision
|
||||||
const revsToTry: number[] = [];
|
const revsToTry: number[] = [];
|
||||||
if (rev > 0) revsToTry.push(rev);
|
if (rev > 0) revsToTry.push(rev);
|
||||||
const dbItem = await prisma.itemsBase.findFirst({
|
const [dbItem] = await db
|
||||||
where: { itemName: item.classname },
|
.select({
|
||||||
select: {
|
id: ItemsBase.id,
|
||||||
id: true,
|
spriteId: ItemsBase.spriteId,
|
||||||
spriteId: true,
|
publicName: ItemsBase.publicName,
|
||||||
publicName: true,
|
type: ItemsBase.type,
|
||||||
type: true,
|
})
|
||||||
},
|
.from(ItemsBase)
|
||||||
});
|
.where(eq(ItemsBase.itemName, item.classname))
|
||||||
|
.limit(1);
|
||||||
if (dbItem?.spriteId && !revsToTry.includes(dbItem.spriteId)) {
|
if (dbItem?.spriteId && !revsToTry.includes(dbItem.spriteId)) {
|
||||||
revsToTry.push(dbItem.spriteId);
|
revsToTry.push(dbItem.spriteId);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,10 +1,11 @@
|
|||||||
import { existsSync, promises as fs } from "node:fs";
|
import { existsSync, promises as fs } from "node:fs";
|
||||||
import path from "node:path";
|
import path from "node:path";
|
||||||
|
import { eq, sql } from "drizzle-orm";
|
||||||
import type { NextRequest } from "next/server";
|
import type { NextRequest } from "next/server";
|
||||||
import { withAdmin } from "@/lib/api-handler";
|
import { withAdmin } from "@/lib/api-handler";
|
||||||
import { apiError, apiOk } from "@/lib/api-response";
|
import { apiError, apiOk } from "@/lib/api-response";
|
||||||
|
import { db, ItemsBase } from "@/lib/db";
|
||||||
import { PERMS } from "@/lib/permissions";
|
import { PERMS } from "@/lib/permissions";
|
||||||
import { prisma } from "@/lib/prisma";
|
|
||||||
import { logAudit } from "@/lib/services/audit";
|
import { logAudit } from "@/lib/services/audit";
|
||||||
import { getFurniAssetDirs } from "@/lib/services/furni-asset-dirs";
|
import { getFurniAssetDirs } from "@/lib/services/furni-asset-dirs";
|
||||||
import {
|
import {
|
||||||
@@ -47,16 +48,17 @@ export const GET = withAdmin(
|
|||||||
const { json } = parseNitroBundle(buffer);
|
const { json } = parseNitroBundle(buffer);
|
||||||
|
|
||||||
// Fetch interaction flags from DB
|
// Fetch interaction flags from DB
|
||||||
const items = await prisma.$queryRaw<
|
const [items] = (await db.execute(sql`
|
||||||
|
SELECT allow_sit, allow_lay, allow_walk
|
||||||
|
FROM items_base WHERE item_name = ${rawClassname} LIMIT 1
|
||||||
|
`)) as unknown as [
|
||||||
Array<{
|
Array<{
|
||||||
allow_sit: string;
|
allow_sit: string;
|
||||||
allow_lay: string;
|
allow_lay: string;
|
||||||
allow_walk: string;
|
allow_walk: string;
|
||||||
}>
|
}>,
|
||||||
>`
|
unknown,
|
||||||
SELECT allow_sit, allow_lay, allow_walk
|
];
|
||||||
FROM items_base WHERE item_name = ${rawClassname} LIMIT 1
|
|
||||||
`;
|
|
||||||
|
|
||||||
const dbRow = items[0];
|
const dbRow = items[0];
|
||||||
const flags = {
|
const flags = {
|
||||||
@@ -153,14 +155,14 @@ export const PUT = withAdmin(
|
|||||||
|
|
||||||
// 5. Update items_base (best-effort)
|
// 5. Update items_base (best-effort)
|
||||||
try {
|
try {
|
||||||
await prisma.$executeRaw`
|
await db.execute(sql`
|
||||||
UPDATE items_base
|
UPDATE items_base
|
||||||
SET width = ${dims.x}, length = ${dims.y}, stack_height = ${dims.z},
|
SET width = ${dims.x}, length = ${dims.y}, stack_height = ${dims.z},
|
||||||
allow_sit = ${cansiton ? "1" : "0"},
|
allow_sit = ${cansiton ? "1" : "0"},
|
||||||
allow_lay = ${canlayon ? "1" : "0"},
|
allow_lay = ${canlayon ? "1" : "0"},
|
||||||
allow_walk = ${canstandon ? "1" : "0"}
|
allow_walk = ${canstandon ? "1" : "0"}
|
||||||
WHERE item_name = ${rawClassname}
|
WHERE item_name = ${rawClassname}
|
||||||
`;
|
`);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.warn(
|
console.warn(
|
||||||
"[nitro-editor] items_base update failed:",
|
"[nitro-editor] items_base update failed:",
|
||||||
@@ -180,10 +182,11 @@ export const PUT = withAdmin(
|
|||||||
|
|
||||||
// 7. Audit log
|
// 7. Audit log
|
||||||
try {
|
try {
|
||||||
const dbItem = await prisma.itemsBase.findFirst({
|
const [dbItem] = await db
|
||||||
where: { itemName: rawClassname },
|
.select({ id: ItemsBase.id })
|
||||||
select: { id: true },
|
.from(ItemsBase)
|
||||||
});
|
.where(eq(ItemsBase.itemName, rawClassname))
|
||||||
|
.limit(1);
|
||||||
await logAudit({
|
await logAudit({
|
||||||
userId: ctx.session.user.id,
|
userId: ctx.session.user.id,
|
||||||
action: "furni_edit_nitro",
|
action: "furni_edit_nitro",
|
||||||
|
|||||||
@@ -1,8 +1,9 @@
|
|||||||
import { existsSync } from "node:fs";
|
import { existsSync } from "node:fs";
|
||||||
|
import { asc, inArray, sql } from "drizzle-orm";
|
||||||
import { withAdmin } from "@/lib/api-handler";
|
import { withAdmin } from "@/lib/api-handler";
|
||||||
import { apiOk } from "@/lib/api-response";
|
import { apiOk } from "@/lib/api-response";
|
||||||
|
import { db, ItemsBase } from "@/lib/db";
|
||||||
import { PERMS } from "@/lib/permissions";
|
import { PERMS } from "@/lib/permissions";
|
||||||
import { prisma } from "@/lib/prisma";
|
|
||||||
import {
|
import {
|
||||||
appendFurniEntriesBatch,
|
appendFurniEntriesBatch,
|
||||||
buildFurniEntry,
|
buildFurniEntry,
|
||||||
@@ -64,19 +65,19 @@ export const POST = withAdmin(
|
|||||||
|
|
||||||
for (let i = 0; i < targetIds.length; i += CHUNK) {
|
for (let i = 0; i < targetIds.length; i += CHUNK) {
|
||||||
const slice = targetIds.slice(i, i + CHUNK);
|
const slice = targetIds.slice(i, i + CHUNK);
|
||||||
const rows = await prisma.itemsBase.findMany({
|
const rows = await db
|
||||||
where: { id: { in: slice } },
|
.select({
|
||||||
select: {
|
id: ItemsBase.id,
|
||||||
id: true,
|
spriteId: ItemsBase.spriteId,
|
||||||
spriteId: true,
|
itemName: ItemsBase.itemName,
|
||||||
itemName: true,
|
publicName: ItemsBase.publicName,
|
||||||
publicName: true,
|
type: ItemsBase.type,
|
||||||
type: true,
|
width: ItemsBase.width,
|
||||||
width: true,
|
length: ItemsBase.length,
|
||||||
length: true,
|
stackHeight: ItemsBase.stackHeight,
|
||||||
stackHeight: true,
|
})
|
||||||
},
|
.from(ItemsBase)
|
||||||
});
|
.where(inArray(ItemsBase.id, slice));
|
||||||
|
|
||||||
const entries: Array<{
|
const entries: Array<{
|
||||||
entry: Record<string, unknown>;
|
entry: Record<string, unknown>;
|
||||||
@@ -158,20 +159,20 @@ async function resolveTargetIds(
|
|||||||
days: number,
|
days: number,
|
||||||
): Promise<number[]> {
|
): Promise<number[]> {
|
||||||
if (mode === "all") {
|
if (mode === "all") {
|
||||||
const rows = await prisma.itemsBase.findMany({
|
const rows = await db
|
||||||
select: { id: true },
|
.select({ id: ItemsBase.id })
|
||||||
orderBy: { id: "asc" },
|
.from(ItemsBase)
|
||||||
});
|
.orderBy(asc(ItemsBase.id));
|
||||||
return rows.map((r) => r.id);
|
return rows.map((r) => r.id);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (mode === "missing" || mode === "broken") {
|
if (mode === "missing" || mode === "broken") {
|
||||||
const furniDataPath = await getFurnitureDataPath();
|
const furniDataPath = await getFurnitureDataPath();
|
||||||
if (!existsSync(furniDataPath)) {
|
if (!existsSync(furniDataPath)) {
|
||||||
const rows = await prisma.itemsBase.findMany({
|
const rows = await db
|
||||||
select: { id: true },
|
.select({ id: ItemsBase.id })
|
||||||
orderBy: { id: "asc" },
|
.from(ItemsBase)
|
||||||
});
|
.orderBy(asc(ItemsBase.id));
|
||||||
return rows.map((r) => r.id);
|
return rows.map((r) => r.id);
|
||||||
}
|
}
|
||||||
const furniData = (await readFurniData()) as {
|
const furniData = (await readFurniData()) as {
|
||||||
@@ -184,16 +185,16 @@ async function resolveTargetIds(
|
|||||||
if (typeof e?.classname === "string") byClassname.set(e.classname, e);
|
if (typeof e?.classname === "string") byClassname.set(e.classname, e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
const rows = await prisma.itemsBase.findMany({
|
const rows = await db
|
||||||
select: {
|
.select({
|
||||||
id: true,
|
id: ItemsBase.id,
|
||||||
itemName: true,
|
itemName: ItemsBase.itemName,
|
||||||
spriteId: true,
|
spriteId: ItemsBase.spriteId,
|
||||||
width: true,
|
width: ItemsBase.width,
|
||||||
length: true,
|
length: ItemsBase.length,
|
||||||
},
|
})
|
||||||
orderBy: { id: "asc" },
|
.from(ItemsBase)
|
||||||
});
|
.orderBy(asc(ItemsBase.id));
|
||||||
|
|
||||||
if (mode === "missing") {
|
if (mode === "missing") {
|
||||||
return rows.filter((r) => !byClassname.has(r.itemName)).map((r) => r.id);
|
return rows.filter((r) => !byClassname.has(r.itemName)).map((r) => r.id);
|
||||||
@@ -220,7 +221,7 @@ async function resolveTargetIds(
|
|||||||
Date.now() - days * 24 * 60 * 60 * 1000,
|
Date.now() - days * 24 * 60 * 60 * 1000,
|
||||||
).toISOString();
|
).toISOString();
|
||||||
const sinceMysql = sinceIso.slice(0, 19).replace("T", " ");
|
const sinceMysql = sinceIso.slice(0, 19).replace("T", " ");
|
||||||
const auditRows = await prisma.$queryRaw<Array<{ target_id: number }>>`
|
const [auditRows] = (await db.execute(sql`
|
||||||
SELECT DISTINCT target_id
|
SELECT DISTINCT target_id
|
||||||
FROM admin_audit_log
|
FROM admin_audit_log
|
||||||
WHERE action = 'furni_import'
|
WHERE action = 'furni_import'
|
||||||
@@ -228,6 +229,6 @@ async function resolveTargetIds(
|
|||||||
AND target_id IS NOT NULL
|
AND target_id IS NOT NULL
|
||||||
AND created_at >= ${sinceMysql}
|
AND created_at >= ${sinceMysql}
|
||||||
ORDER BY target_id ASC
|
ORDER BY target_id ASC
|
||||||
`;
|
`)) as unknown as [Array<{ target_id: number }>, unknown];
|
||||||
return auditRows.map((r) => r.target_id).filter(Boolean);
|
return auditRows.map((r) => r.target_id).filter(Boolean);
|
||||||
}
|
}
|
||||||
@@ -1,9 +1,10 @@
|
|||||||
import { existsSync, promises as fs } from "node:fs";
|
import { existsSync, promises as fs } from "node:fs";
|
||||||
import path from "node:path";
|
import path from "node:path";
|
||||||
|
import { and, eq, inArray, sql } from "drizzle-orm";
|
||||||
import { withAdmin } from "@/lib/api-handler";
|
import { withAdmin } from "@/lib/api-handler";
|
||||||
import { apiError, apiOk } from "@/lib/api-response";
|
import { apiError, apiOk } from "@/lib/api-response";
|
||||||
|
import { CatalogPages, db, ItemsBase } from "@/lib/db";
|
||||||
import { PERMS } from "@/lib/permissions";
|
import { PERMS } from "@/lib/permissions";
|
||||||
import { prisma } from "@/lib/prisma";
|
|
||||||
import { logAudit } from "@/lib/services/audit";
|
import { logAudit } from "@/lib/services/audit";
|
||||||
import { getFurniAssetDirs } from "@/lib/services/furni-asset-dirs";
|
import { getFurniAssetDirs } from "@/lib/services/furni-asset-dirs";
|
||||||
import {
|
import {
|
||||||
@@ -97,29 +98,32 @@ export const GET = withAdmin(
|
|||||||
if (action === "stats") {
|
if (action === "stats") {
|
||||||
const [totalResult, catalogResult, nitroMissingResult] =
|
const [totalResult, catalogResult, nitroMissingResult] =
|
||||||
await Promise.all([
|
await Promise.all([
|
||||||
prisma.$queryRaw<
|
db.execute(sql`SELECT COUNT(*) as cnt FROM items_base`),
|
||||||
[{ cnt: bigint }]
|
db.execute(
|
||||||
>`SELECT COUNT(*) as cnt FROM items_base`,
|
sql`SELECT COUNT(DISTINCT item_ids) as cnt FROM catalog_items`,
|
||||||
prisma.$queryRaw<
|
),
|
||||||
[{ cnt: bigint }]
|
db.execute(
|
||||||
>`SELECT COUNT(DISTINCT item_ids) as cnt FROM catalog_items`,
|
sql`SELECT COUNT(*) as cnt FROM items_base WHERE item_name NOT IN (SELECT catalog_name FROM catalog_items)`,
|
||||||
prisma.$queryRaw<
|
),
|
||||||
[{ cnt: bigint }]
|
|
||||||
>`SELECT COUNT(*) as cnt FROM items_base WHERE item_name NOT IN (SELECT catalog_name FROM catalog_items)`,
|
|
||||||
]);
|
]);
|
||||||
|
|
||||||
const totalInDb = Number(totalResult[0].cnt);
|
const totalInDb = Number(
|
||||||
const inCatalog = Number(catalogResult[0].cnt);
|
(totalResult as unknown as [[{ cnt: bigint }], unknown])[0][0].cnt,
|
||||||
const notInCatalog = Number(nitroMissingResult[0].cnt);
|
);
|
||||||
|
const inCatalog = Number(
|
||||||
|
(catalogResult as unknown as [[{ cnt: bigint }], unknown])[0][0].cnt,
|
||||||
|
);
|
||||||
|
const notInCatalog = Number(
|
||||||
|
(nitroMissingResult as unknown as [[{ cnt: bigint }], unknown])[0][0]
|
||||||
|
.cnt,
|
||||||
|
);
|
||||||
|
|
||||||
// Count missing .nitro files by checking filesystem
|
// Count missing .nitro files by checking filesystem
|
||||||
const importedItems = await prisma.$queryRaw<
|
const [importedItems] = (await db.execute(sql`
|
||||||
Array<{ item_name: string }>
|
|
||||||
>`
|
|
||||||
SELECT ib.item_name FROM items_base ib
|
SELECT ib.item_name FROM items_base ib
|
||||||
INNER JOIN catalog_items ci ON ci.catalog_name = ib.item_name
|
INNER JOIN catalog_items ci ON ci.catalog_name = ib.item_name
|
||||||
LIMIT 10000
|
LIMIT 10000
|
||||||
`;
|
`)) as unknown as [Array<{ item_name: string }>, unknown];
|
||||||
|
|
||||||
let missingNitro = 0;
|
let missingNitro = 0;
|
||||||
for (const item of importedItems) {
|
for (const item of importedItems) {
|
||||||
@@ -146,17 +150,18 @@ export const GET = withAdmin(
|
|||||||
);
|
);
|
||||||
const perPage = 50;
|
const perPage = 50;
|
||||||
|
|
||||||
const importedItems = await prisma.$queryRaw<
|
const [importedItems] = (await db.execute(sql`
|
||||||
|
SELECT ib.item_name, ib.public_name, ib.sprite_id, ib.type FROM items_base ib
|
||||||
|
INNER JOIN catalog_items ci ON ci.catalog_name = ib.item_name
|
||||||
|
`)) as unknown as [
|
||||||
Array<{
|
Array<{
|
||||||
item_name: string;
|
item_name: string;
|
||||||
public_name: string;
|
public_name: string;
|
||||||
sprite_id: number;
|
sprite_id: number;
|
||||||
type: string;
|
type: string;
|
||||||
}>
|
}>,
|
||||||
>`
|
unknown,
|
||||||
SELECT ib.item_name, ib.public_name, ib.sprite_id, ib.type FROM items_base ib
|
];
|
||||||
INNER JOIN catalog_items ci ON ci.catalog_name = ib.item_name
|
|
||||||
`;
|
|
||||||
|
|
||||||
// Filter to only those missing .nitro on filesystem
|
// Filter to only those missing .nitro on filesystem
|
||||||
const missing = importedItems.filter((item) => {
|
const missing = importedItems.filter((item) => {
|
||||||
@@ -209,9 +214,9 @@ export const GET = withAdmin(
|
|||||||
// Pull every imported classname once so we can both (a) sort non-imported
|
// Pull every imported classname once so we can both (a) sort non-imported
|
||||||
// items to the top *before* pagination and (b) skip the per-page lookup
|
// items to the top *before* pagination and (b) skip the per-page lookup
|
||||||
// below. Single-column indexed query — fast even at 10k+ rows.
|
// below. Single-column indexed query — fast even at 10k+ rows.
|
||||||
const allImported = await prisma.itemsBase.findMany({
|
const allImported = await db
|
||||||
select: { itemName: true },
|
.select({ itemName: ItemsBase.itemName })
|
||||||
});
|
.from(ItemsBase);
|
||||||
const importedSet = new Set(
|
const importedSet = new Set(
|
||||||
allImported.map((e) => e.itemName).filter((n): n is string => !!n),
|
allImported.map((e) => e.itemName).filter((n): n is string => !!n),
|
||||||
);
|
);
|
||||||
@@ -442,10 +447,11 @@ export const PATCH = withAdmin(
|
|||||||
// Download SWF if not present — try multiple revision candidates
|
// Download SWF if not present — try multiple revision candidates
|
||||||
if (!existsSync(swfPath)) {
|
if (!existsSync(swfPath)) {
|
||||||
// Build list of revisions to try: known rev, spriteId, common fallbacks
|
// Build list of revisions to try: known rev, spriteId, common fallbacks
|
||||||
const spriteId = await prisma.itemsBase.findFirst({
|
const [spriteId] = await db
|
||||||
where: { itemName: item.classname },
|
.select({ spriteId: ItemsBase.spriteId })
|
||||||
select: { spriteId: true },
|
.from(ItemsBase)
|
||||||
});
|
.where(eq(ItemsBase.itemName, item.classname))
|
||||||
|
.limit(1);
|
||||||
const revsToTry = [
|
const revsToTry = [
|
||||||
...new Set([rev, spriteId?.spriteId || 0].filter((r) => r > 0)),
|
...new Set([rev, spriteId?.spriteId || 0].filter((r) => r > 0)),
|
||||||
];
|
];
|
||||||
@@ -500,15 +506,16 @@ export const PATCH = withAdmin(
|
|||||||
);
|
);
|
||||||
|
|
||||||
if (!alreadyInFD) {
|
if (!alreadyInFD) {
|
||||||
const dbItem = await prisma.itemsBase.findFirst({
|
const [dbItem] = await db
|
||||||
where: { itemName: item.classname },
|
.select({
|
||||||
select: {
|
id: ItemsBase.id,
|
||||||
id: true,
|
spriteId: ItemsBase.spriteId,
|
||||||
spriteId: true,
|
publicName: ItemsBase.publicName,
|
||||||
publicName: true,
|
type: ItemsBase.type,
|
||||||
type: true,
|
})
|
||||||
},
|
.from(ItemsBase)
|
||||||
});
|
.where(eq(ItemsBase.itemName, item.classname))
|
||||||
|
.limit(1);
|
||||||
const furnidata = await getHabboItFurnidata();
|
const furnidata = await getHabboItFurnidata();
|
||||||
const fd =
|
const fd =
|
||||||
furnidata.get(item.classname) || furnidata.get(baseClassname);
|
furnidata.get(item.classname) || furnidata.get(baseClassname);
|
||||||
@@ -560,10 +567,11 @@ export const PUT = withAdmin(
|
|||||||
const isDryRun = request.nextUrl.searchParams.get("dryrun") === "1";
|
const isDryRun = request.nextUrl.searchParams.get("dryrun") === "1";
|
||||||
|
|
||||||
// 1. Find the root "imported_furni" page
|
// 1. Find the root "imported_furni" page
|
||||||
const parentPage = await prisma.catalogPages.findFirst({
|
const [parentPage] = await db
|
||||||
where: { captionSave: IMPORTED_PAGE_CAPTION_SAVE },
|
.select({ id: CatalogPages.id })
|
||||||
select: { id: true },
|
.from(CatalogPages)
|
||||||
});
|
.where(eq(CatalogPages.captionSave, IMPORTED_PAGE_CAPTION_SAVE))
|
||||||
|
.limit(1);
|
||||||
if (!parentPage) {
|
if (!parentPage) {
|
||||||
return apiOk({
|
return apiOk({
|
||||||
dryRun: isDryRun,
|
dryRun: isDryRun,
|
||||||
@@ -574,10 +582,13 @@ export const PUT = withAdmin(
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 2. Collect ALL imported page IDs (parent + every imp_* sub-page)
|
// 2. Collect ALL imported page IDs (parent + every imp_* sub-page)
|
||||||
const subPages = await prisma.catalogPages.findMany({
|
const subPages = await db
|
||||||
where: { parentId: parentPage.id },
|
.select({
|
||||||
select: { id: true, captionSave: true },
|
id: CatalogPages.id,
|
||||||
});
|
captionSave: CatalogPages.captionSave,
|
||||||
|
})
|
||||||
|
.from(CatalogPages)
|
||||||
|
.where(eq(CatalogPages.parentId, parentPage.id));
|
||||||
const allImportedPageIds = [
|
const allImportedPageIds = [
|
||||||
String(parentPage.id),
|
String(parentPage.id),
|
||||||
...subPages.map((p) => String(p.id)),
|
...subPages.map((p) => String(p.id)),
|
||||||
@@ -585,19 +596,23 @@ export const PUT = withAdmin(
|
|||||||
|
|
||||||
// 3. Find ALL catalog_items across all imported pages
|
// 3. Find ALL catalog_items across all imported pages
|
||||||
// page_id is VARCHAR in DB so we use string placeholders
|
// page_id is VARCHAR in DB so we use string placeholders
|
||||||
const inPlaceholders = allImportedPageIds.map(() => "?").join(",");
|
const [allItems] = (await db.execute(sql`
|
||||||
const allItems = await prisma.$queryRawUnsafe<
|
SELECT id, item_ids, catalog_name, page_id, cost_credits
|
||||||
|
FROM catalog_items
|
||||||
|
WHERE CAST(page_id AS CHAR) IN (${sql.join(
|
||||||
|
allImportedPageIds.map((id) => sql`${id}`),
|
||||||
|
sql`, `,
|
||||||
|
)})
|
||||||
|
`)) as unknown as [
|
||||||
Array<{
|
Array<{
|
||||||
id: number;
|
id: number;
|
||||||
item_ids: string;
|
item_ids: string;
|
||||||
catalog_name: string;
|
catalog_name: string;
|
||||||
page_id: string;
|
page_id: string;
|
||||||
cost_credits: number;
|
cost_credits: number;
|
||||||
}>
|
}>,
|
||||||
>(
|
unknown,
|
||||||
`SELECT id, item_ids, catalog_name, page_id, cost_credits FROM catalog_items WHERE page_id IN (${inPlaceholders})`,
|
];
|
||||||
...allImportedPageIds,
|
|
||||||
);
|
|
||||||
|
|
||||||
if (allItems.length === 0) {
|
if (allItems.length === 0) {
|
||||||
return apiOk({
|
return apiOk({
|
||||||
@@ -614,10 +629,14 @@ export const PUT = withAdmin(
|
|||||||
].filter((n) => !Number.isNaN(n));
|
].filter((n) => !Number.isNaN(n));
|
||||||
const dbItems =
|
const dbItems =
|
||||||
uniqueItemIds.length > 0
|
uniqueItemIds.length > 0
|
||||||
? await prisma.itemsBase.findMany({
|
? await db
|
||||||
where: { id: { in: uniqueItemIds } },
|
.select({
|
||||||
select: { id: true, itemName: true, type: true },
|
id: ItemsBase.id,
|
||||||
})
|
itemName: ItemsBase.itemName,
|
||||||
|
type: ItemsBase.type,
|
||||||
|
})
|
||||||
|
.from(ItemsBase)
|
||||||
|
.where(inArray(ItemsBase.id, uniqueItemIds))
|
||||||
: [];
|
: [];
|
||||||
const typeMap = new Map<string, string>();
|
const typeMap = new Map<string, string>();
|
||||||
for (const di of dbItems) {
|
for (const di of dbItems) {
|
||||||
@@ -641,14 +660,20 @@ export const PUT = withAdmin(
|
|||||||
// Create the page (only happens once per new category)
|
// Create the page (only happens once per new category)
|
||||||
const catInfo = CATEGORY_PAGE[catKey] || CATEGORY_PAGE.other;
|
const catInfo = CATEGORY_PAGE[catKey] || CATEGORY_PAGE.other;
|
||||||
const parentId = parentPage?.id;
|
const parentId = parentPage?.id;
|
||||||
await prisma.$executeRaw`
|
await db.execute(sql`
|
||||||
INSERT INTO catalog_pages (caption_save, caption, page_layout, parent_id, min_rank, order_num, icon_image, enabled, visible, includes, page_headline, page_teaser, page_special, page_text1, page_text2, page_text_details, page_text_teaser)
|
INSERT INTO catalog_pages (caption_save, caption, page_layout, parent_id, min_rank, order_num, icon_image, enabled, visible, includes, page_headline, page_teaser, page_special, page_text1, page_text2, page_text_details, page_text_teaser)
|
||||||
VALUES (${captionSave}, ${catInfo.label}, 'default_3x3', ${parentId}, '1', ${catInfo.order}, ${catInfo.icon}, '1', '1', '', '', '', '', '', '', '', '')
|
VALUES (${captionSave}, ${catInfo.label}, 'default_3x3', ${parentId}, '1', ${catInfo.order}, ${catInfo.icon}, '1', '1', '', '', '', '', '', '', '', '')
|
||||||
`;
|
`);
|
||||||
const created = await prisma.catalogPages.findFirst({
|
const [created] = await db
|
||||||
where: { captionSave, parentId },
|
.select({ id: CatalogPages.id })
|
||||||
select: { id: true },
|
.from(CatalogPages)
|
||||||
});
|
.where(
|
||||||
|
and(
|
||||||
|
eq(CatalogPages.captionSave, captionSave),
|
||||||
|
eq(CatalogPages.parentId, parentId!),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.limit(1);
|
||||||
categoryPageMap.set(captionSave, created?.id);
|
categoryPageMap.set(captionSave, created?.id);
|
||||||
return String(created?.id);
|
return String(created?.id);
|
||||||
}
|
}
|
||||||
@@ -719,15 +744,17 @@ export const PUT = withAdmin(
|
|||||||
for (const [batchKey, ids] of updateBatches) {
|
for (const [batchKey, ids] of updateBatches) {
|
||||||
const [pageId, credits, points, pointsType] = batchKey.split("|");
|
const [pageId, credits, points, pointsType] = batchKey.split("|");
|
||||||
try {
|
try {
|
||||||
const idPlaceholders = ids.map(() => "?").join(",");
|
await db.execute(sql`
|
||||||
await prisma.$queryRawUnsafe(
|
UPDATE catalog_items
|
||||||
`UPDATE catalog_items SET page_id = ?, cost_credits = ?, cost_points = ?, points_type = ? WHERE id IN (${idPlaceholders})`,
|
SET page_id = ${pageId},
|
||||||
pageId,
|
cost_credits = ${Number(credits)},
|
||||||
Number(credits),
|
cost_points = ${Number(points)},
|
||||||
Number(points),
|
points_type = ${Number(pointsType)}
|
||||||
Number(pointsType),
|
WHERE id IN (${sql.join(
|
||||||
...ids,
|
ids.map((id) => sql`${id}`),
|
||||||
);
|
sql`, `,
|
||||||
|
)})
|
||||||
|
`);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
errors.push(`batch update: ${(err as Error).message}`);
|
errors.push(`batch update: ${(err as Error).message}`);
|
||||||
}
|
}
|
||||||
@@ -741,15 +768,14 @@ export const PUT = withAdmin(
|
|||||||
const subPageIdStr = String(subPage.id);
|
const subPageIdStr = String(subPage.id);
|
||||||
if (!pagesWithItems.has(subPageIdStr)) {
|
if (!pagesWithItems.has(subPageIdStr)) {
|
||||||
try {
|
try {
|
||||||
const remaining = await prisma.$queryRawUnsafe<
|
const [remaining] = (await db.execute(
|
||||||
Array<{ cnt: bigint }>
|
sql`SELECT COUNT(*) as cnt FROM catalog_items WHERE CAST(page_id AS CHAR) = ${subPageIdStr}`,
|
||||||
>(
|
)) as unknown as [Array<{ cnt: bigint }>, unknown];
|
||||||
`SELECT COUNT(*) as cnt FROM catalog_items WHERE page_id = ?`,
|
|
||||||
subPageIdStr,
|
|
||||||
);
|
|
||||||
const count = Number(remaining[0]?.cnt ?? 0);
|
const count = Number(remaining[0]?.cnt ?? 0);
|
||||||
if (count === 0) {
|
if (count === 0) {
|
||||||
await prisma.$executeRaw`DELETE FROM catalog_pages WHERE id = ${subPage.id}`;
|
await db.execute(
|
||||||
|
sql`DELETE FROM catalog_pages WHERE id = ${subPage.id}`,
|
||||||
|
);
|
||||||
pagesDeleted++;
|
pagesDeleted++;
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
|
|||||||
@@ -1,9 +1,10 @@
|
|||||||
|
import { and, eq } from "drizzle-orm";
|
||||||
import { revalidateTag } from "next/cache";
|
import { revalidateTag } from "next/cache";
|
||||||
import { NextResponse } from "next/server";
|
import { NextResponse } from "next/server";
|
||||||
import { withAdmin } from "@/lib/api-handler";
|
import { withAdmin } from "@/lib/api-handler";
|
||||||
import { apiError } from "@/lib/api-response";
|
import { apiError } from "@/lib/api-response";
|
||||||
|
import { AclModelRole, db } from "@/lib/db";
|
||||||
import { PERMS } from "@/lib/permissions";
|
import { PERMS } from "@/lib/permissions";
|
||||||
import { prisma } from "@/lib/prisma";
|
|
||||||
|
|
||||||
export const POST = withAdmin(
|
export const POST = withAdmin(
|
||||||
{ permission: PERMS.PERMISSIONS_MANAGE },
|
{ permission: PERMS.PERMISSIONS_MANAGE },
|
||||||
@@ -20,16 +21,32 @@ export const POST = withAdmin(
|
|||||||
return apiError("Valid userId and roleId required", 400);
|
return apiError("Valid userId and roleId required", 400);
|
||||||
}
|
}
|
||||||
if (body.action === "remove") {
|
if (body.action === "remove") {
|
||||||
await prisma.aclModelRole.deleteMany({
|
await db
|
||||||
where: { modelType: "User", modelId: userId, roleId },
|
.delete(AclModelRole)
|
||||||
});
|
.where(
|
||||||
|
and(
|
||||||
|
eq(AclModelRole.modelType, "User"),
|
||||||
|
eq(AclModelRole.modelId, userId),
|
||||||
|
eq(AclModelRole.roleId, roleId),
|
||||||
|
),
|
||||||
|
);
|
||||||
} else {
|
} else {
|
||||||
const existing = await prisma.aclModelRole.findFirst({
|
const [existing] = await db
|
||||||
where: { modelType: "User", modelId: userId, roleId },
|
.select({ modelId: AclModelRole.modelId })
|
||||||
});
|
.from(AclModelRole)
|
||||||
|
.where(
|
||||||
|
and(
|
||||||
|
eq(AclModelRole.modelType, "User"),
|
||||||
|
eq(AclModelRole.modelId, userId),
|
||||||
|
eq(AclModelRole.roleId, roleId),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.limit(1);
|
||||||
if (!existing)
|
if (!existing)
|
||||||
await prisma.aclModelRole.create({
|
await db.insert(AclModelRole).values({
|
||||||
data: { modelType: "User", modelId: userId, roleId },
|
modelType: "User",
|
||||||
|
modelId: userId,
|
||||||
|
roleId,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
revalidateTag("permissions", { expire: 0 });
|
revalidateTag("permissions", { expire: 0 });
|
||||||
|
|||||||
@@ -1,9 +1,11 @@
|
|||||||
|
import { and, eq } from "drizzle-orm";
|
||||||
|
import type { ResultSetHeader } from "mysql2";
|
||||||
import { revalidateTag } from "next/cache";
|
import { revalidateTag } from "next/cache";
|
||||||
import { NextResponse } from "next/server";
|
import { NextResponse } from "next/server";
|
||||||
import { withAdmin } from "@/lib/api-handler";
|
import { withAdmin } from "@/lib/api-handler";
|
||||||
import { apiError } from "@/lib/api-response";
|
import { apiError } from "@/lib/api-response";
|
||||||
|
import { AclModelPermission, AclModelRole, AclRole, db } from "@/lib/db";
|
||||||
import { PERMS } from "@/lib/permissions";
|
import { PERMS } from "@/lib/permissions";
|
||||||
import { prisma } from "@/lib/prisma";
|
|
||||||
|
|
||||||
export const POST = withAdmin(
|
export const POST = withAdmin(
|
||||||
{ permission: PERMS.PERMISSIONS_MANAGE },
|
{ permission: PERMS.PERMISSIONS_MANAGE },
|
||||||
@@ -15,7 +17,10 @@ export const POST = withAdmin(
|
|||||||
const title = String(body.title ?? "").trim();
|
const title = String(body.title ?? "").trim();
|
||||||
if (!/^[a-z0-9._-]{2,64}$/.test(slug) || !title)
|
if (!/^[a-z0-9._-]{2,64}$/.test(slug) || !title)
|
||||||
return apiError("Valid slug and title required", 400);
|
return apiError("Valid slug and title required", 400);
|
||||||
const role = await prisma.aclRole.create({ data: { slug, title } });
|
const [result] = (await db
|
||||||
|
.insert(AclRole)
|
||||||
|
.values({ slug, title })) as unknown as [ResultSetHeader];
|
||||||
|
const role = { id: Number(result.insertId), slug, title };
|
||||||
revalidateTag("permissions", { expire: 0 });
|
revalidateTag("permissions", { expire: 0 });
|
||||||
return NextResponse.json({ role });
|
return NextResponse.json({ role });
|
||||||
},
|
},
|
||||||
@@ -27,20 +32,26 @@ export const DELETE = withAdmin(
|
|||||||
const id = Number(request.nextUrl.searchParams.get("id"));
|
const id = Number(request.nextUrl.searchParams.get("id"));
|
||||||
if (!Number.isInteger(id) || id <= 0)
|
if (!Number.isInteger(id) || id <= 0)
|
||||||
return apiError("Valid ID required", 400);
|
return apiError("Valid ID required", 400);
|
||||||
const role = await prisma.aclRole.findUnique({
|
const [role] = await db
|
||||||
where: { id },
|
.select({ slug: AclRole.slug })
|
||||||
select: { slug: true },
|
.from(AclRole)
|
||||||
});
|
.where(eq(AclRole.id, id))
|
||||||
|
.limit(1);
|
||||||
if (!role) return apiError("Role not found", 404);
|
if (!role) return apiError("Role not found", 404);
|
||||||
if (role.slug.startsWith("rank_"))
|
if (role.slug.startsWith("rank_"))
|
||||||
return apiError("Rank roles must be managed through emulator ranks", 409);
|
return apiError("Rank roles must be managed through emulator ranks", 409);
|
||||||
await prisma.$transaction([
|
await db.transaction(async (tx) => {
|
||||||
prisma.aclModelPermission.deleteMany({
|
await tx
|
||||||
where: { modelType: "Role", modelId: id },
|
.delete(AclModelPermission)
|
||||||
}),
|
.where(
|
||||||
prisma.aclModelRole.deleteMany({ where: { roleId: id } }),
|
and(
|
||||||
prisma.aclRole.delete({ where: { id } }),
|
eq(AclModelPermission.modelType, "Role"),
|
||||||
]);
|
eq(AclModelPermission.modelId, id),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
await tx.delete(AclModelRole).where(eq(AclModelRole.roleId, id));
|
||||||
|
await tx.delete(AclRole).where(eq(AclRole.id, id));
|
||||||
|
});
|
||||||
revalidateTag("permissions", { expire: 0 });
|
revalidateTag("permissions", { expire: 0 });
|
||||||
return NextResponse.json({ success: true });
|
return NextResponse.json({ success: true });
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
|
import { sql } from "drizzle-orm";
|
||||||
import { withAdmin } from "@/lib/api-handler";
|
import { withAdmin } from "@/lib/api-handler";
|
||||||
import { apiError, apiOk } from "@/lib/api-response";
|
import { apiError, apiOk } from "@/lib/api-response";
|
||||||
|
import { db } from "@/lib/db";
|
||||||
import { PERMS } from "@/lib/permissions";
|
import { PERMS } from "@/lib/permissions";
|
||||||
import { prisma } from "@/lib/prisma";
|
|
||||||
|
|
||||||
interface BlacklistWord {
|
interface BlacklistWord {
|
||||||
id: number;
|
id: number;
|
||||||
@@ -9,9 +10,9 @@ interface BlacklistWord {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const GET = withAdmin({ permission: PERMS.PREFIXES_VIEW }, async () => {
|
export const GET = withAdmin({ permission: PERMS.PREFIXES_VIEW }, async () => {
|
||||||
const words = await prisma.$queryRaw<BlacklistWord[]>`
|
const [words] = (await db.execute(
|
||||||
SELECT id, word FROM custom_prefix_blacklist ORDER BY word ASC
|
sql`SELECT id, word FROM custom_prefix_blacklist ORDER BY word ASC`,
|
||||||
`;
|
)) as unknown as [BlacklistWord[], unknown];
|
||||||
|
|
||||||
return apiOk({
|
return apiOk({
|
||||||
words: words.map((w) => ({ ...w, created_at: "" })),
|
words: words.map((w) => ({ ...w, created_at: "" })),
|
||||||
@@ -26,7 +27,9 @@ export const POST = withAdmin(
|
|||||||
|
|
||||||
if (!word) return apiError("Word is required");
|
if (!word) return apiError("Word is required");
|
||||||
|
|
||||||
await prisma.$executeRaw`INSERT INTO custom_prefix_blacklist (word) VALUES (${word})`;
|
await db.execute(
|
||||||
|
sql`INSERT INTO custom_prefix_blacklist (word) VALUES (${word})`,
|
||||||
|
);
|
||||||
|
|
||||||
return apiOk();
|
return apiOk();
|
||||||
},
|
},
|
||||||
@@ -41,7 +44,7 @@ export const DELETE = withAdmin(
|
|||||||
if (!Number.isInteger(id) || id <= 0)
|
if (!Number.isInteger(id) || id <= 0)
|
||||||
return apiError("Missing or invalid word id");
|
return apiError("Missing or invalid word id");
|
||||||
|
|
||||||
await prisma.$executeRaw`DELETE FROM custom_prefix_blacklist WHERE id = ${id}`;
|
await db.execute(sql`DELETE FROM custom_prefix_blacklist WHERE id = ${id}`);
|
||||||
|
|
||||||
return apiOk({ deleted: id });
|
return apiOk({ deleted: id });
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
|
import { sql } from "drizzle-orm";
|
||||||
import { withAdmin } from "@/lib/api-handler";
|
import { withAdmin } from "@/lib/api-handler";
|
||||||
import { apiError, apiOk } from "@/lib/api-response";
|
import { apiError, apiOk } from "@/lib/api-response";
|
||||||
|
import { db } from "@/lib/db";
|
||||||
import { PERMS } from "@/lib/permissions";
|
import { PERMS } from "@/lib/permissions";
|
||||||
import { Prisma, prisma } from "@/lib/prisma";
|
|
||||||
|
|
||||||
interface UserPrefix {
|
interface UserPrefix {
|
||||||
id: number;
|
id: number;
|
||||||
@@ -24,23 +25,24 @@ export const GET = withAdmin(
|
|||||||
const offset = (page - 1) * limit;
|
const offset = (page - 1) * limit;
|
||||||
|
|
||||||
const whereFragment = q
|
const whereFragment = q
|
||||||
? Prisma.sql`WHERE up.text LIKE ${`%${q}%`} OR u.username LIKE ${`%${q}%`}`
|
? sql`WHERE up.text LIKE ${`%${q}%`} OR u.username LIKE ${`%${q}%`}`
|
||||||
: Prisma.empty;
|
: sql``;
|
||||||
|
|
||||||
const prefixes = await prisma.$queryRaw<UserPrefix[]>(
|
const [prefixes] =
|
||||||
Prisma.sql`SELECT up.id, up.user_id, up.text, up.color, up.icon, up.effect, up.active, u.username
|
(await db.execute(sql`SELECT up.id, up.user_id, up.text, up.color, up.icon, up.effect, up.active, u.username
|
||||||
FROM custom_prefixes up
|
FROM custom_prefixes up
|
||||||
LEFT JOIN users u ON u.id = up.user_id
|
LEFT JOIN users u ON u.id = up.user_id
|
||||||
${whereFragment}
|
${whereFragment}
|
||||||
ORDER BY up.id DESC
|
ORDER BY up.id DESC
|
||||||
LIMIT ${limit} OFFSET ${offset}`,
|
LIMIT ${limit} OFFSET ${offset}`)) as unknown as [
|
||||||
);
|
UserPrefix[],
|
||||||
|
unknown,
|
||||||
|
];
|
||||||
|
|
||||||
const countResult = await prisma.$queryRaw<[{ total: bigint }]>(
|
const [countResult] =
|
||||||
Prisma.sql`SELECT COUNT(*) as total FROM custom_prefixes up
|
(await db.execute(sql`SELECT COUNT(*) as total FROM custom_prefixes up
|
||||||
LEFT JOIN users u ON u.id = up.user_id
|
LEFT JOIN users u ON u.id = up.user_id
|
||||||
${whereFragment}`,
|
${whereFragment}`)) as unknown as [[{ total: bigint }], unknown];
|
||||||
);
|
|
||||||
|
|
||||||
const total = Number(countResult[0]?.total || 0);
|
const total = Number(countResult[0]?.total || 0);
|
||||||
|
|
||||||
@@ -69,9 +71,9 @@ export const POST = withAdmin(
|
|||||||
return apiError("Missing required fields: username, text, color");
|
return apiError("Missing required fields: username, text, color");
|
||||||
}
|
}
|
||||||
|
|
||||||
const users = await prisma.$queryRaw<{ id: number }[]>(
|
const [users] = (await db.execute(
|
||||||
Prisma.sql`SELECT id FROM users WHERE username = ${username} LIMIT 1`,
|
sql`SELECT id FROM users WHERE username = ${username} LIMIT 1`,
|
||||||
);
|
)) as unknown as [{ id: number }[], unknown];
|
||||||
|
|
||||||
if (!users || users.length === 0) {
|
if (!users || users.length === 0) {
|
||||||
return apiError("User not found");
|
return apiError("User not found");
|
||||||
@@ -79,8 +81,8 @@ export const POST = withAdmin(
|
|||||||
|
|
||||||
const userId = users[0].id;
|
const userId = users[0].id;
|
||||||
|
|
||||||
await prisma.$executeRaw(
|
await db.execute(
|
||||||
Prisma.sql`INSERT INTO custom_prefixes (user_id, text, color, icon, effect, active)
|
sql`INSERT INTO custom_prefixes (user_id, text, color, icon, effect, active)
|
||||||
VALUES (${userId}, ${text}, ${color}, ${icon || ""}, ${effect || ""}, ${active ? 1 : 0})`,
|
VALUES (${userId}, ${text}, ${color}, ${icon || ""}, ${effect || ""}, ${active ? 1 : 0})`,
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -96,8 +98,8 @@ export const PUT = withAdmin(
|
|||||||
|
|
||||||
if (!id) return apiError("Missing prefix id");
|
if (!id) return apiError("Missing prefix id");
|
||||||
|
|
||||||
await prisma.$executeRaw(
|
await db.execute(
|
||||||
Prisma.sql`UPDATE custom_prefixes
|
sql`UPDATE custom_prefixes
|
||||||
SET text = ${text}, color = ${color}, icon = ${icon || ""}, effect = ${effect || ""}, active = ${active ? 1 : 0}
|
SET text = ${text}, color = ${color}, icon = ${icon || ""}, effect = ${effect || ""}, active = ${active ? 1 : 0}
|
||||||
WHERE id = ${id}`,
|
WHERE id = ${id}`,
|
||||||
);
|
);
|
||||||
@@ -114,9 +116,7 @@ export const DELETE = withAdmin(
|
|||||||
|
|
||||||
if (!id) return apiError("Missing prefix id");
|
if (!id) return apiError("Missing prefix id");
|
||||||
|
|
||||||
await prisma.$executeRaw(
|
await db.execute(sql`DELETE FROM custom_prefixes WHERE id = ${id}`);
|
||||||
Prisma.sql`DELETE FROM custom_prefixes WHERE id = ${id}`,
|
|
||||||
);
|
|
||||||
|
|
||||||
return apiOk({ deleted: id });
|
return apiOk({ deleted: id });
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
|
import { sql } from "drizzle-orm";
|
||||||
import { withAdmin } from "@/lib/api-handler";
|
import { withAdmin } from "@/lib/api-handler";
|
||||||
import { apiError, apiOk } from "@/lib/api-response";
|
import { apiError, apiOk } from "@/lib/api-response";
|
||||||
|
import { db } from "@/lib/db";
|
||||||
import { PERMS } from "@/lib/permissions";
|
import { PERMS } from "@/lib/permissions";
|
||||||
import { prisma } from "@/lib/prisma";
|
|
||||||
|
|
||||||
interface PrefixSetting {
|
interface PrefixSetting {
|
||||||
key: string;
|
key: string;
|
||||||
@@ -31,9 +32,9 @@ const DEFAULT_SETTINGS: Record<string, string> = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export const GET = withAdmin({ permission: PERMS.PREFIXES_VIEW }, async () => {
|
export const GET = withAdmin({ permission: PERMS.PREFIXES_VIEW }, async () => {
|
||||||
const settings = await prisma.$queryRaw<PrefixSetting[]>`
|
const [settings] = (await db.execute(
|
||||||
SELECT \`key\`, \`value\` FROM custom_prefix_settings
|
sql`SELECT \`key\`, \`value\` FROM custom_prefix_settings`,
|
||||||
`;
|
)) as unknown as [PrefixSetting[], unknown];
|
||||||
|
|
||||||
const result: Record<string, string> = { ...DEFAULT_SETTINGS };
|
const result: Record<string, string> = { ...DEFAULT_SETTINGS };
|
||||||
for (const s of settings) {
|
for (const s of settings) {
|
||||||
@@ -58,11 +59,11 @@ export const PUT = withAdmin(
|
|||||||
return apiError(`Invalid setting key: ${key}`);
|
return apiError(`Invalid setting key: ${key}`);
|
||||||
}
|
}
|
||||||
const strValue = String(value);
|
const strValue = String(value);
|
||||||
await prisma.$executeRaw`
|
await db.execute(sql`
|
||||||
INSERT INTO custom_prefix_settings (\`key\`, \`value\`)
|
INSERT INTO custom_prefix_settings (\`key\`, \`value\`)
|
||||||
VALUES (${key}, ${strValue})
|
VALUES (${key}, ${strValue})
|
||||||
ON DUPLICATE KEY UPDATE \`value\` = ${strValue}
|
ON DUPLICATE KEY UPDATE \`value\` = ${strValue}
|
||||||
`;
|
`);
|
||||||
}
|
}
|
||||||
|
|
||||||
return apiOk();
|
return apiOk();
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
|
import { eq, sql } from "drizzle-orm";
|
||||||
import { NextResponse } from "next/server";
|
import { NextResponse } from "next/server";
|
||||||
import { withAdmin } from "@/lib/api-handler";
|
import { withAdmin } from "@/lib/api-handler";
|
||||||
|
import { db, User } from "@/lib/db";
|
||||||
import { PERMS } from "@/lib/permissions";
|
import { PERMS } from "@/lib/permissions";
|
||||||
import { prisma } from "@/lib/prisma";
|
|
||||||
import { rcon } from "@/lib/services/rcon";
|
import { rcon } from "@/lib/services/rcon";
|
||||||
import { logStaffActivity } from "@/lib/services/staff-activity";
|
import { logStaffActivity } from "@/lib/services/staff-activity";
|
||||||
|
|
||||||
@@ -31,9 +32,14 @@ export const POST = withAdmin(
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const rankExists = await prisma.$queryRaw<{ id: number }[]>`
|
const [rankExists] = (await db
|
||||||
SELECT id FROM permission_ranks WHERE id = ${rank} LIMIT 1
|
.execute(
|
||||||
`.catch(() => [] as { id: number }[]);
|
sql`SELECT id FROM permission_ranks WHERE id = ${rank} LIMIT 1`,
|
||||||
|
)
|
||||||
|
.catch(() => [[]] as unknown as [unknown[], unknown])) as unknown as [
|
||||||
|
{ id: number }[],
|
||||||
|
unknown,
|
||||||
|
];
|
||||||
if (rankExists.length === 0) {
|
if (rankExists.length === 0) {
|
||||||
return NextResponse.json(
|
return NextResponse.json(
|
||||||
{ success: false, message: "Rank does not exist" },
|
{ success: false, message: "Rank does not exist" },
|
||||||
@@ -41,10 +47,11 @@ export const POST = withAdmin(
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const target = await prisma.user.findUnique({
|
const [target] = await db
|
||||||
where: { id: userId },
|
.select({ rank: User.rank })
|
||||||
select: { rank: true },
|
.from(User)
|
||||||
});
|
.where(eq(User.id, userId))
|
||||||
|
.limit(1);
|
||||||
if (!target) {
|
if (!target) {
|
||||||
return NextResponse.json(
|
return NextResponse.json(
|
||||||
{ success: false, message: "User not found" },
|
{ success: false, message: "User not found" },
|
||||||
@@ -74,7 +81,7 @@ export const POST = withAdmin(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
await prisma.user.update({ where: { id: userId }, data: { rank } });
|
await db.update(User).set({ rank }).where(eq(User.id, userId));
|
||||||
await rcon.setRank(userId, rank);
|
await rcon.setRank(userId, rank);
|
||||||
await logStaffActivity({
|
await logStaffActivity({
|
||||||
staffId,
|
staffId,
|
||||||
|
|||||||
@@ -6,9 +6,10 @@
|
|||||||
// chars (matches the VARCHAR(255) column). Fails soft — never returns a 500 for
|
// chars (matches the VARCHAR(255) column). Fails soft — never returns a 500 for
|
||||||
// DB issues, just a generic error envelope.
|
// DB issues, just a generic error envelope.
|
||||||
|
|
||||||
|
import { eq } from "drizzle-orm";
|
||||||
import { apiError, apiJson } from "@/lib/api";
|
import { apiError, apiJson } from "@/lib/api";
|
||||||
import { bearerUserId } from "@/lib/api-auth";
|
import { bearerUserId } from "@/lib/api-auth";
|
||||||
import { prisma } from "@/lib/prisma";
|
import { db, WebsiteArticleComments, WebsiteArticles } from "@/lib/db";
|
||||||
|
|
||||||
export const dynamic = "force-dynamic";
|
export const dynamic = "force-dynamic";
|
||||||
|
|
||||||
@@ -31,24 +32,22 @@ export async function POST(
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const article = await prisma.websiteArticles.findUnique({
|
const [article] = await db
|
||||||
where: { slug },
|
.select({ id: WebsiteArticles.id })
|
||||||
select: { id: true },
|
.from(WebsiteArticles)
|
||||||
});
|
.where(eq(WebsiteArticles.slug, slug))
|
||||||
|
.limit(1);
|
||||||
if (!article) {
|
if (!article) {
|
||||||
return apiError("Article not found", 404);
|
return apiError("Article not found", 404);
|
||||||
}
|
}
|
||||||
|
|
||||||
const now = new Date();
|
const now = new Date();
|
||||||
await prisma.websiteArticleComments.create({
|
await db.insert(WebsiteArticleComments).values({
|
||||||
data: {
|
articleId: article.id,
|
||||||
articleId: article.id,
|
userId: uid,
|
||||||
userId: uid,
|
comment,
|
||||||
comment,
|
createdAt: now,
|
||||||
createdAt: now,
|
updatedAt: now,
|
||||||
updatedAt: now,
|
|
||||||
},
|
|
||||||
select: { id: true },
|
|
||||||
});
|
});
|
||||||
|
|
||||||
return apiJson({ ok: true });
|
return apiJson({ ok: true });
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
|
import { eq } from "drizzle-orm";
|
||||||
import { apiJson } from "@/lib/api";
|
import { apiJson } from "@/lib/api";
|
||||||
import { prisma } from "@/lib/prisma";
|
import { db, WebsiteArticles } from "@/lib/db";
|
||||||
|
|
||||||
export const dynamic = "force-dynamic";
|
export const dynamic = "force-dynamic";
|
||||||
|
|
||||||
@@ -14,19 +15,20 @@ export async function GET(
|
|||||||
const { slug } = await params;
|
const { slug } = await params;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const article = await prisma.websiteArticles.findUnique({
|
const [article] = await db
|
||||||
where: { slug },
|
.select({
|
||||||
select: {
|
id: WebsiteArticles.id,
|
||||||
id: true,
|
title: WebsiteArticles.title,
|
||||||
title: true,
|
slug: WebsiteArticles.slug,
|
||||||
slug: true,
|
shortStory: WebsiteArticles.shortStory,
|
||||||
shortStory: true,
|
fullStory: WebsiteArticles.fullStory,
|
||||||
fullStory: true,
|
image: WebsiteArticles.image,
|
||||||
image: true,
|
createdAt: WebsiteArticles.createdAt,
|
||||||
createdAt: true,
|
updatedAt: WebsiteArticles.updatedAt,
|
||||||
updatedAt: true,
|
})
|
||||||
},
|
.from(WebsiteArticles)
|
||||||
});
|
.where(eq(WebsiteArticles.slug, slug))
|
||||||
|
.limit(1);
|
||||||
|
|
||||||
if (!article) {
|
if (!article) {
|
||||||
return apiJson({ error: "Article not found" }, { status: 404 });
|
return apiJson({ error: "Article not found" }, { status: 404 });
|
||||||
|
|||||||
@@ -1,34 +1,36 @@
|
|||||||
|
import { count, desc } from "drizzle-orm";
|
||||||
import { apiJson, pagination } from "@/lib/api";
|
import { apiJson, pagination } from "@/lib/api";
|
||||||
import { prisma } from "@/lib/prisma";
|
import { db, WebsiteArticles } from "@/lib/db";
|
||||||
|
|
||||||
export const dynamic = "force-dynamic";
|
export const dynamic = "force-dynamic";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* GET /api/articles — paginated list of website_articles, newest first.
|
* GET /api/articles — paginated list of website_articles, newest first.
|
||||||
* Mirrors the /news page query (prisma.websiteArticles). Returns list cards
|
* Mirrors the /news page query. Returns list cards (shortStory only, never
|
||||||
* (shortStory only, never fullStory) plus pagination metadata.
|
* fullStory) plus pagination metadata.
|
||||||
*/
|
*/
|
||||||
export async function GET(req: Request) {
|
export async function GET(req: Request) {
|
||||||
const sp = new URL(req.url).searchParams;
|
const sp = new URL(req.url).searchParams;
|
||||||
const { page, perPage, skip, take } = pagination(sp);
|
const { page, perPage, skip, take } = pagination(sp);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const [total, articles] = await Promise.all([
|
const [totalRows, articles] = await Promise.all([
|
||||||
prisma.websiteArticles.count(),
|
db.select({ total: count() }).from(WebsiteArticles),
|
||||||
prisma.websiteArticles.findMany({
|
db
|
||||||
select: {
|
.select({
|
||||||
id: true,
|
id: WebsiteArticles.id,
|
||||||
title: true,
|
title: WebsiteArticles.title,
|
||||||
slug: true,
|
slug: WebsiteArticles.slug,
|
||||||
shortStory: true,
|
shortStory: WebsiteArticles.shortStory,
|
||||||
image: true,
|
image: WebsiteArticles.image,
|
||||||
createdAt: true,
|
createdAt: WebsiteArticles.createdAt,
|
||||||
},
|
})
|
||||||
orderBy: { createdAt: "desc" },
|
.from(WebsiteArticles)
|
||||||
skip,
|
.orderBy(desc(WebsiteArticles.createdAt))
|
||||||
take,
|
.limit(take)
|
||||||
}),
|
.offset(skip),
|
||||||
]);
|
]);
|
||||||
|
const total = totalRows[0]?.total ?? 0;
|
||||||
|
|
||||||
return apiJson({
|
return apiJson({
|
||||||
data: articles,
|
data: articles,
|
||||||
|
|||||||
@@ -1,8 +1,9 @@
|
|||||||
|
import { eq, sql } from "drizzle-orm";
|
||||||
import { apiJson } from "@/lib/api";
|
import { apiJson } from "@/lib/api";
|
||||||
import { bearerUserId } from "@/lib/api-auth";
|
import { bearerUserId } from "@/lib/api-auth";
|
||||||
import { auth } from "@/lib/auth";
|
import { auth } from "@/lib/auth";
|
||||||
|
import { db, User, UsersSettings } from "@/lib/db";
|
||||||
import { logger } from "@/lib/logger";
|
import { logger } from "@/lib/logger";
|
||||||
import { Prisma, prisma } from "@/lib/prisma";
|
|
||||||
|
|
||||||
export const dynamic = "force-dynamic";
|
export const dynamic = "force-dynamic";
|
||||||
|
|
||||||
@@ -60,21 +61,27 @@ function rankEntries(
|
|||||||
.map((e, i) => ({ ...e, rank: i + 1 }));
|
.map((e, i) => ({ ...e, rank: i + 1 }));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function rawRows<T>(query: ReturnType<typeof sql>): Promise<T[]> {
|
||||||
|
const [rows] = (await db.execute(query)) as unknown as [T[], unknown];
|
||||||
|
return rows;
|
||||||
|
}
|
||||||
|
|
||||||
async function loadTotalBadgesBoard(
|
async function loadTotalBadgesBoard(
|
||||||
userId: number | null,
|
userId: number | null,
|
||||||
): Promise<BadgeLeaderboardBoard> {
|
): Promise<BadgeLeaderboardBoard> {
|
||||||
const rows = await prisma.$queryRaw<
|
const rows = await rawRows<{
|
||||||
Array<{ userId: bigint; username: string; look: string; cnt: bigint }>
|
userId: bigint;
|
||||||
>(
|
username: string;
|
||||||
Prisma.sql`
|
look: string;
|
||||||
|
cnt: bigint;
|
||||||
|
}>(sql`
|
||||||
SELECT ub.user_id AS userId, u.username, u.look, COUNT(*) AS cnt
|
SELECT ub.user_id AS userId, u.username, u.look, COUNT(*) AS cnt
|
||||||
FROM users_badges ub
|
FROM users_badges ub
|
||||||
JOIN users u ON u.id = ub.user_id
|
JOIN users u ON u.id = ub.user_id
|
||||||
GROUP BY ub.user_id
|
GROUP BY ub.user_id
|
||||||
ORDER BY cnt DESC
|
ORDER BY cnt DESC
|
||||||
LIMIT 20
|
LIMIT 20
|
||||||
`,
|
`);
|
||||||
);
|
|
||||||
|
|
||||||
const entries = rankEntries(
|
const entries = rankEntries(
|
||||||
rows.map((r) => ({
|
rows.map((r) => ({
|
||||||
@@ -90,8 +97,8 @@ async function loadTotalBadgesBoard(
|
|||||||
entries.length > 0
|
entries.length > 0
|
||||||
? Number(
|
? Number(
|
||||||
(
|
(
|
||||||
await prisma.$queryRaw<Array<{ cnt: bigint }>>(
|
await rawRows<{ cnt: bigint }>(
|
||||||
Prisma.sql`SELECT COUNT(DISTINCT user_id) AS cnt FROM users_badges`,
|
sql`SELECT COUNT(DISTINCT user_id) AS cnt FROM users_badges`,
|
||||||
)
|
)
|
||||||
)[0]?.cnt ?? 0,
|
)[0]?.cnt ?? 0,
|
||||||
)
|
)
|
||||||
@@ -99,8 +106,8 @@ async function loadTotalBadgesBoard(
|
|||||||
|
|
||||||
let viewerEntry: Partial<BadgeLeaderboardEntry> | undefined;
|
let viewerEntry: Partial<BadgeLeaderboardEntry> | undefined;
|
||||||
if (userId) {
|
if (userId) {
|
||||||
const viewerRow = await prisma.$queryRaw<Array<{ cnt: bigint }>>(
|
const viewerRow = await rawRows<{ cnt: bigint }>(
|
||||||
Prisma.sql`SELECT COUNT(*) AS cnt FROM users_badges WHERE user_id = ${userId}`,
|
sql`SELECT COUNT(*) AS cnt FROM users_badges WHERE user_id = ${userId}`,
|
||||||
);
|
);
|
||||||
const viewerScore = Number(viewerRow[0]?.cnt ?? 0);
|
const viewerScore = Number(viewerRow[0]?.cnt ?? 0);
|
||||||
if (viewerScore > 0) {
|
if (viewerScore > 0) {
|
||||||
@@ -108,20 +115,19 @@ async function loadTotalBadgesBoard(
|
|||||||
entries.length > 0
|
entries.length > 0
|
||||||
? Number(
|
? Number(
|
||||||
(
|
(
|
||||||
await prisma.$queryRaw<Array<{ cnt: bigint }>>(
|
await rawRows<{ cnt: bigint }>(sql`
|
||||||
Prisma.sql`
|
|
||||||
SELECT COUNT(*) + 1 AS cnt
|
SELECT COUNT(*) + 1 AS cnt
|
||||||
FROM (SELECT user_id, COUNT(*) AS total FROM users_badges GROUP BY user_id) t
|
FROM (SELECT user_id, COUNT(*) AS total FROM users_badges GROUP BY user_id) t
|
||||||
WHERE t.total > ${viewerScore}
|
WHERE t.total > ${viewerScore}
|
||||||
`,
|
`)
|
||||||
)
|
|
||||||
)[0]?.cnt ?? entries.length + 1,
|
)[0]?.cnt ?? entries.length + 1,
|
||||||
)
|
)
|
||||||
: 1;
|
: 1;
|
||||||
const viewerUser = await prisma.user.findUnique({
|
const [viewerUser] = await db
|
||||||
where: { id: userId },
|
.select({ username: User.username, look: User.look })
|
||||||
select: { username: true, look: true },
|
.from(User)
|
||||||
});
|
.where(eq(User.id, userId))
|
||||||
|
.limit(1);
|
||||||
if (viewerUser) {
|
if (viewerUser) {
|
||||||
viewerEntry = {
|
viewerEntry = {
|
||||||
userId,
|
userId,
|
||||||
@@ -140,17 +146,18 @@ async function loadTotalBadgesBoard(
|
|||||||
async function loadAchievementBoard(
|
async function loadAchievementBoard(
|
||||||
userId: number | null,
|
userId: number | null,
|
||||||
): Promise<BadgeLeaderboardBoard> {
|
): Promise<BadgeLeaderboardBoard> {
|
||||||
const rows = await prisma.$queryRaw<
|
const rows = await rawRows<{
|
||||||
Array<{ userId: bigint; username: string; look: string; score: number }>
|
userId: bigint;
|
||||||
>(
|
username: string;
|
||||||
Prisma.sql`
|
look: string;
|
||||||
|
score: number;
|
||||||
|
}>(sql`
|
||||||
SELECT us.user_id AS userId, u.username, u.look, us.achievement_score AS score
|
SELECT us.user_id AS userId, u.username, u.look, us.achievement_score AS score
|
||||||
FROM users_settings us
|
FROM users_settings us
|
||||||
JOIN users u ON u.id = us.user_id
|
JOIN users u ON u.id = us.user_id
|
||||||
ORDER BY us.achievement_score DESC
|
ORDER BY us.achievement_score DESC
|
||||||
LIMIT 20
|
LIMIT 20
|
||||||
`,
|
`);
|
||||||
);
|
|
||||||
|
|
||||||
const entries = rankEntries(
|
const entries = rankEntries(
|
||||||
rows.map((r) => ({
|
rows.map((r) => ({
|
||||||
@@ -166,8 +173,8 @@ async function loadAchievementBoard(
|
|||||||
entries.length > 0
|
entries.length > 0
|
||||||
? Number(
|
? Number(
|
||||||
(
|
(
|
||||||
await prisma.$queryRaw<Array<{ cnt: bigint }>>(
|
await rawRows<{ cnt: bigint }>(
|
||||||
Prisma.sql`SELECT COUNT(*) AS cnt FROM users_settings WHERE achievement_score > 0`,
|
sql`SELECT COUNT(*) AS cnt FROM users_settings WHERE achievement_score > 0`,
|
||||||
)
|
)
|
||||||
)[0]?.cnt ?? 0,
|
)[0]?.cnt ?? 0,
|
||||||
)
|
)
|
||||||
@@ -175,23 +182,23 @@ async function loadAchievementBoard(
|
|||||||
|
|
||||||
let viewerEntry: Partial<BadgeLeaderboardEntry> | undefined;
|
let viewerEntry: Partial<BadgeLeaderboardEntry> | undefined;
|
||||||
if (userId) {
|
if (userId) {
|
||||||
const viewerUser = await prisma.user.findUnique({
|
const [viewerUser] = await db
|
||||||
where: { id: userId },
|
.select({ username: User.username, look: User.look })
|
||||||
select: { username: true, look: true },
|
.from(User)
|
||||||
});
|
.where(eq(User.id, userId))
|
||||||
|
.limit(1);
|
||||||
if (viewerUser) {
|
if (viewerUser) {
|
||||||
const viewerSettings = await prisma.usersSettings.findUnique({
|
const [viewerSettings] = await db
|
||||||
where: { userId },
|
.select({ achievementScore: UsersSettings.achievementScore })
|
||||||
select: { achievementScore: true },
|
.from(UsersSettings)
|
||||||
});
|
.where(eq(UsersSettings.userId, userId))
|
||||||
|
.limit(1);
|
||||||
if (viewerSettings && viewerSettings.achievementScore > 0) {
|
if (viewerSettings && viewerSettings.achievementScore > 0) {
|
||||||
const viewerRank = Number(
|
const viewerRank = Number(
|
||||||
(
|
(
|
||||||
await prisma.$queryRaw<Array<{ cnt: bigint }>>(
|
await rawRows<{ cnt: bigint }>(sql`
|
||||||
Prisma.sql`
|
|
||||||
SELECT COUNT(*) + 1 AS cnt FROM users_settings WHERE achievement_score > ${viewerSettings.achievementScore}
|
SELECT COUNT(*) + 1 AS cnt FROM users_settings WHERE achievement_score > ${viewerSettings.achievementScore}
|
||||||
`,
|
`)
|
||||||
)
|
|
||||||
)[0]?.cnt ?? entries.length + 1,
|
)[0]?.cnt ?? entries.length + 1,
|
||||||
);
|
);
|
||||||
viewerEntry = {
|
viewerEntry = {
|
||||||
@@ -215,12 +222,17 @@ async function loadRarityBoard(
|
|||||||
): Promise<BadgeLeaderboardBoard> {
|
): Promise<BadgeLeaderboardBoard> {
|
||||||
if (badgeCodes.length === 0) return { entries: [], totalPlayers: 0 };
|
if (badgeCodes.length === 0) return { entries: [], totalPlayers: 0 };
|
||||||
|
|
||||||
const codes = Prisma.join(badgeCodes);
|
const codes = sql.join(
|
||||||
|
badgeCodes.map((c) => sql`${c}`),
|
||||||
|
sql`, `,
|
||||||
|
);
|
||||||
|
|
||||||
const rows = await prisma.$queryRaw<
|
const rows = await rawRows<{
|
||||||
Array<{ userId: bigint; username: string; look: string; cnt: bigint }>
|
userId: bigint;
|
||||||
>(
|
username: string;
|
||||||
Prisma.sql`
|
look: string;
|
||||||
|
cnt: bigint;
|
||||||
|
}>(sql`
|
||||||
SELECT ub.user_id AS userId, u.username, u.look, COUNT(*) AS cnt
|
SELECT ub.user_id AS userId, u.username, u.look, COUNT(*) AS cnt
|
||||||
FROM users_badges ub
|
FROM users_badges ub
|
||||||
JOIN users u ON u.id = ub.user_id
|
JOIN users u ON u.id = ub.user_id
|
||||||
@@ -228,8 +240,7 @@ async function loadRarityBoard(
|
|||||||
GROUP BY ub.user_id
|
GROUP BY ub.user_id
|
||||||
ORDER BY cnt DESC
|
ORDER BY cnt DESC
|
||||||
LIMIT 20
|
LIMIT 20
|
||||||
`,
|
`);
|
||||||
);
|
|
||||||
|
|
||||||
const entries = rankEntries(
|
const entries = rankEntries(
|
||||||
rows.map((r) => ({
|
rows.map((r) => ({
|
||||||
@@ -245,39 +256,34 @@ async function loadRarityBoard(
|
|||||||
rows.length > 0
|
rows.length > 0
|
||||||
? Number(
|
? Number(
|
||||||
(
|
(
|
||||||
await prisma.$queryRaw<Array<{ cnt: bigint }>>(
|
await rawRows<{ cnt: bigint }>(sql`
|
||||||
Prisma.sql`
|
|
||||||
SELECT COUNT(DISTINCT user_id) AS cnt FROM users_badges WHERE badge_code IN (${codes})
|
SELECT COUNT(DISTINCT user_id) AS cnt FROM users_badges WHERE badge_code IN (${codes})
|
||||||
`,
|
`)
|
||||||
)
|
|
||||||
)[0]?.cnt ?? 0,
|
)[0]?.cnt ?? 0,
|
||||||
)
|
)
|
||||||
: 0;
|
: 0;
|
||||||
|
|
||||||
let viewerEntry: Partial<BadgeLeaderboardEntry> | undefined;
|
let viewerEntry: Partial<BadgeLeaderboardEntry> | undefined;
|
||||||
if (userId) {
|
if (userId) {
|
||||||
const viewerRow = await prisma.$queryRaw<Array<{ cnt: bigint }>>(
|
const viewerRow = await rawRows<{ cnt: bigint }>(sql`
|
||||||
Prisma.sql`
|
|
||||||
SELECT COUNT(*) AS cnt FROM users_badges
|
SELECT COUNT(*) AS cnt FROM users_badges
|
||||||
WHERE user_id = ${userId} AND badge_code IN (${codes})
|
WHERE user_id = ${userId} AND badge_code IN (${codes})
|
||||||
`,
|
`);
|
||||||
);
|
|
||||||
const viewerScore = Number(viewerRow[0]?.cnt ?? 0);
|
const viewerScore = Number(viewerRow[0]?.cnt ?? 0);
|
||||||
if (viewerScore > 0) {
|
if (viewerScore > 0) {
|
||||||
const viewerUser = await prisma.user.findUnique({
|
const [viewerUser] = await db
|
||||||
where: { id: userId },
|
.select({ username: User.username, look: User.look })
|
||||||
select: { username: true, look: true },
|
.from(User)
|
||||||
});
|
.where(eq(User.id, userId))
|
||||||
|
.limit(1);
|
||||||
if (viewerUser) {
|
if (viewerUser) {
|
||||||
const viewerRank = Number(
|
const viewerRank = Number(
|
||||||
(
|
(
|
||||||
await prisma.$queryRaw<Array<{ cnt: bigint }>>(
|
await rawRows<{ cnt: bigint }>(sql`
|
||||||
Prisma.sql`
|
|
||||||
SELECT COUNT(*) + 1 AS cnt
|
SELECT COUNT(*) + 1 AS cnt
|
||||||
FROM (SELECT user_id, COUNT(*) AS total FROM users_badges WHERE badge_code IN (${codes}) GROUP BY user_id) t
|
FROM (SELECT user_id, COUNT(*) AS total FROM users_badges WHERE badge_code IN (${codes}) GROUP BY user_id) t
|
||||||
WHERE t.total > ${viewerScore}
|
WHERE t.total > ${viewerScore}
|
||||||
`,
|
`)
|
||||||
)
|
|
||||||
)[0]?.cnt ?? entries.length + 1,
|
)[0]?.cnt ?? entries.length + 1,
|
||||||
);
|
);
|
||||||
viewerEntry = {
|
viewerEntry = {
|
||||||
@@ -302,15 +308,14 @@ export async function GET(req: Request) {
|
|||||||
userId = session?.user?.id ? Number(session.user.id) : null;
|
userId = session?.user?.id ? Number(session.user.id) : null;
|
||||||
}
|
}
|
||||||
|
|
||||||
const badgeStatsRaw = await prisma.$queryRaw<
|
const badgeStatsRaw = await rawRows<{
|
||||||
Array<{ badgeCode: string; ownerCount: bigint }>
|
badgeCode: string;
|
||||||
>(
|
ownerCount: bigint;
|
||||||
Prisma.sql`
|
}>(sql`
|
||||||
SELECT badge_code AS badgeCode, COUNT(DISTINCT user_id) AS ownerCount
|
SELECT badge_code AS badgeCode, COUNT(DISTINCT user_id) AS ownerCount
|
||||||
FROM users_badges
|
FROM users_badges
|
||||||
GROUP BY badge_code
|
GROUP BY badge_code
|
||||||
`,
|
`);
|
||||||
);
|
|
||||||
|
|
||||||
const badgeStats: BadgeLeaderboardStat[] = badgeStatsRaw.map((r) => ({
|
const badgeStats: BadgeLeaderboardStat[] = badgeStatsRaw.map((r) => ({
|
||||||
badgeCode: r.badgeCode,
|
badgeCode: r.badgeCode,
|
||||||
|
|||||||
@@ -2,8 +2,9 @@
|
|||||||
// (guilds_members). AtomCMS JSON API parity. Returns { error } (404) when the
|
// (guilds_members). AtomCMS JSON API parity. Returns { error } (404) when the
|
||||||
// id is unknown. The owner is the user_id column (accessor userId), surfaced
|
// id is unknown. The owner is the user_id column (accessor userId), surfaced
|
||||||
// here as ownerId.
|
// here as ownerId.
|
||||||
|
import { count, eq } from "drizzle-orm";
|
||||||
import { apiJson } from "@/lib/api";
|
import { apiJson } from "@/lib/api";
|
||||||
import { prisma } from "@/lib/prisma";
|
import { db, Guilds, GuildsMembers } from "@/lib/db";
|
||||||
|
|
||||||
export const dynamic = "force-dynamic";
|
export const dynamic = "force-dynamic";
|
||||||
|
|
||||||
@@ -19,18 +20,19 @@ export async function GET(
|
|||||||
const guildId = Number(id);
|
const guildId = Number(id);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const guild = await prisma.guilds.findUnique({
|
const [guild] = await db
|
||||||
where: { id: guildId },
|
.select({
|
||||||
select: {
|
id: Guilds.id,
|
||||||
id: true,
|
name: Guilds.name,
|
||||||
name: true,
|
description: Guilds.description,
|
||||||
description: true,
|
userId: Guilds.userId,
|
||||||
userId: true,
|
roomId: Guilds.roomId,
|
||||||
roomId: true,
|
badge: Guilds.badge,
|
||||||
badge: true,
|
dateCreated: Guilds.dateCreated,
|
||||||
dateCreated: true,
|
})
|
||||||
},
|
.from(Guilds)
|
||||||
});
|
.where(eq(Guilds.id, guildId))
|
||||||
|
.limit(1);
|
||||||
|
|
||||||
if (!guild) {
|
if (!guild) {
|
||||||
return apiJson({ error: "Guild not found" }, { status: 404 });
|
return apiJson({ error: "Guild not found" }, { status: 404 });
|
||||||
@@ -39,9 +41,11 @@ export async function GET(
|
|||||||
// Member count is a separate guarded query (no relation is modelled).
|
// Member count is a separate guarded query (no relation is modelled).
|
||||||
let memberCount = 0;
|
let memberCount = 0;
|
||||||
try {
|
try {
|
||||||
memberCount = await prisma.guildsMembers.count({
|
const [row] = await db
|
||||||
where: { guildId: guild.id },
|
.select({ total: count() })
|
||||||
});
|
.from(GuildsMembers)
|
||||||
|
.where(eq(GuildsMembers.guildId, guild.id));
|
||||||
|
memberCount = row?.total ?? 0;
|
||||||
} catch {
|
} catch {
|
||||||
memberCount = 0;
|
memberCount = 0;
|
||||||
}
|
}
|
||||||
|
|||||||
+16
-14
@@ -2,8 +2,9 @@
|
|||||||
// paginated list ordered newest first (by id), exposing the basic public
|
// paginated list ordered newest first (by id), exposing the basic public
|
||||||
// fields. The owner is stored as the user_id column (accessor userId), surfaced
|
// fields. The owner is stored as the user_id column (accessor userId), surfaced
|
||||||
// here as ownerId for API clarity.
|
// here as ownerId for API clarity.
|
||||||
|
import { count, desc } from "drizzle-orm";
|
||||||
import { apiJson, pagination } from "@/lib/api";
|
import { apiJson, pagination } from "@/lib/api";
|
||||||
import { prisma } from "@/lib/prisma";
|
import { db, Guilds } from "@/lib/db";
|
||||||
|
|
||||||
export const dynamic = "force-dynamic";
|
export const dynamic = "force-dynamic";
|
||||||
|
|
||||||
@@ -12,20 +13,21 @@ export async function GET(req: Request) {
|
|||||||
const { page, perPage, skip, take } = pagination(sp);
|
const { page, perPage, skip, take } = pagination(sp);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const [total, rows] = await Promise.all([
|
const [totalRows, rows] = await Promise.all([
|
||||||
prisma.guilds.count(),
|
db.select({ total: count() }).from(Guilds),
|
||||||
prisma.guilds.findMany({
|
db
|
||||||
orderBy: { id: "desc" },
|
.select({
|
||||||
skip,
|
id: Guilds.id,
|
||||||
take,
|
name: Guilds.name,
|
||||||
select: {
|
description: Guilds.description,
|
||||||
id: true,
|
userId: Guilds.userId,
|
||||||
name: true,
|
})
|
||||||
description: true,
|
.from(Guilds)
|
||||||
userId: true,
|
.orderBy(desc(Guilds.id))
|
||||||
},
|
.limit(take)
|
||||||
}),
|
.offset(skip),
|
||||||
]);
|
]);
|
||||||
|
const total = totalRows[0]?.total ?? 0;
|
||||||
|
|
||||||
// Rename userId → ownerId for the public payload.
|
// Rename userId → ownerId for the public payload.
|
||||||
const data = rows.map(({ userId, ...rest }) => ({
|
const data = rows.map(({ userId, ...rest }) => ({
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
|
import { sql } from "drizzle-orm";
|
||||||
import { env } from "@/env";
|
import { env } from "@/env";
|
||||||
import { apiJson } from "@/lib/api";
|
import { apiJson } from "@/lib/api";
|
||||||
import { prisma } from "@/lib/prisma";
|
import { db } from "@/lib/db";
|
||||||
import { redis } from "@/lib/redis";
|
import { redis } from "@/lib/redis";
|
||||||
import { rcon } from "@/lib/services/rcon";
|
import { rcon } from "@/lib/services/rcon";
|
||||||
|
|
||||||
@@ -13,7 +14,8 @@ export const dynamic = "force-dynamic";
|
|||||||
* only care about reachability.
|
* only care about reachability.
|
||||||
*/
|
*/
|
||||||
export async function GET() {
|
export async function GET() {
|
||||||
const database = await prisma.$queryRaw`SELECT 1`
|
const database = await db
|
||||||
|
.execute(sql`SELECT 1`)
|
||||||
.then(() => true)
|
.then(() => true)
|
||||||
.catch(() => false);
|
.catch(() => false);
|
||||||
|
|
||||||
|
|||||||
+17
-16
@@ -1,7 +1,8 @@
|
|||||||
|
import { count, desc, eq } from "drizzle-orm";
|
||||||
import { apiJson } from "@/lib/api";
|
import { apiJson } from "@/lib/api";
|
||||||
import { FALLBACK_HOTEL_NAME } from "@/lib/brand";
|
import { FALLBACK_HOTEL_NAME } from "@/lib/brand";
|
||||||
|
import { db, User, WebsiteArticles } from "@/lib/db";
|
||||||
import { resolveHotelName } from "@/lib/hotel-name";
|
import { resolveHotelName } from "@/lib/hotel-name";
|
||||||
import { prisma } from "@/lib/prisma";
|
|
||||||
import { apiCacheKey, redisCache } from "@/lib/redis-cache";
|
import { apiCacheKey, redisCache } from "@/lib/redis-cache";
|
||||||
|
|
||||||
export const dynamic = "force-dynamic";
|
export const dynamic = "force-dynamic";
|
||||||
@@ -14,23 +15,23 @@ export const dynamic = "force-dynamic";
|
|||||||
export async function GET(_req: Request) {
|
export async function GET(_req: Request) {
|
||||||
try {
|
try {
|
||||||
const data = await redisCache(apiCacheKey("home"), 15, async () => {
|
const data = await redisCache(apiCacheKey("home"), 15, async () => {
|
||||||
const [articles, online, hotelName] = await Promise.all([
|
const [articles, onlineRows, hotelName] = await Promise.all([
|
||||||
prisma.websiteArticles.findMany({
|
db
|
||||||
select: {
|
.select({
|
||||||
id: true,
|
id: WebsiteArticles.id,
|
||||||
title: true,
|
title: WebsiteArticles.title,
|
||||||
slug: true,
|
slug: WebsiteArticles.slug,
|
||||||
shortStory: true,
|
shortStory: WebsiteArticles.shortStory,
|
||||||
image: true,
|
image: WebsiteArticles.image,
|
||||||
createdAt: true,
|
createdAt: WebsiteArticles.createdAt,
|
||||||
},
|
})
|
||||||
orderBy: { createdAt: "desc" },
|
.from(WebsiteArticles)
|
||||||
take: 4,
|
.orderBy(desc(WebsiteArticles.createdAt))
|
||||||
}),
|
.limit(4),
|
||||||
prisma.user.count({ where: { online: "1" } }),
|
db.select({ total: count() }).from(User).where(eq(User.online, "1")),
|
||||||
resolveHotelName(),
|
resolveHotelName(),
|
||||||
]);
|
]);
|
||||||
return { articles, online, hotelName };
|
return { articles, online: onlineRows[0]?.total ?? 0, hotelName };
|
||||||
});
|
});
|
||||||
return apiJson(data);
|
return apiJson(data);
|
||||||
} catch {
|
} catch {
|
||||||
|
|||||||
@@ -1,8 +1,9 @@
|
|||||||
|
import { desc, eq, inArray } from "drizzle-orm";
|
||||||
import { apiJson } from "@/lib/api";
|
import { apiJson } from "@/lib/api";
|
||||||
import { prisma } from "@/lib/prisma";
|
import { db, User, UsersCurrency } from "@/lib/db";
|
||||||
|
|
||||||
// Public REST API — leaderboard. Mirrors src/app/leaderboard/page.tsx.
|
// Public REST API — leaderboard. Mirrors src/app/leaderboard/page.tsx.
|
||||||
// AtomCMS-faithful currency type ids (see prisma/schema.prisma UsersCurrency):
|
// AtomCMS-faithful currency type ids (see UsersCurrency):
|
||||||
// Credits = -1 (lives on users.credits), Duckets = 0, Diamonds = 5.
|
// Credits = -1 (lives on users.credits), Duckets = 0, Diamonds = 5.
|
||||||
export const dynamic = "force-dynamic";
|
export const dynamic = "force-dynamic";
|
||||||
|
|
||||||
@@ -16,11 +17,15 @@ const CURRENCY_TYPE: Record<Exclude<LeaderboardType, "credits">, number> = {
|
|||||||
type Row = { rank: number; username: string; look: string; value: number };
|
type Row = { rank: number; username: string; look: string; value: number };
|
||||||
|
|
||||||
async function loadCreditsRows(): Promise<Row[]> {
|
async function loadCreditsRows(): Promise<Row[]> {
|
||||||
const users = await prisma.user.findMany({
|
const users = await db
|
||||||
orderBy: { credits: "desc" },
|
.select({
|
||||||
take: 20,
|
username: User.username,
|
||||||
select: { username: true, look: true, credits: true },
|
look: User.look,
|
||||||
});
|
credits: User.credits,
|
||||||
|
})
|
||||||
|
.from(User)
|
||||||
|
.orderBy(desc(User.credits))
|
||||||
|
.limit(20);
|
||||||
return users.map((u, i) => ({
|
return users.map((u, i) => ({
|
||||||
rank: i + 1,
|
rank: i + 1,
|
||||||
username: u.username,
|
username: u.username,
|
||||||
@@ -30,18 +35,26 @@ async function loadCreditsRows(): Promise<Row[]> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function loadCurrencyRows(type: number): Promise<Row[]> {
|
async function loadCurrencyRows(type: number): Promise<Row[]> {
|
||||||
const top = await prisma.usersCurrency.findMany({
|
const top = await db
|
||||||
where: { type },
|
.select({
|
||||||
orderBy: { amount: "desc" },
|
userId: UsersCurrency.userId,
|
||||||
take: 20,
|
amount: UsersCurrency.amount,
|
||||||
select: { userId: true, amount: true },
|
})
|
||||||
});
|
.from(UsersCurrency)
|
||||||
|
.where(eq(UsersCurrency.type, type))
|
||||||
|
.orderBy(desc(UsersCurrency.amount))
|
||||||
|
.limit(20);
|
||||||
if (top.length === 0) return [];
|
if (top.length === 0) return [];
|
||||||
|
|
||||||
const users = await prisma.user.findMany({
|
const users = await db
|
||||||
where: { id: { in: top.map((t) => t.userId) } },
|
.select({ id: User.id, username: User.username, look: User.look })
|
||||||
select: { id: true, username: true, look: true },
|
.from(User)
|
||||||
});
|
.where(
|
||||||
|
inArray(
|
||||||
|
User.id,
|
||||||
|
top.map((t) => t.userId),
|
||||||
|
),
|
||||||
|
);
|
||||||
const byId = new Map(users.map((u) => [u.id, u]));
|
const byId = new Map(users.map((u) => [u.id, u]));
|
||||||
|
|
||||||
return top
|
return top
|
||||||
|
|||||||
+20
-18
@@ -1,13 +1,14 @@
|
|||||||
// Public REST API — the currently signed-in user.
|
// Public REST API — the currently signed-in user.
|
||||||
//
|
//
|
||||||
// Reads the NextAuth session, then re-queries prisma.user by the session id to
|
// Reads the NextAuth session, then re-queries users by the session id to
|
||||||
// return a safe field set (never password / auth_ticket / 2FA secrets / pincode
|
// return a safe field set (never password / auth_ticket / 2FA secrets / pincode
|
||||||
// / mail). Returns { user: null } only when unauthenticated or missing.
|
// / mail). Returns { user: null } only when unauthenticated or missing.
|
||||||
|
|
||||||
|
import { eq } from "drizzle-orm";
|
||||||
import { apiJson, apiUnavailable } from "@/lib/api";
|
import { apiJson, apiUnavailable } from "@/lib/api";
|
||||||
import { auth } from "@/lib/auth";
|
import { auth } from "@/lib/auth";
|
||||||
import { sessionUserId } from "@/lib/auth/session-user";
|
import { sessionUserId } from "@/lib/auth/session-user";
|
||||||
import { prisma } from "@/lib/prisma";
|
import { db, User } from "@/lib/db";
|
||||||
|
|
||||||
export const dynamic = "force-dynamic";
|
export const dynamic = "force-dynamic";
|
||||||
|
|
||||||
@@ -19,22 +20,23 @@ export async function GET(_req: Request) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const user = await prisma.user.findUnique({
|
const [user] = await db
|
||||||
where: { id },
|
.select({
|
||||||
select: {
|
id: User.id,
|
||||||
id: true,
|
username: User.username,
|
||||||
username: true,
|
look: User.look,
|
||||||
look: true,
|
motto: User.motto,
|
||||||
motto: true,
|
rank: User.rank,
|
||||||
rank: true,
|
credits: User.credits,
|
||||||
credits: true,
|
pixels: User.pixels,
|
||||||
pixels: true,
|
points: User.points,
|
||||||
points: true,
|
gender: User.gender,
|
||||||
gender: true,
|
online: User.online,
|
||||||
online: true,
|
accountCreated: User.accountCreated,
|
||||||
accountCreated: true,
|
})
|
||||||
},
|
.from(User)
|
||||||
});
|
.where(eq(User.id, id))
|
||||||
|
.limit(1);
|
||||||
|
|
||||||
if (!user) {
|
if (!user) {
|
||||||
return apiJson({ user: null });
|
return apiJson({ user: null });
|
||||||
|
|||||||
@@ -8,11 +8,13 @@
|
|||||||
// to the user via personal_access_tokens.tokenable_id (a BigInt). NOTE: the live
|
// to the user via personal_access_tokens.tokenable_id (a BigInt). NOTE: the live
|
||||||
// table has no expires_at column, so it is never read or written here.
|
// table has no expires_at column, so it is never read or written here.
|
||||||
|
|
||||||
|
import { and, desc, eq } from "drizzle-orm";
|
||||||
|
import type { ResultSetHeader } from "mysql2";
|
||||||
import { apiError, apiJson, apiUnavailable, positiveBigInt } from "@/lib/api";
|
import { apiError, apiJson, apiUnavailable, positiveBigInt } from "@/lib/api";
|
||||||
import { auth } from "@/lib/auth";
|
import { auth } from "@/lib/auth";
|
||||||
import { personalTokenScope } from "@/lib/auth/personal-token-scope";
|
import { personalTokenScope } from "@/lib/auth/personal-token-scope";
|
||||||
import { sessionUserId } from "@/lib/auth/session-user";
|
import { sessionUserId } from "@/lib/auth/session-user";
|
||||||
import { prisma } from "@/lib/prisma";
|
import { db, PersonalAccessTokens } from "@/lib/db";
|
||||||
|
|
||||||
export const dynamic = "force-dynamic";
|
export const dynamic = "force-dynamic";
|
||||||
|
|
||||||
@@ -26,11 +28,21 @@ export async function GET(_req: Request) {
|
|||||||
if (!id) return apiError("Unauthorized", 401);
|
if (!id) return apiError("Unauthorized", 401);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const tokens = await prisma.personalAccessTokens.findMany({
|
const scope = personalTokenScope(id);
|
||||||
where: personalTokenScope(id),
|
const tokens = await db
|
||||||
select: { id: true, name: true, lastUsedAt: true },
|
.select({
|
||||||
orderBy: { id: "desc" },
|
id: PersonalAccessTokens.id,
|
||||||
});
|
name: PersonalAccessTokens.name,
|
||||||
|
lastUsedAt: PersonalAccessTokens.lastUsedAt,
|
||||||
|
})
|
||||||
|
.from(PersonalAccessTokens)
|
||||||
|
.where(
|
||||||
|
and(
|
||||||
|
eq(PersonalAccessTokens.tokenableId, scope.tokenableId),
|
||||||
|
eq(PersonalAccessTokens.tokenableType, scope.tokenableType),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.orderBy(desc(PersonalAccessTokens.id));
|
||||||
// Never expose the token hash.
|
// Never expose the token hash.
|
||||||
return apiJson({ data: tokens });
|
return apiJson({ data: tokens });
|
||||||
} catch {
|
} catch {
|
||||||
@@ -50,10 +62,17 @@ export async function DELETE(req: Request) {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
// Scope the delete to the owner so users cannot revoke others' tokens.
|
// Scope the delete to the owner so users cannot revoke others' tokens.
|
||||||
const result = await prisma.personalAccessTokens.deleteMany({
|
const scope = personalTokenScope(id);
|
||||||
where: { id: parsedTokenId, ...personalTokenScope(id) },
|
const [result] = (await db
|
||||||
});
|
.delete(PersonalAccessTokens)
|
||||||
if (result.count === 0) {
|
.where(
|
||||||
|
and(
|
||||||
|
eq(PersonalAccessTokens.id, parsedTokenId),
|
||||||
|
eq(PersonalAccessTokens.tokenableId, scope.tokenableId),
|
||||||
|
eq(PersonalAccessTokens.tokenableType, scope.tokenableType),
|
||||||
|
),
|
||||||
|
)) as unknown as [ResultSetHeader];
|
||||||
|
if (!result.affectedRows) {
|
||||||
return apiError("Token not found", 404);
|
return apiError("Token not found", 404);
|
||||||
}
|
}
|
||||||
return apiJson({ ok: true });
|
return apiJson({ ok: true });
|
||||||
|
|||||||
@@ -3,18 +3,23 @@
|
|||||||
// `online` is the emulator's string flag "1" / "0" (see User model). Returns
|
// `online` is the emulator's string flag "1" / "0" (see User model). Returns
|
||||||
// { count: 0 } (never 500) on DB failure.
|
// { count: 0 } (never 500) on DB failure.
|
||||||
|
|
||||||
|
import { count, eq } from "drizzle-orm";
|
||||||
import { apiJson } from "@/lib/api";
|
import { apiJson } from "@/lib/api";
|
||||||
import { cached } from "@/lib/cache";
|
import { cached } from "@/lib/cache";
|
||||||
import { prisma } from "@/lib/prisma";
|
import { db, User } from "@/lib/db";
|
||||||
|
|
||||||
export const dynamic = "force-dynamic";
|
export const dynamic = "force-dynamic";
|
||||||
|
|
||||||
export async function GET(_req: Request) {
|
export async function GET(_req: Request) {
|
||||||
try {
|
try {
|
||||||
const count = await cached("online_count", 10_000, () =>
|
const result = await cached("online_count", 10_000, async () => {
|
||||||
prisma.user.count({ where: { online: "1" } }),
|
const [row] = await db
|
||||||
);
|
.select({ total: count() })
|
||||||
return apiJson({ count });
|
.from(User)
|
||||||
|
.where(eq(User.online, "1"));
|
||||||
|
return row?.total ?? 0;
|
||||||
|
});
|
||||||
|
return apiJson({ count: result });
|
||||||
} catch {
|
} catch {
|
||||||
return apiJson({ count: 0 });
|
return apiJson({ count: 0 });
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,23 +1,23 @@
|
|||||||
// Public REST API — list of currently-online users (username + look only).
|
// Public REST API — list of currently-online users (username + look only).
|
||||||
//
|
//
|
||||||
// `online` is stored by the emulator as the string "1" / "0" (see User model in
|
// `online` is stored by the emulator as the string "1" / "0" (see User model).
|
||||||
// prisma/schema.prisma). Capped at 100 rows. Returns empty data (never 500) on
|
// Capped at 100 rows. Returns empty data (never 500) on DB failure.
|
||||||
// DB failure.
|
|
||||||
|
|
||||||
|
import { eq } from "drizzle-orm";
|
||||||
import { apiJson } from "@/lib/api";
|
import { apiJson } from "@/lib/api";
|
||||||
import { cached } from "@/lib/cache";
|
import { cached } from "@/lib/cache";
|
||||||
import { prisma } from "@/lib/prisma";
|
import { db, User } from "@/lib/db";
|
||||||
|
|
||||||
export const dynamic = "force-dynamic";
|
export const dynamic = "force-dynamic";
|
||||||
|
|
||||||
export async function GET(_req: Request) {
|
export async function GET(_req: Request) {
|
||||||
try {
|
try {
|
||||||
const users = await cached("online_users", 10_000, async () =>
|
const users = await cached("online_users", 10_000, async () =>
|
||||||
prisma.user.findMany({
|
db
|
||||||
where: { online: "1" },
|
.select({ username: User.username, look: User.look })
|
||||||
select: { username: true, look: true },
|
.from(User)
|
||||||
take: 100,
|
.where(eq(User.online, "1"))
|
||||||
}),
|
.limit(100),
|
||||||
);
|
);
|
||||||
|
|
||||||
return apiJson({ users });
|
return apiJson({ users });
|
||||||
|
|||||||
@@ -1,9 +1,11 @@
|
|||||||
|
import { and, eq } from "drizzle-orm";
|
||||||
|
import type { ResultSetHeader } from "mysql2";
|
||||||
import { NextResponse } from "next/server";
|
import { NextResponse } from "next/server";
|
||||||
import { auth } from "@/lib/auth";
|
import { auth } from "@/lib/auth";
|
||||||
import { sessionUserId } from "@/lib/auth/session-user";
|
import { sessionUserId } from "@/lib/auth/session-user";
|
||||||
|
import { db, WebsitePaypalTransactions } from "@/lib/db";
|
||||||
import { resolveHotelName } from "@/lib/hotel-name";
|
import { resolveHotelName } from "@/lib/hotel-name";
|
||||||
import { logger } from "@/lib/logger";
|
import { logger } from "@/lib/logger";
|
||||||
import { prisma } from "@/lib/prisma";
|
|
||||||
import { logServerError } from "@/lib/server-log";
|
import { logServerError } from "@/lib/server-log";
|
||||||
import {
|
import {
|
||||||
type CaptureResult,
|
type CaptureResult,
|
||||||
@@ -18,7 +20,7 @@ import {
|
|||||||
TopupCaptureError,
|
TopupCaptureError,
|
||||||
} from "@/lib/services/paypal-topup";
|
} from "@/lib/services/paypal-topup";
|
||||||
import { rcon } from "@/lib/services/rcon";
|
import { rcon } from "@/lib/services/rcon";
|
||||||
import { sendCurrency } from "@/lib/services/send-currency";
|
import { currencyDb, sendCurrency } from "@/lib/services/send-currency";
|
||||||
|
|
||||||
export const dynamic = "force-dynamic";
|
export const dynamic = "force-dynamic";
|
||||||
|
|
||||||
@@ -79,11 +81,18 @@ export async function POST(req: Request): Promise<Response> {
|
|||||||
authorized = await authorizeTopupCapture(
|
authorized = await authorizeTopupCapture(
|
||||||
userId,
|
userId,
|
||||||
orderId,
|
orderId,
|
||||||
async (transactionId) =>
|
async (transactionId) => {
|
||||||
prisma.websitePaypalTransactions.findFirst({
|
const [row] = await db
|
||||||
where: { transactionId },
|
.select({
|
||||||
select: { userId: true, status: true, amount: true },
|
userId: WebsitePaypalTransactions.userId,
|
||||||
}),
|
status: WebsitePaypalTransactions.status,
|
||||||
|
amount: WebsitePaypalTransactions.amount,
|
||||||
|
})
|
||||||
|
.from(WebsitePaypalTransactions)
|
||||||
|
.where(eq(WebsitePaypalTransactions.transactionId, transactionId))
|
||||||
|
.limit(1);
|
||||||
|
return row ?? null;
|
||||||
|
},
|
||||||
);
|
);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (error instanceof TopupCaptureError) {
|
if (error instanceof TopupCaptureError) {
|
||||||
@@ -103,21 +112,30 @@ export async function POST(req: Request): Promise<Response> {
|
|||||||
const amount = authorized.amount ?? 0;
|
const amount = authorized.amount ?? 0;
|
||||||
const credits = Math.floor(amount * creditsPerUnit());
|
const credits = Math.floor(amount * creditsPerUnit());
|
||||||
try {
|
try {
|
||||||
await claimTopupDelivery(() =>
|
await claimTopupDelivery(async () => {
|
||||||
prisma.websitePaypalTransactions.updateMany({
|
const [result] = (await db
|
||||||
where: {
|
.update(WebsitePaypalTransactions)
|
||||||
userId,
|
.set({ status: "CREDIT_DELIVERING", updatedAt: new Date() })
|
||||||
transactionId: orderId,
|
.where(
|
||||||
status: "CAPTURED_PENDING_CREDIT",
|
and(
|
||||||
},
|
eq(WebsitePaypalTransactions.userId, userId),
|
||||||
data: { status: "CREDIT_DELIVERING", updatedAt: new Date() },
|
eq(WebsitePaypalTransactions.transactionId, orderId),
|
||||||
}),
|
eq(WebsitePaypalTransactions.status, "CAPTURED_PENDING_CREDIT"),
|
||||||
);
|
),
|
||||||
await sendCurrency({ rcon, db: prisma }, userId, "credits", credits);
|
)) as unknown as [ResultSetHeader];
|
||||||
await prisma.websitePaypalTransactions.updateMany({
|
return { count: result.affectedRows };
|
||||||
where: { userId, transactionId: orderId, status: "CREDIT_DELIVERING" },
|
|
||||||
data: { status: "COMPLETED", updatedAt: new Date() },
|
|
||||||
});
|
});
|
||||||
|
await sendCurrency({ rcon, db: currencyDb }, userId, "credits", credits);
|
||||||
|
await db
|
||||||
|
.update(WebsitePaypalTransactions)
|
||||||
|
.set({ status: "COMPLETED", updatedAt: new Date() })
|
||||||
|
.where(
|
||||||
|
and(
|
||||||
|
eq(WebsitePaypalTransactions.userId, userId),
|
||||||
|
eq(WebsitePaypalTransactions.transactionId, orderId),
|
||||||
|
eq(WebsitePaypalTransactions.status, "CREDIT_DELIVERING"),
|
||||||
|
),
|
||||||
|
);
|
||||||
return NextResponse.json({
|
return NextResponse.json({
|
||||||
ok: true,
|
ok: true,
|
||||||
recovered: true,
|
recovered: true,
|
||||||
@@ -162,17 +180,23 @@ export async function POST(req: Request): Promise<Response> {
|
|||||||
if (result.status !== "COMPLETED") {
|
if (result.status !== "COMPLETED") {
|
||||||
// Record the non-completed attempt so support can trace it.
|
// Record the non-completed attempt so support can trace it.
|
||||||
try {
|
try {
|
||||||
await prisma.websitePaypalTransactions.updateMany({
|
await db
|
||||||
where: { userId, transactionId: orderId, status: "CREATED" },
|
.update(WebsitePaypalTransactions)
|
||||||
data: {
|
.set({
|
||||||
status: result.status,
|
status: result.status,
|
||||||
description: `${hotelName} top-up (not completed)`,
|
description: `${hotelName} top-up (not completed)`,
|
||||||
amount: result.amount,
|
amount: result.amount,
|
||||||
currency: result.currency,
|
currency: result.currency,
|
||||||
createdAt: new Date(),
|
createdAt: new Date(),
|
||||||
updatedAt: new Date(),
|
updatedAt: new Date(),
|
||||||
},
|
})
|
||||||
});
|
.where(
|
||||||
|
and(
|
||||||
|
eq(WebsitePaypalTransactions.userId, userId),
|
||||||
|
eq(WebsitePaypalTransactions.transactionId, orderId),
|
||||||
|
eq(WebsitePaypalTransactions.status, "CREATED"),
|
||||||
|
),
|
||||||
|
);
|
||||||
} catch {
|
} catch {
|
||||||
/* best-effort logging */
|
/* best-effort logging */
|
||||||
}
|
}
|
||||||
@@ -187,18 +211,24 @@ export async function POST(req: Request): Promise<Response> {
|
|||||||
// Record the transaction BEFORE crediting so a crash mid-grant can't be
|
// Record the transaction BEFORE crediting so a crash mid-grant can't be
|
||||||
// reprocessed into a double credit (the idempotency check above keys on this).
|
// reprocessed into a double credit (the idempotency check above keys on this).
|
||||||
try {
|
try {
|
||||||
const claimed = await prisma.websitePaypalTransactions.updateMany({
|
const [claimed] = (await db
|
||||||
where: { userId, transactionId: orderId, status: "CREATED" },
|
.update(WebsitePaypalTransactions)
|
||||||
data: {
|
.set({
|
||||||
status: "CAPTURED_PENDING_CREDIT",
|
status: "CAPTURED_PENDING_CREDIT",
|
||||||
description: `${hotelName} top-up: ${credits} credits`,
|
description: `${hotelName} top-up: ${credits} credits`,
|
||||||
amount: result.amount,
|
amount: result.amount,
|
||||||
currency: result.currency,
|
currency: result.currency,
|
||||||
createdAt: new Date(),
|
createdAt: new Date(),
|
||||||
updatedAt: new Date(),
|
updatedAt: new Date(),
|
||||||
},
|
})
|
||||||
});
|
.where(
|
||||||
if (claimed.count !== 1)
|
and(
|
||||||
|
eq(WebsitePaypalTransactions.userId, userId),
|
||||||
|
eq(WebsitePaypalTransactions.transactionId, orderId),
|
||||||
|
eq(WebsitePaypalTransactions.status, "CREATED"),
|
||||||
|
),
|
||||||
|
)) as unknown as [ResultSetHeader];
|
||||||
|
if (claimed.affectedRows !== 1)
|
||||||
throw new Error("Top-up order was already claimed");
|
throw new Error("Top-up order was already claimed");
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
logger.error("PayPal capture record failed", {
|
logger.error("PayPal capture record failed", {
|
||||||
@@ -218,21 +248,30 @@ export async function POST(req: Request): Promise<Response> {
|
|||||||
// external delivery outcome is ambiguous, CREDIT_DELIVERING remains visible
|
// external delivery outcome is ambiguous, CREDIT_DELIVERING remains visible
|
||||||
// for staff reconciliation instead of automatically risking a second grant.
|
// for staff reconciliation instead of automatically risking a second grant.
|
||||||
try {
|
try {
|
||||||
await claimTopupDelivery(() =>
|
await claimTopupDelivery(async () => {
|
||||||
prisma.websitePaypalTransactions.updateMany({
|
const [claimResult] = (await db
|
||||||
where: {
|
.update(WebsitePaypalTransactions)
|
||||||
userId,
|
.set({ status: "CREDIT_DELIVERING", updatedAt: new Date() })
|
||||||
transactionId: orderId,
|
.where(
|
||||||
status: "CAPTURED_PENDING_CREDIT",
|
and(
|
||||||
},
|
eq(WebsitePaypalTransactions.userId, userId),
|
||||||
data: { status: "CREDIT_DELIVERING", updatedAt: new Date() },
|
eq(WebsitePaypalTransactions.transactionId, orderId),
|
||||||
}),
|
eq(WebsitePaypalTransactions.status, "CAPTURED_PENDING_CREDIT"),
|
||||||
);
|
),
|
||||||
await sendCurrency({ rcon, db: prisma }, userId, "credits", credits);
|
)) as unknown as [ResultSetHeader];
|
||||||
await prisma.websitePaypalTransactions.updateMany({
|
return { count: claimResult.affectedRows };
|
||||||
where: { userId, transactionId: orderId, status: "CREDIT_DELIVERING" },
|
|
||||||
data: { status: "COMPLETED", updatedAt: new Date() },
|
|
||||||
});
|
});
|
||||||
|
await sendCurrency({ rcon, db: currencyDb }, userId, "credits", credits);
|
||||||
|
await db
|
||||||
|
.update(WebsitePaypalTransactions)
|
||||||
|
.set({ status: "COMPLETED", updatedAt: new Date() })
|
||||||
|
.where(
|
||||||
|
and(
|
||||||
|
eq(WebsitePaypalTransactions.userId, userId),
|
||||||
|
eq(WebsitePaypalTransactions.transactionId, orderId),
|
||||||
|
eq(WebsitePaypalTransactions.status, "CREDIT_DELIVERING"),
|
||||||
|
),
|
||||||
|
);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
logger.error("PayPal capture credit failed", {
|
logger.error("PayPal capture credit failed", {
|
||||||
module: "paypal/capture",
|
module: "paypal/capture",
|
||||||
|
|||||||
@@ -2,9 +2,9 @@ import { NextResponse } from "next/server";
|
|||||||
import { env } from "@/env";
|
import { env } from "@/env";
|
||||||
import { auth } from "@/lib/auth";
|
import { auth } from "@/lib/auth";
|
||||||
import { sessionUserId } from "@/lib/auth/session-user";
|
import { sessionUserId } from "@/lib/auth/session-user";
|
||||||
|
import { db, WebsitePaypalTransactions } from "@/lib/db";
|
||||||
import { resolveHotelName } from "@/lib/hotel-name";
|
import { resolveHotelName } from "@/lib/hotel-name";
|
||||||
import { logger } from "@/lib/logger";
|
import { logger } from "@/lib/logger";
|
||||||
import { prisma } from "@/lib/prisma";
|
|
||||||
import {
|
import {
|
||||||
createOrder,
|
createOrder,
|
||||||
creditsPerUnit,
|
creditsPerUnit,
|
||||||
@@ -87,7 +87,7 @@ export async function POST(req: Request): Promise<Response> {
|
|||||||
|
|
||||||
await recordCreatedTopup(
|
await recordCreatedTopup(
|
||||||
{ userId, orderId: order.id, amount, currency: PAYPAL_CURRENCY, credits },
|
{ userId, orderId: order.id, amount, currency: PAYPAL_CURRENCY, credits },
|
||||||
(data) => prisma.websitePaypalTransactions.create({ data }),
|
(data) => db.insert(WebsitePaypalTransactions).values(data),
|
||||||
);
|
);
|
||||||
|
|
||||||
return NextResponse.json({
|
return NextResponse.json({
|
||||||
|
|||||||
Reference in new issue
Block a user