feat(campaigns): let members create and own campaigns
This commit is contained in:
parent
d6b2ffa5d2
commit
0771424446
9 changed files with 367 additions and 24 deletions
|
|
@ -42,9 +42,24 @@ export default async function CampaignDetailPage({ params }: CampaignDetailPageP
|
||||||
collection: "campaigns",
|
collection: "campaigns",
|
||||||
id: Number(id),
|
id: Number(id),
|
||||||
depth: 1,
|
depth: 1,
|
||||||
|
select: {
|
||||||
|
id: true,
|
||||||
|
name: true,
|
||||||
|
summary: true,
|
||||||
|
status: true,
|
||||||
|
startDate: true,
|
||||||
|
endDate: true,
|
||||||
|
owner: true,
|
||||||
|
},
|
||||||
})
|
})
|
||||||
.catch(() => null);
|
.catch(() => null);
|
||||||
|
|
||||||
|
const owner = campaign?.owner;
|
||||||
|
const ownerName =
|
||||||
|
owner && typeof owner === "object" && "displayName" in owner
|
||||||
|
? owner.displayName
|
||||||
|
: "Unknown";
|
||||||
|
|
||||||
if (!campaign) {
|
if (!campaign) {
|
||||||
notFound();
|
notFound();
|
||||||
}
|
}
|
||||||
|
|
@ -237,7 +252,6 @@ export default async function CampaignDetailPage({ params }: CampaignDetailPageP
|
||||||
<span className="font-mono">{count}</span>
|
<span className="font-mono">{count}</span>
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
{campaign.startDate && (
|
|
||||||
<div className="flex justify-between text-sm pt-1 border-t border-border/50">
|
<div className="flex justify-between text-sm pt-1 border-t border-border/50">
|
||||||
<span className="text-muted-foreground">Duration</span>
|
<span className="text-muted-foreground">Duration</span>
|
||||||
<span>
|
<span>
|
||||||
|
|
@ -245,7 +259,10 @@ export default async function CampaignDetailPage({ params }: CampaignDetailPageP
|
||||||
{endDate ? ` — ${endDate}` : " — Present"}
|
{endDate ? ` — ${endDate}` : " — Present"}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
)}
|
<div className="flex justify-between text-sm pt-1 border-t border-border/50">
|
||||||
|
<span className="text-muted-foreground">Owner</span>
|
||||||
|
<span className="font-medium">{ownerName}</span>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</ItemContent>
|
</ItemContent>
|
||||||
</Item>
|
</Item>
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,7 @@ import config from "@payload-config";
|
||||||
import { getPayload } from "payload";
|
import { getPayload } from "payload";
|
||||||
import { ShieldIcon } from "lucide-react";
|
import { ShieldIcon } from "lucide-react";
|
||||||
import { CampaignCard } from "@/components/frontend/intelligence/CampaignCard";
|
import { CampaignCard } from "@/components/frontend/intelligence/CampaignCard";
|
||||||
|
import { CreateCampaignDialog } from "@/components/frontend/intelligence/CreateCampaignDialog";
|
||||||
import type { Media } from "@/payload-types";
|
import type { Media } from "@/payload-types";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
|
|
||||||
|
|
@ -17,6 +18,15 @@ export default async function CampaignsPage() {
|
||||||
sort: "-createdAt",
|
sort: "-createdAt",
|
||||||
limit: 100,
|
limit: 100,
|
||||||
depth: 1,
|
depth: 1,
|
||||||
|
select: {
|
||||||
|
id: true,
|
||||||
|
name: true,
|
||||||
|
summary: true,
|
||||||
|
status: true,
|
||||||
|
startDate: true,
|
||||||
|
endDate: true,
|
||||||
|
owner: true,
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const campaigns = campaignsRes.docs;
|
const campaigns = campaignsRes.docs;
|
||||||
|
|
@ -60,12 +70,19 @@ export default async function CampaignsPage() {
|
||||||
: null;
|
: null;
|
||||||
const coverImageUrl = coverImage?.url ?? null;
|
const coverImageUrl = coverImage?.url ?? null;
|
||||||
|
|
||||||
|
const owner = (campaign as any).owner;
|
||||||
|
const ownerName =
|
||||||
|
owner && typeof owner === "object" && "displayName" in owner
|
||||||
|
? owner.displayName
|
||||||
|
: "Unknown";
|
||||||
|
|
||||||
return {
|
return {
|
||||||
...campaign,
|
...campaign,
|
||||||
coverImageUrl,
|
coverImageUrl,
|
||||||
missionCount,
|
missionCount,
|
||||||
avgPlayers: missionCount === 0 ? 0 : totalPlayers / missionCount,
|
avgPlayers: missionCount === 0 ? 0 : totalPlayers / missionCount,
|
||||||
avgDurationMinutes: missionCount === 0 ? 0 : totalDurationMinutes / missionCount,
|
avgDurationMinutes: missionCount === 0 ? 0 : totalDurationMinutes / missionCount,
|
||||||
|
ownerName,
|
||||||
};
|
};
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
|
|
@ -85,12 +102,7 @@ export default async function CampaignsPage() {
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex flex-col gap-1 justify-end text-center max-w-3xs">
|
<div className="flex flex-col gap-1 justify-end text-center max-w-3xs">
|
||||||
<p className="text-sm text-muted-foreground">
|
<CreateCampaignDialog />
|
||||||
Are you a Zeus and want to start your own campaign?
|
|
||||||
</p>
|
|
||||||
<Button variant="outline" size="sm">
|
|
||||||
Create your own campaign!
|
|
||||||
</Button>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{sorted.length > 0 ? (
|
{sorted.length > 0 ? (
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
import { CollectionConfig } from "payload";
|
import { CollectionConfig } from "payload";
|
||||||
import { requirePermission } from "@/utils/access-control/hasPermission";
|
import { requirePermission, requireCampaignOwnership } from "@/utils/access-control/hasPermission";
|
||||||
|
import type { User } from "@/payload-types";
|
||||||
|
|
||||||
export const Campaigns: CollectionConfig = {
|
export const Campaigns: CollectionConfig = {
|
||||||
slug: "campaigns",
|
slug: "campaigns",
|
||||||
|
|
@ -8,10 +9,10 @@ export const Campaigns: CollectionConfig = {
|
||||||
useAsTitle: "name",
|
useAsTitle: "name",
|
||||||
},
|
},
|
||||||
access: {
|
access: {
|
||||||
create: requirePermission("campaigns:create"),
|
create: ({ req }) => !!req.user, // Any logged-in user can create (becomes owner)
|
||||||
update: requirePermission("campaigns:update"),
|
read: ({ req }) => !!req.user, // Anyone logged in can read
|
||||||
delete: requirePermission("campaigns:delete"),
|
update: requireCampaignOwnership("campaigns:update"),
|
||||||
read: ({ req }) => !!req.user,
|
delete: requireCampaignOwnership("campaigns:delete"),
|
||||||
},
|
},
|
||||||
fields: [
|
fields: [
|
||||||
{
|
{
|
||||||
|
|
@ -67,5 +68,14 @@ export const Campaigns: CollectionConfig = {
|
||||||
"Background image for the campaign selector card on the campaigns page. Renders translucent with hover effects. Recommended aspect ratio: 16:9 or square.",
|
"Background image for the campaign selector card on the campaigns page. Renders translucent with hover effects. Recommended aspect ratio: 16:9 or square.",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: "owner",
|
||||||
|
type: "relationship",
|
||||||
|
relationTo: "users",
|
||||||
|
admin: {
|
||||||
|
position: "sidebar",
|
||||||
|
description: "User who created this campaign. Set automatically on create.",
|
||||||
|
},
|
||||||
|
},
|
||||||
],
|
],
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -51,6 +51,12 @@ export function CampaignCard({ campaign }: CampaignCardProps) {
|
||||||
roundedDuration >= 60 ? `${durationHours}h ${durationMinutes}m` : `${roundedDuration}m`;
|
roundedDuration >= 60 ? `${durationHours}h ${durationMinutes}m` : `${roundedDuration}m`;
|
||||||
const playersLabel = Number.isInteger(avgPlayers) ? avgPlayers.toString() : avgPlayers.toFixed(1);
|
const playersLabel = Number.isInteger(avgPlayers) ? avgPlayers.toString() : avgPlayers.toFixed(1);
|
||||||
|
|
||||||
|
const owner = (campaign as any).owner;
|
||||||
|
const ownerName =
|
||||||
|
owner && typeof owner === "object" && "displayName" in owner
|
||||||
|
? owner.displayName
|
||||||
|
: "Unknown";
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Link
|
<Link
|
||||||
href={`/intelligence/campaigns/${campaign.id}`}
|
href={`/intelligence/campaigns/${campaign.id}`}
|
||||||
|
|
@ -115,6 +121,12 @@ export function CampaignCard({ campaign }: CampaignCardProps) {
|
||||||
<span className="sr-only">Avg duration</span>
|
<span className="sr-only">Avg duration</span>
|
||||||
<span className="truncate">{durationLabel}</span>
|
<span className="truncate">{durationLabel}</span>
|
||||||
</div>
|
</div>
|
||||||
|
<div className="flex min-w-0 flex-1 items-center justify-center gap-1.5 text-xs text-white/80">
|
||||||
|
<div className="text-right">
|
||||||
|
<div className="truncate">{ownerName}</div>
|
||||||
|
<span className="text-[10px] uppercase tracking-wider text-white/60">Owner</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</footer>
|
</footer>
|
||||||
</article>
|
</article>
|
||||||
</Link>
|
</Link>
|
||||||
|
|
|
||||||
164
src/components/frontend/intelligence/CreateCampaignDialog.tsx
Normal file
164
src/components/frontend/intelligence/CreateCampaignDialog.tsx
Normal file
|
|
@ -0,0 +1,164 @@
|
||||||
|
"use client";
|
||||||
|
|
||||||
|
import { useState } from "react";
|
||||||
|
import { createCampaign } from "./create";
|
||||||
|
import { Button } from "@/components/ui/button";
|
||||||
|
import { Input } from "@/components/ui/input";
|
||||||
|
import { Textarea } from "@/components/ui/textarea";
|
||||||
|
import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle, DialogTrigger } from "@/components/ui/dialog";
|
||||||
|
import { Shield, Loader2 } from "lucide-react";
|
||||||
|
|
||||||
|
export function CreateCampaignDialog() {
|
||||||
|
const [open, setOpen] = useState(false);
|
||||||
|
const [isSubmitting, setIsSubmitting] = useState(false);
|
||||||
|
|
||||||
|
const [formData, setFormData] = useState({
|
||||||
|
name: "",
|
||||||
|
summary: "An Arma 3 campaign.",
|
||||||
|
status: "concept" as "concept" | "inProgress" | "completed",
|
||||||
|
description: "",
|
||||||
|
startDate: "",
|
||||||
|
endDate: "",
|
||||||
|
});
|
||||||
|
|
||||||
|
const handleSubmit = async (e: React.FormEvent) => {
|
||||||
|
e.preventDefault();
|
||||||
|
setIsSubmitting(true);
|
||||||
|
|
||||||
|
const result = await createCampaign(formData);
|
||||||
|
|
||||||
|
setIsSubmitting(false);
|
||||||
|
|
||||||
|
if (result.success) {
|
||||||
|
setOpen(false);
|
||||||
|
setFormData({
|
||||||
|
name: "",
|
||||||
|
summary: "An Arma 3 campaign.",
|
||||||
|
status: "concept",
|
||||||
|
description: "",
|
||||||
|
startDate: "",
|
||||||
|
endDate: "",
|
||||||
|
});
|
||||||
|
window.location.href = "/intelligence/campaigns";
|
||||||
|
} else {
|
||||||
|
alert("Error creating campaign: " + (result.error || "Unknown error"));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Dialog open={open} onOpenChange={setOpen}>
|
||||||
|
<DialogTrigger asChild>
|
||||||
|
<Button variant="outline" size="sm" className="gap-2">
|
||||||
|
<Shield className="size-4" />
|
||||||
|
<span>Create campaign</span>
|
||||||
|
</Button>
|
||||||
|
</DialogTrigger>
|
||||||
|
<DialogContent>
|
||||||
|
<DialogHeader>
|
||||||
|
<DialogTitle>Create New Campaign</DialogTitle>
|
||||||
|
<DialogDescription>
|
||||||
|
Create a new Arma 3 campaign to manage your operations.
|
||||||
|
</DialogDescription>
|
||||||
|
</DialogHeader>
|
||||||
|
<form onSubmit={handleSubmit} className="space-y-4">
|
||||||
|
<div className="space-y-2">
|
||||||
|
<label htmlFor="name" className="text-sm font-medium">
|
||||||
|
Campaign Name <span className="text-red-500">*</span>
|
||||||
|
</label>
|
||||||
|
<Input
|
||||||
|
id="name"
|
||||||
|
value={formData.name}
|
||||||
|
onChange={(e) => setFormData({ ...formData, name: e.target.value })}
|
||||||
|
placeholder="Enter campaign name"
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="space-y-2">
|
||||||
|
<label htmlFor="summary" className="text-sm font-medium">
|
||||||
|
Summary <span className="text-red-500">*</span>
|
||||||
|
</label>
|
||||||
|
<Textarea
|
||||||
|
id="summary"
|
||||||
|
value={formData.summary}
|
||||||
|
onChange={(e) => setFormData({ ...formData, summary: e.target.value })}
|
||||||
|
placeholder="Brief description of the campaign"
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="space-y-2">
|
||||||
|
<label htmlFor="status" className="text-sm font-medium">
|
||||||
|
Status <span className="text-red-500">*</span>
|
||||||
|
</label>
|
||||||
|
<select
|
||||||
|
id="status"
|
||||||
|
value={formData.status}
|
||||||
|
onChange={(e) => setFormData({ ...formData, status: e.target.value as any })}
|
||||||
|
className="w-full rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"
|
||||||
|
required
|
||||||
|
>
|
||||||
|
<option value="concept">Concept</option>
|
||||||
|
<option value="inProgress">In Progress</option>
|
||||||
|
<option value="completed">Completed</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="space-y-2">
|
||||||
|
<label htmlFor="description" className="text-sm font-medium">
|
||||||
|
Description
|
||||||
|
</label>
|
||||||
|
<Textarea
|
||||||
|
id="description"
|
||||||
|
value={formData.description}
|
||||||
|
onChange={(e) => setFormData({ ...formData, description: e.target.value })}
|
||||||
|
placeholder="Optional: Detailed description of the campaign"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="space-y-2">
|
||||||
|
<label htmlFor="startDate" className="text-sm font-medium">
|
||||||
|
Start Date
|
||||||
|
</label>
|
||||||
|
<Input
|
||||||
|
id="startDate"
|
||||||
|
type="date"
|
||||||
|
value={formData.startDate}
|
||||||
|
onChange={(e) => setFormData({ ...formData, startDate: e.target.value })}
|
||||||
|
max={formData.endDate}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="space-y-2">
|
||||||
|
<label htmlFor="endDate" className="text-sm font-medium">
|
||||||
|
End Date
|
||||||
|
</label>
|
||||||
|
<Input
|
||||||
|
id="endDate"
|
||||||
|
type="date"
|
||||||
|
value={formData.endDate}
|
||||||
|
onChange={(e) => setFormData({ ...formData, endDate: e.target.value })}
|
||||||
|
min={formData.startDate}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex justify-end gap-2">
|
||||||
|
<Button type="button" variant="outline" onClick={() => setOpen(false)}>
|
||||||
|
Cancel
|
||||||
|
</Button>
|
||||||
|
<Button type="submit" disabled={isSubmitting}>
|
||||||
|
{isSubmitting ? (
|
||||||
|
<>
|
||||||
|
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
|
||||||
|
Creating...
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
"Create Campaign"
|
||||||
|
)}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
|
);
|
||||||
|
}
|
||||||
72
src/components/frontend/intelligence/create.ts
Normal file
72
src/components/frontend/intelligence/create.ts
Normal file
|
|
@ -0,0 +1,72 @@
|
||||||
|
"use server";
|
||||||
|
|
||||||
|
import config from "@payload-config";
|
||||||
|
import { getPayload } from "payload";
|
||||||
|
import { isPayloadUser } from "@/utils/access-control/isPayloadUser";
|
||||||
|
|
||||||
|
interface CreateCampaignData {
|
||||||
|
name: string;
|
||||||
|
summary: string;
|
||||||
|
status: "concept" | "inProgress" | "completed";
|
||||||
|
startDate?: string;
|
||||||
|
endDate?: string;
|
||||||
|
description?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function authenticate() {
|
||||||
|
const payload = await getPayload({ config });
|
||||||
|
const { headers } = await import("next/headers");
|
||||||
|
const hdrs = await headers();
|
||||||
|
const { user } = await payload.auth({
|
||||||
|
headers: hdrs,
|
||||||
|
canSetHeaders: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!isPayloadUser(user)) {
|
||||||
|
throw new Error("Unauthorized");
|
||||||
|
}
|
||||||
|
|
||||||
|
return { payload, user };
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function createCampaign(data: CreateCampaignData) {
|
||||||
|
try {
|
||||||
|
const { payload, user } = await authenticate();
|
||||||
|
|
||||||
|
// Filter out undefined dates
|
||||||
|
const campaignData: Record<string, any> = {
|
||||||
|
name: data.name,
|
||||||
|
summary: data.summary,
|
||||||
|
status: data.status,
|
||||||
|
owner: user.id,
|
||||||
|
};
|
||||||
|
|
||||||
|
// Only add date fields if they have values
|
||||||
|
if (data.startDate) {
|
||||||
|
campaignData.startDate = data.startDate;
|
||||||
|
}
|
||||||
|
if (data.endDate) {
|
||||||
|
campaignData.endDate = data.endDate;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (data.description) {
|
||||||
|
campaignData.description = data.description;
|
||||||
|
}
|
||||||
|
|
||||||
|
const campaign = await payload.create({
|
||||||
|
collection: "campaigns",
|
||||||
|
data: campaignData as any,
|
||||||
|
depth: 1,
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
data: campaign,
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
error: error instanceof Error ? error.message : "Failed to create campaign",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -142,6 +142,10 @@ export const PERMISSION_GROUPS: PermissionGroup[] = [
|
||||||
{ value: "campaigns:delete", label: "Delete" },
|
{ value: "campaigns:delete", label: "Delete" },
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
group: "Campaigns",
|
||||||
|
permissions: [{ value: "campaigns:manage", label: "Manage Campaigns (owner bypass)" }],
|
||||||
|
},
|
||||||
{
|
{
|
||||||
group: "Factions",
|
group: "Factions",
|
||||||
permissions: [
|
permissions: [
|
||||||
|
|
@ -512,6 +516,7 @@ export const ALL_PERMISSION_VALUES = PERMISSION_GROUPS.flatMap((g) =>
|
||||||
"campaigns:read",
|
"campaigns:read",
|
||||||
"campaigns:update",
|
"campaigns:update",
|
||||||
"campaigns:delete",
|
"campaigns:delete",
|
||||||
|
"campaigns:manage",
|
||||||
"factions:create",
|
"factions:create",
|
||||||
"factions:read",
|
"factions:read",
|
||||||
"factions:update",
|
"factions:update",
|
||||||
|
|
@ -628,6 +633,7 @@ export const ALL_PERMISSION_VALUES = PERMISSION_GROUPS.flatMap((g) =>
|
||||||
"game-rules:update",
|
"game-rules:update",
|
||||||
"shims:read",
|
"shims:read",
|
||||||
"shims:update",
|
"shims:update",
|
||||||
|
"campaigns:manage",
|
||||||
"logistics:manage",
|
"logistics:manage",
|
||||||
"banking:manage",
|
"banking:manage",
|
||||||
"tickets:staff",
|
"tickets:staff",
|
||||||
|
|
|
||||||
|
|
@ -14,6 +14,7 @@ const USER_PERMISSIONS: Permission[] = [
|
||||||
"missions:read",
|
"missions:read",
|
||||||
"mission-attendances:read",
|
"mission-attendances:read",
|
||||||
"campaigns:read",
|
"campaigns:read",
|
||||||
|
"campaigns:manage",
|
||||||
"factions:read",
|
"factions:read",
|
||||||
"technologies:read",
|
"technologies:read",
|
||||||
"assets:read",
|
"assets:read",
|
||||||
|
|
|
||||||
|
|
@ -111,3 +111,52 @@ export function requireAnyPermission(...permissions: Permission[]) {
|
||||||
return hasAnyPermission(req.payload, req.user, ...permissions);
|
return hasAnyPermission(req.payload, req.user, ...permissions);
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Factory that creates a Payload collection access function requiring campaign ownership.
|
||||||
|
*
|
||||||
|
* A user can access a campaign if:
|
||||||
|
* 1. They own the campaign (owner field matches their user ID), OR
|
||||||
|
* 2. They have the "campaigns:manage" permission
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* access: {
|
||||||
|
* update: requireCampaignOwnership("campaigns:update"),
|
||||||
|
* delete: requireCampaignOwnership("campaigns:delete"),
|
||||||
|
* }
|
||||||
|
*/
|
||||||
|
export function requireCampaignOwnership(permission: Permission) {
|
||||||
|
return async ({ req, id }: { req: PayloadRequest; id?: any }) => {
|
||||||
|
if (!req.user) return false;
|
||||||
|
|
||||||
|
// Check permission first (grants access to all campaigns for managers)
|
||||||
|
const { permissions: userPerms, isSuperuser: su } = await loadUserPermissions(req.payload, req.user);
|
||||||
|
if (su || userPerms.has(permission)) return true;
|
||||||
|
|
||||||
|
// If no user permissions for manage, check ownership
|
||||||
|
let campaign;
|
||||||
|
try {
|
||||||
|
campaign = await req.payload.findByID({
|
||||||
|
collection: "campaigns",
|
||||||
|
id: id,
|
||||||
|
depth: 1,
|
||||||
|
overrideAccess: true,
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
// If campaign lookup fails (e.g. invalid ID), deny access
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!campaign) return false;
|
||||||
|
|
||||||
|
// User owns the campaign if owner field matches their ID
|
||||||
|
// campaign.owner can be number (ID) or User object
|
||||||
|
const campaignOwnerId = campaign.owner ? (typeof campaign.owner === "object" ? campaign.owner.id : campaign.owner) : null;
|
||||||
|
const userId = Number(req.user.id);
|
||||||
|
|
||||||
|
if (campaignOwnerId && campaignOwnerId === userId) return true;
|
||||||
|
|
||||||
|
// User does not own this campaign and lacks manage permission
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue