mirror of https://github.com/nocodb/nocodb
Pranav C
2 years ago
24 changed files with 1091 additions and 26 deletions
Binary file not shown.
@ -0,0 +1,471 @@
|
||||
import type { ErrorObject } from 'ajv'; |
||||
|
||||
enum DBError { |
||||
TABLE_EXIST = 'TABLE_EXIST', |
||||
TABLE_NOT_EXIST = 'TABLE_NOT_EXIST', |
||||
COLUMN_EXIST = 'COLUMN_EXIST', |
||||
COLUMN_NOT_EXIST = 'COLUMN_NOT_EXIST', |
||||
CONSTRAINT_EXIST = 'CONSTRAINT_EXIST', |
||||
CONSTRAINT_NOT_EXIST = 'CONSTRAINT_NOT_EXIST', |
||||
COLUMN_NOT_NULL = 'COLUMN_NOT_NULL', |
||||
} |
||||
|
||||
// extract db errors using database error code
|
||||
function extractDBError(error): { |
||||
type: DBError; |
||||
message: string; |
||||
info: any; |
||||
extra?: Record<string, any>; |
||||
} | void { |
||||
if (!error.code) return; |
||||
|
||||
let message: string; |
||||
let extra: Record<string, any>; |
||||
let type: DBError; |
||||
|
||||
// todo: handle not null constraint error for all databases
|
||||
switch (error.code) { |
||||
// sqlite errors
|
||||
case 'SQLITE_BUSY': |
||||
message = 'The database is locked by another process or transaction.'; |
||||
break; |
||||
case 'SQLITE_CONSTRAINT': |
||||
{ |
||||
const constraint = /FOREIGN KEY|UNIQUE/.test(error.message) |
||||
? error.message.match(/FOREIGN KEY|UNIQUE/gi)?.join(' ') |
||||
: 'constraint'; |
||||
message = `A ${constraint} constraint was violated: ${error.message}`; |
||||
extra = { |
||||
constraint, |
||||
}; |
||||
} |
||||
break; |
||||
case 'SQLITE_CORRUPT': |
||||
message = 'The database file is corrupt.'; |
||||
break; |
||||
case 'SQLITE_ERROR': |
||||
message = 'A SQL error occurred.'; |
||||
|
||||
if (error.message) { |
||||
const noSuchTableMatch = error.message.match(/no such table: (\w+)/); |
||||
const tableAlreadyExistsMatch = error.message.match( |
||||
/SQLITE_ERROR: table `?(\w+)`? already exists/ |
||||
); |
||||
|
||||
const duplicateColumnExistsMatch = error.message.match( |
||||
/SQLITE_ERROR: duplicate column name: (\w+)/ |
||||
); |
||||
const unrecognizedTokenMatch = error.message.match( |
||||
/SQLITE_ERROR: unrecognized token: "(\w+)"/ |
||||
); |
||||
const columnDoesNotExistMatch = error.message.match( |
||||
/SQLITE_ERROR: no such column: (\w+)/ |
||||
); |
||||
const constraintFailedMatch = error.message.match( |
||||
/SQLITE_ERROR: constraint failed: (\w+)/ |
||||
); |
||||
|
||||
if (noSuchTableMatch && noSuchTableMatch[1]) { |
||||
message = `The table '${noSuchTableMatch[1]}' does not exist.`; |
||||
type = DBError.TABLE_NOT_EXIST; |
||||
extra = { |
||||
table: noSuchTableMatch[1], |
||||
}; |
||||
} else if (tableAlreadyExistsMatch && tableAlreadyExistsMatch[1]) { |
||||
message = `The table '${tableAlreadyExistsMatch[1]}' already exists.`; |
||||
type = DBError.TABLE_EXIST; |
||||
extra = { |
||||
table: tableAlreadyExistsMatch[1], |
||||
}; |
||||
} else if (unrecognizedTokenMatch && unrecognizedTokenMatch[1]) { |
||||
message = `Unrecognized token: ${unrecognizedTokenMatch[1]}`; |
||||
extra = { |
||||
token: unrecognizedTokenMatch[1], |
||||
}; |
||||
} else if (columnDoesNotExistMatch && columnDoesNotExistMatch[1]) { |
||||
message = `The column ${columnDoesNotExistMatch[1]} does not exist.`; |
||||
type = DBError.COLUMN_NOT_EXIST; |
||||
extra = { |
||||
column: columnDoesNotExistMatch[1], |
||||
}; |
||||
} else if (constraintFailedMatch && constraintFailedMatch[1]) { |
||||
message = `A constraint failed: ${constraintFailedMatch[1]}`; |
||||
} else if ( |
||||
duplicateColumnExistsMatch && |
||||
duplicateColumnExistsMatch[1] |
||||
) { |
||||
message = `The column '${duplicateColumnExistsMatch[1]}' already exists.`; |
||||
type = DBError.COLUMN_EXIST; |
||||
extra = { |
||||
column: duplicateColumnExistsMatch[1], |
||||
}; |
||||
} else { |
||||
const match = error.message.match(/SQLITE_ERROR:\s*(\w+)/); |
||||
if (match && match[1]) { |
||||
message = match[1]; |
||||
} |
||||
} |
||||
} |
||||
break; |
||||
case 'SQLITE_RANGE': |
||||
message = 'A column index is out of range.'; |
||||
break; |
||||
case 'SQLITE_SCHEMA': |
||||
message = 'The database schema has changed.'; |
||||
break; |
||||
|
||||
// mysql errors
|
||||
case 'ER_TABLE_EXISTS_ERROR': |
||||
message = 'The table already exists.'; |
||||
|
||||
if (error.message) { |
||||
const extractTableNameMatch = error.message.match( |
||||
/ Table '?(\w+)'? already exists/i |
||||
); |
||||
if (extractTableNameMatch && extractTableNameMatch[1]) { |
||||
message = `The table '${extractTableNameMatch[1]}' already exists.`; |
||||
type = DBError.TABLE_EXIST; |
||||
extra = { |
||||
table: extractTableNameMatch[1], |
||||
}; |
||||
} |
||||
} |
||||
break; |
||||
case 'ER_DUP_FIELDNAME': |
||||
message = 'The column already exists.'; |
||||
|
||||
if (error.message) { |
||||
const extractColumnNameMatch = error.message.match( |
||||
/ Duplicate column name '(\w+)'/i |
||||
); |
||||
if (extractColumnNameMatch && extractColumnNameMatch[1]) { |
||||
message = `The column '${extractColumnNameMatch[1]}' already exists.`; |
||||
type = DBError.COLUMN_EXIST; |
||||
extra = { |
||||
column: extractColumnNameMatch[1], |
||||
}; |
||||
} |
||||
} |
||||
|
||||
break; |
||||
case 'ER_NO_SUCH_TABLE': |
||||
message = 'The table does not exist.'; |
||||
|
||||
if (error.message) { |
||||
const missingTableMatch = error.message.match( |
||||
/ Table '(?:\w+\.)?(\w+)' doesn't exist/i |
||||
); |
||||
if (missingTableMatch && missingTableMatch[1]) { |
||||
message = `The table '${missingTableMatch[1]}' does not exist`; |
||||
type = DBError.TABLE_NOT_EXIST; |
||||
extra = { |
||||
table: missingTableMatch[1], |
||||
}; |
||||
} |
||||
} |
||||
|
||||
break; |
||||
case 'ER_DUP_ENTRY': |
||||
message = 'This record already exists.'; |
||||
break; |
||||
case 'ER_PARSE_ERROR': |
||||
message = 'There was a syntax error in your SQL query.'; |
||||
break; |
||||
case 'ER_NO_DEFAULT_FOR_FIELD': |
||||
message = 'A value is required for this field.'; |
||||
break; |
||||
case 'ER_BAD_NULL_ERROR': |
||||
message = 'A null value is not allowed for this field.'; |
||||
{ |
||||
const extractColNameMatch = error.message.match( |
||||
/Column '(\w+)' cannot be null/i |
||||
); |
||||
if (extractColNameMatch && extractColNameMatch[1]) { |
||||
message = `The column '${extractColNameMatch[1]}' cannot be null.`; |
||||
type = DBError.COLUMN_NOT_NULL; |
||||
extra = { |
||||
column: extractColNameMatch[1], |
||||
}; |
||||
} |
||||
} |
||||
|
||||
break; |
||||
case 'ER_DATA_TOO_LONG': |
||||
message = 'The data entered is too long for this field.'; |
||||
break; |
||||
case 'ER_BAD_FIELD_ERROR': |
||||
{ |
||||
message = 'The field you are trying to access does not exist.'; |
||||
const extractColNameMatch = error.message.match( |
||||
/ Unknown column '(\w+)' in 'field list'/i |
||||
); |
||||
if (extractColNameMatch && extractColNameMatch[1]) { |
||||
message = `The column '${extractColNameMatch[1]}' does not exist.`; |
||||
type = DBError.COLUMN_NOT_EXIST; |
||||
extra = { |
||||
column: extractColNameMatch[1], |
||||
}; |
||||
} |
||||
} |
||||
break; |
||||
case 'ER_ACCESS_DENIED_ERROR': |
||||
message = 'You do not have permission to perform this action.'; |
||||
break; |
||||
case 'ER_LOCK_WAIT_TIMEOUT': |
||||
message = 'A timeout occurred while waiting for a table lock.'; |
||||
break; |
||||
case 'ER_NO_REFERENCED_ROW': |
||||
message = 'The referenced row does not exist.'; |
||||
break; |
||||
case 'ER_ROW_IS_REFERENCED': |
||||
message = 'This record is being referenced by other records.'; |
||||
break; |
||||
|
||||
// postgres errors
|
||||
case '23505': |
||||
message = 'This record already exists.'; |
||||
break; |
||||
case '42601': |
||||
message = 'There was a syntax error in your SQL query.'; |
||||
break; |
||||
case '23502': |
||||
message = 'A value is required for this field.'; |
||||
break; |
||||
case '23503': |
||||
message = 'The referenced row does not exist.'; |
||||
break; |
||||
case '23514': |
||||
message = 'A null value is not allowed for this field.'; |
||||
break; |
||||
case '22001': |
||||
message = 'The data entered is too long for this field.'; |
||||
break; |
||||
case '28000': |
||||
message = 'You do not have permission to perform this action.'; |
||||
break; |
||||
case '40P01': |
||||
message = 'A timeout occurred while waiting for a table lock.'; |
||||
break; |
||||
case '23506': |
||||
message = 'This record is being referenced by other records.'; |
||||
break; |
||||
case '42P07': |
||||
message = 'The table already exists.'; |
||||
if (error.message) { |
||||
const extractTableNameMatch = error.message.match( |
||||
/ relation "?(\w+)"? already exists/i |
||||
); |
||||
if (extractTableNameMatch && extractTableNameMatch[1]) { |
||||
message = `The table '${extractTableNameMatch[1]}' already exists.`; |
||||
type = DBError.TABLE_EXIST; |
||||
extra = { |
||||
table: extractTableNameMatch[1], |
||||
}; |
||||
} |
||||
} |
||||
break; |
||||
case '42701': |
||||
message = 'The column already exists.'; |
||||
if (error.message) { |
||||
const extractTableNameMatch = error.message.match( |
||||
/ column "(\w+)" of relation "(\w+)" already exists/i |
||||
); |
||||
if (extractTableNameMatch && extractTableNameMatch[1]) { |
||||
message = `The column '${extractTableNameMatch[1]}' already exists.`; |
||||
type = DBError.COLUMN_EXIST; |
||||
extra = { |
||||
column: extractTableNameMatch[1], |
||||
}; |
||||
} |
||||
} |
||||
break; |
||||
case '42P01': |
||||
message = 'The table does not exist.'; |
||||
if (error.message) { |
||||
const extractTableNameMatch = error.message.match( |
||||
/ relation "(\w+)" does not exist/i |
||||
); |
||||
if (extractTableNameMatch && extractTableNameMatch[1]) { |
||||
message = `The table '${extractTableNameMatch[1]}' does not exist.`; |
||||
type = DBError.TABLE_NOT_EXIST; |
||||
extra = { |
||||
table: extractTableNameMatch[1], |
||||
}; |
||||
} |
||||
} |
||||
break; |
||||
case '42703': |
||||
message = 'The column does not exist.'; |
||||
if (error.message) { |
||||
const extractTableNameMatch = error.message.match( |
||||
/ column "(\w+)" does not exist/i |
||||
); |
||||
if (extractTableNameMatch && extractTableNameMatch[1]) { |
||||
message = `The column '${extractTableNameMatch[1]}' does not exist.`; |
||||
type = DBError.COLUMN_NOT_EXIST; |
||||
extra = { |
||||
column: extractTableNameMatch[1], |
||||
}; |
||||
} |
||||
} |
||||
break; |
||||
// mssql errors
|
||||
case 'EREQUEST': |
||||
message = 'There was a syntax error in your SQL query.'; |
||||
if (error.message) { |
||||
const extractTableNameMatch = error.message.match( |
||||
/ There is already an object named '(\w+)' in the database/i |
||||
); |
||||
const extractDupColMatch = error.message.match( |
||||
/ Column name '(\w+)' in table '(\w+)' is specified more than once/i |
||||
); |
||||
const extractMissingTableMatch = error.message.match( |
||||
/ Invalid object name '(\w+)'./i |
||||
); |
||||
const extractMissingColMatch = error.message.match( |
||||
/ Invalid column name '(\w+)'./i |
||||
); |
||||
|
||||
if (extractTableNameMatch && extractTableNameMatch[1]) { |
||||
message = `The table '${extractTableNameMatch[1]}' already exists.`; |
||||
type = DBError.TABLE_EXIST; |
||||
extra = { |
||||
table: extractTableNameMatch[1], |
||||
}; |
||||
} else if (extractDupColMatch && extractDupColMatch[1]) { |
||||
message = `The column '${extractDupColMatch[1]}' already exists.`; |
||||
type = DBError.COLUMN_EXIST; |
||||
extra = { |
||||
column: extractDupColMatch[1], |
||||
}; |
||||
} else if (extractMissingTableMatch && extractMissingTableMatch[1]) { |
||||
message = `The table '${extractMissingTableMatch[1]}' does not exist`; |
||||
type = DBError.TABLE_NOT_EXIST; |
||||
extra = { |
||||
table: extractMissingTableMatch[1], |
||||
}; |
||||
} else if (extractMissingColMatch && extractMissingColMatch[1]) { |
||||
message = `The column '${extractMissingColMatch[1]}' does not exist`; |
||||
type = DBError.COLUMN_NOT_EXIST; |
||||
extra = { |
||||
column: extractMissingColMatch[1], |
||||
}; |
||||
} |
||||
} |
||||
break; |
||||
case 'ELOGIN': |
||||
message = 'You do not have permission to perform this action.'; |
||||
break; |
||||
case 'ETIMEOUT': |
||||
message = 'A timeout occurred while waiting for a table lock.'; |
||||
break; |
||||
case 'ECONNRESET': |
||||
message = 'The connection was reset.'; |
||||
break; |
||||
case 'ECONNREFUSED': |
||||
message = 'The connection was refused.'; |
||||
break; |
||||
case 'EHOSTUNREACH': |
||||
message = 'The host is unreachable.'; |
||||
break; |
||||
case 'EHOSTDOWN': |
||||
message = 'The host is down.'; |
||||
break; |
||||
} |
||||
|
||||
if (message) { |
||||
return { |
||||
message, |
||||
type, |
||||
extra, |
||||
info: { message: error.message, code: error.code }, |
||||
}; |
||||
} |
||||
} |
||||
|
||||
export default function ( |
||||
requestHandler: (req: any, res: any, next?: any) => any |
||||
) { |
||||
return async function (req: any, res: any, next: any) { |
||||
try { |
||||
return await requestHandler(req, res, next); |
||||
} catch (e) { |
||||
// todo: error log
|
||||
console.log(requestHandler.name ? `${requestHandler.name} ::` : '', e); |
||||
|
||||
const dbError = extractDBError(e); |
||||
|
||||
if (dbError) { |
||||
return res.status(400).json(dbError); |
||||
} |
||||
|
||||
if (e instanceof BadRequest) { |
||||
return res.status(400).json({ msg: e.message }); |
||||
} else if (e instanceof Unauthorized) { |
||||
return res.status(401).json({ msg: e.message }); |
||||
} else if (e instanceof Forbidden) { |
||||
return res.status(403).json({ msg: e.message }); |
||||
} else if (e instanceof NotFound) { |
||||
return res.status(404).json({ msg: e.message }); |
||||
} else if (e instanceof InternalServerError) { |
||||
return res.status(500).json({ msg: e.message }); |
||||
} else if (e instanceof NotImplemented) { |
||||
return res.status(501).json({ msg: e.message }); |
||||
} else if (e instanceof AjvError) { |
||||
return res.status(400).json({ msg: e.message, errors: e.errors }); |
||||
} |
||||
next(e); |
||||
} |
||||
}; |
||||
} |
||||
|
||||
class BadRequest extends Error {} |
||||
|
||||
class Unauthorized extends Error {} |
||||
|
||||
class Forbidden extends Error {} |
||||
|
||||
class NotFound extends Error {} |
||||
|
||||
class InternalServerError extends Error {} |
||||
|
||||
class NotImplemented extends Error {} |
||||
|
||||
class AjvError extends Error { |
||||
constructor(param: { message: string; errors: ErrorObject[] }) { |
||||
super(param.message); |
||||
this.errors = param.errors; |
||||
} |
||||
|
||||
errors: ErrorObject[]; |
||||
} |
||||
|
||||
export class NcError { |
||||
static notFound(message = 'Not found') { |
||||
throw new NotFound(message); |
||||
} |
||||
|
||||
static badRequest(message) { |
||||
throw new BadRequest(message); |
||||
} |
||||
|
||||
static unauthorized(message) { |
||||
throw new Unauthorized(message); |
||||
} |
||||
|
||||
static forbidden(message) { |
||||
throw new Forbidden(message); |
||||
} |
||||
|
||||
static internalServerError(message = 'Internal server error') { |
||||
throw new InternalServerError(message); |
||||
} |
||||
|
||||
static notImplemented(message = 'Not implemented') { |
||||
throw new NotImplemented(message); |
||||
} |
||||
|
||||
static ajvValidationError(param: { message: string; errors: ErrorObject[] }) { |
||||
throw new AjvError(param); |
||||
} |
||||
} |
@ -0,0 +1,306 @@
|
||||
import { |
||||
CallHandler, |
||||
ExecutionContext, |
||||
Injectable, |
||||
NestInterceptor, |
||||
NestMiddleware, |
||||
SetMetadata, |
||||
UseInterceptors, |
||||
} from '@nestjs/common'; |
||||
import { Reflector } from '@nestjs/core'; |
||||
import { NextFunction, Request, Response } from 'express'; |
||||
import { OrgUserRoles } from 'nocodb-sdk'; |
||||
import passport from 'passport'; |
||||
import { map, Observable, throwError } from 'rxjs'; |
||||
import { promisify } from 'util'; |
||||
import { |
||||
Column, |
||||
Filter, |
||||
FormViewColumn, |
||||
GalleryViewColumn, |
||||
GridViewColumn, |
||||
Hook, |
||||
Model, |
||||
Project, |
||||
Sort, |
||||
View, |
||||
} from '../../models'; |
||||
import projectAcl from '../../utils/projectAcl'; |
||||
import catchError, { NcError } from '../catchError'; |
||||
import extractProjectIdAndAuthenticate from '../extractProjectIdAndAuthenticate'; |
||||
|
||||
@Injectable() |
||||
export class ExtractProjectIdMiddleware implements NestInterceptor { |
||||
constructor(private reflector: Reflector) {} |
||||
|
||||
async intercept( |
||||
context: ExecutionContext, |
||||
next: CallHandler, |
||||
): Promise<Observable<any>> { |
||||
const req = context.switchToHttp().getRequest(); |
||||
const res = context.switchToHttp().getResponse(); |
||||
req.customProperty = 'This is a custom property'; |
||||
|
||||
try { |
||||
const { params } = req; |
||||
|
||||
// extract project id based on request path params
|
||||
if (params.projectName) { |
||||
const project = await Project.getByTitleOrId(params.projectName); |
||||
req.ncProjectId = project.id; |
||||
res.locals.project = project; |
||||
} |
||||
if (params.projectId) { |
||||
req.ncProjectId = params.projectId; |
||||
} else if (req.query.project_id) { |
||||
req.ncProjectId = req.query.project_id; |
||||
} else if ( |
||||
params.tableId || |
||||
req.query.fk_model_id || |
||||
req.body?.fk_model_id |
||||
) { |
||||
const model = await Model.getByIdOrName({ |
||||
id: params.tableId || req.query?.fk_model_id || req.body?.fk_model_id, |
||||
}); |
||||
req.ncProjectId = model?.project_id; |
||||
} else if (params.viewId) { |
||||
const view = |
||||
(await View.get(params.viewId)) || (await Model.get(params.viewId)); |
||||
req.ncProjectId = view?.project_id; |
||||
} else if ( |
||||
params.formViewId || |
||||
params.gridViewId || |
||||
params.kanbanViewId || |
||||
params.galleryViewId |
||||
) { |
||||
const view = await View.get( |
||||
params.formViewId || |
||||
params.gridViewId || |
||||
params.kanbanViewId || |
||||
params.galleryViewId, |
||||
); |
||||
req.ncProjectId = view?.project_id; |
||||
} else if (params.publicDataUuid) { |
||||
const view = await View.getByUUID(req.params.publicDataUuid); |
||||
req.ncProjectId = view?.project_id; |
||||
} else if (params.hookId) { |
||||
const hook = await Hook.get(params.hookId); |
||||
req.ncProjectId = hook?.project_id; |
||||
} else if (params.gridViewColumnId) { |
||||
const gridViewColumn = await GridViewColumn.get( |
||||
params.gridViewColumnId, |
||||
); |
||||
req.ncProjectId = gridViewColumn?.project_id; |
||||
} else if (params.formViewColumnId) { |
||||
const formViewColumn = await FormViewColumn.get( |
||||
params.formViewColumnId, |
||||
); |
||||
req.ncProjectId = formViewColumn?.project_id; |
||||
} else if (params.galleryViewColumnId) { |
||||
const galleryViewColumn = await GalleryViewColumn.get( |
||||
params.galleryViewColumnId, |
||||
); |
||||
req.ncProjectId = galleryViewColumn?.project_id; |
||||
} else if (params.columnId) { |
||||
const column = await Column.get({ colId: params.columnId }); |
||||
req.ncProjectId = column?.project_id; |
||||
} else if (params.filterId) { |
||||
const filter = await Filter.get(params.filterId); |
||||
req.ncProjectId = filter?.project_id; |
||||
} else if (params.filterParentId) { |
||||
const filter = await Filter.get(params.filterParentId); |
||||
req.ncProjectId = filter?.project_id; |
||||
} else if (params.sortId) { |
||||
const sort = await Sort.get(params.sortId); |
||||
req.ncProjectId = sort?.project_id; |
||||
} |
||||
|
||||
// const user = await new Promise((resolve, _reject) => {
|
||||
// passport.authenticate(
|
||||
// 'jwt',
|
||||
// { session: false },
|
||||
// (_err, user, _info) => {
|
||||
// if (user && !req.headers['xc-shared-base-id']) {
|
||||
// if (
|
||||
// req.path.indexOf('/user/me') === -1 &&
|
||||
// req.header('xc-preview') &&
|
||||
// /(?:^|,)(?:owner|creator)(?:$|,)/.test(user.roles)
|
||||
// ) {
|
||||
// return resolve({
|
||||
// ...user,
|
||||
// isAuthorized: true,
|
||||
// roles: req.header('xc-preview'),
|
||||
// });
|
||||
// }
|
||||
//
|
||||
// return resolve({ ...user, isAuthorized: true });
|
||||
// }
|
||||
//
|
||||
// if (req.headers['xc-token']) {
|
||||
// passport.authenticate(
|
||||
// 'authtoken',
|
||||
// {
|
||||
// session: false,
|
||||
// optional: false,
|
||||
// } as any,
|
||||
// (_err, user, _info) => {
|
||||
// // if (_err) return reject(_err);
|
||||
// if (user) {
|
||||
// return resolve({
|
||||
// ...user,
|
||||
// isAuthorized: true,
|
||||
// roles:
|
||||
// user.roles === 'owner' ? 'owner,creator' : user.roles,
|
||||
// });
|
||||
// } else {
|
||||
// resolve({ roles: 'guest' });
|
||||
// }
|
||||
// },
|
||||
// )(req, res, next);
|
||||
// } else if (req.headers['xc-shared-base-id']) {
|
||||
// passport.authenticate('baseView', {}, (_err, user, _info) => {
|
||||
// // if (_err) return reject(_err);
|
||||
// if (user) {
|
||||
// return resolve({
|
||||
// ...user,
|
||||
// isAuthorized: true,
|
||||
// isPublicBase: true,
|
||||
// });
|
||||
// } else {
|
||||
// resolve({ roles: 'guest' });
|
||||
// }
|
||||
// })(req, res, next);
|
||||
// } else {
|
||||
// resolve({ roles: 'guest' });
|
||||
// }
|
||||
// },
|
||||
// )(req, res, next);
|
||||
// });
|
||||
//
|
||||
// await promisify((req as any).login.bind(req))(user);
|
||||
} catch (e) { |
||||
console.log(e); |
||||
return throwError(new Error('Internal error')); |
||||
} |
||||
|
||||
return next.handle().pipe( |
||||
map((data) => { |
||||
return data; |
||||
}), |
||||
); |
||||
} |
||||
|
||||
// async use(req: any, res: any, next: () => void) {
|
||||
// const customValue = this.reflector.get<string>(
|
||||
// 'customValue',
|
||||
// req.route?.stack[0].handle,
|
||||
// );
|
||||
//
|
||||
//
|
||||
// }
|
||||
} |
||||
|
||||
@Injectable() |
||||
export class AclMiddleware implements NestInterceptor { |
||||
constructor(private reflector: Reflector) {} |
||||
|
||||
async intercept( |
||||
context: ExecutionContext, |
||||
next: CallHandler, |
||||
): Promise<Observable<any>> { |
||||
const permissionName = this.reflector.get<string>( |
||||
'permission', |
||||
context.getHandler(), |
||||
); |
||||
const allowedRoles = this.reflector.get<(OrgUserRoles | string)[]>( |
||||
'allowedRoles', |
||||
context.getHandler(), |
||||
); |
||||
const blockApiTokenAccess = this.reflector.get<boolean>( |
||||
'blockApiTokenAccess', |
||||
context.getHandler(), |
||||
); |
||||
|
||||
const req = context.switchToHttp().getRequest(); |
||||
const res = context.switchToHttp().getResponse(); |
||||
req.customProperty = 'This is a custom property'; |
||||
|
||||
const roles = req.user.roles.split(',').reduce((acc, role) => { |
||||
acc[role] = true; |
||||
return acc; |
||||
}, {}); |
||||
|
||||
if (req?.session?.passport?.user?.is_api_token && blockApiTokenAccess) { |
||||
NcError.forbidden('Not allowed with API token'); |
||||
} |
||||
if ( |
||||
(!allowedRoles || allowedRoles.some((role) => roles?.[role])) && |
||||
!( |
||||
roles?.creator || |
||||
roles?.owner || |
||||
roles?.editor || |
||||
roles?.viewer || |
||||
roles?.commenter || |
||||
roles?.[OrgUserRoles.SUPER_ADMIN] || |
||||
roles?.[OrgUserRoles.CREATOR] || |
||||
roles?.[OrgUserRoles.VIEWER] |
||||
) |
||||
) { |
||||
NcError.unauthorized('Unauthorized access'); |
||||
} |
||||
// todo : verify user have access to project or not
|
||||
|
||||
const isAllowed = |
||||
roles && |
||||
Object.entries(roles).some(([name, hasRole]) => { |
||||
return ( |
||||
hasRole && |
||||
projectAcl[name] && |
||||
(projectAcl[name] === '*' || |
||||
(projectAcl[name].exclude && |
||||
!projectAcl[name].exclude[permissionName]) || |
||||
(projectAcl[name].include && |
||||
projectAcl[name].include[permissionName])) |
||||
); |
||||
}); |
||||
if (!isAllowed) { |
||||
NcError.forbidden( |
||||
`${permissionName} - ${Object.keys(roles).filter( |
||||
(k) => roles[k], |
||||
)} : Not allowed`,
|
||||
); |
||||
} |
||||
|
||||
return next.handle().pipe( |
||||
map((data) => { |
||||
return data; |
||||
}), |
||||
); |
||||
} |
||||
} |
||||
|
||||
export const UseProjectIdMiddleware = () => (target: any, key?: string, descriptor?: PropertyDescriptor) => { |
||||
UseInterceptors(ExtractProjectIdMiddleware)(target, key, descriptor); |
||||
}; |
||||
|
||||
export const UseAclMiddleware = |
||||
({ |
||||
permissionName, |
||||
allowedRoles, |
||||
blockApiTokenAccess, |
||||
}: { |
||||
permissionName: string; |
||||
allowedRoles?: (OrgUserRoles | string)[]; |
||||
blockApiTokenAccess?: boolean; |
||||
}) => |
||||
(target: any, key?: string, descriptor?: PropertyDescriptor) => { |
||||
SetMetadata('permission', permissionName)(target, key, descriptor); |
||||
SetMetadata('allowedRoles', allowedRoles)(target, key, descriptor); |
||||
SetMetadata('blockApiTokenAccess', blockApiTokenAccess)( |
||||
target, |
||||
key, |
||||
descriptor, |
||||
); |
||||
UseInterceptors(ExtractProjectIdMiddleware)(target, key, descriptor); |
||||
UseInterceptors(AclMiddleware)(target, key, descriptor); |
||||
}; |
@ -0,0 +1,152 @@
|
||||
|
||||
import { promisify } from 'util'; |
||||
import passport from 'passport'; |
||||
|
||||
import { |
||||
Model, |
||||
View, |
||||
Hook, |
||||
GridViewColumn, |
||||
FormViewColumn, |
||||
GalleryViewColumn, |
||||
Project, |
||||
Column, |
||||
Filter, |
||||
Sort, |
||||
} from '../models'; |
||||
|
||||
export default async (req, res, next) => { |
||||
try { |
||||
const { params } = req; |
||||
|
||||
// extract project id based on request path params
|
||||
if (params.projectName) { |
||||
const project = await Project.getByTitleOrId(params.projectName); |
||||
req.ncProjectId = project.id; |
||||
res.locals.project = project; |
||||
} |
||||
if (params.projectId) { |
||||
req.ncProjectId = params.projectId; |
||||
} else if (req.query.project_id) { |
||||
req.ncProjectId = req.query.project_id; |
||||
} else if ( |
||||
params.tableId || |
||||
req.query.fk_model_id || |
||||
req.body?.fk_model_id |
||||
) { |
||||
const model = await Model.getByIdOrName({ |
||||
id: params.tableId || req.query?.fk_model_id || req.body?.fk_model_id, |
||||
}); |
||||
req.ncProjectId = model?.project_id; |
||||
} else if (params.viewId) { |
||||
const view = |
||||
(await View.get(params.viewId)) || (await Model.get(params.viewId)); |
||||
req.ncProjectId = view?.project_id; |
||||
} else if ( |
||||
params.formViewId || |
||||
params.gridViewId || |
||||
params.kanbanViewId || |
||||
params.galleryViewId |
||||
) { |
||||
const view = await View.get( |
||||
params.formViewId || |
||||
params.gridViewId || |
||||
params.kanbanViewId || |
||||
params.galleryViewId, |
||||
); |
||||
req.ncProjectId = view?.project_id; |
||||
} else if (params.publicDataUuid) { |
||||
const view = await View.getByUUID(req.params.publicDataUuid); |
||||
req.ncProjectId = view?.project_id; |
||||
} else if (params.hookId) { |
||||
const hook = await Hook.get(params.hookId); |
||||
req.ncProjectId = hook?.project_id; |
||||
} else if (params.gridViewColumnId) { |
||||
const gridViewColumn = await GridViewColumn.get(params.gridViewColumnId); |
||||
req.ncProjectId = gridViewColumn?.project_id; |
||||
} else if (params.formViewColumnId) { |
||||
const formViewColumn = await FormViewColumn.get(params.formViewColumnId); |
||||
req.ncProjectId = formViewColumn?.project_id; |
||||
} else if (params.galleryViewColumnId) { |
||||
const galleryViewColumn = await GalleryViewColumn.get( |
||||
params.galleryViewColumnId, |
||||
); |
||||
req.ncProjectId = galleryViewColumn?.project_id; |
||||
} else if (params.columnId) { |
||||
const column = await Column.get({ colId: params.columnId }); |
||||
req.ncProjectId = column?.project_id; |
||||
} else if (params.filterId) { |
||||
const filter = await Filter.get(params.filterId); |
||||
req.ncProjectId = filter?.project_id; |
||||
} else if (params.filterParentId) { |
||||
const filter = await Filter.get(params.filterParentId); |
||||
req.ncProjectId = filter?.project_id; |
||||
} else if (params.sortId) { |
||||
const sort = await Sort.get(params.sortId); |
||||
req.ncProjectId = sort?.project_id; |
||||
} |
||||
|
||||
const user = await new Promise((resolve, _reject) => { |
||||
passport.authenticate('jwt', { session: false }, (_err, user, _info) => { |
||||
if (user && !req.headers['xc-shared-base-id']) { |
||||
if ( |
||||
req.path.indexOf('/user/me') === -1 && |
||||
req.header('xc-preview') && |
||||
/(?:^|,)(?:owner|creator)(?:$|,)/.test(user.roles) |
||||
) { |
||||
return resolve({ |
||||
...user, |
||||
isAuthorized: true, |
||||
roles: req.header('xc-preview'), |
||||
}); |
||||
} |
||||
|
||||
return resolve({ ...user, isAuthorized: true }); |
||||
} |
||||
|
||||
if (req.headers['xc-token']) { |
||||
passport.authenticate( |
||||
'authtoken', |
||||
{ |
||||
session: false, |
||||
optional: false, |
||||
} as any, |
||||
(_err, user, _info) => { |
||||
// if (_err) return reject(_err);
|
||||
if (user) { |
||||
return resolve({ |
||||
...user, |
||||
isAuthorized: true, |
||||
roles: user.roles === 'owner' ? 'owner,creator' : user.roles, |
||||
}); |
||||
} else { |
||||
resolve({ roles: 'guest' }); |
||||
} |
||||
}, |
||||
)(req, res, next); |
||||
} else if (req.headers['xc-shared-base-id']) { |
||||
passport.authenticate('baseView', {}, (_err, user, _info) => { |
||||
// if (_err) return reject(_err);
|
||||
if (user) { |
||||
return resolve({ |
||||
...user, |
||||
isAuthorized: true, |
||||
isPublicBase: true, |
||||
}); |
||||
} else { |
||||
resolve({ roles: 'guest' }); |
||||
} |
||||
})(req, res, next); |
||||
} else { |
||||
resolve({ roles: 'guest' }); |
||||
} |
||||
})(req, res, next); |
||||
}); |
||||
|
||||
await promisify((req as any).login.bind(req))(user); |
||||
next(); |
||||
} catch (e) { |
||||
console.log(e); |
||||
next(new Error('Internal error')); |
||||
} |
||||
}; |
@ -0,0 +1,96 @@
|
||||
import { OrgUserRoles } from 'nocodb-sdk'; |
||||
import projectAcl from '../utils/projectAcl'; |
||||
import catchError, { NcError } from './catchError'; |
||||
import extractProjectIdAndAuthenticate from './extractProjectIdAndAuthenticate'; |
||||
import type { NextFunction, Request, Response } from 'express'; |
||||
|
||||
export default function ( |
||||
handlerFn, |
||||
permissionName, |
||||
{ |
||||
allowedRoles, |
||||
blockApiTokenAccess, |
||||
}: { |
||||
allowedRoles?: (OrgUserRoles | string)[]; |
||||
blockApiTokenAccess?: boolean; |
||||
} = {} |
||||
) { |
||||
return [ |
||||
extractProjectIdAndAuthenticate, |
||||
catchError(async function authMiddleware(req, _res, next) { |
||||
const roles = req?.session?.passport?.user?.roles; |
||||
if (req?.session?.passport?.user?.is_api_token && blockApiTokenAccess) { |
||||
NcError.forbidden('Not allowed with API token'); |
||||
} |
||||
if ( |
||||
(!allowedRoles || allowedRoles.some((role) => roles?.[role])) && |
||||
!( |
||||
roles?.creator || |
||||
roles?.owner || |
||||
roles?.editor || |
||||
roles?.viewer || |
||||
roles?.commenter || |
||||
roles?.[OrgUserRoles.SUPER_ADMIN] || |
||||
roles?.[OrgUserRoles.CREATOR] || |
||||
roles?.[OrgUserRoles.VIEWER] |
||||
) |
||||
) { |
||||
NcError.unauthorized('Unauthorized access'); |
||||
} |
||||
next(); |
||||
}), |
||||
// @ts-ignore
|
||||
catchError(async function projectAclMiddleware( |
||||
req: Request<any, any, any, any, any>, |
||||
_res: Response, |
||||
next: NextFunction |
||||
) { |
||||
// if (req['files'] && req.body.json) {
|
||||
// req.body = JSON.parse(req.body.json);
|
||||
// }
|
||||
// if (req['session']?.passport?.user?.isAuthorized) {
|
||||
// if (
|
||||
// req?.body?.project_id &&
|
||||
// !req['session']?.passport?.user?.isPublicBase &&
|
||||
// !(await this.xcMeta.isUserHaveAccessToProject(
|
||||
// req?.body?.project_id,
|
||||
// req['session']?.passport?.user?.id
|
||||
// ))
|
||||
// ) {
|
||||
// return res
|
||||
// .status(403)
|
||||
// .json({ msg: "User doesn't have project access" });
|
||||
// }
|
||||
//
|
||||
// if (req?.body?.api) {
|
||||
|
||||
// todo : verify user have access to project or not
|
||||
|
||||
const roles = req['session']?.passport?.user?.roles; |
||||
const isAllowed = |
||||
roles && |
||||
Object.entries(roles).some(([name, hasRole]) => { |
||||
return ( |
||||
hasRole && |
||||
projectAcl[name] && |
||||
(projectAcl[name] === '*' || |
||||
(projectAcl[name].exclude && |
||||
!projectAcl[name].exclude[permissionName]) || |
||||
(projectAcl[name].include && |
||||
projectAcl[name].include[permissionName])) |
||||
); |
||||
}); |
||||
if (!isAllowed) { |
||||
NcError.forbidden( |
||||
`${permissionName} - ${Object.keys(roles).filter( |
||||
(k) => roles[k] |
||||
)} : Not allowed` |
||||
); |
||||
} |
||||
// }
|
||||
// }
|
||||
next(); |
||||
}), |
||||
catchError(handlerFn), |
||||
]; |
||||
} |
@ -1,9 +1,10 @@
|
||||
import { Module } from '@nestjs/common'; |
||||
import { ExtractProjectIdMiddleware } from '../middlewares/extract-project-id/extract-project-id.middleware' |
||||
import { ProjectsService } from './projects.service'; |
||||
import { ProjectsController } from './projects.controller'; |
||||
|
||||
@Module({ |
||||
controllers: [ProjectsController], |
||||
providers: [ProjectsService] |
||||
providers: [ProjectsService, ExtractProjectIdMiddleware] |
||||
}) |
||||
export class ProjectsModule {} |
||||
|
@ -1,7 +1,13 @@
|
||||
import { Controller } from '@nestjs/common'; |
||||
import { Controller, Get } from '@nestjs/common'; |
||||
import { UsersService } from './users.service'; |
||||
|
||||
@Controller('users') |
||||
@Controller() |
||||
export class UsersController { |
||||
constructor(private readonly usersService: UsersService) {} |
||||
|
||||
@Get('/api/v1/auth/user/me') |
||||
async me() { |
||||
// return this.usersService.me();
|
||||
} |
||||
|
||||
} |
||||
|
Loading…
Reference in new issue