refactor(db): migrate app pages and APIs from Prisma facade to Drizzle (3)

Co-authored-by: Cursor <[email protected]>
This commit is contained in:
SimoandCursor committed 2026-08-01 14:15:32 +02:00
1 parent 2cd0863cb8
commit 580972c0a0
40 files changed
+1089 -808

No files matched your search

+55 -38
View File
@@ -1,11 +1,12 @@
import { asc, count, eq, inArray } from "drizzle-orm";
import Link from "next/link"; import Link from "next/link";
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 { disbandGuild } from "@/actions/admin-guilds"; import { disbandGuild } from "@/actions/admin-guilds";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import { db, Guilds, GuildsForumsThreads, GuildsMembers, User } 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";
export default async function AdminGuildDetailPage({ export default async function AdminGuildDetailPage({
params, params,
@@ -23,49 +24,65 @@ export default async function AdminGuildDetailPage({
const id = Number(rawId); const id = Number(rawId);
if (!Number.isFinite(id) || id <= 0) notFound(); if (!Number.isFinite(id) || id <= 0) notFound();
const guild = await prisma.guilds.findUnique({ const [guild] = await db
where: { id }, .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, state: Guilds.state,
state: true, forum: Guilds.forum,
forum: true, })
}, .from(Guilds)
}); .where(eq(Guilds.id, id))
.limit(1);
if (!guild) notFound(); if (!guild) notFound();
const [owner, members, memberCount, threadCount] = await Promise.all([ const [ownerRows, members, memberCountRows, threadCountRows] =
prisma.user await Promise.all([
.findUnique({ db
where: { id: guild.userId }, .select({ id: User.id, username: User.username })
select: { id: true, username: true }, .from(User)
}) .where(eq(User.id, guild.userId))
.catch(() => null), .limit(1)
prisma.guildsMembers .catch(() => []),
.findMany({ db
where: { guildId: id }, .select({
orderBy: [{ levelId: "asc" }, { memberSince: "asc" }], userId: GuildsMembers.userId,
take: 100, levelId: GuildsMembers.levelId,
select: { userId: true, levelId: true, memberSince: true }, memberSince: GuildsMembers.memberSince,
}) })
.catch(() => []), .from(GuildsMembers)
prisma.guildsMembers.count({ where: { guildId: id } }).catch(() => 0), .where(eq(GuildsMembers.guildId, id))
prisma.guildsForumsThreads.count({ where: { guildId: id } }).catch(() => 0), .orderBy(asc(GuildsMembers.levelId), asc(GuildsMembers.memberSince))
]); .limit(100)
.catch(() => []),
db
.select({ total: count() })
.from(GuildsMembers)
.where(eq(GuildsMembers.guildId, id))
.catch(() => [{ total: 0 }]),
db
.select({ total: count() })
.from(GuildsForumsThreads)
.where(eq(GuildsForumsThreads.guildId, id))
.catch(() => [{ total: 0 }]),
]);
const owner = ownerRows[0] ?? null;
const memberCount = Number(memberCountRows[0]?.total ?? 0);
const threadCount = Number(threadCountRows[0]?.total ?? 0);
const memberUserIds = members.map((m) => m.userId).filter((uid) => uid > 0); const memberUserIds = members.map((m) => m.userId).filter((uid) => uid > 0);
const memberUsers = const memberUsers =
memberUserIds.length > 0 memberUserIds.length > 0
? await prisma.user ? await db
.findMany({ .select({ id: User.id, username: User.username })
where: { id: { in: memberUserIds } }, .from(User)
select: { id: true, username: true }, .where(inArray(User.id, memberUserIds))
})
.catch(() => []) .catch(() => [])
: []; : [];
const usernameById = new Map(memberUsers.map((u) => [u.id, u.username])); const usernameById = new Map(memberUsers.map((u) => [u.id, u.username]));
+42 -38
View File
@@ -1,3 +1,4 @@
import { count, desc, eq, inArray, like, or } from "drizzle-orm";
import { Shield } from "lucide-react"; import { Shield } from "lucide-react";
import Link from "next/link"; import Link from "next/link";
import { redirect } from "next/navigation"; import { redirect } from "next/navigation";
@@ -7,9 +8,9 @@ import { AdminSimplePager } from "@/components/admin/admin-simple-pager";
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 { calcPagination, parseListParams } from "@/lib/admin-helpers"; import { calcPagination, parseListParams } from "@/lib/admin-helpers";
import { db, Guilds, GuildsMembers, User } 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";
export default async function AdminGuildsPage({ export default async function AdminGuildsPage({
searchParams, searchParams,
@@ -40,34 +41,36 @@ export default async function AdminGuildsPage({
try { try {
const where = q const where = q
? { ? or(
OR: [ like(Guilds.name, `%${q}%`),
{ name: { contains: q } }, like(Guilds.description, `%${q}%`),
{ description: { contains: q } }, ...(Number.isFinite(Number(q)) && Number(q) > 0
...(Number.isFinite(Number(q)) && Number(q) > 0 ? [eq(Guilds.id, Number(q))]
? [{ id: Number(q) }] : []),
: []), )
], : undefined;
}
: {};
total = await prisma.guilds.count({ where }); const [totalRow] = await db
.select({ total: count() })
.from(Guilds)
.where(where);
total = Number(totalRow?.total ?? 0);
const pagination = calcPagination(total, parsed.page, parsed.perPage); const pagination = calcPagination(total, parsed.page, parsed.perPage);
rows = await prisma.guilds.findMany({ rows = await db
where, .select({
select: { id: Guilds.id,
id: true, name: Guilds.name,
name: true, description: Guilds.description,
description: true, userId: Guilds.userId,
userId: true, badge: Guilds.badge,
badge: true, dateCreated: Guilds.dateCreated,
dateCreated: true, roomId: Guilds.roomId,
roomId: true, })
}, .from(Guilds)
orderBy: { id: "desc" }, .where(where)
skip: pagination.offset, .orderBy(desc(Guilds.id))
take: pagination.perPage, .limit(pagination.perPage)
}); .offset(pagination.offset);
} catch { } catch {
loadError = true; loadError = true;
} }
@@ -80,27 +83,28 @@ export default async function AdminGuildsPage({
const [owners, memberGroups] = await Promise.all([ const [owners, memberGroups] = await Promise.all([
ownerIds.length ownerIds.length
? prisma.user ? db
.findMany({ .select({ id: User.id, username: User.username })
where: { id: { in: ownerIds } }, .from(User)
select: { id: true, username: true }, .where(inArray(User.id, ownerIds))
})
.catch(() => []) .catch(() => [])
: Promise.resolve([]), : Promise.resolve([]),
guildIds.length guildIds.length
? prisma.guildsMembers ? db
.groupBy({ .select({
by: ["guildId"], guildId: GuildsMembers.guildId,
where: { guildId: { in: guildIds } }, total: count(),
_count: { _all: true },
}) })
.from(GuildsMembers)
.where(inArray(GuildsMembers.guildId, guildIds))
.groupBy(GuildsMembers.guildId)
.catch(() => []) .catch(() => [])
: Promise.resolve([]), : Promise.resolve([]),
]); ]);
const ownerById = new Map(owners.map((u) => [u.id, u.username])); const ownerById = new Map(owners.map((u) => [u.id, u.username]));
const membersByGuild = new Map( const membersByGuild = new Map(
memberGroups.map((g) => [g.guildId, g._count._all]), memberGroups.map((g) => [g.guildId, Number(g.total)]),
); );
return ( return (
+9 -7
View File
@@ -1,10 +1,11 @@
import { eq } from "drizzle-orm";
import { HelpCircle } from "lucide-react"; import { HelpCircle } from "lucide-react";
import Link from "next/link"; import Link from "next/link";
import { notFound, redirect } from "next/navigation"; import { notFound, redirect } from "next/navigation";
import { deleteHelpQuestion, updateHelpQuestion } from "@/actions/admin-help"; import { deleteHelpQuestion, updateHelpQuestion } from "@/actions/admin-help";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import { db, WebsiteHelpCenterCategories } from "@/lib/db";
import { canAccess, getAdminContext, PERMS } from "@/lib/permissions"; import { canAccess, getAdminContext, PERMS } from "@/lib/permissions";
import { prisma } from "@/lib/prisma";
export default async function EditHelpQuestion({ export default async function EditHelpQuestion({
params, params,
@@ -20,13 +21,14 @@ export default async function EditHelpQuestion({
const { id } = await params; const { id } = await params;
const { error } = await searchParams; const { error } = await searchParams;
let entry: Awaited< let entry: typeof WebsiteHelpCenterCategories.$inferSelect | null = null;
ReturnType<typeof prisma.websiteHelpCenterCategories.findUnique>
> = null;
try { try {
entry = await prisma.websiteHelpCenterCategories.findUnique({ const [row] = await db
where: { id: BigInt(id) }, .select()
}); .from(WebsiteHelpCenterCategories)
.where(eq(WebsiteHelpCenterCategories.id, BigInt(id)))
.limit(1);
entry = row ?? null;
} catch { } catch {
notFound(); notFound();
} }
+11 -8
View File
@@ -1,10 +1,11 @@
import { asc } from "drizzle-orm";
import Link from "next/link"; import Link from "next/link";
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 { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import { db, WebsiteHelpCenterCategories } from "@/lib/db";
import { canAccess, getAdminContext, PERMS } from "@/lib/permissions"; import { canAccess, getAdminContext, PERMS } from "@/lib/permissions";
import { prisma } from "@/lib/prisma";
export default async function AdminHelpQuestions() { export default async function AdminHelpQuestions() {
const { session, permissions } = await getAdminContext(); const { session, permissions } = await getAdminContext();
@@ -13,14 +14,16 @@ export default async function AdminHelpQuestions() {
} }
const t = await getTranslations("pages.admin.helpCenter"); const t = await getTranslations("pages.admin.helpCenter");
let entries: Awaited< let entries: (typeof WebsiteHelpCenterCategories.$inferSelect)[] = [];
ReturnType<typeof prisma.websiteHelpCenterCategories.findMany>
> = [];
try { try {
entries = await prisma.websiteHelpCenterCategories.findMany({ entries = await db
orderBy: [{ position: "asc" }, { id: "asc" }], .select()
take: 500, .from(WebsiteHelpCenterCategories)
}); .orderBy(
asc(WebsiteHelpCenterCategories.position),
asc(WebsiteHelpCenterCategories.id),
)
.limit(500);
} catch { } catch {
entries = []; entries = [];
} }
+65 -45
View File
@@ -1,11 +1,19 @@
import { and, asc, eq, gt, inArray, or } 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 { positiveBigInt } from "@/lib/api"; import { positiveBigInt } from "@/lib/api";
import { activeBanWhere } from "@/lib/bans"; import { unixNow } from "@/lib/bans";
import {
Ban,
db,
User,
WebsiteHelpCenterCategories,
WebsiteHelpCenterTicketReplies,
WebsiteHelpCenterTickets,
} from "@/lib/db";
import { canAccess, getAdminContext, PERMS } from "@/lib/permissions"; import { canAccess, getAdminContext, PERMS } from "@/lib/permissions";
import { prisma } from "@/lib/prisma";
import { AdminHelpTicketDetail } from "./admin-help-ticket-detail"; import { AdminHelpTicketDetail } from "./admin-help-ticket-detail";
export default async function AdminHelpTicketDetailPage({ export default async function AdminHelpTicketDetailPage({
@@ -23,32 +31,33 @@ export default async function AdminHelpTicketDetailPage({
const ticketId = positiveBigInt(id); const ticketId = positiveBigInt(id);
if (!ticketId) notFound(); if (!ticketId) notFound();
const ticket = await prisma.websiteHelpCenterTickets.findUnique({ const [ticket] = await db
where: { id: ticketId }, .select({
select: { id: WebsiteHelpCenterTickets.id,
id: true, userId: WebsiteHelpCenterTickets.userId,
userId: true, categoryId: WebsiteHelpCenterTickets.categoryId,
categoryId: true, title: WebsiteHelpCenterTickets.title,
title: true, content: WebsiteHelpCenterTickets.content,
content: true, open: WebsiteHelpCenterTickets.open,
open: true, createdAt: WebsiteHelpCenterTickets.createdAt,
createdAt: true, updatedAt: WebsiteHelpCenterTickets.updatedAt,
updatedAt: true, })
}, .from(WebsiteHelpCenterTickets)
}); .where(eq(WebsiteHelpCenterTickets.id, ticketId))
.limit(1);
if (!ticket) notFound(); if (!ticket) notFound();
const replies = await prisma.websiteHelpCenterTicketReplies.findMany({ const replies = await db
where: { ticketId: ticket.id }, .select({
orderBy: { id: "asc" }, id: WebsiteHelpCenterTicketReplies.id,
select: { userId: WebsiteHelpCenterTicketReplies.userId,
id: true, content: WebsiteHelpCenterTicketReplies.content,
userId: true, createdAt: WebsiteHelpCenterTicketReplies.createdAt,
content: true, })
createdAt: true, .from(WebsiteHelpCenterTicketReplies)
}, .where(eq(WebsiteHelpCenterTicketReplies.ticketId, ticket.id))
}); .orderBy(asc(WebsiteHelpCenterTicketReplies.id));
const authorIds = [ const authorIds = [
...new Set([ ...new Set([
@@ -57,32 +66,43 @@ export default async function AdminHelpTicketDetailPage({
]), ]),
]; ];
const [users, category, activeBan] = await Promise.all([ const now = unixNow();
const [users, categoryRows, activeBanRows] = await Promise.all([
authorIds.length > 0 authorIds.length > 0
? prisma.user.findMany({ ? db
where: { id: { in: authorIds } }, .select({
select: { id: User.id,
id: true, username: User.username,
username: true, rank: User.rank,
rank: true, mail: User.mail,
mail: true, })
}, .from(User)
}) .where(inArray(User.id, authorIds))
: Promise.resolve([]), : Promise.resolve([]),
ticket.categoryId != null ticket.categoryId != null
? prisma.websiteHelpCenterCategories.findUnique({ ? db
where: { id: ticket.categoryId }, .select({ name: WebsiteHelpCenterCategories.name })
select: { name: true }, .from(WebsiteHelpCenterCategories)
}) .where(eq(WebsiteHelpCenterCategories.id, ticket.categoryId))
: Promise.resolve(null), .limit(1)
: Promise.resolve([]),
ticket.userId != null ticket.userId != null
? prisma.ban.findFirst({ ? db
where: { userId: ticket.userId, ...activeBanWhere() }, .select({ id: Ban.id })
select: { id: true }, .from(Ban)
}) .where(
: Promise.resolve(null), and(
eq(Ban.userId, ticket.userId),
or(eq(Ban.banExpire, 0), gt(Ban.banExpire, now)),
),
)
.limit(1)
: Promise.resolve([]),
]); ]);
const category = categoryRows[0] ?? null;
const activeBan = activeBanRows[0] ?? null;
const userById = new Map(users.map((u) => [u.id, u])); const userById = new Map(users.map((u) => [u.id, u]));
const openerId = ticket.userId; const openerId = ticket.userId;
const minStaffRank = await getMinStaffRank(); const minStaffRank = await getMinStaffRank();
+98 -73
View File
@@ -1,12 +1,28 @@
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 { 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,
WebsiteHelpCenterTicketReplies,
WebsiteHelpCenterTickets,
} 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 HelpTicketRow, HelpTicketsTable } from "./help-tickets-table"; import { type HelpTicketRow, HelpTicketsTable } from "./help-tickets-table";
export default async function AdminHelpTicketsPage({ export default async function AdminHelpTicketsPage({
@@ -24,87 +40,95 @@ export default async function AdminHelpTicketsPage({
const parsed = parseListParams(new URLSearchParams(raw)); const parsed = parseListParams(new URLSearchParams(raw));
const statusFilter = raw.filter_status || "open"; const statusFilter = raw.filter_status || "open";
const conditions: any[] = []; const conditions: SQL[] = [];
if (statusFilter === "open") { if (statusFilter === "open") {
conditions.push({ open: true }); conditions.push(eq(WebsiteHelpCenterTickets.open, true));
} else if (statusFilter === "closed") { } else if (statusFilter === "closed") {
conditions.push({ open: false }); conditions.push(eq(WebsiteHelpCenterTickets.open, false));
} }
if (parsed.search.trim()) { if (parsed.search.trim()) {
const q = parsed.search.trim(); const q = parsed.search.trim();
const asId = /^\d+$/.test(q) ? BigInt(q) : null; const asId = /^\d+$/.test(q) ? BigInt(q) : null;
const matchingUsers = await prisma.user const matchingUsers = await db
.findMany({ .select({ id: User.id })
where: { username: { contains: q } }, .from(User)
select: { id: true }, .where(like(User.username, `%${q}%`))
take: 50, .limit(50)
})
.catch(() => []); .catch(() => []);
conditions.push({ const searchOr = [
OR: [ like(WebsiteHelpCenterTickets.title, `%${q}%`),
{ title: { contains: q } }, like(WebsiteHelpCenterTickets.content, `%${q}%`),
{ content: { contains: q } }, ...(matchingUsers.length > 0
...(matchingUsers.length > 0 ? [
? [{ userId: { in: matchingUsers.map((u) => u.id) } }] inArray(
: []), WebsiteHelpCenterTickets.userId,
...(asId !== null ? [{ id: asId }] : []), matchingUsers.map((u) => u.id),
], ),
}); ]
: []),
...(asId !== null ? [eq(WebsiteHelpCenterTickets.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 =
title: { title: "asc" }, parsed.sort === "title"
open: { open: "desc" }, ? WebsiteHelpCenterTickets.title
user: { userId: "asc" }, : parsed.sort === "open"
date: { createdAt: "desc" }, ? WebsiteHelpCenterTickets.open
updated: { updatedAt: "desc" }, : parsed.sort === "user"
id: { id: "desc" }, ? WebsiteHelpCenterTickets.userId
}; : parsed.sort === "date"
? WebsiteHelpCenterTickets.createdAt
: parsed.sort === "id"
? WebsiteHelpCenterTickets.id
: WebsiteHelpCenterTickets.updatedAt;
const orderBy = parsed.order === "asc" ? asc(orderCol) : desc(orderCol);
const baseOrder = SORT_MAP[parsed.sort ?? "updated"] ?? { updatedAt: "desc" }; const [totalRows, openRows, closedRows, queues] = await Promise.all([
const orderField = Object.keys(baseOrder)[0] as keyof typeof baseOrder; db
const orderBy = { .select({ total: count() })
[orderField]: parsed.order, .from(WebsiteHelpCenterTickets)
} as any; .where(where)
.catch(() => [{ total: 0 }]),
const [total, openCount, closedCount, queues] = await Promise.all([ db
prisma.websiteHelpCenterTickets.count({ where }).catch(() => 0), .select({ total: count() })
prisma.websiteHelpCenterTickets .from(WebsiteHelpCenterTickets)
.count({ where: { open: true } }) .where(eq(WebsiteHelpCenterTickets.open, true))
.catch(() => 0), .catch(() => [{ total: 0 }]),
prisma.websiteHelpCenterTickets db
.count({ where: { open: false } }) .select({ total: count() })
.catch(() => 0), .from(WebsiteHelpCenterTickets)
.where(eq(WebsiteHelpCenterTickets.open, false))
.catch(() => [{ total: 0 }]),
fetchTicketQueueOpenCounts(), fetchTicketQueueOpenCounts(),
]); ]);
const total = Number(totalRows[0]?.total ?? 0);
const openCount = Number(openRows[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.websiteHelpCenterTickets const tickets = await db
.findMany({ .select({
where, id: WebsiteHelpCenterTickets.id,
orderBy, userId: WebsiteHelpCenterTickets.userId,
skip: pagination.offset, title: WebsiteHelpCenterTickets.title,
take: pagination.perPage, open: WebsiteHelpCenterTickets.open,
select: { createdAt: WebsiteHelpCenterTickets.createdAt,
id: true, updatedAt: WebsiteHelpCenterTickets.updatedAt,
userId: true,
title: true,
open: true,
createdAt: true,
updatedAt: true,
},
}) })
.from(WebsiteHelpCenterTickets)
.where(where)
.orderBy(orderBy)
.limit(pagination.perPage)
.offset(pagination.offset)
.catch(() => []); .catch(() => []);
const ticketIds = tickets.map((ticket) => ticket.id); const ticketIds = tickets.map((ticket) => ticket.id);
@@ -118,26 +142,27 @@ export default async function AdminHelpTicketsPage({
const [replyGroups, users] = await Promise.all([ const [replyGroups, users] = await Promise.all([
ticketIds.length > 0 ticketIds.length > 0
? prisma.websiteHelpCenterTicketReplies ? db
.groupBy({ .select({
by: ["ticketId"], ticketId: WebsiteHelpCenterTicketReplies.ticketId,
where: { ticketId: { in: ticketIds } }, total: count(),
_count: true,
}) })
.from(WebsiteHelpCenterTicketReplies)
.where(inArray(WebsiteHelpCenterTicketReplies.ticketId, ticketIds))
.groupBy(WebsiteHelpCenterTicketReplies.ticketId)
.catch(() => []) .catch(() => [])
: Promise.resolve([]), : Promise.resolve([]),
userIds.length > 0 userIds.length > 0
? prisma.user ? db
.findMany({ .select({ id: User.id, username: User.username })
where: { id: { in: userIds } }, .from(User)
select: { id: true, username: true }, .where(inArray(User.id, userIds))
})
.catch(() => []) .catch(() => [])
: Promise.resolve([]), : Promise.resolve([]),
]); ]);
const replyCountByTicket = new Map( const replyCountByTicket = new Map(
replyGroups.map((g) => [String(g.ticketId), g._count]), replyGroups.map((g) => [String(g.ticketId), Number(g.total)]),
); );
const usernameById = new Map(users.map((u) => [u.id, u.username])); const usernameById = new Map(users.map((u) => [u.id, u.username]));
+40 -22
View File
@@ -1,8 +1,15 @@
import { asc, eq } from "drizzle-orm";
import Link from "next/link"; import Link from "next/link";
import { redirect } from "next/navigation"; import { redirect } from "next/navigation";
import { getTranslations } from "next-intl/server"; import { getTranslations } from "next-intl/server";
import {
AclModelPermission,
AclPermission,
AclRole,
db,
WebsiteHousekeepingPermissions,
} from "@/lib/db";
import { canAccess, getAdminContext, PERMS } from "@/lib/permissions"; import { canAccess, getAdminContext, PERMS } from "@/lib/permissions";
import { prisma } from "@/lib/prisma";
import { getAuditLogs } from "@/lib/services/audit"; import { getAuditLogs } from "@/lib/services/audit";
import { AuditSection, ExportSection } from "./client"; import { AuditSection, ExportSection } from "./client";
@@ -31,12 +38,14 @@ export default async function AdminHousekeeping({
const sp = await searchParams; const sp = await searchParams;
const tab = sp.tab ?? "overview"; const tab = sp.tab ?? "overview";
type HKPerm = Awaited< const allPermissions = await db
ReturnType<typeof prisma.websiteHousekeepingPermissions.findMany> .select()
>[0]; .from(WebsiteHousekeepingPermissions)
const allPermissions: HKPerm[] = await prisma.websiteHousekeepingPermissions .orderBy(
.findMany({ orderBy: [{ groupName: "asc" }, { permission: "asc" }] }) asc(WebsiteHousekeepingPermissions.groupName),
.catch(() => []); asc(WebsiteHousekeepingPermissions.permission),
)
.catch((): (typeof WebsiteHousekeepingPermissions.$inferSelect)[] => []);
const ranks = allPermissions.map((p) => p.minRank); const ranks = allPermissions.map((p) => p.minRank);
const maxRank = ranks.length ? Math.max(...ranks) : 0; const maxRank = ranks.length ? Math.max(...ranks) : 0;
@@ -48,34 +57,43 @@ export default async function AdminHousekeeping({
groupCounts[g] = (groupCounts[g] || 0) + 1; groupCounts[g] = (groupCounts[g] || 0) + 1;
} }
const rankGroups: Record<string, typeof allPermissions> = {}; const rankGroups: Record<
string,
(typeof WebsiteHousekeepingPermissions.$inferSelect)[]
> = {};
for (const p of allPermissions) { for (const p of allPermissions) {
if (!rankGroups[p.minRank]) rankGroups[p.minRank] = []; const rk = String(p.minRank);
rankGroups[p.minRank].push(p); if (!rankGroups[rk]) rankGroups[rk] = [];
rankGroups[rk].push(p);
} }
const aclSlugs = await prisma.aclPermission const aclSlugs = await db
.findMany({ select: { slug: true } }) .select({ slug: AclPermission.slug })
.from(AclPermission)
.catch(() => [] as { slug: string }[]); .catch(() => [] as { slug: string }[]);
const aclSlugSet = new Set(aclSlugs.map((p) => p.slug)); const aclSlugSet = new Set(aclSlugs.map((p) => p.slug));
const aclRolePerms = await prisma.aclModelPermission const aclRolePerms = await db
.findMany({ .select({
where: { modelType: "Role" }, roleSlug: AclRole.slug,
include: { permissionSlug: AclPermission.slug,
permission: { select: { slug: true } },
role: { select: { slug: true } },
},
}) })
.catch(() => []); .from(AclModelPermission)
.innerJoin(
AclPermission,
eq(AclModelPermission.permissionId, AclPermission.id),
)
.innerJoin(AclRole, eq(AclModelPermission.modelId, AclRole.id))
.where(eq(AclModelPermission.modelType, "Role"))
.catch((): { roleSlug: string; permissionSlug: string }[] => []);
const aclMinRankBySlug = new Map<string, number>(); const aclMinRankBySlug = new Map<string, number>();
for (const row of aclRolePerms) { for (const row of aclRolePerms) {
const roleSlug = row.role.slug; const roleSlug = row.roleSlug;
if (!roleSlug.startsWith("rank_")) continue; if (!roleSlug.startsWith("rank_")) continue;
const rank = Number(roleSlug.slice(5)); const rank = Number(roleSlug.slice(5));
if (!Number.isFinite(rank)) continue; if (!Number.isFinite(rank)) continue;
const slug = row.permission.slug; const slug = row.permissionSlug;
const prev = aclMinRankBySlug.get(slug); const prev = aclMinRankBySlug.get(slug);
if (prev === undefined || rank < prev) aclMinRankBySlug.set(slug, rank); if (prev === undefined || rank < prev) aclMinRankBySlug.set(slug, rank);
} }
+12 -9
View File
@@ -1,3 +1,4 @@
import { desc } 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,9 +9,9 @@ import {
} from "@/actions/admin-ip"; } from "@/actions/admin-ip";
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, WebsiteIpBlacklist, WebsiteIpWhitelist } 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";
type IpRow = { type IpRow = {
id: bigint; id: bigint;
@@ -30,19 +31,21 @@ export default async function AdminIp() {
let blacklist: IpRow[] = []; let blacklist: IpRow[] = [];
try { try {
whitelist = await prisma.websiteIpWhitelist.findMany({ whitelist = await db
orderBy: { id: "desc" }, .select()
take: 200, .from(WebsiteIpWhitelist)
}); .orderBy(desc(WebsiteIpWhitelist.id))
.limit(200);
} catch { } catch {
whitelist = []; whitelist = [];
} }
try { try {
blacklist = await prisma.websiteIpBlacklist.findMany({ blacklist = await db
orderBy: { id: "desc" }, .select()
take: 200, .from(WebsiteIpBlacklist)
}); .orderBy(desc(WebsiteIpBlacklist.id))
.limit(200);
} catch { } catch {
blacklist = []; blacklist = [];
} }
+7 -2
View File
@@ -1,9 +1,10 @@
import { eq } from "drizzle-orm";
import { Package } from "lucide-react"; import { Package } from "lucide-react";
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 { AdminPageShell } from "@/components/admin/admin-page-shell"; import { AdminPageShell } from "@/components/admin/admin-page-shell";
import { db, ItemsBase } from "@/lib/db";
import { canAccess, getAdminContext, PERMS } from "@/lib/permissions"; import { canAccess, getAdminContext, PERMS } from "@/lib/permissions";
import { prisma } from "@/lib/prisma";
import { ItemsBaseEditForm } from "./items-base-edit-form"; import { ItemsBaseEditForm } from "./items-base-edit-form";
export default async function ItemsBaseDetailPage({ export default async function ItemsBaseDetailPage({
@@ -21,7 +22,11 @@ export default async function ItemsBaseDetailPage({
const id = Number(rawId); const id = Number(rawId);
if (!Number.isFinite(id) || id <= 0) notFound(); if (!Number.isFinite(id) || id <= 0) notFound();
const item = await prisma.itemsBase.findUnique({ where: { id } }); const [item] = await db
.select()
.from(ItemsBase)
.where(eq(ItemsBase.id, id))
.limit(1);
if (!item) notFound(); if (!item) notFound();
const canEdit = canAccess(permissions, PERMS.CATALOG_EDIT, session.user.rank); const canEdit = canAccess(permissions, PERMS.CATALOG_EDIT, session.user.rank);
+8 -6
View File
@@ -1,3 +1,4 @@
import { eq } from "drizzle-orm";
import { LogOut } from "lucide-react"; import { LogOut } from "lucide-react";
import Link from "next/link"; import Link from "next/link";
import { redirect } from "next/navigation"; import { redirect } from "next/navigation";
@@ -15,9 +16,9 @@ import {
ADMIN_NAV_CONFIG_KEY, ADMIN_NAV_CONFIG_KEY,
parseAdminNavConfig, parseAdminNavConfig,
} from "@/lib/admin-nav-config"; } from "@/lib/admin-nav-config";
import { db, User } from "@/lib/db";
import { setCsrfCookie } from "@/lib/foundation/security"; import { setCsrfCookie } from "@/lib/foundation/security";
import { canAccess, getAdminContext, PERMS } from "@/lib/permissions"; import { canAccess, getAdminContext, PERMS } from "@/lib/permissions";
import { prisma } from "@/lib/prisma";
import { siteSettings } from "@/lib/services/site-settings"; import { siteSettings } from "@/lib/services/site-settings";
// Request-time auth: requireStaff, CSRF cookie, and optional 2FA gate cannot be // Request-time auth: requireStaff, CSRF cookie, and optional 2FA gate cannot be
@@ -37,11 +38,12 @@ export default async function AdminLayout({
csrfToken = ""; csrfToken = "";
} }
if (await siteSettings.getBool("force_staff_2fa", false)) { if (await siteSettings.getBool("force_staff_2fa", false)) {
const u = await prisma.user const u = await db
.findUnique({ .select({ twoFactorConfirmedAt: User.twoFactorConfirmedAt })
where: { id: staff.id }, .from(User)
select: { twoFactorConfirmedAt: true }, .where(eq(User.id, staff.id))
}) .limit(1)
.then((rows) => rows[0] ?? null)
.catch(() => null); .catch(() => null);
if (!u?.twoFactorConfirmedAt) redirect("/settings/2fa?error=staffrequired"); if (!u?.twoFactorConfirmedAt) redirect("/settings/2fa?error=staffrequired");
} }
+37 -31
View File
@@ -1,7 +1,8 @@
import { sql } from "drizzle-orm";
import { redirect } from "next/navigation"; import { redirect } from "next/navigation";
import { calcPagination, parseListParams } from "@/lib/admin-helpers"; import { calcPagination, parseListParams } from "@/lib/admin-helpers";
import { db } from "@/lib/db";
import { canAccess, getAdminContext, PERMS } from "@/lib/permissions"; import { canAccess, getAdminContext, PERMS } from "@/lib/permissions";
import { prisma } from "@/lib/prisma";
type ListParamsInput = { type ListParamsInput = {
searchParams: Promise<Record<string, string>>; searchParams: Promise<Record<string, string>>;
@@ -25,6 +26,11 @@ function parsePageParams(
return { search: parsed.search.trim(), page: parsed.page, perPage }; return { search: parsed.search.trim(), page: parsed.page, perPage };
} }
async function rawRows<T>(query: ReturnType<typeof sql>): Promise<T[]> {
const [rows] = (await db.execute(query)) as unknown as [T[], unknown];
return rows ?? [];
}
export type ChatLogListRow = { export type ChatLogListRow = {
id: number; id: number;
username: string; username: string;
@@ -51,35 +57,35 @@ export async function loadChatLogList({
username: string | null; username: string | null;
}; };
const [rawRows, countRows] = search const [rawRowsResult, countRows] = search
? await Promise.all([ ? await Promise.all([
prisma.$queryRaw<RawRow[]>` rawRows<RawRow>(sql`
SELECT c.room_id AS roomId, c.message, c.timestamp, u.username AS username SELECT c.room_id AS roomId, c.message, c.timestamp, u.username AS username
FROM chatlogs_room c FROM chatlogs_room c
LEFT JOIN users u ON u.id = c.user_from_id LEFT JOIN users u ON u.id = c.user_from_id
WHERE c.message LIKE ${like} OR u.username LIKE ${like} WHERE c.message LIKE ${like} OR u.username LIKE ${like}
ORDER BY c.timestamp DESC ORDER BY c.timestamp DESC
LIMIT ${perPage} OFFSET ${offset}`, LIMIT ${perPage} OFFSET ${offset}`),
prisma.$queryRaw<{ count: bigint }[]>` rawRows<{ count: bigint }>(sql`
SELECT COUNT(*) AS count SELECT COUNT(*) AS count
FROM chatlogs_room c FROM chatlogs_room c
LEFT JOIN users u ON u.id = c.user_from_id LEFT JOIN users u ON u.id = c.user_from_id
WHERE c.message LIKE ${like} OR u.username LIKE ${like}`, WHERE c.message LIKE ${like} OR u.username LIKE ${like}`),
]) ])
: await Promise.all([ : await Promise.all([
prisma.$queryRaw<RawRow[]>` rawRows<RawRow>(sql`
SELECT c.room_id AS roomId, c.message, c.timestamp, u.username AS username SELECT c.room_id AS roomId, c.message, c.timestamp, u.username AS username
FROM chatlogs_room c FROM chatlogs_room c
LEFT JOIN users u ON u.id = c.user_from_id LEFT JOIN users u ON u.id = c.user_from_id
ORDER BY c.timestamp DESC ORDER BY c.timestamp DESC
LIMIT ${perPage} OFFSET ${offset}`, LIMIT ${perPage} OFFSET ${offset}`),
prisma.$queryRaw<{ count: bigint }[]>` rawRows<{ count: bigint }>(sql`
SELECT COUNT(*) AS count FROM chatlogs_room`, SELECT COUNT(*) AS count FROM chatlogs_room`),
]); ]);
const total = Number(countRows[0]?.count ?? 0); const total = Number(countRows[0]?.count ?? 0);
const pagination = calcPagination(total, page, perPage); const pagination = calcPagination(total, page, perPage);
const rows: ChatLogListRow[] = rawRows.map((row, index) => ({ const rows: ChatLogListRow[] = rawRowsResult.map((row, index) => ({
id: pagination.offset + index + 1, id: pagination.offset + index + 1,
username: row.username ?? "-", username: row.username ?? "-",
message: row.message, message: row.message,
@@ -116,9 +122,9 @@ export async function loadCommandLogList({
username: string | null; username: string | null;
}; };
const [rawRows, countRows] = search const [rawRowsResult, countRows] = search
? await Promise.all([ ? await Promise.all([
prisma.$queryRaw<RawRow[]>` rawRows<RawRow>(sql`
SELECT c.command, c.params, c.timestamp, u.username AS username SELECT c.command, c.params, c.timestamp, u.username AS username
FROM commandlogs c FROM commandlogs c
LEFT JOIN users u ON u.id = c.user_id LEFT JOIN users u ON u.id = c.user_id
@@ -126,29 +132,29 @@ export async function loadCommandLogList({
OR c.params LIKE ${like} OR c.params LIKE ${like}
OR u.username LIKE ${like} OR u.username LIKE ${like}
ORDER BY c.timestamp DESC ORDER BY c.timestamp DESC
LIMIT ${perPage} OFFSET ${offset}`, LIMIT ${perPage} OFFSET ${offset}`),
prisma.$queryRaw<{ count: bigint }[]>` rawRows<{ count: bigint }>(sql`
SELECT COUNT(*) AS count SELECT COUNT(*) AS count
FROM commandlogs c FROM commandlogs c
LEFT JOIN users u ON u.id = c.user_id LEFT JOIN users u ON u.id = c.user_id
WHERE c.command LIKE ${like} WHERE c.command LIKE ${like}
OR c.params LIKE ${like} OR c.params LIKE ${like}
OR u.username LIKE ${like}`, OR u.username LIKE ${like}`),
]) ])
: await Promise.all([ : await Promise.all([
prisma.$queryRaw<RawRow[]>` rawRows<RawRow>(sql`
SELECT c.command, c.params, c.timestamp, u.username AS username SELECT c.command, c.params, c.timestamp, u.username AS username
FROM commandlogs c FROM commandlogs c
LEFT JOIN users u ON u.id = c.user_id LEFT JOIN users u ON u.id = c.user_id
ORDER BY c.timestamp DESC ORDER BY c.timestamp DESC
LIMIT ${perPage} OFFSET ${offset}`, LIMIT ${perPage} OFFSET ${offset}`),
prisma.$queryRaw<{ count: bigint }[]>` rawRows<{ count: bigint }>(sql`
SELECT COUNT(*) AS count FROM commandlogs`, SELECT COUNT(*) AS count FROM commandlogs`),
]); ]);
const total = Number(countRows[0]?.count ?? 0); const total = Number(countRows[0]?.count ?? 0);
const pagination = calcPagination(total, page, perPage); const pagination = calcPagination(total, page, perPage);
const rows: CommandLogListRow[] = rawRows.map((row, index) => ({ const rows: CommandLogListRow[] = rawRowsResult.map((row, index) => ({
id: pagination.offset + index + 1, id: pagination.offset + index + 1,
username: row.username ?? "-", username: row.username ?? "-",
command: row.command, command: row.command,
@@ -185,9 +191,9 @@ export async function loadTradeLogList({
userTwoName: string | null; userTwoName: string | null;
}; };
const [rawRows, countRows] = search const [rawRowsResult, countRows] = search
? await Promise.all([ ? await Promise.all([
prisma.$queryRaw<RawRow[]>` rawRows<RawRow>(sql`
SELECT t.id, t.timestamp, SELECT t.id, t.timestamp,
u1.username AS userOneName, u2.username AS userTwoName u1.username AS userOneName, u2.username AS userTwoName
FROM room_trade_log t FROM room_trade_log t
@@ -195,30 +201,30 @@ export async function loadTradeLogList({
LEFT JOIN users u2 ON u2.id = t.user_two_id LEFT JOIN users u2 ON u2.id = t.user_two_id
WHERE u1.username LIKE ${like} OR u2.username LIKE ${like} WHERE u1.username LIKE ${like} OR u2.username LIKE ${like}
ORDER BY t.timestamp DESC ORDER BY t.timestamp DESC
LIMIT ${perPage} OFFSET ${offset}`, LIMIT ${perPage} OFFSET ${offset}`),
prisma.$queryRaw<{ count: bigint }[]>` rawRows<{ count: bigint }>(sql`
SELECT COUNT(*) AS count SELECT COUNT(*) AS count
FROM room_trade_log t FROM room_trade_log t
LEFT JOIN users u1 ON u1.id = t.user_one_id LEFT JOIN users u1 ON u1.id = t.user_one_id
LEFT JOIN users u2 ON u2.id = t.user_two_id LEFT JOIN users u2 ON u2.id = t.user_two_id
WHERE u1.username LIKE ${like} OR u2.username LIKE ${like}`, WHERE u1.username LIKE ${like} OR u2.username LIKE ${like}`),
]) ])
: await Promise.all([ : await Promise.all([
prisma.$queryRaw<RawRow[]>` rawRows<RawRow>(sql`
SELECT t.id, t.timestamp, SELECT t.id, t.timestamp,
u1.username AS userOneName, u2.username AS userTwoName u1.username AS userOneName, u2.username AS userTwoName
FROM room_trade_log t FROM room_trade_log t
LEFT JOIN users u1 ON u1.id = t.user_one_id LEFT JOIN users u1 ON u1.id = t.user_one_id
LEFT JOIN users u2 ON u2.id = t.user_two_id LEFT JOIN users u2 ON u2.id = t.user_two_id
ORDER BY t.timestamp DESC ORDER BY t.timestamp DESC
LIMIT ${perPage} OFFSET ${offset}`, LIMIT ${perPage} OFFSET ${offset}`),
prisma.$queryRaw<{ count: bigint }[]>` rawRows<{ count: bigint }>(sql`
SELECT COUNT(*) AS count FROM room_trade_log`, SELECT COUNT(*) AS count FROM room_trade_log`),
]); ]);
const total = Number(countRows[0]?.count ?? 0); const total = Number(countRows[0]?.count ?? 0);
const pagination = calcPagination(total, page, perPage); const pagination = calcPagination(total, page, perPage);
const rows: TradeLogListRow[] = rawRows.map((row) => ({ const rows: TradeLogListRow[] = rawRowsResult.map((row) => ({
id: row.id, id: row.id,
userOneName: row.userOneName ?? "-", userOneName: row.userOneName ?? "-",
userTwoName: row.userTwoName ?? "-", userTwoName: row.userTwoName ?? "-",
+57 -36
View File
@@ -1,20 +1,29 @@
import {
and,
asc,
count,
desc,
inArray,
like,
type SQL,
sql,
} from "drizzle-orm";
import { ClipboardList } from "lucide-react"; import { ClipboardList } 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 { StatusCard } from "@/components/admin/dashboard"; import { StatusCard } from "@/components/admin/dashboard";
import { buildStaffActivityWhere } from "@/lib/admin/log-filters";
import { calcPagination, parseListParams } from "@/lib/admin-helpers"; import { calcPagination, parseListParams } from "@/lib/admin-helpers";
import { db, StaffActivities, User } 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 { StaffActivitiesTable } from "./staff-activities-table"; import { StaffActivitiesTable } from "./staff-activities-table";
const SORT_MAP: Record<string, "createdAt" | "userId" | "action"> = { const SORT_MAP = {
when: "createdAt", when: StaffActivities.createdAt,
staff: "userId", staff: StaffActivities.userId,
action: "action", action: StaffActivities.action,
}; } as const;
export default async function AdminStaffActivities({ export default async function AdminStaffActivities({
searchParams, searchParams,
@@ -30,36 +39,49 @@ export default async function AdminStaffActivities({
const raw = await searchParams; const raw = await searchParams;
const parsed = parseListParams(new URLSearchParams(raw)); const parsed = parseListParams(new URLSearchParams(raw));
const authorizationOnly = raw.filter_authorization === "1"; const authorizationOnly = raw.filter_authorization === "1";
const whereClause = buildStaffActivityWhere({
q: parsed.search,
authorizationOnly,
});
const sortField = SORT_MAP[parsed.sort ?? ""] ?? "createdAt"; const conditions: SQL[] = [];
if (parsed.search.trim()) {
const q = parsed.search.trim();
conditions.push(
sql`(${like(StaffActivities.action, `%${q}%`)} OR ${like(StaffActivities.description, `%${q}%`)} OR ${like(StaffActivities.ipAddress, `%${q}%`)})`,
);
}
if (authorizationOnly) {
conditions.push(like(StaffActivities.action, "permission.%"));
}
const where = conditions.length > 0 ? and(...conditions) : undefined;
const total = await prisma.staffActivities const sortCol =
.count({ where: whereClause }) SORT_MAP[(parsed.sort ?? "") as keyof typeof SORT_MAP] ??
.catch(() => 0); StaffActivities.createdAt;
const orderBy = parsed.order === "asc" ? asc(sortCol) : desc(sortCol);
const [totalRows] = await db
.select({ total: count() })
.from(StaffActivities)
.where(where)
.catch(() => [{ total: 0 }]);
const total = Number(totalRows?.total ?? 0);
const pagination = calcPagination(total, parsed.page, parsed.perPage); const pagination = calcPagination(total, parsed.page, parsed.perPage);
const activities = await prisma.staffActivities const activities = await db
.findMany({ .select({
where: whereClause, id: StaffActivities.id,
select: { userId: StaffActivities.userId,
id: true, action: StaffActivities.action,
userId: true, description: StaffActivities.description,
action: true, targetType: StaffActivities.targetType,
description: true, targetId: StaffActivities.targetId,
targetType: true, ipAddress: StaffActivities.ipAddress,
targetId: true, createdAt: StaffActivities.createdAt,
ipAddress: true,
createdAt: true,
},
orderBy: { [sortField]: parsed.order },
skip: pagination.offset,
take: pagination.perPage,
}) })
.from(StaffActivities)
.where(where)
.orderBy(orderBy)
.limit(pagination.perPage)
.offset(pagination.offset)
.catch(() => []); .catch(() => []);
const staffIds = Array.from( const staffIds = Array.from(
@@ -67,11 +89,10 @@ export default async function AdminStaffActivities({
).filter((n) => n > 0); ).filter((n) => n > 0);
let nameById = new Map<number, string>(); let nameById = new Map<number, string>();
if (staffIds.length) { if (staffIds.length) {
const users = await prisma.user const users = 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(() => []);
nameById = new Map(users.map((x) => [x.id, x.username])); nameById = new Map(users.map((x) => [x.id, x.username]));
} }
+6 -5
View File
@@ -1,10 +1,11 @@
import { inArray } 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 { saveMaintenance } from "@/actions/admin-maintenance"; import { saveMaintenance } from "@/actions/admin-maintenance";
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, WebsiteSetting } from "@/lib/db";
import { canAccess, getAdminContext, PERMS } from "@/lib/permissions"; import { canAccess, getAdminContext, PERMS } from "@/lib/permissions";
import { prisma } from "@/lib/prisma";
const KEYS = [ const KEYS = [
"maintenance_enabled", "maintenance_enabled",
@@ -22,10 +23,10 @@ export default async function AdminMaintenance() {
let rows: { key: string; value: string }[] = []; let rows: { key: string; value: string }[] = [];
try { try {
rows = await prisma.websiteSetting.findMany({ rows = await db
where: { key: { in: [...KEYS] } }, .select({ key: WebsiteSetting.key, value: WebsiteSetting.value })
select: { key: true, value: true }, .from(WebsiteSetting)
}); .where(inArray(WebsiteSetting.key, [...KEYS]));
} catch { } catch {
rows = []; rows = [];
} }
+28 -18
View File
@@ -1,3 +1,4 @@
import { count, desc, eq, inArray } from "drizzle-orm";
import { Store } from "lucide-react"; import { Store } from "lucide-react";
import Link from "next/link"; import Link from "next/link";
import { redirect } from "next/navigation"; import { redirect } from "next/navigation";
@@ -8,9 +9,9 @@ import { AdminSimplePager } from "@/components/admin/admin-simple-pager";
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 { calcPagination, parseListParams } from "@/lib/admin-helpers"; import { calcPagination, parseListParams } from "@/lib/admin-helpers";
import { db, MarketplaceItems, User } 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";
export default async function AdminMarketplacePage({ export default async function AdminMarketplacePage({
searchParams, searchParams,
@@ -28,27 +29,37 @@ export default async function AdminMarketplacePage({
const parsed = parseListParams(new URLSearchParams(raw)); const parsed = parseListParams(new URLSearchParams(raw));
const filter = raw.filter === "sold" ? "sold" : "active"; const filter = raw.filter === "sold" ? "sold" : "active";
const where = filter === "sold" ? { state: 2 } : { state: 1 }; const where = eq(MarketplaceItems.state, filter === "sold" ? 2 : 1);
let total = 0; let total = 0;
let rows: Awaited<ReturnType<typeof prisma.marketplaceItems.findMany>> = []; let rows: (typeof MarketplaceItems.$inferSelect)[] = [];
let activeCount = 0; let activeCount = 0;
let soldCount = 0; let soldCount = 0;
let loadError = false; let loadError = false;
try { try {
[total, activeCount, soldCount] = await Promise.all([ const [totalRows, activeRows, soldRows] = await Promise.all([
prisma.marketplaceItems.count({ where }), db.select({ total: count() }).from(MarketplaceItems).where(where),
prisma.marketplaceItems.count({ where: { state: 1 } }), db
prisma.marketplaceItems.count({ where: { state: 2 } }), .select({ total: count() })
.from(MarketplaceItems)
.where(eq(MarketplaceItems.state, 1)),
db
.select({ total: count() })
.from(MarketplaceItems)
.where(eq(MarketplaceItems.state, 2)),
]); ]);
total = Number(totalRows[0]?.total ?? 0);
activeCount = Number(activeRows[0]?.total ?? 0);
soldCount = Number(soldRows[0]?.total ?? 0);
const pagination = calcPagination(total, parsed.page, parsed.perPage); const pagination = calcPagination(total, parsed.page, parsed.perPage);
rows = await prisma.marketplaceItems.findMany({ rows = await db
where, .select()
orderBy: { timestamp: "desc" }, .from(MarketplaceItems)
skip: pagination.offset, .where(where)
take: pagination.perPage, .orderBy(desc(MarketplaceItems.timestamp))
}); .limit(pagination.perPage)
.offset(pagination.offset);
} catch { } catch {
loadError = true; loadError = true;
} }
@@ -59,11 +70,10 @@ export default async function AdminMarketplacePage({
]; ];
const users = const users =
userIds.length > 0 userIds.length > 0
? await prisma.user ? await db
.findMany({ .select({ id: User.id, username: User.username })
where: { id: { in: userIds } }, .from(User)
select: { id: true, username: true }, .where(inArray(User.id, userIds))
})
.catch(() => []) .catch(() => [])
: []; : [];
const usernameById = new Map(users.map((u) => [u.id, u.username])); const usernameById = new Map(users.map((u) => [u.id, u.username]));
+34 -25
View File
@@ -1,6 +1,7 @@
import { count, desc, eq, inArray } from "drizzle-orm";
import { notFound, redirect } from "next/navigation"; import { notFound, redirect } from "next/navigation";
import { Ban, db, SupportTickets, User } from "@/lib/db";
import { canAccess, getAdminContext, PERMS } from "@/lib/permissions"; import { canAccess, getAdminContext, PERMS } from "@/lib/permissions";
import { prisma } from "@/lib/prisma";
import { CfhDetail } from "./cfh-detail"; import { CfhDetail } from "./cfh-detail";
export default async function CfhDetailPage({ export default async function CfhDetailPage({
@@ -16,9 +17,11 @@ export default async function CfhDetailPage({
const ticketId = Number(id); const ticketId = Number(id);
if (Number.isNaN(ticketId)) notFound(); if (Number.isNaN(ticketId)) notFound();
const ticket = await prisma.supportTickets.findUnique({ const [ticket] = await db
where: { id: ticketId }, .select()
}); .from(SupportTickets)
.where(eq(SupportTickets.id, ticketId))
.limit(1);
if (!ticket) notFound(); if (!ticket) notFound();
const canEdit = canAccess( const canEdit = canAccess(
@@ -30,36 +33,42 @@ export default async function CfhDetailPage({
// Fetch sender, reported, mod info // Fetch sender, reported, mod info
const userIds = [ticket.senderId, ticket.reportedId, ticket.modId].filter( const userIds = [ticket.senderId, ticket.reportedId, ticket.modId].filter(
Boolean, Boolean,
); ) as number[];
const users = const users =
userIds.length > 0 userIds.length > 0
? await prisma.user.findMany({ ? await db
where: { id: { in: userIds } }, .select({
select: { id: User.id,
id: true, username: User.username,
username: true, look: User.look,
look: true, rank: User.rank,
rank: true, online: User.online,
online: true, ipRegister: User.ipRegister,
ipRegister: true, mail: User.mail,
mail: true, })
}, .from(User)
}) .where(inArray(User.id, userIds))
: []; : [];
const userMap = Object.fromEntries(users.map((u) => [u.id, u])); const userMap = Object.fromEntries(users.map((u) => [u.id, u]));
// Get ban/mute history for reported user // Get ban/mute history for reported user
const reportedId = ticket.reportedId; const reportedId = ticket.reportedId;
const [banCount, recentBans] = reportedId const [banCountRows, recentBans] = reportedId
? await Promise.all([ ? await Promise.all([
prisma.ban.count({ where: { userId: reportedId } }), db
prisma.ban.findMany({ .select({ total: count() })
where: { userId: reportedId }, .from(Ban)
orderBy: { id: "desc" }, .where(eq(Ban.userId, reportedId)),
take: 5, db
}), .select()
.from(Ban)
.where(eq(Ban.userId, reportedId))
.orderBy(desc(Ban.id))
.limit(5),
]) ])
: [0, []]; : [[{ total: 0 }], []];
const banCount = Number(banCountRows[0]?.total ?? 0);
return ( return (
<CfhDetail <CfhDetail
+65 -54
View File
@@ -1,9 +1,20 @@
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 { calcPagination, parseListParams } from "@/lib/admin-helpers"; import { calcPagination, parseListParams } from "@/lib/admin-helpers";
import { db, SupportTickets, User } 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 CfhRow, CfhTable } from "./cfh-table"; import { type CfhRow, CfhTable } from "./cfh-table";
export default async function CfhListPage({ export default async function CfhListPage({
@@ -23,9 +34,9 @@ export default async function CfhListPage({
const stateFilter = const stateFilter =
stateRaw !== undefined && stateRaw !== "" ? Number(stateRaw) : -1; stateRaw !== undefined && stateRaw !== "" ? Number(stateRaw) : -1;
const conditions: any[] = []; const conditions: SQL[] = [];
if (stateFilter >= 0) { if (stateFilter >= 0) {
conditions.push({ state: stateFilter }); conditions.push(eq(SupportTickets.state, stateFilter));
} }
if (parsed.search.trim()) { if (parsed.search.trim()) {
@@ -33,63 +44,63 @@ export default async function CfhListPage({
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 = [];
} }
conditions.push({ const searchOr = [
OR: [ like(SupportTickets.issue, `%${q}%`),
{ issue: { contains: q } }, ...(Number.isFinite(asId) && asId > 0
...(Number.isFinite(asId) && asId > 0 ? [{ id: asId }] : []), ? [eq(SupportTickets.id, asId)]
...(userIds.length : []),
? [ ...(userIds.length
{ senderId: { in: userIds } }, ? [
{ reportedId: { in: userIds } }, inArray(SupportTickets.senderId, userIds),
{ modId: { in: userIds } }, inArray(SupportTickets.reportedId, userIds),
] inArray(SupportTickets.modId, userIds),
: []), ]
], : []),
}); ];
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 =
id: { id: "desc" }, parsed.sort === "issue"
issue: { issue: "asc" }, ? SupportTickets.issue
sender: { senderId: "asc" }, : parsed.sort === "sender"
reported: { reportedId: "asc" }, ? SupportTickets.senderId
mod: { modId: "asc" }, : parsed.sort === "reported"
state: { state: "asc" }, ? SupportTickets.reportedId
}; : parsed.sort === "mod"
? SupportTickets.modId
: parsed.sort === "state"
? SupportTickets.state
: SupportTickets.id;
const orderBy = parsed.order === "asc" ? asc(orderCol) : desc(orderCol);
const baseOrder = SORT_MAP[parsed.sort ?? "id"] ?? { id: "desc" }; const [totalRows] = await db
const orderField = Object.keys(baseOrder)[0] as keyof typeof baseOrder; .select({ total: count() })
const orderBy = { .from(SupportTickets)
[orderField]: parsed.order, .where(where)
} as any; .catch(() => [{ total: 0 }]);
const total = Number(totalRows?.total ?? 0);
const total = await prisma.supportTickets.count({ where }).catch(() => 0);
const pagination = calcPagination(total, parsed.page, parsed.perPage); const pagination = calcPagination(total, parsed.page, parsed.perPage);
const tickets = await prisma.supportTickets const tickets = await db
.findMany({ .select()
where, .from(SupportTickets)
orderBy, .where(where)
skip: pagination.offset, .orderBy(orderBy)
take: pagination.perPage, .limit(pagination.perPage)
}) .offset(pagination.offset)
.catch(() => []); .catch(() => []);
const userIds = [ const userIds = [
@@ -98,15 +109,15 @@ export default async function CfhListPage({
...tickets.map((x) => x.reportedId), ...tickets.map((x) => x.reportedId),
...tickets.map((x) => x.modId), ...tickets.map((x) => x.modId),
]), ]),
].filter(Boolean); ].filter(Boolean) as number[];
let userMap = new Map<number, string>(); let userMap = 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));
userMap = new Map(users.map((u) => [u.id, u.username])); userMap = new Map(users.map((u) => [u.id, u.username]));
} catch { } catch {
userMap = new Map(); userMap = new Map();
+54 -33
View File
@@ -1,11 +1,12 @@
import { and, count, desc, eq, gte, inArray, like } from "drizzle-orm";
import { import {
AlertTriangle, AlertTriangle,
Ban, Ban as BanIcon,
Clock, Clock,
Gavel, Gavel,
Radio, Radio,
Shield, Shield,
User, User as UserIcon,
} from "lucide-react"; } from "lucide-react";
import Link from "next/link"; import Link from "next/link";
import { redirect } from "next/navigation"; import { redirect } from "next/navigation";
@@ -13,8 +14,8 @@ import { StatsCard } from "@/components/admin/stats-card";
import { Badge } from "@/components/ui/badge"; import { Badge } from "@/components/ui/badge";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { getMinStaffRank } from "@/lib/admin/min-staff-rank"; import { getMinStaffRank } from "@/lib/admin/min-staff-rank";
import { AdminAuditLog, Ban, db, SupportTickets, User } from "@/lib/db";
import { canAccess, getAdminContext, PERMS } from "@/lib/permissions"; import { canAccess, getAdminContext, PERMS } from "@/lib/permissions";
import { prisma } from "@/lib/prisma";
const CFH_STATES: Record<number, { label: string; class: string }> = { const CFH_STATES: Record<number, { label: string; class: string }> = {
0: { 0: {
@@ -38,33 +39,53 @@ export default async function ModerationDashboard() {
const minStaffRank = await getMinStaffRank(); const minStaffRank = await getMinStaffRank();
const [ const [
openCfh, openCfhRows,
pickedCfh, pickedCfhRows,
todayBans, todayBansRows,
modsOnline, modsOnlineRows,
recentCfh, recentCfh,
recentModActions, recentModActions,
] = await Promise.all([ ] = await Promise.all([
prisma.supportTickets.count({ where: { state: 0 } }), db
prisma.supportTickets.count({ where: { state: 1 } }), .select({ total: count() })
prisma.ban.count({ where: { timestamp: { gte: todayStart } } }), .from(SupportTickets)
prisma.user.count({ .where(eq(SupportTickets.state, 0)),
where: { online: "1", rank: { gte: minStaffRank } }, db
}), .select({ total: count() })
prisma.supportTickets.findMany({ .from(SupportTickets)
orderBy: { id: "desc" }, .where(eq(SupportTickets.state, 1)),
take: 10, db
}), .select({ total: count() })
prisma.adminAuditLog.findMany({ .from(Ban)
where: { .where(gte(Ban.timestamp, todayStart)),
action: { startsWith: "mod_" }, db
}, .select({ total: count() })
orderBy: { id: "desc" }, .from(User)
take: 10, .where(and(eq(User.online, "1"), gte(User.rank, minStaffRank))),
include: { user: { select: { username: true } } }, db.select().from(SupportTickets).orderBy(desc(SupportTickets.id)).limit(10),
}), db
.select({
id: AdminAuditLog.id,
action: AdminAuditLog.action,
details: AdminAuditLog.details,
createdAt: AdminAuditLog.createdAt,
userId: AdminAuditLog.userId,
target: AdminAuditLog.target,
targetId: AdminAuditLog.targetId,
username: User.username,
})
.from(AdminAuditLog)
.leftJoin(User, eq(AdminAuditLog.userId, User.id))
.where(like(AdminAuditLog.action, "mod_%"))
.orderBy(desc(AdminAuditLog.id))
.limit(10),
]); ]);
const openCfh = Number(openCfhRows[0]?.total ?? 0);
const pickedCfh = Number(pickedCfhRows[0]?.total ?? 0);
const todayBans = Number(todayBansRows[0]?.total ?? 0);
const modsOnline = Number(modsOnlineRows[0]?.total ?? 0);
// Fetch usernames for CFH tickets // Fetch usernames for CFH tickets
const userIds = [ const userIds = [
...new Set([ ...new Set([
@@ -72,14 +93,14 @@ export default async function ModerationDashboard() {
...recentCfh.map((t) => t.reportedId), ...recentCfh.map((t) => t.reportedId),
...recentCfh.map((t) => t.modId), ...recentCfh.map((t) => t.modId),
]), ]),
].filter(Boolean); ].filter(Boolean) as number[];
const users = const users =
userIds.length > 0 userIds.length > 0
? await prisma.user.findMany({ ? 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))
: []; : [];
const userMap = new Map(users.map((u) => [u.id, u.username])); const userMap = new Map(users.map((u) => [u.id, u.username]));
@@ -97,7 +118,7 @@ export default async function ModerationDashboard() {
<StatsCard <StatsCard
title="Bans Today" title="Bans Today"
value={todayBans} value={todayBans}
icon={<Ban className="h-4 w-4" />} icon={<BanIcon className="h-4 w-4" />}
href="/admin/bans" href="/admin/bans"
/> />
<StatsCard <StatsCard
@@ -190,13 +211,13 @@ export default async function ModerationDashboard() {
key={log.id} key={log.id}
className="flex items-center gap-3 py-2.5 first:pt-0 last:pb-0" className="flex items-center gap-3 py-2.5 first:pt-0 last:pb-0"
> >
<User className="h-3.5 w-3.5 text-muted-foreground shrink-0" /> <UserIcon className="h-3.5 w-3.5 text-muted-foreground shrink-0" />
<div className="flex-1 min-w-0"> <div className="flex-1 min-w-0">
<p className="text-sm font-medium truncate"> <p className="text-sm font-medium truncate">
{log.action.replace("mod_", "").replace(/_/g, " ")} {log.action.replace("mod_", "").replace(/_/g, " ")}
</p> </p>
<p className="text-xs text-muted-foreground"> <p className="text-xs text-muted-foreground">
by {log.user?.username ?? "?"} · {log.target}{" "} by {log.username ?? "?"} · {log.target}{" "}
{log.targetId ? `#${log.targetId}` : ""} {log.targetId ? `#${log.targetId}` : ""}
</p> </p>
</div> </div>
+61 -35
View File
@@ -1,11 +1,18 @@
import { and, count, desc, gte, inArray, like } from "drizzle-orm";
import Link from "next/link"; import Link from "next/link";
import { redirect } from "next/navigation"; import { redirect } from "next/navigation";
import { AvatarImage } from "@/components/shared/avatar-image"; import { AvatarImage } from "@/components/shared/avatar-image";
import { Badge } from "@/components/ui/badge"; import { Badge } from "@/components/ui/badge";
import { Card, CardContent } from "@/components/ui/card"; import { Card, CardContent } from "@/components/ui/card";
import { getMinStaffRank } from "@/lib/admin/min-staff-rank"; import { getMinStaffRank } from "@/lib/admin/min-staff-rank";
import {
AdminAuditLog,
db,
SupportTickets,
User,
WebsiteTicket,
} from "@/lib/db";
import { canAccess, getAdminContext, PERMS } from "@/lib/permissions"; import { canAccess, getAdminContext, PERMS } from "@/lib/permissions";
import { prisma } from "@/lib/prisma";
export default async function ModTeamPage() { export default async function ModTeamPage() {
const { session, permissions } = await getAdminContext(); const { session, permissions } = await getAdminContext();
@@ -13,48 +20,67 @@ export default async function ModTeamPage() {
redirect("/admin"); redirect("/admin");
const minStaffRank = await getMinStaffRank(); const minStaffRank = await getMinStaffRank();
const staff = await prisma.user.findMany({ const staff = await db
where: { rank: { gte: minStaffRank } }, .select({
select: { id: User.id,
id: true, username: User.username,
username: true, look: User.look,
look: true, rank: User.rank,
rank: true, online: User.online,
online: true, lastLogin: User.lastLogin,
lastLogin: true, lastOnline: User.lastOnline,
lastOnline: true, })
}, .from(User)
orderBy: [{ online: "desc" }, { rank: "desc" }], .where(gte(User.rank, minStaffRank))
}); .orderBy(desc(User.online), desc(User.rank));
const staffIds = staff.map((s) => s.id);
// Get CFH ticket counts per mod and web ticket counts // Get CFH ticket counts per mod and web ticket counts
const [cfhCounts, webTicketCounts, modActionCounts] = await Promise.all([ const [cfhCounts, webTicketCounts, modActionCounts] = await Promise.all([
prisma.supportTickets.groupBy({ staffIds.length
by: ["modId"], ? db
where: { modId: { in: staff.map((s) => s.id) } }, .select({
_count: { _all: true }, modId: SupportTickets.modId,
}), total: count(),
prisma.websiteTicket.groupBy({ })
by: ["assigneeId"], .from(SupportTickets)
where: { assigneeId: { in: staff.map((s) => s.id) } }, .where(inArray(SupportTickets.modId, staffIds))
_count: { _all: true }, .groupBy(SupportTickets.modId)
}), : Promise.resolve([]),
prisma.adminAuditLog.groupBy({ staffIds.length
by: ["userId"], ? db
where: { .select({
userId: { in: staff.map((s) => s.id) }, assigneeId: WebsiteTicket.assigneeId,
action: { startsWith: "mod_" }, total: count(),
}, })
_count: { _all: true }, .from(WebsiteTicket)
}), .where(inArray(WebsiteTicket.assigneeId, staffIds))
.groupBy(WebsiteTicket.assigneeId)
: Promise.resolve([]),
staffIds.length
? db
.select({
userId: AdminAuditLog.userId,
total: count(),
})
.from(AdminAuditLog)
.where(
and(
inArray(AdminAuditLog.userId, staffIds),
like(AdminAuditLog.action, "mod_%"),
),
)
.groupBy(AdminAuditLog.userId)
: Promise.resolve([]),
]); ]);
const cfhMap = new Map(cfhCounts.map((c) => [c.modId, c._count._all])); const cfhMap = new Map(cfhCounts.map((c) => [c.modId, Number(c.total)]));
const webMap = new Map( const webMap = new Map(
webTicketCounts.map((c) => [c.assigneeId, c._count._all]), webTicketCounts.map((c) => [c.assigneeId, Number(c.total)]),
); );
const actionMap = new Map( const actionMap = new Map(
modActionCounts.map((c) => [c.userId, c._count._all]), modActionCounts.map((c) => [c.userId, Number(c.total)]),
); );
return ( return (
+29 -35
View File
@@ -1,11 +1,12 @@
import { asc, inArray, 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 { db, NavigatorFlatcats, NavigatorPubliccats, Rooms } from "@/lib/db";
import { canAccess, getAdminContext, PERMS } from "@/lib/permissions"; import { canAccess, getAdminContext, PERMS } from "@/lib/permissions";
import { prisma } from "@/lib/prisma";
// navigator_flatcats — emulator-owned flat (private-room) categories shown in the // navigator_flatcats — emulator-owned flat (private-room) categories shown in the
// navigator. The Prisma delegate exists because the table has a primary key. // navigator.
type Flatcat = { type Flatcat = {
id: number; id: number;
caption: string; caption: string;
@@ -18,16 +19,13 @@ type Flatcat = {
orderNum: number; orderNum: number;
}; };
// navigator_publics — emulator-owned public-room assignments. This table has no // navigator_publics — keyless table, read via raw SQL.
// primary/unique key, so Prisma marks the model `@@ignore` and generates NO
// delegate (prisma.navigatorPublics does not exist). It must be read via raw SQL.
type PublicRow = { type PublicRow = {
public_cat_id: number; public_cat_id: number;
room_id: number; room_id: number;
visible: string; visible: string;
}; };
// Reusable yes/no pill so emulator boolean-ish "1"/"0" columns read consistently.
function YesNo({ on }: { on: boolean }) { function YesNo({ on }: { on: boolean }) {
return ( return (
<span <span
@@ -47,48 +45,46 @@ export default async function AdminNavigation() {
const t = await getTranslations("pages.admin.navigation"); const t = await getTranslations("pages.admin.navigation");
let flatcats: Flatcat[] = []; let flatcats: Flatcat[] = [];
try { try {
flatcats = await prisma.navigatorFlatcats.findMany({ flatcats = await db
select: { .select({
id: true, id: NavigatorFlatcats.id,
caption: true, caption: NavigatorFlatcats.caption,
captionSave: true, captionSave: NavigatorFlatcats.captionSave,
minRank: true, minRank: NavigatorFlatcats.minRank,
maxUserCount: true, maxUserCount: NavigatorFlatcats.maxUserCount,
canTrade: true, canTrade: NavigatorFlatcats.canTrade,
public: true, public: NavigatorFlatcats.public,
listType: true, listType: NavigatorFlatcats.listType,
orderNum: true, orderNum: NavigatorFlatcats.orderNum,
}, })
orderBy: [{ orderNum: "asc" }, { id: "asc" }], .from(NavigatorFlatcats)
}); .orderBy(asc(NavigatorFlatcats.orderNum), asc(NavigatorFlatcats.id));
} catch { } catch {
flatcats = []; flatcats = [];
} }
// Raw read because navigator_publics is an `@@ignore`d (keyless) model.
let publics: PublicRow[] = []; let publics: PublicRow[] = [];
try { try {
publics = await prisma.$queryRaw<PublicRow[]>` const [rows] = (await db.execute(sql`
SELECT public_cat_id, room_id, visible SELECT public_cat_id, room_id, visible
FROM navigator_publics FROM navigator_publics
ORDER BY public_cat_id ASC, room_id ASC ORDER BY public_cat_id ASC, room_id ASC
`; `)) as unknown as [PublicRow[], unknown];
publics = rows ?? [];
} catch { } catch {
publics = []; publics = [];
} }
// Resolve the public-category names and room names referenced by the rows above
// so the read-only view is legible. Each lookup degrades to an empty map on error.
const catIds = Array.from(new Set(publics.map((p) => p.public_cat_id))); const catIds = Array.from(new Set(publics.map((p) => p.public_cat_id)));
const roomIds = Array.from(new Set(publics.map((p) => p.room_id))); const roomIds = Array.from(new Set(publics.map((p) => p.room_id)));
let catNameById = new Map<number, string>(); let catNameById = new Map<number, string>();
if (catIds.length) { if (catIds.length) {
try { try {
const cats = await prisma.navigatorPubliccats.findMany({ const cats = await db
where: { id: { in: catIds } }, .select({ id: NavigatorPubliccats.id, name: NavigatorPubliccats.name })
select: { id: true, name: true }, .from(NavigatorPubliccats)
}); .where(inArray(NavigatorPubliccats.id, catIds));
catNameById = new Map(cats.map((c) => [c.id, c.name])); catNameById = new Map(cats.map((c) => [c.id, c.name]));
} catch { } catch {
catNameById = new Map(); catNameById = new Map();
@@ -98,10 +94,10 @@ export default async function AdminNavigation() {
let roomNameById = new Map<number, string>(); let roomNameById = new Map<number, string>();
if (roomIds.length) { if (roomIds.length) {
try { try {
const rooms = await prisma.rooms.findMany({ const rooms = await db
where: { id: { in: roomIds } }, .select({ id: Rooms.id, name: Rooms.name })
select: { id: true, name: true }, .from(Rooms)
}); .where(inArray(Rooms.id, roomIds));
roomNameById = new Map(rooms.map((r) => [r.id, r.name])); roomNameById = new Map(rooms.map((r) => [r.id, r.name]));
} catch { } catch {
roomNameById = new Map(); roomNameById = new Map();
@@ -134,7 +130,6 @@ export default async function AdminNavigation() {
/> />
</div> </div>
{/* ── navigator_flatcats ───────────────────────────────── */}
<section className="mt-6"> <section className="mt-6">
<h2 className="admin-section-title">{t("flatCategories")}</h2> <h2 className="admin-section-title">{t("flatCategories")}</h2>
<p className="text-xs theme-text-muted dark:theme-text-muted"> <p className="text-xs theme-text-muted dark:theme-text-muted">
@@ -186,7 +181,6 @@ export default async function AdminNavigation() {
)} )}
</section> </section>
{/* ── navigator_publics ────────────────────────────────── */}
<section className="mt-6"> <section className="mt-6">
<h2 className="admin-section-title">{t("publicRooms")}</h2> <h2 className="admin-section-title">{t("publicRooms")}</h2>
<p className="text-xs theme-text-muted dark:theme-text-muted"> <p className="text-xs theme-text-muted dark:theme-text-muted">
+39 -27
View File
@@ -1,3 +1,4 @@
import { count, desc, eq, gt, inArray, or } from "drizzle-orm";
import { import {
Activity, Activity,
ArrowRight, ArrowRight,
@@ -9,57 +10,68 @@ import {
import Link from "next/link"; import Link from "next/link";
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 { activeBanWhere } from "@/lib/bans"; import { unixNow } from "@/lib/bans";
import {
Ban as BanTable,
db,
StaffActivities,
User,
WebsiteArticles,
} from "@/lib/db";
import { formatDate } from "@/lib/format-date"; import { formatDate } from "@/lib/format-date";
import { prisma } from "@/lib/prisma";
type RankCount = { rank: number; count: number }; type RankCount = { rank: number; count: number };
export default async function AdminDashboard() { export default async function AdminDashboard() {
const t = await getTranslations("pages.admin.dashboard"); const t = await getTranslations("pages.admin.dashboard");
const now = unixNow();
let users = -1; let users = -1;
let online = 0; let online = 0;
let articles = 0; let articles = 0;
let bans = 0; let bans = 0;
try { try {
[users, online, articles, bans] = await Promise.all([ const [usersRow, onlineRow, articlesRow, bansRow] = await Promise.all([
prisma.user.count(), db.select({ total: count() }).from(User),
prisma.user.count({ where: { online: "1" } }), db.select({ total: count() }).from(User).where(eq(User.online, "1")),
prisma.websiteArticles.count(), db.select({ total: count() }).from(WebsiteArticles),
prisma.ban.count({ db
where: activeBanWhere(), .select({ total: count() })
}), .from(BanTable)
.where(or(eq(BanTable.banExpire, 0), gt(BanTable.banExpire, now))),
]); ]);
users = usersRow[0]?.total ?? 0;
online = onlineRow[0]?.total ?? 0;
articles = articlesRow[0]?.total ?? 0;
bans = bansRow[0]?.total ?? 0;
} catch {} } catch {}
const dbOk = users >= 0; const dbOk = users >= 0;
const rankGroups = await prisma.user const rankGroups = await db
.groupBy({ .select({ rank: User.rank, total: count() })
by: ["rank"], .from(User)
_count: { _all: true }, .groupBy(User.rank)
orderBy: { rank: "desc" }, .orderBy(desc(User.rank))
}) .catch(() => [] as Array<{ rank: number; total: number }>);
.catch(() => [] as Array<{ rank: number; _count: { _all: number } }>);
const rankCounts: RankCount[] = rankGroups const rankCounts: RankCount[] = rankGroups
.map((g) => ({ rank: g.rank, count: g._count._all })) .map((g) => ({ rank: g.rank, count: Number(g.total) }))
.sort((a, b) => b.count - a.count); .sort((a, b) => b.count - a.count);
const maxRankCount = rankCounts.reduce((m, r) => Math.max(m, r.count), 0); const maxRankCount = rankCounts.reduce((m, r) => Math.max(m, r.count), 0);
const activity = await prisma.staffActivities const activity = await db
.findMany({ orderBy: { createdAt: "desc" }, take: 8 }) .select()
.catch( .from(StaffActivities)
() => [] as Awaited<ReturnType<typeof prisma.staffActivities.findMany>>, .orderBy(desc(StaffActivities.createdAt))
); .limit(8)
.catch(() => [] as (typeof StaffActivities.$inferSelect)[]);
const staffIds = Array.from( const staffIds = Array.from(
new Set(activity.map((a) => Number(a.userId))), new Set(activity.map((a) => Number(a.userId))),
).filter((n) => n > 0); ).filter((n) => n > 0);
let nameById = new Map<number, string>(); let nameById = new Map<number, string>();
if (staffIds.length) { if (staffIds.length) {
const u = await prisma.user const u = 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(() => []);
nameById = new Map(u.map((x) => [x.id, x.username])); nameById = new Map(u.map((x) => [x.id, x.username]));
} }
+18 -13
View File
@@ -1,3 +1,4 @@
import { count, desc, inArray } from "drizzle-orm";
import { Camera } from "lucide-react"; import { Camera } from "lucide-react";
import Link from "next/link"; import Link from "next/link";
import { redirect } from "next/navigation"; import { redirect } from "next/navigation";
@@ -7,8 +8,8 @@ import { AdminPageShell } from "@/components/admin/admin-page-shell";
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 { calcPagination, parseListParams } from "@/lib/admin-helpers"; import { calcPagination, parseListParams } from "@/lib/admin-helpers";
import { CameraWeb, db, User } from "@/lib/db";
import { canAccess, getAdminContext, PERMS } from "@/lib/permissions"; import { canAccess, getAdminContext, PERMS } from "@/lib/permissions";
import { prisma } from "@/lib/prisma";
export default async function AdminPhotos({ export default async function AdminPhotos({
searchParams, searchParams,
@@ -23,16 +24,21 @@ export default async function AdminPhotos({
const t = await getTranslations("pages.admin.photos"); const t = await getTranslations("pages.admin.photos");
const parsed = parseListParams(new URLSearchParams(await searchParams)); const parsed = parseListParams(new URLSearchParams(await searchParams));
const total = await prisma.cameraWeb.count().catch(() => 0); const [totalRow] = await db
.select({ total: count() })
.from(CameraWeb)
.catch(() => [{ total: 0 }]);
const total = Number(totalRow?.total ?? 0);
const pagination = calcPagination(total, parsed.page, parsed.perPage || 24); const pagination = calcPagination(total, parsed.page, parsed.perPage || 24);
let photos: Awaited<ReturnType<typeof prisma.cameraWeb.findMany>> = []; let photos: (typeof CameraWeb.$inferSelect)[] = [];
try { try {
photos = await prisma.cameraWeb.findMany({ photos = await db
orderBy: { timestamp: "desc" }, .select()
skip: pagination.offset, .from(CameraWeb)
take: pagination.perPage, .orderBy(desc(CameraWeb.timestamp))
}); .limit(pagination.perPage)
.offset(pagination.offset);
} catch { } catch {
photos = []; photos = [];
} }
@@ -40,11 +46,10 @@ export default async function AdminPhotos({
const userIds = [...new Set(photos.map((p) => p.userId))]; const userIds = [...new Set(photos.map((p) => p.userId))];
const users = const users =
userIds.length > 0 userIds.length > 0
? await prisma.user ? await db
.findMany({ .select({ id: User.id, username: User.username })
where: { id: { in: userIds } }, .from(User)
select: { id: true, username: true }, .where(inArray(User.id, userIds))
})
.catch(() => []) .catch(() => [])
: []; : [];
const usernameById = new Map(users.map((u) => [u.id, u.username])); const usernameById = new Map(users.map((u) => [u.id, u.username]));
+7 -6
View File
@@ -1,10 +1,11 @@
import { eq } 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 { AdminBreadcrumb } from "@/components/admin/admin-breadcrumb"; import { AdminBreadcrumb } from "@/components/admin/admin-breadcrumb";
import { Badge } from "@/components/ui/badge"; import { Badge } from "@/components/ui/badge";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { db, WebsitePoll } from "@/lib/db";
import { canAccess, getAdminContext, PERMS } from "@/lib/permissions"; import { canAccess, getAdminContext, PERMS } from "@/lib/permissions";
import { prisma } from "@/lib/prisma";
import { PollForm } from "../poll-form"; import { PollForm } from "../poll-form";
import { PollQuestionsManager } from "./poll-questions"; import { PollQuestionsManager } from "./poll-questions";
import { PollResultsView } from "./poll-results"; import { PollResultsView } from "./poll-results";
@@ -26,12 +27,12 @@ export default async function PollDetailPage({
const t = await getTranslations("pages.admin.polls"); const t = await getTranslations("pages.admin.polls");
const tHubs = await getTranslations("pages.admin.hubs"); const tHubs = await getTranslations("pages.admin.hubs");
const poll = await prisma.websitePoll.findUnique({ const poll = await db.query.WebsitePoll.findFirst({
where: { id: pollId }, where: eq(WebsitePoll.id, pollId),
include: { with: {
questions: { questions: {
orderBy: { sortOrder: "asc" }, orderBy: (q, { asc }) => [asc(q.sortOrder)],
include: { with: {
votes: true, votes: true,
}, },
}, },
+16 -16
View File
@@ -1,3 +1,4 @@
import { desc } from "drizzle-orm";
import { getTranslations } from "next-intl/server"; import { getTranslations } from "next-intl/server";
import { import {
createApiKey, createApiKey,
@@ -6,8 +7,8 @@ import {
} from "@/actions/admin-radio-api-keys"; } from "@/actions/admin-radio-api-keys";
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, RadioApiKeys } from "@/lib/db";
import { formatDate } from "@/lib/format-date"; import { formatDate } from "@/lib/format-date";
import { prisma } from "@/lib/prisma";
export const metadata = { title: "Radio API Keys" }; export const metadata = { title: "Radio API Keys" };
@@ -34,22 +35,21 @@ export default async function AdminRadioApiKeysPage() {
let dbError = false; let dbError = false;
try { try {
keys = await prisma.radioApiKeys keys = await db
.findMany({ .select({
select: { id: RadioApiKeys.id,
id: true, name: RadioApiKeys.name,
name: true, key: RadioApiKeys.key,
key: true, allowedIps: RadioApiKeys.allowedIps,
allowedIps: true, rateLimit: RadioApiKeys.rateLimit,
rateLimit: true, lastUsedAt: RadioApiKeys.lastUsedAt,
lastUsedAt: true, expiresAt: RadioApiKeys.expiresAt,
expiresAt: true, isActive: RadioApiKeys.isActive,
isActive: true, createdAt: RadioApiKeys.createdAt,
createdAt: true,
},
orderBy: [{ isActive: "desc" }, { id: "desc" }],
take: 500,
}) })
.from(RadioApiKeys)
.orderBy(desc(RadioApiKeys.isActive), desc(RadioApiKeys.id))
.limit(500)
.catch(() => { .catch(() => {
dbError = true; dbError = true;
return []; return [];
+19 -16
View File
@@ -1,3 +1,4 @@
import { asc } from "drizzle-orm";
import Image from "next/image"; import Image from "next/image";
import { getTranslations } from "next-intl/server"; import { getTranslations } from "next-intl/server";
import { import {
@@ -7,8 +8,8 @@ import {
} from "@/actions/admin-radio-autodj"; } from "@/actions/admin-radio-autodj";
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, RadioAutoDjPlaylist } from "@/lib/db";
import { formatDate } from "@/lib/format-date"; import { formatDate } from "@/lib/format-date";
import { prisma } from "@/lib/prisma";
export const metadata = { title: "Radio AutoDJ" }; export const metadata = { title: "Radio AutoDJ" };
@@ -39,22 +40,24 @@ export default async function AdminRadioAutoDjPage() {
let dbError = false; let dbError = false;
try { try {
tracks = await prisma.radioAutoDjPlaylist tracks = await db
.findMany({ .select({
select: { id: RadioAutoDjPlaylist.id,
id: true, title: RadioAutoDjPlaylist.title,
title: true, artist: RadioAutoDjPlaylist.artist,
artist: true, album: RadioAutoDjPlaylist.album,
album: true, artworkUrl: RadioAutoDjPlaylist.artworkUrl,
artworkUrl: true, duration: RadioAutoDjPlaylist.duration,
duration: true, playCount: RadioAutoDjPlaylist.playCount,
playCount: true, lastPlayedAt: RadioAutoDjPlaylist.lastPlayedAt,
lastPlayedAt: true, isActive: RadioAutoDjPlaylist.isActive,
isActive: true, sortOrder: RadioAutoDjPlaylist.sortOrder,
sortOrder: true,
},
orderBy: [{ sortOrder: "asc" }, { title: "asc" }],
}) })
.from(RadioAutoDjPlaylist)
.orderBy(
asc(RadioAutoDjPlaylist.sortOrder),
asc(RadioAutoDjPlaylist.title),
)
.catch(() => { .catch(() => {
dbError = true; dbError = true;
return []; return [];
+15 -14
View File
@@ -1,3 +1,4 @@
import { asc } from "drizzle-orm";
import NextImage from "next/image"; import NextImage from "next/image";
import { getTranslations } from "next-intl/server"; import { getTranslations } from "next-intl/server";
import { import {
@@ -7,8 +8,8 @@ import {
} from "@/actions/admin-radio-extra"; } from "@/actions/admin-radio-extra";
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, RadioBanners } from "@/lib/db";
import { formatDate } from "@/lib/format-date"; import { formatDate } from "@/lib/format-date";
import { prisma } from "@/lib/prisma";
type Banner = { type Banner = {
id: bigint; id: bigint;
@@ -28,19 +29,19 @@ export default async function AdminRadioBannersPage() {
let dbError = false; let dbError = false;
try { try {
banners = await prisma.radioBanners.findMany({ banners = await db
select: { .select({
id: true, id: RadioBanners.id,
userId: true, userId: RadioBanners.userId,
imagePath: true, imagePath: RadioBanners.imagePath,
title: true, title: RadioBanners.title,
description: true, description: RadioBanners.description,
sortOrder: true, sortOrder: RadioBanners.sortOrder,
isActive: true, isActive: RadioBanners.isActive,
createdAt: true, createdAt: RadioBanners.createdAt,
}, })
orderBy: [{ sortOrder: "asc" }, { id: "asc" }], .from(RadioBanners)
}); .orderBy(asc(RadioBanners.sortOrder), asc(RadioBanners.id));
} catch { } catch {
dbError = true; dbError = true;
banners = []; banners = [];
+19 -18
View File
@@ -1,7 +1,8 @@
import { desc, inArray } from "drizzle-orm";
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 { db, RadioHistory, User } from "@/lib/db";
import { formatDate } from "@/lib/format-date"; import { formatDate } from "@/lib/format-date";
import { prisma } from "@/lib/prisma";
type History = { type History = {
id: bigint; id: bigint;
@@ -31,28 +32,28 @@ export default async function AdminRadioHistoryPage() {
let dbError = false; let dbError = false;
try { try {
history = await prisma.radioHistory.findMany({ history = await db
select: { .select({
id: true, id: RadioHistory.id,
userId: true, userId: RadioHistory.userId,
showName: true, showName: RadioHistory.showName,
startedAt: true, startedAt: RadioHistory.startedAt,
endedAt: true, endedAt: RadioHistory.endedAt,
listenersCount: true, listenersCount: RadioHistory.listenersCount,
notes: true, notes: RadioHistory.notes,
}, })
orderBy: { startedAt: "desc" }, .from(RadioHistory)
take: 100, .orderBy(desc(RadioHistory.startedAt))
}); .limit(100);
const ids = Array.from( const ids = Array.from(
new Set(history.map((h) => Number(h.userId))), new Set(history.map((h) => Number(h.userId))),
).filter((n) => Number.isFinite(n) && n > 0); ).filter((n) => Number.isFinite(n) && n > 0);
if (ids.length > 0) { if (ids.length > 0) {
const users = await prisma.user.findMany({ const users = await db
where: { id: { in: ids } }, .select({ id: User.id, username: User.username })
select: { id: true, username: true }, .from(User)
}); .where(inArray(User.id, ids));
names = new Map(users.map((u) => [String(u.id), u.username])); names = new Map(users.map((u) => [String(u.id), u.username]));
} }
} catch { } catch {
+13 -12
View File
@@ -1,9 +1,10 @@
import { desc } from "drizzle-orm";
import { getTranslations } from "next-intl/server"; import { getTranslations } from "next-intl/server";
import { deleteShout } from "@/actions/admin-radio-moderation"; import { deleteShout } from "@/actions/admin-radio-moderation";
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, RadioShouts } from "@/lib/db";
import { formatDate } from "@/lib/format-date"; import { formatDate } from "@/lib/format-date";
import { prisma } from "@/lib/prisma";
type Shout = { type Shout = {
id: bigint; id: bigint;
@@ -18,17 +19,17 @@ export default async function AdminRadioModerationPage() {
let shouts: Shout[] = []; let shouts: Shout[] = [];
try { try {
shouts = await prisma.radioShouts.findMany({ shouts = await db
select: { .select({
id: true, id: RadioShouts.id,
userId: true, userId: RadioShouts.userId,
message: true, message: RadioShouts.message,
createdAt: true, createdAt: RadioShouts.createdAt,
updatedAt: true, updatedAt: RadioShouts.updatedAt,
}, })
orderBy: { id: "desc" }, .from(RadioShouts)
take: 50, .orderBy(desc(RadioShouts.id))
}); .limit(50);
} catch { } catch {
shouts = []; shouts = [];
} }
+39 -33
View File
@@ -1,9 +1,10 @@
import { desc } from "drizzle-orm";
import { getTranslations } from "next-intl/server"; import { getTranslations } from "next-intl/server";
import { deleteShout } from "@/actions/admin-radio-moderation"; import { deleteShout } from "@/actions/admin-radio-moderation";
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, RadioApplications, RadioSchedules, RadioShouts } from "@/lib/db";
import { formatDate } from "@/lib/format-date"; import { formatDate } from "@/lib/format-date";
import { prisma } from "@/lib/prisma";
type Shout = { type Shout = {
id: bigint; id: bigint;
@@ -52,48 +53,53 @@ export default async function AdminRadioPage() {
let schedules: Schedule[] = []; let schedules: Schedule[] = [];
try { try {
shouts = await prisma.radioShouts.findMany({ shouts = await db
select: { id: true, userId: true, message: true, createdAt: true }, .select({
orderBy: { id: "desc" }, id: RadioShouts.id,
take: 25, userId: RadioShouts.userId,
}); message: RadioShouts.message,
createdAt: RadioShouts.createdAt,
})
.from(RadioShouts)
.orderBy(desc(RadioShouts.id))
.limit(25);
} catch { } catch {
shouts = []; shouts = [];
} }
try { try {
applications = await prisma.radioApplications.findMany({ applications = await db
select: { .select({
id: true, id: RadioApplications.id,
userId: true, userId: RadioApplications.userId,
realName: true, realName: RadioApplications.realName,
age: true, age: RadioApplications.age,
status: true, status: RadioApplications.status,
musicStyle: true, musicStyle: RadioApplications.musicStyle,
discordUsername: true, discordUsername: RadioApplications.discordUsername,
createdAt: true, createdAt: RadioApplications.createdAt,
}, })
orderBy: { id: "desc" }, .from(RadioApplications)
take: 25, .orderBy(desc(RadioApplications.id))
}); .limit(25);
} catch { } catch {
applications = []; applications = [];
} }
try { try {
schedules = await prisma.radioSchedules.findMany({ schedules = await db
select: { .select({
id: true, id: RadioSchedules.id,
userId: true, userId: RadioSchedules.userId,
day: true, day: RadioSchedules.day,
startTime: true, startTime: RadioSchedules.startTime,
endTime: true, endTime: RadioSchedules.endTime,
showName: true, showName: RadioSchedules.showName,
isActive: true, isActive: RadioSchedules.isActive,
}, })
orderBy: { id: "desc" }, .from(RadioSchedules)
take: 25, .orderBy(desc(RadioSchedules.id))
}); .limit(25);
} catch { } catch {
schedules = []; schedules = [];
} }
+12 -11
View File
@@ -1,3 +1,4 @@
import { asc } from "drizzle-orm";
import { getTranslations } from "next-intl/server"; import { getTranslations } from "next-intl/server";
import { import {
createRadioRank, createRadioRank,
@@ -6,7 +7,7 @@ import {
} from "@/actions/admin-radio-extra"; } from "@/actions/admin-radio-extra";
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 { prisma } from "@/lib/prisma"; import { db, RadioRanks } from "@/lib/db";
type Rank = { type Rank = {
id: bigint; id: bigint;
@@ -23,16 +24,16 @@ export default async function AdminRadioRanksPage() {
let dbError = false; let dbError = false;
try { try {
ranks = await prisma.radioRanks.findMany({ ranks = await db
select: { .select({
id: true, id: RadioRanks.id,
name: true, name: RadioRanks.name,
description: true, description: RadioRanks.description,
badgeCode: true, badgeCode: RadioRanks.badgeCode,
isActive: true, isActive: RadioRanks.isActive,
}, })
orderBy: { id: "asc" }, .from(RadioRanks)
}); .orderBy(asc(RadioRanks.id));
} catch { } catch {
dbError = true; dbError = true;
ranks = []; ranks = [];
+8 -4
View File
@@ -5,7 +5,7 @@ import {
} from "@/actions/admin-radio-extra"; } from "@/actions/admin-radio-extra";
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 { prisma } from "@/lib/prisma"; import { db, WebsiteSetting } from "@/lib/db";
type Field = { key: string; comment: string }; type Field = { key: string; comment: string };
type Group = { title: string; fields: Field[] }; type Group = { title: string; fields: Field[] };
@@ -364,9 +364,13 @@ export default async function AdminRadioSettingsPage() {
let extra: Field[] = []; let extra: Field[] = [];
try { try {
const rows = await prisma.websiteSetting.findMany({ const rows = await db
select: { key: true, value: true, comment: true }, .select({
}); key: WebsiteSetting.key,
value: WebsiteSetting.value,
comment: WebsiteSetting.comment,
})
.from(WebsiteSetting);
values = new Map(rows.map((r) => [r.key, r.value])); values = new Map(rows.map((r) => [r.key, r.value]));
extra = rows extra = rows
.filter( .filter(
+15 -12
View File
@@ -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-rare-values"; } from "@/actions/admin-rare-values";
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, WebsiteRareValueCategories, WebsiteRareValues } from "@/lib/db";
import { canAccess, getAdminContext, PERMS } from "@/lib/permissions"; import { canAccess, getAdminContext, PERMS } from "@/lib/permissions";
import { prisma } from "@/lib/prisma";
const CURRENCY_TYPES = ["diamonds", "credits", "duckets", "points"]; const CURRENCY_TYPES = ["diamonds", "credits", "duckets", "points"];
@@ -21,23 +22,25 @@ export default async function AdminRareValues() {
const t = await getTranslations("pages.admin.rareValues"); const t = await getTranslations("pages.admin.rareValues");
let categories: Awaited< let categories: (typeof WebsiteRareValueCategories.$inferSelect)[] = [];
ReturnType<typeof prisma.websiteRareValueCategories.findMany>
> = [];
try { try {
categories = await prisma.websiteRareValueCategories.findMany({ categories = await db
orderBy: [{ priority: "asc" }, { name: "asc" }], .select()
}); .from(WebsiteRareValueCategories)
.orderBy(
asc(WebsiteRareValueCategories.priority),
asc(WebsiteRareValueCategories.name),
);
} catch { } catch {
categories = []; categories = [];
} }
let values: Awaited<ReturnType<typeof prisma.websiteRareValues.findMany>> = let values: (typeof WebsiteRareValues.$inferSelect)[] = [];
[];
try { try {
values = await prisma.websiteRareValues.findMany({ values = await db
orderBy: { name: "asc" }, .select()
}); .from(WebsiteRareValues)
.orderBy(asc(WebsiteRareValues.name));
} catch { } catch {
values = []; values = [];
} }
+20 -13
View File
@@ -1,7 +1,8 @@
import { desc, eq, inArray } from "drizzle-orm";
import { redirect } from "next/navigation"; import { redirect } from "next/navigation";
import { AdminBreadcrumb } from "@/components/admin/admin-breadcrumb"; import { AdminBreadcrumb } from "@/components/admin/admin-breadcrumb";
import { db, Items, ItemsBase, User } from "@/lib/db";
import { canAccess, getAdminContext, PERMS } from "@/lib/permissions"; import { canAccess, getAdminContext, PERMS } from "@/lib/permissions";
import { prisma } from "@/lib/prisma";
import { loadRoom } from "../../_lib/load-room"; import { loadRoom } from "../../_lib/load-room";
import { FurniClient } from "./furni-client"; import { FurniClient } from "./furni-client";
@@ -18,10 +19,11 @@ export default async function RoomFurniPage({
const room = await loadRoom(id); const room = await loadRoom(id);
// Load items in this room. items.item_id → items_base.id // Load items in this room. items.item_id → items_base.id
const items = await prisma.items.findMany({ const items = await db
where: { roomId: room.id }, .select()
orderBy: { id: "desc" }, .from(Items)
}); .where(eq(Items.roomId, room.id))
.orderBy(desc(Items.id));
// Resolve item bases // Resolve item bases
const baseIds = Array.from( const baseIds = Array.from(
@@ -30,10 +32,15 @@ export default async function RoomFurniPage({
), ),
); );
const bases = baseIds.length const bases = baseIds.length
? await prisma.itemsBase.findMany({ ? await db
where: { id: { in: baseIds } }, .select({
select: { id: true, itemName: true, publicName: true, type: true }, id: ItemsBase.id,
}) itemName: ItemsBase.itemName,
publicName: ItemsBase.publicName,
type: ItemsBase.type,
})
.from(ItemsBase)
.where(inArray(ItemsBase.id, baseIds))
: []; : [];
const baseMap = new Map(bases.map((b) => [b.id, b])); const baseMap = new Map(bases.map((b) => [b.id, b]));
@@ -42,10 +49,10 @@ export default async function RoomFurniPage({
new Set(items.map((i) => i.userId).filter((u) => u > 0)), new Set(items.map((i) => i.userId).filter((u) => u > 0)),
); );
const users = userIds.length const users = userIds.length
? await prisma.user.findMany({ ? 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))
: []; : [];
const userMap = new Map(users.map((u) => [u.id, u.username])); const userMap = new Map(users.map((u) => [u.id, u.username]));
+18 -8
View File
@@ -1,21 +1,31 @@
import { eq } from "drizzle-orm";
import { notFound } from "next/navigation"; import { notFound } from "next/navigation";
import { prisma } from "@/lib/prisma"; import { db, Rooms, User } from "@/lib/db";
export async function loadRoom(id: string) { export async function loadRoom(id: string) {
const roomId = parseInt(id, 10); const roomId = parseInt(id, 10);
if (Number.isNaN(roomId)) notFound(); if (Number.isNaN(roomId)) notFound();
const room = await prisma.rooms.findUnique({ const [room] = await db
where: { id: roomId }, .select()
}); .from(Rooms)
.where(eq(Rooms.id, roomId))
.limit(1);
if (!room) notFound(); if (!room) notFound();
const owner = const owner =
room.ownerId > 0 room.ownerId > 0
? await prisma.user.findUnique({ ? ((
where: { id: room.ownerId }, await db
select: { id: true, username: true, look: true }, .select({
}) id: User.id,
username: User.username,
look: User.look,
})
.from(User)
.where(eq(User.id, room.ownerId))
.limit(1)
)[0] ?? null)
: null; : null;
return { ...room, owner }; return { ...room, owner };
} }
+7 -5
View File
@@ -1,9 +1,10 @@
import { asc } from "drizzle-orm";
import { Settings } from "lucide-react"; import { Settings } 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, WebsiteSetting } from "@/lib/db";
import { canAccess, getAdminContext, PERMS } from "@/lib/permissions"; import { canAccess, getAdminContext, PERMS } from "@/lib/permissions";
import { prisma } from "@/lib/prisma";
import { AdvancedSettingsPanel } from "./advanced-settings-panel"; import { AdvancedSettingsPanel } from "./advanced-settings-panel";
import { FIELD_DEFAULTS, MANAGED_SETTING_KEYS } from "./cms-settings-config"; import { FIELD_DEFAULTS, MANAGED_SETTING_KEYS } from "./cms-settings-config";
import { CmsSettingsForm } from "./cms-settings-form"; import { CmsSettingsForm } from "./cms-settings-form";
@@ -22,11 +23,12 @@ export default async function AdminSettings() {
); );
const t = await getTranslations("pages.admin.settings"); const t = await getTranslations("pages.admin.settings");
let settings: Awaited<ReturnType<typeof prisma.websiteSetting.findMany>> = []; let settings: (typeof WebsiteSetting.$inferSelect)[] = [];
try { try {
settings = await prisma.websiteSetting.findMany({ settings = await db
orderBy: { key: "asc" }, .select()
}); .from(WebsiteSetting)
.orderBy(asc(WebsiteSetting.key));
} catch { } catch {
settings = []; settings = [];
} }
+9 -6
View File
@@ -1,11 +1,12 @@
import { eq } from "drizzle-orm";
import { Package } from "lucide-react"; import { Package } from "lucide-react";
import Link from "next/link"; import Link from "next/link";
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 { deleteShopArticle, updateShopArticle } from "@/actions/admin-shop"; import { deleteShopArticle, updateShopArticle } from "@/actions/admin-shop";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import { db, WebsiteShopArticles } from "@/lib/db";
import { canAccess, getAdminContext, PERMS } from "@/lib/permissions"; import { canAccess, getAdminContext, PERMS } from "@/lib/permissions";
import { prisma } from "@/lib/prisma";
export default async function EditShopArticle({ export default async function EditShopArticle({
params, params,
@@ -20,12 +21,14 @@ export default async function EditShopArticle({
const t = await getTranslations("pages.admin.shop"); const t = await getTranslations("pages.admin.shop");
const { id } = await params; const { id } = await params;
let pkg: Awaited<ReturnType<typeof prisma.websiteShopArticles.findUnique>> = let pkg: typeof WebsiteShopArticles.$inferSelect | null = null;
null;
try { try {
pkg = await prisma.websiteShopArticles.findUnique({ const [row] = await db
where: { id: BigInt(id) }, .select()
}); .from(WebsiteShopArticles)
.where(eq(WebsiteShopArticles.id, BigInt(id)))
.limit(1);
pkg = row ?? null;
} catch { } catch {
notFound(); notFound();
} }
+19 -17
View File
@@ -1,3 +1,4 @@
import { asc, count, desc } from "drizzle-orm";
import { ShoppingCart } from "lucide-react"; import { ShoppingCart } from "lucide-react";
import Link from "next/link"; import Link from "next/link";
import { redirect } from "next/navigation"; import { redirect } from "next/navigation";
@@ -8,9 +9,9 @@ import { StatusCard } from "@/components/admin/dashboard";
import { CurrencyIcon } from "@/components/shared/currency-icon"; import { CurrencyIcon } from "@/components/shared/currency-icon";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import { calcPagination, parseListParams } from "@/lib/admin-helpers"; import { calcPagination, parseListParams } from "@/lib/admin-helpers";
import { db, WebsitePaypalTransactions, WebsiteShopArticles } 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";
const PAID_STATUS = new Set(["completed", "paid", "success", "approved"]); const PAID_STATUS = new Set(["completed", "paid", "success", "approved"]);
const RECENT_ORDERS = 25; const RECENT_ORDERS = 25;
@@ -65,22 +66,24 @@ export default async function AdminShop({
const parsed = parseListParams(new URLSearchParams(raw)); const parsed = parseListParams(new URLSearchParams(raw));
let packageTotal = 0; let packageTotal = 0;
let packages: Awaited< let packages: (typeof WebsiteShopArticles.$inferSelect)[] = [];
ReturnType<typeof prisma.websiteShopArticles.findMany>
> = [];
let loadError = false; let loadError = false;
try { try {
packageTotal = await prisma.websiteShopArticles.count(); const [totalRow] = await db
.select({ total: count() })
.from(WebsiteShopArticles);
packageTotal = Number(totalRow?.total ?? 0);
const pagination = calcPagination( const pagination = calcPagination(
packageTotal, packageTotal,
parsed.page, parsed.page,
parsed.perPage, parsed.perPage,
); );
packages = await prisma.websiteShopArticles.findMany({ packages = await db
orderBy: [{ position: "asc" }, { name: "asc" }], .select()
skip: pagination.offset, .from(WebsiteShopArticles)
take: pagination.perPage, .orderBy(asc(WebsiteShopArticles.position), asc(WebsiteShopArticles.name))
}); .limit(pagination.perPage)
.offset(pagination.offset);
} catch { } catch {
packages = []; packages = [];
packageTotal = 0; packageTotal = 0;
@@ -93,14 +96,13 @@ export default async function AdminShop({
parsed.perPage, parsed.perPage,
); );
let orders: Awaited< let orders: (typeof WebsitePaypalTransactions.$inferSelect)[] = [];
ReturnType<typeof prisma.websitePaypalTransactions.findMany>
> = [];
try { try {
orders = await prisma.websitePaypalTransactions.findMany({ orders = await db
orderBy: { createdAt: "desc" }, .select()
take: RECENT_ORDERS, .from(WebsitePaypalTransactions)
}); .orderBy(desc(WebsitePaypalTransactions.createdAt))
.limit(RECENT_ORDERS);
} catch { } catch {
orders = []; orders = [];
loadError = true; loadError = true;
+22 -18
View File
@@ -1,6 +1,7 @@
import { asc, count, gt } from "drizzle-orm";
import { redirect } from "next/navigation"; import { redirect } from "next/navigation";
import { CatalogItems, db, Soundtracks } from "@/lib/db";
import { canAccess, getAdminContext, PERMS } from "@/lib/permissions"; import { canAccess, getAdminContext, PERMS } from "@/lib/permissions";
import { prisma } from "@/lib/prisma";
import { siteSettings } from "@/lib/services/site-settings"; import { siteSettings } from "@/lib/services/site-settings";
import { import {
DEFAULT_SOUNDTRACK_BASE_URL, DEFAULT_SOUNDTRACK_BASE_URL,
@@ -18,28 +19,31 @@ export default async function SoundsPage() {
const canEdit = canAccess(permissions, PERMS.CATALOG_EDIT, session.user.rank); const canEdit = canAccess(permissions, PERMS.CATALOG_EDIT, session.user.rank);
const [rows, usage, baseUrlSetting] = await Promise.all([ const [rows, usage, baseUrlSetting] = await Promise.all([
prisma.soundtracks.findMany({ db
orderBy: { id: "asc" }, .select({
select: { id: Soundtracks.id,
id: true, code: Soundtracks.code,
code: true, name: Soundtracks.name,
name: true, author: Soundtracks.author,
author: true, length: Soundtracks.length,
length: true, track: Soundtracks.track,
track: true, })
}, .from(Soundtracks)
}), .orderBy(asc(Soundtracks.id)),
prisma.catalogItems.groupBy({ db
by: ["songId"], .select({
where: { songId: { gt: 0 } }, songId: CatalogItems.songId,
_count: { _all: true }, total: count(),
}), })
.from(CatalogItems)
.where(gt(CatalogItems.songId, 0))
.groupBy(CatalogItems.songId),
siteSettings.get("soundtrack_base_url", DEFAULT_SOUNDTRACK_BASE_URL), siteSettings.get("soundtrack_base_url", DEFAULT_SOUNDTRACK_BASE_URL),
]); ]);
const usageMap: Record<number, number> = {}; const usageMap: Record<number, number> = {};
for (const row of usage) { for (const row of usage) {
if (row.songId != null) usageMap[row.songId] = row._count._all; if (row.songId != null) usageMap[row.songId] = Number(row.total);
} }
const soundtracks = rows.map((r) => { const soundtracks = rows.map((r) => {
+23 -16
View File
@@ -1,11 +1,12 @@
import { 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 { AdminSimplePager } from "@/components/admin/admin-simple-pager"; import { AdminSimplePager } from "@/components/admin/admin-simple-pager";
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 } 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";
type SubRow = { type SubRow = {
id: number; id: number;
@@ -14,6 +15,19 @@ type SubRow = {
expiresAt: number; expiresAt: number;
}; };
type CountRow = { c: number | bigint };
type SubSqlRow = {
id: number;
user_id: number | null;
subscription_type: string | null;
expires_at: number;
};
async function rawRows<T>(query: ReturnType<typeof sql>): Promise<T[]> {
const [rows] = (await db.execute(query)) as unknown as [T[], unknown];
return rows ?? [];
}
export default async function AdminSubscriptions({ export default async function AdminSubscriptions({
searchParams, searchParams,
}: { }: {
@@ -36,39 +50,32 @@ export default async function AdminSubscriptions({
let loadError = false; let loadError = false;
try { try {
const [countRows, typeRows, soonRows] = await Promise.all([ const [countRows, typeRows, soonRows] = await Promise.all([
prisma.$queryRaw<{ c: bigint }[]>` rawRows<CountRow>(sql`
SELECT COUNT(*) AS c SELECT COUNT(*) AS c
FROM users_subscriptions FROM users_subscriptions
WHERE active = 1 WHERE active = 1
AND COALESCE(timestamp_start, 0) + COALESCE(duration, 0) > ${now} AND COALESCE(timestamp_start, 0) + COALESCE(duration, 0) > ${now}
`, `),
prisma.$queryRaw<{ c: bigint }[]>` rawRows<CountRow>(sql`
SELECT COUNT(DISTINCT subscription_type) AS c SELECT COUNT(DISTINCT subscription_type) AS c
FROM users_subscriptions FROM users_subscriptions
WHERE active = 1 WHERE active = 1
AND COALESCE(timestamp_start, 0) + COALESCE(duration, 0) > ${now} AND COALESCE(timestamp_start, 0) + COALESCE(duration, 0) > ${now}
`, `),
prisma.$queryRaw<{ c: bigint }[]>` rawRows<CountRow>(sql`
SELECT COUNT(*) AS c SELECT COUNT(*) AS c
FROM users_subscriptions FROM users_subscriptions
WHERE active = 1 WHERE active = 1
AND COALESCE(timestamp_start, 0) + COALESCE(duration, 0) > ${now} AND COALESCE(timestamp_start, 0) + COALESCE(duration, 0) > ${now}
AND COALESCE(timestamp_start, 0) + COALESCE(duration, 0) <= ${now + 7 * 24 * 60 * 60} AND COALESCE(timestamp_start, 0) + COALESCE(duration, 0) <= ${now + 7 * 24 * 60 * 60}
`, `),
]); ]);
total = Number(countRows[0]?.c ?? 0); total = Number(countRows[0]?.c ?? 0);
types = Number(typeRows[0]?.c ?? 0); types = Number(typeRows[0]?.c ?? 0);
expiringSoon = Number(soonRows[0]?.c ?? 0); expiringSoon = Number(soonRows[0]?.c ?? 0);
const pagination = calcPagination(total, parsed.page, parsed.perPage); const pagination = calcPagination(total, parsed.page, parsed.perPage);
const rows = await prisma.$queryRaw< const rows = await rawRows<SubSqlRow>(sql`
{
id: number;
user_id: number | null;
subscription_type: string | null;
expires_at: number;
}[]
>`
SELECT id, SELECT id,
user_id, user_id,
subscription_type, subscription_type,
@@ -78,7 +85,7 @@ export default async function AdminSubscriptions({
AND COALESCE(timestamp_start, 0) + COALESCE(duration, 0) > ${now} AND COALESCE(timestamp_start, 0) + COALESCE(duration, 0) > ${now}
ORDER BY expires_at ASC ORDER BY expires_at ASC
LIMIT ${pagination.perPage} OFFSET ${pagination.offset} LIMIT ${pagination.perPage} OFFSET ${pagination.offset}
`; `);
active = rows.map((r) => ({ active = rows.map((r) => ({
id: Number(r.id), id: Number(r.id),
+20 -11
View File
@@ -1,11 +1,12 @@
import { asc, count } 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 { createTag, deleteTag, updateTag } from "@/actions/admin-tags"; import { createTag, deleteTag, updateTag } from "@/actions/admin-tags";
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, Taggables, Tags } 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";
type Tag = { type Tag = {
id: bigint; id: bigint;
@@ -26,18 +27,26 @@ export default async function AdminTagsPage() {
let dbError = false; let dbError = false;
try { try {
tags = await prisma.tags.findMany({ tags = await db
select: { id: true, name: true, backgroundColor: true, createdAt: true }, .select({
orderBy: { name: "asc" }, id: Tags.id,
take: 500, name: Tags.name,
}); backgroundColor: Tags.backgroundColor,
createdAt: Tags.createdAt,
})
.from(Tags)
.orderBy(asc(Tags.name))
.limit(500);
// How many things each tag is attached to (taggables join table). // How many things each tag is attached to (taggables join table).
const counts = await prisma.taggables.groupBy({ const counts = await db
by: ["tagId"], .select({
_count: { tagId: true }, tagId: Taggables.tagId,
}); total: count(),
usage = new Map(counts.map((c) => [String(c.tagId), c._count.tagId])); })
.from(Taggables)
.groupBy(Taggables.tagId);
usage = new Map(counts.map((c) => [String(c.tagId), Number(c.total)]));
} catch { } catch {
dbError = true; dbError = true;
tags = []; tags = [];
+13 -12
View File
@@ -1,10 +1,11 @@
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 { createTeam, deleteTeam } from "@/actions/admin-teams"; import { createTeam, deleteTeam } from "@/actions/admin-teams";
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, WebsiteTeams } from "@/lib/db";
import { canAccess, getAdminContext, PERMS } from "@/lib/permissions"; import { canAccess, getAdminContext, PERMS } from "@/lib/permissions";
import { prisma } from "@/lib/prisma";
type TeamRow = { type TeamRow = {
id: bigint; id: bigint;
@@ -24,17 +25,17 @@ export default async function AdminTeams() {
const t = await getTranslations("pages.admin.teams"); const t = await getTranslations("pages.admin.teams");
let teams: TeamRow[] = []; let teams: TeamRow[] = [];
try { try {
teams = await prisma.websiteTeams.findMany({ teams = await db
select: { .select({
id: true, id: WebsiteTeams.id,
rankName: true, rankName: WebsiteTeams.rankName,
badge: true, badge: WebsiteTeams.badge,
jobDescription: true, jobDescription: WebsiteTeams.jobDescription,
staffColor: true, staffColor: WebsiteTeams.staffColor,
hiddenRank: true, hiddenRank: WebsiteTeams.hiddenRank,
}, })
orderBy: { id: "asc" }, .from(WebsiteTeams)
}); .orderBy(asc(WebsiteTeams.id));
} catch { } catch {
teams = []; teams = [];
} }