feat(rbac): migrate server actions and service layers to RBAC
Replace hasRoles calls with hasPermission in all server actions and page components. Update qualification checks (logistics, intelligence) to use permission-based checks instead of role name matching. Update staff lookup in tickets/staff.ts to query roles collection. Use enlistmentDate field on profile page instead of createdAt.
This commit is contained in:
parent
653caa8064
commit
eef1b11bb1
17 changed files with 181 additions and 97 deletions
|
|
@ -144,26 +144,34 @@ export async function requestDiscordUsernameChange(input: {
|
||||||
return { success: false, error: "That Discord username is already in use." };
|
return { success: false, error: "That Discord username is already in use." };
|
||||||
}
|
}
|
||||||
|
|
||||||
const staffRes = await payload.find({
|
const staffRoles = await payload.find({
|
||||||
collection: "users",
|
collection: "roles",
|
||||||
where: {
|
where: { slug: { in: ["admin", "developer"] } },
|
||||||
or: [{ roles: { contains: "admin" } }, { roles: { contains: "developer" } }],
|
limit: 2,
|
||||||
},
|
|
||||||
limit: 100,
|
|
||||||
depth: 0,
|
depth: 0,
|
||||||
overrideAccess: true,
|
overrideAccess: true,
|
||||||
});
|
});
|
||||||
const staffIds = [...new Set(staffRes.docs.map((doc) => doc.id as number))].filter(
|
const staffRoleIds = staffRoles.docs.map((d) => d.id);
|
||||||
(id) => id !== userId,
|
if (staffRoleIds.length > 0) {
|
||||||
);
|
const staffRes = await payload.find({
|
||||||
for (const staffId of staffIds) {
|
collection: "users",
|
||||||
await notifyUser(payload, {
|
where: { roleDocs: { in: staffRoleIds } },
|
||||||
userId: staffId,
|
limit: 100,
|
||||||
type: "account:discord-request",
|
depth: 0,
|
||||||
title: `Discord username change requested by ${user.username}`,
|
overrideAccess: true,
|
||||||
message: `wants to change their Discord username to "${trimmed}".`,
|
|
||||||
link: `/admin/collections/users/${userId}`,
|
|
||||||
});
|
});
|
||||||
|
const staffIds = [...new Set(staffRes.docs.map((doc) => doc.id as number))].filter(
|
||||||
|
(id) => id !== userId,
|
||||||
|
);
|
||||||
|
for (const staffId of staffIds) {
|
||||||
|
await notifyUser(payload, {
|
||||||
|
userId: staffId,
|
||||||
|
type: "account:discord-request",
|
||||||
|
title: `Discord username change requested by ${user.username}`,
|
||||||
|
message: `wants to change their Discord username to "${trimmed}".`,
|
||||||
|
link: `/admin/collections/users/${userId}`,
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
await notifyUser(payload, {
|
await notifyUser(payload, {
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,7 @@ import { notFound, redirect } from "next/navigation";
|
||||||
import { getPayload } from "payload";
|
import { getPayload } from "payload";
|
||||||
import type { Ticket } from "@/payload-types";
|
import type { Ticket } from "@/payload-types";
|
||||||
import { TicketDetail } from "@/components/frontend/helpdesk/TicketDetail";
|
import { TicketDetail } from "@/components/frontend/helpdesk/TicketDetail";
|
||||||
import hasRoles from "@/utils/access-control/hasRoles";
|
import { hasPermission } from "@/utils/access-control/hasPermission";
|
||||||
import { ArrowLeftIcon, LifeBuoyIcon } from "lucide-react";
|
import { ArrowLeftIcon, LifeBuoyIcon } from "lucide-react";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
|
|
||||||
|
|
@ -18,14 +18,16 @@ interface TicketPageProps {
|
||||||
|
|
||||||
export default async function TicketPage({ params }: TicketPageProps) {
|
export default async function TicketPage({ params }: TicketPageProps) {
|
||||||
const { id } = await params;
|
const { id } = await params;
|
||||||
|
const headers = await nextHeaders();
|
||||||
const payload = await getPayload({ config });
|
const payload = await getPayload({ config });
|
||||||
const { user } = await payload.auth({
|
const { user } = await payload.auth({
|
||||||
headers: await nextHeaders(),
|
headers,
|
||||||
canSetHeaders: false,
|
canSetHeaders: false,
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!user) {
|
if (!user) {
|
||||||
redirect("/login");
|
const pathname = headers.get("x-invoke-path") || `/helpdesk/${id}`;
|
||||||
|
redirect(`/login?returnTo=${encodeURIComponent(pathname)}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
const ticket = await payload
|
const ticket = await payload
|
||||||
|
|
@ -40,8 +42,8 @@ export default async function TicketPage({ params }: TicketPageProps) {
|
||||||
notFound();
|
notFound();
|
||||||
}
|
}
|
||||||
|
|
||||||
const typedTicket = ticket as unknown as Ticket;
|
const typedTicket = ticket as unknown as Ticket;
|
||||||
const isStaff = hasRoles(["admin", "developer"], user);
|
const isStaff = await hasPermission(payload, user, "tickets:staff");
|
||||||
const assigneeOptions = isStaff
|
const assigneeOptions = isStaff
|
||||||
? (
|
? (
|
||||||
await payload.find({
|
await payload.find({
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,7 @@
|
||||||
import config from "@payload-config";
|
import config from "@payload-config";
|
||||||
import { getPayload } from "payload";
|
import { getPayload } from "payload";
|
||||||
import type { Ticket } from "@/payload-types";
|
import type { Ticket } from "@/payload-types";
|
||||||
import hasRoles from "@/utils/access-control/hasRoles";
|
import { hasPermission } from "@/utils/access-control/hasPermission";
|
||||||
import { emitGameEvent } from "@/utils/event-log/emit";
|
import { emitGameEvent } from "@/utils/event-log/emit";
|
||||||
import { EventTypes } from "@/utils/event-log/eventTypes";
|
import { EventTypes } from "@/utils/event-log/eventTypes";
|
||||||
import { notifyUser } from "@/lib/notifications";
|
import { notifyUser } from "@/lib/notifications";
|
||||||
|
|
@ -144,7 +144,7 @@ export async function replyToTicket(ticketId: number, content: string): Promise<
|
||||||
const userId = user.id as number;
|
const userId = user.id as number;
|
||||||
const ticket = await getTicketOrThrow(payload, ticketId);
|
const ticket = await getTicketOrThrow(payload, ticketId);
|
||||||
|
|
||||||
const isStaff = hasRoles(["admin", "developer"], user);
|
const isStaff = await hasPermission(payload, user, "tickets:staff");
|
||||||
const reporterId = reporterIdOf(ticket);
|
const reporterId = reporterIdOf(ticket);
|
||||||
if (!isStaff && reporterId !== userId) {
|
if (!isStaff && reporterId !== userId) {
|
||||||
throw new Error("You don't have access to this ticket.");
|
throw new Error("You don't have access to this ticket.");
|
||||||
|
|
@ -241,7 +241,7 @@ export async function updateTicketStatus(
|
||||||
const ticket = await getTicketOrThrow(payload, ticketId);
|
const ticket = await getTicketOrThrow(payload, ticketId);
|
||||||
if (status === ticket.status) return { success: true };
|
if (status === ticket.status) return { success: true };
|
||||||
|
|
||||||
const isStaff = hasRoles(["admin", "developer"], user);
|
const isStaff = await hasPermission(payload, user, "tickets:staff");
|
||||||
const reporterId = reporterIdOf(ticket);
|
const reporterId = reporterIdOf(ticket);
|
||||||
if (!isStaff) {
|
if (!isStaff) {
|
||||||
const reporterCancel =
|
const reporterCancel =
|
||||||
|
|
@ -299,7 +299,7 @@ export async function assignTicket(
|
||||||
const { payload, user } = await authenticate();
|
const { payload, user } = await authenticate();
|
||||||
const userId = user.id as number;
|
const userId = user.id as number;
|
||||||
|
|
||||||
if (!hasRoles(["admin", "developer"], user)) {
|
if (!(await hasPermission(payload, user, "tickets:staff"))) {
|
||||||
throw new Error("Only staff can assign tickets.");
|
throw new Error("Only staff can assign tickets.");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,7 @@ import { redirect } from "next/navigation";
|
||||||
import { getPayload } from "payload";
|
import { getPayload } from "payload";
|
||||||
import type { Ticket } from "@/payload-types";
|
import type { Ticket } from "@/payload-types";
|
||||||
import { TicketsView } from "@/components/frontend/helpdesk/TicketsView";
|
import { TicketsView } from "@/components/frontend/helpdesk/TicketsView";
|
||||||
import hasRoles from "@/utils/access-control/hasRoles";
|
import { hasPermission } from "@/utils/access-control/hasPermission";
|
||||||
import { LifeBuoyIcon } from "lucide-react";
|
import { LifeBuoyIcon } from "lucide-react";
|
||||||
|
|
||||||
export const metadata = {
|
export const metadata = {
|
||||||
|
|
@ -12,14 +12,16 @@ export const metadata = {
|
||||||
};
|
};
|
||||||
|
|
||||||
export default async function HelpdeskPage() {
|
export default async function HelpdeskPage() {
|
||||||
|
const headers = await nextHeaders();
|
||||||
const payload = await getPayload({ config });
|
const payload = await getPayload({ config });
|
||||||
const { user } = await payload.auth({
|
const { user } = await payload.auth({
|
||||||
headers: await nextHeaders(),
|
headers,
|
||||||
canSetHeaders: false,
|
canSetHeaders: false,
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!user) {
|
if (!user) {
|
||||||
redirect("/login");
|
const pathname = headers.get("x-invoke-path") || "/helpdesk";
|
||||||
|
redirect(`/login?returnTo=${encodeURIComponent(pathname)}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
const ticketsRes = await payload.find({
|
const ticketsRes = await payload.find({
|
||||||
|
|
@ -28,8 +30,8 @@ export default async function HelpdeskPage() {
|
||||||
limit: 200,
|
limit: 200,
|
||||||
depth: 2,
|
depth: 2,
|
||||||
});
|
});
|
||||||
const tickets = ticketsRes.docs as unknown as Ticket[];
|
const tickets = ticketsRes.docs as unknown as Ticket[];
|
||||||
const isStaff = hasRoles(["admin", "developer"], user);
|
const isStaff = await hasPermission(payload, user, "tickets:staff");
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col gap-6 p-5">
|
<div className="flex flex-col gap-6 p-5">
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,7 @@
|
||||||
import config from "@payload-config";
|
import config from "@payload-config";
|
||||||
import { getPayload } from "payload";
|
import { getPayload } from "payload";
|
||||||
import type { Asset, Loadout, LockerStorage, User } from "@/payload-types";
|
import type { Asset, Loadout, LockerStorage, User } from "@/payload-types";
|
||||||
import hasRoles from "@/utils/access-control/hasRoles";
|
import { hasPermission } from "@/utils/access-control/hasPermission";
|
||||||
import { emitGameEvent } from "@/utils/event-log/emit";
|
import { emitGameEvent } from "@/utils/event-log/emit";
|
||||||
import { EventTypes } from "@/utils/event-log/eventTypes";
|
import { EventTypes } from "@/utils/event-log/eventTypes";
|
||||||
import {
|
import {
|
||||||
|
|
@ -24,6 +24,7 @@ import {
|
||||||
skinAppliesTo,
|
skinAppliesTo,
|
||||||
toLockerGridItems,
|
toLockerGridItems,
|
||||||
} from "@/lib/locker";
|
} from "@/lib/locker";
|
||||||
|
import { hasLogisticsQualification } from "@/utils/access-control/hasLogisticsQualification";
|
||||||
|
|
||||||
export interface ActionResult<T = undefined> {
|
export interface ActionResult<T = undefined> {
|
||||||
success: boolean;
|
success: boolean;
|
||||||
|
|
@ -45,8 +46,9 @@ async function authenticate() {
|
||||||
return { payload, user };
|
return { payload, user };
|
||||||
}
|
}
|
||||||
|
|
||||||
function isLockerManager(user: User): boolean {
|
async function isLockerManager(payload: Awaited<ReturnType<typeof getPayload>>, user: User): Promise<boolean> {
|
||||||
return hasRoles(["admin", "developer"], user);
|
if (await hasPermission(payload, user, "locker-storages:update")) return true;
|
||||||
|
return hasLogisticsQualification(payload, user);
|
||||||
}
|
}
|
||||||
|
|
||||||
async function getLockerWithItems(
|
async function getLockerWithItems(
|
||||||
|
|
@ -114,7 +116,7 @@ export async function addItemToLocker(
|
||||||
): Promise<ActionResult<LockerStorage>> {
|
): Promise<ActionResult<LockerStorage>> {
|
||||||
try {
|
try {
|
||||||
const { payload, user } = await authenticate();
|
const { payload, user } = await authenticate();
|
||||||
if (!isLockerManager(user)) {
|
if (!(await hasPermission(payload, user, "locker-storages:update"))) {
|
||||||
return {
|
return {
|
||||||
success: false,
|
success: false,
|
||||||
error: "Insufficient permissions. Admin or Developer role required to add items.",
|
error: "Insufficient permissions. Admin or Developer role required to add items.",
|
||||||
|
|
@ -759,7 +761,7 @@ async function getOwnedLoadout(
|
||||||
? (loadout.ownerUser as { id: number }).id
|
? (loadout.ownerUser as { id: number }).id
|
||||||
: (loadout.ownerUser as number);
|
: (loadout.ownerUser as number);
|
||||||
|
|
||||||
if (ownerId !== user.id && !isLockerManager(user)) return null;
|
if (ownerId !== user.id && !(await hasPermission(payload, user, "locker-storages:update"))) return null;
|
||||||
return loadout;
|
return loadout;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,7 @@ import type { Asset, Loadout, LockerStorage } from "@/payload-types";
|
||||||
import { LockKeyholeIcon } from "lucide-react";
|
import { LockKeyholeIcon } from "lucide-react";
|
||||||
import { ensureLockerStorage, getLockerGridDimensions } from "@/lib/locker";
|
import { ensureLockerStorage, getLockerGridDimensions } from "@/lib/locker";
|
||||||
import { LockerView } from "@/components/frontend/locker/LockerView";
|
import { LockerView } from "@/components/frontend/locker/LockerView";
|
||||||
import hasRoles from "@/utils/access-control/hasRoles";
|
import { hasPermission } from "@/utils/access-control/hasPermission";
|
||||||
|
|
||||||
export const metadata = {
|
export const metadata = {
|
||||||
title: "Locker — Polaris Task Force",
|
title: "Locker — Polaris Task Force",
|
||||||
|
|
@ -34,9 +34,9 @@ export default async function LockerPage() {
|
||||||
limit: 100,
|
limit: 100,
|
||||||
depth: 1,
|
depth: 1,
|
||||||
});
|
});
|
||||||
const loadouts = loadoutsRes.docs as unknown as Loadout[];
|
const loadouts = loadoutsRes.docs as unknown as Loadout[];
|
||||||
|
|
||||||
const isManager = user ? hasRoles(["admin", "developer"], user) : false;
|
const isManager = user ? (await hasPermission(payload, user, "locker-storages:update")) : false;
|
||||||
|
|
||||||
let assetsCatalog: Asset[] = [];
|
let assetsCatalog: Asset[] = [];
|
||||||
if (isManager) {
|
if (isManager) {
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,7 @@ import { headers as nextHeaders } from "next/headers";
|
||||||
import { notFound } from "next/navigation";
|
import { notFound } from "next/navigation";
|
||||||
import type { BankAccount, LedgerEntry } from "@/payload-types";
|
import type { BankAccount, LedgerEntry } from "@/payload-types";
|
||||||
import { AccountDetail } from "@/components/frontend/banking/AccountDetail";
|
import { AccountDetail } from "@/components/frontend/banking/AccountDetail";
|
||||||
import hasRoles from "@/utils/access-control/hasRoles";
|
import { hasPermission } from "@/utils/access-control/hasPermission";
|
||||||
import { hasLogisticsQualification } from "@/utils/access-control/hasLogisticsQualification";
|
import { hasLogisticsQualification } from "@/utils/access-control/hasLogisticsQualification";
|
||||||
|
|
||||||
export const metadata = {
|
export const metadata = {
|
||||||
|
|
@ -35,12 +35,12 @@ export default async function AccountPage({ params }: AccountPageProps) {
|
||||||
notFound();
|
notFound();
|
||||||
}
|
}
|
||||||
|
|
||||||
const typedAccount = account as unknown as BankAccount;
|
const typedAccount = account as unknown as BankAccount;
|
||||||
|
|
||||||
const isManager = user
|
const isManager = user
|
||||||
? hasRoles(["admin", "developer"], user) ||
|
? (await hasPermission(payload, user, "banking:manage")) ||
|
||||||
(await hasLogisticsQualification(payload, user).catch(() => false))
|
(await hasLogisticsQualification(payload, user).catch(() => false))
|
||||||
: false;
|
: false;
|
||||||
|
|
||||||
const ownerId =
|
const ownerId =
|
||||||
typeof typedAccount.ownerUser === "object"
|
typeof typedAccount.ownerUser === "object"
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,7 @@
|
||||||
import config from "@payload-config";
|
import config from "@payload-config";
|
||||||
import { getPayload } from "payload";
|
import { getPayload } from "payload";
|
||||||
import type { User } from "@/payload-types";
|
import type { User } from "@/payload-types";
|
||||||
import hasRoles from "@/utils/access-control/hasRoles";
|
import { hasPermission } from "@/utils/access-control/hasPermission";
|
||||||
import { hasLogisticsQualification } from "@/utils/access-control/hasLogisticsQualification";
|
import { hasLogisticsQualification } from "@/utils/access-control/hasLogisticsQualification";
|
||||||
import { emitGameEvent } from "@/utils/event-log/emit";
|
import { emitGameEvent } from "@/utils/event-log/emit";
|
||||||
import { EventTypes } from "@/utils/event-log/eventTypes";
|
import { EventTypes } from "@/utils/event-log/eventTypes";
|
||||||
|
|
@ -42,7 +42,7 @@ async function isBankingManager(
|
||||||
payload: Awaited<ReturnType<typeof getPayload>>,
|
payload: Awaited<ReturnType<typeof getPayload>>,
|
||||||
user: User,
|
user: User,
|
||||||
): Promise<boolean> {
|
): Promise<boolean> {
|
||||||
if (hasRoles(["admin", "developer"], user)) return true;
|
if (await hasPermission(payload, user, "banking:manage")) return true;
|
||||||
return hasLogisticsQualification(payload, user);
|
return hasLogisticsQualification(payload, user);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -188,7 +188,7 @@ async function moveFunds(
|
||||||
): Promise<ActionResult<number>> {
|
): Promise<ActionResult<number>> {
|
||||||
try {
|
try {
|
||||||
const { payload, user } = await authenticate();
|
const { payload, user } = await authenticate();
|
||||||
if (!hasRoles(["user"], user)) {
|
if (!(await hasPermission(payload, user, "bank-accounts:create"))) {
|
||||||
return { success: false, error: "Insufficient permissions." };
|
return { success: false, error: "Insufficient permissions." };
|
||||||
}
|
}
|
||||||
if (!amount || amount <= 0) {
|
if (!amount || amount <= 0) {
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,7 @@ import { headers as nextHeaders } from "next/headers";
|
||||||
import type { BankAccount, BankTransaction, Faction, Resource } from "@/payload-types";
|
import type { BankAccount, BankTransaction, Faction, Resource } from "@/payload-types";
|
||||||
import { LandmarkIcon } from "lucide-react";
|
import { LandmarkIcon } from "lucide-react";
|
||||||
import { BankingOverview } from "@/components/frontend/banking/BankingOverview";
|
import { BankingOverview } from "@/components/frontend/banking/BankingOverview";
|
||||||
import hasRoles from "@/utils/access-control/hasRoles";
|
import { hasPermission } from "@/utils/access-control/hasPermission";
|
||||||
import { hasLogisticsQualification } from "@/utils/access-control/hasLogisticsQualification";
|
import { hasLogisticsQualification } from "@/utils/access-control/hasLogisticsQualification";
|
||||||
|
|
||||||
export const metadata = {
|
export const metadata = {
|
||||||
|
|
@ -34,20 +34,20 @@ export default async function BankingPage() {
|
||||||
});
|
});
|
||||||
const recentTransactions = transactionsRes.docs as unknown as BankTransaction[];
|
const recentTransactions = transactionsRes.docs as unknown as BankTransaction[];
|
||||||
|
|
||||||
const factionsRes = await payload.find({
|
const factionsRes = await payload.find({
|
||||||
collection: "factions",
|
collection: "factions",
|
||||||
limit: 100,
|
limit: 100,
|
||||||
depth: 0,
|
depth: 0,
|
||||||
});
|
});
|
||||||
const factions = (factionsRes.docs as unknown as Faction[]).map((f) => ({
|
const factions = (factionsRes.docs as unknown as Faction[]).map((f) => ({
|
||||||
id: f.id,
|
id: f.id,
|
||||||
name: f.name,
|
name: f.name,
|
||||||
}));
|
}));
|
||||||
|
|
||||||
const isManager = user
|
const isManager = user
|
||||||
? hasRoles(["admin", "developer"], user) ||
|
? (await hasPermission(payload, user, "banking:manage")) ||
|
||||||
(await hasLogisticsQualification(payload, user).catch(() => false))
|
(await hasLogisticsQualification(payload, user).catch(() => false))
|
||||||
: false;
|
: false;
|
||||||
|
|
||||||
const currentUser = user
|
const currentUser = user
|
||||||
? {
|
? {
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,7 @@
|
||||||
import config from "@payload-config";
|
import config from "@payload-config";
|
||||||
import { getPayload } from "payload";
|
import { getPayload } from "payload";
|
||||||
import type { Asset, LockerStorage, MarketListing, MarketNegotiation, User } from "@/payload-types";
|
import type { Asset, LockerStorage, MarketListing, MarketNegotiation, User } from "@/payload-types";
|
||||||
import hasRoles from "@/utils/access-control/hasRoles";
|
import { hasPermission } from "@/utils/access-control/hasPermission";
|
||||||
import { emitGameEvent } from "@/utils/event-log/emit";
|
import { emitGameEvent } from "@/utils/event-log/emit";
|
||||||
import { EventTypes } from "@/utils/event-log/eventTypes";
|
import { EventTypes } from "@/utils/event-log/eventTypes";
|
||||||
import {
|
import {
|
||||||
|
|
@ -44,8 +44,8 @@ async function authenticate() {
|
||||||
return { payload, user };
|
return { payload, user };
|
||||||
}
|
}
|
||||||
|
|
||||||
function isMarketManager(user: User): boolean {
|
async function isMarketManager(payload: Awaited<ReturnType<typeof getPayload>>, user: User): Promise<boolean> {
|
||||||
return hasRoles(["admin", "developer"], user);
|
return await hasPermission(payload, user, "market-listings:update");
|
||||||
}
|
}
|
||||||
|
|
||||||
function sellerIdOf(listing: MarketListing): number | null {
|
function sellerIdOf(listing: MarketListing): number | null {
|
||||||
|
|
@ -467,7 +467,7 @@ export async function cancelMarketListing(listingId: number): Promise<ActionResu
|
||||||
return { success: false, error: "Only active listings can be cancelled." };
|
return { success: false, error: "Only active listings can be cancelled." };
|
||||||
}
|
}
|
||||||
const sellerId = sellerIdOf(listing);
|
const sellerId = sellerIdOf(listing);
|
||||||
if (sellerId !== userId && !isMarketManager(user)) {
|
if (sellerId !== userId && !(await isMarketManager(payload, user))) {
|
||||||
return { success: false, error: "You can only cancel your own listings." };
|
return { success: false, error: "You can only cancel your own listings." };
|
||||||
}
|
}
|
||||||
if (listing.isAutoGenerated) {
|
if (listing.isAutoGenerated) {
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,7 @@ import { headers as nextHeaders } from "next/headers";
|
||||||
import type { LockerStorage, MarketListing, MarketNegotiation } from "@/payload-types";
|
import type { LockerStorage, MarketListing, MarketNegotiation } from "@/payload-types";
|
||||||
import { StoreIcon } from "lucide-react";
|
import { StoreIcon } from "lucide-react";
|
||||||
import { MarketView } from "@/components/frontend/market/MarketView";
|
import { MarketView } from "@/components/frontend/market/MarketView";
|
||||||
import hasRoles from "@/utils/access-control/hasRoles";
|
import { hasPermission } from "@/utils/access-control/hasPermission";
|
||||||
import { ensureLockerStorage } from "@/lib/locker";
|
import { ensureLockerStorage } from "@/lib/locker";
|
||||||
import { getMainCurrencyName } from "@/lib/banking";
|
import { getMainCurrencyName } from "@/lib/banking";
|
||||||
|
|
||||||
|
|
@ -76,7 +76,7 @@ export default async function MarketPage() {
|
||||||
return false;
|
return false;
|
||||||
});
|
});
|
||||||
|
|
||||||
const isManager = user ? hasRoles(["admin", "developer"], user) : false;
|
const isManager = user ? (await hasPermission(payload, user, "logistics:manage")) : false;
|
||||||
const currencyLabel = await getMainCurrencyName(payload);
|
const currencyLabel = await getMainCurrencyName(payload);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,7 @@
|
||||||
import config from "@payload-config";
|
import config from "@payload-config";
|
||||||
import { getPayload } from "payload";
|
import { getPayload } from "payload";
|
||||||
import type { GameStructure, GameVehicle, Resource, Structure, Vehicle } from "@/payload-types";
|
import type { GameStructure, GameVehicle, Resource, Structure, Vehicle } from "@/payload-types";
|
||||||
import hasRoles from "@/utils/access-control/hasRoles";
|
import { hasPermission } from "@/utils/access-control/hasPermission";
|
||||||
import { emitGameEvent } from "@/utils/event-log/emit";
|
import { emitGameEvent } from "@/utils/event-log/emit";
|
||||||
import { EventTypes } from "@/utils/event-log/eventTypes";
|
import { EventTypes } from "@/utils/event-log/eventTypes";
|
||||||
import { calculateDistance } from "@/lib/distance";
|
import { calculateDistance } from "@/lib/distance";
|
||||||
|
|
@ -42,7 +42,7 @@ export async function createShipment(params: {
|
||||||
try {
|
try {
|
||||||
const { payload, user } = await authenticate();
|
const { payload, user } = await authenticate();
|
||||||
|
|
||||||
if (!hasRoles(["user"], user)) {
|
if (!(await hasPermission(payload, user, "shipments:create"))) {
|
||||||
return { success: false, error: "Insufficient permissions." };
|
return { success: false, error: "Insufficient permissions." };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -380,7 +380,7 @@ export async function cancelShipment(shipmentId: number): Promise<ActionResult>
|
||||||
try {
|
try {
|
||||||
const { payload, user } = await authenticate();
|
const { payload, user } = await authenticate();
|
||||||
|
|
||||||
if (!hasRoles(["user"], user)) {
|
if (!(await hasPermission(payload, user, "shipments:update"))) {
|
||||||
return { success: false, error: "Insufficient permissions." };
|
return { success: false, error: "Insufficient permissions." };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -501,7 +501,7 @@ export async function toggleAutoReturn(
|
||||||
try {
|
try {
|
||||||
const { payload, user } = await authenticate();
|
const { payload, user } = await authenticate();
|
||||||
|
|
||||||
if (!hasRoles(["user"], user)) {
|
if (!(await hasPermission(payload, user, "shipments:update"))) {
|
||||||
return { success: false, error: "Insufficient permissions." };
|
return { success: false, error: "Insufficient permissions." };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,7 @@
|
||||||
import config from "@payload-config";
|
import config from "@payload-config";
|
||||||
import { getPayload } from "payload";
|
import { getPayload } from "payload";
|
||||||
import { GameStructure, Resource, Structure } from "@/payload-types";
|
import { GameStructure, Resource, Structure } from "@/payload-types";
|
||||||
import hasRoles from "@/utils/access-control/hasRoles";
|
import { hasPermission } from "@/utils/access-control/hasPermission";
|
||||||
import { emitGameEvent } from "@/utils/event-log/emit";
|
import { emitGameEvent } from "@/utils/event-log/emit";
|
||||||
import { EventTypes } from "@/utils/event-log/eventTypes";
|
import { EventTypes } from "@/utils/event-log/eventTypes";
|
||||||
import { checkStorageDeposit, storageViolationMessage } from "@/lib/storageRules";
|
import { checkStorageDeposit, storageViolationMessage } from "@/lib/storageRules";
|
||||||
|
|
@ -74,10 +74,10 @@ export async function addResource(
|
||||||
): Promise<ActionResult> {
|
): Promise<ActionResult> {
|
||||||
try {
|
try {
|
||||||
const { payload, user } = await authenticate();
|
const { payload, user } = await authenticate();
|
||||||
if (!hasRoles(["admin"], user)) {
|
if (!(await hasPermission(payload, user, "structures:update"))) {
|
||||||
return {
|
return {
|
||||||
success: false,
|
success: false,
|
||||||
error: "Insufficient permissions. Admin or Developer role required for deposit.",
|
error: "Insufficient permissions. User, Admin, or Developer role required.",
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -264,7 +264,7 @@ export async function removeResource(
|
||||||
): Promise<ActionResult> {
|
): Promise<ActionResult> {
|
||||||
try {
|
try {
|
||||||
const { payload, user } = await authenticate();
|
const { payload, user } = await authenticate();
|
||||||
if (!hasRoles(["admin"], user)) {
|
if (!(await hasPermission(payload, user, "structures:update"))) {
|
||||||
return {
|
return {
|
||||||
success: false,
|
success: false,
|
||||||
error: "Insufficient permissions. Admin or Developer role required for withdrawal.",
|
error: "Insufficient permissions. Admin or Developer role required for withdrawal.",
|
||||||
|
|
@ -373,7 +373,7 @@ export async function transferResource(
|
||||||
): Promise<ActionResult> {
|
): Promise<ActionResult> {
|
||||||
try {
|
try {
|
||||||
const { payload, user } = await authenticate();
|
const { payload, user } = await authenticate();
|
||||||
if (!hasRoles(["admin"], user) && !hasRoles(["user"], user)) {
|
if (!(await hasPermission(payload, user, "structures:update"))) {
|
||||||
return {
|
return {
|
||||||
success: false,
|
success: false,
|
||||||
error: "Insufficient permissions. User, Admin, or Developer role required for transfer.",
|
error: "Insufficient permissions. User, Admin, or Developer role required for transfer.",
|
||||||
|
|
@ -524,7 +524,7 @@ export async function placeResourceOnGrid(
|
||||||
): Promise<GridActionResult> {
|
): Promise<GridActionResult> {
|
||||||
try {
|
try {
|
||||||
const { payload, user } = await authenticate();
|
const { payload, user } = await authenticate();
|
||||||
if (!hasRoles(["admin"], user)) {
|
if (!(await hasPermission(payload, user, "structures:update"))) {
|
||||||
return {
|
return {
|
||||||
success: false,
|
success: false,
|
||||||
error: "Insufficient permissions. Admin or Developer role required for placement.",
|
error: "Insufficient permissions. Admin or Developer role required for placement.",
|
||||||
|
|
@ -641,7 +641,7 @@ export async function moveGridItem(
|
||||||
): Promise<GridActionResult> {
|
): Promise<GridActionResult> {
|
||||||
try {
|
try {
|
||||||
const { payload, user } = await authenticate();
|
const { payload, user } = await authenticate();
|
||||||
if (!hasRoles(["admin"], user) && !hasRoles(["user"], user)) {
|
if (!(await hasPermission(payload, user, "structures:update"))) {
|
||||||
return {
|
return {
|
||||||
success: false,
|
success: false,
|
||||||
error: "Insufficient permissions. User, Admin, or Developer role required.",
|
error: "Insufficient permissions. User, Admin, or Developer role required.",
|
||||||
|
|
@ -730,7 +730,7 @@ export async function mergeGridStacks(
|
||||||
): Promise<GridActionResult> {
|
): Promise<GridActionResult> {
|
||||||
try {
|
try {
|
||||||
const { payload, user } = await authenticate();
|
const { payload, user } = await authenticate();
|
||||||
if (!hasRoles(["admin"], user) && !hasRoles(["user"], user)) {
|
if (!(await hasPermission(payload, user, "structures:update"))) {
|
||||||
return {
|
return {
|
||||||
success: false,
|
success: false,
|
||||||
error: "Insufficient permissions. User, Admin, or Developer role required.",
|
error: "Insufficient permissions. User, Admin, or Developer role required.",
|
||||||
|
|
@ -812,7 +812,7 @@ export async function rotateGridItem(
|
||||||
): Promise<GridActionResult> {
|
): Promise<GridActionResult> {
|
||||||
try {
|
try {
|
||||||
const { payload, user } = await authenticate();
|
const { payload, user } = await authenticate();
|
||||||
if (!hasRoles(["admin"], user) && !hasRoles(["user"], user)) {
|
if (!(await hasPermission(payload, user, "structures:update"))) {
|
||||||
return {
|
return {
|
||||||
success: false,
|
success: false,
|
||||||
error: "Insufficient permissions. User, Admin, or Developer role required.",
|
error: "Insufficient permissions. User, Admin, or Developer role required.",
|
||||||
|
|
@ -899,7 +899,7 @@ export async function removeGridItem(
|
||||||
): Promise<GridActionResult> {
|
): Promise<GridActionResult> {
|
||||||
try {
|
try {
|
||||||
const { payload, user } = await authenticate();
|
const { payload, user } = await authenticate();
|
||||||
if (!hasRoles(["admin"], user)) {
|
if (!(await hasPermission(payload, user, "structures:update"))) {
|
||||||
return {
|
return {
|
||||||
success: false,
|
success: false,
|
||||||
error: "Insufficient permissions. Admin or Developer role required for removal.",
|
error: "Insufficient permissions. Admin or Developer role required for removal.",
|
||||||
|
|
@ -953,7 +953,7 @@ export async function splitGridStack(
|
||||||
): Promise<GridActionResult> {
|
): Promise<GridActionResult> {
|
||||||
try {
|
try {
|
||||||
const { payload, user } = await authenticate();
|
const { payload, user } = await authenticate();
|
||||||
if (!hasRoles(["admin"], user) && !hasRoles(["user"], user)) {
|
if (!(await hasPermission(payload, user, "structures:update"))) {
|
||||||
return {
|
return {
|
||||||
success: false,
|
success: false,
|
||||||
error: "Insufficient permissions. User, Admin, or Developer role required.",
|
error: "Insufficient permissions. User, Admin, or Developer role required.",
|
||||||
|
|
@ -1061,7 +1061,7 @@ export async function splitGridStack(
|
||||||
export async function retrieveFromVoid(structureId: number): Promise<GridActionResult> {
|
export async function retrieveFromVoid(structureId: number): Promise<GridActionResult> {
|
||||||
try {
|
try {
|
||||||
const { payload, user } = await authenticate();
|
const { payload, user } = await authenticate();
|
||||||
if (!hasRoles(["admin"], user) && !hasRoles(["user"], user)) {
|
if (!(await hasPermission(payload, user, "structures:update"))) {
|
||||||
return { success: false, error: "Insufficient permissions." };
|
return { success: false, error: "Insufficient permissions." };
|
||||||
}
|
}
|
||||||
const structure = await getStructure(payload, structureId);
|
const structure = await getStructure(payload, structureId);
|
||||||
|
|
@ -1159,7 +1159,7 @@ export async function retrieveFromVoid(structureId: number): Promise<GridActionR
|
||||||
export async function autoArrangeGrid(structureId: number): Promise<GridActionResult> {
|
export async function autoArrangeGrid(structureId: number): Promise<GridActionResult> {
|
||||||
try {
|
try {
|
||||||
const { payload, user } = await authenticate();
|
const { payload, user } = await authenticate();
|
||||||
if (!hasRoles(["admin"], user) && !hasRoles(["user"], user)) {
|
if (!(await hasPermission(payload, user, "structures:update"))) {
|
||||||
return {
|
return {
|
||||||
success: false,
|
success: false,
|
||||||
error: "Insufficient permissions. User, Admin, or Developer role required.",
|
error: "Insufficient permissions. User, Admin, or Developer role required.",
|
||||||
|
|
|
||||||
|
|
@ -107,7 +107,7 @@ export default async function ProfilePage({ params }: ProfilePageProps) {
|
||||||
const totalKills = infantryKills + vehicleKills + armorKills + airKills;
|
const totalKills = infantryKills + vehicleKills + armorKills + airKills;
|
||||||
const kdRatio = deaths > 0 ? (totalKills / deaths).toFixed(2) : totalKills > 0 ? "∞" : "—";
|
const kdRatio = deaths > 0 ? (totalKills / deaths).toFixed(2) : totalKills > 0 ? "∞" : "—";
|
||||||
|
|
||||||
const enlistDate = new Date(profile.createdAt);
|
const enlistDate = new Date(profile.dossier.enlistmentDate);
|
||||||
const enlistDateStr = enlistDate.toLocaleDateString("en-US", {
|
const enlistDateStr = enlistDate.toLocaleDateString("en-US", {
|
||||||
year: "numeric",
|
year: "numeric",
|
||||||
month: "short",
|
month: "short",
|
||||||
|
|
|
||||||
|
|
@ -8,11 +8,19 @@ type PayloadType = Awaited<ReturnType<typeof import("payload").getPayload>>;
|
||||||
|
|
||||||
export async function getStaffUserIds(payload: PayloadType): Promise<number[]> {
|
export async function getStaffUserIds(payload: PayloadType): Promise<number[]> {
|
||||||
try {
|
try {
|
||||||
|
const adminRole = await payload.find({
|
||||||
|
collection: "roles",
|
||||||
|
where: { slug: { in: ["admin", "developer"] } },
|
||||||
|
limit: 2,
|
||||||
|
depth: 0,
|
||||||
|
overrideAccess: true,
|
||||||
|
});
|
||||||
|
const roleIds = adminRole.docs.map((d) => d.id);
|
||||||
|
if (roleIds.length === 0) return [];
|
||||||
|
|
||||||
const res = await payload.find({
|
const res = await payload.find({
|
||||||
collection: "users",
|
collection: "users",
|
||||||
where: {
|
where: { roleDocs: { in: roleIds } },
|
||||||
or: [{ roles: { contains: "admin" } }, { roles: { contains: "developer" } }],
|
|
||||||
},
|
|
||||||
limit: 50,
|
limit: 50,
|
||||||
depth: 0,
|
depth: 0,
|
||||||
select: { username: true },
|
select: { username: true },
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,26 @@
|
||||||
import type { Payload } from "payload";
|
import type { Payload } from "payload";
|
||||||
|
import { hasAnyPermission } from "@/utils/access-control/hasPermission";
|
||||||
|
|
||||||
|
const INTELLIGENCE_PERMISSIONS = [
|
||||||
|
"intelligence:manage",
|
||||||
|
"missions:read",
|
||||||
|
"missions:create",
|
||||||
|
"missions:update",
|
||||||
|
"missions:delete",
|
||||||
|
"missions:read-sensitive",
|
||||||
|
"campaigns:read",
|
||||||
|
"campaigns:create",
|
||||||
|
"campaigns:update",
|
||||||
|
"campaigns:delete",
|
||||||
|
"factions:read",
|
||||||
|
"factions:create",
|
||||||
|
"factions:update",
|
||||||
|
"factions:delete",
|
||||||
|
"technologies:read",
|
||||||
|
"technologies:create",
|
||||||
|
"technologies:update",
|
||||||
|
"technologies:delete",
|
||||||
|
] as const;
|
||||||
|
|
||||||
export async function hasIntelligenceQualification(
|
export async function hasIntelligenceQualification(
|
||||||
payload: Payload,
|
payload: Payload,
|
||||||
|
|
@ -6,10 +28,7 @@ export async function hasIntelligenceQualification(
|
||||||
): Promise<boolean> {
|
): Promise<boolean> {
|
||||||
if (!user) return false;
|
if (!user) return false;
|
||||||
|
|
||||||
const roles = user.roles as string[] | undefined;
|
if (await hasAnyPermission(payload, user, ...INTELLIGENCE_PERMISSIONS)) return true;
|
||||||
if (roles?.includes("developer") || roles?.includes("admin")) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
const profile = (await payload.find({
|
const profile = (await payload.find({
|
||||||
collection: "profiles",
|
collection: "profiles",
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,55 @@
|
||||||
import type { Payload } from "payload";
|
import type { Payload } from "payload";
|
||||||
|
import { hasAnyPermission } from "@/utils/access-control/hasPermission";
|
||||||
|
|
||||||
|
const LOGISTICS_PERMISSIONS = [
|
||||||
|
"logistics:manage",
|
||||||
|
"assets:read",
|
||||||
|
"assets:create",
|
||||||
|
"assets:update",
|
||||||
|
"assets:delete",
|
||||||
|
"resources:read",
|
||||||
|
"resources:create",
|
||||||
|
"resources:update",
|
||||||
|
"resources:delete",
|
||||||
|
"vehicles:read",
|
||||||
|
"vehicles:create",
|
||||||
|
"vehicles:update",
|
||||||
|
"vehicles:delete",
|
||||||
|
"structures:read",
|
||||||
|
"structures:create",
|
||||||
|
"structures:update",
|
||||||
|
"structures:delete",
|
||||||
|
"shipments:read",
|
||||||
|
"shipments:create",
|
||||||
|
"shipments:update",
|
||||||
|
"shipments:delete",
|
||||||
|
"bank-accounts:read",
|
||||||
|
"bank-accounts:create",
|
||||||
|
"bank-accounts:update",
|
||||||
|
"bank-accounts:delete",
|
||||||
|
"banking:manage",
|
||||||
|
"market-listings:read",
|
||||||
|
"market-listings:create",
|
||||||
|
"market-listings:update",
|
||||||
|
"market-listings:delete",
|
||||||
|
"game-structures:read",
|
||||||
|
"game-structures:create",
|
||||||
|
"game-structures:update",
|
||||||
|
"game-structures:delete",
|
||||||
|
"game-vehicles:read",
|
||||||
|
"game-vehicles:create",
|
||||||
|
"game-vehicles:update",
|
||||||
|
"game-vehicles:delete",
|
||||||
|
"game-npcs:read",
|
||||||
|
"game-npcs:create",
|
||||||
|
"game-npcs:update",
|
||||||
|
"game-npcs:delete",
|
||||||
|
] as const;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Checks if a user has the Logistics qualification.
|
* Checks if a user has the Logistics qualification.
|
||||||
* Queries the user's profile for qualifications matching "Logistics" (case-insensitive).
|
* Queries the user's profile for qualifications matching "Logistics" (case-insensitive).
|
||||||
* Developers and admins always pass.
|
* Users with ANY logistics-domain RBAC permission always pass.
|
||||||
*/
|
*/
|
||||||
export async function hasLogisticsQualification(
|
export async function hasLogisticsQualification(
|
||||||
payload: Payload,
|
payload: Payload,
|
||||||
|
|
@ -11,10 +57,7 @@ export async function hasLogisticsQualification(
|
||||||
): Promise<boolean> {
|
): Promise<boolean> {
|
||||||
if (!user) return false;
|
if (!user) return false;
|
||||||
|
|
||||||
const roles = user.roles as string[] | undefined;
|
if (await hasAnyPermission(payload, user, ...LOGISTICS_PERMISSIONS)) return true;
|
||||||
if (roles?.includes("developer") || roles?.includes("admin")) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
const profileRes = await payload.find({
|
const profileRes = await payload.find({
|
||||||
collection: "profiles",
|
collection: "profiles",
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue