Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions apps/trustlab/src/app/preview/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import { redirect } from "next/navigation";
import type { NextRequest } from "next/server";

import configPromise from "@payload-config";
import { canManageContent } from "@/trustlab/payload/access/abilities";
import { isAuthor } from "@/trustlab/payload/access/abilities";

export async function GET(req: NextRequest): Promise<Response> {
const payload = await getPayload({ config: configPromise });
Expand Down Expand Up @@ -37,7 +37,7 @@ export async function GET(req: NextRequest): Promise<Response> {
"Error verifying token for live preview",
);
}
if (!(authResult && canManageContent(authResult?.user))) {
if (!(authResult && isAuthor(authResult?.user))) {
return new Response("You are not allowed to preview this page", {
status: 403,
});
Expand Down
2 changes: 2 additions & 0 deletions apps/trustlab/src/lib/data/rest/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,8 @@ export const api = {
findPage,
};

// pages/_error getInitialProps can run on the server or in the browser.
// Keep this path on REST so it does not import Payload's server-only local API.
export async function getErrorPageProps(context) {
const slug = context?.params?.slugs?.[0] || "404";
const props = await getProps(api, slug);
Expand Down
51 changes: 36 additions & 15 deletions apps/trustlab/src/payload/access/abilities.js
Original file line number Diff line number Diff line change
@@ -1,39 +1,57 @@
import { checkRole } from "./checkRole";
import { ROLE_ADMIN, ROLE_AUTHOR, ROLE_EDITOR } from "./roles";
import { hasValidRole, ROLE_ADMIN, ROLE_AUTHOR, ROLE_EDITOR } from "./roles";

export const canManageContent = (user) => {
if (!user) {
export const isLoggedIn = (user) => hasValidRole(user);

export const hasLoggedInAccess = ({ req } = {}) => isLoggedIn(req?.user);

// Admins and editors can manage any content
export const isEditor = (user) => checkRole([ROLE_ADMIN, ROLE_EDITOR], user);

export const hasEditorAccess = ({ req } = {}) => isEditor(req?.user);

export const isAuthor = (user) => {
// Any valid CMS user has at least author-level capability.
return isLoggedIn(user);
};
Comment thread
koechkevin marked this conversation as resolved.

export const canAuthor = (user) => {
if (!isLoggedIn(user)) {
return false;
}
// Admin and editors can manage any content
if (checkRole([ROLE_ADMIN, ROLE_EDITOR], user)) {
if (isEditor(user)) {
return true;
}

// Everyone else can only manage their own content
if (!user.id) {
return false;
}
// Authors can manage own content only
return {
createdBy: {
equals: user.id,
},
};
};

export const canManagePages = (user) =>
checkRole([ROLE_ADMIN, ROLE_EDITOR], user);
export const hasAuthorAccess = ({ req } = {}) => canAuthor(req?.user);

export const canManageSiteSettings = ({ req: { user } }) =>
checkRole([ROLE_ADMIN], user);
export const isAdmin = (user) => checkRole([ROLE_ADMIN], user);

export const hasAdminAccess = ({ req } = {}) => isAdmin(req?.user);

// TODO(@kelvinkipruto): what happens on delete? cascade or not?
export const canManageUsers = (user) => {
if (!user) {
export const canManageUser = (user) => {
if (!isLoggedIn(user)) {
return false;
}
// Admins can manage all users
if (user.role === ROLE_ADMIN) {
return true;
}
// All other users can manage their own accounts
if (!user.id) {
return false;
}
const orQuery = [
{
id: {
Expand All @@ -50,5 +68,8 @@ export const canManageUsers = (user) => {
};
};

export const canCreateAccounts = (user) =>
checkRole([ROLE_ADMIN, ROLE_EDITOR], user);
export const hasManageUserAccess = ({ req } = {}) => canManageUser(req?.user);

export const hasCreateUserAccess = (args) => hasEditorAccess(args);

export default undefined;
2 changes: 1 addition & 1 deletion apps/trustlab/src/payload/access/index.js
Original file line number Diff line number Diff line change
@@ -1,2 +1,2 @@
export * from "./abilities";
export * from "./anyone";
export * from "./loggedIn";
194 changes: 194 additions & 0 deletions apps/trustlab/src/payload/access/index.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,194 @@
import {
canAuthor,
hasAdminAccess,
hasAuthorAccess,
hasCreateUserAccess,
hasManageUserAccess,
hasEditorAccess,
hasLoggedInAccess,
isAuthor,
} from "./abilities";
import { anyone } from "./anyone";
import { ROLE_ADMIN, ROLE_AUTHOR, ROLE_EDITOR } from "./roles";

describe("payload.access", () => {
describe("abilities.hasAdminAccess", () => {
it("allows requests from administrators", () => {
expect(hasAdminAccess({ req: { user: { role: ROLE_ADMIN } } })).toBe(
true,
);
});

it("denies requests from authors, editors, and anonymous users", () => {
expect(hasAdminAccess({ req: { user: { role: ROLE_AUTHOR } } })).toBe(
false,
);
expect(hasAdminAccess({ req: { user: { role: ROLE_EDITOR } } })).toBe(
false,
);
expect(hasAdminAccess({ req: {} })).toBe(false);
expect(hasAdminAccess(undefined)).toBe(false);
});
});

describe("abilities.isAuthor", () => {
it("allows users with administrators, authors and editors roles", () => {
expect(isAuthor({ role: ROLE_ADMIN })).toBe(true);
expect(isAuthor({ role: ROLE_AUTHOR })).toBe(true);
expect(isAuthor({ role: ROLE_EDITOR })).toBe(true);
});

it("denies users without a valid role", () => {
expect(isAuthor({ role: "unknown" })).toBe(false);
expect(isAuthor(1)).toBe(false);
expect(isAuthor(undefined)).toBe(false);
});
});

describe("abilities.canAuthor", () => {
it("allows administrators and editors to author any content", () => {
expect(canAuthor({ role: ROLE_ADMIN })).toBe(true);
expect(canAuthor({ role: ROLE_EDITOR })).toBe(true);
});

it("limits authors to their own content", () => {
expect(canAuthor({ id: 1, role: ROLE_AUTHOR })).toEqual({
createdBy: { equals: 1 },
});
});

it("denies authors without a user id for ownership checks", () => {
expect(canAuthor({ role: ROLE_AUTHOR })).toBe(false);
});

it("denies users without a valid role", () => {
expect(canAuthor({ role: "unknown" })).toBe(false);
expect(canAuthor(1)).toBe(false);
expect(canAuthor(undefined)).toBe(false);
});
});

describe("abilities.hasAuthorAccess", () => {
it("allows requests from administrators, authors and editors", () => {
expect(hasAuthorAccess({ req: { user: { role: ROLE_ADMIN } } })).toBe(
true,
);
expect(
hasAuthorAccess({ req: { user: { id: 1, role: ROLE_AUTHOR } } }),
).toEqual({ createdBy: { equals: 1 } });
expect(hasAuthorAccess({ req: { user: { role: ROLE_EDITOR } } })).toBe(
true,
);
});

it("denies requests from anonymous users", () => {
expect(hasAuthorAccess({ req: {} })).toBe(false);
expect(hasAuthorAccess(undefined)).toBe(false);
});
});

describe("abilities.hasCreateUserAccess", () => {
it("allows requests from administrators and editors", () => {
expect(hasCreateUserAccess({ req: { user: { role: ROLE_ADMIN } } })).toBe(
true,
);
expect(
hasCreateUserAccess({ req: { user: { role: ROLE_EDITOR } } }),
).toBe(true);
});

it("denies requests from authors and anonymous users", () => {
expect(
hasCreateUserAccess({ req: { user: { role: ROLE_AUTHOR } } }),
).toBe(false);
expect(hasCreateUserAccess({ req: {} })).toBe(false);
expect(hasCreateUserAccess(undefined)).toBe(false);
});
});

describe("abilities.hasManageUserAccess", () => {
it("allows requests from administrators, authors and editors", () => {
expect(hasManageUserAccess({ req: { user: { role: ROLE_ADMIN } } })).toBe(
true,
);
expect(
hasManageUserAccess({ req: { user: { id: 1, role: ROLE_AUTHOR } } }),
).toEqual({ or: [{ id: { equals: 1 } }] });
expect(
hasManageUserAccess({ req: { user: { id: 1, role: ROLE_EDITOR } } }),
).toEqual({
or: [{ id: { equals: 1 } }, { role: { equals: ROLE_AUTHOR } }],
});
});

it("denies requests from invalid-role and anonymous users", () => {
expect(
hasManageUserAccess({ req: { user: { id: 1, role: "unknown" } } }),
).toBe(false);
expect(hasManageUserAccess({ req: {} })).toBe(false);
expect(hasManageUserAccess(undefined)).toBe(false);
});

it("denies requests from non-admin users without a user id for ownership checks", () => {
expect(
hasManageUserAccess({ req: { user: { role: ROLE_AUTHOR } } }),
).toBe(false);
expect(
hasManageUserAccess({ req: { user: { role: ROLE_EDITOR } } }),
).toBe(false);
});
});

describe("abilities.hasEditorAccess", () => {
it("allows requests from administrators and editors", () => {
expect(hasEditorAccess({ req: { user: { role: ROLE_ADMIN } } })).toBe(
true,
);
expect(hasEditorAccess({ req: { user: { role: ROLE_EDITOR } } })).toBe(
true,
);
});

it("denies requests from authors and anonymous users", () => {
expect(hasEditorAccess({ req: { user: { role: ROLE_AUTHOR } } })).toBe(
false,
);
expect(hasEditorAccess({ req: {} })).toBe(false);
expect(hasEditorAccess(undefined)).toBe(false);
});
});

describe("abilities.hasLoggedInAccess", () => {
it("allows requests with users that have valid roles", () => {
expect(hasLoggedInAccess({ req: { user: { role: ROLE_ADMIN } } })).toBe(
true,
);
expect(hasLoggedInAccess({ req: { user: { role: ROLE_AUTHOR } } })).toBe(
true,
);
expect(hasLoggedInAccess({ req: { user: { role: ROLE_EDITOR } } })).toBe(
true,
);
});

it("denies requests without a user that has a valid role", () => {
expect(hasLoggedInAccess({ req: { user: { role: "unknown" } } })).toBe(
false,
);
expect(hasLoggedInAccess({ req: { user: 1 } })).toBe(false);
expect(hasLoggedInAccess({ req: {} })).toBe(false);
expect(hasLoggedInAccess(undefined)).toBe(false);
});
});

describe("anyone.anyone", () => {
it("allows requests from administrators, authors, editors and anonymous users", () => {
expect(anyone({ req: { user: { role: ROLE_ADMIN } } })).toBe(true);
expect(anyone({ req: { user: { role: ROLE_AUTHOR } } })).toBe(true);
expect(anyone({ req: { user: { role: ROLE_EDITOR } } })).toBe(true);
expect(anyone({ req: { user: 1 } })).toBe(true);
expect(anyone({ req: {} })).toBe(true);
expect(anyone(undefined)).toBe(true);
});
});
});
3 changes: 0 additions & 3 deletions apps/trustlab/src/payload/access/loggedIn.js

This file was deleted.

5 changes: 5 additions & 0 deletions apps/trustlab/src/payload/access/roles.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,3 +6,8 @@ export const ROLE_OPTIONS = [
{ label: "Editor", value: ROLE_EDITOR },
{ label: "Author", value: ROLE_AUTHOR },
];

export const hasValidRole = (user) =>
ROLE_OPTIONS.some(({ value }) => user?.role === value);

export default undefined;
8 changes: 6 additions & 2 deletions apps/trustlab/src/payload/collections/Donors.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ import {
linkGroup,
} from "@commons-ui/payload";

import { anyone, hasEditorAccess } from "@/trustlab/payload/access";

const Donors = {
slug: "donors",
labels: {
Expand All @@ -23,9 +25,11 @@ const Donors = {
useAsTitle: "name",
},
access: {
read: () => true,
read: anyone,
create: hasEditorAccess,
update: hasEditorAccess,
delete: hasEditorAccess,
},

fields: [
{
name: "name",
Expand Down
9 changes: 4 additions & 5 deletions apps/trustlab/src/payload/collections/Media.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,7 @@ import path from "path";

import { createdBy } from "@commons-ui/payload";

import { canManageContent } from "@/trustlab/payload/access/abilities";
import { anyone } from "@/trustlab/payload/access/anyone";
import { anyone, hasAuthorAccess } from "@/trustlab/payload/access";
import { hideAPIURL, slugify } from "@/trustlab/payload/utils";

function slugifyFilename(filename) {
Expand All @@ -19,9 +18,9 @@ const Media = {
slug: "media",
access: {
read: anyone,
create: ({ req: { user } }) => canManageContent(user),
update: ({ req: { user } }) => canManageContent(user),
delete: ({ req: { user } }) => canManageContent(user),
create: hasAuthorAccess,
update: hasAuthorAccess,
delete: hasAuthorAccess,
},
admin: {
group: "Publication",
Expand Down
6 changes: 5 additions & 1 deletion apps/trustlab/src/payload/collections/Opportunities.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { image, appendPathnameToCollection, slug } from "@commons-ui/payload";

import { anyone, hasEditorAccess } from "@/trustlab/payload/access";
import blocks from "@/trustlab/payload/blocks";

const pageByType = {
Expand All @@ -25,7 +26,10 @@ const Opportunities = {
defaultColumns: ["title", "type"],
},
access: {
read: () => true,
read: anyone,
create: hasEditorAccess,
update: hasEditorAccess,
delete: hasEditorAccess,
},
fields: [
{
Expand Down
Loading