feat(platform): add Discord onboarding and Velocity gate
This commit is contained in:
@@ -0,0 +1,132 @@
|
||||
"use server";
|
||||
|
||||
import { formatDiscordNickname, 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 { recordUserEvent } from "@/lib/audit";
|
||||
import { db } from "@/lib/database";
|
||||
import { requireCurrentUser } from "@/lib/auth/user-session";
|
||||
|
||||
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();
|
||||
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));
|
||||
await recordUserEvent(user, "games.minecraft.account-manager.user.first-name.updated", { firstName });
|
||||
redirect("/account?confirmNickname=1");
|
||||
}
|
||||
|
||||
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 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);
|
||||
|
||||
let failed = false;
|
||||
try {
|
||||
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,
|
||||
});
|
||||
} catch {
|
||||
failed = true;
|
||||
}
|
||||
if (failed) redirect("/account?error=already-registered");
|
||||
await recordUserEvent(user, "games.minecraft.account-manager.minecraft-account.added", {
|
||||
username: profile?.username ?? requestedUsername,
|
||||
minecraftUuid: profile?.uuid ?? null,
|
||||
validationStatus: profile ? "verified" : "user_confirmed",
|
||||
});
|
||||
redirect(existing ? "/account?added=1" : "/account?confirmNickname=1");
|
||||
}
|
||||
|
||||
export async function setPrimaryAccount(formData: FormData) {
|
||||
const user = await requireCurrentUser();
|
||||
const accountId = String(formData.get("accountId") ?? "");
|
||||
|
||||
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;
|
||||
|
||||
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");
|
||||
await recordUserEvent(user, "games.minecraft.account-manager.minecraft-account.primary-changed", { accountId });
|
||||
redirect("/account?confirmNickname=1");
|
||||
}
|
||||
|
||||
export async function removeMinecraftAccount(formData: FormData) {
|
||||
const user = await requireCurrentUser();
|
||||
const accountId = String(formData.get("accountId") ?? "");
|
||||
|
||||
const removed = 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 false;
|
||||
|
||||
await tx.update(minecraftAccounts).set({ deletedAt: new Date(), isPrimary: false, updatedAt: new Date() }).where(eq(minecraftAccounts.id, account.id));
|
||||
if (account.isPrimary) {
|
||||
const [replacement] = await tx.select({ id: minecraftAccounts.id }).from(minecraftAccounts).where(
|
||||
and(eq(minecraftAccounts.userId, user.id), ne(minecraftAccounts.id, account.id), isNull(minecraftAccounts.deletedAt)),
|
||||
).limit(1);
|
||||
if (replacement) {
|
||||
await tx.update(minecraftAccounts).set({ isPrimary: true, updatedAt: new Date() }).where(eq(minecraftAccounts.id, replacement.id));
|
||||
}
|
||||
}
|
||||
return true;
|
||||
});
|
||||
|
||||
if (!removed) redirect("/account?error=unknown-account");
|
||||
await recordUserEvent(user, "games.minecraft.account-manager.minecraft-account.removed", { accountId });
|
||||
redirect("/account?removed=1&confirmNickname=1");
|
||||
}
|
||||
|
||||
export async function confirmDashboardNickname() {
|
||||
const user = await requireCurrentUser();
|
||||
const [account] = 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 guildId = process.env.DISCORD_GUILD_ID?.trim();
|
||||
const botToken = process.env.DISCORD_BOT_TOKEN?.trim();
|
||||
|
||||
if (!user.firstName || !account || !guildId || !botToken) redirect("/account?error=nickname-not-configured");
|
||||
|
||||
try {
|
||||
await updateGuildNickname({
|
||||
guildId,
|
||||
discordUserId: user.discordUserId,
|
||||
nickname: formatDiscordNickname(user.firstName, account.username),
|
||||
botToken,
|
||||
});
|
||||
} catch {
|
||||
redirect("/account?error=nickname-update-failed&confirmNickname=1");
|
||||
}
|
||||
await recordUserEvent(user, "games.minecraft.account-manager.discord.nickname.updated", {
|
||||
nickname: formatDiscordNickname(user.firstName, account.username),
|
||||
});
|
||||
redirect("/account?nicknameUpdated=1");
|
||||
}
|
||||
Reference in New Issue
Block a user