Skip to content

Commit

Permalink
Merge pull request #932 from CodeForAfrica/ft/climatemappedafrica-pay…
Browse files Browse the repository at this point in the history
…load-setup

ClimateMapped Payload Setup
  • Loading branch information
kelvinkipruto authored Oct 4, 2024
2 parents fc68f58 + d602808 commit bd079da
Show file tree
Hide file tree
Showing 22 changed files with 939 additions and 47 deletions.
20 changes: 17 additions & 3 deletions apps/climatemappedafrica/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -21,13 +21,15 @@
"url": "https://github.com/codeforafrica/ui/issues"
},
"scripts": {
"dev": "NODE_OPTIONS='--inspect' next dev",
"build-server": "tsc --project tsconfig.server.json",
"build-next": "NEXT_BUILD=true 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 --passWithNoTests",
"playwright": "npx playwright test",
"build": "next build",
"start": "next start",
"clean": "rm -rf .next .turbo node_modules"
},
"dependencies": {
Expand All @@ -42,17 +44,28 @@
"@mui/material": "catalog:mui-styles",
"@mui/styles": "catalog:mui-styles",
"@mui/utils": "catalog:mui-styles",
"@next/env": "catalog:",
"@payloadcms/bundler-webpack": "catalog:",
"@payloadcms/db-mongodb": "catalog:",
"@payloadcms/plugin-cloud-storage": "catalog:",
"@payloadcms/plugin-nested-docs": "catalog:",
"@payloadcms/plugin-sentry": "catalog:",
"@payloadcms/plugin-seo": "catalog:",
"@payloadcms/richtext-slate": "catalog:",
"@reactour/tour": "catalog:",
"aws-sdk": "catalog:",
"clsx": "catalog:",
"d3-format": "catalog:",
"deepmerge": "catalog:",
"express": "catalog:",
"leaflet": "catalog:",
"lodash": "catalog:",
"next": "catalog:",
"next-images": "catalog:",
"next-seo": "catalog:",
"nodemailer-sendgrid": "catalog:",
"papaparse": "catalog:",
"payload": "catalog:",
"plaiceholder": "catalog:",
"prop-types": "catalog:",
"react": "catalog:",
Expand Down Expand Up @@ -112,6 +125,7 @@
"prettier": "catalog:",
"react-test-renderer": "catalog:",
"svg-url-loader": "catalog:",
"ts-node": "catalog:",
"typescript": "catalog:"
},
"engines": {
Expand Down
120 changes: 120 additions & 0 deletions apps/climatemappedafrica/payload.config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
import { buildConfig } from "payload/config";
import { loadEnvConfig } from "@next/env";
import { cloudStorage } from "@payloadcms/plugin-cloud-storage";
import { s3Adapter } from "@payloadcms/plugin-cloud-storage/s3";
import { slateEditor } from "@payloadcms/richtext-slate";
import { mongooseAdapter } from "@payloadcms/db-mongodb";
import { webpackBundler } from "@payloadcms/bundler-webpack";
import { CollectionConfig, GlobalConfig } from "payload/types";
import nestedDocs from "@payloadcms/plugin-nested-docs";
import seo from "@payloadcms/plugin-seo";
import { sentry } from "@payloadcms/plugin-sentry";
import { defaultLocale, locales } from "./src/payload/utils/locales";

import Media from "./src/payload/collections/Media";
import Pages from "./src/payload/collections/Pages";
import Users from "./src/payload/collections/Users";

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

const appURL = process.env.PAYLOAD_PUBLIC_APP_URL;

const cors =
process?.env?.PAYLOAD_CORS?.split(",")
?.map((d) => d.trim())
?.filter(Boolean) ?? [];

const csrf =
process?.env?.PAYLOAD_CSRF?.split(",")
?.map((d) => d.trim())
?.filter(Boolean) ?? [];

const adapter = s3Adapter({
config: {
region: process?.env?.S3_REGION,
credentials: {
accessKeyId: process?.env?.S3_ACCESS_KEY_ID,
secretAccessKey: process?.env?.S3_SECRET_ACCESS_KEY,
},
},
bucket: process?.env?.S3_BUCKET,
} as any);

export default buildConfig({
serverURL: appURL,
editor: slateEditor({}),
routes: {
admin: "/admin",
},
db: mongooseAdapter({
url: process.env.MONGO_URL,
migrationDir: process.env.MIGRATIONS_DIR,
}),
collections: [Media, Pages, Users] as CollectionConfig[],
globals: [] as GlobalConfig[],
...(locales?.length
? {
localization: {
locales,
defaultLocale,
fallback: true,
},
}
: undefined),
admin: {
webpack: (config) => ({
...config,
resolve: {
...config.resolve,
fallback: {
...config?.resolve?.fallback,
fs: false,
os: false,
"process/browser": false,
},
},
}),
bundler: webpackBundler(),
},
cors,
csrf,
i18n: {
fallbackLng: "en", // default
debug: false, // default
resources: {
en: {
"codeforafrica.validation": {
uniquePlatforms: "Please select a unique platform",
},
},
},
},
plugins: [
cloudStorage({
collections: {
media: {
adapter,
prefix: "media",
},
},
}),
sentry({
dsn: process?.env?.NEXT_PUBLIC_SENTRY_DSN,
}),
seo({
collections: ["pages"],
globals: ["settings"],
uploadsCollection: "media",
generateTitle: ({ doc }: any) => doc?.title?.value as string,
generateURL: ({ doc }: any) =>
doc?.slug?.value ? `${appURL}/${doc.slug.value}` : undefined,
}),
nestedDocs({
collections: ["pages"],
generateLabel: (_, doc) => doc.title as string,
generateURL: (docs) =>
docs.reduce((url, doc) => `${url}/${doc.slug}`, ""),
}),
],
});
93 changes: 93 additions & 0 deletions apps/climatemappedafrica/server.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
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 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));
process.on("SIGINT", () => process.exit(0));
}

const app = express();

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 || "ClimateMapped Africa CMS",
fromAddress:
process.env.SENDGRID_FROM_EMAIL || "[email protected]",
},
}
: undefined),
secret: process.env.PAYLOAD_SECRET,
express: app,
onInit: (initPayload) => {
initPayload.logger.info(`Payload Admin URL: ${payload.getAdminURL()}`);
},
});
} catch (e: any) {
console.error(e);
process.exit();
}

if (process.env.NEXT_BUILD) {
app.listen(port, async () => {
localPayload.logger.info("NextJS is now building...");
const nextBuild = spawn(
"pnpm",
["next", "build", path.resolve(projectDir)],
{
shell: true,
stdio: "inherit",
},
);
nextBuild.on("close", (code) => {
process.exit(code);
});
nextBuild.on("error", (err) => {
localPayload.logger.error(err);
process.exit(1);
});
});

return;
}

const nextApp = next({ dev, hostname, port });
const nextHandler = nextApp.getRequestHandler();
nextApp.prepare().then(() => {
localPayload.logger.info("NextJS started");

app.get("*", (req: any, res: any) => nextHandler(req, res));
app.post("*", (req: any, res: any) => nextHandler(req, res));
app.put("*", (req: any, res: any) => nextHandler(req, res));

app.listen(port, async () => {
localPayload.logger.info(
`Next.js App URL: ${process.env.NEXT_PUBLIC_APP_URL}`,
);
});
});
};

start();
11 changes: 11 additions & 0 deletions apps/climatemappedafrica/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/climatemappedafrica/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/climatemappedafrica/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/climatemappedafrica/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 },
];
29 changes: 29 additions & 0 deletions apps/climatemappedafrica/src/payload/collections/Media.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
const Media = {
slug: "media",
admin: {
defaultColumns: ["alt", "updatedAt"],
enableRichTextLink: false,
group: "Publication",
useAsTitle: "alt",
},
access: {
read: () => true, // Everyone can read Media
},
upload: {
staticURL: "/media",
staticDir: "media",
},
fields: [
{
name: "alt",
label: "Alt Text",
type: "text",
required: true,
},
],
hooks: {
afterRead: [({ doc }) => ({ ...doc, src: doc.url })],
},
};

export default Media;
Loading

0 comments on commit bd079da

Please sign in to comment.