feat(admin): streamline group management
CI / validate (push) Successful in 4m16s
Release / release (push) Failing after 9m14s

This commit is contained in:
dmg
2026-08-02 11:06:55 -04:00
parent 71856bb869
commit d4afb71798
24 changed files with 1588 additions and 379 deletions
@@ -1,16 +1,28 @@
import { groups, userGroupMemberships, users } from "@minecraft-account-manager/database";
import { asc, eq } from "drizzle-orm";
import { groups, minecraftAccounts, userGroupMemberships, users } from "@minecraft-account-manager/database";
import { and, asc, desc, eq, isNull, sql } from "drizzle-orm";
import type { ReactNode } from "react";
import Link from "next/link";
import { notFound } from "next/navigation";
import { AdminModalForm } from "@/components/admin-modal-form";
import { AdminUserTable } from "@/components/admin-user-table";
import { GroupPolicyControl } from "@/components/group-policy-control";
import { db } from "@/lib/database";
import { addGroupMember, assignDefaultGroup, deleteGroup, removeGroupMember, setGroupAccess, setGroupAnonymizedNetworkAccess } from "../actions";
import { isEffectiveGroupMember } from "@/lib/group-management";
import { assignUserGroupFromRegistry } from "../../users/actions";
import { deleteGroup, setGroupAccess, setGroupAnonymizedNetworkAccess, updateGroupDetails } from "../actions";
const savedMessages: Record<string, string> = {
created: "Group created with access disabled.",
access: "Group access policy updated.",
"network-access": "Group VPN, proxy, and Tor policy updated.",
"member-added": "User assigned to the group.",
"member-removed": "User returned to the default group.",
created: "Group created.",
details: "Group details updated.",
access: "Minecraft access policy updated.",
"network-access": "VPN, proxy, and Tor policy updated.",
group: "Member group updated.",
};
const errorMessages: Record<string, string> = {
"invalid-group": "Enter a valid name and a description of no more than 500 characters.",
"duplicate-group": "A group with that name already exists.",
"invalid-group-assignment": "The user or destination group no longer exists. No membership change was applied.",
};
export const dynamic = "force-dynamic";
@@ -20,31 +32,43 @@ export default async function GroupPage({
searchParams,
}: {
params: Promise<{ groupId: string }>;
searchParams: Promise<{ saved?: string }>;
searchParams: Promise<{ error?: string; saved?: string }>;
}) {
const { groupId } = await params;
const query = await searchParams;
const [group] = await db.select().from(groups).where(eq(groups.id, groupId)).limit(1);
if (!group) notFound();
const [allUsers, memberships] = await Promise.all([
const [allUsers, allGroups, memberships] = await Promise.all([
db.select({
id: users.id,
firstName: users.firstName,
discordUsername: users.discordUsername,
discordGlobalName: users.discordGlobalName,
discordUserId: users.discordUserId,
}).from(users).orderBy(asc(users.discordUsername)),
db.select({
userId: userGroupMemberships.userId,
groupId: userGroupMemberships.groupId,
groupName: groups.name,
}).from(userGroupMemberships).innerJoin(groups, eq(groups.id, userGroupMemberships.groupId)),
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),
))
.orderBy(users.firstName, users.discordUsername),
db.select({ id: groups.id, name: groups.name, isDefault: groups.isDefault })
.from(groups).orderBy(desc(groups.isDefault), asc(groups.name)),
db.select({ userId: userGroupMemberships.userId, groupId: userGroupMemberships.groupId })
.from(userGroupMemberships),
]);
const assignmentByUser = new Map(memberships.map((membership) => [membership.userId, membership]));
const memberCount = group.isDefault
? allUsers.length - assignmentByUser.size
: memberships.filter((membership) => membership.groupId === group.id).length;
const assignmentByUser = Object.fromEntries(memberships.map((membership) => [membership.userId, membership.groupId]));
const memberUsers = allUsers.filter((user) => isEffectiveGroupMember(user.id, assignmentByUser, group));
const returnTo = `/admin/groups/${group.id}`;
return (
<main className="mx-auto max-w-6xl px-6 py-12">
@@ -53,83 +77,57 @@ export default async function GroupPage({
<div>
<p className="font-mono text-xs font-bold uppercase tracking-[0.25em] text-accent">Access group</p>
<div className="mt-4 flex flex-wrap items-center gap-3"><h1 className="font-display text-5xl font-black uppercase sm:text-7xl">{group.name}</h1>{group.isDefault && <span className="bg-ink px-3 py-2 font-mono text-[9px] font-bold uppercase text-canvas">Default</span>}</div>
<p className="mt-3 max-w-2xl text-sm leading-6 text-muted">{group.description ?? "No description."}</p>
</div>
<div className="grid gap-6 sm:grid-cols-2">
<form action={setGroupAccess} className="border-l-2 border-accent pl-5">
<input name="groupId" type="hidden" value={group.id} />
<input name="accessEnabled" type="hidden" value={group.accessEnabled ? "no" : "yes"} />
<p className="font-mono text-[9px] font-bold uppercase tracking-widest text-muted">Minecraft admission</p>
<p className="mt-2 font-display text-2xl font-black uppercase">{group.accessEnabled ? "Allowed" : "Denied"}</p>
<button className="mt-3 font-mono text-[9px] font-bold uppercase text-accent underline underline-offset-4" type="submit">Turn access {group.accessEnabled ? "off" : "on"}</button>
</form>
<form action={setGroupAnonymizedNetworkAccess} className="border-l-2 border-accent pl-5">
<input name="groupId" type="hidden" value={group.id} />
<input name="anonymizedNetworksAllowed" type="hidden" value={group.anonymizedNetworksAllowed ? "no" : "yes"} />
<p className="font-mono text-[9px] font-bold uppercase tracking-widest text-muted">VPN / proxy / Tor</p>
<p className="mt-2 font-display text-2xl font-black uppercase">{group.anonymizedNetworksAllowed ? "Allowed" : "Denied"}</p>
<button className="mt-3 font-mono text-[9px] font-bold uppercase text-accent underline underline-offset-4" type="submit">Turn exceptions {group.anonymizedNetworksAllowed ? "off" : "on"}</button>
</form>
<p className="mt-3 max-w-2xl whitespace-pre-line text-sm leading-6 text-muted">{group.description ?? "No description."}</p>
</div>
<AdminModalForm
action={updateGroupDetails}
description={group.isDefault ? "Update the protected default group's description. Its name remains everyone." : "Update the administrator-facing name and description. The internal slug remains stable."}
submitLabel="Save details"
title={`Edit ${group.name}`}
triggerClassName="border border-ink px-5 py-3 font-mono text-[10px] font-bold uppercase tracking-wider"
triggerLabel="Edit group"
>
<input name="groupId" type="hidden" value={group.id} />
<div className="space-y-5">
<label className="block text-sm font-bold">Name<input className="mt-2 w-full border border-line bg-canvas px-4 py-3 font-normal outline-none focus:border-accent read-only:cursor-not-allowed read-only:text-muted" defaultValue={group.name} maxLength={50} name="name" readOnly={group.isDefault} required /></label>
<label className="block text-sm font-bold">Description<textarea className="mt-2 min-h-32 w-full resize-y border border-line bg-canvas px-4 py-3 font-normal outline-none focus:border-accent" defaultValue={group.description ?? ""} maxLength={500} name="description" /></label>
</div>
</AdminModalForm>
</header>
{query.saved && <p className="mt-7 border-l-2 border-signal bg-panel px-5 py-4 font-mono text-xs font-bold uppercase tracking-wider" role="status">{savedMessages[query.saved] ?? "Group updated."}</p>}
{query.saved && <p className="mt-7 border-l-2 border-signal bg-panel px-5 py-4 text-sm" role="status">{savedMessages[query.saved] ?? "Group updated."}</p>}
{query.error && <p className="mt-7 border-l-2 border-accent bg-panel px-5 py-4 text-sm text-accent" role="alert">{errorMessages[query.error] ?? "The group operation failed."}</p>}
<section className="mt-10">
<div className="flex items-end justify-between border-b border-line pb-4">
<div><p className="font-mono text-[9px] font-bold uppercase tracking-widest text-muted">Membership</p><h2 className="mt-2 font-display text-3xl font-black uppercase">Registered users</h2></div>
<span className="font-mono text-xs text-muted">{memberCount} members</span>
</div>
{group.isDefault && <p className="border-b border-line bg-panel px-5 py-4 text-sm text-muted">Users belong to <strong className="text-ink">everyone</strong> only while they have no explicit group assignment.</p>}
<div className="divide-y divide-line">
{allUsers.map((user) => {
const assignment = assignmentByUser.get(user.id);
const isMember = group.isDefault ? !assignment : assignment?.groupId === group.id;
return (
<article className="grid gap-4 py-5 sm:grid-cols-[1fr_auto] sm:items-center" key={user.id}>
<div>
<Link className="font-mono text-sm font-bold underline decoration-line underline-offset-4 hover:decoration-accent" href={`/admin/users/${user.id}`}>{user.firstName ?? user.discordGlobalName ?? user.discordUsername}</Link>
<p className="mt-1 font-mono text-[10px] text-muted">@{user.discordUsername} · {user.discordUserId}</p>
{!isMember && assignment && <p className="mt-1 text-xs text-muted">Currently assigned to {assignment.groupName}</p>}
</div>
{isMember ? (
group.isDefault ? <span className="font-mono text-[9px] font-bold uppercase text-muted">Default assignment</span> : (
<form action={removeGroupMember}>
<input name="groupId" type="hidden" value={group.id} />
<input name="userId" type="hidden" value={user.id} />
<button className="font-mono text-[9px] font-bold uppercase text-accent underline underline-offset-4" type="submit">Return to everyone</button>
</form>
)
) : (
<form action={group.isDefault ? assignDefaultGroup : addGroupMember}>
<input name="groupId" type="hidden" value={group.id} />
<input name="userId" type="hidden" value={user.id} />
<button className="font-mono text-[9px] font-bold uppercase text-ink underline underline-offset-4" type="submit">Move to {group.name}</button>
</form>
)}
</article>
);
})}
{!allUsers.length && <p className="py-8 text-sm text-muted">No registered users yet.</p>}
<section aria-labelledby="group-policy-heading" className="mt-10 border border-line bg-panel p-6 shadow-[6px_6px_0_var(--color-shadow)]">
<div className="border-b border-line pb-4"><p className="font-mono text-[9px] font-bold uppercase tracking-widest text-muted">Admission controls</p><h2 className="mt-2 font-display text-3xl font-black uppercase" id="group-policy-heading">Group policies</h2></div>
<div className="mt-6 grid gap-6 sm:grid-cols-2">
<PolicyDetail description="Controls whether members can connect to Minecraft." label="Minecraft access"><GroupPolicyControl action={setGroupAccess} enabled={group.accessEnabled} groupId={group.id} groupName={group.name} memberCount={memberUsers.length} policy="Minecraft access" returnLocation="detail" /></PolicyDetail>
<PolicyDetail description="Allows confirmed VPN, proxy, and Tor connections." label="VPN / proxy / Tor"><GroupPolicyControl action={setGroupAnonymizedNetworkAccess} enabled={group.anonymizedNetworksAllowed} groupId={group.id} groupName={group.name} memberCount={memberUsers.length} policy="VPN / proxy / Tor" returnLocation="detail" /></PolicyDetail>
</div>
</section>
<section className="mt-10" aria-labelledby="group-members-heading">
<div className="flex items-end justify-between border-b border-line pb-4">
<div><p className="font-mono text-[9px] font-bold uppercase tracking-widest text-muted">Effective membership</p><h2 className="mt-2 font-display text-3xl font-black uppercase" id="group-members-heading">Members</h2></div>
<span className="font-mono text-xs text-muted">{memberUsers.length} {memberUsers.length === 1 ? "member" : "members"}</span>
</div>
<p className="border-x border-line bg-panel px-5 py-4 text-sm text-muted">{group.isDefault ? <>These users have no explicit assignment and therefore use <strong className="text-ink">everyone</strong>.</> : <>Choose another group to move a member, or choose <strong className="text-ink">everyone</strong> to remove the member from {group.name}. Every change requires confirmation.</>}</p>
<div className="mt-5"><AdminUserTable action={assignUserGroupFromRegistry} assignmentByUser={assignmentByUser} emptyMessage="This group has no effective members." groups={allGroups} returnTo={returnTo} users={memberUsers} /></div>
</section>
{!group.isDefault && (
<section className="mt-12 border border-accent bg-panel p-6">
<p className="font-mono text-[10px] font-bold uppercase tracking-widest text-accent">Danger zone</p>
<h2 className="mt-3 font-display text-2xl font-black uppercase">Delete {group.name}</h2>
<p className="mt-3 max-w-2xl text-sm leading-6 text-muted">Deleting this group returns its {memberCount} {memberCount === 1 ? "member" : "members"} to the protected default group. This cannot be undone.</p>
<details className="mt-5">
<summary className="w-fit cursor-pointer font-mono text-[10px] font-bold uppercase text-accent underline underline-offset-4">Review deletion</summary>
<form action={deleteGroup} className="mt-4 flex flex-wrap items-center gap-4">
<input name="groupId" type="hidden" value={group.id} />
<input name="confirmDelete" type="hidden" value="yes" />
<button className="bg-accent px-5 py-3 font-mono text-[10px] font-bold uppercase tracking-wider text-canvas" type="submit">Delete group permanently</button>
<span className="text-xs text-muted">Members will use everyone immediately.</span>
</form>
</details>
<section className="mt-12 flex flex-col gap-5 border border-accent bg-panel p-6 sm:flex-row sm:items-center sm:justify-between">
<div><p className="font-mono text-[10px] font-bold uppercase tracking-widest text-accent">Danger zone</p><h2 className="mt-2 font-display text-2xl font-black uppercase">Delete {group.name}</h2><p className="mt-2 max-w-2xl text-sm leading-6 text-muted">All {memberUsers.length} effective {memberUsers.length === 1 ? "member" : "members"} will return to everyone.</p></div>
<AdminModalForm action={deleteGroup} description={`Permanently delete ${group.name} and return ${memberUsers.length} ${memberUsers.length === 1 ? "member" : "members"} to everyone. This cannot be undone.`} intent="danger" submitLabel="Delete group" title={`Delete ${group.name}?`} triggerClassName="bg-accent px-5 py-3 font-mono text-[10px] font-bold uppercase tracking-wider text-canvas" triggerLabel="Delete group">
<input name="groupId" type="hidden" value={group.id} />
<input name="confirmDelete" type="hidden" value="yes" />
</AdminModalForm>
</section>
)}
</main>
);
}
function PolicyDetail({ children, description, label }: { children: ReactNode; description: string; label: string }) {
return <div className="flex items-center justify-between gap-5 border-l-2 border-accent pl-5"><div><h3 className="font-mono text-xs font-bold uppercase">{label}</h3><p className="mt-2 text-xs leading-5 text-muted">{description}</p></div>{children}</div>;
}
+144 -153
View File
@@ -1,179 +1,178 @@
"use server";
import { randomUUID } from "node:crypto";
import { events, groups, userGroupMemberships, users } from "@minecraft-account-manager/database";
import { events, groups, userGroupMemberships } from "@minecraft-account-manager/database";
import { getClientIp } from "@minecraft-account-manager/network";
import { and, eq } from "drizzle-orm";
import { and, eq, ne, sql } from "drizzle-orm";
import { headers } from "next/headers";
import { redirect } from "next/navigation";
import { recordAdminSubjectEvent } from "@/lib/audit";
import { requireAdminSession } from "@/lib/auth/require-admin";
import { db } from "@/lib/database";
import { editableGroupName, groupSlug, validateGroupDetails } from "@/lib/group-management";
const UUID_PATTERN = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
const SLUG_PATTERN = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
type Admin = Awaited<ReturnType<typeof requireAdminSession>>;
type ReturnLocation = "list" | "detail";
function groupPath(groupId: string, query?: string) {
return `/admin/groups/${encodeURIComponent(groupId)}${query ? `?${query}` : ""}`;
}
function returnLocation(formData: FormData): ReturnLocation {
return formData.get("returnLocation") === "list" ? "list" : "detail";
}
function operationPath(groupId: string, location: ReturnLocation, query: string) {
return location === "list" ? `/admin/groups?${query}` : groupPath(groupId, query);
}
async function auditContext() {
const requestHeaders = await headers();
return getClientIp(requestHeaders, process.env.TRUST_PROXY === "true");
}
function auditData(admin: Admin, data: Record<string, unknown>) {
return { ...data, adminEmail: admin.email, adminName: admin.name };
}
export async function createGroup(formData: FormData) {
const admin = await requireAdminSession();
const name = String(formData.get("name") ?? "").trim();
const slug = String(formData.get("slug") ?? "").trim().toLowerCase();
const description = String(formData.get("description") ?? "").trim();
if (name.length < 1 || name.length > 50 || !SLUG_PATTERN.test(slug) || slug.length > 50 || description.length > 500) {
redirect("/admin/groups?error=invalid-group");
}
let group: { id: string } | undefined;
try {
[group] = await db.insert(groups).values({
name,
slug,
description: description || null,
accessEnabled: false,
anonymizedNetworksAllowed: false,
isDefault: false,
}).returning({ id: groups.id });
} catch {
redirect("/admin/groups?error=duplicate-group");
}
if (!group) redirect("/admin/groups?error=create-failed");
await recordAdminSubjectEvent(admin, `group/${group.id}`, "games.minecraft.account-manager.group.created", {
name,
slug,
accessEnabled: false,
anonymizedNetworksAllowed: false,
});
redirect(groupPath(group.id, "saved=created"));
}
export async function setGroupAccess(formData: FormData) {
const admin = await requireAdminSession();
const groupId = String(formData.get("groupId") ?? "");
const details = validateGroupDetails(formData.get("name"), formData.get("description"));
if (!details) redirect("/admin/groups?error=invalid-group");
const accessEnabled = formData.get("accessEnabled") === "yes";
if (!UUID_PATTERN.test(groupId)) redirect("/admin/groups?error=unknown-group");
const anonymizedNetworksAllowed = formData.get("anonymizedNetworksAllowed") === "yes";
const ipAddress = await auditContext();
const [group] = await db.update(groups).set({ accessEnabled, updatedAt: new Date() })
.where(eq(groups.id, groupId)).returning({ id: groups.id, name: groups.name });
if (!group) redirect("/admin/groups?error=unknown-group");
await recordAdminSubjectEvent(admin, `group/${group.id}`, "games.minecraft.account-manager.group.access-updated", {
name: group.name,
accessEnabled,
});
redirect(groupPath(group.id, "saved=access"));
let created: { id: string } | null = null;
try {
created = await db.transaction(async (tx) => {
await tx.execute(sql`select pg_advisory_xact_lock(hashtext('minecraft-account-manager-group-identity'))`);
const [duplicate] = await tx.select({ id: groups.id }).from(groups)
.where(sql`lower(${groups.name}) = lower(${details.name})`).limit(1);
if (duplicate) return null;
const existing = await tx.select({ slug: groups.slug }).from(groups);
const slug = groupSlug(details.name, new Set(existing.map((group) => group.slug.toLowerCase())));
const [group] = await tx.insert(groups).values({
name: details.name,
slug,
description: details.description || null,
accessEnabled,
anonymizedNetworksAllowed,
isDefault: false,
}).returning({ id: groups.id });
if (!group) throw new Error("Group insert returned no row");
await tx.insert(events).values({
id: randomUUID(),
source: "/web/admin",
type: "games.minecraft.account-manager.group.created",
subject: `group/${group.id}`,
time: new Date(),
data: auditData(admin, {
name: details.name,
slug,
accessEnabled,
anonymizedNetworksAllowed,
}),
ipAddress: ipAddress ?? null,
});
return group;
});
} catch {
redirect("/admin/groups?error=create-failed");
}
if (!created) redirect("/admin/groups?error=duplicate-group");
redirect(groupPath(created.id, "saved=created"));
}
export async function setGroupAnonymizedNetworkAccess(formData: FormData) {
export async function updateGroupDetails(formData: FormData) {
const admin = await requireAdminSession();
const groupId = String(formData.get("groupId") ?? "");
const anonymizedNetworksAllowed = formData.get("anonymizedNetworksAllowed") === "yes";
const details = validateGroupDetails(formData.get("name"), formData.get("description"));
if (!UUID_PATTERN.test(groupId) || !details) redirect(operationPath(groupId, "detail", "error=invalid-group"));
const ipAddress = await auditContext();
const result = await db.transaction(async (tx) => {
await tx.execute(sql`select pg_advisory_xact_lock(hashtext('minecraft-account-manager-group-identity'))`);
await tx.execute(sql`select ${groups.id} from ${groups} where ${groups.id} = ${groupId} for update`);
const [current] = await tx.select().from(groups).where(eq(groups.id, groupId)).limit(1);
if (!current) return "missing" as const;
const name = editableGroupName(current.name, current.isDefault, details.name);
if (!current.isDefault) {
const [duplicate] = await tx.select({ id: groups.id }).from(groups)
.where(and(sql`lower(${groups.name}) = lower(${name})`, ne(groups.id, current.id))).limit(1);
if (duplicate) return "duplicate" as const;
}
const [updated] = await tx.update(groups).set({ name, description: details.description || null, updatedAt: new Date() })
.where(eq(groups.id, current.id)).returning({ id: groups.id });
if (!updated) return "missing" as const;
await tx.insert(events).values({
id: randomUUID(),
source: "/web/admin",
type: "games.minecraft.account-manager.group.details-updated",
subject: `group/${current.id}`,
time: new Date(),
data: auditData(admin, {
previousName: current.name,
name,
previousDescription: current.description,
description: details.description || null,
}),
ipAddress: ipAddress ?? null,
});
return "updated" as const;
});
if (result === "missing") redirect("/admin/groups?error=unknown-group");
if (result === "duplicate") redirect(groupPath(groupId, "error=duplicate-group"));
redirect(groupPath(groupId, "saved=details"));
}
async function updateGroupPolicy(
formData: FormData,
policy: "access" | "anonymized-networks",
) {
const admin = await requireAdminSession();
const groupId = String(formData.get("groupId") ?? "");
const location = returnLocation(formData);
if (!UUID_PATTERN.test(groupId)) redirect("/admin/groups?error=unknown-group");
const requestHeaders = await headers();
const ipAddress = getClientIp(requestHeaders, process.env.TRUST_PROXY === "true");
const enabled = formData.get("enabled") === "yes";
const ipAddress = await auditContext();
const group = await db.transaction(async (tx) => {
const [updated] = await tx.update(groups).set({ anonymizedNetworksAllowed, updatedAt: new Date() })
.where(eq(groups.id, groupId)).returning({ id: groups.id, name: groups.name });
await tx.execute(sql`select ${groups.id} from ${groups} where ${groups.id} = ${groupId} for update`);
const [current] = await tx.select().from(groups).where(eq(groups.id, groupId)).limit(1);
if (!current) return null;
const update = policy === "access" ? { accessEnabled: enabled } : { anonymizedNetworksAllowed: enabled };
const [updated] = await tx.update(groups).set({ ...update, updatedAt: new Date() })
.where(eq(groups.id, current.id)).returning({ id: groups.id });
if (!updated) return null;
await tx.insert(events).values({
id: randomUUID(),
source: "/web/admin",
type: "games.minecraft.account-manager.group.anonymized-network-access-updated",
subject: `group/${updated.id}`,
type: policy === "access"
? "games.minecraft.account-manager.group.access-updated"
: "games.minecraft.account-manager.group.anonymized-network-access-updated",
subject: `group/${current.id}`,
time: new Date(),
data: { name: updated.name, anonymizedNetworksAllowed, adminEmail: admin.email, adminName: admin.name },
data: auditData(admin, {
name: current.name,
previousEnabled: policy === "access" ? current.accessEnabled : current.anonymizedNetworksAllowed,
enabled,
}),
ipAddress: ipAddress ?? null,
});
return updated;
return current;
});
if (!group) redirect("/admin/groups?error=unknown-group");
redirect(groupPath(group.id, "saved=network-access"));
redirect(operationPath(group.id, location, `saved=${policy === "access" ? "access" : "network-access"}`));
}
export async function addGroupMember(formData: FormData) {
const admin = await requireAdminSession();
const groupId = String(formData.get("groupId") ?? "");
const userId = String(formData.get("userId") ?? "");
if (!UUID_PATTERN.test(groupId) || !UUID_PATTERN.test(userId)) redirect("/admin/groups?error=invalid-membership");
const [[group], [user]] = await Promise.all([
db.select({ id: groups.id, name: groups.name, isDefault: groups.isDefault }).from(groups).where(eq(groups.id, groupId)).limit(1),
db.select({ id: users.id }).from(users).where(eq(users.id, userId)).limit(1),
]);
if (!group || !user || group.isDefault) redirect("/admin/groups?error=invalid-membership");
const previousGroup = await db.transaction(async (tx) => {
const [previous] = await tx
.select({ id: groups.id, name: groups.name })
.from(userGroupMemberships)
.innerJoin(groups, eq(groups.id, userGroupMemberships.groupId))
.where(eq(userGroupMemberships.userId, user.id))
.limit(1);
await tx.delete(userGroupMemberships).where(eq(userGroupMemberships.userId, user.id));
await tx.insert(userGroupMemberships).values({ groupId: group.id, userId: user.id });
return previous ?? null;
});
await recordAdminSubjectEvent(admin, `user/${user.id}`, "games.minecraft.account-manager.group.assignment-updated", {
groupId: group.id,
groupName: group.name,
previousGroupId: previousGroup?.id ?? null,
previousGroupName: previousGroup?.name ?? "everyone",
});
redirect(groupPath(group.id, "saved=member-added"));
export async function setGroupAccess(formData: FormData) {
return updateGroupPolicy(formData, "access");
}
export async function removeGroupMember(formData: FormData) {
const admin = await requireAdminSession();
const groupId = String(formData.get("groupId") ?? "");
const userId = String(formData.get("userId") ?? "");
if (!UUID_PATTERN.test(groupId) || !UUID_PATTERN.test(userId)) redirect("/admin/groups?error=invalid-membership");
const [group] = await db.select({ id: groups.id, name: groups.name, isDefault: groups.isDefault })
.from(groups).where(eq(groups.id, groupId)).limit(1);
if (!group || group.isDefault) redirect("/admin/groups?error=invalid-membership");
await db.delete(userGroupMemberships).where(and(
eq(userGroupMemberships.groupId, group.id),
eq(userGroupMemberships.userId, userId),
));
await recordAdminSubjectEvent(admin, `user/${userId}`, "games.minecraft.account-manager.group.assignment-removed", {
groupId: group.id,
groupName: group.name,
fallbackGroup: "everyone",
});
redirect(groupPath(group.id, "saved=member-removed"));
}
export async function assignDefaultGroup(formData: FormData) {
const admin = await requireAdminSession();
const groupId = String(formData.get("groupId") ?? "");
const userId = String(formData.get("userId") ?? "");
if (!UUID_PATTERN.test(groupId) || !UUID_PATTERN.test(userId)) redirect("/admin/groups?error=invalid-membership");
const [[defaultGroup], [user]] = await Promise.all([
db.select({ id: groups.id, name: groups.name, isDefault: groups.isDefault }).from(groups).where(eq(groups.id, groupId)).limit(1),
db.select({ id: users.id }).from(users).where(eq(users.id, userId)).limit(1),
]);
if (!defaultGroup?.isDefault || !user) redirect("/admin/groups?error=invalid-membership");
const [previous] = await db
.select({ id: groups.id, name: groups.name })
.from(userGroupMemberships)
.innerJoin(groups, eq(groups.id, userGroupMemberships.groupId))
.where(eq(userGroupMemberships.userId, user.id))
.limit(1);
await db.delete(userGroupMemberships).where(eq(userGroupMemberships.userId, user.id));
await recordAdminSubjectEvent(admin, `user/${user.id}`, "games.minecraft.account-manager.group.assignment-updated", {
groupId: defaultGroup.id,
groupName: defaultGroup.name,
previousGroupId: previous?.id ?? null,
previousGroupName: previous?.name ?? null,
});
redirect(groupPath(defaultGroup.id, "saved=member-added"));
export async function setGroupAnonymizedNetworkAccess(formData: FormData) {
return updateGroupPolicy(formData, "anonymized-networks");
}
export async function deleteGroup(formData: FormData) {
@@ -181,40 +180,32 @@ export async function deleteGroup(formData: FormData) {
const groupId = String(formData.get("groupId") ?? "");
const confirmed = formData.get("confirmDelete") === "yes";
if (!UUID_PATTERN.test(groupId) || !confirmed) redirect("/admin/groups?error=invalid-delete");
const requestHeaders = await headers();
const ipAddress = getClientIp(requestHeaders, process.env.TRUST_PROXY === "true");
const ipAddress = await auditContext();
const deleted = await db.transaction(async (tx) => {
const [group] = await tx
.select({ id: groups.id, name: groups.name, slug: groups.slug, isDefault: groups.isDefault })
.from(groups)
.where(eq(groups.id, groupId))
.limit(1);
await tx.execute(sql`select pg_advisory_xact_lock(hashtext('minecraft-account-manager-group-membership'))`);
await tx.execute(sql`select ${groups.id} from ${groups} where ${groups.id} = ${groupId} for update`);
const [group] = await tx.select().from(groups).where(eq(groups.id, groupId)).limit(1);
if (!group || group.isDefault) return null;
const members = await tx
.select({ userId: userGroupMemberships.userId })
.from(userGroupMemberships)
.where(eq(userGroupMemberships.groupId, group.id));
const members = await tx.select({ userId: userGroupMemberships.userId })
.from(userGroupMemberships).where(eq(userGroupMemberships.groupId, group.id));
await tx.insert(events).values({
id: randomUUID(),
source: "/web/admin",
type: "games.minecraft.account-manager.group.deleted",
subject: `group/${group.id}`,
time: new Date(),
data: {
data: auditData(admin, {
name: group.name,
slug: group.slug,
affectedUsers: members.length,
fallbackGroup: "everyone",
adminEmail: admin.email,
adminName: admin.name,
},
}),
ipAddress: ipAddress ?? null,
});
await tx.delete(groups).where(eq(groups.id, group.id));
return group;
});
if (!deleted) redirect("/admin/groups?error=protected-group");
redirect("/admin/groups?saved=deleted");
}
@@ -1,84 +1,102 @@
import { groups, userGroupMemberships, users } from "@minecraft-account-manager/database";
import { asc, desc } from "drizzle-orm";
import { asc, count, desc } from "drizzle-orm";
import Link from "next/link";
import { AdminModalForm } from "@/components/admin-modal-form";
import { GroupPolicyControl } from "@/components/group-policy-control";
import { db } from "@/lib/database";
import { createGroup, setGroupAccess } from "./actions";
import { effectiveGroupMemberCount } from "@/lib/group-management";
import { createGroup, setGroupAccess, setGroupAnonymizedNetworkAccess } from "./actions";
const errors: Record<string, string> = {
"invalid-group": "Enter a name and a lowercase slug containing letters, numbers, or hyphens.",
"duplicate-group": "That group slug already exists.",
"invalid-group": "Enter a group name and an optional description of no more than 500 characters.",
"duplicate-group": "A group with that name already exists.",
"create-failed": "The group could not be created.",
"unknown-group": "That group no longer exists.",
"invalid-membership": "That membership change was invalid.",
"invalid-delete": "Confirm the group deletion before continuing.",
"protected-group": "The protected default group cannot be deleted.",
};
const savedMessages: Record<string, string> = {
deleted: "Group deleted. Its former members now use the default group.",
access: "Minecraft access policy updated.",
"network-access": "VPN, proxy, and Tor policy updated.",
};
export const dynamic = "force-dynamic";
export default async function GroupsPage({ searchParams }: { searchParams: Promise<{ error?: string; saved?: string }> }) {
const query = await searchParams;
const [allGroups, memberships, registeredUsers] = await Promise.all([
const [allGroups, memberships, [registeredUsers]] = await Promise.all([
db.select().from(groups).orderBy(desc(groups.isDefault), asc(groups.name)),
db.select({ groupId: userGroupMemberships.groupId }).from(userGroupMemberships),
db.select({ id: users.id }).from(users),
db.select({ count: count() }).from(users),
]);
const membershipCounts = new Map<string, number>();
for (const membership of memberships) {
membershipCounts.set(membership.groupId, (membershipCounts.get(membership.groupId) ?? 0) + 1);
}
const explicitlyAssignedUsers = memberships.length;
return (
<main className="mx-auto max-w-6xl px-6 py-14">
<header className="border-b border-line pb-8">
<p className="font-mono text-xs font-bold uppercase tracking-[0.25em] text-accent">Admission policy</p>
<h1 className="mt-4 font-display text-5xl font-black uppercase">Access groups</h1>
<p className="mt-5 max-w-2xl leading-7 text-muted">Each user has one effective group. Users without an explicit assignment fall back to <strong className="text-ink">everyone</strong>; Minecraft admission follows only that groups access setting.</p>
<header className="flex flex-col gap-6 border-b border-line pb-8 sm:flex-row sm:items-end sm:justify-between">
<div>
<p className="font-mono text-xs font-bold uppercase tracking-[0.25em] text-accent">Admission policy</p>
<h1 className="mt-4 font-display text-5xl font-black uppercase">Access groups</h1>
<p className="mt-5 max-w-2xl leading-7 text-muted">One effective group controls Minecraft and VPN access. Every policy change asks for confirmation before it applies.</p>
</div>
<AdminModalForm
action={createGroup}
description="Create a named access group. Both policies start denied unless you explicitly enable them below."
submitLabel="Create group"
title="Add access group"
triggerClassName="border border-ink bg-ink px-5 py-3 font-mono text-[10px] font-bold uppercase tracking-wider text-canvas"
triggerLabel="Add group"
>
<div className="space-y-5">
<label className="block text-sm font-bold">Name<input autoComplete="off" className="mt-2 w-full border border-line bg-canvas px-4 py-3 font-normal outline-none focus:border-accent" maxLength={50} name="name" required /></label>
<label className="block text-sm font-bold">Description<textarea className="mt-2 min-h-28 w-full resize-y border border-line bg-canvas px-4 py-3 font-normal outline-none focus:border-accent" maxLength={500} name="description" /></label>
<PolicyCheckbox description="Allow members to connect to Minecraft." label="Minecraft access" name="accessEnabled" />
<PolicyCheckbox description="Allow confirmed VPN, proxy, and Tor connections." label="VPN / proxy / Tor exception" name="anonymizedNetworksAllowed" />
</div>
</AdminModalForm>
</header>
{query.error && <p className="mt-7 border-l-2 border-accent bg-panel px-5 py-4 text-sm text-accent" role="alert">{errors[query.error] ?? "The group operation failed."}</p>}
{query.saved === "deleted" && <p className="mt-7 border-l-2 border-signal bg-panel px-5 py-4 text-sm" role="status">Group deleted. Its former members now use the default group.</p>}
{query.saved && <p className="mt-7 border-l-2 border-signal bg-panel px-5 py-4 text-sm" role="status">{savedMessages[query.saved] ?? "Group updated."}</p>}
<section className="mt-10 grid gap-5 md:grid-cols-2">
{allGroups.map((group) => {
const memberCount = group.isDefault ? registeredUsers.length - explicitlyAssignedUsers : membershipCounts.get(group.id) ?? 0;
return (
<article className="border border-line bg-panel p-6 shadow-[5px_5px_0_var(--color-shadow)]" key={group.id}>
<div className="flex items-start justify-between gap-4">
<div>
<div className="flex flex-wrap items-center gap-2">
<h2 className="font-display text-2xl font-black uppercase">{group.name}</h2>
{group.isDefault && <span className="bg-ink px-2 py-1 font-mono text-[9px] font-bold uppercase text-canvas">Default</span>}
</div>
<p className="mt-1 font-mono text-[10px] text-muted">{group.slug} · {memberCount} members</p>
</div>
<div className="flex flex-col items-end gap-2"><span className={`px-3 py-2 font-mono text-[9px] font-bold uppercase ${group.accessEnabled ? "bg-signal text-ink" : "bg-accent text-canvas"}`}>{group.accessEnabled ? "Access on" : "Access off"}</span><span className="font-mono text-[9px] font-bold uppercase text-muted">VPN {group.anonymizedNetworksAllowed ? "allowed" : "denied"}</span></div>
</div>
<p className="mt-4 min-h-12 text-sm leading-6 text-muted">{group.description ?? "No description."}</p>
<div className="mt-5 flex items-center justify-between gap-4 border-t border-line pt-4">
<Link className="font-mono text-[10px] font-bold uppercase underline underline-offset-4" href={`/admin/groups/${group.id}`}>Manage members</Link>
<form action={setGroupAccess}>
<input name="groupId" type="hidden" value={group.id} />
<input name="accessEnabled" type="hidden" value={group.accessEnabled ? "no" : "yes"} />
<button className="font-mono text-[10px] font-bold uppercase text-accent underline underline-offset-4" type="submit">Turn access {group.accessEnabled ? "off" : "on"}</button>
</form>
</div>
</article>
);
})}
</section>
<form action={createGroup} className="mt-12 border border-line bg-panel p-7 shadow-[8px_8px_0_var(--color-shadow)]">
<p className="font-mono text-[10px] font-bold uppercase tracking-widest text-accent">Create a group</p>
<div className="mt-5 grid gap-5 sm:grid-cols-2">
<label className="text-sm font-bold">Name<input className="mt-2 w-full border border-line bg-canvas px-4 py-3 font-normal outline-none focus:border-accent" maxLength={50} name="name" required /></label>
<label className="text-sm font-bold">Slug<input className="mt-2 w-full border border-line bg-canvas px-4 py-3 font-mono font-normal outline-none focus:border-accent" maxLength={50} name="slug" pattern="[a-z0-9]+(?:-[a-z0-9]+)*" placeholder="ops" required /></label>
</div>
<label className="mt-5 block text-sm font-bold">Description<textarea className="mt-2 min-h-24 w-full border border-line bg-canvas px-4 py-3 font-normal outline-none focus:border-accent" maxLength={500} name="description" /></label>
<p className="mt-4 text-xs text-muted">New groups start with Minecraft access and VPN/proxy/Tor exceptions disabled.</p>
<button className="mt-6 border border-ink bg-ink px-5 py-3 font-mono text-[10px] font-bold uppercase tracking-wider text-canvas" type="submit">Create group</button>
</form>
<div className="mt-9 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">
<caption className="sr-only">Access groups and their effective policies</caption>
<thead className="border-b border-line font-mono text-[10px] uppercase tracking-widest text-muted">
<tr><th className="p-4" scope="col">Name</th><th className="p-4" scope="col">Minecraft access</th><th className="p-4" scope="col">VPN access</th><th className="p-4 text-right" scope="col">Users</th></tr>
</thead>
<tbody className="divide-y divide-line">
{allGroups.map((group) => {
const memberCount = effectiveGroupMemberCount(
Number(registeredUsers?.count ?? 0),
memberships.map((membership) => membership.groupId),
group,
);
return (
<tr className="transition-colors hover:bg-canvas/60" key={group.id}>
<th className="p-4 text-left" scope="row">
<Link className="font-display text-xl font-black uppercase underline decoration-line underline-offset-4 hover:text-accent" href={`/admin/groups/${group.id}`}>{group.name}</Link>
{group.isDefault && <span className="ml-3 bg-ink px-2 py-1 font-mono text-[8px] font-bold uppercase text-canvas">Default</span>}
</th>
<td className="p-4"><GroupPolicyControl action={setGroupAccess} enabled={group.accessEnabled} groupId={group.id} groupName={group.name} memberCount={memberCount} policy="Minecraft access" returnLocation="list" /></td>
<td className="p-4"><GroupPolicyControl action={setGroupAnonymizedNetworkAccess} enabled={group.anonymizedNetworksAllowed} groupId={group.id} groupName={group.name} memberCount={memberCount} policy="VPN / proxy / Tor" returnLocation="list" /></td>
<td className="p-4 text-right font-mono text-sm font-bold">{memberCount}</td>
</tr>
);
})}
</tbody>
</table>
</div>
<p className="mt-4 text-xs leading-5 text-muted">Users without an explicit assignment count toward <strong className="text-ink">everyone</strong>.</p>
</main>
);
}
function PolicyCheckbox({ description, label, name }: { description: string; label: string; name: string }) {
return (
<label className="flex cursor-pointer items-start justify-between gap-4 border border-line bg-canvas p-4">
<span><span className="block font-mono text-xs font-bold uppercase">{label}</span><span className="mt-1 block text-xs leading-5 text-muted">{description}</span></span>
<input className="mt-1 size-5 accent-[var(--color-accent)]" name={name} type="checkbox" value="yes" />
</label>
);
}
@@ -14,6 +14,7 @@ import { redirect } from "next/navigation";
import { recordAdminEvent } from "@/lib/audit";
import { requireAdminSession } from "@/lib/auth/require-admin";
import { db } from "@/lib/database";
import { adminGroupReturnPath } from "@/lib/group-management";
const USERNAME_PATTERN = /^[A-Za-z0-9_]{3,16}$/;
const UUID_PATTERN = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
@@ -22,12 +23,6 @@ function userPath(userId: string, query?: string) {
return `/admin/users/${encodeURIComponent(userId)}${query ? `?${query}` : ""}`;
}
function userRegistryPath(search: string, result: "saved=group" | "error=invalid-group-assignment") {
const query = new URLSearchParams(result);
if (search) query.set("q", search);
return `/admin/users?${query.toString()}`;
}
async function targetUser(userId: string) {
if (!UUID_PATTERN.test(userId)) return null;
const [user] = await db.select().from(users).where(eq(users.id, userId)).limit(1);
@@ -85,20 +80,26 @@ export async function assignUserGroupFromRegistry(formData: FormData) {
const admin = await requireAdminSession();
const userId = String(formData.get("userId") ?? "");
const groupId = String(formData.get("groupId") ?? "");
const search = String(formData.get("search") ?? "").trim().slice(0, 100);
if (!UUID_PATTERN.test(userId) || !UUID_PATTERN.test(groupId)) redirect(userRegistryPath(search, "error=invalid-group-assignment"));
const [[user], [targetGroup]] = await Promise.all([
db.select({ id: users.id }).from(users).where(eq(users.id, userId)).limit(1),
db.select({ id: groups.id, name: groups.name, isDefault: groups.isDefault }).from(groups).where(eq(groups.id, groupId)).limit(1),
]);
if (!user || !targetGroup) redirect(userRegistryPath(search, "error=invalid-group-assignment"));
const returnTo = formData.get("returnTo");
if (!UUID_PATTERN.test(userId) || !UUID_PATTERN.test(groupId)) redirect(adminGroupReturnPath(returnTo, "error=invalid-group-assignment"));
const requestHeaders = await headers();
const ipAddress = getClientIp(requestHeaders, process.env.TRUST_PROXY === "true");
try {
await db.transaction(async (tx) => {
await tx.execute(sql`select ${users.id} from ${users} where ${users.id} = ${user.id} for update`);
await tx.execute(sql`select pg_advisory_xact_lock(hashtext('minecraft-account-manager-group-membership'))`);
await tx.execute(sql`select ${groups.id} from ${groups} where ${groups.id} = ${groupId} for update`);
await tx.execute(sql`select ${users.id} from ${users} where ${users.id} = ${userId} for update`);
const [[user], [targetGroup]] = await Promise.all([
tx.select({ id: users.id }).from(users).where(eq(users.id, userId)).limit(1),
tx.select({ id: groups.id, name: groups.name, isDefault: groups.isDefault }).from(groups).where(eq(groups.id, groupId)).limit(1),
]);
if (!user || !targetGroup) throw new Error("User or destination group no longer exists");
const [membership] = await tx.select({ groupId: userGroupMemberships.groupId })
.from(userGroupMemberships).where(eq(userGroupMemberships.userId, user.id)).limit(1);
if (membership && membership.groupId !== targetGroup.id) {
await tx.execute(sql`select ${groups.id} from ${groups} where ${groups.id} = ${membership.groupId} for update`);
}
const [previous] = await tx.select({ id: groups.id, name: groups.name })
.from(userGroupMemberships)
.innerJoin(groups, eq(groups.id, userGroupMemberships.groupId))
@@ -131,9 +132,9 @@ export async function assignUserGroupFromRegistry(formData: FormData) {
});
});
} catch {
redirect(userRegistryPath(search, "error=invalid-group-assignment"));
redirect(adminGroupReturnPath(returnTo, "error=invalid-group-assignment"));
}
redirect(userRegistryPath(search, "saved=group"));
redirect(adminGroupReturnPath(returnTo, "saved=group"));
}
export async function updateUserName(formData: FormData) {
@@ -1,7 +1,6 @@
import { groups, minecraftAccounts, userGroupMemberships, users } from "@minecraft-account-manager/database";
import { and, asc, desc, eq, ilike, isNull, or, sql } from "drizzle-orm";
import Link from "next/link";
import { UserGroupSelect } from "@/components/user-group-select";
import { AdminUserTable } from "@/components/admin-user-table";
import { db } from "@/lib/database";
import { assignUserGroupFromRegistry } from "./actions";
@@ -66,8 +65,8 @@ export default async function AdminUsersPage({
db.select({ userId: userGroupMemberships.userId, groupId: userGroupMemberships.groupId })
.from(userGroupMemberships),
]);
const defaultGroup = allGroups.find((group) => group.isDefault);
const groupByUser = new Map(memberships.map((membership) => [membership.userId, membership.groupId]));
const assignmentByUser = Object.fromEntries(memberships.map((membership) => [membership.userId, membership.groupId]));
const returnTo = `/admin/users${search ? `?${new URLSearchParams({ q: search }).toString()}` : ""}`;
return (
<main className="mx-auto max-w-6xl px-6 py-14">
@@ -92,26 +91,8 @@ export default async function AdminUsersPage({
{query.error && <p className="mt-7 border-l-2 border-accent bg-panel px-5 py-4 text-sm text-accent" role="alert">{query.error === "invalid-group-assignment" ? "The user or group no longer exists. No group change was applied." : "The requested user could not be found."}</p>}
{query.saved === "group" && <p className="mt-7 border-l-2 border-signal bg-panel px-5 py-4 text-sm" role="status">User group updated.</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-[900px] border-collapse text-left">
<caption className="sr-only">Registered portal users</caption>
<thead className="border-b border-line font-mono text-[10px] uppercase tracking-widest text-muted">
<tr><th className="p-4" scope="col">User</th><th className="p-4" scope="col">Discord</th><th className="p-4" scope="col">Primary</th><th className="p-4" scope="col">Accounts</th><th className="p-4" scope="col">Group</th><th className="p-4" scope="col">Status</th></tr>
</thead>
<tbody className="divide-y divide-line">
{results.map((user) => (
<tr className="transition-colors hover:bg-canvas/60" key={user.id}>
<th className="p-4 text-left" scope="row"><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></th>
<td className="p-4"><div className="font-mono text-xs font-bold">{user.discordGlobalName ?? user.discordUsername}</div><div className="mt-1 font-mono text-[10px] text-muted">@{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">{defaultGroup ? <UserGroupSelect action={assignUserGroupFromRegistry} effectiveGroupId={groupByUser.get(user.id) ?? defaultGroup.id} groups={allGroups} search={search} userId={user.id} userLabel={user.firstName ?? user.discordUsername} /> : <span className="text-xs text-accent">Default group missing</span>}</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={6}>No users match that search.</td></tr>}
</tbody>
</table>
<div className="mt-8">
<AdminUserTable action={assignUserGroupFromRegistry} assignmentByUser={assignmentByUser} emptyMessage="No users match that search." groups={allGroups} returnTo={returnTo} users={results} />
</div>
<p className="mt-4 font-mono text-[9px] uppercase tracking-widest text-muted">Showing up to 100 users</p>
</main>