diff --git a/packages/nocodb/src/lib/controllers/orgLicenseController.ts b/packages/nocodb/src/lib/controllers/orgLicenseController.ts index 0281145b7a..02b167c3df 100644 --- a/packages/nocodb/src/lib/controllers/orgLicenseController.ts +++ b/packages/nocodb/src/lib/controllers/orgLicenseController.ts @@ -1,21 +1,16 @@ import { Router } from 'express'; import { OrgUserRoles } from 'nocodb-sdk'; -import { NC_LICENSE_KEY } from '../constants'; -import Store from '../models/Store'; -import Noco from '../Noco'; import { metaApiMetrics } from '../meta/helpers/apiMetrics'; import ncMetaAclMw from '../meta/helpers/ncMetaAclMw'; import { getAjvValidatorMw } from '../meta/api/helpers'; +import { orgLicenseService } from '../services'; async function licenseGet(_req, res) { - const license = await Store.get(NC_LICENSE_KEY); - - res.json({ key: license?.value }); + res.json(await orgLicenseService.licenseGet()); } async function licenseSet(req, res) { - await Store.saveOrUpdate({ value: req.body.key, key: NC_LICENSE_KEY }); - await Noco.loadEEState(); + await orgLicenseService.licenseSet({ key: req.body.key }) res.json({ msg: 'License key saved' }); } diff --git a/packages/nocodb/src/lib/controllers/orgUserController.ts b/packages/nocodb/src/lib/controllers/orgUserController.ts index 5da0bccee9..c89e031124 100644 --- a/packages/nocodb/src/lib/controllers/orgUserController.ts +++ b/packages/nocodb/src/lib/controllers/orgUserController.ts @@ -1,255 +1,75 @@ import { Router } from 'express'; -import { - AuditOperationSubTypes, - AuditOperationTypes, - PluginCategory, -} from 'nocodb-sdk'; -import { v4 as uuidv4 } from 'uuid'; -import validator from 'validator'; import { OrgUserRoles } from 'nocodb-sdk'; -import { NC_APP_SETTINGS } from '../constants'; -import Audit from '../models/Audit'; -import ProjectUser from '../models/ProjectUser'; -import Store from '../models/Store'; -import SyncSource from '../models/SyncSource'; -import User from '../models/User'; -import Noco from '../Noco'; -import { MetaTable } from '../utils/globals'; -import { Tele } from 'nc-help'; import { metaApiMetrics } from '../meta/helpers/apiMetrics'; -import { NcError } from '../meta/helpers/catchError'; -import { extractProps } from '../meta/helpers/extractProps'; import ncMetaAclMw from '../meta/helpers/ncMetaAclMw'; -import { PagedResponseImpl } from '../meta/helpers/PagedResponse'; -import { randomTokenString } from '../meta/helpers/stringHelpers'; import { getAjvValidatorMw } from '../meta/api/helpers'; -import { sendInviteEmail } from './projectUserController'; +import { orgUserService } from '../services'; async function userList(req, res) { res.json( - new PagedResponseImpl(await User.list(req.query), { - ...req.query, - count: await User.count(req.query), + await orgUserService.userList({ + query: req.query, }) ); } async function userUpdate(req, res) { - const updateBody = extractProps(req.body, ['roles']); - - const user = await User.get(req.params.userId); - - if (user.roles.includes(OrgUserRoles.SUPER_ADMIN)) { - NcError.badRequest('Cannot update super admin roles'); - } - res.json( - await User.update(req.params.userId, { - ...updateBody, - token_version: null, + await orgUserService.userUpdate({ + user: req.body, + userId: req.params.userId, }) ); } async function userDelete(req, res) { - const ncMeta = await Noco.ncMeta.startTransaction(); - try { - const user = await User.get(req.params.userId, ncMeta); - - if (user.roles.includes(OrgUserRoles.SUPER_ADMIN)) { - NcError.badRequest('Cannot delete super admin'); - } - - // delete project user entry and assign to super admin - const projectUsers = await ProjectUser.getProjectsIdList( - req.params.userId, - ncMeta - ); - - // todo: clear cache - - // TODO: assign super admin as project owner - for (const projectUser of projectUsers) { - await ProjectUser.delete( - projectUser.project_id, - projectUser.fk_user_id, - ncMeta - ); - } - - // delete sync source entry - await SyncSource.deleteByUserId(req.params.userId, ncMeta); - - // delete user - await User.delete(req.params.userId, ncMeta); - await ncMeta.commit(); - } catch (e) { - await ncMeta.rollback(e); - throw e; - } - + await orgUserService.userDelete({ + userId: req.params.userId, + }); res.json({ msg: 'success' }); } -async function userAdd(req, res, next) { - // allow only viewer or creator role - if ( - req.body.roles && - ![OrgUserRoles.VIEWER, OrgUserRoles.CREATOR].includes(req.body.roles) - ) { - NcError.badRequest('Invalid role'); - } - - // extract emails from request body - const emails = (req.body.email || '') - .toLowerCase() - .split(/\s*,\s*/) - .map((v) => v.trim()); - - // check for invalid emails - const invalidEmails = emails.filter((v) => !validator.isEmail(v)); - - if (!emails.length) { - return NcError.badRequest('Invalid email address'); - } - if (invalidEmails.length) { - NcError.badRequest('Invalid email address : ' + invalidEmails.join(', ')); - } - - const invite_token = uuidv4(); - const error = []; - - for (const email of emails) { - // add user to project if user already exist - const user = await User.getByEmail(email); - - if (user) { - NcError.badRequest('User already exist'); - } else { - try { - // create new user with invite token - await User.insert({ - invite_token, - invite_token_expires: new Date(Date.now() + 24 * 60 * 60 * 1000), - email, - roles: req.body.roles || OrgUserRoles.VIEWER, - token_version: randomTokenString(), - }); - - const count = await User.count(); - Tele.emit('evt', { evt_type: 'org:user:invite', count }); - - await Audit.insert({ - op_type: AuditOperationTypes.ORG_USER, - op_sub_type: AuditOperationSubTypes.INVITE, - user: req.user.email, - description: `invited ${email} to ${req.params.projectId} project `, - ip: req.clientIp, - }); - // in case of single user check for smtp failure - // and send back token if failed - if ( - emails.length === 1 && - !(await sendInviteEmail(email, invite_token, req)) - ) { - return res.json({ invite_token, email }); - } else { - sendInviteEmail(email, invite_token, req); - } - } catch (e) { - console.log(e); - if (emails.length === 1) { - return next(e); - } else { - error.push({ email, error: e.message }); - } - } - } - } +async function userAdd(req, res) { + const result = await orgUserService.userAdd({ + user: req.body, + req, + projectId: req.params.projectId, + }); - if (emails.length === 1) { - res.json({ - msg: 'success', - }); - } else { - return res.json({ invite_token, emails, error }); - } + res.json(result); } -async function userSettings(_req, _res): Promise { - NcError.notImplemented(); +async function userSettings(_req, res): Promise { + await orgUserService.userSettings({}); + res.json({}); } async function userInviteResend(req, res): Promise { - const user = await User.get(req.params.userId); - - if (!user) { - NcError.badRequest(`User with id '${req.params.userId}' not found`); - } - - const invite_token = uuidv4(); - - await User.update(user.id, { - invite_token, - invite_token_expires: new Date(Date.now() + 24 * 60 * 60 * 1000), - }); - - const pluginData = await Noco.ncMeta.metaGet2(null, null, MetaTable.PLUGIN, { - category: PluginCategory.EMAIL, - active: true, - }); - - if (!pluginData) { - NcError.badRequest( - `No Email Plugin is found. Please go to App Store to configure first or copy the invitation URL to users instead.` - ); - } - - await sendInviteEmail(user.email, invite_token, req); - - await Audit.insert({ - op_type: AuditOperationTypes.ORG_USER, - op_sub_type: AuditOperationSubTypes.RESEND_INVITE, - user: user.email, - description: `resent a invite to ${user.email} `, - ip: req.clientIp, + await orgUserService.userInviteResend({ + userId: req.params.userId, + req, }); res.json({ msg: 'success' }); } async function generateResetUrl(req, res) { - const user = await User.get(req.params.userId); - - if (!user) { - NcError.badRequest(`User with id '${req.params.userId}' not found`); - } - const token = uuidv4(); - await User.update(user.id, { - email: user.email, - reset_password_token: token, - reset_password_expires: new Date(Date.now() + 60 * 60 * 1000), - token_version: null, + const result = await orgUserService.generateResetUrl({ + siteUrl: req.ncSiteUrl, + userId: req.params.userId, }); - res.json({ - reset_password_token: token, - reset_password_url: req.ncSiteUrl + `/auth/password/reset/${token}`, - }); + res.json(result); } async function appSettingsGet(_req, res) { - let settings = {}; - try { - settings = JSON.parse((await Store.get(NC_APP_SETTINGS))?.value); - } catch {} + const settings = await orgUserService.appSettingsGet(); res.json(settings); } async function appSettingsSet(req, res) { - await Store.saveOrUpdate({ - value: JSON.stringify(req.body), - key: NC_APP_SETTINGS, + await orgUserService.appSettingsSet({ + settings: req.body, }); res.json({ msg: 'Settings saved' }); diff --git a/packages/nocodb/src/lib/services/attachmentService.ts b/packages/nocodb/src/lib/services/attachmentService.ts new file mode 100644 index 0000000000..7b2b6d3169 --- /dev/null +++ b/packages/nocodb/src/lib/services/attachmentService.ts @@ -0,0 +1,118 @@ +// @ts-ignore +import { Request, Response, Router } from 'express'; +import { nanoid } from 'nanoid'; +import path from 'path'; +import slash from 'slash'; +import mimetypes, { mimeIcons } from '../utils/mimeTypes'; +import { Tele } from 'nc-help'; +import NcPluginMgrv2 from '../meta/helpers/NcPluginMgrv2'; +import Local from '../v1-legacy/plugins/adapters/storage/Local'; + +export async function upload(param: { + path?: string; + // todo: proper type + files: unknown[]; +}) { + const filePath = sanitizeUrlPath(param.path?.toString()?.split('/') || ['']); + const destPath = path.join('nc', 'uploads', ...filePath); + + const storageAdapter = await NcPluginMgrv2.storageAdapter(); + + const attachments = await Promise.all( + param.files?.map(async (file: any) => { + const fileName = `${nanoid(18)}${path.extname(file.originalname)}`; + + const url = await storageAdapter.fileCreate( + slash(path.join(destPath, fileName)), + file + ); + + let attachmentPath; + + // if `url` is null, then it is local attachment + if (!url) { + // then store the attachement path only + // url will be constructued in `useAttachmentCell` + attachmentPath = `download/${filePath.join('/')}/${fileName}`; + } + + return { + ...(url ? { url } : {}), + ...(attachmentPath ? { path: attachmentPath } : {}), + title: file.originalname, + mimetype: file.mimetype, + size: file.size, + icon: mimeIcons[path.extname(file.originalname).slice(1)] || undefined, + }; + }) + ); + + Tele.emit('evt', { evt_type: 'image:uploaded' }); + + return attachments; +} + +export async function uploadViaURL(param: { + path?: string; + urls: { + url: string; + fileName: string; + mimetype?: string; + size?: string | number; + }[]; +}) { + const filePath = sanitizeUrlPath(param?.path?.toString()?.split('/') || ['']); + const destPath = path.join('nc', 'uploads', ...filePath); + + const storageAdapter = await NcPluginMgrv2.storageAdapter(); + + const attachments = await Promise.all( + param.urls?.map?.(async (urlMeta) => { + const { url, fileName: _fileName } = urlMeta; + + const fileName = `${nanoid(18)}${_fileName || url.split('/').pop()}`; + + const attachmentUrl = await (storageAdapter as any).fileCreateByUrl( + slash(path.join(destPath, fileName)), + url + ); + + let attachmentPath; + + // if `attachmentUrl` is null, then it is local attachment + if (!attachmentUrl) { + // then store the attachement path only + // url will be constructued in `useAttachmentCell` + attachmentPath = `download/${filePath.join('/')}/${fileName}`; + } + + return { + ...(attachmentUrl ? { url: attachmentUrl } : {}), + ...(attachmentPath ? { path: attachmentPath } : {}), + title: fileName, + mimetype: urlMeta.mimetype, + size: urlMeta.size, + icon: mimeIcons[path.extname(fileName).slice(1)] || undefined, + }; + }) + ); + + Tele.emit('evt', { evt_type: 'image:uploaded' }); + + return attachments; +} + +export async function fileRead(param: { path: string }) { + // get the local storage adapter to display local attachments + const storageAdapter = new Local(); + const type = + mimetypes[path.extname(param.path).split('/').pop().slice(1)] || + 'text/plain'; + + const img = await storageAdapter.fileRead(slash(param.path)); + return { img, type }; +} + +export function sanitizeUrlPath(paths) { + return paths.map((url) => url.replace(/[/.?#]+/g, '_')); +} diff --git a/packages/nocodb/src/lib/services/index.ts b/packages/nocodb/src/lib/services/index.ts index f487e405ed..9873bb2e11 100644 --- a/packages/nocodb/src/lib/services/index.ts +++ b/packages/nocodb/src/lib/services/index.ts @@ -21,3 +21,7 @@ export * as mapViewService from './mapViewService'; export * as modelVisibilityService from './modelVisibilityService'; export * as sharedBaseService from './sharedBaseService'; export * as orgUserService from './orgUserService'; +export * as orgTokenService from './orgTokenService'; +export * as orgLicenseService from './orgLicenseService'; +export * as projectUserService from './projectUserService'; +export * as attachmentService from './attachmentService'; diff --git a/packages/nocodb/src/lib/services/orgLicenseService.ts b/packages/nocodb/src/lib/services/orgLicenseService.ts new file mode 100644 index 0000000000..7de1f717bf --- /dev/null +++ b/packages/nocodb/src/lib/services/orgLicenseService.ts @@ -0,0 +1,17 @@ +import { NC_LICENSE_KEY } from '../constants'; +import Store from '../models/Store'; +import Noco from '../Noco'; + +export async function licenseGet() { + const license = await Store.get(NC_LICENSE_KEY); + + return { key: license?.value } +} + +export async function licenseSet(param:{ + key: string +}) { + await Store.saveOrUpdate({ value: param.key, key: NC_LICENSE_KEY }); + await Noco.loadEEState(); + return true +} diff --git a/packages/nocodb/src/lib/services/orgTokenService.ts b/packages/nocodb/src/lib/services/orgTokenService.ts new file mode 100644 index 0000000000..ede91f0bc6 --- /dev/null +++ b/packages/nocodb/src/lib/services/orgTokenService.ts @@ -0,0 +1,53 @@ +import { ApiTokenReqType, OrgUserRoles } from 'nocodb-sdk'; +import { User } from '../models'; +import ApiToken from '../models/ApiToken'; +import { Tele } from 'nc-help'; +import { NcError } from '../meta/helpers/catchError'; +import { PagedResponseImpl } from '../meta/helpers/PagedResponse'; + +export async function apiTokenList(param: { user: User; query: any }) { + const fk_user_id = param.user.id; + let includeUnmappedToken = false; + if (param.user.roles.includes(OrgUserRoles.SUPER_ADMIN)) { + includeUnmappedToken = true; + } + + return new PagedResponseImpl( + await ApiToken.listWithCreatedBy({ + ...param.query, + fk_user_id, + includeUnmappedToken, + }), + { + ...param.query, + count: await ApiToken.count({ + includeUnmappedToken, + fk_user_id, + }), + } + ); +} + +export async function apiTokenCreate(param: { + user: User; + apiToken: ApiTokenReqType; +}) { + Tele.emit('evt', { evt_type: 'org:apiToken:created' }); + return await ApiToken.insert({ + ...param.apiToken, + fk_user_id: param['user'].id, + }); +} + +export async function apiTokenDelete(param: { user: User; token: string }) { + const fk_user_id = param.user.id; + const apiToken = await ApiToken.getByToken(param.token); + if ( + !param.user.roles.includes(OrgUserRoles.SUPER_ADMIN) && + apiToken.fk_user_id !== fk_user_id + ) { + NcError.notFound('Token not found'); + } + Tele.emit('evt', { evt_type: 'org:apiToken:deleted' }); + return await ApiToken.delete(param.token); +} diff --git a/packages/nocodb/src/lib/services/orgUserService.ts b/packages/nocodb/src/lib/services/orgUserService.ts index e1986a3be6..f58e85f890 100644 --- a/packages/nocodb/src/lib/services/orgUserService.ts +++ b/packages/nocodb/src/lib/services/orgUserService.ts @@ -16,7 +16,7 @@ import { NcError } from '../meta/helpers/catchError'; import { extractProps } from '../meta/helpers/extractProps'; import { PagedResponseImpl } from '../meta/helpers/PagedResponse'; import { randomTokenString } from '../meta/helpers/stringHelpers'; -import { sendInviteEmail } from '../meta/api/projectUserApis'; +import { sendInviteEmail } from './projectUserService'; export async function userList(param: { // todo: add better typing diff --git a/packages/nocodb/src/lib/services/projectUserService.ts b/packages/nocodb/src/lib/services/projectUserService.ts new file mode 100644 index 0000000000..1ee50e9410 --- /dev/null +++ b/packages/nocodb/src/lib/services/projectUserService.ts @@ -0,0 +1,315 @@ +import { OrgUserRoles, ProjectUserReqType } from 'nocodb-sdk'; +import { Tele } from 'nc-help'; +import { PagedResponseImpl } from '../meta/helpers/PagedResponse'; +import ProjectUser from '../models/ProjectUser'; +import validator from 'validator'; +import { NcError } from '../meta/helpers/catchError'; +import { v4 as uuidv4 } from 'uuid'; +import User from '../models/User'; +import Audit from '../models/Audit'; +import NocoCache from '../cache/NocoCache'; +import { CacheGetType, CacheScope, MetaTable } from '../utils/globals'; +import * as ejs from 'ejs'; +import NcPluginMgrv2 from '../meta/helpers/NcPluginMgrv2'; +import Noco from '../Noco'; +import { PluginCategory } from 'nocodb-sdk'; +import { randomTokenString } from '../meta/helpers/stringHelpers'; + +export async function userList(param: { projectId: string; query: any }) { + return new PagedResponseImpl( + await ProjectUser.getUsersList({ + ...param.query, + project_id: param.projectId, + }), + { + ...param.query, + count: await ProjectUser.getUsersCount(param.query), + } + ); +} + +export async function userInvite(param: { + projectId: string; + projectUser: ProjectUserReqType; + req: any; +}): Promise { + const emails = (param.projectUser.email || '') + .toLowerCase() + .split(/\s*,\s*/) + .map((v) => v.trim()); + + // check for invalid emails + const invalidEmails = emails.filter((v) => !validator.isEmail(v)); + if (!emails.length) { + return NcError.badRequest('Invalid email address'); + } + if (invalidEmails.length) { + NcError.badRequest('Invalid email address : ' + invalidEmails.join(', ')); + } + + const invite_token = uuidv4(); + const error = []; + + for (const email of emails) { + // add user to project if user already exist + const user = await User.getByEmail(email); + + if (user) { + // check if this user has been added to this project + const projectUser = await ProjectUser.get(param.projectId, user.id); + if (projectUser) { + NcError.badRequest( + `${user.email} with role ${projectUser.roles} already exists in this project` + ); + } + + await ProjectUser.insert({ + project_id: param.projectId, + fk_user_id: user.id, + roles: param.projectUser.roles || 'editor', + }); + + const cachedUser = await NocoCache.get( + `${CacheScope.USER}:${email}___${param.projectId}`, + CacheGetType.TYPE_OBJECT + ); + + if (cachedUser) { + cachedUser.roles = param.projectUser.roles || 'editor'; + await NocoCache.set( + `${CacheScope.USER}:${email}___${param.projectId}`, + cachedUser + ); + } + + await Audit.insert({ + project_id: param.projectId, + op_type: 'AUTHENTICATION', + op_sub_type: 'INVITE', + user: param.req.user.email, + description: `invited ${email} to ${param.projectId} project `, + ip: param.req.clientIp, + }); + } else { + try { + // create new user with invite token + const { id } = await User.insert({ + invite_token, + invite_token_expires: new Date(Date.now() + 24 * 60 * 60 * 1000), + email, + roles: OrgUserRoles.VIEWER, + token_version: randomTokenString(), + }); + + // add user to project + await ProjectUser.insert({ + project_id: param.projectId, + fk_user_id: id, + roles: param.projectUser.roles, + }); + + const count = await User.count(); + Tele.emit('evt', { evt_type: 'project:invite', count }); + + await Audit.insert({ + project_id: param.projectId, + op_type: 'AUTHENTICATION', + op_sub_type: 'INVITE', + user: param.req.user.email, + description: `invited ${email} to ${param.projectId} project `, + ip: param.req.clientIp, + }); + // in case of single user check for smtp failure + // and send back token if failed + if ( + emails.length === 1 && + !(await sendInviteEmail(email, invite_token, param.req)) + ) { + return { invite_token, email }; + } else { + sendInviteEmail(email, invite_token, param.req); + } + } catch (e) { + console.log(e); + if (emails.length === 1) { + throw e; + } else { + error.push({ email, error: e.message }); + } + } + } + } + + if (emails.length === 1) { + return { + msg: 'success', + }; + } else { + return { invite_token, emails, error }; + } +} + +export async function projectUserUpdate(param: { + userId: string; + // todo: update swagger + projectUser: ProjectUserReqType & { project_id: string }; + // todo: refactor + req: any; + projectId: string; +}): Promise { + // todo: use param.projectId + if (!param.projectUser?.project_id) { + NcError.badRequest('Missing project id in request body.'); + } + + if ( + param.req.session?.passport?.user?.roles?.owner && + param.req.session?.passport?.user?.id === param.userId && + param.projectUser.roles.indexOf('owner') === -1 + ) { + NcError.badRequest("Super admin can't remove Super role themselves"); + } + const user = await User.get(param.userId); + + if (!user) { + NcError.badRequest(`User with id '${param.userId}' doesn't exist`); + } + + // todo: handle roles which contains super + if ( + !param.req.session?.passport?.user?.roles?.owner && + param.projectUser.roles.indexOf('owner') > -1 + ) { + NcError.forbidden('Insufficient privilege to add super admin role.'); + } + + await ProjectUser.update( + param.projectId, + param.userId, + param.projectUser.roles + ); + + await Audit.insert({ + op_type: 'AUTHENTICATION', + op_sub_type: 'ROLES_MANAGEMENT', + user: param.req.user.email, + description: `updated roles for ${user.email} with ${param.projectUser.roles} `, + ip: param.req.clientIp, + }); + + return { + msg: 'User details updated successfully', + }; +} + +export async function projectUserDelete(param: { + projectId: string; + userId: string; + // todo: refactor + req: any; +}): Promise { + const project_id = param.projectId; + + if (param.req.session?.passport?.user?.id === param.userId) { + NcError.badRequest("Admin can't delete themselves!"); + } + + if (!param.req.session?.passport?.user?.roles?.owner) { + const user = await User.get(param.userId); + if (user.roles?.split(',').includes('super')) + NcError.forbidden('Insufficient privilege to delete a super admin user.'); + + const projectUser = await ProjectUser.get(project_id, param.userId); + if (projectUser?.roles?.split(',').includes('super')) + NcError.forbidden('Insufficient privilege to delete a owner user.'); + } + + await ProjectUser.delete(project_id, param.userId); + return true; +} + +export async function projectUserInviteResend(param: { + userId: string; + projectUser: ProjectUserReqType; + projectId: string; + // todo: refactor + req: any; +}): Promise { + const user = await User.get(param.userId); + + if (!user) { + NcError.badRequest(`User with id '${param.userId}' not found`); + } + + param.projectUser.roles = user.roles; + const invite_token = uuidv4(); + + await User.update(user.id, { + invite_token, + invite_token_expires: new Date(Date.now() + 24 * 60 * 60 * 1000), + }); + + const pluginData = await Noco.ncMeta.metaGet2(null, null, MetaTable.PLUGIN, { + category: PluginCategory.EMAIL, + active: true, + }); + + if (!pluginData) { + NcError.badRequest( + `No Email Plugin is found. Please go to App Store to configure first or copy the invitation URL to users instead.` + ); + } + + await sendInviteEmail(user.email, invite_token, param.req); + + await Audit.insert({ + op_type: 'AUTHENTICATION', + op_sub_type: 'RESEND_INVITE', + user: user.email, + description: `resent a invite to ${user.email} `, + ip: param.req.clientIp, + project_id: param.projectId, + }); + + return true; +} + +// todo: refactor the whole function +export async function sendInviteEmail( + email: string, + token: string, + req: any +): Promise { + try { + const template = ( + await import('../meta/api/userApi/ui/emailTemplates/invite') + ).default; + + const emailAdapter = await NcPluginMgrv2.emailAdapter(); + + if (emailAdapter) { + await emailAdapter.mailSend({ + to: email, + subject: 'Verify email', + html: ejs.render(template, { + signupLink: `${req.ncSiteUrl}${ + Noco.getConfig()?.dashboardPath + }#/signup/${token}`, + projectName: req.body?.projectName, + roles: (req.body?.roles || '') + .split(',') + .map((r) => r.replace(/^./, (m) => m.toUpperCase())) + .join(', '), + adminEmail: req.session?.passport?.user?.email, + }), + }); + return true; + } + } catch (e) { + console.log( + 'Warning : `mailSend` failed, Please configure emailClient configuration.', + e.message + ); + throw e; + } +}