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