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:
parent
f9c33c3fb9
commit
a3e88ef9fa
2 changed files with 111 additions and 5 deletions
|
|
@ -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.",
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
Loading…
Reference in a new issue