import { resolveEffectiveGroup } from "@minecraft-account-manager/auth"; import { formatManagedDiscordNickname } from "@minecraft-account-manager/minecraft"; import { events, groups, ipObservations, minecraftAccounts, userGroupMemberships, users } from "@minecraft-account-manager/database"; import { and, desc, eq, inArray, 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 { eventCategory, eventCategoryValues, normalizeEventCategory, normalizeSelectedEventTypes } from "@/lib/event-filters"; import { eventIpSummary } from "@/lib/event-ip-summary"; import { addUserMinecraftAccount, removeUserMinecraftAccount, setUserPrimaryAccount, synchronizeUserNickname, updateUserName, } from "../actions"; const errorMessages: Record = { "invalid-name": "Enter a valid name between 1 and 50 characters.", "invalid-username": "Java usernames use 3–16 letters, numbers, or underscores.", "mojang-unavailable": "Mojang profile lookup is unavailable. No account was added.", "already-registered": "That Minecraft account is already actively registered.", "unknown-account": "That account is no longer active for this user.", "missing-name": "Set the user's name before synchronizing Discord.", "discord-update": "Discord rejected the nickname update. No requested profile change was saved.", }; const savedMessages: Record = { name: "Name and Discord nickname updated.", "account-added": "Minecraft account added.", "account-removed": "Minecraft account removed.", primary: "Primary account and Discord nickname updated.", nickname: "Discord nickname synchronized.", }; export const dynamic = "force-dynamic"; function queryValues(value: string | string[] | undefined) { return Array.isArray(value) ? value : value ? [value] : []; } export default async function AdminUserPage({ params, searchParams, }: { params: Promise<{ userId: string }>; searchParams: Promise>; }) { const { userId } = await params; const query = await searchParams; const error = queryValues(query.error)[0]; const saved = queryValues(query.saved)[0]; const unverified = queryValues(query.unverified)[0]; const [user] = await db.select().from(users).where(eq(users.id, userId)).limit(1); if (!user) notFound(); const userEventCondition = or(eq(events.subject, `user/${user.id}`), eq(events.actorUserId, user.id)); const eventTypeRows = await db.select({ type: events.type }).from(events).where(userEventCondition).groupBy(events.type).orderBy(events.type); const availableEventTypes = eventTypeRows.map((row) => row.type); const selectedCategory = normalizeEventCategory(queryValues(query.eventCategory)[0]); const selectedEventTypes = normalizeSelectedEventTypes(query.eventType, availableEventTypes); const categoryTypes = selectedCategory === "all" ? availableEventTypes : availableEventTypes.filter((type) => eventCategory(type) === selectedCategory); const filteredEventTypes = selectedEventTypes.length ? selectedEventTypes.filter((type) => categoryTypes.includes(type)) : categoryTypes; const recentEventsQuery = filteredEventTypes.length ? db.select().from(events).where(and(userEventCondition, inArray(events.type, filteredEventTypes))).orderBy(desc(events.time)).limit(30) : Promise.resolve([]); const [accounts, recentEvents, observations, discord, availableGroups] = await Promise.all([ db .select() .from(minecraftAccounts) .where(and(eq(minecraftAccounts.userId, user.id), isNull(minecraftAccounts.deletedAt))) .orderBy(desc(minecraftAccounts.isPrimary), minecraftAccounts.username), recentEventsQuery, db .select() .from(ipObservations) .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, anonymizedNetworksAllowed: groups.anonymizedNetworksAllowed, 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 explicitGroup = availableGroups.find((group) => !group.isDefault) ?? null; const defaultGroup = availableGroups.find((group) => group.isDefault) ?? null; const effectiveGroup = resolveEffectiveGroup(explicitGroup, defaultGroup); const addressGroups = groupAccessAddresses( observations.map((observation) => ({ ...observation, intelligence: null })), ); const primary = accounts.find((account) => account.isPrimary); const nickname = user.firstName ? formatManagedDiscordNickname(user.firstName, primary?.username ?? null) : null; return (
← All users

User record

{user.firstName ?? "Name needed"}

Discord name
{discord.globalName ?? discord.username}
Discord username
@{discord.username}
Guild nickname
{discord.nickname ?? "No guild nickname"}
Discord ID
{discord.id}

Expected Discord nickname

{nickname ?? "Set a first name"}

{nickname &&
}
{error &&

{errorMessages[error] ?? "The requested operation failed."}

} {saved &&

{savedMessages[saved] ?? "Changes saved."}

}

Whitelist identities

Minecraft accounts

{accounts.length} active
{accounts.map((account) => (
{account.username} {account.isPrimary && Primary} {account.validationStatus === "verified" ? "UUID verified" : "Unverified override"}

{account.minecraftUuid ?? "No UUID recorded"}

{!account.isPrimary &&
}
Remove

Remove {account.username}? This immediately blocks it from joining.

))} {!accounts.length &&

No active Minecraft accounts.

}
{unverified ? (

Mojang couldn’t verify “{unverified}”

Only override this when you have independently confirmed the spelling.

Cancel
) : (
)}

Audit trail

Recent events

Types
{selectedEventTypes.length ? `${selectedEventTypes.length} selected` : "All types"}
{availableEventTypes.map((type) => )}
Clear
{recentEvents.map((event) => { const ip = eventIpSummary(event.data); return

{event.type}

{(ip.location || ip.classification) &&

{ip.location ?? "Location unavailable"} · {ip.classification ?? "unknown"}

}
; })} {!recentEvents.length &&

No events recorded for this user.

}
); }