Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent) Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
257 lines
7.7 KiB
TypeScript
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 };
|
|
}
|
|
}
|