diff --git a/server/src/app.module.ts b/server/src/app.module.ts index 813eeebf0..cb00efef2 100644 --- a/server/src/app.module.ts +++ b/server/src/app.module.ts @@ -3,16 +3,17 @@ import { GraphQLModule } from '@nestjs/graphql'; import { ConfigModule } from '@nestjs/config'; import { APP_FILTER, ContextIdFactory, ModuleRef } from '@nestjs/core'; +import { GraphQLError, GraphQLSchema } from 'graphql'; import { YogaDriver, YogaDriverConfig } from '@graphql-yoga/nestjs'; import GraphQLJSON from 'graphql-type-json'; -import { GraphQLError, GraphQLSchema } from 'graphql'; -import { ExtractJwt } from 'passport-jwt'; -import { TokenExpiredError, JsonWebTokenError, verify } from 'jsonwebtoken'; +import { TokenExpiredError, JsonWebTokenError } from 'jsonwebtoken'; import { WorkspaceFactory } from 'src/workspace/workspace.factory'; import { TypeOrmExceptionFilter } from 'src/filters/typeorm-exception.filter'; import { HttpExceptionFilter } from 'src/filters/http-exception.filter'; import { GlobalExceptionFilter } from 'src/filters/global-exception.filter'; +import { TokenService } from 'src/core/auth/services/token.service'; +import { Workspace } from 'src/core/workspace/workspace.entity'; import { AppService } from './app.service'; @@ -20,11 +21,6 @@ import { CoreModule } from './core/core.module'; import { IntegrationsModule } from './integrations/integrations.module'; import { HealthModule } from './health/health.module'; import { WorkspaceModule } from './workspace/workspace.module'; -import { EnvironmentService } from './integrations/environment/environment.service'; -import { - JwtAuthStrategy, - JwtPayload, -} from './core/auth/strategies/jwt.auth.strategy'; @Module({ imports: [ @@ -38,38 +34,19 @@ import { include: [CoreModule], conditionalSchema: async (request) => { try { - // Get the JwtAuthStrategy from the AppModule - const jwtStrategy = AppModule.moduleRef.get(JwtAuthStrategy, { + // Get TokenService from AppModule + const tokenService = AppModule.moduleRef.get(TokenService, { strict: false, }); - // Get the EnvironmentService from the AppModule - const environmentService = AppModule.moduleRef.get( - EnvironmentService, - { - strict: false, - }, - ); + let workspace: Workspace; - // Extract JWT from the request - const token = ExtractJwt.fromAuthHeaderAsBearerToken()(request.req); - - // If there is no token return an empty schema - if (!token) { + try { + workspace = await tokenService.validateToken(request.req); + } catch (err) { return new GraphQLSchema({}); } - // Verify and decode JWT - const decoded = verify( - token, - environmentService.getAccessTokenSecret(), - ); - - // Validate JWT - const { workspace } = await jwtStrategy.validate( - decoded as JwtPayload, - ); - const contextId = ContextIdFactory.create(); AppModule.moduleRef.registerRequestByContextId(request, contextId); diff --git a/server/src/core/api-rest/api-rest-query-builder/api-rest-query-builder.factory.ts b/server/src/core/api-rest/api-rest-query-builder/api-rest-query-builder.factory.ts index 67ea47972..68c7f48d3 100644 --- a/server/src/core/api-rest/api-rest-query-builder/api-rest-query-builder.factory.ts +++ b/server/src/core/api-rest/api-rest-query-builder/api-rest-query-builder.factory.ts @@ -1,8 +1,4 @@ -import { - BadRequestException, - Injectable, - UnauthorizedException, -} from '@nestjs/common'; +import { BadRequestException, Injectable } from '@nestjs/common'; import { Request } from 'express'; @@ -44,18 +40,10 @@ export class ApiRestQueryBuilderFactory { objectMetadataItems: ObjectMetadataEntity[]; objectMetadataItem: ObjectMetadataEntity; }> { - let workspaceId; - - try { - workspaceId = await this.tokenService.verifyApiKeyToken(request); - } catch (err) { - throw new UnauthorizedException( - `Invalid API key. Double check your API key or generate a new one here ${this.environmentService.getFrontBaseUrl()}/settings/developers/api-keys`, - ); - } + const workspace = await this.tokenService.validateToken(request); const objectMetadataItems = - await this.objectMetadataService.findManyWithinWorkspace(workspaceId); + await this.objectMetadataService.findManyWithinWorkspace(workspace.id); if (!objectMetadataItems.length) { throw new BadRequestException( diff --git a/server/src/core/api-rest/api-rest-query-builder/factories/create-query.factory.ts b/server/src/core/api-rest/api-rest-query-builder/factories/create-query.factory.ts index f84da4c91..16622d8ed 100644 --- a/server/src/core/api-rest/api-rest-query-builder/factories/create-query.factory.ts +++ b/server/src/core/api-rest/api-rest-query-builder/factories/create-query.factory.ts @@ -6,13 +6,13 @@ import { mapFieldMetadataToGraphqlQuery } from 'src/core/api-rest/api-rest-query @Injectable() export class CreateQueryFactory { create(objectMetadata, depth?: number): string { + const objectNameSingular = capitalize( + objectMetadata.objectMetadataItem.nameSingular, + ); + return ` - mutation Create${capitalize( - objectMetadata.objectMetadataItem.nameSingular, - )}($data: CompanyCreateInput!) { - create${capitalize( - objectMetadata.objectMetadataItem.nameSingular, - )}(data: $data) { + mutation Create${objectNameSingular}($data: ${objectNameSingular}CreateInput!) { + create${objectNameSingular}(data: $data) { id ${objectMetadata.objectMetadataItem.fields .map((field) => diff --git a/server/src/core/api-rest/api-rest-query-builder/factories/delete-query.factory.ts b/server/src/core/api-rest/api-rest-query-builder/factories/delete-query.factory.ts index 829552a64..d1f0a497e 100644 --- a/server/src/core/api-rest/api-rest-query-builder/factories/delete-query.factory.ts +++ b/server/src/core/api-rest/api-rest-query-builder/factories/delete-query.factory.ts @@ -5,9 +5,11 @@ import { capitalize } from 'src/utils/capitalize'; @Injectable() export class DeleteQueryFactory { create(objectMetadataItem): string { + const objectNameSingular = capitalize(objectMetadataItem.nameSingular); + return ` - mutation Delete${capitalize(objectMetadataItem.nameSingular)}($id: ID!) { - delete${capitalize(objectMetadataItem.nameSingular)}(id: $id) { + mutation Delete${objectNameSingular}($id: ID!) { + delete${objectNameSingular}(id: $id) { id } } diff --git a/server/src/core/api-rest/api-rest-query-builder/factories/find-many-query.factory.ts b/server/src/core/api-rest/api-rest-query-builder/factories/find-many-query.factory.ts index 56071504b..5c3437721 100644 --- a/server/src/core/api-rest/api-rest-query-builder/factories/find-many-query.factory.ts +++ b/server/src/core/api-rest/api-rest-query-builder/factories/find-many-query.factory.ts @@ -6,18 +6,19 @@ import { mapFieldMetadataToGraphqlQuery } from 'src/core/api-rest/api-rest-query @Injectable() export class FindManyQueryFactory { create(objectMetadata, depth?: number): string { + const objectNameSingular = capitalize( + objectMetadata.objectMetadataItem.nameSingular, + ); + const objectNamePlural = objectMetadata.objectMetadataItem.namePlural; + return ` - query FindMany${capitalize(objectMetadata.objectMetadataItem.namePlural)}( - $filter: ${capitalize( - objectMetadata.objectMetadataItem.nameSingular, - )}FilterInput, - $orderBy: ${capitalize( - objectMetadata.objectMetadataItem.nameSingular, - )}OrderByInput, + query FindMany${capitalize(objectNamePlural)}( + $filter: ${objectNameSingular}FilterInput, + $orderBy: ${objectNameSingular}OrderByInput, $lastCursor: String, $limit: Float = 60 ) { - ${objectMetadata.objectMetadataItem.namePlural}( + ${objectNamePlural}( filter: $filter, orderBy: $orderBy, first: $limit, after: $lastCursor ) { edges { diff --git a/server/src/core/api-rest/api-rest-query-builder/factories/find-one-query.factory.ts b/server/src/core/api-rest/api-rest-query-builder/factories/find-one-query.factory.ts index 856137f94..677bfd760 100644 --- a/server/src/core/api-rest/api-rest-query-builder/factories/find-one-query.factory.ts +++ b/server/src/core/api-rest/api-rest-query-builder/factories/find-one-query.factory.ts @@ -6,15 +6,13 @@ import { mapFieldMetadataToGraphqlQuery } from 'src/core/api-rest/api-rest-query @Injectable() export class FindOneQueryFactory { create(objectMetadata, depth?: number): string { + const objectNameSingular = objectMetadata.objectMetadataItem.nameSingular; + return ` - query FindOne${capitalize( - objectMetadata.objectMetadataItem.nameSingular, - )}( - $filter: ${capitalize( - objectMetadata.objectMetadataItem.nameSingular, - )}FilterInput!, + query FindOne${capitalize(objectNameSingular)}( + $filter: ${capitalize(objectNameSingular)}FilterInput!, ) { - ${objectMetadata.objectMetadataItem.nameSingular}(filter: $filter) { + ${objectNameSingular}(filter: $filter) { id ${objectMetadata.objectMetadataItem.fields .map((field) => diff --git a/server/src/core/api-rest/api-rest-query-builder/factories/update-query.factory.ts b/server/src/core/api-rest/api-rest-query-builder/factories/update-query.factory.ts index 3b9665e31..386ec4b97 100644 --- a/server/src/core/api-rest/api-rest-query-builder/factories/update-query.factory.ts +++ b/server/src/core/api-rest/api-rest-query-builder/factories/update-query.factory.ts @@ -6,13 +6,13 @@ import { mapFieldMetadataToGraphqlQuery } from 'src/core/api-rest/api-rest-query @Injectable() export class UpdateQueryFactory { create(objectMetadata, depth?: number): string { + const objectNameSingular = objectMetadata.objectMetadataItem.nameSingular; + return ` mutation Update${capitalize( - objectMetadata.objectMetadataItem.nameSingular, - )}($id: ID!, $data: CompanyUpdateInput!) { - update${capitalize( - objectMetadata.objectMetadataItem.nameSingular, - )}(id: $id, data: $data) { + objectNameSingular, + )}($id: ID!, $data: ${capitalize(objectNameSingular)}UpdateInput!) { + update${capitalize(objectNameSingular)}(id: $id, data: $data) { id ${objectMetadata.objectMetadataItem.fields .map((field) => diff --git a/server/src/core/auth/services/token.service.spec.ts b/server/src/core/auth/services/token.service.spec.ts index b36b18fdb..25bcb1a61 100644 --- a/server/src/core/auth/services/token.service.spec.ts +++ b/server/src/core/auth/services/token.service.spec.ts @@ -5,6 +5,7 @@ import { getRepositoryToken } from '@nestjs/typeorm'; import { EnvironmentService } from 'src/integrations/environment/environment.service'; import { RefreshToken } from 'src/core/refresh-token/refresh-token.entity'; import { User } from 'src/core/user/user.entity'; +import { JwtAuthStrategy } from 'src/core/auth/strategies/jwt.auth.strategy'; import { TokenService } from './token.service'; @@ -19,6 +20,10 @@ describe('TokenService', () => { provide: JwtService, useValue: {}, }, + { + provide: JwtAuthStrategy, + useValue: {}, + }, { provide: EnvironmentService, useValue: {}, diff --git a/server/src/core/auth/services/token.service.ts b/server/src/core/auth/services/token.service.ts index 225dc9679..438b5fd37 100644 --- a/server/src/core/auth/services/token.service.ts +++ b/server/src/core/auth/services/token.service.ts @@ -11,22 +11,27 @@ import { InjectRepository } from '@nestjs/typeorm'; import { addMilliseconds } from 'date-fns'; import ms from 'ms'; -import { TokenExpiredError } from 'jsonwebtoken'; +import { JsonWebTokenError, TokenExpiredError } from 'jsonwebtoken'; import { Repository } from 'typeorm'; import { Request } from 'express'; import { ExtractJwt } from 'passport-jwt'; -import { JwtPayload } from 'src/core/auth/strategies/jwt.auth.strategy'; +import { + JwtAuthStrategy, + JwtPayload, +} from 'src/core/auth/strategies/jwt.auth.strategy'; import { assert } from 'src/utils/assert'; import { ApiKeyToken, AuthToken } from 'src/core/auth/dto/token.entity'; import { EnvironmentService } from 'src/integrations/environment/environment.service'; import { User } from 'src/core/user/user.entity'; import { RefreshToken } from 'src/core/refresh-token/refresh-token.entity'; +import { Workspace } from 'src/core/workspace/workspace.entity'; @Injectable() export class TokenService { constructor( private readonly jwtService: JwtService, + private readonly jwtStrategy: JwtAuthStrategy, private readonly environmentService: EnvironmentService, @InjectRepository(User, 'core') private readonly userRepository: Repository, @@ -167,18 +172,22 @@ export class TokenService { return { token }; } - async verifyApiKeyToken(request: Request) { + async validateToken(request: Request): Promise { const token = ExtractJwt.fromAuthHeaderAsBearerToken()(request); if (!token) { throw new UnauthorizedException('missing authentication token'); } - const payload = await this.verifyJwt( + const decoded = await this.verifyJwt( token, this.environmentService.getAccessTokenSecret(), ); - return payload.workspaceId; + const { workspace } = await this.jwtStrategy.validate( + decoded as JwtPayload, + ); + + return workspace; } async verifyLoginToken(loginToken: string): Promise { @@ -290,6 +299,8 @@ export class TokenService { } catch (error) { if (error instanceof TokenExpiredError) { throw new UnauthorizedException('Token has expired.'); + } else if (error instanceof JsonWebTokenError) { + throw new UnauthorizedException('Token invalid.'); } else { throw new UnprocessableEntityException(); } diff --git a/server/src/core/auth/strategies/jwt.auth.strategy.ts b/server/src/core/auth/strategies/jwt.auth.strategy.ts index 7b59241eb..487055f8f 100644 --- a/server/src/core/auth/strategies/jwt.auth.strategy.ts +++ b/server/src/core/auth/strategies/jwt.auth.strategy.ts @@ -60,7 +60,7 @@ export class JwtAuthStrategy extends PassportStrategy(Strategy, 'jwt') { ); assert( - apiKey.length === 1 && !apiKey[0].revokedAt, + apiKey.length === 1 && !apiKey?.[0].revokedAt, 'This API Key is revoked', ForbiddenException, );