Browse Source

feat: org users apis

Signed-off-by: Pranav C <pranavxc@gmail.com>
pull/5444/head
Pranav C 1 year ago
parent
commit
86d2496479
  1. 3
      packages/nocodb-nest/src/app.module.ts
  2. 20
      packages/nocodb-nest/src/modules/org-users/org-users.controller.spec.ts
  3. 146
      packages/nocodb-nest/src/modules/org-users/org-users.controller.ts
  4. 9
      packages/nocodb-nest/src/modules/org-users/org-users.module.ts
  5. 18
      packages/nocodb-nest/src/modules/org-users/org-users.service.spec.ts
  6. 269
      packages/nocodb-nest/src/modules/org-users/org-users.service.ts

3
packages/nocodb-nest/src/app.module.ts

@ -32,9 +32,10 @@ import { ApiTokensModule } from './modules/api-tokens/api-tokens.module';
import { AttachmentsModule } from './modules/attachments/attachments.module';
import { OrgLcenseModule } from './modules/org-lcense/org-lcense.module';
import { OrgTokensModule } from './modules/org-tokens/org-tokens.module';
import { OrgUsersModule } from './modules/org-users/org-users.module';
@Module({
imports: [AuthModule, UsersModule, UtilsModule, ProjectsModule, TablesModule, ViewsModule, FiltersModule, SortsModule, ColumnsModule, ViewColumnsModule, BasesModule, HooksModule, SharedBasesModule, FormsModule, GridsModule, KanbansModule, GalleriesModule, FormColumnsModule, GridColumnsModule, MapsModule, ProjectUsersModule, ModelVisibilitiesModule, HookFiltersModule, ApiTokensModule, AttachmentsModule, OrgLcenseModule, OrgTokensModule],
imports: [AuthModule, UsersModule, UtilsModule, ProjectsModule, TablesModule, ViewsModule, FiltersModule, SortsModule, ColumnsModule, ViewColumnsModule, BasesModule, HooksModule, SharedBasesModule, FormsModule, GridsModule, KanbansModule, GalleriesModule, FormColumnsModule, GridColumnsModule, MapsModule, ProjectUsersModule, ModelVisibilitiesModule, HookFiltersModule, ApiTokensModule, AttachmentsModule, OrgLcenseModule, OrgTokensModule, OrgUsersModule],
controllers: [],
providers: [Connection, MetaService, JwtStrategy, ExtractProjectIdMiddleware],
exports: [Connection, MetaService],

20
packages/nocodb-nest/src/modules/org-users/org-users.controller.spec.ts

@ -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();
});
});

146
packages/nocodb-nest/src/modules/org-users/org-users.controller.ts

@ -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' });
}
}

9
packages/nocodb-nest/src/modules/org-users/org-users.module.ts

@ -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 {}

18
packages/nocodb-nest/src/modules/org-users/org-users.service.spec.ts

@ -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();
});
});

269
packages/nocodb-nest/src/modules/org-users/org-users.service.ts

@ -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…
Cancel
Save