From 10554eeaff0bf83f5894b7d8fb9e8678fdc49fdb Mon Sep 17 00:00:00 2001 From: Dylan Garvis Date: Sat, 1 Aug 2026 14:16:46 -0400 Subject: [PATCH] feat(admin): add user account management --- README.md | 1 + apps/web/src/app/admin/(console)/actions.ts | 7 +- apps/web/src/app/admin/(console)/layout.tsx | 1 + .../admin/(console)/users/[userId]/page.tsx | 170 ++++++++ .../src/app/admin/(console)/users/actions.ts | 370 ++++++++++++++++++ .../src/app/admin/(console)/users/page.tsx | 101 +++++ apps/web/src/lib/audit.ts | 17 + apps/web/src/lib/auth/require-admin.ts | 14 + packages/minecraft/src/index.ts | 8 + packages/minecraft/test/minecraft.test.ts | 7 +- 10 files changed, 690 insertions(+), 6 deletions(-) create mode 100644 apps/web/src/app/admin/(console)/users/[userId]/page.tsx create mode 100644 apps/web/src/app/admin/(console)/users/actions.ts create mode 100644 apps/web/src/app/admin/(console)/users/page.tsx create mode 100644 apps/web/src/lib/auth/require-admin.ts diff --git a/README.md b/README.md index db44336..43d5088 100644 --- a/README.md +++ b/README.md @@ -63,6 +63,7 @@ The token is displayed once and stored only as a SHA-256 hash. - PostgreSQL and Drizzle ORM - Keycloak OIDC for admin access with the `minecraft-account-manager-admin` role +- Admin user search, account management, primary-account changes, and Discord nickname synchronization - Deployment-managed Discord guild ID and invite URL - discord.js bot with `/register` and `/account` - Java Edition online-mode accounts only diff --git a/apps/web/src/app/admin/(console)/actions.ts b/apps/web/src/app/admin/(console)/actions.ts index 17b6802..9f898a3 100644 --- a/apps/web/src/app/admin/(console)/actions.ts +++ b/apps/web/src/app/admin/(console)/actions.ts @@ -1,15 +1,12 @@ "use server"; import { appSettings } from "@minecraft-account-manager/database"; -import { getServerSession } from "next-auth"; import { redirect } from "next/navigation"; -import { adminAuthOptions, requiredAdminRole } from "@/lib/auth/admin-auth"; +import { requireAdminSession } from "@/lib/auth/require-admin"; import { db } from "@/lib/database"; export async function saveDiscordSettings(formData: FormData) { - const session = await getServerSession(adminAuthOptions); - const roles = (session?.user as { roles?: string[] } | undefined)?.roles ?? []; - if (!session || !roles.includes(requiredAdminRole)) redirect("/admin/login"); + await requireAdminSession(); const registrationMessage = String(formData.get("registrationMessage") ?? "").trim(); diff --git a/apps/web/src/app/admin/(console)/layout.tsx b/apps/web/src/app/admin/(console)/layout.tsx index 4ebf4cb..a8de27f 100644 --- a/apps/web/src/app/admin/(console)/layout.tsx +++ b/apps/web/src/app/admin/(console)/layout.tsx @@ -33,6 +33,7 @@ export default async function AdminConsoleLayout({ children }: { children: React Blocklist / Ops diff --git a/apps/web/src/app/admin/(console)/users/[userId]/page.tsx b/apps/web/src/app/admin/(console)/users/[userId]/page.tsx new file mode 100644 index 0000000..6fd324c --- /dev/null +++ b/apps/web/src/app/admin/(console)/users/[userId]/page.tsx @@ -0,0 +1,170 @@ +import { formatManagedDiscordNickname } from "@minecraft-account-manager/minecraft"; +import { events, ipObservations, minecraftAccounts, 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 { db } from "@/lib/database"; +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 default async function AdminUserPage({ + params, + searchParams, +}: { + params: Promise<{ userId: string }>; + searchParams: Promise<{ error?: string; saved?: string; unverified?: string }>; +}) { + const { userId } = await params; + const query = await searchParams; + const [user] = await db.select().from(users).where(eq(users.id, userId)).limit(1); + if (!user) notFound(); + + const [accounts, recentEvents, observations] = await Promise.all([ + db + .select() + .from(minecraftAccounts) + .where(and(eq(minecraftAccounts.userId, user.id), isNull(minecraftAccounts.deletedAt))) + .orderBy(desc(minecraftAccounts.isPrimary), minecraftAccounts.username), + db + .select() + .from(events) + .where(or(eq(events.subject, `user/${user.id}`), eq(events.actorUserId, user.id))) + .orderBy(desc(events.time)) + .limit(30), + db + .select() + .from(ipObservations) + .where(eq(ipObservations.userId, user.id)) + .orderBy(desc(ipObservations.observedAt)) + .limit(20), + ]); + 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"}

+

@{user.discordUsername} · {user.discordUserId}

+
+
+

Expected Discord nickname

+

{nickname ?? "Set a first name"}

+ {nickname &&
} +
+
+ + {query.error &&

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

} + {query.saved &&

{savedMessages[query.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.

} +
+ + {query.unverified ? ( +
+ +

Mojang couldn’t verify “{query.unverified}”

+

Only override this when you have independently confirmed the spelling.

+ + Cancel +
+ ) : ( +
+ + + +
+ )} +
+ +
+

Audit trail

+

Recent events

+
+ {recentEvents.map((event) =>
{event.type}{event.time.toISOString()}
)} + {!recentEvents.length &&

No events recorded for this user.

} +
+
+
+ + +
+
+ ); +} diff --git a/apps/web/src/app/admin/(console)/users/actions.ts b/apps/web/src/app/admin/(console)/users/actions.ts new file mode 100644 index 0000000..e583d2f --- /dev/null +++ b/apps/web/src/app/admin/(console)/users/actions.ts @@ -0,0 +1,370 @@ +"use server"; + +import { + formatManagedDiscordNickname, + lookupJavaProfile, + updateGuildNickname, +} from "@minecraft-account-manager/minecraft"; +import { minecraftAccounts, users } from "@minecraft-account-manager/database"; +import { and, eq, isNull, ne } from "drizzle-orm"; +import { redirect } from "next/navigation"; +import { recordAdminEvent } from "@/lib/audit"; +import { requireAdminSession } from "@/lib/auth/require-admin"; +import { db } from "@/lib/database"; + +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; + +function userPath(userId: string, query?: string) { + return `/admin/users/${encodeURIComponent(userId)}${query ? `?${query}` : ""}`; +} + +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); + return user ?? null; +} + +async function primaryUsername(userId: string) { + const [account] = await db + .select({ username: minecraftAccounts.username }) + .from(minecraftAccounts) + .where( + and( + eq(minecraftAccounts.userId, userId), + eq(minecraftAccounts.isPrimary, true), + isNull(minecraftAccounts.deletedAt), + ), + ) + .limit(1); + return account?.username ?? null; +} + +async function synchronizeNickname(input: { + discordUserId: string; + firstName: string; + minecraftUsername: string | null; +}) { + const guildId = process.env.DISCORD_GUILD_ID?.trim(); + const botToken = process.env.DISCORD_BOT_TOKEN?.trim(); + if (!guildId || !botToken) throw new Error("Discord nickname updates are not configured"); + + const nickname = formatManagedDiscordNickname(input.firstName, input.minecraftUsername); + await updateGuildNickname({ + guildId, + discordUserId: input.discordUserId, + nickname, + botToken, + }); + return nickname; +} + +async function recordSyncFailure( + admin: Awaited>, + userId: string, + operation: string, +) { + await recordAdminEvent( + admin, + userId, + "games.minecraft.account-manager.discord.nickname.update-failed", + { operation }, + ); +} + +export async function updateUserName(formData: FormData) { + const admin = await requireAdminSession(); + const userId = String(formData.get("userId") ?? ""); + const firstName = String(formData.get("firstName") ?? "").trim(); + const user = await targetUser(userId); + + if (!user) redirect("/admin/users?error=unknown-user"); + if (firstName.length < 1 || firstName.length > 50 || /[\u0000-\u001f\u007f]/.test(firstName)) { + redirect(userPath(user.id, "error=invalid-name")); + } + + const minecraftUsername = await primaryUsername(user.id); + let nickname: string; + try { + nickname = await synchronizeNickname({ + discordUserId: user.discordUserId, + firstName, + minecraftUsername, + }); + } catch { + await recordSyncFailure(admin, user.id, "update-first-name"); + redirect(userPath(user.id, "error=discord-update")); + } + + await db.update(users).set({ firstName, updatedAt: new Date() }).where(eq(users.id, user.id)); + await recordAdminEvent( + admin, + user.id, + "games.minecraft.account-manager.user.first-name.updated", + { firstName, nickname }, + ); + await recordAdminEvent( + admin, + user.id, + "games.minecraft.account-manager.discord.nickname.updated", + { nickname, operation: "update-first-name" }, + ); + redirect(userPath(user.id, "saved=name")); +} + +export async function addUserMinecraftAccount(formData: FormData) { + const admin = await requireAdminSession(); + const userId = String(formData.get("userId") ?? ""); + const requestedUsername = String(formData.get("username") ?? "").trim(); + const forceUnverified = formData.get("forceUnverified") === "yes"; + const user = await targetUser(userId); + + if (!user) redirect("/admin/users?error=unknown-user"); + if (!USERNAME_PATTERN.test(requestedUsername)) { + redirect(userPath(user.id, "error=invalid-username")); + } + + let profile: Awaited>; + try { + profile = await lookupJavaProfile(requestedUsername); + } catch { + redirect(userPath(user.id, "error=mojang-unavailable")); + } + if (!profile && !forceUnverified) { + redirect(userPath(user.id, `unverified=${encodeURIComponent(requestedUsername)}`)); + } + + const [existing] = await db + .select({ id: minecraftAccounts.id }) + .from(minecraftAccounts) + .where(and(eq(minecraftAccounts.userId, user.id), isNull(minecraftAccounts.deletedAt))) + .limit(1); + + let account: { id: string } | undefined; + try { + [account] = await db + .insert(minecraftAccounts) + .values({ + userId: user.id, + minecraftUuid: profile?.uuid ?? null, + username: profile?.username ?? requestedUsername, + validationStatus: profile ? "verified" : "user_confirmed", + lastVerifiedAt: profile ? new Date() : null, + isPrimary: !existing, + }) + .returning({ id: minecraftAccounts.id }); + } catch { + redirect(userPath(user.id, "error=already-registered")); + } + + const username = profile?.username ?? requestedUsername; + let nickname: string | null = null; + if (!existing && user.firstName && account) { + try { + nickname = await synchronizeNickname({ + discordUserId: user.discordUserId, + firstName: user.firstName, + minecraftUsername: username, + }); + } catch { + await db.delete(minecraftAccounts).where(eq(minecraftAccounts.id, account.id)); + await recordSyncFailure(admin, user.id, "add-first-account"); + redirect(userPath(user.id, "error=discord-update")); + } + } + + await recordAdminEvent( + admin, + user.id, + "games.minecraft.account-manager.minecraft-account.added", + { + accountId: account?.id, + username, + minecraftUuid: profile?.uuid ?? null, + validationStatus: profile ? "verified" : "user_confirmed", + }, + ); + if (nickname) { + await recordAdminEvent( + admin, + user.id, + "games.minecraft.account-manager.discord.nickname.updated", + { nickname, operation: "add-first-account" }, + ); + } + redirect(userPath(user.id, "saved=account-added")); +} + +export async function setUserPrimaryAccount(formData: FormData) { + const admin = await requireAdminSession(); + const userId = String(formData.get("userId") ?? ""); + const accountId = String(formData.get("accountId") ?? ""); + const user = await targetUser(userId); + if (!user) redirect("/admin/users?error=unknown-user"); + + const [account] = await db + .select({ id: minecraftAccounts.id, username: minecraftAccounts.username }) + .from(minecraftAccounts) + .where( + and( + eq(minecraftAccounts.id, accountId), + eq(minecraftAccounts.userId, user.id), + isNull(minecraftAccounts.deletedAt), + ), + ) + .limit(1); + if (!account) redirect(userPath(user.id, "error=unknown-account")); + if (!user.firstName) redirect(userPath(user.id, "error=missing-name")); + + let nickname: string; + try { + nickname = await synchronizeNickname({ + discordUserId: user.discordUserId, + firstName: user.firstName, + minecraftUsername: account.username, + }); + } catch { + await recordSyncFailure(admin, user.id, "set-primary-account"); + redirect(userPath(user.id, "error=discord-update")); + } + + await db.transaction(async (tx) => { + await tx + .update(minecraftAccounts) + .set({ isPrimary: false, updatedAt: new Date() }) + .where(and(eq(minecraftAccounts.userId, user.id), isNull(minecraftAccounts.deletedAt))); + await tx + .update(minecraftAccounts) + .set({ isPrimary: true, updatedAt: new Date() }) + .where(eq(minecraftAccounts.id, account.id)); + }); + + await recordAdminEvent( + admin, + user.id, + "games.minecraft.account-manager.minecraft-account.primary-changed", + { accountId: account.id, username: account.username, nickname }, + ); + await recordAdminEvent( + admin, + user.id, + "games.minecraft.account-manager.discord.nickname.updated", + { nickname, operation: "set-primary-account" }, + ); + redirect(userPath(user.id, "saved=primary")); +} + +export async function removeUserMinecraftAccount(formData: FormData) { + const admin = await requireAdminSession(); + const userId = String(formData.get("userId") ?? ""); + const accountId = String(formData.get("accountId") ?? ""); + const user = await targetUser(userId); + if (!user) redirect("/admin/users?error=unknown-user"); + + const [account] = await db + .select({ id: minecraftAccounts.id, username: minecraftAccounts.username, isPrimary: minecraftAccounts.isPrimary }) + .from(minecraftAccounts) + .where( + and( + eq(minecraftAccounts.id, accountId), + eq(minecraftAccounts.userId, user.id), + isNull(minecraftAccounts.deletedAt), + ), + ) + .limit(1); + if (!account) redirect(userPath(user.id, "error=unknown-account")); + if (account.isPrimary && !user.firstName) { + redirect(userPath(user.id, "error=missing-name")); + } + + const [replacement] = account.isPrimary + ? await db + .select({ id: minecraftAccounts.id, username: minecraftAccounts.username }) + .from(minecraftAccounts) + .where( + and( + eq(minecraftAccounts.userId, user.id), + ne(minecraftAccounts.id, account.id), + isNull(minecraftAccounts.deletedAt), + ), + ) + .limit(1) + : [undefined]; + + let nickname: string | null = null; + if (account.isPrimary && user.firstName) { + try { + nickname = await synchronizeNickname({ + discordUserId: user.discordUserId, + firstName: user.firstName, + minecraftUsername: replacement?.username ?? null, + }); + } catch { + await recordSyncFailure(admin, user.id, "remove-primary-account"); + redirect(userPath(user.id, "error=discord-update")); + } + } + + await db.transaction(async (tx) => { + await tx + .update(minecraftAccounts) + .set({ deletedAt: new Date(), isPrimary: false, updatedAt: new Date() }) + .where(eq(minecraftAccounts.id, account.id)); + if (replacement) { + await tx + .update(minecraftAccounts) + .set({ isPrimary: true, updatedAt: new Date() }) + .where(eq(minecraftAccounts.id, replacement.id)); + } + }); + + await recordAdminEvent( + admin, + user.id, + "games.minecraft.account-manager.minecraft-account.removed", + { + accountId: account.id, + username: account.username, + replacementAccountId: replacement?.id ?? null, + nickname, + }, + ); + if (nickname) { + await recordAdminEvent( + admin, + user.id, + "games.minecraft.account-manager.discord.nickname.updated", + { nickname, operation: "remove-primary-account" }, + ); + } + redirect(userPath(user.id, "saved=account-removed")); +} + +export async function synchronizeUserNickname(formData: FormData) { + const admin = await requireAdminSession(); + const userId = String(formData.get("userId") ?? ""); + const user = await targetUser(userId); + if (!user) redirect("/admin/users?error=unknown-user"); + if (!user.firstName) redirect(userPath(user.id, "error=missing-name")); + + const minecraftUsername = await primaryUsername(user.id); + let nickname: string; + try { + nickname = await synchronizeNickname({ + discordUserId: user.discordUserId, + firstName: user.firstName, + minecraftUsername, + }); + } catch { + await recordSyncFailure(admin, user.id, "manual-sync"); + redirect(userPath(user.id, "error=discord-update")); + } + + await recordAdminEvent( + admin, + user.id, + "games.minecraft.account-manager.discord.nickname.updated", + { nickname, operation: "manual-sync" }, + ); + redirect(userPath(user.id, "saved=nickname")); +} diff --git a/apps/web/src/app/admin/(console)/users/page.tsx b/apps/web/src/app/admin/(console)/users/page.tsx new file mode 100644 index 0000000..9e79c83 --- /dev/null +++ b/apps/web/src/app/admin/(console)/users/page.tsx @@ -0,0 +1,101 @@ +import { minecraftAccounts, users } from "@minecraft-account-manager/database"; +import { and, eq, ilike, isNull, or, sql } from "drizzle-orm"; +import Link from "next/link"; +import { db } from "@/lib/database"; + +export default async function AdminUsersPage({ + searchParams, +}: { + searchParams: Promise<{ q?: string; error?: string }>; +}) { + const query = await searchParams; + const search = query.q?.trim().slice(0, 100) ?? ""; + const pattern = `%${search}%`; + const where = search + ? or( + ilike(users.firstName, pattern), + ilike(users.discordUsername, pattern), + eq(users.discordUserId, search), + sql`exists ( + select 1 from ${minecraftAccounts} + where ${minecraftAccounts.userId} = ${users.id} + and ${minecraftAccounts.deletedAt} is null + and ( + ${minecraftAccounts.username} ilike ${pattern} + or ${minecraftAccounts.minecraftUuid} = ${search.toLowerCase()} + ) + )`, + ) + : undefined; + + const results = await db + .select({ + id: users.id, + firstName: users.firstName, + discordUsername: users.discordUsername, + discordUserId: users.discordUserId, + onboardingCompletedAt: users.onboardingCompletedAt, + primaryUsername: minecraftAccounts.username, + accountCount: sql`( + 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), + ), + ) + .where(where) + .orderBy(users.firstName, users.discordUsername) + .limit(100); + + return ( +
+
+
+

Identity registry

+

Users

+
+
+ + +
+
+ + {query.error &&

The requested user could not be found.

} + +
+ + + + + + {results.map((user) => ( + + + + + + + + ))} + {!results.length && } + +
UserDiscordPrimaryAccountsStatus
{user.firstName ?? "Name needed"}
@{user.discordUsername}
{user.discordUserId}
{user.primaryUsername ?? "—"}{user.accountCount}{user.onboardingCompletedAt ? "Ready" : "Onboarding"}
No users match that search.
+
+

Showing up to 100 users

+
+ ); +} diff --git a/apps/web/src/lib/audit.ts b/apps/web/src/lib/audit.ts index 42e0cc6..13053bb 100644 --- a/apps/web/src/lib/audit.ts +++ b/apps/web/src/lib/audit.ts @@ -5,6 +5,23 @@ import { db } from "@/lib/database"; const UI_ACCESSED = "games.minecraft.account-manager.ui.accessed"; +export async function recordAdminEvent( + admin: { email: string | null; name: string | null }, + targetUserId: string, + type: string, + data: Record, +) { + const requestHeaders = await headers(); + const ipAddress = getClientIp(requestHeaders, process.env.TRUST_PROXY === "true"); + return recordEvent(db, { + type, + source: "/web/admin", + subject: `user/${targetUserId}`, + ipAddress: ipAddress ?? undefined, + data: { ...data, adminEmail: admin.email, adminName: admin.name }, + }); +} + export async function recordUserEvent( user: { id: string }, type: string, diff --git a/apps/web/src/lib/auth/require-admin.ts b/apps/web/src/lib/auth/require-admin.ts new file mode 100644 index 0000000..32a52f0 --- /dev/null +++ b/apps/web/src/lib/auth/require-admin.ts @@ -0,0 +1,14 @@ +import { getServerSession } from "next-auth"; +import { redirect } from "next/navigation"; +import { adminAuthOptions, requiredAdminRole } from "./admin-auth"; + +export async function requireAdminSession() { + const session = await getServerSession(adminAuthOptions); + const roles = (session?.user as { roles?: string[] } | undefined)?.roles ?? []; + if (!session || !roles.includes(requiredAdminRole)) redirect("/admin/login"); + + return { + email: session.user?.email ?? null, + name: session.user?.name ?? null, + }; +} diff --git a/packages/minecraft/src/index.ts b/packages/minecraft/src/index.ts index 72fb699..21002b7 100644 --- a/packages/minecraft/src/index.ts +++ b/packages/minecraft/src/index.ts @@ -38,6 +38,14 @@ export function formatDiscordNickname(firstName: string, minecraftUsername: stri return `${shortenedName}${suffix}`; } +export function formatManagedDiscordNickname( + firstName: string, + minecraftUsername: string | null, +) { + if (minecraftUsername) return formatDiscordNickname(firstName, minecraftUsername); + return [...firstName.trim()].slice(0, DISCORD_NICKNAME_LIMIT).join("").trimEnd(); +} + export async function updateGuildNickname( input: { guildId: string; diff --git a/packages/minecraft/test/minecraft.test.ts b/packages/minecraft/test/minecraft.test.ts index 339a1ff..447b409 100644 --- a/packages/minecraft/test/minecraft.test.ts +++ b/packages/minecraft/test/minecraft.test.ts @@ -1,5 +1,5 @@ import { describe, expect, it, vi } from "vitest"; -import { formatDiscordNickname, lookupJavaProfile } from "../src/index"; +import { formatDiscordNickname, formatManagedDiscordNickname, lookupJavaProfile } from "../src/index"; describe("Java Edition profiles", () => { it("returns Mojang's canonical UUID and username", async () => { @@ -27,4 +27,9 @@ describe("Java Edition profiles", () => { ); expect(formatDiscordNickname("Sam", "Notch")).toBe("Sam (Notch)"); }); + + it("falls back to the user's name when an admin removes their final account", () => { + expect(formatManagedDiscordNickname("Alexandria Catherine", null)).toBe("Alexandria Catherine"); + expect(formatManagedDiscordNickname("A name that is definitely longer than Discord allows", null)).toHaveLength(32); + }); });