# Conflicts: # TODO.md # src/components/frontend/eod/MinesweeperBoard.tsx # src/migrations/index.ts
336 lines
9 KiB
TypeScript
336 lines
9 KiB
TypeScript
import type { CollectionConfig } from "payload";
|
|
import {
|
|
requirePermission,
|
|
hasPermission,
|
|
isSuperuser,
|
|
} from "@/utils/access-control/hasPermission";
|
|
import { ensurePersonalAccount } from "@/lib/banking/index";
|
|
import { MUTEABLE_NOTIFICATION_TYPES } from "@/lib/notifications/notificationTypes";
|
|
|
|
export const Users: CollectionConfig = {
|
|
hooks: {
|
|
afterChange: [
|
|
async ({ req, doc, operation }) => {
|
|
if (operation !== "create") return doc;
|
|
|
|
const payload = req.payload;
|
|
const userId = doc.id as number;
|
|
|
|
try {
|
|
await ensurePersonalAccount(payload, userId, req);
|
|
} catch (e) {
|
|
payload.logger.error(
|
|
`[Users] Failed to create personal bank account for user ${userId}: ${e}`,
|
|
);
|
|
}
|
|
|
|
let recruit: { id: number } | undefined;
|
|
|
|
try {
|
|
const ranks = await payload.find({
|
|
collection: "ranks",
|
|
where: {
|
|
name: {
|
|
like: "Recruit",
|
|
},
|
|
},
|
|
limit: 1,
|
|
depth: 0,
|
|
overrideAccess: true,
|
|
req,
|
|
});
|
|
recruit = (ranks as { docs: Array<{ id: number }> }).docs[0];
|
|
|
|
if (!recruit) {
|
|
const created = await payload.create({
|
|
collection: "ranks",
|
|
data: {
|
|
name: "Recruit",
|
|
abbreviation: "Rct",
|
|
description: "Entry-level rank for new members.",
|
|
},
|
|
overrideAccess: true,
|
|
req,
|
|
});
|
|
recruit = { id: created.id };
|
|
payload.logger.info("[Users] Created default 'Recruit' rank on demand.");
|
|
}
|
|
} catch (e) {
|
|
payload.logger.error(`[Users] Failed to resolve Recruit rank: ${e}`);
|
|
}
|
|
|
|
try {
|
|
await payload.create({
|
|
collection: "profiles",
|
|
draft: false,
|
|
data: {
|
|
user: userId,
|
|
rank: recruit?.id ?? (null as unknown as number),
|
|
dossier: {
|
|
enlistmentDate: new Date().toISOString(),
|
|
},
|
|
progression: {
|
|
qualifications: [],
|
|
experience: 0,
|
|
awards: [],
|
|
} as any,
|
|
},
|
|
overrideAccess: true,
|
|
req,
|
|
});
|
|
} catch (e) {
|
|
payload.logger.error(`[Users] Failed to create profile for user ${userId}: ${e}`);
|
|
}
|
|
|
|
return doc;
|
|
},
|
|
],
|
|
},
|
|
slug: "users",
|
|
access: {
|
|
admin: requirePermission("system:admin-access"),
|
|
unlock: requirePermission("users:unlock"),
|
|
create: requirePermission("users:create"),
|
|
update: async ({ req, id, data }) => {
|
|
if (data?.roles?.includes("developer")) {
|
|
return isSuperuser(req.payload, req.user);
|
|
}
|
|
const existing = id
|
|
? ((await req.payload.findByID({
|
|
collection: "users",
|
|
id,
|
|
depth: 2,
|
|
overrideAccess: true,
|
|
})) as { roleDocs?: Array<{ isSuperuser?: boolean }> | null })
|
|
: null;
|
|
if (existing?.roleDocs?.some((r) => r.isSuperuser)) {
|
|
return isSuperuser(req.payload, req.user);
|
|
}
|
|
return hasPermission(req.payload, req.user, "users:update");
|
|
},
|
|
delete: async ({ req, id, data }) => {
|
|
if (data?.roles?.includes("developer")) {
|
|
return isSuperuser(req.payload, req.user);
|
|
}
|
|
const existing = id
|
|
? ((await req.payload.findByID({
|
|
collection: "users",
|
|
id,
|
|
depth: 2,
|
|
overrideAccess: true,
|
|
})) as { roleDocs?: Array<{ isSuperuser?: boolean }> | null })
|
|
: null;
|
|
if (existing?.roleDocs?.some((r) => r.isSuperuser)) {
|
|
return isSuperuser(req.payload, req.user);
|
|
}
|
|
return hasPermission(req.payload, req.user, "users:delete");
|
|
},
|
|
},
|
|
admin: {
|
|
useAsTitle: "payloadDisplayName",
|
|
group: "Users",
|
|
},
|
|
auth: {
|
|
tokenExpiration: 43200,
|
|
loginWithUsername: {
|
|
requireUsername: true,
|
|
allowEmailLogin: false,
|
|
requireEmail: false,
|
|
},
|
|
},
|
|
fields: [
|
|
// Email added by default by Payload, but essentially ignored in this application
|
|
{
|
|
name: "username",
|
|
type: "text",
|
|
required: true,
|
|
unique: true,
|
|
},
|
|
{
|
|
name: "discordUsername",
|
|
type: "text",
|
|
required: true,
|
|
unique: true,
|
|
},
|
|
{
|
|
name: "discordId",
|
|
type: "text",
|
|
label: "Discord User ID",
|
|
unique: true,
|
|
index: true,
|
|
},
|
|
{
|
|
name: "displayName",
|
|
type: "text",
|
|
label: "In-Game Name",
|
|
required: true,
|
|
},
|
|
{
|
|
name: "payloadDisplayName",
|
|
type: "text",
|
|
label: "Display Name",
|
|
admin: {
|
|
hidden: true,
|
|
readOnly: true,
|
|
},
|
|
hooks: {
|
|
beforeChange: [
|
|
({ data }) => {
|
|
// console.log(data);
|
|
return `${data!.displayName ? data!.displayName + " " : ""}(${data!.username})`;
|
|
},
|
|
],
|
|
},
|
|
},
|
|
{
|
|
name: "steamId",
|
|
type: "text",
|
|
label: "Steam ID 64",
|
|
required: true,
|
|
},
|
|
{
|
|
name: "roles",
|
|
type: "select",
|
|
access: {
|
|
create: requirePermission("users:assign-roles"),
|
|
update: requirePermission("users:assign-roles"),
|
|
},
|
|
hasMany: true,
|
|
options: [
|
|
{
|
|
label: "Guest",
|
|
value: "guest",
|
|
},
|
|
{
|
|
label: "User",
|
|
value: "user",
|
|
},
|
|
{
|
|
label: "Trusted",
|
|
value: "trusted",
|
|
},
|
|
{
|
|
label: "Admin",
|
|
value: "admin",
|
|
},
|
|
{
|
|
label: "Developer",
|
|
value: "developer",
|
|
},
|
|
],
|
|
/*filterOptions: ({ options, data, req }) => {
|
|
return !isDeveloper({ req })
|
|
? options.filter(
|
|
(option) => (typeof option === "string" ? options : !["developer", "admin"].includes(option.value)),
|
|
)
|
|
: options;
|
|
},*/
|
|
admin: {
|
|
description:
|
|
"Legacy role enum — being replaced by the dynamic RBAC system (see 'Role Assignments' below).",
|
|
},
|
|
},
|
|
{
|
|
name: "roleDocs",
|
|
type: "relationship",
|
|
relationTo: "roles",
|
|
hasMany: true,
|
|
access: {
|
|
create: requirePermission("users:assign-roles"),
|
|
update: requirePermission("users:assign-roles"),
|
|
},
|
|
admin: {
|
|
description:
|
|
"Dynamic RBAC role assignments. These determine the user's permissions via the roles collection.",
|
|
},
|
|
},
|
|
{
|
|
name: "preferences",
|
|
label: "Preferences",
|
|
type: "group",
|
|
fields: [
|
|
{
|
|
name: "notifications",
|
|
label: "Notifications",
|
|
type: "group",
|
|
fields: [
|
|
{
|
|
name: "mutedAll",
|
|
label: "Mute all in-app notifications",
|
|
type: "checkbox",
|
|
defaultValue: false,
|
|
},
|
|
{
|
|
name: "mutedTypes",
|
|
label: "Muted notification types",
|
|
type: "select",
|
|
hasMany: true,
|
|
options: MUTEABLE_NOTIFICATION_TYPES.map((t) => ({ label: t.label, value: t.value })),
|
|
},
|
|
],
|
|
},
|
|
{
|
|
name: "display",
|
|
label: "Display",
|
|
type: "group",
|
|
fields: [
|
|
{
|
|
name: "showCallsign",
|
|
label: "Show callsign (username) instead of display name in the sidebar",
|
|
type: "checkbox",
|
|
defaultValue: false,
|
|
},
|
|
{
|
|
name: "leadOrActual",
|
|
label: 'Use "Lead" or "Actual" when reviewing the unit roster',
|
|
type: "select",
|
|
options: [
|
|
{ label: "Lead", value: "lead" },
|
|
{ label: "Actual", value: "actual" },
|
|
],
|
|
defaultValue: "lead",
|
|
},
|
|
{
|
|
name: "showVersionOverlay",
|
|
label: "Show version overlay at top of page",
|
|
type: "checkbox",
|
|
defaultValue: true,
|
|
},
|
|
{
|
|
name: "xpDisplayMini",
|
|
label: "Use mini mode for XP display",
|
|
type: "checkbox",
|
|
defaultValue: false,
|
|
},
|
|
{
|
|
name: "showBackgroundTexture",
|
|
label: "Show diagonal background texture",
|
|
type: "checkbox",
|
|
defaultValue: true,
|
|
},
|
|
],
|
|
},
|
|
{
|
|
name: "discord",
|
|
label: "Discord",
|
|
type: "group",
|
|
fields: [
|
|
{
|
|
name: "enabled",
|
|
label: "Discord notifications",
|
|
type: "checkbox",
|
|
defaultValue: false,
|
|
},
|
|
{
|
|
name: "mutedTypes",
|
|
label: "Muted notification types",
|
|
type: "select",
|
|
hasMany: true,
|
|
options: MUTEABLE_NOTIFICATION_TYPES.map((t) => ({ label: t.label, value: t.value })),
|
|
},
|
|
],
|
|
},
|
|
],
|
|
},
|
|
],
|
|
};
|