feat(groups): add fail-closed admission management
CI / validate (push) Successful in 5m13s
Release / release (push) Successful in 7m8s

This commit is contained in:
dmg
2026-08-01 18:36:45 -04:00
parent 19a5d04178
commit b88097c15a
32 changed files with 1910 additions and 29 deletions
@@ -0,0 +1,92 @@
import { groups, userGroupMemberships, users } from "@minecraft-account-manager/database";
import { asc, eq } from "drizzle-orm";
import Link from "next/link";
import { notFound } from "next/navigation";
import { db } from "@/lib/database";
import { addGroupMember, removeGroupMember, setGroupAccess } from "../actions";
const savedMessages: Record<string, string> = {
created: "Group created with access disabled.",
access: "Group access policy updated.",
"member-added": "User added to the group.",
"member-removed": "User removed from the group.",
};
export default async function GroupPage({
params,
searchParams,
}: {
params: Promise<{ groupId: string }>;
searchParams: Promise<{ 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([
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 }).from(userGroupMemberships)
.where(eq(userGroupMemberships.groupId, group.id)),
]);
const memberIds = new Set(memberships.map((membership) => membership.userId));
return (
<main className="mx-auto max-w-6xl px-6 py-12">
<Link className="font-mono text-[10px] font-bold uppercase tracking-widest text-muted underline underline-offset-4" href="/admin/groups"> All groups</Link>
<header className="mt-7 flex flex-col gap-6 border-b border-line pb-8 lg:flex-row lg:items-end lg:justify-between">
<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>
<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>
</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">{savedMessages[query.saved] ?? "Group updated."}</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">{group.isDefault ? allUsers.length : memberIds.size} members</span>
</div>
{group.isDefault && <p className="border-b border-line bg-panel px-5 py-4 text-sm text-muted">Membership in <strong className="text-ink">everyone</strong> is automatic and cannot be removed.</p>}
<div className="divide-y divide-line">
{allUsers.map((user) => {
const isMember = group.isDefault || memberIds.has(user.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>
</div>
{group.isDefault ? (
<span className="font-mono text-[9px] font-bold uppercase text-muted">Automatic member</span>
) : (
<form action={isMember ? removeGroupMember : 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 underline underline-offset-4 ${isMember ? "text-accent" : "text-ink"}`} type="submit">{isMember ? "Remove from group" : "Add to group"}</button>
</form>
)}
</article>
);
})}
{!allUsers.length && <p className="py-8 text-sm text-muted">No registered users yet.</p>}
</div>
</section>
</main>
);
}