Browse Source

Merge branch 'develop' into refactor/lint

pull/5773/head
Wing-Kam Wong 2 years ago
parent
commit
8aa9dd614d
  1. 9
      packages/nc-gui/pages/index/index/index.vue
  2. 2
      packages/nocodb-sdk/src/lib/Api.ts
  3. 4
      packages/nocodb/src/cache/RedisCacheMgr.ts
  4. 75
      packages/nocodb/src/controllers/projects.controller.ts
  5. 111
      packages/nocodb/src/controllers/users/users.controller.ts
  6. 20
      packages/nocodb/src/models/ProjectUser.ts
  7. 4
      packages/nocodb/src/schema/swagger.json
  8. 4
      packages/nocodb/src/services/users/users.service.ts

9
packages/nc-gui/pages/index/index/index.vue

@ -308,6 +308,7 @@ const copyProjectMeta = async () => {
<div v-if="record.status !== ProjectStatus.JOB" class="flex items-center gap-2"> <div v-if="record.status !== ProjectStatus.JOB" class="flex items-center gap-2">
<component <component
:is="iconMap.edit" :is="iconMap.edit"
v-if="isUIAllowed('projectUpdate', true)"
v-e="['c:project:edit:rename']" v-e="['c:project:edit:rename']"
class="nc-action-btn" class="nc-action-btn"
@click.stop="navigateTo(`/${text}`)" @click.stop="navigateTo(`/${text}`)"
@ -315,12 +316,18 @@ const copyProjectMeta = async () => {
<component <component
:is="iconMap.delete" :is="iconMap.delete"
v-if="isUIAllowed('projectDelete', true)"
class="nc-action-btn" class="nc-action-btn"
:data-testid="`delete-project-${record.title}`" :data-testid="`delete-project-${record.title}`"
@click.stop="deleteProject(record)" @click.stop="deleteProject(record)"
/> />
<a-dropdown :trigger="['click']" overlay-class-name="nc-dropdown-import-menu" @click.stop> <a-dropdown
v-if="isUIAllowed('duplicateProject', true)"
:trigger="['click']"
overlay-class-name="nc-dropdown-import-menu"
@click.stop
>
<GeneralIcon <GeneralIcon
icon="threeDotVertical" icon="threeDotVertical"
class="nc-import-menu outline-0" class="nc-import-menu outline-0"

2
packages/nocodb-sdk/src/lib/Api.ts

@ -2319,6 +2319,8 @@ export interface UserType {
* @example org-level-viewer * @example org-level-viewer
*/ */
roles?: string; roles?: string;
/** Access token version */
token_version?: string;
} }
/** /**

4
packages/nocodb/src/cache/RedisCacheMgr.ts vendored

@ -135,7 +135,7 @@ export default class RedisCacheMgr extends CacheMgr {
// e.g. arr = ["nc:<orgs>:<scope>:<model_id_1>", "nc:<orgs>:<scope>:<model_id_2>"] // e.g. arr = ["nc:<orgs>:<scope>:<model_id_1>", "nc:<orgs>:<scope>:<model_id_2>"]
const arr = (await this.get(key, CacheGetType.TYPE_ARRAY)) || []; const arr = (await this.get(key, CacheGetType.TYPE_ARRAY)) || [];
log(`RedisCacheMgr::getList: getting list with key ${key}`); log(`RedisCacheMgr::getList: getting list with key ${key}`);
const isNoneList = arr.length && arr[0] === 'NONE'; const isNoneList = arr.length && arr.includes('NONE');
if (isNoneList) { if (isNoneList) {
return Promise.resolve({ return Promise.resolve({
@ -248,7 +248,7 @@ export default class RedisCacheMgr extends CacheMgr {
: `${this.prefix}:${scope}:${subListKeys.join(':')}:list`; : `${this.prefix}:${scope}:${subListKeys.join(':')}:list`;
log(`RedisCacheMgr::appendToList: append key ${key} to ${listKey}`); log(`RedisCacheMgr::appendToList: append key ${key} to ${listKey}`);
let list = (await this.get(listKey, CacheGetType.TYPE_ARRAY)) || []; let list = (await this.get(listKey, CacheGetType.TYPE_ARRAY)) || [];
if (list.length && list[0] === 'NONE') { if (list.length && list.includes('NONE')) {
list = []; list = [];
await this.del(listKey); await this.del(listKey);
} }

75
packages/nocodb/src/controllers/projects.controller.ts

@ -17,7 +17,7 @@ import { GlobalGuard } from '../guards/global/global.guard';
import { PagedResponseImpl } from '../helpers/PagedResponse'; import { PagedResponseImpl } from '../helpers/PagedResponse';
import { import {
ExtractProjectIdMiddleware, ExtractProjectIdMiddleware,
UseAclMiddleware, Acl,
} from '../middlewares/extract-project-id/extract-project-id.middleware'; } from '../middlewares/extract-project-id/extract-project-id.middleware';
import Noco from '../Noco'; import Noco from '../Noco';
import { packageVersion } from '../utils/packageVersion'; import { packageVersion } from '../utils/packageVersion';
@ -29,9 +29,7 @@ import type { ProjectType } from 'nocodb-sdk';
export class ProjectsController { export class ProjectsController {
constructor(private readonly projectsService: ProjectsService) {} constructor(private readonly projectsService: ProjectsService) {}
@UseAclMiddleware({ @Acl('projectList')
permissionName: 'projectList',
})
@Get('/api/v1/db/meta/projects/') @Get('/api/v1/db/meta/projects/')
async list(@Query() queryParams: Record<string, any>, @Request() req) { async list(@Query() queryParams: Record<string, any>, @Request() req) {
const projects = await this.projectsService.projectList({ const projects = await this.projectsService.projectList({
@ -55,7 +53,7 @@ export class ProjectsController {
PackageVersion: packageVersion, PackageVersion: packageVersion,
}; };
} }
@Acl('projectGet')
@Get('/api/v1/db/meta/projects/:projectId') @Get('/api/v1/db/meta/projects/:projectId')
async projectGet(@Param('projectId') projectId: string) { async projectGet(@Param('projectId') projectId: string) {
const project = await this.projectsService.getProjectWithInfo({ const project = await this.projectsService.getProjectWithInfo({
@ -66,7 +64,7 @@ export class ProjectsController {
return project; return project;
} }
@Acl('projectUpdate')
@Patch('/api/v1/db/meta/projects/:projectId') @Patch('/api/v1/db/meta/projects/:projectId')
async projectUpdate( async projectUpdate(
@Param('projectId') projectId: string, @Param('projectId') projectId: string,
@ -80,6 +78,7 @@ export class ProjectsController {
return project; return project;
} }
@Acl('projectDelete')
@Delete('/api/v1/db/meta/projects/:projectId') @Delete('/api/v1/db/meta/projects/:projectId')
async projectDelete(@Param('projectId') projectId: string) { async projectDelete(@Param('projectId') projectId: string) {
const deleted = await this.projectsService.projectSoftDelete({ const deleted = await this.projectsService.projectSoftDelete({
@ -89,6 +88,7 @@ export class ProjectsController {
return deleted; return deleted;
} }
@Acl('projectCreate')
@Post('/api/v1/db/meta/projects') @Post('/api/v1/db/meta/projects')
@HttpCode(200) @HttpCode(200)
async projectCreate(@Body() projectBody: ProjectReqType, @Request() req) { async projectCreate(@Body() projectBody: ProjectReqType, @Request() req) {
@ -100,66 +100,3 @@ export class ProjectsController {
return project; return project;
} }
} }
/*
// // Project CRUD
export async function projectCost(req, res) {
let cost = 0;
const project = await Project.getWithInfo(req.params.projectId);
for (const base of project.bases) {
const sqlClient = await NcConnectionMgrv2.getSqlClient(base);
const userCount = await ProjectUser.getUsersCount(req.query);
const recordCount = (await sqlClient.totalRecords())?.data.TotalRecords;
if (recordCount > 100000) {
// 36,000 or $79/user/month
cost = Math.max(36000, 948 * userCount);
} else if (recordCount > 50000) {
// $36,000 or $50/user/month
cost = Math.max(36000, 600 * userCount);
} else if (recordCount > 10000) {
// $240/user/yr
cost = Math.min(240 * userCount, 36000);
} else if (recordCount > 1000) {
// $120/user/yr
cost = Math.min(120 * userCount, 36000);
}
}
T.event({
event: 'a:project:cost',
data: {
cost,
},
});
res.json({ cost });
}
export async function hasEmptyOrNullFilters(req, res) {
res.json(await Filter.hasEmptyOrNullFilters(req.params.projectId));
}
export default (router) => {
router.get(
'/api/v1/db/meta/projects/:projectId/cost',
metaApiMetrics,
ncMetaAclMw(projectCost, 'projectCost')
);
router.get(
'/api/v1/db/meta/projects/:projectId/has-empty-or-null-filters',
metaApiMetrics,
ncMetaAclMw(hasEmptyOrNullFilters, 'hasEmptyOrNullFilters')
);
};
* */

111
packages/nocodb/src/controllers/users/users.controller.ts

@ -1,5 +1,3 @@
import { promisify } from 'util';
import { AuditOperationSubTypes, AuditOperationTypes } from 'nocodb-sdk';
import { import {
Body, Body,
Controller, Controller,
@ -19,23 +17,17 @@ import {
Acl, Acl,
ExtractProjectIdMiddleware, ExtractProjectIdMiddleware,
} from '../../middlewares/extract-project-id/extract-project-id.middleware'; } from '../../middlewares/extract-project-id/extract-project-id.middleware';
import Noco from '../../Noco'; import { User } from '../../models';
import { GoogleStrategy } from '../../strategies/google.strategy/google.strategy';
import extractRolesObj from '../../utils/extractRolesObj';
import { Audit, User } from '../../models';
import { import {
genJwt,
randomTokenString, randomTokenString,
setTokenCookie, setTokenCookie,
} from '../../services/users/helpers'; } from '../../services/users/helpers';
import { UsersService } from '../../services/users/users.service'; import { UsersService } from '../../services/users/users.service';
import extractRolesObj from '../../utils/extractRolesObj';
@Controller() @Controller()
export class UsersController { export class UsersController {
constructor( constructor(private readonly usersService: UsersService) {}
private readonly usersService: UsersService,
private googleStrategy: GoogleStrategy,
) {}
@Post([ @Post([
'/auth/user/signup', '/auth/user/signup',
@ -59,56 +51,14 @@ export class UsersController {
'/api/v1/auth/token/refresh', '/api/v1/auth/token/refresh',
]) ])
@HttpCode(200) @HttpCode(200)
async refreshToken(@Request() req: any, @Request() res: any): Promise<any> { async refreshToken(@Request() req: any, @Response() res: any): Promise<any> {
return await this.usersService.refreshToken({ res.json(
body: req.body, await this.usersService.refreshToken({
req, body: req.body,
res, req,
}); res,
} }),
);
async successfulSignIn({ user, err, info, req, res, auditDescription }) {
try {
if (!user || !user.email) {
if (err) {
return res.status(400).send(err);
}
if (info) {
return res.status(400).send(info);
}
return res.status(400).send({ msg: 'Your signin has failed' });
}
await promisify((req as any).login.bind(req))(user);
const refreshToken = randomTokenString();
if (!user.token_version) {
user.token_version = randomTokenString();
}
await User.update(user.id, {
refresh_token: refreshToken,
email: user.email,
token_version: user.token_version,
});
setTokenCookie(res, refreshToken);
await Audit.insert({
op_type: AuditOperationTypes.AUTHENTICATION,
op_sub_type: AuditOperationSubTypes.SIGNIN,
user: user.email,
ip: req.clientIp,
description: auditDescription,
});
res.json({
token: genJwt(user, Noco.getConfig()),
} as any);
} catch (e) {
console.log(e);
throw e;
}
} }
@Post([ @Post([
@ -118,8 +68,9 @@ export class UsersController {
]) ])
@UseGuards(AuthGuard('local')) @UseGuards(AuthGuard('local'))
@HttpCode(200) @HttpCode(200)
async signin(@Request() req) { async signin(@Request() req, @Response() res) {
return this.usersService.login(req.user); await this.setRefreshToken({ req, res });
res.json(this.usersService.login(req.user));
} }
@Post('/api/v1/auth/user/signout') @Post('/api/v1/auth/user/signout')
@ -136,18 +87,15 @@ export class UsersController {
@Post(`/auth/google/genTokenByCode`) @Post(`/auth/google/genTokenByCode`)
@HttpCode(200) @HttpCode(200)
@UseGuards(AuthGuard('google')) @UseGuards(AuthGuard('google'))
async googleSignin(@Request() req) { async googleSignin(@Request() req, @Response() res) {
return this.usersService.login(req.user); await this.setRefreshToken({ req, res });
res.json(this.usersService.login(req.user));
} }
@Get('/auth/google') @Get('/auth/google')
@UseGuards(AuthGuard('google')) @UseGuards(AuthGuard('google'))
googleAuthenticate(@Request() req) { googleAuthenticate(@Request() req) {
// this.googleStrategy.authenticate(req, { // google strategy will take care the request
// scope: ['profile', 'email'],
// state: req.query.state,
// callbackURL: req.ncSiteUrl + Noco.getConfig().dashboardPath,
// });
} }
@Get(['/auth/user/me', '/api/v1/db/auth/user/me', '/api/v1/auth/user/me']) @Get(['/auth/user/me', '/api/v1/db/auth/user/me', '/api/v1/auth/user/me'])
@ -269,4 +217,27 @@ export class UsersController {
return res.status(400).json({ msg: e.message }); return res.status(400).json({ msg: e.message });
} }
} }
async setRefreshToken({ res, req }) {
const userId = req.user?.id;
if (!userId) return;
const user = await User.get(userId);
if (!user) return;
const refreshToken = randomTokenString();
if (!user.token_version) {
user.token_version = randomTokenString();
}
await User.update(user.id, {
refresh_token: refreshToken,
email: user.email,
token_version: user.token_version,
});
setTokenCookie(res, refreshToken);
}
} }

20
packages/nocodb/src/models/ProjectUser.ts

@ -174,11 +174,6 @@ export default class ProjectUser {
} }
static async delete(projectId: string, userId: string, ncMeta = Noco.ncMeta) { static async delete(projectId: string, userId: string, ncMeta = Noco.ncMeta) {
// await NocoCache.deepDel(
// CacheScope.PROJECT_USER,
// `${CacheScope.PROJECT_USER}:${projectId}:${userId}`,
// CacheDelDirection.CHILD_TO_PARENT
// );
const { email } = await ncMeta.metaGet2(null, null, MetaTable.USERS, { const { email } = await ncMeta.metaGet2(null, null, MetaTable.USERS, {
id: userId, id: userId,
}); });
@ -194,11 +189,16 @@ export default class ProjectUser {
const { isNoneList } = cachedList; const { isNoneList } = cachedList;
if (!isNoneList && cachedProjectList?.length) { if (!isNoneList && cachedProjectList?.length) {
cachedProjectList = cachedProjectList.filter((p) => p.id !== projectId); cachedProjectList = cachedProjectList.filter((p) => p.id !== projectId);
await NocoCache.setList( // delete the whole list first so that the old one won't be included
CacheScope.USER_PROJECT, await NocoCache.del(`${CacheScope.USER_PROJECT}:${userId}:list`);
[userId], if (cachedProjectList.length > 0) {
cachedProjectList, // set the updated list (i.e. excluding the to-be-deleted project id)
); await NocoCache.setList(
CacheScope.USER_PROJECT,
[userId],
cachedProjectList,
);
}
} }
await NocoCache.del(`${CacheScope.PROJECT_USER}:${projectId}:${userId}`); await NocoCache.del(`${CacheScope.PROJECT_USER}:${projectId}:${userId}`);

4
packages/nocodb/src/schema/swagger.json

@ -20014,6 +20014,10 @@
"description": "The roles of the user", "description": "The roles of the user",
"example": "org-level-viewer", "example": "org-level-viewer",
"type": "string" "type": "string"
},
"token_version": {
"description": "Access token version",
"type": "string"
} }
}, },
"required": ["email", "email_verified", "firstname", "id", "lastname"], "required": ["email", "email_verified", "firstname", "id", "lastname"],

4
packages/nocodb/src/services/users/users.service.ts

@ -486,9 +486,9 @@ export class UsersService {
return this.login(user); return this.login(user);
} }
async login(user: any) { login(user: UserType) {
return { return {
token: genJwt(user, Noco.getConfig()), //this.jwtService.sign(payload), token: genJwt(user, Noco.getConfig()),
}; };
} }

Loading…
Cancel
Save