1
0
Fork 0

feat: add mission access control with assignment-based permissions

- Add commandAssignments and intelAssignments to GameRules global
- Implement role-based read access for missions (developer, admin, command, intel)
- Add visibility levels: unit, leadership, intel, private
- Restrict draft missions to authors, zeus, command, and intel
- Change faction field from text to relationship in missions
- Make missionFile optional in game setup
This commit is contained in:
Jason Fraley 2026-07-31 20:03:35 -04:00
parent f9c33c3fb9
commit a3e88ef9fa
2 changed files with 111 additions and 5 deletions

View file

@ -18,5 +18,25 @@ export const GameRules: GlobalConfig = {
'The base currency ("gold standard") used by all factions to pay for goods and services.',
},
},
{
name: "commandAssignments",
type: "relationship",
relationTo: "assignments",
hasMany: true,
admin: {
description:
"Assignment(s) considered unit command. Members and leaders of these assignments get elevated access to draft missions and other restricted content.",
},
},
{
name: "intelAssignments",
type: "relationship",
relationTo: "assignments",
hasMany: true,
admin: {
description:
"Assignment(s) considered the intelligence division. Members and leaders of these assignments get elevated access to draft missions and intelligence content.",
},
},
],
};

View file

@ -1,5 +1,40 @@
import { CollectionConfig } from "payload";
import type { CollectionConfig, Payload, User } from "payload";
import { isDeveloper } from "@/utils/access-control/isRole";
import hasRoles from "@/utils/access-control/hasRoles";
type AssignmentRef = { id: number } | number;
async function isInAssignments(
payload: Payload,
userId: number,
assignmentIds: number[],
): Promise<boolean> {
if (assignmentIds.length === 0) return false;
const assignments = await payload.find({
collection: "assignments",
where: { id: { in: assignmentIds } },
limit: assignmentIds.length,
depth: 0,
select: { leader: true, members: true },
});
return assignments.docs.some((a) => {
const leaderId =
a.leader && typeof a.leader === "object" ? a.leader.id : a.leader;
const memberIds = (a.members ?? []).map((m) =>
typeof m === "object" ? m.id : m,
);
return (
(leaderId !== null && leaderId !== undefined && leaderId === userId) ||
memberIds.includes(userId)
);
});
}
function extractIds(refs: unknown[] | undefined): number[] {
return (refs ?? []).map((r) =>
typeof r === "object" && r !== null ? (r as { id: number }).id : (r as number),
);
}
export const Missions: CollectionConfig = {
slug: "missions",
@ -10,7 +45,55 @@ export const Missions: CollectionConfig = {
create: isDeveloper,
update: isDeveloper,
delete: isDeveloper,
read: isDeveloper,
read: async ({ req, data }) => {
if (!req.user) return false;
if (hasRoles(["developer", "admin"], req.user)) return true;
const userId = req.user.id;
const gameRules = await req.payload.findGlobal({ slug: "game-rules" });
const commandIds = extractIds(
gameRules.commandAssignments as AssignmentRef[] | undefined,
);
const intelIds = extractIds(
gameRules.intelAssignments as AssignmentRef[] | undefined,
);
const [isCommand, isIntel] = await Promise.all([
isInAssignments(req.payload, userId, commandIds),
isInAssignments(req.payload, userId, intelIds),
]);
const authorIds = extractIds(
data?.ownershipAndStatus?.authors as unknown[] | undefined,
);
const zeusIds = extractIds(
data?.ownershipAndStatus?.zeus as unknown[] | undefined,
);
const isAuthorOrZeus =
authorIds.includes(userId) || zeusIds.includes(userId);
const status = data?.ownershipAndStatus?.status;
const isDraft = status === "Concept" || status === "Planning";
if (isDraft) {
return isAuthorOrZeus || isCommand || isIntel;
}
const visibility = data?.ownershipAndStatus?.visibility;
switch (visibility) {
case "unit":
return true;
case "leadership":
return isCommand || isIntel || isAuthorOrZeus;
case "intel":
return isIntel || isAuthorOrZeus;
case "private":
return isAuthorOrZeus;
default:
return true;
}
},
},
fields: [
{
@ -344,8 +427,9 @@ export const Missions: CollectionConfig = {
type: "array",
fields: [
{
name: "factionName",
type: "text",
name: "faction",
type: "relationship",
relationTo: "factions",
required: true,
},
{
@ -406,6 +490,9 @@ export const Missions: CollectionConfig = {
type: "text",
hidden: true,
defaultValue: "ptf313",
access: {
read: isDeveloper,
},
},
],
},
@ -417,7 +504,6 @@ export const Missions: CollectionConfig = {
name: "missionFile",
type: "upload",
relationTo: "mission-files",
required: true,
},
{
name: "requiredMods",