Build exceptions and handler (#6459)
Adding exceptions and handler for auth services. Tested with: - Workspace creation - Workspace signup - Workspace invitation - Reset password - Adding email account - Impersonation --------- Co-authored-by: Weiko <corentin@twenty.com>
This commit is contained in:
@ -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',
|
||||
}
|
||||
@ -1,14 +1,5 @@
|
||||
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';
|
||||
@ -24,6 +15,7 @@ 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';
|
||||
@ -31,7 +23,6 @@ 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';
|
||||
@ -48,10 +39,9 @@ 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,
|
||||
@ -81,16 +71,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)
|
||||
@ -151,8 +135,6 @@ export class AuthResolver {
|
||||
verifyInput.loginToken,
|
||||
);
|
||||
|
||||
assert(email, 'Invalid token', ForbiddenException);
|
||||
|
||||
const result = await this.authService.verify(email);
|
||||
|
||||
return result;
|
||||
@ -188,10 +170,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(
|
||||
args.appToken,
|
||||
);
|
||||
@ -205,10 +183,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);
|
||||
}
|
||||
|
||||
@UseGuards(JwtAuthGuard)
|
||||
@ -240,20 +215,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,
|
||||
);
|
||||
const { id } =
|
||||
await this.tokenService.validatePasswordResetToken(passwordResetToken);
|
||||
|
||||
assert(id, 'User not found', NotFoundException);
|
||||
|
||||
const { success } = await this.authService.updatePassword(
|
||||
id,
|
||||
args.newPassword,
|
||||
);
|
||||
|
||||
assert(success, 'Password update failed', InternalServerErrorException);
|
||||
await this.authService.updatePassword(id, newPassword);
|
||||
|
||||
return await this.tokenService.invalidatePasswordResetToken(id);
|
||||
}
|
||||
|
||||
@ -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';
|
||||
@ -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,
|
||||
@ -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;
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -1,8 +1,16 @@
|
||||
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';
|
||||
@ -10,6 +18,7 @@ import { TokenService } from 'src/engine/core-modules/auth/services/token.servic
|
||||
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,
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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,
|
||||
@ -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(
|
||||
|
||||
@ -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';
|
||||
@ -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(
|
||||
|
||||
@ -1,9 +1,13 @@
|
||||
import { Injectable, CanActivate, NotFoundException } from '@nestjs/common';
|
||||
import { CanActivate, Injectable } from '@nestjs/common';
|
||||
|
||||
import { Observable } from 'rxjs';
|
||||
|
||||
import { EnvironmentService } from 'src/engine/integrations/environment/environment.service';
|
||||
import {
|
||||
AuthException,
|
||||
AuthExceptionCode,
|
||||
} from 'src/engine/core-modules/auth/auth.exception';
|
||||
import { GoogleStrategy } from 'src/engine/core-modules/auth/strategies/google.auth.strategy';
|
||||
import { EnvironmentService } from 'src/engine/integrations/environment/environment.service';
|
||||
|
||||
@Injectable()
|
||||
export class GoogleProviderEnabledGuard implements CanActivate {
|
||||
@ -11,7 +15,10 @@ export class GoogleProviderEnabledGuard implements CanActivate {
|
||||
|
||||
canActivate(): boolean | Promise<boolean> | Observable<boolean> {
|
||||
if (!this.environmentService.get('AUTH_GOOGLE_ENABLED')) {
|
||||
throw new NotFoundException('Google auth is not enabled');
|
||||
throw new AuthException(
|
||||
'Google auth is not enabled',
|
||||
AuthExceptionCode.FORBIDDEN_EXCEPTION,
|
||||
);
|
||||
}
|
||||
|
||||
new GoogleStrategy(this.environmentService);
|
||||
|
||||
@ -1,9 +1,13 @@
|
||||
import { Injectable, CanActivate, NotFoundException } from '@nestjs/common';
|
||||
import { CanActivate, Injectable } from '@nestjs/common';
|
||||
|
||||
import { Observable } from 'rxjs';
|
||||
|
||||
import { EnvironmentService } from 'src/engine/integrations/environment/environment.service';
|
||||
import {
|
||||
AuthException,
|
||||
AuthExceptionCode,
|
||||
} from 'src/engine/core-modules/auth/auth.exception';
|
||||
import { MicrosoftStrategy } from 'src/engine/core-modules/auth/strategies/microsoft.auth.strategy';
|
||||
import { EnvironmentService } from 'src/engine/integrations/environment/environment.service';
|
||||
|
||||
@Injectable()
|
||||
export class MicrosoftProviderEnabledGuard implements CanActivate {
|
||||
@ -11,7 +15,10 @@ export class MicrosoftProviderEnabledGuard implements CanActivate {
|
||||
|
||||
canActivate(): boolean | Promise<boolean> | Observable<boolean> {
|
||||
if (!this.environmentService.get('AUTH_MICROSOFT_ENABLED')) {
|
||||
throw new NotFoundException('Microsoft auth is not enabled');
|
||||
throw new AuthException(
|
||||
'Microsoft auth is not enabled',
|
||||
AuthExceptionCode.FORBIDDEN_EXCEPTION,
|
||||
);
|
||||
}
|
||||
|
||||
new MicrosoftStrategy(this.environmentService);
|
||||
|
||||
@ -1,45 +1,43 @@
|
||||
import {
|
||||
BadRequestException,
|
||||
ForbiddenException,
|
||||
Injectable,
|
||||
NotFoundException,
|
||||
} from '@nestjs/common';
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { InjectRepository } from '@nestjs/typeorm';
|
||||
|
||||
import crypto from 'node:crypto';
|
||||
|
||||
import { Repository } from 'typeorm';
|
||||
import { render } from '@react-email/components';
|
||||
import { PasswordUpdateNotifyEmail } from 'twenty-emails';
|
||||
import { addMilliseconds } from 'date-fns';
|
||||
import ms from 'ms';
|
||||
import { PasswordUpdateNotifyEmail } from 'twenty-emails';
|
||||
import { Repository } from 'typeorm';
|
||||
|
||||
import { NodeEnvironment } from 'src/engine/integrations/environment/interfaces/node-environment.interface';
|
||||
|
||||
import { ChallengeInput } from 'src/engine/core-modules/auth/dto/challenge.input';
|
||||
import { assert } from 'src/utils/assert';
|
||||
import {
|
||||
AppToken,
|
||||
AppTokenType,
|
||||
} from 'src/engine/core-modules/app-token/app-token.entity';
|
||||
import {
|
||||
AuthException,
|
||||
AuthExceptionCode,
|
||||
} from 'src/engine/core-modules/auth/auth.exception';
|
||||
import {
|
||||
PASSWORD_REGEX,
|
||||
compareHash,
|
||||
hashPassword,
|
||||
} from 'src/engine/core-modules/auth/auth.util';
|
||||
import { Verify } from 'src/engine/core-modules/auth/dto/verify.entity';
|
||||
import { AuthorizeApp } from 'src/engine/core-modules/auth/dto/authorize-app.entity';
|
||||
import { AuthorizeAppInput } from 'src/engine/core-modules/auth/dto/authorize-app.input';
|
||||
import { ChallengeInput } from 'src/engine/core-modules/auth/dto/challenge.input';
|
||||
import { UpdatePassword } from 'src/engine/core-modules/auth/dto/update-password.entity';
|
||||
import { UserExists } from 'src/engine/core-modules/auth/dto/user-exists.entity';
|
||||
import { Verify } from 'src/engine/core-modules/auth/dto/verify.entity';
|
||||
import { WorkspaceInviteHashValid } from 'src/engine/core-modules/auth/dto/workspace-invite-hash-valid.entity';
|
||||
import { SignInUpService } from 'src/engine/core-modules/auth/services/sign-in-up.service';
|
||||
import { WorkspaceMember } from 'src/engine/core-modules/user/dtos/workspace-member.dto';
|
||||
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 { UserService } from 'src/engine/core-modules/user/services/user.service';
|
||||
import { EnvironmentService } from 'src/engine/integrations/environment/environment.service';
|
||||
import { EmailService } from 'src/engine/integrations/email/email.service';
|
||||
import { UpdatePassword } from 'src/engine/core-modules/auth/dto/update-password.entity';
|
||||
import { SignInUpService } from 'src/engine/core-modules/auth/services/sign-in-up.service';
|
||||
import { AuthorizeAppInput } from 'src/engine/core-modules/auth/dto/authorize-app.input';
|
||||
import { AuthorizeApp } from 'src/engine/core-modules/auth/dto/authorize-app.entity';
|
||||
import {
|
||||
AppToken,
|
||||
AppTokenType,
|
||||
} from 'src/engine/core-modules/app-token/app-token.entity';
|
||||
import { WorkspaceMember } from 'src/engine/core-modules/user/dtos/workspace-member.dto';
|
||||
import { EnvironmentService } from 'src/engine/integrations/environment/environment.service';
|
||||
|
||||
import { TokenService } from './token.service';
|
||||
|
||||
@ -70,15 +68,31 @@ export class AuthService {
|
||||
email: challengeInput.email,
|
||||
});
|
||||
|
||||
assert(user, "This user doesn't exist", NotFoundException);
|
||||
assert(user.passwordHash, 'Incorrect login method', ForbiddenException);
|
||||
if (!user) {
|
||||
throw new AuthException(
|
||||
'User not found',
|
||||
AuthExceptionCode.USER_NOT_FOUND,
|
||||
);
|
||||
}
|
||||
|
||||
if (!user.passwordHash) {
|
||||
throw new AuthException(
|
||||
'Incorrect login method',
|
||||
AuthExceptionCode.INVALID_INPUT,
|
||||
);
|
||||
}
|
||||
|
||||
const isValid = await compareHash(
|
||||
challengeInput.password,
|
||||
user.passwordHash,
|
||||
);
|
||||
|
||||
assert(isValid, 'Wrong password', ForbiddenException);
|
||||
if (!isValid) {
|
||||
throw new AuthException(
|
||||
'Wrong password',
|
||||
AuthExceptionCode.FORBIDDEN_EXCEPTION,
|
||||
);
|
||||
}
|
||||
|
||||
return user;
|
||||
}
|
||||
@ -112,6 +126,13 @@ export class AuthService {
|
||||
}
|
||||
|
||||
async verify(email: string): Promise<Verify> {
|
||||
if (!email) {
|
||||
throw new AuthException(
|
||||
'Email is required',
|
||||
AuthExceptionCode.INVALID_INPUT,
|
||||
);
|
||||
}
|
||||
|
||||
const user = await this.userRepository.findOne({
|
||||
where: {
|
||||
email,
|
||||
@ -119,13 +140,19 @@ export class AuthService {
|
||||
relations: ['defaultWorkspace', 'workspaces', 'workspaces.workspace'],
|
||||
});
|
||||
|
||||
assert(user, "This user doesn't exist", NotFoundException);
|
||||
if (!user) {
|
||||
throw new AuthException(
|
||||
'User not found',
|
||||
AuthExceptionCode.USER_NOT_FOUND,
|
||||
);
|
||||
}
|
||||
|
||||
assert(
|
||||
user.defaultWorkspace,
|
||||
'User has no default workspace',
|
||||
NotFoundException,
|
||||
);
|
||||
if (!user.defaultWorkspace) {
|
||||
throw new AuthException(
|
||||
'User has no default workspace',
|
||||
AuthExceptionCode.INVALID_DATA,
|
||||
);
|
||||
}
|
||||
|
||||
// passwordHash is hidden for security reasons
|
||||
user.passwordHash = '';
|
||||
@ -165,18 +192,33 @@ export class AuthService {
|
||||
return { isValid: !!workspace };
|
||||
}
|
||||
|
||||
async impersonate(userId: string) {
|
||||
async impersonate(userIdToImpersonate: string, userImpersonating: User) {
|
||||
if (!userImpersonating.canImpersonate) {
|
||||
throw new AuthException(
|
||||
'User cannot impersonate',
|
||||
AuthExceptionCode.FORBIDDEN_EXCEPTION,
|
||||
);
|
||||
}
|
||||
|
||||
const user = await this.userRepository.findOne({
|
||||
where: {
|
||||
id: userId,
|
||||
id: userIdToImpersonate,
|
||||
},
|
||||
relations: ['defaultWorkspace', 'workspaces', 'workspaces.workspace'],
|
||||
});
|
||||
|
||||
assert(user, "This user doesn't exist", NotFoundException);
|
||||
if (!user) {
|
||||
throw new AuthException(
|
||||
'User not found',
|
||||
AuthExceptionCode.USER_NOT_FOUND,
|
||||
);
|
||||
}
|
||||
|
||||
if (!user.defaultWorkspace.allowImpersonation) {
|
||||
throw new ForbiddenException('Impersonation not allowed');
|
||||
throw new AuthException(
|
||||
'Impersonation not allowed',
|
||||
AuthExceptionCode.FORBIDDEN_EXCEPTION,
|
||||
);
|
||||
}
|
||||
|
||||
const accessToken = await this.tokenService.generateAccessToken(user.id);
|
||||
@ -215,15 +257,24 @@ export class AuthService {
|
||||
const client = apps.find((app) => app.id === clientId);
|
||||
|
||||
if (!client) {
|
||||
throw new NotFoundException(`Invalid client '${clientId}'`);
|
||||
throw new AuthException(
|
||||
`Client not found for '${clientId}'`,
|
||||
AuthExceptionCode.CLIENT_NOT_FOUND,
|
||||
);
|
||||
}
|
||||
|
||||
if (!client.redirectUrl || !authorizeAppInput.redirectUrl) {
|
||||
throw new NotFoundException(`redirectUrl not found for '${clientId}'`);
|
||||
throw new AuthException(
|
||||
`redirectUrl not found for '${clientId}'`,
|
||||
AuthExceptionCode.FORBIDDEN_EXCEPTION,
|
||||
);
|
||||
}
|
||||
|
||||
if (client.redirectUrl !== authorizeAppInput.redirectUrl) {
|
||||
throw new ForbiddenException(`redirectUrl mismatch for '${clientId}'`);
|
||||
throw new AuthException(
|
||||
`redirectUrl mismatch for '${clientId}'`,
|
||||
AuthExceptionCode.FORBIDDEN_EXCEPTION,
|
||||
);
|
||||
}
|
||||
|
||||
const authorizationCode = crypto.randomBytes(42).toString('hex');
|
||||
@ -272,13 +323,30 @@ export class AuthService {
|
||||
userId: string,
|
||||
newPassword: string,
|
||||
): Promise<UpdatePassword> {
|
||||
if (!userId) {
|
||||
throw new AuthException(
|
||||
'User ID is required',
|
||||
AuthExceptionCode.INVALID_INPUT,
|
||||
);
|
||||
}
|
||||
|
||||
const user = await this.userRepository.findOneBy({ id: userId });
|
||||
|
||||
assert(user, 'User not found', NotFoundException);
|
||||
if (!user) {
|
||||
throw new AuthException(
|
||||
'User not found',
|
||||
AuthExceptionCode.USER_NOT_FOUND,
|
||||
);
|
||||
}
|
||||
|
||||
const isPasswordValid = PASSWORD_REGEX.test(newPassword);
|
||||
|
||||
assert(isPasswordValid, 'Password too weak', BadRequestException);
|
||||
if (!isPasswordValid) {
|
||||
throw new AuthException(
|
||||
'Password is too weak',
|
||||
AuthExceptionCode.INVALID_INPUT,
|
||||
);
|
||||
}
|
||||
|
||||
const newPasswordHash = await hashPassword(newPassword);
|
||||
|
||||
@ -311,4 +379,21 @@ export class AuthService {
|
||||
|
||||
return { success: true };
|
||||
}
|
||||
|
||||
async findWorkspaceFromInviteHashOrFail(
|
||||
inviteHash: string,
|
||||
): Promise<Workspace> {
|
||||
const workspace = await this.workspaceRepository.findOneBy({
|
||||
inviteHash,
|
||||
});
|
||||
|
||||
if (!workspace) {
|
||||
throw new AuthException(
|
||||
'Workspace does not exist',
|
||||
AuthExceptionCode.INVALID_INPUT,
|
||||
);
|
||||
}
|
||||
|
||||
return workspace;
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,9 +1,5 @@
|
||||
import { HttpService } from '@nestjs/axios';
|
||||
import {
|
||||
BadRequestException,
|
||||
ForbiddenException,
|
||||
Injectable,
|
||||
} from '@nestjs/common';
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { InjectRepository } from '@nestjs/typeorm';
|
||||
|
||||
import FileType from 'file-type';
|
||||
@ -12,6 +8,10 @@ import { v4 } from 'uuid';
|
||||
|
||||
import { FileFolder } from 'src/engine/core-modules/file/interfaces/file-folder.interface';
|
||||
|
||||
import {
|
||||
AuthException,
|
||||
AuthExceptionCode,
|
||||
} from 'src/engine/core-modules/auth/auth.exception';
|
||||
import {
|
||||
PASSWORD_REGEX,
|
||||
compareHash,
|
||||
@ -26,7 +26,6 @@ import {
|
||||
WorkspaceActivationStatus,
|
||||
} from 'src/engine/core-modules/workspace/workspace.entity';
|
||||
import { EnvironmentService } from 'src/engine/integrations/environment/environment.service';
|
||||
import { assert } from 'src/utils/assert';
|
||||
import { getImageBufferFromUrl } from 'src/utils/image';
|
||||
|
||||
export type SignInUpServiceInput = {
|
||||
@ -66,12 +65,22 @@ export class SignInUpService {
|
||||
if (!firstName) firstName = '';
|
||||
if (!lastName) lastName = '';
|
||||
|
||||
assert(email, 'Email is required', BadRequestException);
|
||||
if (!email) {
|
||||
throw new AuthException(
|
||||
'Email is required',
|
||||
AuthExceptionCode.INVALID_INPUT,
|
||||
);
|
||||
}
|
||||
|
||||
if (password) {
|
||||
const isPasswordValid = PASSWORD_REGEX.test(password);
|
||||
|
||||
assert(isPasswordValid, 'Password too weak', BadRequestException);
|
||||
if (!isPasswordValid) {
|
||||
throw new AuthException(
|
||||
'Password too weak',
|
||||
AuthExceptionCode.INVALID_INPUT,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const passwordHash = password ? await hashPassword(password) : undefined;
|
||||
@ -89,7 +98,12 @@ export class SignInUpService {
|
||||
existingUser.passwordHash,
|
||||
);
|
||||
|
||||
assert(isValid, 'Wrong password', ForbiddenException);
|
||||
if (!isValid) {
|
||||
throw new AuthException(
|
||||
'Wrong password',
|
||||
AuthExceptionCode.FORBIDDEN_EXCEPTION,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if (workspaceInviteHash) {
|
||||
@ -137,17 +151,19 @@ export class SignInUpService {
|
||||
inviteHash: workspaceInviteHash,
|
||||
});
|
||||
|
||||
assert(
|
||||
workspace,
|
||||
'This workspace inviteHash is invalid',
|
||||
ForbiddenException,
|
||||
);
|
||||
if (!workspace) {
|
||||
throw new AuthException(
|
||||
'Invit hash is invalid',
|
||||
AuthExceptionCode.FORBIDDEN_EXCEPTION,
|
||||
);
|
||||
}
|
||||
|
||||
assert(
|
||||
workspace.activationStatus === WorkspaceActivationStatus.ACTIVE,
|
||||
'Workspace is not ready to welcome new members',
|
||||
ForbiddenException,
|
||||
);
|
||||
if (!(workspace.activationStatus === WorkspaceActivationStatus.ACTIVE)) {
|
||||
throw new AuthException(
|
||||
'Workspace is not ready to welcome new members',
|
||||
AuthExceptionCode.FORBIDDEN_EXCEPTION,
|
||||
);
|
||||
}
|
||||
|
||||
if (existingUser) {
|
||||
const updatedUser = await this.userWorkspaceService.addUserToWorkspace(
|
||||
@ -203,11 +219,12 @@ export class SignInUpService {
|
||||
lastName: string;
|
||||
picture: SignInUpServiceInput['picture'];
|
||||
}) {
|
||||
assert(
|
||||
!this.environmentService.get('IS_SIGN_UP_DISABLED'),
|
||||
'Sign up is disabled',
|
||||
ForbiddenException,
|
||||
);
|
||||
if (this.environmentService.get('IS_SIGN_UP_DISABLED')) {
|
||||
throw new AuthException(
|
||||
'Sign up is disabled',
|
||||
AuthExceptionCode.FORBIDDEN_EXCEPTION,
|
||||
);
|
||||
}
|
||||
|
||||
const workspaceToCreate = this.workspaceRepository.create({
|
||||
displayName: '',
|
||||
|
||||
@ -1,8 +1,3 @@
|
||||
import {
|
||||
BadRequestException,
|
||||
InternalServerErrorException,
|
||||
NotFoundException,
|
||||
} from '@nestjs/common';
|
||||
import { Test, TestingModule } from '@nestjs/testing';
|
||||
import { getRepositoryToken } from '@nestjs/typeorm';
|
||||
|
||||
@ -14,6 +9,7 @@ import {
|
||||
AppToken,
|
||||
AppTokenType,
|
||||
} from 'src/engine/core-modules/app-token/app-token.entity';
|
||||
import { AuthException } from 'src/engine/core-modules/auth/auth.exception';
|
||||
import { JwtAuthStrategy } from 'src/engine/core-modules/auth/strategies/jwt.auth.strategy';
|
||||
import { JwtWrapperService } from 'src/engine/core-modules/jwt/services/jwt-wrapper.service';
|
||||
import { User } from 'src/engine/core-modules/user/user.entity';
|
||||
@ -106,7 +102,7 @@ describe('TokenService', () => {
|
||||
expect(result.passwordResetTokenExpiresAt).toBeDefined();
|
||||
});
|
||||
|
||||
it('should throw BadRequestException if an existing valid token is found', async () => {
|
||||
it('should throw AuthException if an existing valid token is found', async () => {
|
||||
const mockUser = { id: '1', email: 'test@example.com' } as User;
|
||||
const mockToken = {
|
||||
userId: '1',
|
||||
@ -120,18 +116,18 @@ describe('TokenService', () => {
|
||||
|
||||
await expect(
|
||||
service.generatePasswordResetToken(mockUser.email),
|
||||
).rejects.toThrow(BadRequestException);
|
||||
).rejects.toThrow(AuthException);
|
||||
});
|
||||
|
||||
it('should throw NotFoundException if no user is found', async () => {
|
||||
it('should throw AuthException if no user is found', async () => {
|
||||
jest.spyOn(userRepository, 'findOneBy').mockResolvedValue(null);
|
||||
|
||||
await expect(
|
||||
service.generatePasswordResetToken('nonexistent@example.com'),
|
||||
).rejects.toThrow(NotFoundException);
|
||||
).rejects.toThrow(AuthException);
|
||||
});
|
||||
|
||||
it('should throw InternalServerErrorException if environment variable is not found', async () => {
|
||||
it('should throw AuthException if environment variable is not found', async () => {
|
||||
const mockUser = { id: '1', email: 'test@example.com' } as User;
|
||||
|
||||
jest.spyOn(userRepository, 'findOneBy').mockResolvedValue(mockUser);
|
||||
@ -139,7 +135,7 @@ describe('TokenService', () => {
|
||||
|
||||
await expect(
|
||||
service.generatePasswordResetToken(mockUser.email),
|
||||
).rejects.toThrow(InternalServerErrorException);
|
||||
).rejects.toThrow(AuthException);
|
||||
});
|
||||
});
|
||||
|
||||
@ -181,17 +177,17 @@ describe('TokenService', () => {
|
||||
expect(result).toEqual({ id: mockUser.id, email: mockUser.email });
|
||||
});
|
||||
|
||||
it('should throw NotFoundException if token is invalid or expired', async () => {
|
||||
it('should throw AuthException if token is invalid or expired', async () => {
|
||||
const resetToken = 'invalid-reset-token';
|
||||
|
||||
jest.spyOn(appTokenRepository, 'findOne').mockResolvedValue(null);
|
||||
|
||||
await expect(
|
||||
service.validatePasswordResetToken(resetToken),
|
||||
).rejects.toThrow(NotFoundException);
|
||||
).rejects.toThrow(AuthException);
|
||||
});
|
||||
|
||||
it('should throw NotFoundException if user does not exist for a valid token', async () => {
|
||||
it('should throw AuthException if user does not exist for a valid token', async () => {
|
||||
const resetToken = 'orphan-token';
|
||||
const hashedToken = crypto
|
||||
.createHash('sha256')
|
||||
@ -212,10 +208,10 @@ describe('TokenService', () => {
|
||||
|
||||
await expect(
|
||||
service.validatePasswordResetToken(resetToken),
|
||||
).rejects.toThrow(NotFoundException);
|
||||
).rejects.toThrow(AuthException);
|
||||
});
|
||||
|
||||
it('should throw NotFoundException if token is revoked', async () => {
|
||||
it('should throw AuthException if token is revoked', async () => {
|
||||
const resetToken = 'revoked-token';
|
||||
const hashedToken = crypto
|
||||
.createHash('sha256')
|
||||
@ -234,7 +230,7 @@ describe('TokenService', () => {
|
||||
.mockResolvedValue(mockToken as AppToken);
|
||||
await expect(
|
||||
service.validatePasswordResetToken(resetToken),
|
||||
).rejects.toThrow(NotFoundException);
|
||||
).rejects.toThrow(AuthException);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@ -1,12 +1,4 @@
|
||||
import {
|
||||
BadRequestException,
|
||||
ForbiddenException,
|
||||
Injectable,
|
||||
InternalServerErrorException,
|
||||
NotFoundException,
|
||||
UnauthorizedException,
|
||||
UnprocessableEntityException,
|
||||
} from '@nestjs/common';
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { InjectRepository } from '@nestjs/typeorm';
|
||||
|
||||
import crypto from 'crypto';
|
||||
@ -24,6 +16,10 @@ import {
|
||||
AppToken,
|
||||
AppTokenType,
|
||||
} from 'src/engine/core-modules/app-token/app-token.entity';
|
||||
import {
|
||||
AuthException,
|
||||
AuthExceptionCode,
|
||||
} from 'src/engine/core-modules/auth/auth.exception';
|
||||
import { EmailPasswordResetLink } from 'src/engine/core-modules/auth/dto/email-password-reset-link.entity';
|
||||
import { ExchangeAuthCode } from 'src/engine/core-modules/auth/dto/exchange-auth-code.entity';
|
||||
import { ExchangeAuthCodeInput } from 'src/engine/core-modules/auth/dto/exchange-auth-code.input';
|
||||
@ -45,7 +41,6 @@ import { User } from 'src/engine/core-modules/user/user.entity';
|
||||
import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity';
|
||||
import { EmailService } from 'src/engine/integrations/email/email.service';
|
||||
import { EnvironmentService } from 'src/engine/integrations/environment/environment.service';
|
||||
import { assert } from 'src/utils/assert';
|
||||
|
||||
@Injectable()
|
||||
export class TokenService {
|
||||
@ -68,7 +63,13 @@ export class TokenService {
|
||||
): Promise<AuthToken> {
|
||||
const expiresIn = this.environmentService.get('ACCESS_TOKEN_EXPIRES_IN');
|
||||
|
||||
assert(expiresIn, '', InternalServerErrorException);
|
||||
if (!expiresIn) {
|
||||
throw new AuthException(
|
||||
'Expiration time for access token is not set',
|
||||
AuthExceptionCode.INTERNAL_SERVER_ERROR,
|
||||
);
|
||||
}
|
||||
|
||||
const expiresAt = addMilliseconds(new Date().getTime(), ms(expiresIn));
|
||||
|
||||
const user = await this.userRepository.findOne({
|
||||
@ -77,11 +78,17 @@ export class TokenService {
|
||||
});
|
||||
|
||||
if (!user) {
|
||||
throw new NotFoundException('User is not found');
|
||||
throw new AuthException(
|
||||
'User is not found',
|
||||
AuthExceptionCode.INVALID_INPUT,
|
||||
);
|
||||
}
|
||||
|
||||
if (!user.defaultWorkspace) {
|
||||
throw new NotFoundException('User does not have a default workspace');
|
||||
throw new AuthException(
|
||||
'User does not have a default workspace',
|
||||
AuthExceptionCode.INVALID_DATA,
|
||||
);
|
||||
}
|
||||
|
||||
const jwtPayload: JwtPayload = {
|
||||
@ -99,7 +106,13 @@ export class TokenService {
|
||||
const secret = this.environmentService.get('REFRESH_TOKEN_SECRET');
|
||||
const expiresIn = this.environmentService.get('REFRESH_TOKEN_EXPIRES_IN');
|
||||
|
||||
assert(expiresIn, '', InternalServerErrorException);
|
||||
if (!expiresIn) {
|
||||
throw new AuthException(
|
||||
'Expiration time for access token is not set',
|
||||
AuthExceptionCode.INTERNAL_SERVER_ERROR,
|
||||
);
|
||||
}
|
||||
|
||||
const expiresAt = addMilliseconds(new Date().getTime(), ms(expiresIn));
|
||||
|
||||
const refreshTokenPayload = {
|
||||
@ -130,7 +143,13 @@ export class TokenService {
|
||||
const secret = this.environmentService.get('LOGIN_TOKEN_SECRET');
|
||||
const expiresIn = this.environmentService.get('LOGIN_TOKEN_EXPIRES_IN');
|
||||
|
||||
assert(expiresIn, '', InternalServerErrorException);
|
||||
if (!expiresIn) {
|
||||
throw new AuthException(
|
||||
'Expiration time for access token is not set',
|
||||
AuthExceptionCode.INTERNAL_SERVER_ERROR,
|
||||
);
|
||||
}
|
||||
|
||||
const expiresAt = addMilliseconds(new Date().getTime(), ms(expiresIn));
|
||||
const jwtPayload = {
|
||||
sub: email,
|
||||
@ -155,7 +174,13 @@ export class TokenService {
|
||||
'SHORT_TERM_TOKEN_EXPIRES_IN',
|
||||
);
|
||||
|
||||
assert(expiresIn, '', InternalServerErrorException);
|
||||
if (!expiresIn) {
|
||||
throw new AuthException(
|
||||
'Expiration time for access token is not set',
|
||||
AuthExceptionCode.INTERNAL_SERVER_ERROR,
|
||||
);
|
||||
}
|
||||
|
||||
const expiresAt = addMilliseconds(new Date().getTime(), ms(expiresIn));
|
||||
const jwtPayload = {
|
||||
sub: workspaceMemberId,
|
||||
@ -212,7 +237,10 @@ export class TokenService {
|
||||
const token = ExtractJwt.fromAuthHeaderAsBearerToken()(request);
|
||||
|
||||
if (!token) {
|
||||
throw new UnauthorizedException('missing authentication token');
|
||||
throw new AuthException(
|
||||
'missing authentication token',
|
||||
AuthExceptionCode.FORBIDDEN_EXCEPTION,
|
||||
);
|
||||
}
|
||||
const decoded = await this.verifyJwt(
|
||||
token,
|
||||
@ -257,22 +285,35 @@ export class TokenService {
|
||||
): Promise<AuthTokens> {
|
||||
const userExists = await this.userRepository.findBy({ id: user.id });
|
||||
|
||||
assert(userExists, 'User not found', NotFoundException);
|
||||
if (!userExists) {
|
||||
throw new AuthException(
|
||||
'User not found',
|
||||
AuthExceptionCode.INVALID_INPUT,
|
||||
);
|
||||
}
|
||||
|
||||
const workspace = await this.workspaceRepository.findOne({
|
||||
where: { id: workspaceId },
|
||||
relations: ['workspaceUsers'],
|
||||
});
|
||||
|
||||
assert(workspace, 'workspace doesnt exist', NotFoundException);
|
||||
if (!workspace) {
|
||||
throw new AuthException(
|
||||
'workspace doesnt exist',
|
||||
AuthExceptionCode.INVALID_INPUT,
|
||||
);
|
||||
}
|
||||
|
||||
assert(
|
||||
workspace.workspaceUsers
|
||||
if (
|
||||
!workspace.workspaceUsers
|
||||
.map((userWorkspace) => userWorkspace.userId)
|
||||
.includes(user.id),
|
||||
'user does not belong to workspace',
|
||||
ForbiddenException,
|
||||
);
|
||||
.includes(user.id)
|
||||
) {
|
||||
throw new AuthException(
|
||||
'user does not belong to workspace',
|
||||
AuthExceptionCode.FORBIDDEN_EXCEPTION,
|
||||
);
|
||||
}
|
||||
|
||||
await this.userRepository.save({
|
||||
id: user.id,
|
||||
@ -293,29 +334,17 @@ export class TokenService {
|
||||
async verifyAuthorizationCode(
|
||||
exchangeAuthCodeInput: ExchangeAuthCodeInput,
|
||||
): Promise<ExchangeAuthCode> {
|
||||
const { authorizationCode, codeVerifier, clientSecret } =
|
||||
exchangeAuthCodeInput;
|
||||
const { authorizationCode, codeVerifier } = exchangeAuthCodeInput;
|
||||
|
||||
assert(
|
||||
authorizationCode,
|
||||
'Authorization code not found',
|
||||
NotFoundException,
|
||||
);
|
||||
|
||||
assert(
|
||||
!codeVerifier || !clientSecret,
|
||||
'client secret or code verifier not found',
|
||||
NotFoundException,
|
||||
);
|
||||
if (!authorizationCode) {
|
||||
throw new AuthException(
|
||||
'Authorization code not found',
|
||||
AuthExceptionCode.INVALID_INPUT,
|
||||
);
|
||||
}
|
||||
|
||||
let userId = '';
|
||||
|
||||
if (clientSecret) {
|
||||
// TODO: replace this with call to third party apps table
|
||||
// assert(client.secret, 'client secret code does not exist', ForbiddenException);
|
||||
throw new ForbiddenException();
|
||||
}
|
||||
|
||||
if (codeVerifier) {
|
||||
const authorizationCodeAppToken = await this.appTokenRepository.findOne({
|
||||
where: {
|
||||
@ -323,17 +352,19 @@ export class TokenService {
|
||||
},
|
||||
});
|
||||
|
||||
assert(
|
||||
authorizationCodeAppToken,
|
||||
'Authorization code does not exist',
|
||||
NotFoundException,
|
||||
);
|
||||
if (!authorizationCodeAppToken) {
|
||||
throw new AuthException(
|
||||
'Authorization code does not exist',
|
||||
AuthExceptionCode.INVALID_INPUT,
|
||||
);
|
||||
}
|
||||
|
||||
assert(
|
||||
authorizationCodeAppToken.expiresAt.getTime() >= Date.now(),
|
||||
'Authorization code expired.',
|
||||
ForbiddenException,
|
||||
);
|
||||
if (!(authorizationCodeAppToken.expiresAt.getTime() >= Date.now())) {
|
||||
throw new AuthException(
|
||||
'Authorization code expired.',
|
||||
AuthExceptionCode.FORBIDDEN_EXCEPTION,
|
||||
);
|
||||
}
|
||||
|
||||
const codeChallenge = crypto
|
||||
.createHash('sha256')
|
||||
@ -350,26 +381,32 @@ export class TokenService {
|
||||
},
|
||||
});
|
||||
|
||||
assert(
|
||||
codeChallengeAppToken,
|
||||
'code verifier doesnt match the challenge',
|
||||
ForbiddenException,
|
||||
);
|
||||
if (!codeChallengeAppToken) {
|
||||
throw new AuthException(
|
||||
'code verifier doesnt match the challenge',
|
||||
AuthExceptionCode.FORBIDDEN_EXCEPTION,
|
||||
);
|
||||
}
|
||||
|
||||
assert(
|
||||
codeChallengeAppToken.expiresAt.getTime() >= Date.now(),
|
||||
'code challenge expired.',
|
||||
ForbiddenException,
|
||||
);
|
||||
if (!(codeChallengeAppToken.expiresAt.getTime() >= Date.now())) {
|
||||
throw new AuthException(
|
||||
'code challenge expired.',
|
||||
AuthExceptionCode.FORBIDDEN_EXCEPTION,
|
||||
);
|
||||
}
|
||||
|
||||
assert(
|
||||
codeChallengeAppToken.userId === authorizationCodeAppToken.userId,
|
||||
'authorization code / code verifier was not created by same client',
|
||||
ForbiddenException,
|
||||
);
|
||||
if (codeChallengeAppToken.userId !== authorizationCodeAppToken.userId) {
|
||||
throw new AuthException(
|
||||
'authorization code / code verifier was not created by same client',
|
||||
AuthExceptionCode.FORBIDDEN_EXCEPTION,
|
||||
);
|
||||
}
|
||||
|
||||
if (codeChallengeAppToken.revokedAt) {
|
||||
throw new ForbiddenException('Token has been revoked.');
|
||||
throw new AuthException(
|
||||
'Token has been revoked.',
|
||||
AuthExceptionCode.FORBIDDEN_EXCEPTION,
|
||||
);
|
||||
}
|
||||
|
||||
await this.appTokenRepository.save({
|
||||
@ -386,13 +423,17 @@ export class TokenService {
|
||||
});
|
||||
|
||||
if (!user) {
|
||||
throw new NotFoundException(
|
||||
throw new AuthException(
|
||||
'User who generated the token does not exist',
|
||||
AuthExceptionCode.INVALID_INPUT,
|
||||
);
|
||||
}
|
||||
|
||||
if (!user.defaultWorkspace) {
|
||||
throw new NotFoundException('User does not have a default workspace');
|
||||
throw new AuthException(
|
||||
'User does not have a default workspace',
|
||||
AuthExceptionCode.INVALID_DATA,
|
||||
);
|
||||
}
|
||||
|
||||
const accessToken = await this.generateAccessToken(
|
||||
@ -414,24 +455,35 @@ export class TokenService {
|
||||
const coolDown = this.environmentService.get('REFRESH_TOKEN_COOL_DOWN');
|
||||
const jwtPayload = await this.verifyJwt(refreshToken, secret);
|
||||
|
||||
assert(
|
||||
jwtPayload.jti && jwtPayload.sub,
|
||||
'This refresh token is malformed',
|
||||
UnprocessableEntityException,
|
||||
);
|
||||
if (!(jwtPayload.jti && jwtPayload.sub)) {
|
||||
throw new AuthException(
|
||||
'This refresh token is malformed',
|
||||
AuthExceptionCode.INVALID_INPUT,
|
||||
);
|
||||
}
|
||||
|
||||
const token = await this.appTokenRepository.findOneBy({
|
||||
id: jwtPayload.jti,
|
||||
});
|
||||
|
||||
assert(token, "This refresh token doesn't exist", NotFoundException);
|
||||
if (!token) {
|
||||
throw new AuthException(
|
||||
"This refresh token doesn't exist",
|
||||
AuthExceptionCode.INVALID_INPUT,
|
||||
);
|
||||
}
|
||||
|
||||
const user = await this.userRepository.findOne({
|
||||
where: { id: jwtPayload.sub },
|
||||
relations: ['appTokens'],
|
||||
});
|
||||
|
||||
assert(user, 'User not found', NotFoundException);
|
||||
if (!user) {
|
||||
throw new AuthException(
|
||||
'User not found',
|
||||
AuthExceptionCode.INVALID_INPUT,
|
||||
);
|
||||
}
|
||||
|
||||
// Check if revokedAt is less than coolDown
|
||||
if (
|
||||
@ -452,8 +504,9 @@ export class TokenService {
|
||||
}),
|
||||
);
|
||||
|
||||
throw new ForbiddenException(
|
||||
'Suspicious activity detected, this refresh token has been revoked. All tokens has been revoked.',
|
||||
throw new AuthException(
|
||||
'Suspicious activity detected, this refresh token has been revoked. All tokens have been revoked.',
|
||||
AuthExceptionCode.FORBIDDEN_EXCEPTION,
|
||||
);
|
||||
}
|
||||
|
||||
@ -464,6 +517,13 @@ export class TokenService {
|
||||
accessToken: AuthToken;
|
||||
refreshToken: AuthToken;
|
||||
}> {
|
||||
if (!token) {
|
||||
throw new AuthException(
|
||||
'Refresh token not found',
|
||||
AuthExceptionCode.INVALID_INPUT,
|
||||
);
|
||||
}
|
||||
|
||||
const {
|
||||
user,
|
||||
token: { id },
|
||||
@ -502,11 +562,20 @@ export class TokenService {
|
||||
);
|
||||
} catch (error) {
|
||||
if (error instanceof TokenExpiredError) {
|
||||
throw new UnauthorizedException('Token has expired.');
|
||||
throw new AuthException(
|
||||
'Token has expired.',
|
||||
AuthExceptionCode.FORBIDDEN_EXCEPTION,
|
||||
);
|
||||
} else if (error instanceof JsonWebTokenError) {
|
||||
throw new UnauthorizedException('Token invalid.');
|
||||
throw new AuthException(
|
||||
'Token invalid.',
|
||||
AuthExceptionCode.FORBIDDEN_EXCEPTION,
|
||||
);
|
||||
} else {
|
||||
throw new UnprocessableEntityException();
|
||||
throw new AuthException(
|
||||
'Unknown token error.',
|
||||
AuthExceptionCode.INVALID_INPUT,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -516,17 +585,23 @@ export class TokenService {
|
||||
email,
|
||||
});
|
||||
|
||||
assert(user, 'User not found', NotFoundException);
|
||||
if (!user) {
|
||||
throw new AuthException(
|
||||
'User not found',
|
||||
AuthExceptionCode.INVALID_INPUT,
|
||||
);
|
||||
}
|
||||
|
||||
const expiresIn = this.environmentService.get(
|
||||
'PASSWORD_RESET_TOKEN_EXPIRES_IN',
|
||||
);
|
||||
|
||||
assert(
|
||||
expiresIn,
|
||||
'PASSWORD_RESET_TOKEN_EXPIRES_IN constant value not found',
|
||||
InternalServerErrorException,
|
||||
);
|
||||
if (!expiresIn) {
|
||||
throw new AuthException(
|
||||
'PASSWORD_RESET_TOKEN_EXPIRES_IN constant value not found',
|
||||
AuthExceptionCode.INTERNAL_SERVER_ERROR,
|
||||
);
|
||||
}
|
||||
|
||||
const existingToken = await this.appTokenRepository.findOne({
|
||||
where: {
|
||||
@ -543,10 +618,9 @@ export class TokenService {
|
||||
{ long: true },
|
||||
);
|
||||
|
||||
assert(
|
||||
false,
|
||||
throw new AuthException(
|
||||
`Token has already been generated. Please wait for ${timeToWait} to generate again.`,
|
||||
BadRequestException,
|
||||
AuthExceptionCode.INVALID_INPUT,
|
||||
);
|
||||
}
|
||||
|
||||
@ -579,7 +653,12 @@ export class TokenService {
|
||||
email,
|
||||
});
|
||||
|
||||
assert(user, 'User not found', NotFoundException);
|
||||
if (!user) {
|
||||
throw new AuthException(
|
||||
'User not found',
|
||||
AuthExceptionCode.INVALID_INPUT,
|
||||
);
|
||||
}
|
||||
|
||||
const frontBaseURL = this.environmentService.get('FRONT_BASE_URL');
|
||||
const resetLink = `${frontBaseURL}/reset-password/${resetToken.passwordResetToken}`;
|
||||
@ -636,13 +715,23 @@ export class TokenService {
|
||||
},
|
||||
});
|
||||
|
||||
assert(token, 'Token is invalid', NotFoundException);
|
||||
if (!token) {
|
||||
throw new AuthException(
|
||||
'Token is invalid',
|
||||
AuthExceptionCode.FORBIDDEN_EXCEPTION,
|
||||
);
|
||||
}
|
||||
|
||||
const user = await this.userRepository.findOneBy({
|
||||
id: token.userId,
|
||||
});
|
||||
|
||||
assert(user, 'Token is invalid', NotFoundException);
|
||||
if (!user) {
|
||||
throw new AuthException(
|
||||
'User not found',
|
||||
AuthExceptionCode.INVALID_INPUT,
|
||||
);
|
||||
}
|
||||
|
||||
return {
|
||||
id: user.id,
|
||||
@ -657,7 +746,12 @@ export class TokenService {
|
||||
id: userId,
|
||||
});
|
||||
|
||||
assert(user, 'User not found', NotFoundException);
|
||||
if (!user) {
|
||||
throw new AuthException(
|
||||
'User not found',
|
||||
AuthExceptionCode.INVALID_INPUT,
|
||||
);
|
||||
}
|
||||
|
||||
await this.appTokenRepository.update(
|
||||
{
|
||||
|
||||
@ -1,8 +1,4 @@
|
||||
import {
|
||||
ForbiddenException,
|
||||
Injectable,
|
||||
UnauthorizedException,
|
||||
} from '@nestjs/common';
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { PassportStrategy } from '@nestjs/passport';
|
||||
import { InjectRepository } from '@nestjs/typeorm';
|
||||
|
||||
@ -10,12 +6,15 @@ import { ExtractJwt, Strategy } from 'passport-jwt';
|
||||
import { Repository } from 'typeorm';
|
||||
|
||||
import { TypeORMService } from 'src/database/typeorm/typeorm.service';
|
||||
import {
|
||||
AuthException,
|
||||
AuthExceptionCode,
|
||||
} from 'src/engine/core-modules/auth/auth.exception';
|
||||
import { AuthContext } from 'src/engine/core-modules/auth/types/auth-context.type';
|
||||
import { User } from 'src/engine/core-modules/user/user.entity';
|
||||
import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity';
|
||||
import { EnvironmentService } from 'src/engine/integrations/environment/environment.service';
|
||||
import { DataSourceService } from 'src/engine/metadata-modules/data-source/data-source.service';
|
||||
import { assert } from 'src/utils/assert';
|
||||
import { AuthContext } from 'src/engine/core-modules/auth/types/auth-context.type';
|
||||
import { ApiKeyWorkspaceEntity } from 'src/modules/api-key/standard-objects/api-key.workspace-entity';
|
||||
|
||||
export type JwtPayload = { sub: string; workspaceId: string; jti?: string };
|
||||
@ -46,8 +45,12 @@ export class JwtAuthStrategy extends PassportStrategy(Strategy, 'jwt') {
|
||||
let apiKey: ApiKeyWorkspaceEntity | null = null;
|
||||
|
||||
if (!workspace) {
|
||||
throw new UnauthorizedException();
|
||||
throw new AuthException(
|
||||
'Workspace not found',
|
||||
AuthExceptionCode.INVALID_INPUT,
|
||||
);
|
||||
}
|
||||
|
||||
if (payload.jti) {
|
||||
// TODO: Check why it's not working
|
||||
// const apiKeyRepository =
|
||||
@ -71,11 +74,12 @@ export class JwtAuthStrategy extends PassportStrategy(Strategy, 'jwt') {
|
||||
|
||||
apiKey = res?.[0];
|
||||
|
||||
assert(
|
||||
apiKey && !apiKey.revokedAt,
|
||||
'This API Key is revoked',
|
||||
ForbiddenException,
|
||||
);
|
||||
if (!apiKey || apiKey.revokedAt) {
|
||||
throw new AuthException(
|
||||
'This API Key is revoked',
|
||||
AuthExceptionCode.FORBIDDEN_EXCEPTION,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if (payload.workspaceId) {
|
||||
@ -84,7 +88,10 @@ export class JwtAuthStrategy extends PassportStrategy(Strategy, 'jwt') {
|
||||
relations: ['defaultWorkspace'],
|
||||
});
|
||||
if (!user) {
|
||||
throw new UnauthorizedException();
|
||||
throw new AuthException(
|
||||
'User not found',
|
||||
AuthExceptionCode.INVALID_INPUT,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -1,10 +1,13 @@
|
||||
import { BadRequestException } from '@nestjs/common';
|
||||
import { PassportStrategy } from '@nestjs/passport';
|
||||
|
||||
import { Request } from 'express';
|
||||
import { VerifyCallback } from 'passport-google-oauth20';
|
||||
import { Strategy } from 'passport-microsoft';
|
||||
|
||||
import {
|
||||
AuthException,
|
||||
AuthExceptionCode,
|
||||
} from 'src/engine/core-modules/auth/auth.exception';
|
||||
import { EnvironmentService } from 'src/engine/integrations/environment/environment.service';
|
||||
|
||||
export type MicrosoftRequest = Omit<
|
||||
@ -60,7 +63,10 @@ export class MicrosoftStrategy extends PassportStrategy(Strategy, 'microsoft') {
|
||||
const email = emails?.[0]?.value ?? null;
|
||||
|
||||
if (!email) {
|
||||
throw new BadRequestException('No email found in your Microsoft profile');
|
||||
throw new AuthException(
|
||||
'Email not found',
|
||||
AuthExceptionCode.INVALID_INPUT,
|
||||
);
|
||||
}
|
||||
|
||||
const user: MicrosoftRequest['user'] = {
|
||||
|
||||
@ -1,3 +1,7 @@
|
||||
import {
|
||||
AuthException,
|
||||
AuthExceptionCode,
|
||||
} from 'src/engine/core-modules/auth/auth.exception';
|
||||
import { GoogleAPIsRequest } from 'src/engine/core-modules/auth/types/google-api-request.type';
|
||||
|
||||
type GoogleAPIsRequestExtraParams = {
|
||||
@ -19,7 +23,10 @@ export const setRequestExtraParams = (
|
||||
} = params;
|
||||
|
||||
if (!transientToken) {
|
||||
throw new Error('transientToken is required');
|
||||
throw new AuthException(
|
||||
'transientToken is required',
|
||||
AuthExceptionCode.INVALID_INPUT,
|
||||
);
|
||||
}
|
||||
|
||||
request.params.transientToken = transientToken;
|
||||
|
||||
Reference in New Issue
Block a user