feat(groups): add fail-closed admission management
This commit is contained in:
@@ -0,0 +1,78 @@
|
||||
import { groups, userGroupMemberships, users } from "@minecraft-account-manager/database";
|
||||
import { asc, desc } from "drizzle-orm";
|
||||
import Link from "next/link";
|
||||
import { db } from "@/lib/database";
|
||||
import { createGroup, setGroupAccess } 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.",
|
||||
"create-failed": "The group could not be created.",
|
||||
"unknown-group": "That group no longer exists.",
|
||||
"invalid-membership": "That membership change was invalid.",
|
||||
};
|
||||
|
||||
export default async function GroupsPage({ searchParams }: { searchParams: Promise<{ error?: string }> }) {
|
||||
const query = await searchParams;
|
||||
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),
|
||||
]);
|
||||
const membershipCounts = new Map<string, number>();
|
||||
for (const membership of memberships) {
|
||||
membershipCounts.set(membership.groupId, (membershipCounts.get(membership.groupId) ?? 0) + 1);
|
||||
}
|
||||
|
||||
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">Every registered user belongs to <strong className="text-ink">everyone</strong>. A player can join only when at least one of their groups has access enabled.</p>
|
||||
</header>
|
||||
|
||||
{query.error && <p className="mt-7 border-l-2 border-accent bg-panel px-5 py-4 text-sm text-accent">{errors[query.error] ?? "The group operation failed."}</p>}
|
||||
|
||||
<section className="mt-10 grid gap-5 md:grid-cols-2">
|
||||
{allGroups.map((group) => {
|
||||
const memberCount = group.isDefault ? registeredUsers.length : 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>
|
||||
<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>
|
||||
</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 access 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>
|
||||
</main>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user