Browse Source

refactor: attachment, orguser, orgtoken, appsettings and projectuser

Signed-off-by: Pranav C <pranavxc@gmail.com>
pull/5239/head
Pranav C 2 years ago
parent
commit
88365ec683
  1. 11
      packages/nocodb/src/lib/controllers/orgLicenseController.ts
  2. 238
      packages/nocodb/src/lib/controllers/orgUserController.ts
  3. 118
      packages/nocodb/src/lib/services/attachmentService.ts
  4. 4
      packages/nocodb/src/lib/services/index.ts
  5. 17
      packages/nocodb/src/lib/services/orgLicenseService.ts
  6. 53
      packages/nocodb/src/lib/services/orgTokenService.ts
  7. 2
      packages/nocodb/src/lib/services/orgUserService.ts
  8. 315
      packages/nocodb/src/lib/services/projectUserService.ts

11
packages/nocodb/src/lib/controllers/orgLicenseController.ts

@ -1,21 +1,16 @@
import { Router } from 'express';
import { OrgUserRoles } from 'nocodb-sdk';
import { NC_LICENSE_KEY } from '../constants';
import Store from '../models/Store';
import Noco from '../Noco';
import { metaApiMetrics } from '../meta/helpers/apiMetrics';
import ncMetaAclMw from '../meta/helpers/ncMetaAclMw';
import { getAjvValidatorMw } from '../meta/api/helpers';
import { orgLicenseService } from '../services';
async function licenseGet(_req, res) {
const license = await Store.get(NC_LICENSE_KEY);
res.json({ key: license?.value });
res.json(await orgLicenseService.licenseGet());
}
async function licenseSet(req, res) {
await Store.saveOrUpdate({ value: req.body.key, key: NC_LICENSE_KEY });
await Noco.loadEEState();
await orgLicenseService.licenseSet({ key: req.body.key })
res.json({ msg: 'License key saved' });
}

238
packages/nocodb/src/lib/controllers/orgUserController.ts

@ -1,255 +1,75 @@
import { Router } from 'express';
import {
AuditOperationSubTypes,
AuditOperationTypes,
PluginCategory,
} from 'nocodb-sdk';
import { v4 as uuidv4 } from 'uuid';
import validator from 'validator';
import { OrgUserRoles } from 'nocodb-sdk';
import { NC_APP_SETTINGS } from '../constants';
import Audit from '../models/Audit';
import ProjectUser from '../models/ProjectUser';
import Store from '../models/Store';
import SyncSource from '../models/SyncSource';
import User from '../models/User';
import Noco from '../Noco';
import { MetaTable } from '../utils/globals';
import { Tele } from 'nc-help';
import { metaApiMetrics } from '../meta/helpers/apiMetrics';
import { NcError } from '../meta/helpers/catchError';
import { extractProps } from '../meta/helpers/extractProps';
import ncMetaAclMw from '../meta/helpers/ncMetaAclMw';
import { PagedResponseImpl } from '../meta/helpers/PagedResponse';
import { randomTokenString } from '../meta/helpers/stringHelpers';
import { getAjvValidatorMw } from '../meta/api/helpers';
import { sendInviteEmail } from './projectUserController';
import { orgUserService } from '../services';
async function userList(req, res) {
res.json(
new PagedResponseImpl(await User.list(req.query), {
...req.query,
count: await User.count(req.query),
await orgUserService.userList({
query: req.query,
})
);
}
async function userUpdate(req, res) {
const updateBody = extractProps(req.body, ['roles']);
const user = await User.get(req.params.userId);
if (user.roles.includes(OrgUserRoles.SUPER_ADMIN)) {
NcError.badRequest('Cannot update super admin roles');
}
res.json(
await User.update(req.params.userId, {
...updateBody,
token_version: null,
await orgUserService.userUpdate({
user: req.body,
userId: req.params.userId,
})
);
}
async function userDelete(req, res) {
const ncMeta = await Noco.ncMeta.startTransaction();
try {
const user = await User.get(req.params.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(
req.params.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(req.params.userId, ncMeta);
// delete user
await User.delete(req.params.userId, ncMeta);
await ncMeta.commit();
} catch (e) {
await ncMeta.rollback(e);
throw e;
}
await orgUserService.userDelete({
userId: req.params.userId,
});
res.json({ msg: 'success' });
}
async function userAdd(req, res, next) {
// allow only viewer or creator role
if (
req.body.roles &&
![OrgUserRoles.VIEWER, OrgUserRoles.CREATOR].includes(req.body.roles)
) {
NcError.badRequest('Invalid role');
}
// extract emails from request body
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) {
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: req.body.roles || OrgUserRoles.VIEWER,
token_version: randomTokenString(),
});
const count = await User.count();
Tele.emit('evt', { evt_type: 'org:user:invite', count });
await Audit.insert({
op_type: AuditOperationTypes.ORG_USER,
op_sub_type: AuditOperationSubTypes.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 });
}
}
}
}
async function userAdd(req, res) {
const result = await orgUserService.userAdd({
user: req.body,
req,
projectId: req.params.projectId,
});
if (emails.length === 1) {
res.json({
msg: 'success',
});
} else {
return res.json({ invite_token, emails, error });
}
res.json(result);
}
async function userSettings(_req, _res): Promise<any> {
NcError.notImplemented();
async function userSettings(_req, res): Promise<any> {
await orgUserService.userSettings({});
res.json({});
}
async function userInviteResend(req, res): Promise<any> {
const user = await User.get(req.params.userId);
if (!user) {
NcError.badRequest(`User with id '${req.params.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, 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: req.clientIp,
await orgUserService.userInviteResend({
userId: req.params.userId,
req,
});
res.json({ msg: 'success' });
}
async function generateResetUrl(req, res) {
const user = await User.get(req.params.userId);
if (!user) {
NcError.badRequest(`User with id '${req.params.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,
const result = await orgUserService.generateResetUrl({
siteUrl: req.ncSiteUrl,
userId: req.params.userId,
});
res.json({
reset_password_token: token,
reset_password_url: req.ncSiteUrl + `/auth/password/reset/${token}`,
});
res.json(result);
}
async function appSettingsGet(_req, res) {
let settings = {};
try {
settings = JSON.parse((await Store.get(NC_APP_SETTINGS))?.value);
} catch {}
const settings = await orgUserService.appSettingsGet();
res.json(settings);
}
async function appSettingsSet(req, res) {
await Store.saveOrUpdate({
value: JSON.stringify(req.body),
key: NC_APP_SETTINGS,
await orgUserService.appSettingsSet({
settings: req.body,
});
res.json({ msg: 'Settings saved' });

118
packages/nocodb/src/lib/services/attachmentService.ts

@ -0,0 +1,118 @@
// @ts-ignore
import { Request, Response, Router } from 'express';
import { nanoid } from 'nanoid';
import path from 'path';
import slash from 'slash';
import mimetypes, { mimeIcons } from '../utils/mimeTypes';
import { Tele } from 'nc-help';
import NcPluginMgrv2 from '../meta/helpers/NcPluginMgrv2';
import Local from '../v1-legacy/plugins/adapters/storage/Local';
export async function upload(param: {
path?: string;
// todo: proper type
files: unknown[];
}) {
const filePath = sanitizeUrlPath(param.path?.toString()?.split('/') || ['']);
const destPath = path.join('nc', 'uploads', ...filePath);
const storageAdapter = await NcPluginMgrv2.storageAdapter();
const attachments = await Promise.all(
param.files?.map(async (file: any) => {
const fileName = `${nanoid(18)}${path.extname(file.originalname)}`;
const url = await storageAdapter.fileCreate(
slash(path.join(destPath, fileName)),
file
);
let attachmentPath;
// if `url` is null, then it is local attachment
if (!url) {
// then store the attachement path only
// url will be constructued in `useAttachmentCell`
attachmentPath = `download/${filePath.join('/')}/${fileName}`;
}
return {
...(url ? { url } : {}),
...(attachmentPath ? { path: attachmentPath } : {}),
title: file.originalname,
mimetype: file.mimetype,
size: file.size,
icon: mimeIcons[path.extname(file.originalname).slice(1)] || undefined,
};
})
);
Tele.emit('evt', { evt_type: 'image:uploaded' });
return attachments;
}
export async function uploadViaURL(param: {
path?: string;
urls: {
url: string;
fileName: string;
mimetype?: string;
size?: string | number;
}[];
}) {
const filePath = sanitizeUrlPath(param?.path?.toString()?.split('/') || ['']);
const destPath = path.join('nc', 'uploads', ...filePath);
const storageAdapter = await NcPluginMgrv2.storageAdapter();
const attachments = await Promise.all(
param.urls?.map?.(async (urlMeta) => {
const { url, fileName: _fileName } = urlMeta;
const fileName = `${nanoid(18)}${_fileName || url.split('/').pop()}`;
const attachmentUrl = await (storageAdapter as any).fileCreateByUrl(
slash(path.join(destPath, fileName)),
url
);
let attachmentPath;
// if `attachmentUrl` is null, then it is local attachment
if (!attachmentUrl) {
// then store the attachement path only
// url will be constructued in `useAttachmentCell`
attachmentPath = `download/${filePath.join('/')}/${fileName}`;
}
return {
...(attachmentUrl ? { url: attachmentUrl } : {}),
...(attachmentPath ? { path: attachmentPath } : {}),
title: fileName,
mimetype: urlMeta.mimetype,
size: urlMeta.size,
icon: mimeIcons[path.extname(fileName).slice(1)] || undefined,
};
})
);
Tele.emit('evt', { evt_type: 'image:uploaded' });
return attachments;
}
export async function fileRead(param: { path: string }) {
// get the local storage adapter to display local attachments
const storageAdapter = new Local();
const type =
mimetypes[path.extname(param.path).split('/').pop().slice(1)] ||
'text/plain';
const img = await storageAdapter.fileRead(slash(param.path));
return { img, type };
}
export function sanitizeUrlPath(paths) {
return paths.map((url) => url.replace(/[/.?#]+/g, '_'));
}

4
packages/nocodb/src/lib/services/index.ts

@ -21,3 +21,7 @@ export * as mapViewService from './mapViewService';
export * as modelVisibilityService from './modelVisibilityService';
export * as sharedBaseService from './sharedBaseService';
export * as orgUserService from './orgUserService';
export * as orgTokenService from './orgTokenService';
export * as orgLicenseService from './orgLicenseService';
export * as projectUserService from './projectUserService';
export * as attachmentService from './attachmentService';

17
packages/nocodb/src/lib/services/orgLicenseService.ts

@ -0,0 +1,17 @@
import { NC_LICENSE_KEY } from '../constants';
import Store from '../models/Store';
import Noco from '../Noco';
export async function licenseGet() {
const license = await Store.get(NC_LICENSE_KEY);
return { key: license?.value }
}
export async function licenseSet(param:{
key: string
}) {
await Store.saveOrUpdate({ value: param.key, key: NC_LICENSE_KEY });
await Noco.loadEEState();
return true
}

53
packages/nocodb/src/lib/services/orgTokenService.ts

@ -0,0 +1,53 @@
import { ApiTokenReqType, OrgUserRoles } from 'nocodb-sdk';
import { User } from '../models';
import ApiToken from '../models/ApiToken';
import { Tele } from 'nc-help';
import { NcError } from '../meta/helpers/catchError';
import { PagedResponseImpl } from '../meta/helpers/PagedResponse';
export async function apiTokenList(param: { user: User; query: any }) {
const fk_user_id = param.user.id;
let includeUnmappedToken = false;
if (param.user.roles.includes(OrgUserRoles.SUPER_ADMIN)) {
includeUnmappedToken = true;
}
return new PagedResponseImpl(
await ApiToken.listWithCreatedBy({
...param.query,
fk_user_id,
includeUnmappedToken,
}),
{
...param.query,
count: await ApiToken.count({
includeUnmappedToken,
fk_user_id,
}),
}
);
}
export async function apiTokenCreate(param: {
user: User;
apiToken: ApiTokenReqType;
}) {
Tele.emit('evt', { evt_type: 'org:apiToken:created' });
return await ApiToken.insert({
...param.apiToken,
fk_user_id: param['user'].id,
});
}
export async function apiTokenDelete(param: { user: User; token: string }) {
const fk_user_id = param.user.id;
const apiToken = await ApiToken.getByToken(param.token);
if (
!param.user.roles.includes(OrgUserRoles.SUPER_ADMIN) &&
apiToken.fk_user_id !== fk_user_id
) {
NcError.notFound('Token not found');
}
Tele.emit('evt', { evt_type: 'org:apiToken:deleted' });
return await ApiToken.delete(param.token);
}

2
packages/nocodb/src/lib/services/orgUserService.ts

@ -16,7 +16,7 @@ import { NcError } from '../meta/helpers/catchError';
import { extractProps } from '../meta/helpers/extractProps';
import { PagedResponseImpl } from '../meta/helpers/PagedResponse';
import { randomTokenString } from '../meta/helpers/stringHelpers';
import { sendInviteEmail } from '../meta/api/projectUserApis';
import { sendInviteEmail } from './projectUserService';
export async function userList(param: {
// todo: add better typing

315
packages/nocodb/src/lib/services/projectUserService.ts

@ -0,0 +1,315 @@
import { OrgUserRoles, ProjectUserReqType } from 'nocodb-sdk';
import { Tele } from 'nc-help';
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 { randomTokenString } from '../meta/helpers/stringHelpers';
export async function userList(param: { projectId: string; query: any }) {
return new PagedResponseImpl(
await ProjectUser.getUsersList({
...param.query,
project_id: param.projectId,
}),
{
...param.query,
count: await ProjectUser.getUsersCount(param.query),
}
);
}
export async function userInvite(param: {
projectId: string;
projectUser: ProjectUserReqType;
req: any;
}): Promise<any> {
const emails = (param.projectUser.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(param.projectId, user.id);
if (projectUser) {
NcError.badRequest(
`${user.email} with role ${projectUser.roles} already exists in this project`
);
}
await ProjectUser.insert({
project_id: param.projectId,
fk_user_id: user.id,
roles: param.projectUser.roles || 'editor',
});
const cachedUser = await NocoCache.get(
`${CacheScope.USER}:${email}___${param.projectId}`,
CacheGetType.TYPE_OBJECT
);
if (cachedUser) {
cachedUser.roles = param.projectUser.roles || 'editor';
await NocoCache.set(
`${CacheScope.USER}:${email}___${param.projectId}`,
cachedUser
);
}
await Audit.insert({
project_id: param.projectId,
op_type: 'AUTHENTICATION',
op_sub_type: 'INVITE',
user: param.req.user.email,
description: `invited ${email} to ${param.projectId} project `,
ip: param.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: param.projectId,
fk_user_id: id,
roles: param.projectUser.roles,
});
const count = await User.count();
Tele.emit('evt', { evt_type: 'project:invite', count });
await Audit.insert({
project_id: param.projectId,
op_type: 'AUTHENTICATION',
op_sub_type: '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 };
}
}
export async function projectUserUpdate(param: {
userId: string;
// todo: update swagger
projectUser: ProjectUserReqType & { project_id: string };
// todo: refactor
req: any;
projectId: string;
}): Promise<any> {
// todo: use param.projectId
if (!param.projectUser?.project_id) {
NcError.badRequest('Missing project id in request body.');
}
if (
param.req.session?.passport?.user?.roles?.owner &&
param.req.session?.passport?.user?.id === param.userId &&
param.projectUser.roles.indexOf('owner') === -1
) {
NcError.badRequest("Super admin can't remove Super role themselves");
}
const user = await User.get(param.userId);
if (!user) {
NcError.badRequest(`User with id '${param.userId}' doesn't exist`);
}
// todo: handle roles which contains super
if (
!param.req.session?.passport?.user?.roles?.owner &&
param.projectUser.roles.indexOf('owner') > -1
) {
NcError.forbidden('Insufficient privilege to add super admin role.');
}
await ProjectUser.update(
param.projectId,
param.userId,
param.projectUser.roles
);
await Audit.insert({
op_type: 'AUTHENTICATION',
op_sub_type: 'ROLES_MANAGEMENT',
user: param.req.user.email,
description: `updated roles for ${user.email} with ${param.projectUser.roles} `,
ip: param.req.clientIp,
});
return {
msg: 'User details updated successfully',
};
}
export async function projectUserDelete(param: {
projectId: string;
userId: string;
// todo: refactor
req: any;
}): Promise<any> {
const project_id = param.projectId;
if (param.req.session?.passport?.user?.id === param.userId) {
NcError.badRequest("Admin can't delete themselves!");
}
if (!param.req.session?.passport?.user?.roles?.owner) {
const user = await User.get(param.userId);
if (user.roles?.split(',').includes('super'))
NcError.forbidden('Insufficient privilege to delete a super admin user.');
const projectUser = await ProjectUser.get(project_id, param.userId);
if (projectUser?.roles?.split(',').includes('super'))
NcError.forbidden('Insufficient privilege to delete a owner user.');
}
await ProjectUser.delete(project_id, param.userId);
return true;
}
export async function projectUserInviteResend(param: {
userId: string;
projectUser: ProjectUserReqType;
projectId: string;
// todo: refactor
req: any;
}): Promise<any> {
const user = await User.get(param.userId);
if (!user) {
NcError.badRequest(`User with id '${param.userId}' not found`);
}
param.projectUser.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, param.req);
await Audit.insert({
op_type: 'AUTHENTICATION',
op_sub_type: 'RESEND_INVITE',
user: user.email,
description: `resent a invite to ${user.email} `,
ip: param.req.clientIp,
project_id: param.projectId,
});
return true;
}
// todo: refactor the whole function
export async function sendInviteEmail(
email: string,
token: string,
req: any
): Promise<any> {
try {
const template = (
await import('../meta/api/userApi/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;
}
}
Loading…
Cancel
Save