mirror of https://github.com/nocodb/nocodb
Pranav C
2 years ago
6 changed files with 464 additions and 1 deletions
@ -0,0 +1,20 @@ |
|||||||
|
import { Test, TestingModule } from '@nestjs/testing'; |
||||||
|
import { OrgUsersController } from './org-users.controller'; |
||||||
|
import { OrgUsersService } from './org-users.service'; |
||||||
|
|
||||||
|
describe('OrgUsersController', () => { |
||||||
|
let controller: OrgUsersController; |
||||||
|
|
||||||
|
beforeEach(async () => { |
||||||
|
const module: TestingModule = await Test.createTestingModule({ |
||||||
|
controllers: [OrgUsersController], |
||||||
|
providers: [OrgUsersService], |
||||||
|
}).compile(); |
||||||
|
|
||||||
|
controller = module.get<OrgUsersController>(OrgUsersController); |
||||||
|
}); |
||||||
|
|
||||||
|
it('should be defined', () => { |
||||||
|
expect(controller).toBeDefined(); |
||||||
|
}); |
||||||
|
}); |
@ -0,0 +1,146 @@ |
|||||||
|
import { |
||||||
|
Body, |
||||||
|
Controller, |
||||||
|
Delete, |
||||||
|
Get, |
||||||
|
Param, |
||||||
|
Patch, |
||||||
|
Post, |
||||||
|
Request, |
||||||
|
} from '@nestjs/common'; |
||||||
|
import { OrgUserRoles } from 'nocodb-sdk'; |
||||||
|
import { PagedResponseImpl } from '../../helpers/PagedResponse'; |
||||||
|
import { Acl } from '../../middlewares/extract-project-id/extract-project-id.middleware'; |
||||||
|
import { User } from '../../models'; |
||||||
|
import { OrgUsersService } from './org-users.service'; |
||||||
|
|
||||||
|
@Controller('org-users') |
||||||
|
export class OrgUsersController { |
||||||
|
constructor(private readonly orgUsersService: OrgUsersService) {} |
||||||
|
|
||||||
|
@Get('/api/v1/users') |
||||||
|
@Acl('userList', { |
||||||
|
allowedRoles: [OrgUserRoles.SUPER_ADMIN], |
||||||
|
blockApiTokenAccess: true, |
||||||
|
}) |
||||||
|
async userList(@Request() req) { |
||||||
|
return new PagedResponseImpl( |
||||||
|
await this.orgUsersService.userList({ |
||||||
|
query: req.query, |
||||||
|
}), |
||||||
|
{ |
||||||
|
...req.query, |
||||||
|
// todo: fix - wrong count
|
||||||
|
count: await User.count(req.query), |
||||||
|
}, |
||||||
|
); |
||||||
|
} |
||||||
|
|
||||||
|
@Patch('/api/v1/users/:userId') |
||||||
|
@Acl('userUpdate', { |
||||||
|
allowedRoles: [OrgUserRoles.SUPER_ADMIN], |
||||||
|
blockApiTokenAccess: true, |
||||||
|
}) |
||||||
|
async userUpdate(@Body() body, @Param('userId') userId: string) { |
||||||
|
return; |
||||||
|
await this.orgUsersService.userUpdate({ |
||||||
|
user: body, |
||||||
|
userId, |
||||||
|
}); |
||||||
|
} |
||||||
|
|
||||||
|
@Delete('/api/v1/users/:userId') |
||||||
|
@Acl('userDelete', { |
||||||
|
allowedRoles: [OrgUserRoles.SUPER_ADMIN], |
||||||
|
blockApiTokenAccess: true, |
||||||
|
}) |
||||||
|
async userDelete(@Param('userId') userId: string) { |
||||||
|
await this.orgUsersService.userDelete({ |
||||||
|
userId, |
||||||
|
}); |
||||||
|
return { msg: 'The user has been deleted successfully' }; |
||||||
|
} |
||||||
|
|
||||||
|
@Post('/api/v1/users') |
||||||
|
@Acl('userAdd', { |
||||||
|
allowedRoles: [OrgUserRoles.SUPER_ADMIN], |
||||||
|
blockApiTokenAccess: true, |
||||||
|
}) |
||||||
|
async userAdd( |
||||||
|
@Body() body, |
||||||
|
@Request() req, |
||||||
|
@Param('projectId') projectId: string, |
||||||
|
) { |
||||||
|
const result = await this.orgUsersService.userAdd({ |
||||||
|
user: req.body, |
||||||
|
req, |
||||||
|
projectId, |
||||||
|
}); |
||||||
|
|
||||||
|
return result; |
||||||
|
} |
||||||
|
|
||||||
|
@Post('/api/v1/users/settings') |
||||||
|
@Acl('userSettings', { |
||||||
|
allowedRoles: [OrgUserRoles.SUPER_ADMIN], |
||||||
|
blockApiTokenAccess: true, |
||||||
|
}) |
||||||
|
async userSettings(@Body() body): Promise<any> { |
||||||
|
await this.orgUsersService.userSettings(body); |
||||||
|
return {}; |
||||||
|
} |
||||||
|
|
||||||
|
@Post('/api/v1/users/:userId/resend-invite') |
||||||
|
@Acl('userInviteResend', { |
||||||
|
allowedRoles: [OrgUserRoles.SUPER_ADMIN], |
||||||
|
blockApiTokenAccess: true, |
||||||
|
}) |
||||||
|
async userInviteResend( |
||||||
|
@Request() req, |
||||||
|
@Param('userId') userId: string, |
||||||
|
): Promise<any> { |
||||||
|
await this.orgUsersService.userInviteResend({ |
||||||
|
userId, |
||||||
|
req, |
||||||
|
}); |
||||||
|
|
||||||
|
return { msg: 'The invitation has been sent to the user' }; |
||||||
|
} |
||||||
|
|
||||||
|
@Post('/api/v1/users/:userId/generate-reset-url') |
||||||
|
@Acl('generateResetUrl', { |
||||||
|
allowedRoles: [OrgUserRoles.SUPER_ADMIN], |
||||||
|
blockApiTokenAccess: true, |
||||||
|
}) |
||||||
|
async generateResetUrl(@Request() req, @Param('userId') userId: string) { |
||||||
|
const result = await this.orgUsersService.generateResetUrl({ |
||||||
|
siteUrl: req.ncSiteUrl, |
||||||
|
userId, |
||||||
|
}); |
||||||
|
|
||||||
|
return result; |
||||||
|
} |
||||||
|
|
||||||
|
@Get('/api/v1/app-settings') |
||||||
|
@Acl('appSettingsGet', { |
||||||
|
allowedRoles: [OrgUserRoles.SUPER_ADMIN], |
||||||
|
blockApiTokenAccess: true, |
||||||
|
}) |
||||||
|
async appSettingsGet() { |
||||||
|
const settings = await this.orgUsersService.appSettingsGet(); |
||||||
|
return settings; |
||||||
|
} |
||||||
|
|
||||||
|
@Post('/api/v1/app-settings') |
||||||
|
@Acl('appSettingsSet', { |
||||||
|
allowedRoles: [OrgUserRoles.SUPER_ADMIN], |
||||||
|
blockApiTokenAccess: true, |
||||||
|
}) |
||||||
|
async appSettingsSet(req, res) { |
||||||
|
await this.orgUsersService.appSettingsSet({ |
||||||
|
settings: req.body, |
||||||
|
}); |
||||||
|
|
||||||
|
res.json({ msg: 'The app settings have been saved' }); |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,9 @@ |
|||||||
|
import { Module } from '@nestjs/common'; |
||||||
|
import { OrgUsersService } from './org-users.service'; |
||||||
|
import { OrgUsersController } from './org-users.controller'; |
||||||
|
|
||||||
|
@Module({ |
||||||
|
controllers: [OrgUsersController], |
||||||
|
providers: [OrgUsersService] |
||||||
|
}) |
||||||
|
export class OrgUsersModule {} |
@ -0,0 +1,18 @@ |
|||||||
|
import { Test, TestingModule } from '@nestjs/testing'; |
||||||
|
import { OrgUsersService } from './org-users.service'; |
||||||
|
|
||||||
|
describe('OrgUsersService', () => { |
||||||
|
let service: OrgUsersService; |
||||||
|
|
||||||
|
beforeEach(async () => { |
||||||
|
const module: TestingModule = await Test.createTestingModule({ |
||||||
|
providers: [OrgUsersService], |
||||||
|
}).compile(); |
||||||
|
|
||||||
|
service = module.get<OrgUsersService>(OrgUsersService); |
||||||
|
}); |
||||||
|
|
||||||
|
it('should be defined', () => { |
||||||
|
expect(service).toBeDefined(); |
||||||
|
}); |
||||||
|
}); |
@ -0,0 +1,269 @@ |
|||||||
|
import { Injectable } from '@nestjs/common'; |
||||||
|
import { |
||||||
|
AuditOperationSubTypes, |
||||||
|
AuditOperationTypes, |
||||||
|
OrgUserRoles, |
||||||
|
PluginCategory, |
||||||
|
UserType, |
||||||
|
} from 'nocodb-sdk'; |
||||||
|
import { sendInviteEmail } from '../../../../nocodb/src/lib/services/projectUser.svc'; |
||||||
|
import { NC_APP_SETTINGS } from '../../constants'; |
||||||
|
import { validatePayload } from '../../helpers'; |
||||||
|
import { NcError } from '../../helpers/catchError'; |
||||||
|
import { extractProps } from '../../helpers/extractProps'; |
||||||
|
import { randomTokenString } from '../../helpers/stringHelpers'; |
||||||
|
import { Audit, ProjectUser, Store, SyncSource, User } from '../../models'; |
||||||
|
import validator from 'validator'; |
||||||
|
|
||||||
|
import { v4 as uuidv4 } from 'uuid'; |
||||||
|
import Noco from '../../Noco'; |
||||||
|
import { MetaTable } from '../../utils/globals'; |
||||||
|
import { T } from 'nc-help'; |
||||||
|
|
||||||
|
@Injectable() |
||||||
|
export class OrgUsersService { |
||||||
|
async userList(param: { |
||||||
|
// todo: add better typing
|
||||||
|
query: Record<string, any>; |
||||||
|
}) { |
||||||
|
return await User.list(param.query); |
||||||
|
} |
||||||
|
|
||||||
|
async userUpdate(param: { |
||||||
|
// todo: better typing
|
||||||
|
user: Partial<UserType>; |
||||||
|
userId: string; |
||||||
|
}) { |
||||||
|
validatePayload('swagger.json#/components/schemas/OrgUserReq', param.user); |
||||||
|
|
||||||
|
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, |
||||||
|
}); |
||||||
|
} |
||||||
|
|
||||||
|
async 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; |
||||||
|
} |
||||||
|
|
||||||
|
async userAdd(param: { |
||||||
|
user: UserType; |
||||||
|
projectId: string; |
||||||
|
// todo: refactor
|
||||||
|
req: any; |
||||||
|
}) { |
||||||
|
validatePayload('swagger.json#/components/schemas/OrgUserReq', param.user); |
||||||
|
|
||||||
|
// 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(); |
||||||
|
T.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 }; |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
async userSettings(_param): Promise<any> { |
||||||
|
NcError.notImplemented(); |
||||||
|
} |
||||||
|
|
||||||
|
async userInviteResend(param: { userId: string; req: any }): Promise<any> { |
||||||
|
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; |
||||||
|
} |
||||||
|
|
||||||
|
async 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}`, |
||||||
|
}; |
||||||
|
} |
||||||
|
|
||||||
|
async appSettingsGet() { |
||||||
|
let settings = {}; |
||||||
|
try { |
||||||
|
settings = JSON.parse((await Store.get(NC_APP_SETTINGS))?.value); |
||||||
|
} catch {} |
||||||
|
return settings; |
||||||
|
} |
||||||
|
|
||||||
|
async appSettingsSet(param: { settings: any }) { |
||||||
|
await Store.saveOrUpdate({ |
||||||
|
value: JSON.stringify(param.settings), |
||||||
|
key: NC_APP_SETTINGS, |
||||||
|
}); |
||||||
|
return true; |
||||||
|
} |
||||||
|
} |
Loading…
Reference in new issue