@websolutespa/payload-plugin-bowl
Version:
Bowl PayloadCms plugin of the BOM Repository
441 lines (440 loc) • 16.7 kB
JavaScript
import { passwordStrategy } from '@websolutespa/bom-core';
import { eachDataField, HttpStatus, isDataField } from '@websolutespa/payload-utils';
import { getRequestCollection, ResponseError, ResponseSuccess } from '@websolutespa/payload-utils/server';
import { ValidationError } from 'payload';
import { v4 as uuid } from 'uuid';
import { options } from '../../options';
import { hasRole } from '../access';
import { sendEmail } from './email.service';
/**
* optin handler:
* - assign consent preferences
* - createEndUser === 'user' -> update existing endUser adding roles ['guest', 'user']
*/ const optin = async (req)=>{
const { payload } = req;
const actionId = req.routeParams?.id;
const actionSlug = req.routeParams?.slug;
const action = await payload.findByID({
collection: actionSlug,
id: actionId,
overrideAccess: true
});
const { email, consentPreferences } = action;
const collection = payload.collections[actionSlug];
if (!collection) {
throw {
status: HttpStatus.NOT_IMPLEMENTED,
message: 'Not Implemented'
};
}
const actionConfig = collection.config;
const createEndUser = actionConfig.custom?.createEndUser;
/**
* find existing endUsers
*/ const existingEndUsers = await payload.find({
collection: options.slug.endUsers,
where: {
email: {
equals: email
}
},
overrideAccess: true
});
const existingEndUser = existingEndUsers.totalDocs > 0 ? existingEndUsers.docs[0] : undefined;
if (existingEndUser) {
if (createEndUser) {
/**
* all optin actions set emailVerified to true
*/ const endUserData = {
emailVerified: true,
roles: [
...existingEndUser.roles || []
],
consentPreferences: []
};
/**
* optin set endUser role 'user' for action with flag createEndUser === 'user'
*/ if (createEndUser === options.roles.User && !hasRole(existingEndUser, options.roles.User)) {
endUserData.roles.push(options.roles.User);
}
/**
* updating the endUser with consentPreferences and fields
*/ const preferences = consentPreferences?.length > 0 ? consentPreferences : [];
const existingConsentPreferences = existingEndUser.consentPreferences || [];
/**
* if the preference already exists update the date
*/ endUserData.consentPreferences = existingConsentPreferences.map((x)=>({
consentPreference: x.consentPreference.id,
date: preferences.find((p)=>p.id === x.consentPreference.id) !== undefined ? new Date() : x.date
}));
/**
* if the preference does not exists insert the preference
*/ preferences.forEach((preference)=>{
if (!endUserData.consentPreferences.find((x)=>x.consentPreference === preference.id)) {
endUserData.consentPreferences.push({
consentPreference: preference.id,
date: new Date()
});
}
});
const endUser = await payload.update({
collection: options.slug.endUsers,
id: existingEndUser.id,
data: {
...endUserData
},
overrideAccess: true
});
// console.log(actionSlug, 'optin');
}
}
/**
* execute after opt-ins associated with the action
*/ if (typeof actionConfig.custom?.afterOptin === 'function') {
await actionConfig.custom.afterOptin({
collection: actionConfig,
doc: action,
previousDoc: {
...action,
...{
endUser: existingEndUser ? existingEndUser.id : undefined
}
},
req: req
});
}
};
export const optinGet = {
path: '/actions/optin/:id/:slug',
method: 'get',
handler: async (req)=>{
try {
await optin(req);
return ResponseSuccess({
status: 200,
message: 'optin success'
});
} catch (error) {
console.error('ActionService.optinGet.error', error);
return ResponseError(error);
}
}
};
const optout = async (req)=>{
const { payload } = req;
const actionId = req.routeParams?.id;
const actionSlug = req.routeParams?.slug;
const action = await payload.findByID({
collection: actionSlug,
id: actionId,
overrideAccess: true
});
const { endUser, consentPreferences } = action;
const collection = payload.collections[actionSlug];
if (!collection) {
throw {
status: HttpStatus.NOT_IMPLEMENTED,
message: 'Not Implemented'
};
}
const actionConfig = collection.config;
// remove consent preferences from the endUser
if (endUser) {
const data = {
consentPreferences: endUser.consentPreferences?.filter((x)=>!consentPreferences.map((y)=>y.id).includes(x.consentPreference.id)).map((x)=>({
consentPreference: x.consentPreference.id,
date: x.date
})) ?? []
};
await payload.update({
collection: options.slug.endUsers,
id: endUser.id,
data: data,
overrideAccess: true
});
}
// set the revoked field on the action
const data = {
consentsRevoked: true,
consentsRevokedDate: new Date()
};
await payload.update({
collection: actionSlug,
id: actionId,
data: data,
overrideAccess: true
});
if (typeof actionConfig.custom?.afterOptout === 'function') {
await actionConfig.custom.afterOptout({
collection: actionConfig,
doc: {
...action,
...data
},
previousDoc: action,
req: req
});
}
};
export const optoutGet = {
path: '/actions/optout/:id/:slug',
method: 'get',
handler: async (req)=>{
try {
await optout(req);
return ResponseSuccess({
status: 200,
message: 'optout success'
});
} catch (error) {
console.error('ActionService.optoutGet.error', error);
return ResponseError(error);
}
}
};
/**
* beforeValidateActionHook:
* check existing email address if createEndUser == 'user'
*/ export const beforeValidateActionHook = (collectionConfig)=>async ({ data, req, operation, originalDoc })=>{
if (operation !== 'create') {
return data;
}
const { payload } = req;
const collection = getRequestCollection(req);
const config = collection.config;
const actionSlug = config.slug;
/**
* check email validation
*/ if (!data || !data.email) {
throw new ValidationError({
collection: actionSlug,
errors: [
{
path: 'email',
message: 'Missing field email.'
}
]
});
}
// avoid mismatch between action and endUser email values (payload will force a lowercase email value on endUser creation)
data.email = data.email.toLowerCase();
if (config.custom?.createEndUser === 'user') {
/**
* check password validation
* !! todo add custom pattern password validation
*/ if (!data.password) {
throw new ValidationError({
collection: actionSlug,
errors: [
{
path: 'password',
message: 'Missing field password.'
}
]
});
}
if (!new RegExp(passwordStrategy).test(data.password)) {
throw new ValidationError({
collection: actionSlug,
errors: [
{
path: 'password',
message: 'Your password must be at least 8 characters long, contain at least one number and have a mixture of uppercase and lowercase letters.'
}
]
});
}
/**
* check user existence
*/ const existingEndUsers = await payload.find({
collection: options.slug.endUsers,
where: {
email: {
equals: data.email
}
},
overrideAccess: true
});
const existingEndUser = existingEndUsers.totalDocs > 0 ? existingEndUsers.docs[0] : undefined;
/**
* !!! check
* If the end user exists
* and does not have the "user" role (because it was not confirmed via opt-in)
* throw a validation error
*/ if (existingEndUser && hasRole(existingEndUser, options.roles.User)) {
throw new ValidationError({
collection: actionSlug,
errors: [
{
path: 'email',
message: 'This email address is not available. Choose a different address.'
}
]
});
}
}
/**
* Returning data to either create or update a document with
*/ return data;
};
/**
* beforeChangeActionHook:
* assigning endUser id if user is logged in
*/ export const beforeChangeActionHook = (collectionConfig)=>async ({ data, req, operation, originalDoc })=>{
/**
* If the endUser is logged in
* add the logged end user to the action
*/ if (req.user && req.user.collection === options.slug.endUsers && hasRole(req.user, options.roles.User)) {
data['endUser'] = req.user.id;
}
/**
* Checking for existence of submitted preferences
* here we filter for non existing preferences to avoid successive data integrity errors
*/ if (Array.isArray(data.consentPreferences)) {
const { payload } = req;
const preferences = await payload.find({
collection: options.slug.consentPreference,
where: {
id: {
in: data.consentPreferences
}
},
req: req,
overrideAccess: true
});
data.consentPreferences = data.consentPreferences.filter((preferenceId)=>{
return preferences.docs.find((x)=>x.id === preferenceId) !== undefined;
});
}
if (operation !== 'create') {
return data;
}
const { payload } = req;
const collection = getRequestCollection(req);
const actionSlug = collection.config.slug;
const actionCollection = payload.collections[actionSlug];
if (!actionCollection) {
throw {
status: HttpStatus.NOT_IMPLEMENTED,
message: 'Not Implemented'
};
}
const actionConfig = actionCollection.config;
const createEndUser = actionConfig.custom?.createEndUser;
/*
- createEndUser === 'guest' -> create or update existing endUser with roles ['guest']
*/ const createUser = actionConfig.custom?.createUser;
/*
- createEndUser === 'user' -> create or update existing endUser with roles ['guest', 'user']
*/ /**
* only actions with createEndUser can create or update endUser
*/ if (createEndUser) {
const filteredData = Object.fromEntries(Object.entries(data).filter(([k, v])=>{
const i = actionConfig.fields.findIndex((x)=>isDataField(x) && x.name === k && x.custom?.updateEndUser === true);
return v && k === 'email' || i !== -1;
}));
console.log('filteredData', filteredData);
const endUserCollection = payload.collections[options.slug.endUsers];
if (!endUserCollection) {
throw {
status: HttpStatus.NOT_IMPLEMENTED,
message: 'Not Implemented'
};
}
const endUserConfig = endUserCollection.config;
const endUserData = {};
eachDataField(endUserConfig.fields, (field)=>{
/**
* roles, consentPreferences and emailVerified will be added after optin
*/ if ([
'roles',
'consentPreferences',
'emailVerified'
].includes(field.name)) {
return;
}
if (filteredData[field.name] != null) {
switch(field.type){
case 'relationship':
endUserData[field.name] = filteredData[field.name].id;
break;
default:
endUserData[field.name] = filteredData[field.name];
}
}
});
/**
* assign user roles
* only guest role allowed
* user role will be added after optin
*/ // const roles = createEndUser === 'user' ? [options.roles.Guest, options.roles.User] : [options.roles.Guest];
endUserData.roles = [
options.roles.Guest
];
/**
* find existing endUsers
*/ const existingEndUsers = await payload.find({
collection: options.slug.endUsers,
where: {
email: {
equals: data.email
}
},
overrideAccess: true
});
const existingEndUser = existingEndUsers.totalDocs > 0 ? existingEndUsers.docs[0] : undefined;
if (existingEndUser) {
/**
* only action with createEndUser === 'user' can update user role
*/ if (hasRole(existingEndUser, options.roles.User) && createEndUser !== 'user') {
return data;
}
/**
* updating endUser
*/ const endUser = await payload.update({
collection: options.slug.endUsers,
id: existingEndUser.id,
data: {
...endUserData
},
overrideAccess: true
});
// console.log('ActionService.beforeChangeActionHook.endUser.update', endUser);
data.endUser = endUser ? endUser.id : undefined;
} else {
/**
* creating endUser
*/ endUserData.password = createEndUser === 'user' ? data.password : uuid();
const endUser = await payload.create({
collection: options.slug.endUsers,
data: {
...endUserData
},
overrideAccess: true
});
// console.log('ActionService.beforeChangeActionHook.endUser.create', endUser);
data.endUser = endUser ? endUser.id : undefined;
}
}
/**
* Returning data to either create or update a document with
*/ return data;
};
/**
* afterChangeActionHook:
* on operation create, send emails if the post data contains an emailData array
*/ export const afterChangeActionHook = (collectionConfig)=>async ({ doc, req, previousDoc, operation })=>{
if (operation !== 'create' || req.body === undefined) {
return doc;
}
const collection = getRequestCollection(req);
const actionSlug = collection.config.slug;
const actionId = doc.id;
await sendEmail(req, actionSlug, (options)=>{
const html = typeof options.html === 'string' ? options.html : '';
options.html = html.replace(/(\{(actionId|actionSlug)\})/gm, (m, g1, g2)=>g2 === 'actionId' ? actionId : actionSlug);
return options;
});
return doc;
};
//# sourceMappingURL=action.service.js.map