Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
44 commits
Select commit Hold shift + click to select a range
a8bd38d
Add initial multi-tenancy support
Custard7 May 1, 2024
a9c807b
Add support for whitelabelling tenant environments
Custard7 May 2, 2024
b79051c
Fix side nav styles and favicon
Custard7 May 2, 2024
241d791
Reorder side nav
Custard7 May 2, 2024
04dd035
Reorganize side menu
Custard7 May 2, 2024
a40310f
Add tenant and user roles, and enforcement, client and server side
Custard7 May 3, 2024
ed29779
Update readme with role description
Custard7 May 3, 2024
5e5a78f
Carry over permissions for membership batches
Custard7 May 3, 2024
464f0fd
Merge payload 2.0
Custard7 May 6, 2024
0b9e724
Fix public media query
Custard7 May 6, 2024
2b132fc
Fix revocation permissions
Custard7 May 6, 2024
f58a517
Add Trust Registry Feat
Custard7 May 6, 2024
928855a
Fix saving user bug
Custard7 May 7, 2024
4615279
Add user roles manager
Custard7 May 7, 2024
ece796e
Rename tenants to network labels
Custard7 May 7, 2024
18d0b38
Rename to network language
Custard7 May 7, 2024
535b4e0
Add health-check
Custard7 May 8, 2024
e388c8b
Add issuer coordinator to handle internal/external signing
Custard7 May 9, 2024
f6702f3
Add buildspec.yml
Custard7 May 9, 2024
9556b4a
Remove aws ecr
Custard7 May 9, 2024
3d2f56f
CD in yml
Custard7 May 9, 2024
92e2474
Fix build errors and async redis
Custard7 May 9, 2024
e36ccd8
Add aws ecr login to buildspec
Custard7 May 9, 2024
cc93e1f
cd back to parent directory in buildspec
Custard7 May 9, 2024
11cf4e7
Remove swc
Custard7 May 9, 2024
f2b0d6b
Update run command
Custard7 May 9, 2024
e59618e
Update dockerfil'
Custard7 May 9, 2024
ecdcf09
Fix production build to build server with swc
Custard7 May 9, 2024
429a372
Merge branch 'payload-2.0' into multi-tenancy
Custard7 May 9, 2024
e34ad9e
Fix scrollbars and tenant creation without media
Custard7 May 9, 2024
d3e1194
Solve Navbar styles
Custard7 May 9, 2024
41bf85e
Add req to all payload update calls for atlas transactions
Custard7 May 9, 2024
0673fe9
Turn off transactions
Custard7 May 9, 2024
2d2ffcb
Merge branch 'payload-2.0' into multi-tenancy
Custard7 May 10, 2024
055ba07
Add s3 cloud storage
Custard7 May 10, 2024
3ed5479
Streamline send creds / template selection
Custard7 May 10, 2024
00e4bea
Fix email sending jobs
Custard7 May 10, 2024
c0adb32
add bullmq queue prefix to solve CROSSLOT error
Custard7 May 10, 2024
bec1f67
Add prefix to all mq queues
Custard7 May 10, 2024
ea42589
Mark batch as sent for now
Custard7 May 10, 2024
5c37e1e
Fix exchange endpoint
Custard7 May 14, 2024
871604c
Update endpoints for/list and /issue to be the same
Custard7 May 14, 2024
461a9c2
Update get user credentials endpoint to resolve template
Custard7 May 15, 2024
182cec5
Update exchange to inject membership ID
Custard7 May 15, 2024
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
499 changes: 227 additions & 272 deletions pnpm-lock.yaml

Large diffs are not rendered by default.

23 changes: 23 additions & 0 deletions services/payload/.env.deploy-sample
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
NODE_ENV=production
PORT=3000
PAYLOAD_SECRET=secret
LC_SEED=aaaaaa

MONGODB_URI=mongo-uri

REDIS_PORT=6379
REDIS_URL=redis-url

SMTP_HOST=smtp.sendgrid.net
SMTP_USER=apikey
SMTP_PASS=base 64 api key
EMAIL_FROM_TITLE=LearnCloud
EMAIL_FROM_SENDER=no-reply@learncloud.ai

USE_CLOUD_STORAGE=true
S3_REGION=us-east-1
S3_BUCKET=bucket_name

CLAIM_PAGE_URL=https://learncard.app/claim/from-dashboard
COORDINATOR_URL=false
STATUS_URL=false
3 changes: 3 additions & 0 deletions services/payload/.gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -165,3 +165,6 @@ build
*.code-workspace

# End of https://www.toptal.com/developers/gitignore/api/node,visualstudiocode
*.DS_Store

src/media/*
13 changes: 13 additions & 0 deletions services/payload/.swcrc
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
{
"jsc": {
"parser": {
"syntax": "typescript",
"tsx": true
},
"target": "es2016"
},
"module": {
"type": "commonjs"
},
"isModule": true
}
8 changes: 6 additions & 2 deletions services/payload/Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ FROM node:20.10-alpine as base
FROM base as builder

WORKDIR /home/node/app
COPY package*.json ./

COPY . .

Expand All @@ -17,14 +18,17 @@ ENV NODE_ENV=production
ENV PAYLOAD_CONFIG_PATH=dist/payload.config.js

WORKDIR /home/node/app
COPY package.json ./
COPY package*.json ./

RUN npm i -g pnpm
RUN pnpm i --production

COPY --from=builder /home/node/app/dist ./dist
COPY --from=builder /home/node/app/build ./build

RUN ls -l /home/node/app
RUN ls -l /home/node/app/dist

EXPOSE 3000

CMD ["node", "dist/server.js"]
CMD ["node", "./dist/server.js"]
11 changes: 11 additions & 0 deletions services/payload/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -67,5 +67,16 @@ The 3 typical env vars will be `MONGODB_URI`, `PAYLOAD_SECRET`, and `PAYLOAD_CON

`docker run --env-file .env -p 3000:3000 my-tag`

### Tenant Roles

| **Tenant Role** | **Permissions** |
|--------------------|--------------------------------------------------------|
| Admin | Full Read Permissions, Access to Admin Dashboard |
| Revocation Manager | Revoke Credential |
| Issuer | Send/Issue Credentials; Create, Update, Delete Batches |
| Batch Manager | Create, Update, Delete Batches |
| Template Manager | Create, Update Templates |
| User | No privileges |

## License
MIT © [MIT](#)
2 changes: 1 addition & 1 deletion services/payload/build/index.html
Original file line number Diff line number Diff line change
@@ -1 +1 @@
<!doctype html><html><head><meta http-equiv="Content-Type" content="text/html; charset=UTF-8"/><meta name="viewport" content="width=device-width,initial-scale=1,user-scalable=no"/><link rel="icon" href="data:," data-placeholder-favicon/><script defer="defer" src="/admin/styles.4931cf4b52c293daff97.js"></script><script defer="defer" src="/admin/main.2e6afa8d00f8b64a8e33.js"></script><link href="/admin/styles.css" rel="stylesheet"></head><body><div id="app"></div><div id="portal"></div></body></html>
<!doctype html><html><head><meta http-equiv="Content-Type" content="text/html; charset=UTF-8"/><meta name="viewport" content="width=device-width,initial-scale=1,user-scalable=no"/><link rel="icon" href="data:," data-placeholder-favicon/><script defer="defer" src="/admin/styles.fa987f1c28cbce320c3c.js"></script><script defer="defer" src="/admin/main.116a5ca72448bd0e6c2b.js"></script><link href="/admin/styles.08e12788f1ec9e78fdbb.css" rel="stylesheet"></head><body><div id="app"></div><div id="portal"></div></body></html>
43 changes: 43 additions & 0 deletions services/payload/buildspec.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
version: 0.2

env:
secrets-manager:
GITHUB_TOKEN: arn:aws:secretsmanager:us-east-1:206533012615:secret:welibrary-github-token-ify1Nc
DOCKER_TOKEN: arn:aws:secretsmanager:us-east-1:206533012615:secret:welibrary-docker-token-nyQDGR

phases:
pre_build:
commands:
- echo Authenticating with Docker
- echo $DOCKER_TOKEN | docker login --username ntonani --password-stdin
- echo Logging in to Amazon ECR...
- aws --version
- aws ecr get-login-password --region $AWS_DEFAULT_REGION | docker login --username $AWS_AUTH_USERNAME --password-stdin $AWS_AUTH_REGISTRY_URI
- COMMIT_HASH=$(echo $CODEBUILD_RESOLVED_SOURCE_VERSION | cut -c 1-7)
- IMAGE_TAG=${COMMIT_HASH:=latest}
- echo Appending GH creds to Docker image...
- echo //npm.pkg.github.com/:_authToken=$GITHUB_TOKEN >> .npmrc
# - echo Adding keys
# - base64 --help
- ls
build:
commands:
- ls
- cd services/payload
- echo Build started on `date`
- echo Building the Docker image...
- docker build -t $REPOSITORY_URI:latest .
- docker tag $REPOSITORY_URI:latest $REPOSITORY_URI:$IMAGE_TAG
- cd ../../
post_build:
commands:
- ls
- echo Build completed on `date`
- echo Pushing the Docker images...
- docker push $REPOSITORY_URI:latest
- docker push $REPOSITORY_URI:$IMAGE_TAG
- echo Writing image definitions file...
- printf '[{"name":"app","imageUri":"%s"}]' $REPOSITORY_URI:$IMAGE_TAG > imagedefinitions.json

artifacts:
files: imagedefinitions.json
18 changes: 13 additions & 5 deletions services/payload/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,8 @@
"dev": "cross-env PAYLOAD_CONFIG_PATH=src/payload.config.ts nodemon",
"build:payload": "cross-env PAYLOAD_CONFIG_PATH=src/payload.config.ts payload build",
"typecheck": "tsc",
"build": "pnpm copyfiles && pnpm build:payload",
"build:server": "swc src -d dist --strip-leading-paths",
"build": "pnpm copyfiles && pnpm build:server && pnpm build:payload",
"serve": "cross-env PAYLOAD_CONFIG_PATH=dist/payload.config.js NODE_ENV=production node dist/server.js",
"copyfiles": "copyfiles -u 1 \"src/**/*.{html,css,scss,ttf,woff,woff2,eot,svg,jpg,png}\" dist/",
"generate:types": "cross-env PAYLOAD_CONFIG_PATH=src/payload.config.ts payload generate:types",
Expand All @@ -18,12 +19,16 @@
"dependencies": {
"@faceless-ui/modal": "2.0.1",
"@faceless-ui/window-info": "^2.1.1",
"@learncard/didkit-plugin": "^1.4.3",
"@learncard/init": "^1.2.15",
"@learncard/didkit-plugin": "^1.4.4",
"@learncard/init": "^1.2.19",
"@payloadcms/bundler-webpack": "^1.0.6",
"@payloadcms/db-mongodb": "^1.5.1",
"@payloadcms/plugin-cloud": "^3.0.0",
"@payloadcms/richtext-slate": "^1.5.1",
"@payloadcms/plugin-cloud-storage": "1.1.2",
"@aws-sdk/client-s3": "3.572.0",
"@aws-sdk/lib-storage": "3.572.0",
"aws-crt": "1.21.2",
"@radix-ui/react-accordion": "^1.1.2",
"better-error-message-for-json-parse": "^0.1.6",
"bullmq": "^4.9.0",
Expand All @@ -47,17 +52,20 @@
"qs": "^6.11.2",
"react": "^18.2.0",
"react-flip-toolkit": "^7.1.0",
"react-helmet": "^6.1.0",
"react-i18next": "^13.2.2",
"react-router-dom": "^5.3.4",
"react-toastify": "^9.1.3",
"sharp": "^0.32.6",
"@swc/core": "1.3.75",
"tailwind-merge": "^1.14.0",
"tailwindcss-animate": "^1.0.7",
"uuid": "^9.0.1",
"webpack-hot-middleware": "^2.25.4"
},
"devDependencies": {
"@learncard/types": "^5.4.0",
"@learncard/types": "^5.5.0",
"@swc/cli": "^0.3.12",
"@types/express": "^4.17.9",
"@types/jsonwebtoken": "^9.0.2",
"@types/papaparse": "^5.3.9",
Expand All @@ -69,7 +77,7 @@
"nodemon": "^2.0.6",
"postcss": "^8.4.30",
"tailwindcss": "^3.0.24",
"ts-node": "^9.1.1",
"ts-node": "10.9.1",
"typescript": "^5.2.2"
}
}
3 changes: 3 additions & 0 deletions services/payload/src/access/anyone.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
import type { Access } from 'payload/config'

export const anyone: Access = () => true
9 changes: 9 additions & 0 deletions services/payload/src/access/superAdmins.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import type { Access } from 'payload/config'
import type { FieldAccess } from 'payload/types'

import { checkUserRoles } from '../utils/checkUserRoles'

export const superAdmins: Access = ({ req: { user } }) => checkUserRoles(['super-admin'], user)

export const superAdminFieldAccess: FieldAccess = ({ req: { user } }) =>
checkUserRoles(['super-admin'], user)
37 changes: 37 additions & 0 deletions services/payload/src/access/tenantIssuersOrBatchManager.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import type { Access } from 'payload/config';

import { checkUserRoles } from '../utils/checkUserRoles';
import { checkTenantRoles } from '../collections/Users/utilities/checkTenantRoles';
import { TENANT_ROLES } from '../constants/roles/tenantRoles';

import getTenantForRequest from '../utils/getTenantForRequest';

// the user must be an issuer of the document's tenant
export const tenantIssuersOrBatchManager: Access = async ({ req }) => {
const { user } = req;
if (checkUserRoles(['super-admin'], user)) {
return true;
}

const tenant = await getTenantForRequest(req);
const tenantId = typeof tenant === 'string' ? tenant : tenant?.id;
if (!checkTenantRoles([TENANT_ROLES.ISSUER, TENANT_ROLES.BATCH_MANAGER], user, tenantId)) {
return false;
}

return {
tenant: {
in:
user?.tenants
?.map(({ tenant, roles }) =>
roles.includes(TENANT_ROLES.ISSUER) ||
roles.includes(TENANT_ROLES.BATCH_MANAGER)
? typeof tenant === 'string'
? tenant
: tenant.id
: null
) // eslint-disable-line function-paren-newline
.filter(Boolean) || [],
},
};
};
32 changes: 32 additions & 0 deletions services/payload/src/access/tenantTemplateManagers.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import type { Access } from 'payload/config'

import { checkUserRoles } from '../utils/checkUserRoles'
import { checkTenantRoles } from '../collections/Users/utilities/checkTenantRoles';
import { TENANT_ROLES } from '../constants/roles/tenantRoles';

import getTenantForRequest from '../utils/getTenantForRequest'

// the user must be an template manager of the document's tenant
export const tenantTemplateManagers: Access = async ({ req }) => {
const { user } = req;
if (checkUserRoles(['super-admin'], user)) {
return true
}

const tenant = await getTenantForRequest(req);
const tenantId = typeof tenant === 'string' ? tenant : tenant?.id
if(!checkTenantRoles([TENANT_ROLES.TEMPLATE_MANAGER], user, tenantId)) {
return false;
}

return {
tenant: {
in:
user?.tenants
?.map(({ tenant, roles }) =>
roles.includes(TENANT_ROLES.TEMPLATE_MANAGER) ? (typeof tenant === 'string' ? tenant : tenant.id) : null,
) // eslint-disable-line function-paren-newline
.filter(Boolean) || [],
},
}
}
32 changes: 32 additions & 0 deletions services/payload/src/access/tenantTrustRegistryManager.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import type { Access } from 'payload/config'

import { checkUserRoles } from '../utils/checkUserRoles'
import { checkTenantRoles } from '../collections/Users/utilities/checkTenantRoles';
import { TENANT_ROLES } from '../constants/roles/tenantRoles';

import getTenantForRequest from '../utils/getTenantForRequest'

// the user must be an trust registry manager of the document's tenant
export const tenantTrustRegistryManager: Access = async ({ req }) => {
const { user } = req;
if (checkUserRoles(['super-admin'], user)) {
return true
}

const tenant = await getTenantForRequest(req);
const tenantId = typeof tenant === 'string' ? tenant : tenant?.id
if(!checkTenantRoles([TENANT_ROLES.TRUST_REGISTRY_MANAGER], user, tenantId)) {
return false;
}

return {
tenant: {
in:
user?.tenants
?.map(({ tenant, roles }) =>
roles.includes(TENANT_ROLES.TRUST_REGISTRY_MANAGER) ? (typeof tenant === 'string' ? tenant : tenant.id) : null,
) // eslint-disable-line function-paren-newline
.filter(Boolean) || [],
},
}
}
Binary file added services/payload/src/assets/lef-icon.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file not shown.
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
import type { Access } from 'payload/types'

export const lastLoggedInTenant: Access = ({ req: { user }, data }) =>
user?.lastLoggedInTenant?.id === data?.id
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import type { Access } from 'payload/config'

export const loggedIn: Access = ({ req: { user } }) => {
return Boolean(user)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import type { Access } from 'payload/config'

import { checkUserRoles } from '../../../utils/checkUserRoles'
import isDraftState from '../utilities/isDraftState';

// the user must be an admin of the document's tenant
export const tenantAdmins: Access = async ({ req: { user }, id }) => {

if (!(await isDraftState(id))) {
return false;
}

if (checkUserRoles(['super-admin'], user)) {
return true
}

return {
tenant: {
in:
user?.tenants
?.map(({ tenant, roles }) =>
roles.includes('admin') ? (typeof tenant === 'string' ? tenant : tenant.id) : null,
) // eslint-disable-line function-paren-newline
.filter(Boolean) || [],
},
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import type { Access } from 'payload/types'

import { isSuperAdmin } from '../../../utils/isSuperAdmin'

export const tenants: Access = ({ req: { user }, data }) =>
// individual documents
(data?.tenant?.id && user?.lastLoggedInTenant?.id === data.tenant.id) ||
(!user?.lastLoggedInTenant?.id && isSuperAdmin(user)) || {
// list of documents
tenant: {
equals: user?.lastLoggedInTenant?.id,
},
}
Loading