feat(admin): add user account management

This commit is contained in:
dmg
2026-08-01 14:16:46 -04:00
parent c5de0a1810
commit 10554eeaff
10 changed files with 690 additions and 6 deletions
@@ -0,0 +1,101 @@
import { minecraftAccounts, users } from "@minecraft-account-manager/database";
import { and, eq, ilike, isNull, or, sql } from "drizzle-orm";
import Link from "next/link";
import { db } from "@/lib/database";
export default async function AdminUsersPage({
searchParams,
}: {
searchParams: Promise<{ q?: string; error?: string }>;
}) {
const query = await searchParams;
const search = query.q?.trim().slice(0, 100) ?? "";
const pattern = `%${search}%`;
const where = search
? or(
ilike(users.firstName, pattern),
ilike(users.discordUsername, pattern),
eq(users.discordUserId, search),
sql`exists (
select 1 from ${minecraftAccounts}
where ${minecraftAccounts.userId} = ${users.id}
and ${minecraftAccounts.deletedAt} is null
and (
${minecraftAccounts.username} ilike ${pattern}
or ${minecraftAccounts.minecraftUuid} = ${search.toLowerCase()}
)
)`,
)
: undefined;
const results = await db
.select({
id: users.id,
firstName: users.firstName,
discordUsername: users.discordUsername,
discordUserId: users.discordUserId,
onboardingCompletedAt: users.onboardingCompletedAt,
primaryUsername: minecraftAccounts.username,
accountCount: sql<number>`(
select count(*)::int from ${minecraftAccounts} account_count
where account_count.user_id = ${users.id}
and account_count.deleted_at is null
)`,
})
.from(users)
.leftJoin(
minecraftAccounts,
and(
eq(minecraftAccounts.userId, users.id),
eq(minecraftAccounts.isPrimary, true),
isNull(minecraftAccounts.deletedAt),
),
)
.where(where)
.orderBy(users.firstName, users.discordUsername)
.limit(100);
return (
<main className="mx-auto max-w-6xl px-6 py-14">
<div className="flex flex-col gap-6 border-b border-line pb-7 sm:flex-row sm:items-end sm:justify-between">
<div>
<p className="font-mono text-xs font-bold uppercase tracking-[0.25em] text-accent">Identity registry</p>
<h1 className="mt-4 font-display text-5xl font-black uppercase">Users</h1>
</div>
<form className="flex w-full max-w-md gap-2" method="get">
<input
className="min-w-0 flex-1 border border-line bg-panel px-4 py-3 font-mono text-xs outline-none focus:border-accent"
defaultValue={search}
name="q"
placeholder="Name, Discord ID, player, or UUID"
type="search"
/>
<button className="border border-ink bg-ink px-5 py-3 font-mono text-[10px] font-bold uppercase tracking-wider text-canvas" type="submit">Search</button>
</form>
</div>
{query.error && <p className="mt-7 border-l-2 border-accent bg-panel px-5 py-4 text-sm text-accent">The requested user could not be found.</p>}
<div className="mt-8 overflow-x-auto border border-line bg-panel shadow-[8px_8px_0_var(--color-shadow)]">
<table className="w-full min-w-[760px] border-collapse text-left">
<thead className="border-b border-line font-mono text-[10px] uppercase tracking-widest text-muted">
<tr><th className="p-4">User</th><th className="p-4">Discord</th><th className="p-4">Primary</th><th className="p-4">Accounts</th><th className="p-4">Status</th></tr>
</thead>
<tbody className="divide-y divide-line">
{results.map((user) => (
<tr className="transition-colors hover:bg-canvas/60" key={user.id}>
<td className="p-4"><Link className="font-display text-lg font-black underline decoration-line underline-offset-4 hover:text-accent" href={`/admin/users/${user.id}`}>{user.firstName ?? "Name needed"}</Link></td>
<td className="p-4"><div className="font-mono text-xs">@{user.discordUsername}</div><div className="mt-1 font-mono text-[9px] text-muted">{user.discordUserId}</div></td>
<td className="p-4 font-mono text-xs">{user.primaryUsername ?? "—"}</td>
<td className="p-4 font-mono text-xs">{user.accountCount}</td>
<td className="p-4"><span className={`border px-2 py-1 font-mono text-[9px] uppercase tracking-wider ${user.onboardingCompletedAt ? "border-line text-muted" : "border-accent text-accent"}`}>{user.onboardingCompletedAt ? "Ready" : "Onboarding"}</span></td>
</tr>
))}
{!results.length && <tr><td className="p-8 text-muted" colSpan={5}>No users match that search.</td></tr>}
</tbody>
</table>
</div>
<p className="mt-4 font-mono text-[9px] uppercase tracking-widest text-muted">Showing up to 100 users</p>
</main>
);
}