Skip to content

Commit

Permalink
Merge pull request #619 from CodeForAfrica/fix/codeforafrica_cms
Browse files Browse the repository at this point in the history
Review of @/codeforafrica CMS
  • Loading branch information
kilemensi authored Oct 4, 2023
2 parents 2ed8e77 + 4d738a8 commit 12ef998
Show file tree
Hide file tree
Showing 56 changed files with 1,039 additions and 574 deletions.
7 changes: 6 additions & 1 deletion apps/codeforafrica/.eslintignore
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,18 @@ node_modules
.pnp.js
.pnpm-debug.log

# typescript
dist/

# testing
coverage

# next.js
.next/
out/
build

# payload
build/

# misc
.DS_Store
Expand Down
5 changes: 3 additions & 2 deletions apps/codeforafrica/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -23,15 +23,15 @@
"scripts": {
"cms": "netlify-cms-proxy-server",
"build-server": "tsc --project tsconfig.server.json",
"build-next": "NEXT_BUILD=true pnpm pnpm build-server && NEXT_BUILD=true PAYLOAD_CONFIG_PATH=dist/payload.config.js node dist/server.js",
"build-next": "NEXT_BUILD=true pnpm pnpm build-server && NEXT_BUILD=true PAYLOAD_CONFIG_PATH=${PAYLOAD_CONFIG_PATH:-dist/payload.config.js} node dist/server.js",
"build-payload": "payload build",
"start": "PAYLOAD_CONFIG_PATH=${PAYLOAD_CONFIG_PATH:-dist/payload.config.js} NODE_ENV=${NODE_ENV:-production} node dist/server.js",
"dev": "NODE_OPTIONS='--inspect' ts-node --project tsconfig.server.json server.ts",
"lint-check": "TIMING=1 eslint './'",
"lint": "TIMING=1 eslint --fix './'",
"jest": "jest",
"playwright": "npx playwright test",
"build": "next build",
"start": "next start",
"clean": "rm -rf .next .turbo node_modules"
},
"dependencies": {
Expand Down Expand Up @@ -62,6 +62,7 @@
"nanoid": "^4.0.2",
"next": "~13.4.11",
"next-seo": "^5.15.0",
"nodemailer-sendgrid": "^1.0.3",
"payload": "^1.7.5",
"prop-types": "^15.8.1",
"qs": "^6.11.2",
Expand Down
35 changes: 27 additions & 8 deletions apps/codeforafrica/payload.config.ts
Original file line number Diff line number Diff line change
@@ -1,25 +1,28 @@
import path from "path";

import { buildConfig } from "payload/config";
import { CollectionConfig, GlobalConfig } from "payload/types";
import { cloudStorage } from "@payloadcms/plugin-cloud-storage";
import dotenv from "dotenv";
import seo from "@payloadcms/plugin-seo";
import nestedDocs from "@payloadcms/plugin-nested-docs";
import { s3Adapter } from "@payloadcms/plugin-cloud-storage/s3";

import Authors from "./src/payload/collections/Authors";
import Donors from "./src/payload/collections/Donors";
import GuidingPrinciples from "./src/payload/collections/GuidingPrinciples";
import Impact from "./src/payload/collections/Impact";
import Media from "./src/payload/collections/Media";
import Members from "./src/payload/collections/Members";
import Pages from "./src/payload/collections/Pages";
import Partners from "./src/payload/collections/Partners";
import Posts from "./src/payload/collections/Posts";
import Projects from "./src/payload/collections/Projects";
import Settings from "./src/payload/globals/Settings";
import Tags from "./src/payload/collections/Tags";
import Projects from "./src/payload/collections/Projects";
import Donors from "./src/payload/collections/Donors";
import Teams from "./src/payload/collections/Teams";
import { CollectionConfig, GlobalConfig } from "payload/types";
import dotenv from "dotenv";
import seo from "@payloadcms/plugin-seo";
import nestedDocs from "@payloadcms/plugin-nested-docs";
import { cloudStorage } from "@payloadcms/plugin-cloud-storage";
import { s3Adapter } from "@payloadcms/plugin-cloud-storage/s3";
import Users from "./src/payload/collections/Users";
import { defaultLocale, locales } from "./src/payload/utils/locales";

dotenv.config();
dotenv.config({ path: "./.env.local" });
Expand Down Expand Up @@ -62,10 +65,21 @@ export default buildConfig({
Posts,
Tags,
Teams,
Users,
] as CollectionConfig[],
globals: [Settings] as GlobalConfig[],
...(locales?.length
? {
localization: {
locales,
defaultLocale,
fallback: true,
},
}
: undefined),
admin: {
css: path.resolve(__dirname, "./src/payload/admin/scss/custom.scss"),
user: Users.slug,
webpack: (config) => ({
...config,
resolve: {
Expand All @@ -81,6 +95,10 @@ export default buildConfig({
},
cors,
csrf,
i18n: {
fallbackLng: "en", // default
debug: false, // default
},
plugins: [
cloudStorage({
collections: {
Expand All @@ -105,4 +123,5 @@ export default buildConfig({
docs.reduce((url, doc) => `${url}/${doc.slug}`, ""),
}),
] as any[],
telemetry: process?.env?.NODE_ENV !== "production",
});
25 changes: 19 additions & 6 deletions apps/codeforafrica/server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,16 +2,18 @@ import path from "path";
import { spawn } from "child_process";
import express from "express";
import next from "next";
import nodemailerSendgrid from "nodemailer-sendgrid";
import payload from "payload";
import { Payload } from "payload/dist/payload";
import { loadEnvConfig } from "@next/env";

const projectDir = process.cwd();
loadEnvConfig(projectDir);

const hostname = process.env.NEXT_HOSTNAME || "localhost";
const PORT = parseInt(process.env.PORT || "3000", 10);
const dev = process.env.NODE_ENV !== "production";
const hostname = process.env.NEXT_HOSTNAME || "localhost";
const port = parseInt(process.env.PORT || "3000", 10);
const sendGridAPIKey = process.env.SENDGRID_API_KEY;

if (!process.env.NEXT_MANUAL_SIG_HANDLE) {
process.on("SIGTERM", () => process.exit(0));
Expand All @@ -24,6 +26,18 @@ const start = async (): Promise<void> => {
let localPayload: Payload;
try {
localPayload = await payload.init({
...(sendGridAPIKey
? {
email: {
transportOptions: nodemailerSendgrid({
apiKey: sendGridAPIKey,
}),
fromName: process.env.SENDGRID_FROM_NAME || "Code for Africa CMS",
fromAddress:
process.env.SENDGRID_FROM_EMAIL || "[email protected]",
},
}
: undefined),
secret: process.env.PAYLOAD_SECRET,
mongoURL: process.env.MONGODB_URL,
express: app,
Expand All @@ -37,7 +51,7 @@ const start = async (): Promise<void> => {
}

if (process.env.NEXT_BUILD) {
app.listen(PORT, async () => {
app.listen(port, async () => {
localPayload.logger.info("NextJS is now building...");
const nextBuild = spawn(
"pnpm",
Expand All @@ -50,7 +64,6 @@ const start = async (): Promise<void> => {
nextBuild.on("close", (code) => {
process.exit(code);
});

nextBuild.on("error", (err) => {
localPayload.logger.error(err);
process.exit(1);
Expand All @@ -60,7 +73,7 @@ const start = async (): Promise<void> => {
return;
}

const nextApp = next({ dev, hostname, port: PORT });
const nextApp = next({ dev, hostname, port });
const nextHandler = nextApp.getRequestHandler();
nextApp.prepare().then(() => {
localPayload.logger.info("NextJS started");
Expand All @@ -69,7 +82,7 @@ const start = async (): Promise<void> => {
app.post("*", (req: any, res: any) => nextHandler(req, res));
app.put("*", (req: any, res: any) => nextHandler(req, res));

app.listen(PORT, async () => {
app.listen(port, async () => {
localPayload.logger.info(
`Next.js App URL: ${process.env.NEXT_PUBLIC_APP_URL}`,
);
Expand Down
7 changes: 4 additions & 3 deletions apps/codeforafrica/src/lib/data/common/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,15 +5,16 @@ import { imageFromMedia } from "@/codeforafrica/lib/data/utils";

function getNavBar(settings) {
const {
connect: { links: socialLinks = [] },
connect: { links = [] },
primaryLogo: media,
primaryNavigation,
primaryNavigation: { menus = null, connect },
title,
} = settings;
const socialLinks = links.filter((link) => link.platform === connect);

return {
logo: imageFromMedia({ alt: title, ...media }),
menus: primaryNavigation?.menus || null,
menus,
socialLinks,
};
}
Expand Down
5 changes: 5 additions & 0 deletions apps/codeforafrica/src/pages/api/v1/disable-draft.page.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
// By default, the Draft Mode session ends when the browser is closed.
// This method clears it manually / on demand.
export default function handler(req, res) {
res.setDraftMode({ enable: false });
}
20 changes: 20 additions & 0 deletions apps/codeforafrica/src/pages/api/v1/draft/index.page.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
export default async function handler(req, res) {
// Since payload and Next.js are running on the same server process, lets
// make sure the user requesting to preview, is logged into Payload
// See "Tip" on: https://payloadcms.com/docs/authentication/overview#token-based-auth
if (!req.user) {
return res.status(401).json({ message: "UNAUTHORIZED_USER" });
}
const { slug } = req.query;
res.setDraftMode({ enable: true });

// Guard against open redirect vulnerabilities
// Since slug will be a path, redirect to pathname instead of original slug
// just in case
const appUrl = new URL(process.env.NEXT_PUBLIC_APP_URL);
const requestedUrl = new URL(slug, appUrl);
if (requestedUrl.origin !== appUrl.origin) {
return res.status(401).json({ message: "UNAUTHORIZED_REDIRECT" });
}
return res.redirect(requestedUrl.pathname);
}
11 changes: 11 additions & 0 deletions apps/codeforafrica/src/payload/access/isAdmin.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import { ROLE_ADMIN } from "./roles";

export const isAdmin = ({ req: { user } }) => {
// Return true or false based on if the user has an admin role
return Boolean(user?.roles?.includes(ROLE_ADMIN));
};

export const isAdminFieldLevel = ({ req: { user } }) => {
// Return true or false based on if the user has an admin role
return Boolean(user?.roles?.includes(ROLE_ADMIN));
};
14 changes: 14 additions & 0 deletions apps/codeforafrica/src/payload/access/isAdminOrPublished.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import { ROLE_ADMIN } from "./roles";

export const isAdminOrPublished = ({ req: { user } }) => {
if (user?.roles?.includes(ROLE_ADMIN)) {
return true;
}
return {
_status: {
equals: "published",
},
};
};

export default undefined;
24 changes: 24 additions & 0 deletions apps/codeforafrica/src/payload/access/isAdminOrSelf.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import { ROLE_ADMIN } from "./roles";

export const isAdminOrSelf = ({ req: { user } }) => {
// Need to be logged in
if (user) {
// If user has role of 'admin'
if (user.roles?.includes(ROLE_ADMIN)) {
return true;
}
// If any other type of user, only provide access to themselves
return {
id: {
equals: user.id,
},
};
}

// Reject everyone else
return false;
};

export const isAdminOrSelfFieldLevel = ({ id, req: { user } }) => {
return user?.roles?.includes(ROLE_ADMIN) || id === user?.id;
};
7 changes: 7 additions & 0 deletions apps/codeforafrica/src/payload/access/roles.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
export const ROLE_ADMIN = "admin";
export const ROLE_EDITOR = "editor";
export const ROLE_DEFAULT = ROLE_EDITOR;
export const ROLE_OPTIONS = [
{ label: "Admin", value: ROLE_ADMIN },
{ label: "Editor", value: ROLE_EDITOR },
];
1 change: 0 additions & 1 deletion apps/codeforafrica/src/payload/blocks/ContactForm.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@ const ContactForm = {
{
name: "embedCode",
type: "code",
label: "Embed Code",
required: true,
admin: {
language: "html",
Expand Down
6 changes: 2 additions & 4 deletions apps/codeforafrica/src/payload/blocks/CustomPageHeader.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,15 +7,13 @@ const CustomPageHeader = {
fields: [
{
name: "title",
label: "Title",
required: true,
type: "text",
required: true,
},
{
name: "subtitle",
label: "Subtitle",
required: true,
type: "text",
required: true,
},
image({
overrides: {
Expand Down
4 changes: 1 addition & 3 deletions apps/codeforafrica/src/payload/blocks/Error.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,17 +3,15 @@ import richText from "../fields/richText";
const Error = {
slug: "error",
imageURL: "/images/cms/blocks/error.png",
imageAltText: "Used in Error page.",
imageAltText: "Used to describe errors in error pages.",
fields: [
{
name: "title",
label: "Title",
type: "text",
required: true,
},
richText({
name: "subtitle",
label: "Subtitle",
admin: {
elements: ["link"],
},
Expand Down
2 changes: 0 additions & 2 deletions apps/codeforafrica/src/payload/blocks/ExternalEmbed.js
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,6 @@ const ExternalEmbed = {
},
{
name: "caption",
label: "Caption",
type: "text",
localized: true,
admin: {
Expand All @@ -46,7 +45,6 @@ const ExternalEmbed = {
},
{
name: "code",
label: "Code",
type: "code",
required: true,
admin: {
Expand Down
6 changes: 2 additions & 4 deletions apps/codeforafrica/src/payload/blocks/GetInTouch.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,15 +7,13 @@ const GetInTouch = {
fields: [
{
name: "title",
label: "Title",
required: true,
type: "text",
required: true,
},
{
name: "subtitle",
label: "Subtitle",
required: true,
type: "text",
required: true,
},
linkGroup({ overrides: { name: "action", label: "Action" } }),
],
Expand Down
4 changes: 0 additions & 4 deletions apps/codeforafrica/src/payload/blocks/GuidingPrinciples.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,7 @@ const GuidingPrinciples = {
fields: [
{
name: "title",
label: {
en: "Title",
},
type: "text",
localized: true,
required: true,
},
{
Expand Down
Loading

0 comments on commit 12ef998

Please sign in to comment.