1
0
Fork 0
polaris-task-force/src/app/(frontend)/account/actions.ts
Z8MB1E a9d9ba50ea feat(preferences): add version overlay toggle and redesign settings UI
Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
2026-08-19 20:40:56 -04:00

257 lines
7.7 KiB
TypeScript

"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<T = undefined> {
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<ActionResult> {
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<ActionResult> {
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<ActionResult> {
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<ActionResult> {
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 };
}
}