[1/n]: Migrate deleteOne Rest API to use TwentyORM directly (#9784)
# This PR - Addressing #3644 - Migrates the `DELETE /rest/*` endpoint to use TwentyORM - Factorizes common middleware logic into a common module --------- Co-authored-by: martmull <martmull@hotmail.fr>
This commit is contained in:
@ -0,0 +1 @@
|
||||
export const INTERNAL_SERVER_ERROR = 'Internal server error';
|
||||
@ -0,0 +1,19 @@
|
||||
export const EXCLUDED_MIDDLEWARE_OPERATIONS = [
|
||||
'GetClientConfig',
|
||||
'GetWorkspaceFromInviteHash',
|
||||
'Track',
|
||||
'CheckUserExists',
|
||||
'GetLoginTokenFromCredentials',
|
||||
'GetAuthTokensFromLoginToken',
|
||||
'GetLoginTokenFromEmailVerificationToken',
|
||||
'ResendEmailVerificationToken',
|
||||
'SignUp',
|
||||
'RenewToken',
|
||||
'EmailPasswordResetLink',
|
||||
'ValidatePasswordResetToken',
|
||||
'UpdatePasswordViaResetToken',
|
||||
'IntrospectionQuery',
|
||||
'ExchangeAuthorizationCode',
|
||||
'GetAuthorizationUrl',
|
||||
'GetPublicWorkspaceDataBySubdomain',
|
||||
] as const;
|
||||
@ -1,114 +1,28 @@
|
||||
import { Injectable, NestMiddleware } from '@nestjs/common';
|
||||
|
||||
import { NextFunction, Request, Response } from 'express';
|
||||
import { ExtractJwt } from 'passport-jwt';
|
||||
|
||||
import { AuthGraphqlApiExceptionFilter } from 'src/engine/core-modules/auth/filters/auth-graphql-api-exception.filter';
|
||||
import { AccessTokenService } from 'src/engine/core-modules/auth/token/services/access-token.service';
|
||||
import { AuthContext } from 'src/engine/core-modules/auth/types/auth-context.type';
|
||||
import { ExceptionHandlerService } from 'src/engine/core-modules/exception-handler/exception-handler.service';
|
||||
import { handleExceptionAndConvertToGraphQLError } from 'src/engine/utils/global-exception-handler.util';
|
||||
import { WorkspaceCacheStorageService } from 'src/engine/workspace-cache-storage/workspace-cache-storage.service';
|
||||
class GraphqlTokenValidationProxy {
|
||||
private accessTokenService: AccessTokenService;
|
||||
|
||||
constructor(accessTokenService: AccessTokenService) {
|
||||
this.accessTokenService = accessTokenService;
|
||||
}
|
||||
|
||||
async validateToken(req: Request) {
|
||||
try {
|
||||
return await this.accessTokenService.validateTokenByRequest(req);
|
||||
} catch (error) {
|
||||
const authGraphqlApiExceptionFilter = new AuthGraphqlApiExceptionFilter();
|
||||
|
||||
throw authGraphqlApiExceptionFilter.catch(error);
|
||||
}
|
||||
}
|
||||
}
|
||||
import { MiddlewareService } from 'src/engine/middlewares/middleware.service';
|
||||
|
||||
@Injectable()
|
||||
export class GraphQLHydrateRequestFromTokenMiddleware
|
||||
implements NestMiddleware
|
||||
{
|
||||
constructor(
|
||||
private readonly accessTokenService: AccessTokenService,
|
||||
private readonly workspaceStorageCacheService: WorkspaceCacheStorageService,
|
||||
private readonly exceptionHandlerService: ExceptionHandlerService,
|
||||
) {}
|
||||
constructor(private readonly middlewareService: MiddlewareService) {}
|
||||
|
||||
async use(req: Request, res: Response, next: NextFunction) {
|
||||
const body = req.body;
|
||||
|
||||
const excludedOperations = [
|
||||
'GetClientConfig',
|
||||
'GetWorkspaceFromInviteHash',
|
||||
'Track',
|
||||
'CheckUserExists',
|
||||
'GetLoginTokenFromCredentials',
|
||||
'GetAuthTokensFromLoginToken',
|
||||
'GetLoginTokenFromEmailVerificationToken',
|
||||
'ResendEmailVerificationToken',
|
||||
'SignUp',
|
||||
'RenewToken',
|
||||
'EmailPasswordResetLink',
|
||||
'ValidatePasswordResetToken',
|
||||
'UpdatePasswordViaResetToken',
|
||||
'IntrospectionQuery',
|
||||
'ExchangeAuthorizationCode',
|
||||
'GetAuthorizationUrl',
|
||||
'GetPublicWorkspaceDataBySubdomain',
|
||||
];
|
||||
|
||||
if (
|
||||
!this.isTokenPresent(req) &&
|
||||
(!body?.operationName || excludedOperations.includes(body.operationName))
|
||||
) {
|
||||
if (this.middlewareService.checkUnauthenticatedAccess(req)) {
|
||||
return next();
|
||||
}
|
||||
|
||||
let data: AuthContext;
|
||||
|
||||
try {
|
||||
const graphqlTokenValidationProxy = new GraphqlTokenValidationProxy(
|
||||
this.accessTokenService,
|
||||
);
|
||||
|
||||
data = await graphqlTokenValidationProxy.validateToken(req);
|
||||
const metadataVersion =
|
||||
await this.workspaceStorageCacheService.getMetadataVersion(
|
||||
data.workspace.id,
|
||||
);
|
||||
|
||||
req.user = data.user;
|
||||
req.apiKey = data.apiKey;
|
||||
req.workspace = data.workspace;
|
||||
req.workspaceId = data.workspace.id;
|
||||
req.workspaceMetadataVersion = metadataVersion;
|
||||
req.workspaceMemberId = data.workspaceMemberId;
|
||||
await this.middlewareService.authenticateGraphqlRequest(req);
|
||||
} catch (error) {
|
||||
res.writeHead(200, { 'Content-Type': 'application/json' });
|
||||
res.write(
|
||||
JSON.stringify({
|
||||
errors: [
|
||||
handleExceptionAndConvertToGraphQLError(
|
||||
error,
|
||||
this.exceptionHandlerService,
|
||||
),
|
||||
],
|
||||
}),
|
||||
);
|
||||
res.end();
|
||||
this.middlewareService.writeGraphqlResponseOnExceptionCaught(res, error);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
next();
|
||||
}
|
||||
|
||||
isTokenPresent(request: Request): boolean {
|
||||
const token = ExtractJwt.fromAuthHeaderAsBearerToken()(request);
|
||||
|
||||
return !!token;
|
||||
}
|
||||
}
|
||||
|
||||
@ -0,0 +1,19 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
|
||||
import { TokenModule } from 'src/engine/core-modules/auth/token/token.module';
|
||||
import { DataSourceModule } from 'src/engine/metadata-modules/data-source/data-source.module';
|
||||
import { WorkspaceMetadataCacheModule } from 'src/engine/metadata-modules/workspace-metadata-cache/workspace-metadata-cache.module';
|
||||
import { MiddlewareService } from 'src/engine/middlewares/middleware.service';
|
||||
import { WorkspaceCacheStorageModule } from 'src/engine/workspace-cache-storage/workspace-cache-storage.module';
|
||||
|
||||
@Module({
|
||||
imports: [
|
||||
DataSourceModule,
|
||||
WorkspaceCacheStorageModule,
|
||||
WorkspaceMetadataCacheModule,
|
||||
TokenModule,
|
||||
],
|
||||
providers: [MiddlewareService],
|
||||
exports: [MiddlewareService],
|
||||
})
|
||||
export class MiddlewareModule {}
|
||||
@ -0,0 +1,167 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
|
||||
import { Request, Response } from 'express';
|
||||
import { ExtractJwt } from 'passport-jwt';
|
||||
|
||||
import { AuthExceptionCode } from 'src/engine/core-modules/auth/auth.exception';
|
||||
import { AccessTokenService } from 'src/engine/core-modules/auth/token/services/access-token.service';
|
||||
import { AuthContext } from 'src/engine/core-modules/auth/types/auth-context.type';
|
||||
import { ExceptionHandlerService } from 'src/engine/core-modules/exception-handler/exception-handler.service';
|
||||
import { ErrorCode } from 'src/engine/core-modules/graphql/utils/graphql-errors.util';
|
||||
import { DataSourceService } from 'src/engine/metadata-modules/data-source/data-source.service';
|
||||
import { WorkspaceMetadataCacheService } from 'src/engine/metadata-modules/workspace-metadata-cache/services/workspace-metadata-cache.service';
|
||||
import { INTERNAL_SERVER_ERROR } from 'src/engine/middlewares/constants/default-error-message.constant';
|
||||
import { EXCLUDED_MIDDLEWARE_OPERATIONS } from 'src/engine/middlewares/constants/excluded-middleware-operations.constant';
|
||||
import { GraphqlTokenValidationProxy } from 'src/engine/middlewares/utils/graphql-token-validation-utils';
|
||||
import {
|
||||
handleException,
|
||||
handleExceptionAndConvertToGraphQLError,
|
||||
} from 'src/engine/utils/global-exception-handler.util';
|
||||
import { WorkspaceCacheStorageService } from 'src/engine/workspace-cache-storage/workspace-cache-storage.service';
|
||||
import { CustomException } from 'src/utils/custom-exception';
|
||||
import { isDefined } from 'src/utils/is-defined';
|
||||
|
||||
@Injectable()
|
||||
export class MiddlewareService {
|
||||
constructor(
|
||||
private readonly accessTokenService: AccessTokenService,
|
||||
private readonly workspaceStorageCacheService: WorkspaceCacheStorageService,
|
||||
private readonly workspaceMetadataCacheService: WorkspaceMetadataCacheService,
|
||||
private readonly dataSourceService: DataSourceService,
|
||||
private readonly exceptionHandlerService: ExceptionHandlerService,
|
||||
) {}
|
||||
|
||||
private excludedOperations = EXCLUDED_MIDDLEWARE_OPERATIONS;
|
||||
|
||||
public isTokenPresent(request: Request): boolean {
|
||||
const token = ExtractJwt.fromAuthHeaderAsBearerToken()(request);
|
||||
|
||||
return !!token;
|
||||
}
|
||||
|
||||
public checkUnauthenticatedAccess(request: Request): boolean {
|
||||
const { body } = request;
|
||||
|
||||
const isUserUnauthenticated = !this.isTokenPresent(request);
|
||||
const isExcludedOperation =
|
||||
!body?.operationName ||
|
||||
this.excludedOperations.includes(body.operationName);
|
||||
|
||||
return isUserUnauthenticated && isExcludedOperation;
|
||||
}
|
||||
|
||||
public writeRestResponseOnExceptionCaught(res: Response, error: any) {
|
||||
// capture and handle custom exceptions
|
||||
handleException(error as CustomException, this.exceptionHandlerService);
|
||||
|
||||
const statusCode = this.getStatus(error);
|
||||
|
||||
res.writeHead(statusCode, { 'Content-Type': 'application/json' });
|
||||
res.write(
|
||||
JSON.stringify({
|
||||
statusCode,
|
||||
messages: [error?.message || INTERNAL_SERVER_ERROR],
|
||||
error: error?.code || ErrorCode.INTERNAL_SERVER_ERROR,
|
||||
}),
|
||||
);
|
||||
|
||||
res.end();
|
||||
}
|
||||
|
||||
public writeGraphqlResponseOnExceptionCaught(res: Response, error: any) {
|
||||
const errors = [
|
||||
handleExceptionAndConvertToGraphQLError(
|
||||
error as Error,
|
||||
this.exceptionHandlerService,
|
||||
),
|
||||
];
|
||||
|
||||
const statusCode = 200;
|
||||
|
||||
res.writeHead(statusCode, {
|
||||
'Content-Type': 'application/json',
|
||||
});
|
||||
|
||||
res.write(
|
||||
JSON.stringify({
|
||||
errors,
|
||||
}),
|
||||
);
|
||||
|
||||
res.end();
|
||||
}
|
||||
|
||||
public async authenticateRestRequest(request: Request) {
|
||||
const data = await this.accessTokenService.validateTokenByRequest(request);
|
||||
const metadataVersion =
|
||||
await this.workspaceStorageCacheService.getMetadataVersion(
|
||||
data.workspace.id,
|
||||
);
|
||||
|
||||
if (metadataVersion === undefined) {
|
||||
await this.workspaceMetadataCacheService.recomputeMetadataCache({
|
||||
workspaceId: data.workspace.id,
|
||||
});
|
||||
throw new Error('Metadata cache version not found');
|
||||
}
|
||||
|
||||
const dataSourcesMetadata =
|
||||
await this.dataSourceService.getDataSourcesMetadataFromWorkspaceId(
|
||||
data.workspace.id,
|
||||
);
|
||||
|
||||
if (!dataSourcesMetadata || dataSourcesMetadata.length === 0) {
|
||||
throw new Error('No data sources found');
|
||||
}
|
||||
|
||||
this.bindDataToRequestObject(data, request, metadataVersion);
|
||||
}
|
||||
|
||||
public async authenticateGraphqlRequest(request: Request) {
|
||||
const graphqlTokenValidationProxy = new GraphqlTokenValidationProxy(
|
||||
this.accessTokenService,
|
||||
);
|
||||
|
||||
const data = await graphqlTokenValidationProxy.validateToken(request);
|
||||
const metadataVersion =
|
||||
await this.workspaceStorageCacheService.getMetadataVersion(
|
||||
data.workspace.id,
|
||||
);
|
||||
|
||||
this.bindDataToRequestObject(data, request, metadataVersion);
|
||||
}
|
||||
|
||||
private hasErrorStatus(error: unknown): error is { status: number } {
|
||||
return isDefined((error as { status: number })?.status);
|
||||
}
|
||||
|
||||
private bindDataToRequestObject(
|
||||
data: AuthContext,
|
||||
request: Request,
|
||||
metadataVersion: number | undefined,
|
||||
) {
|
||||
request.user = data.user;
|
||||
request.apiKey = data.apiKey;
|
||||
request.workspace = data.workspace;
|
||||
request.workspaceId = data.workspace.id;
|
||||
request.workspaceMetadataVersion = metadataVersion;
|
||||
request.workspaceMemberId = data.workspaceMemberId;
|
||||
}
|
||||
|
||||
private getStatus(error: any): number {
|
||||
if (this.hasErrorStatus(error)) {
|
||||
return error.status;
|
||||
}
|
||||
|
||||
if (error instanceof CustomException) {
|
||||
switch (error.code) {
|
||||
case AuthExceptionCode.UNAUTHENTICATED:
|
||||
return 401;
|
||||
default:
|
||||
return 400;
|
||||
}
|
||||
}
|
||||
|
||||
return 500;
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,22 @@
|
||||
import { Injectable, NestMiddleware } from '@nestjs/common';
|
||||
|
||||
import { NextFunction, Request, Response } from 'express';
|
||||
|
||||
import { MiddlewareService } from 'src/engine/middlewares/middleware.service';
|
||||
|
||||
@Injectable()
|
||||
export class RestCoreMiddleware implements NestMiddleware {
|
||||
constructor(private readonly middlewareService: MiddlewareService) {}
|
||||
|
||||
async use(req: Request, res: Response, next: NextFunction) {
|
||||
try {
|
||||
await this.middlewareService.authenticateRestRequest(req);
|
||||
} catch (error) {
|
||||
this.middlewareService.writeRestResponseOnExceptionCaught(res, error);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
next();
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,22 @@
|
||||
import { Request } from 'express';
|
||||
|
||||
import { AuthGraphqlApiExceptionFilter } from 'src/engine/core-modules/auth/filters/auth-graphql-api-exception.filter';
|
||||
import { AccessTokenService } from 'src/engine/core-modules/auth/token/services/access-token.service';
|
||||
|
||||
export class GraphqlTokenValidationProxy {
|
||||
private accessTokenService: AccessTokenService;
|
||||
|
||||
constructor(accessTokenService: AccessTokenService) {
|
||||
this.accessTokenService = accessTokenService;
|
||||
}
|
||||
|
||||
async validateToken(req: Request) {
|
||||
try {
|
||||
return await this.accessTokenService.validateTokenByRequest(req);
|
||||
} catch (error) {
|
||||
const authGraphqlApiExceptionFilter = new AuthGraphqlApiExceptionFilter();
|
||||
|
||||
throw authGraphqlApiExceptionFilter.catch(error);
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user