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
@@ -1,10 +1,11 @@
import { formatManagedDiscordNickname } from "@minecraft-account-manager/minecraft";
import { events, ipObservations, minecraftAccounts, users } from "@minecraft-account-manager/database";
import { events, groups, ipObservations, minecraftAccounts, userGroupMemberships, users } from "@minecraft-account-manager/database";
import { and, desc, eq, isNull, or } from "drizzle-orm";
import Link from "next/link";
import { notFound } from "next/navigation";
import { groupAccessAddresses } from "@/lib/access-address-groups";
import { db } from "@/lib/database";
import { discordIdentity } from "@/lib/discord-identity";
import { eventIpSummary } from "@/lib/event-ip-summary";
import {
addUserMinecraftAccount,
@@ -44,7 +45,7 @@ export default async function AdminUserPage({
const [user] = await db.select().from(users).where(eq(users.id, userId)).limit(1);
if (!user) notFound();
const [accounts, recentEvents, observations] = await Promise.all([
const [accounts, recentEvents, observations, discord, accessGroups] = await Promise.all([
db
.select()
.from(minecraftAccounts)
@@ -62,6 +63,12 @@ export default async function AdminUserPage({
.where(eq(ipObservations.userId, user.id))
.orderBy(desc(ipObservations.observedAt))
.limit(100),
discordIdentity(user),
db.select({ id: groups.id, name: groups.name, accessEnabled: groups.accessEnabled, isDefault: groups.isDefault })
.from(groups)
.leftJoin(userGroupMemberships, eq(userGroupMemberships.groupId, groups.id))
.where(or(eq(groups.isDefault, true), eq(userGroupMemberships.userId, user.id)))
.orderBy(desc(groups.isDefault), groups.name),
]);
const addressGroups = groupAccessAddresses(
observations.map((observation) => ({ ...observation, intelligence: null })),
@@ -78,7 +85,12 @@ export default async function AdminUserPage({
<div>
<p className="font-mono text-xs font-bold uppercase tracking-[0.25em] text-accent">User record</p>
<h1 className="mt-4 font-display text-5xl font-black uppercase sm:text-7xl">{user.firstName ?? "Name needed"}</h1>
<p className="mt-3 font-mono text-xs text-muted">@{user.discordUsername} · {user.discordUserId}</p>
<dl className="mt-4 grid gap-x-8 gap-y-2 font-mono text-[10px] text-muted sm:grid-cols-2">
<div><dt className="uppercase tracking-wider">Discord name</dt><dd className="mt-1 text-xs text-ink">{discord.globalName ?? discord.username}</dd></div>
<div><dt className="uppercase tracking-wider">Discord username</dt><dd className="mt-1 text-xs text-ink">@{discord.username}</dd></div>
<div><dt className="uppercase tracking-wider">Guild nickname</dt><dd className="mt-1 text-xs text-ink">{discord.nickname ?? "No guild nickname"}</dd></div>
<div><dt className="uppercase tracking-wider">Discord ID</dt><dd className="mt-1 break-all text-xs text-ink">{discord.id}</dd></div>
</dl>
</div>
<div className="border-l-2 border-accent pl-5">
<p className="font-mono text-[9px] font-bold uppercase tracking-widest text-muted">Expected Discord nickname</p>
@@ -164,6 +176,13 @@ export default async function AdminUserPage({
<button className="mt-6 border border-ink px-4 py-2 font-mono text-[9px] font-bold uppercase tracking-wider" type="submit">Save and synchronize</button>
</form>
<section className="border border-line bg-panel p-6">
<div className="flex items-center justify-between gap-3"><p className="font-mono text-[9px] font-bold uppercase tracking-widest text-muted">Access groups</p><Link className="font-mono text-[9px] font-bold uppercase underline underline-offset-4" href="/admin/groups">Manage</Link></div>
<div className="mt-4 space-y-3">
{accessGroups.map((group) => <div className="flex items-center justify-between gap-3" key={group.id}><Link className="font-mono text-xs font-bold underline decoration-line underline-offset-4" href={`/admin/groups/${group.id}`}>{group.name}{group.isDefault ? " · default" : ""}</Link><span className={`px-2 py-1 font-mono text-[9px] font-bold uppercase ${group.accessEnabled ? "bg-signal text-ink" : "bg-accent text-canvas"}`}>{group.accessEnabled ? "On" : "Off"}</span></div>)}
</div>
</section>
<section className="border border-line bg-panel p-6">
<p className="font-mono text-[9px] font-bold uppercase tracking-widest text-muted">Recent addresses</p>
<p className="mt-3 text-[10px] leading-5 text-muted">Grouped by IPv4 /24 or IPv6 /64 network across the 100 most recent observations.</p>