From ec0c21b7e7e3d137d8bb26f7d793dd599a69c625 Mon Sep 17 00:00:00 2001 From: Pranav C Date: Wed, 1 Mar 2023 12:20:03 +0530 Subject: [PATCH] refactor: orgUserApi handler Signed-off-by: Pranav C --- packages/nocodb/src/lib/services/index.ts | 1 + .../nocodb/src/lib/services/orgUserService.ts | 266 ++++++++++++++++++ 2 files changed, 267 insertions(+) create mode 100644 packages/nocodb/src/lib/services/orgUserService.ts diff --git a/packages/nocodb/src/lib/services/index.ts b/packages/nocodb/src/lib/services/index.ts index 7efb0a2e74..f487e405ed 100644 --- a/packages/nocodb/src/lib/services/index.ts +++ b/packages/nocodb/src/lib/services/index.ts @@ -20,3 +20,4 @@ export * as metaDiffService from './metaDiffService'; export * as mapViewService from './mapViewService'; export * as modelVisibilityService from './modelVisibilityService'; export * as sharedBaseService from './sharedBaseService'; +export * as orgUserService from './orgUserService'; diff --git a/packages/nocodb/src/lib/services/orgUserService.ts b/packages/nocodb/src/lib/services/orgUserService.ts new file mode 100644 index 0000000000..e1986a3be6 --- /dev/null +++ b/packages/nocodb/src/lib/services/orgUserService.ts @@ -0,0 +1,266 @@ +import { + AuditOperationSubTypes, + AuditOperationTypes, + PluginCategory, + UserType, +} 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, ProjectUser, Store, SyncSource, User } from '../models'; +import Noco from '../Noco'; +import { MetaTable } from '../utils/globals'; +import { Tele } from 'nc-help'; +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'; + +export async function userList(param: { + // todo: add better typing + query: Record; +}) { + const { query = {} } = param; + + return new PagedResponseImpl(await User.list(query), { + ...query, + count: await User.count(query), + }); +} + +export async function userUpdate(param: { + // todo: better typing + user: Partial; + userId: string; +}) { + const updateBody = extractProps(param.user, ['roles']); + + const user = await User.get(param.userId); + + if (user.roles.includes(OrgUserRoles.SUPER_ADMIN)) { + NcError.badRequest('Cannot update super admin roles'); + } + + return await User.update(param.userId, { + ...updateBody, + token_version: null, + }); +} + +export async function userDelete(param: { userId: string }) { + const ncMeta = await Noco.ncMeta.startTransaction(); + try { + const user = await User.get(param.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( + param.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(param.userId, ncMeta); + + // delete user + await User.delete(param.userId, ncMeta); + await ncMeta.commit(); + } catch (e) { + await ncMeta.rollback(e); + throw e; + } + + return true; +} + +export async function userAdd(param: { + user: UserType; + projectId: string; + // todo: refactor + req: any; +}) { + // allow only viewer or creator role + if ( + param.user.roles && + ![OrgUserRoles.VIEWER, OrgUserRoles.CREATOR].includes( + param.user.roles as OrgUserRoles + ) + ) { + NcError.badRequest('Invalid role'); + } + + // extract emails from request body + const emails = (param.user.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: param.user.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: 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 userSettings(_param): Promise { + NcError.notImplemented(); +} + +export async function userInviteResend(param: { + userId: string; + req: any; +}): Promise { + const user = await User.get(param.userId); + + if (!user) { + NcError.badRequest(`User with id '${param.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, param.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: param.req.clientIp, + }); + + return true; +} + +export async function generateResetUrl(param: { + userId: string; + siteUrl: string; +}) { + const user = await User.get(param.userId); + + if (!user) { + NcError.badRequest(`User with id '${param.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, + }); + + return { + reset_password_token: token, + reset_password_url: param.siteUrl + `/auth/password/reset/${token}`, + }; +} + +export async function appSettingsGet() { + let settings = {}; + try { + settings = JSON.parse((await Store.get(NC_APP_SETTINGS))?.value); + } catch {} + return settings; +} + +export async function appSettingsSet(param: { settings: any }) { + await Store.saveOrUpdate({ + value: JSON.stringify(param.settings), + key: NC_APP_SETTINGS, + }); + return true; +}