|
|
|
@ -1,305 +1,59 @@
|
|
|
|
|
import { OrgUserRoles } from 'nocodb-sdk'; |
|
|
|
|
import { Tele } from 'nc-help'; |
|
|
|
|
import ncMetaAclMw from '../meta/helpers/ncMetaAclMw'; |
|
|
|
|
import { Router } from 'express'; |
|
|
|
|
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 { metaApiMetrics } from '../meta/helpers/apiMetrics'; |
|
|
|
|
import { randomTokenString } from '../meta/helpers/stringHelpers'; |
|
|
|
|
import { getAjvValidatorMw } from '../meta/api/helpers'; |
|
|
|
|
import { projectUserService } from '../services'; |
|
|
|
|
|
|
|
|
|
async function userList(req, res) { |
|
|
|
|
res.json({ |
|
|
|
|
users: new PagedResponseImpl( |
|
|
|
|
await ProjectUser.getUsersList({ |
|
|
|
|
...req.query, |
|
|
|
|
project_id: req.params.projectId, |
|
|
|
|
}), |
|
|
|
|
{ |
|
|
|
|
...req.query, |
|
|
|
|
count: await ProjectUser.getUsersCount(req.query), |
|
|
|
|
} |
|
|
|
|
), |
|
|
|
|
users: await projectUserService.userList({ |
|
|
|
|
projectId: req.params.projectId, |
|
|
|
|
query: req.query, |
|
|
|
|
}), |
|
|
|
|
}); |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
async function userInvite(req, res, next): Promise<any> { |
|
|
|
|
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) { |
|
|
|
|
// check if this user has been added to this project
|
|
|
|
|
const projectUser = await ProjectUser.get(req.params.projectId, user.id); |
|
|
|
|
if (projectUser) { |
|
|
|
|
NcError.badRequest( |
|
|
|
|
`${user.email} with role ${projectUser.roles} already exists in this project` |
|
|
|
|
); |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
await ProjectUser.insert({ |
|
|
|
|
project_id: req.params.projectId, |
|
|
|
|
fk_user_id: user.id, |
|
|
|
|
roles: req.body.roles || 'editor', |
|
|
|
|
}); |
|
|
|
|
|
|
|
|
|
const cachedUser = await NocoCache.get( |
|
|
|
|
`${CacheScope.USER}:${email}___${req.params.projectId}`, |
|
|
|
|
CacheGetType.TYPE_OBJECT |
|
|
|
|
); |
|
|
|
|
|
|
|
|
|
if (cachedUser) { |
|
|
|
|
cachedUser.roles = req.body.roles || 'editor'; |
|
|
|
|
await NocoCache.set( |
|
|
|
|
`${CacheScope.USER}:${email}___${req.params.projectId}`, |
|
|
|
|
cachedUser |
|
|
|
|
); |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
await Audit.insert({ |
|
|
|
|
project_id: req.params.projectId, |
|
|
|
|
op_type: 'AUTHENTICATION', |
|
|
|
|
op_sub_type: 'INVITE', |
|
|
|
|
user: req.user.email, |
|
|
|
|
description: `invited ${email} to ${req.params.projectId} project `, |
|
|
|
|
ip: 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: req.params.projectId, |
|
|
|
|
fk_user_id: id, |
|
|
|
|
roles: req.body.roles, |
|
|
|
|
}); |
|
|
|
|
|
|
|
|
|
const count = await User.count(); |
|
|
|
|
Tele.emit('evt', { evt_type: 'project:invite', count }); |
|
|
|
|
|
|
|
|
|
await Audit.insert({ |
|
|
|
|
project_id: req.params.projectId, |
|
|
|
|
op_type: 'AUTHENTICATION', |
|
|
|
|
op_sub_type: '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 }); |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
if (emails.length === 1) { |
|
|
|
|
res.json({ |
|
|
|
|
msg: 'success', |
|
|
|
|
}); |
|
|
|
|
} else { |
|
|
|
|
return res.json({ invite_token, emails, error }); |
|
|
|
|
} |
|
|
|
|
async function userInvite(req, res): Promise<any> { |
|
|
|
|
res.json( |
|
|
|
|
await projectUserService.userInvite({ |
|
|
|
|
projectId: req.params.projectId, |
|
|
|
|
projectUser: req.body, |
|
|
|
|
req, |
|
|
|
|
}) |
|
|
|
|
); |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
// @ts-ignore
|
|
|
|
|
async function projectUserUpdate(req, res, next): Promise<any> { |
|
|
|
|
if (!req?.body?.project_id) { |
|
|
|
|
return next(new Error('Missing project id in request body.')); |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
if ( |
|
|
|
|
req.session?.passport?.user?.roles?.owner && |
|
|
|
|
req.session?.passport?.user?.id === req.params.userId && |
|
|
|
|
req.body.roles.indexOf('owner') === -1 |
|
|
|
|
) { |
|
|
|
|
NcError.badRequest("Super admin can't remove Super role themselves"); |
|
|
|
|
} |
|
|
|
|
try { |
|
|
|
|
const user = await User.get(req.params.userId); |
|
|
|
|
|
|
|
|
|
if (!user) { |
|
|
|
|
NcError.badRequest(`User with id '${req.params.userId}' doesn't exist`); |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
// todo: handle roles which contains super
|
|
|
|
|
if ( |
|
|
|
|
!req.session?.passport?.user?.roles?.owner && |
|
|
|
|
req.body.roles.indexOf('owner') > -1 |
|
|
|
|
) { |
|
|
|
|
NcError.forbidden('Insufficient privilege to add super admin role.'); |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
await ProjectUser.update( |
|
|
|
|
req.params.projectId, |
|
|
|
|
req.params.userId, |
|
|
|
|
req.body.roles |
|
|
|
|
); |
|
|
|
|
|
|
|
|
|
await Audit.insert({ |
|
|
|
|
op_type: 'AUTHENTICATION', |
|
|
|
|
op_sub_type: 'ROLES_MANAGEMENT', |
|
|
|
|
user: req.user.email, |
|
|
|
|
description: `updated roles for ${user.email} with ${req.body.roles} `, |
|
|
|
|
ip: req.clientIp, |
|
|
|
|
}); |
|
|
|
|
|
|
|
|
|
res.json({ |
|
|
|
|
msg: 'User details updated successfully', |
|
|
|
|
}); |
|
|
|
|
} catch (e) { |
|
|
|
|
next(e); |
|
|
|
|
} |
|
|
|
|
res.json( |
|
|
|
|
await projectUserService.projectUserUpdate({ |
|
|
|
|
projectUser: req.body, |
|
|
|
|
projectId: req.params.projectId, |
|
|
|
|
userId: req.params.userId, |
|
|
|
|
req, |
|
|
|
|
}) |
|
|
|
|
); |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
async function projectUserDelete(req, res): Promise<any> { |
|
|
|
|
const project_id = req.params.projectId; |
|
|
|
|
|
|
|
|
|
if (req.session?.passport?.user?.id === req.params.userId) { |
|
|
|
|
NcError.badRequest("Admin can't delete themselves!"); |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
if (!req.session?.passport?.user?.roles?.owner) { |
|
|
|
|
const user = await User.get(req.params.userId); |
|
|
|
|
if (user.roles?.split(',').includes('super')) |
|
|
|
|
NcError.forbidden('Insufficient privilege to delete a super admin user.'); |
|
|
|
|
|
|
|
|
|
const projectUser = await ProjectUser.get(project_id, req.params.userId); |
|
|
|
|
if (projectUser?.roles?.split(',').includes('super')) |
|
|
|
|
NcError.forbidden('Insufficient privilege to delete a owner user.'); |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
await ProjectUser.delete(project_id, req.params.userId); |
|
|
|
|
await projectUserService.projectUserDelete({ |
|
|
|
|
projectId: req.params.projectId, |
|
|
|
|
userId: req.params.userId, |
|
|
|
|
req, |
|
|
|
|
}); |
|
|
|
|
res.json({ |
|
|
|
|
msg: 'success', |
|
|
|
|
}); |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
async function projectUserInviteResend(req, res): Promise<any> { |
|
|
|
|
const user = await User.get(req.params.userId); |
|
|
|
|
|
|
|
|
|
if (!user) { |
|
|
|
|
NcError.badRequest(`User with id '${req.params.userId}' not found`); |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
req.body.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, req); |
|
|
|
|
|
|
|
|
|
await Audit.insert({ |
|
|
|
|
op_type: 'AUTHENTICATION', |
|
|
|
|
op_sub_type: 'RESEND_INVITE', |
|
|
|
|
user: user.email, |
|
|
|
|
description: `resent a invite to ${user.email} `, |
|
|
|
|
ip: req.clientIp, |
|
|
|
|
project_id: req.params.projectId, |
|
|
|
|
}); |
|
|
|
|
|
|
|
|
|
res.json({ msg: 'success' }); |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
export async function sendInviteEmail( |
|
|
|
|
email: string, |
|
|
|
|
token: string, |
|
|
|
|
req: any |
|
|
|
|
): Promise<any> { |
|
|
|
|
try { |
|
|
|
|
const template = (await import('./userController/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; |
|
|
|
|
} |
|
|
|
|
res.json( |
|
|
|
|
await projectUserService.projectUserInviteResend({ |
|
|
|
|
projectId: req.params.projectId, |
|
|
|
|
userId: req.params.userId, |
|
|
|
|
projectUser: req.body, |
|
|
|
|
req, |
|
|
|
|
}) |
|
|
|
|
); |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
const router = Router({ mergeParams: true }); |
|
|
|
@ -311,13 +65,11 @@ router.get(
|
|
|
|
|
router.post( |
|
|
|
|
'/api/v1/db/meta/projects/:projectId/users', |
|
|
|
|
metaApiMetrics, |
|
|
|
|
getAjvValidatorMw('swagger.json#/components/schemas/ProjectUserReq'), |
|
|
|
|
ncMetaAclMw(userInvite, 'userInvite') |
|
|
|
|
); |
|
|
|
|
router.patch( |
|
|
|
|
'/api/v1/db/meta/projects/:projectId/users/:userId', |
|
|
|
|
metaApiMetrics, |
|
|
|
|
getAjvValidatorMw('swagger.json#/components/schemas/ProjectUserReq'), |
|
|
|
|
ncMetaAclMw(projectUserUpdate, 'projectUserUpdate') |
|
|
|
|
); |
|
|
|
|
router.delete( |
|
|
|
|