feat(portal): refine account identity controls
CI / validate (push) Successful in 4m51s
Release / release (push) Successful in 6m36s

This commit is contained in:
dmg
2026-08-01 18:06:30 -04:00
parent 86c87153b4
commit 19a5d04178
17 changed files with 357 additions and 43 deletions
+85 -14
View File
@@ -6,20 +6,56 @@ import { and, eq, isNull, ne } from "drizzle-orm";
import { redirect } from "next/navigation";
import { recordUserEvent } from "@/lib/audit";
import { db } from "@/lib/database";
import { hasDiscordNicknameConfirmation } from "@/lib/dashboard-change-confirmation";
import { requireCurrentUser } from "@/lib/auth/user-session";
import { checkAccountAdditionNetwork, toAuditIpData } from "@/lib/ip-intelligence";
import { logger } from "@/lib/logger";
const USERNAME_PATTERN = /^[A-Za-z0-9_]{3,16}$/;
export async function updateFirstName(formData: FormData) {
const user = await requireCurrentUser();
const firstName = String(formData.get("firstName") ?? "").trim();
const confirmed = hasDiscordNicknameConfirmation(formData);
if (firstName.length < 1 || firstName.length > 50 || /[\u0000-\u001f\u007f]/.test(firstName)) {
redirect("/account?error=invalid-name");
}
await db.update(users).set({ firstName, updatedAt: new Date() }).where(eq(users.id, user.id));
const [primary] = await db.select({ username: minecraftAccounts.username }).from(minecraftAccounts).where(
and(eq(minecraftAccounts.userId, user.id), eq(minecraftAccounts.isPrimary, true), isNull(minecraftAccounts.deletedAt)),
).limit(1);
if (!primary) redirect("/account?error=nickname-not-configured");
if (!confirmed) redirect(`/account?pendingName=${encodeURIComponent(firstName)}`);
const guildId = process.env.DISCORD_GUILD_ID?.trim();
const botToken = process.env.DISCORD_BOT_TOKEN?.trim();
if (!guildId || !botToken) redirect("/account?error=nickname-not-configured");
const nickname = formatDiscordNickname(firstName, primary.username);
try {
await db.transaction(async (tx) => {
await tx.update(users).set({ firstName, updatedAt: new Date() }).where(eq(users.id, user.id));
await updateGuildNickname({
guildId,
discordUserId: user.discordUserId,
nickname,
botToken,
});
});
} catch (error) {
logger.error(
{ err: error, event: "account.first_name_update_failed" },
"Failed to update the user name and Discord nickname",
);
redirect(`/account?error=nickname-update-failed&pendingName=${encodeURIComponent(firstName)}`);
}
await recordUserEvent(user, "games.minecraft.account-manager.user.first-name.updated", { firstName });
redirect("/account?confirmNickname=1");
await recordUserEvent(user, "games.minecraft.account-manager.discord.nickname.updated", {
nickname,
operation: "update-first-name",
});
redirect("/account?nicknameUpdated=1");
}
export async function addMinecraftAccount(formData: FormData) {
@@ -75,23 +111,58 @@ export async function addMinecraftAccount(formData: FormData) {
export async function setPrimaryAccount(formData: FormData) {
const user = await requireCurrentUser();
const accountId = String(formData.get("accountId") ?? "");
const confirmed = hasDiscordNicknameConfirmation(formData);
const [requestedAccount] = 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);
const changed = await db.transaction(async (tx) => {
const [account] = await tx.select({ id: minecraftAccounts.id }).from(minecraftAccounts).where(
and(eq(minecraftAccounts.id, accountId), eq(minecraftAccounts.userId, user.id), isNull(minecraftAccounts.deletedAt)),
).limit(1);
if (!account) return false;
if (!requestedAccount) redirect("/account?error=unknown-account");
if (!user.firstName) redirect("/account?error=nickname-not-configured");
if (!confirmed) redirect(`/account?pendingPrimary=${encodeURIComponent(requestedAccount.id)}`);
await tx.update(minecraftAccounts).set({ isPrimary: false, updatedAt: new Date() }).where(
and(eq(minecraftAccounts.userId, user.id), isNull(minecraftAccounts.deletedAt)),
const guildId = process.env.DISCORD_GUILD_ID?.trim();
const botToken = process.env.DISCORD_BOT_TOKEN?.trim();
if (!guildId || !botToken) redirect("/account?error=nickname-not-configured");
const nickname = formatDiscordNickname(user.firstName, requestedAccount.username);
let changed = false;
try {
changed = await db.transaction(async (tx) => {
const [account] = await tx.select({ id: minecraftAccounts.id }).from(minecraftAccounts).where(
and(eq(minecraftAccounts.id, requestedAccount.id), eq(minecraftAccounts.userId, user.id), isNull(minecraftAccounts.deletedAt)),
).limit(1);
if (!account) return false;
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 updateGuildNickname({
guildId,
discordUserId: user.discordUserId,
nickname,
botToken,
});
return true;
});
} catch (error) {
logger.error(
{ err: error, event: "account.primary_update_failed" },
"Failed to update the primary account and Discord nickname",
);
await tx.update(minecraftAccounts).set({ isPrimary: true, updatedAt: new Date() }).where(eq(minecraftAccounts.id, account.id));
return true;
});
redirect(`/account?error=nickname-update-failed&pendingPrimary=${encodeURIComponent(requestedAccount.id)}`);
}
if (!changed) redirect("/account?error=unknown-account");
await recordUserEvent(user, "games.minecraft.account-manager.minecraft-account.primary-changed", { accountId });
redirect("/account?confirmNickname=1");
await recordUserEvent(user, "games.minecraft.account-manager.minecraft-account.primary-changed", {
accountId: requestedAccount.id,
nickname,
});
await recordUserEvent(user, "games.minecraft.account-manager.discord.nickname.updated", {
nickname,
operation: "set-primary-account",
});
redirect("/account?nicknameUpdated=1");
}
export async function removeMinecraftAccount(formData: FormData) {