"use server"; import { minecraftAccounts, users } from "@minecraft-account-manager/database"; import { formatManagedDiscordNickname, lookupJavaProfile, updateGuildNickname } from "@minecraft-account-manager/minecraft"; import { and, eq, isNull, ne } from "drizzle-orm"; import { redirect } from "next/navigation"; import { recordUserEvent } from "@/lib/audit"; import { requireCurrentUser } from "@/lib/auth/user-session"; import { db } from "@/lib/database"; import { checkAccountAdditionNetwork, toAuditIpData } from "@/lib/ip-intelligence"; import { logger } from "@/lib/logger"; const USERNAME_PATTERN = /^[A-Za-z0-9_]{3,16}$/; type CurrentUser = Awaited>; type NicknameSyncResult = "updated" | "not-configured" | "failed"; async function synchronizeNickname(user: CurrentUser, nickname: string, operation: string): Promise { const guildId = process.env.DISCORD_GUILD_ID?.trim(); const botToken = process.env.DISCORD_BOT_TOKEN?.trim(); if (!guildId || !botToken) return "not-configured"; try { await updateGuildNickname({ guildId, discordUserId: user.discordUserId, nickname, botToken }); } catch (error) { logger.error( { err: error, event: "account.discord_nickname_update_failed", operation }, "Failed to update the Discord guild nickname", ); return "failed"; } try { await recordUserEvent(user, "games.minecraft.account-manager.discord.nickname.updated", { nickname, operation, }); } catch (error) { logger.error( { err: error, event: "account.discord_nickname_audit_failed", operation }, "Discord nickname updated but its audit event could not be recorded", ); } return "updated"; } function nicknameResultUrl(nickname: string, synchronization: NicknameSyncResult, additionalQuery?: string) { const result = synchronization === "updated" ? `nicknameUpdated=${encodeURIComponent(nickname)}` : `error=${synchronization === "not-configured" ? "nickname-not-configured" : "nickname-update-failed"}&nicknameExpected=${encodeURIComponent(nickname)}`; return `/account?${additionalQuery ? `${additionalQuery}&` : ""}${result}`; } export async function updateFirstName(formData: FormData) { const user = await requireCurrentUser(); const firstName = String(formData.get("firstName") ?? "").trim(); if (firstName.length < 1 || firstName.length > 50 || /[\u0000-\u001f\u007f]/.test(firstName)) { redirect("/account?error=invalid-name"); } 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); const nickname = formatManagedDiscordNickname(firstName, primary?.username ?? null); await db.update(users).set({ firstName, updatedAt: new Date() }).where(eq(users.id, user.id)); const synchronization = await synchronizeNickname(user, nickname, "update-first-name"); await recordUserEvent(user, "games.minecraft.account-manager.user.first-name.updated", { firstName }); redirect(nicknameResultUrl(nickname, synchronization)); } export async function addMinecraftAccount(formData: FormData) { const user = await requireCurrentUser(); const requestedUsername = String(formData.get("username") ?? "").trim(); const confirmed = formData.get("confirmUnverified") === "yes"; if (!USERNAME_PATTERN.test(requestedUsername)) redirect("/account?error=invalid-username"); const network = await checkAccountAdditionNetwork(); if (!network.allowed) { await recordUserEvent( user, network.reason === "blocked" ? "games.minecraft.account-manager.network.vpn-blocked" : "games.minecraft.account-manager.network.classification-unavailable", { attemptedUsername: requestedUsername, ipIntelligence: network.intelligence ? toAuditIpData(network.intelligence) : null, }, ); redirect(`/account?error=${network.reason === "blocked" ? "vpn-blocked" : "ip-check-unavailable"}`); } const profile = await lookupJavaProfile(requestedUsername); if (!profile && !confirmed) redirect(`/account?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); const username = profile?.username ?? requestedUsername; try { await db.insert(minecraftAccounts).values({ userId: user.id, minecraftUuid: profile?.uuid ?? null, username, validationStatus: profile ? "verified" : "user_confirmed", lastVerifiedAt: profile ? new Date() : null, isPrimary: !existing, }); } catch { redirect("/account?error=already-registered"); } const nickname = !existing && user.firstName ? formatManagedDiscordNickname(user.firstName, username) : null; const synchronization = nickname ? await synchronizeNickname(user, nickname, "add-first-account") : null; await recordUserEvent(user, "games.minecraft.account-manager.minecraft-account.added", { username, minecraftUuid: profile?.uuid ?? null, validationStatus: profile ? "verified" : "user_confirmed", }); redirect(nickname && synchronization ? nicknameResultUrl(nickname, synchronization, "added=1") : "/account?added=1"); } export async function setPrimaryAccount(formData: FormData) { const user = await requireCurrentUser(); const accountId = String(formData.get("accountId") ?? ""); 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); if (!requestedAccount) redirect("/account?error=unknown-account"); if (!user.firstName) redirect("/account?error=nickname-not-configured"); const 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)); return true; }); if (!changed) redirect("/account?error=unknown-account"); const nickname = formatManagedDiscordNickname(user.firstName, requestedAccount.username); const synchronization = await synchronizeNickname(user, nickname, "set-primary-account"); await recordUserEvent(user, "games.minecraft.account-manager.minecraft-account.primary-changed", { accountId: requestedAccount.id, nickname, }); redirect(nicknameResultUrl(nickname, synchronization)); } export async function removeMinecraftAccount(formData: FormData) { const user = await requireCurrentUser(); const accountId = String(formData.get("accountId") ?? ""); const result = await db.transaction(async (tx) => { const [account] = await tx .select({ id: minecraftAccounts.id, isPrimary: minecraftAccounts.isPrimary }) .from(minecraftAccounts) .where(and(eq(minecraftAccounts.id, accountId), eq(minecraftAccounts.userId, user.id), isNull(minecraftAccounts.deletedAt))) .limit(1); if (!account) return null; await tx.update(minecraftAccounts).set({ deletedAt: new Date(), isPrimary: false, updatedAt: new Date() }).where(eq(minecraftAccounts.id, account.id)); let replacementUsername: string | null = null; if (account.isPrimary) { const [replacement] = await tx .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); if (replacement) { replacementUsername = replacement.username; await tx.update(minecraftAccounts).set({ isPrimary: true, updatedAt: new Date() }).where(eq(minecraftAccounts.id, replacement.id)); } } else { const [primary] = await tx .select({ username: minecraftAccounts.username }) .from(minecraftAccounts) .where(and(eq(minecraftAccounts.userId, user.id), eq(minecraftAccounts.isPrimary, true), isNull(minecraftAccounts.deletedAt))) .limit(1); replacementUsername = primary?.username ?? null; } return { replacementUsername }; }); if (!result) redirect("/account?error=unknown-account"); const nickname = user.firstName ? formatManagedDiscordNickname(user.firstName, result.replacementUsername) : null; const synchronization = nickname ? await synchronizeNickname(user, nickname, "remove-account") : null; await recordUserEvent(user, "games.minecraft.account-manager.minecraft-account.removed", { accountId }); redirect(nickname && synchronization ? nicknameResultUrl(nickname, synchronization, "removed=1") : "/account?removed=1"); }