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.',
|
'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 { 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 = {
|
export const Missions: CollectionConfig = {
|
||||||
slug: "missions",
|
slug: "missions",
|
||||||
|
|
@ -10,7 +45,55 @@ export const Missions: CollectionConfig = {
|
||||||
create: isDeveloper,
|
create: isDeveloper,
|
||||||
update: isDeveloper,
|
update: isDeveloper,
|
||||||
delete: 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: [
|
fields: [
|
||||||
{
|
{
|
||||||
|
|
@ -344,8 +427,9 @@ export const Missions: CollectionConfig = {
|
||||||
type: "array",
|
type: "array",
|
||||||
fields: [
|
fields: [
|
||||||
{
|
{
|
||||||
name: "factionName",
|
name: "faction",
|
||||||
type: "text",
|
type: "relationship",
|
||||||
|
relationTo: "factions",
|
||||||
required: true,
|
required: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|
@ -406,6 +490,9 @@ export const Missions: CollectionConfig = {
|
||||||
type: "text",
|
type: "text",
|
||||||
hidden: true,
|
hidden: true,
|
||||||
defaultValue: "ptf313",
|
defaultValue: "ptf313",
|
||||||
|
access: {
|
||||||
|
read: isDeveloper,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
|
@ -417,7 +504,6 @@ export const Missions: CollectionConfig = {
|
||||||
name: "missionFile",
|
name: "missionFile",
|
||||||
type: "upload",
|
type: "upload",
|
||||||
relationTo: "mission-files",
|
relationTo: "mission-files",
|
||||||
required: true,
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "requiredMods",
|
name: "requiredMods",
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue