Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Build exceptions and handler #6459

Merged
merged 6 commits into from
Aug 7, 2024
Merged
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import { CustomException } from 'src/utils/custom-exception';

export class AuthException extends CustomException {
code: AuthExceptionCode;
constructor(message: string, code: AuthExceptionCode) {
super(message, code);
}
}

export enum AuthExceptionCode {
USER_NOT_FOUND = 'USER_NOT_FOUND',
CLIENT_NOT_FOUND = 'CLIENT_NOT_FOUND',
INVALID_INPUT = 'INVALID_INPUT',
FORBIDDEN_EXCEPTION = 'FORBIDDEN_EXCEPTION',
INVALID_DATA = 'INVALID_DATA',
INTERNAL_SERVER_ERROR = 'INTERNAL_SERVER_ERROR',
}
Original file line number Diff line number Diff line change
@@ -1,15 +1,7 @@
import {
BadRequestException,
ForbiddenException,
InternalServerErrorException,
NotFoundException,
UseGuards,
} from '@nestjs/common';
import { UseFilters, UseGuards } from '@nestjs/common';
import { Args, Mutation, Query, Resolver } from '@nestjs/graphql';
import { InjectRepository } from '@nestjs/typeorm';

import { Repository } from 'typeorm';

import { ApiKeyTokenInput } from 'src/engine/core-modules/auth/dto/api-key-token.input';
import { AppTokenInput } from 'src/engine/core-modules/auth/dto/app-token.input';
import { AuthorizeApp } from 'src/engine/core-modules/auth/dto/authorize-app.entity';
Expand All @@ -24,14 +16,14 @@ import { TransientToken } from 'src/engine/core-modules/auth/dto/transient-token
import { UpdatePasswordViaResetTokenInput } from 'src/engine/core-modules/auth/dto/update-password-via-reset-token.input';
import { ValidatePasswordResetToken } from 'src/engine/core-modules/auth/dto/validate-password-reset-token.entity';
import { ValidatePasswordResetTokenInput } from 'src/engine/core-modules/auth/dto/validate-password-reset-token.input';
import { AuthGraphqlApiExceptionFilter } from 'src/engine/core-modules/auth/filters/auth-graphql-api-exception.filter';
import { UserService } from 'src/engine/core-modules/user/services/user.service';
import { User } from 'src/engine/core-modules/user/user.entity';
import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity';
import { AuthUser } from 'src/engine/decorators/auth/auth-user.decorator';
import { AuthWorkspace } from 'src/engine/decorators/auth/auth-workspace.decorator';
import { JwtAuthGuard } from 'src/engine/guards/jwt.auth.guard';
import { CaptchaGuard } from 'src/engine/integrations/captcha/captcha.guard';
import { assert } from 'src/utils/assert';

import { ChallengeInput } from './dto/challenge.input';
import { ImpersonateInput } from './dto/impersonate.input';
Expand All @@ -48,10 +40,10 @@ import { AuthService } from './services/auth.service';
import { TokenService } from './services/token.service';

@Resolver()
@UseFilters(AuthGraphqlApiExceptionFilter)
export class AuthResolver {
constructor(
@InjectRepository(Workspace, 'core')
private readonly workspaceRepository: Repository<Workspace>,
private authService: AuthService,
private tokenService: TokenService,
private userService: UserService,
Expand Down Expand Up @@ -81,16 +73,10 @@ export class AuthResolver {
@Query(() => Workspace)
async findWorkspaceFromInviteHash(
@Args() workspaceInviteHashValidInput: WorkspaceInviteHashValidInput,
) {
const workspace = await this.workspaceRepository.findOneBy({
inviteHash: workspaceInviteHashValidInput.inviteHash,
});

if (!workspace) {
throw new BadRequestException('Workspace does not exist');
}

return workspace;
): Promise<Workspace> {
return await this.authService.findWorkspaceFromInviteHashOrFail(
workspaceInviteHashValidInput.inviteHash,
);
}

@UseGuards(CaptchaGuard)
Expand Down Expand Up @@ -151,8 +137,6 @@ export class AuthResolver {
verifyInput.loginToken,
);

assert(email, 'Invalid token', ForbiddenException);

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I moved the check to the function bellow

const result = await this.authService.verify(email);

return result;
Expand Down Expand Up @@ -188,10 +172,6 @@ export class AuthResolver {

@Mutation(() => AuthTokens)
async renewToken(@Args() args: AppTokenInput): Promise<AuthTokens> {
if (!args.appToken) {
throw new BadRequestException('Refresh token is mendatory');
}

const tokens = await this.tokenService.generateTokensFromRefreshToken(
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

same here

args.appToken,
);
Expand All @@ -205,10 +185,7 @@ export class AuthResolver {
@Args() impersonateInput: ImpersonateInput,
@AuthUser() user: User,
): Promise<Verify> {
// Check if user can impersonate
assert(user.canImpersonate, 'User cannot impersonate', ForbiddenException);

return this.authService.impersonate(impersonateInput.userId);
return await this.authService.impersonate(impersonateInput.userId, user);
}
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

same here


@UseGuards(JwtAuthGuard)
Expand Down Expand Up @@ -240,20 +217,13 @@ export class AuthResolver {

@Mutation(() => InvalidatePassword)
async updatePasswordViaResetToken(
@Args() args: UpdatePasswordViaResetTokenInput,
@Args()
{ passwordResetToken, newPassword }: UpdatePasswordViaResetTokenInput,
): Promise<InvalidatePassword> {
const { id } = await this.tokenService.validatePasswordResetToken(
args.passwordResetToken,
);

assert(id, 'User not found', NotFoundException);

const { success } = await this.authService.updatePassword(
id,
args.newPassword,
);
const { id } =
await this.tokenService.validatePasswordResetToken(passwordResetToken);

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

same here

assert(success, 'Password update failed', InternalServerErrorException);
await this.authService.updatePassword(id, newPassword);

return await this.tokenService.invalidatePasswordResetToken(id);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,17 @@ import {
Get,
Req,
Res,
UnauthorizedException,
UseFilters,
UseGuards,
} from '@nestjs/common';

import { Response } from 'express';

import {
AuthException,
AuthExceptionCode,
} from 'src/engine/core-modules/auth/auth.exception';
import { AuthRestApiExceptionFilter } from 'src/engine/core-modules/auth/filters/auth-rest-api-exception.filter';
import { GoogleAPIsOauthExchangeCodeForTokenGuard } from 'src/engine/core-modules/auth/guards/google-apis-oauth-exchange-code-for-token.guard';
import { GoogleAPIsOauthRequestCodeGuard } from 'src/engine/core-modules/auth/guards/google-apis-oauth-request-code.guard';
import { GoogleAPIsService } from 'src/engine/core-modules/auth/services/google-apis.service';
Expand All @@ -19,6 +24,7 @@ import { EnvironmentService } from 'src/engine/integrations/environment/environm
import { LoadServiceWithWorkspaceContext } from 'src/engine/twenty-orm/context/load-service-with-workspace.context';

@Controller('auth/google-apis')
@UseFilters(AuthRestApiExceptionFilter)
export class GoogleAPIsAuthController {
constructor(
private readonly googleAPIsService: GoogleAPIsService,
Expand Down Expand Up @@ -59,13 +65,17 @@ export class GoogleAPIsAuthController {
const demoWorkspaceIds = this.environmentService.get('DEMO_WORKSPACE_IDS');

if (demoWorkspaceIds.includes(workspaceId)) {
throw new UnauthorizedException(
throw new AuthException(
'Cannot connect Google account to demo workspace',
AuthExceptionCode.FORBIDDEN_EXCEPTION,
);
}

if (!workspaceId) {
throw new Error('Workspace not found');
throw new AuthException(
'Workspace not found',
AuthExceptionCode.INVALID_INPUT,
);
}

const handle = emails[0].value;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,14 +1,23 @@
import { Controller, Get, Req, Res, UseGuards } from '@nestjs/common';
import {
Controller,
Get,
Req,
Res,
UseFilters,
UseGuards,
} from '@nestjs/common';

import { Response } from 'express';

import { GoogleRequest } from 'src/engine/core-modules/auth/strategies/google.auth.strategy';
import { TokenService } from 'src/engine/core-modules/auth/services/token.service';
import { GoogleProviderEnabledGuard } from 'src/engine/core-modules/auth/guards/google-provider-enabled.guard';
import { AuthRestApiExceptionFilter } from 'src/engine/core-modules/auth/filters/auth-rest-api-exception.filter';
import { GoogleOauthGuard } from 'src/engine/core-modules/auth/guards/google-oauth.guard';
import { GoogleProviderEnabledGuard } from 'src/engine/core-modules/auth/guards/google-provider-enabled.guard';
import { AuthService } from 'src/engine/core-modules/auth/services/auth.service';
import { TokenService } from 'src/engine/core-modules/auth/services/token.service';
import { GoogleRequest } from 'src/engine/core-modules/auth/strategies/google.auth.strategy';

@Controller('auth/google')
@UseFilters(AuthRestApiExceptionFilter)
export class GoogleAuthController {
constructor(
private readonly tokenService: TokenService,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,15 +1,24 @@
import { Controller, Get, Req, Res, UseGuards } from '@nestjs/common';
import {
Controller,
Get,
Req,
Res,
UseFilters,
UseGuards,
} from '@nestjs/common';

import { Response } from 'express';

import { TypeORMService } from 'src/database/typeorm/typeorm.service';
import { AuthRestApiExceptionFilter } from 'src/engine/core-modules/auth/filters/auth-rest-api-exception.filter';
import { MicrosoftOAuthGuard } from 'src/engine/core-modules/auth/guards/microsoft-oauth.guard';
import { MicrosoftProviderEnabledGuard } from 'src/engine/core-modules/auth/guards/microsoft-provider-enabled.guard';
import { AuthService } from 'src/engine/core-modules/auth/services/auth.service';
import { TokenService } from 'src/engine/core-modules/auth/services/token.service';
import { MicrosoftRequest } from 'src/engine/core-modules/auth/strategies/microsoft.auth.strategy';

@Controller('auth/microsoft')
@UseFilters(AuthRestApiExceptionFilter)
export class MicrosoftAuthController {
constructor(
private readonly tokenService: TokenService,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
import { Body, Controller, Post } from '@nestjs/common';
import { Body, Controller, Post, UseFilters } from '@nestjs/common';

import { AuthService } from 'src/engine/core-modules/auth/services/auth.service';
import { VerifyInput } from 'src/engine/core-modules/auth/dto/verify.input';
import { Verify } from 'src/engine/core-modules/auth/dto/verify.entity';
import { VerifyInput } from 'src/engine/core-modules/auth/dto/verify.input';
import { AuthRestApiExceptionFilter } from 'src/engine/core-modules/auth/filters/auth-rest-api-exception.filter';
import { AuthService } from 'src/engine/core-modules/auth/services/auth.service';
import { TokenService } from 'src/engine/core-modules/auth/services/token.service';

@Controller('auth/verify')
@UseFilters(AuthRestApiExceptionFilter)
export class VerifyAuthController {
constructor(
private readonly authService: AuthService,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import { Catch } from '@nestjs/common';

import {
AuthException,
AuthExceptionCode,
} from 'src/engine/core-modules/auth/auth.exception';
import {
ForbiddenError,
InternalServerError,
NotFoundError,
UserInputError,
} from 'src/engine/core-modules/graphql/utils/graphql-errors.util';

@Catch(AuthException)
export class AuthGraphqlApiExceptionFilter {
catch(exception: AuthException) {
switch (exception.code) {
case AuthExceptionCode.USER_NOT_FOUND:
case AuthExceptionCode.CLIENT_NOT_FOUND:
throw new NotFoundError(exception.message);
case AuthExceptionCode.INVALID_INPUT:
throw new UserInputError(exception.message);
case AuthExceptionCode.FORBIDDEN_EXCEPTION:
throw new ForbiddenError(exception.message);
case AuthExceptionCode.INVALID_DATA:
case AuthExceptionCode.INTERNAL_SERVER_ERROR:
default:
throw new InternalServerError(exception.message);
thomtrp marked this conversation as resolved.
Show resolved Hide resolved
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import {
ArgumentsHost,
BadRequestException,
Catch,
ExceptionFilter,
InternalServerErrorException,
NotFoundException,
UnauthorizedException,
} from '@nestjs/common';

import {
AuthException,
AuthExceptionCode,
} from 'src/engine/core-modules/auth/auth.exception';

@Catch(AuthException)
export class AuthRestApiExceptionFilter implements ExceptionFilter {
catch(exception: AuthException, _: ArgumentsHost) {
switch (exception.code) {
case AuthExceptionCode.USER_NOT_FOUND:
case AuthExceptionCode.CLIENT_NOT_FOUND:
throw new NotFoundException(exception.message);
case AuthExceptionCode.INVALID_INPUT:
throw new BadRequestException(exception.message);
case AuthExceptionCode.FORBIDDEN_EXCEPTION:
throw new UnauthorizedException(exception.message);
case AuthExceptionCode.INVALID_DATA:
case AuthExceptionCode.INTERNAL_SERVER_ERROR:
default:
throw new InternalServerErrorException(exception.message);
thomtrp marked this conversation as resolved.
Show resolved Hide resolved
}
}
}
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
import {
ExecutionContext,
Injectable,
NotFoundException,
} from '@nestjs/common';
import { ExecutionContext, Injectable } from '@nestjs/common';
import { AuthGuard } from '@nestjs/passport';
import { InjectRepository } from '@nestjs/typeorm';

import { Repository } from 'typeorm';

import {
AuthException,
AuthExceptionCode,
} from 'src/engine/core-modules/auth/auth.exception';
import { TokenService } from 'src/engine/core-modules/auth/services/token.service';
import {
GoogleAPIScopeConfig,
Expand Down Expand Up @@ -39,7 +39,10 @@ export class GoogleAPIsOauthExchangeCodeForTokenGuard extends AuthGuard(
!this.environmentService.get('MESSAGING_PROVIDER_GMAIL_ENABLED') &&
!this.environmentService.get('CALENDAR_PROVIDER_GOOGLE_ENABLED')
) {
throw new NotFoundException('Google apis auth is not enabled');
throw new AuthException(
'Google apis auth is not enabled',
AuthExceptionCode.FORBIDDEN_EXCEPTION,
);
}

const { workspaceId } = await this.tokenService.verifyTransientToken(
Expand Down
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
import {
ExecutionContext,
Injectable,
NotFoundException,
} from '@nestjs/common';
import { ExecutionContext, Injectable } from '@nestjs/common';
import { AuthGuard } from '@nestjs/passport';
import { InjectRepository } from '@nestjs/typeorm';

import { Repository } from 'typeorm';

import {
AuthException,
AuthExceptionCode,
} from 'src/engine/core-modules/auth/auth.exception';
import { TokenService } from 'src/engine/core-modules/auth/services/token.service';
import { GoogleAPIScopeConfig } from 'src/engine/core-modules/auth/strategies/google-apis-oauth-exchange-code-for-token.auth.strategy';
import { GoogleAPIsOauthRequestCodeStrategy } from 'src/engine/core-modules/auth/strategies/google-apis-oauth-request-code.auth.strategy';
Expand Down Expand Up @@ -36,7 +36,10 @@ export class GoogleAPIsOauthRequestCodeGuard extends AuthGuard('google-apis') {
!this.environmentService.get('MESSAGING_PROVIDER_GMAIL_ENABLED') &&
!this.environmentService.get('CALENDAR_PROVIDER_GOOGLE_ENABLED')
) {
throw new NotFoundException('Google apis auth is not enabled');
throw new AuthException(
'Google apis auth is not enabled',
AuthExceptionCode.FORBIDDEN_EXCEPTION,
);
}

const { workspaceId } = await this.tokenService.verifyTransientToken(
Expand Down
Loading
Loading