UNPKG

@websolutespa/payload-plugin-bowl

Version:

Bowl PayloadCms plugin of the BOM Repository

441 lines (440 loc) 16.7 kB
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