1
0
Fork 0
polaris-task-force/src/collections/users/Users.ts

352 lines
9.6 KiB
TypeScript

import type { CollectionConfig } from "payload";
import {
requirePermission,
hasPermission,
isSuperuser,
canAccessAdminPanel,
} 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 panel entry: superusers, plus anyone holding a page-manage
// permission (division roles). Per-page visibility is filtered separately
// by requireAdminPageAccess (read + admin:<slug>:manage per collection).
admin: async ({ req }) => canAccessAdminPanel(req.payload, req.user),
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 })),
},
],
},
{
name: "dashboard",
label: "Dashboard",
type: "group",
fields: [
{
name: "layout",
label: "Dashboard layout (widget ids per zone, managed by the dashboard)",
type: "json",
},
],
},
],
},
],
};