"use server"; import config from "@payload-config"; import { getPayload } from "payload"; import { emitGameEvent } from "@/utils/event-log/emit"; import { EventTypes } from "@/utils/event-log/eventTypes"; import { notifyUser } from "@/lib/notifications"; import { MUTEABLE_NOTIFICATION_VALUES, type MuteableNotificationType, } from "@/lib/notifications/notificationTypes"; export interface ActionResult { success: boolean; error?: string; data?: T; } async function authenticate() { const payloadConfig = await config; const payload = await getPayload({ config: payloadConfig }); const { headers } = await import("next/headers"); const hdrs = await headers(); const { user } = await payload.auth({ headers: hdrs, canSetHeaders: false }); if (!user) { throw new Error("Unauthorized"); } return { payload, user }; } export async function changePassword(input: { currentPassword: string; newPassword: string; }): Promise { try { const { payload, user } = await authenticate(); const userId = user.id as number; if (!input.currentPassword || !input.newPassword) { return { success: false, error: "Current and new password are required." }; } if (input.newPassword.length < 8) { return { success: false, error: "New password must be at least 8 characters." }; } try { await payload.login({ collection: "users", data: { username: user.username, password: input.currentPassword }, }); } catch { return { success: false, error: "Current password is incorrect." }; } await payload.update({ collection: "users", id: userId, data: { password: input.newPassword }, overrideAccess: true, }); await emitGameEvent(payload, { type: EventTypes.AccountPasswordChange, message: "Password changed", actor: userId, targetCollection: "users", targetId: userId, }); return { success: true }; } catch (error) { const message = error instanceof Error ? error.message : "Unknown error"; return message === "Unauthorized" ? { success: false, error: "You must be logged in." } : { success: false, error: message }; } } export async function updateDisplayName(input: { displayName: string }): Promise { try { const { payload, user } = await authenticate(); const userId = user.id as number; const trimmed = input.displayName.trim(); if (!trimmed) { return { success: false, error: "Display name is required." }; } if (trimmed.length > 60) { return { success: false, error: "Display name must be 60 characters or fewer." }; } await payload.update({ collection: "users", id: userId, data: { displayName: trimmed }, overrideAccess: true, }); await emitGameEvent(payload, { type: EventTypes.AccountProfileUpdate, message: `Updated display name to "${trimmed}"`, actor: userId, targetCollection: "users", targetId: userId, }); return { success: true }; } catch (error) { const message = error instanceof Error ? error.message : "Unknown error"; return message === "Unauthorized" ? { success: false, error: "You must be logged in." } : { success: false, error: message }; } } export async function requestDiscordUsernameChange(input: { desiredDiscordUsername: string; }): Promise { try { const { payload, user } = await authenticate(); const userId = user.id as number; const trimmed = input.desiredDiscordUsername.trim(); if (!trimmed) { return { success: false, error: "Discord username is required." }; } if (trimmed.length < 2 || trimmed.length > 32) { return { success: false, error: "Discord usernames must be between 2 and 32 characters." }; } if (/\s/.test(trimmed)) { return { success: false, error: "Discord usernames cannot contain spaces." }; } const existingRes = await payload.find({ collection: "users", where: { discordUsername: { equals: trimmed } }, limit: 1, depth: 0, overrideAccess: true, }); if (existingRes.docs.some((doc) => doc.id !== userId)) { return { success: false, error: "That Discord username is already in use." }; } const staffRoles = await payload.find({ collection: "roles", where: { slug: { in: ["admin", "developer"] } }, limit: 2, depth: 0, overrideAccess: true, }); const staffRoleIds = staffRoles.docs.map((d) => d.id); if (staffRoleIds.length > 0) { const staffRes = await payload.find({ collection: "users", where: { roleDocs: { in: staffRoleIds } }, limit: 100, depth: 0, overrideAccess: true, }); const staffIds = [...new Set(staffRes.docs.map((doc) => doc.id as number))].filter( (id) => id !== userId, ); for (const staffId of staffIds) { await notifyUser(payload, { userId: staffId, type: "account:discord-request", title: `Discord username change requested by ${user.username}`, message: `wants to change their Discord username to "${trimmed}".`, link: `/admin/collections/users/${userId}`, }); } } await notifyUser(payload, { userId, type: "account:discord-request", title: "Request submitted", message: `Your request to change your Discord username to "${trimmed}" has been sent to leadership.`, }); await emitGameEvent(payload, { type: EventTypes.AccountDiscordRequest, message: `Requested a Discord username change to "${trimmed}"`, actor: userId, targetCollection: "users", targetId: userId, data: { requestedDiscordUsername: trimmed }, }); return { success: true }; } catch (error) { const message = error instanceof Error ? error.message : "Unknown error"; return message === "Unauthorized" ? { success: false, error: "You must be logged in." } : { success: false, error: message }; } } export async function updatePreferences(input: { mutedAll: boolean; mutedTypes: string[]; showCallsign: boolean; showVersionOverlay: boolean; leadOrActual: "lead" | "actual"; discordEnabled: boolean; discordMutedTypes: string[]; }): Promise { try { const { payload, user } = await authenticate(); const userId = user.id as number; const mutedTypes = input.mutedTypes.filter((t): t is MuteableNotificationType => (MUTEABLE_NOTIFICATION_VALUES as readonly string[]).includes(t), ); const discordMutedTypes = input.discordMutedTypes.filter((t): t is MuteableNotificationType => (MUTEABLE_NOTIFICATION_VALUES as readonly string[]).includes(t), ); await payload.update({ collection: "users", id: userId, data: { preferences: { notifications: { mutedAll: input.mutedAll, mutedTypes }, display: { showCallsign: input.showCallsign, showVersionOverlay: input.showVersionOverlay, leadOrActual: input.leadOrActual, }, discord: { enabled: input.discordEnabled, mutedTypes: discordMutedTypes, }, }, }, overrideAccess: true, }); await emitGameEvent(payload, { type: EventTypes.AccountPreferencesUpdate, message: "Updated preferences", actor: userId, targetCollection: "users", targetId: userId, }); return { success: true }; } catch (error) { const message = error instanceof Error ? error.message : "Unknown error"; return message === "Unauthorized" ? { success: false, error: "You must be logged in." } : { success: false, error: message }; } }