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 {
|
import { UseFilters, UseGuards } from '@nestjs/common';
|
||||||
BadRequestException,
|
|
||||||
ForbiddenException,
|
|
||||||
InternalServerErrorException,
|
|
||||||
NotFoundException,
|
|
||||||
UseGuards,
|
|
||||||
} from '@nestjs/common';
|
|
||||||
import { Args, Mutation, Query, Resolver } from '@nestjs/graphql';
|
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 { 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 { 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 { 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 { 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 { 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 { UserService } from 'src/engine/core-modules/user/services/user.service';
|
||||||
import { User } from 'src/engine/core-modules/user/user.entity';
|
import { User } from 'src/engine/core-modules/user/user.entity';
|
||||||
import { Workspace } from 'src/engine/core-modules/workspace/workspace.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 { AuthWorkspace } from 'src/engine/decorators/auth/auth-workspace.decorator';
|
||||||
import { JwtAuthGuard } from 'src/engine/guards/jwt.auth.guard';
|
import { JwtAuthGuard } from 'src/engine/guards/jwt.auth.guard';
|
||||||
import { CaptchaGuard } from 'src/engine/integrations/captcha/captcha.guard';
|
import { CaptchaGuard } from 'src/engine/integrations/captcha/captcha.guard';
|
||||||
import { assert } from 'src/utils/assert';
|
|
||||||
|
|
||||||
import { ChallengeInput } from './dto/challenge.input';
|
import { ChallengeInput } from './dto/challenge.input';
|
||||||
import { ImpersonateInput } from './dto/impersonate.input';
|
import { ImpersonateInput } from './dto/impersonate.input';
|
||||||
@ -48,10 +39,9 @@ import { AuthService } from './services/auth.service';
|
|||||||
import { TokenService } from './services/token.service';
|
import { TokenService } from './services/token.service';
|
||||||
|
|
||||||
@Resolver()
|
@Resolver()
|
||||||
|
@UseFilters(AuthGraphqlApiExceptionFilter)
|
||||||
export class AuthResolver {
|
export class AuthResolver {
|
||||||
constructor(
|
constructor(
|
||||||
@InjectRepository(Workspace, 'core')
|
|
||||||
private readonly workspaceRepository: Repository<Workspace>,
|
|
||||||
private authService: AuthService,
|
private authService: AuthService,
|
||||||
private tokenService: TokenService,
|
private tokenService: TokenService,
|
||||||
private userService: UserService,
|
private userService: UserService,
|
||||||
@ -81,16 +71,10 @@ export class AuthResolver {
|
|||||||
@Query(() => Workspace)
|
@Query(() => Workspace)
|
||||||
async findWorkspaceFromInviteHash(
|
async findWorkspaceFromInviteHash(
|
||||||
@Args() workspaceInviteHashValidInput: WorkspaceInviteHashValidInput,
|
@Args() workspaceInviteHashValidInput: WorkspaceInviteHashValidInput,
|
||||||
) {
|
): Promise<Workspace> {
|
||||||
const workspace = await this.workspaceRepository.findOneBy({
|
return await this.authService.findWorkspaceFromInviteHashOrFail(
|
||||||
inviteHash: workspaceInviteHashValidInput.inviteHash,
|
workspaceInviteHashValidInput.inviteHash,
|
||||||
});
|
);
|
||||||
|
|
||||||
if (!workspace) {
|
|
||||||
throw new BadRequestException('Workspace does not exist');
|
|
||||||
}
|
|
||||||
|
|
||||||
return workspace;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@UseGuards(CaptchaGuard)
|
@UseGuards(CaptchaGuard)
|
||||||
@ -151,8 +135,6 @@ export class AuthResolver {
|
|||||||
verifyInput.loginToken,
|
verifyInput.loginToken,
|
||||||
);
|
);
|
||||||
|
|
||||||
assert(email, 'Invalid token', ForbiddenException);
|
|
||||||
|
|
||||||
const result = await this.authService.verify(email);
|
const result = await this.authService.verify(email);
|
||||||
|
|
||||||
return result;
|
return result;
|
||||||
@ -188,10 +170,6 @@ export class AuthResolver {
|
|||||||
|
|
||||||
@Mutation(() => AuthTokens)
|
@Mutation(() => AuthTokens)
|
||||||
async renewToken(@Args() args: AppTokenInput): Promise<AuthTokens> {
|
async renewToken(@Args() args: AppTokenInput): Promise<AuthTokens> {
|
||||||
if (!args.appToken) {
|
|
||||||
throw new BadRequestException('Refresh token is mendatory');
|
|
||||||
}
|
|
||||||
|
|
||||||
const tokens = await this.tokenService.generateTokensFromRefreshToken(
|
const tokens = await this.tokenService.generateTokensFromRefreshToken(
|
||||||
args.appToken,
|
args.appToken,
|
||||||
);
|
);
|
||||||
@ -205,10 +183,7 @@ export class AuthResolver {
|
|||||||
@Args() impersonateInput: ImpersonateInput,
|
@Args() impersonateInput: ImpersonateInput,
|
||||||
@AuthUser() user: User,
|
@AuthUser() user: User,
|
||||||
): Promise<Verify> {
|
): Promise<Verify> {
|
||||||
// Check if user can impersonate
|
return await this.authService.impersonate(impersonateInput.userId, user);
|
||||||
assert(user.canImpersonate, 'User cannot impersonate', ForbiddenException);
|
|
||||||
|
|
||||||
return this.authService.impersonate(impersonateInput.userId);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@UseGuards(JwtAuthGuard)
|
@UseGuards(JwtAuthGuard)
|
||||||
@ -240,20 +215,13 @@ export class AuthResolver {
|
|||||||
|
|
||||||
@Mutation(() => InvalidatePassword)
|
@Mutation(() => InvalidatePassword)
|
||||||
async updatePasswordViaResetToken(
|
async updatePasswordViaResetToken(
|
||||||
@Args() args: UpdatePasswordViaResetTokenInput,
|
@Args()
|
||||||
|
{ passwordResetToken, newPassword }: UpdatePasswordViaResetTokenInput,
|
||||||
): Promise<InvalidatePassword> {
|
): Promise<InvalidatePassword> {
|
||||||
const { id } = await this.tokenService.validatePasswordResetToken(
|
const { id } =
|
||||||
args.passwordResetToken,
|
await this.tokenService.validatePasswordResetToken(passwordResetToken);
|
||||||
);
|
|
||||||
|
|
||||||
assert(id, 'User not found', NotFoundException);
|
await this.authService.updatePassword(id, newPassword);
|
||||||
|
|
||||||
const { success } = await this.authService.updatePassword(
|
|
||||||
id,
|
|
||||||
args.newPassword,
|
|
||||||
);
|
|
||||||
|
|
||||||
assert(success, 'Password update failed', InternalServerErrorException);
|
|
||||||
|
|
||||||
return await this.tokenService.invalidatePasswordResetToken(id);
|
return await this.tokenService.invalidatePasswordResetToken(id);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -3,12 +3,17 @@ import {
|
|||||||
Get,
|
Get,
|
||||||
Req,
|
Req,
|
||||||
Res,
|
Res,
|
||||||
UnauthorizedException,
|
UseFilters,
|
||||||
UseGuards,
|
UseGuards,
|
||||||
} from '@nestjs/common';
|
} from '@nestjs/common';
|
||||||
|
|
||||||
import { Response } from 'express';
|
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 { 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 { 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';
|
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';
|
import { LoadServiceWithWorkspaceContext } from 'src/engine/twenty-orm/context/load-service-with-workspace.context';
|
||||||
|
|
||||||
@Controller('auth/google-apis')
|
@Controller('auth/google-apis')
|
||||||
|
@UseFilters(AuthRestApiExceptionFilter)
|
||||||
export class GoogleAPIsAuthController {
|
export class GoogleAPIsAuthController {
|
||||||
constructor(
|
constructor(
|
||||||
private readonly googleAPIsService: GoogleAPIsService,
|
private readonly googleAPIsService: GoogleAPIsService,
|
||||||
@ -59,13 +65,17 @@ export class GoogleAPIsAuthController {
|
|||||||
const demoWorkspaceIds = this.environmentService.get('DEMO_WORKSPACE_IDS');
|
const demoWorkspaceIds = this.environmentService.get('DEMO_WORKSPACE_IDS');
|
||||||
|
|
||||||
if (demoWorkspaceIds.includes(workspaceId)) {
|
if (demoWorkspaceIds.includes(workspaceId)) {
|
||||||
throw new UnauthorizedException(
|
throw new AuthException(
|
||||||
'Cannot connect Google account to demo workspace',
|
'Cannot connect Google account to demo workspace',
|
||||||
|
AuthExceptionCode.FORBIDDEN_EXCEPTION,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!workspaceId) {
|
if (!workspaceId) {
|
||||||
throw new Error('Workspace not found');
|
throw new AuthException(
|
||||||
|
'Workspace not found',
|
||||||
|
AuthExceptionCode.INVALID_INPUT,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const handle = emails[0].value;
|
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 { Response } from 'express';
|
||||||
|
|
||||||
import { GoogleRequest } from 'src/engine/core-modules/auth/strategies/google.auth.strategy';
|
import { AuthRestApiExceptionFilter } from 'src/engine/core-modules/auth/filters/auth-rest-api-exception.filter';
|
||||||
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 { GoogleOauthGuard } from 'src/engine/core-modules/auth/guards/google-oauth.guard';
|
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 { 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')
|
@Controller('auth/google')
|
||||||
|
@UseFilters(AuthRestApiExceptionFilter)
|
||||||
export class GoogleAuthController {
|
export class GoogleAuthController {
|
||||||
constructor(
|
constructor(
|
||||||
private readonly tokenService: TokenService,
|
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 { Response } from 'express';
|
||||||
|
|
||||||
import { TypeORMService } from 'src/database/typeorm/typeorm.service';
|
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 { 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 { MicrosoftProviderEnabledGuard } from 'src/engine/core-modules/auth/guards/microsoft-provider-enabled.guard';
|
||||||
import { AuthService } from 'src/engine/core-modules/auth/services/auth.service';
|
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';
|
import { MicrosoftRequest } from 'src/engine/core-modules/auth/strategies/microsoft.auth.strategy';
|
||||||
|
|
||||||
@Controller('auth/microsoft')
|
@Controller('auth/microsoft')
|
||||||
|
@UseFilters(AuthRestApiExceptionFilter)
|
||||||
export class MicrosoftAuthController {
|
export class MicrosoftAuthController {
|
||||||
constructor(
|
constructor(
|
||||||
private readonly tokenService: TokenService,
|
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 { 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';
|
import { TokenService } from 'src/engine/core-modules/auth/services/token.service';
|
||||||
|
|
||||||
@Controller('auth/verify')
|
@Controller('auth/verify')
|
||||||
|
@UseFilters(AuthRestApiExceptionFilter)
|
||||||
export class VerifyAuthController {
|
export class VerifyAuthController {
|
||||||
constructor(
|
constructor(
|
||||||
private readonly authService: AuthService,
|
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 {
|
import { ExecutionContext, Injectable } from '@nestjs/common';
|
||||||
ExecutionContext,
|
|
||||||
Injectable,
|
|
||||||
NotFoundException,
|
|
||||||
} from '@nestjs/common';
|
|
||||||
import { AuthGuard } from '@nestjs/passport';
|
import { AuthGuard } from '@nestjs/passport';
|
||||||
import { InjectRepository } from '@nestjs/typeorm';
|
import { InjectRepository } from '@nestjs/typeorm';
|
||||||
|
|
||||||
import { Repository } from '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 { TokenService } from 'src/engine/core-modules/auth/services/token.service';
|
||||||
import {
|
import {
|
||||||
GoogleAPIScopeConfig,
|
GoogleAPIScopeConfig,
|
||||||
@ -39,7 +39,10 @@ export class GoogleAPIsOauthExchangeCodeForTokenGuard extends AuthGuard(
|
|||||||
!this.environmentService.get('MESSAGING_PROVIDER_GMAIL_ENABLED') &&
|
!this.environmentService.get('MESSAGING_PROVIDER_GMAIL_ENABLED') &&
|
||||||
!this.environmentService.get('CALENDAR_PROVIDER_GOOGLE_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(
|
const { workspaceId } = await this.tokenService.verifyTransientToken(
|
||||||
|
|||||||
@ -1,13 +1,13 @@
|
|||||||
import {
|
import { ExecutionContext, Injectable } from '@nestjs/common';
|
||||||
ExecutionContext,
|
|
||||||
Injectable,
|
|
||||||
NotFoundException,
|
|
||||||
} from '@nestjs/common';
|
|
||||||
import { AuthGuard } from '@nestjs/passport';
|
import { AuthGuard } from '@nestjs/passport';
|
||||||
import { InjectRepository } from '@nestjs/typeorm';
|
import { InjectRepository } from '@nestjs/typeorm';
|
||||||
|
|
||||||
import { Repository } from '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 { 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 { 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';
|
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('MESSAGING_PROVIDER_GMAIL_ENABLED') &&
|
||||||
!this.environmentService.get('CALENDAR_PROVIDER_GOOGLE_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(
|
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 { 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 { GoogleStrategy } from 'src/engine/core-modules/auth/strategies/google.auth.strategy';
|
||||||
|
import { EnvironmentService } from 'src/engine/integrations/environment/environment.service';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class GoogleProviderEnabledGuard implements CanActivate {
|
export class GoogleProviderEnabledGuard implements CanActivate {
|
||||||
@ -11,7 +15,10 @@ export class GoogleProviderEnabledGuard implements CanActivate {
|
|||||||
|
|
||||||
canActivate(): boolean | Promise<boolean> | Observable<boolean> {
|
canActivate(): boolean | Promise<boolean> | Observable<boolean> {
|
||||||
if (!this.environmentService.get('AUTH_GOOGLE_ENABLED')) {
|
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);
|
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 { 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 { MicrosoftStrategy } from 'src/engine/core-modules/auth/strategies/microsoft.auth.strategy';
|
||||||
|
import { EnvironmentService } from 'src/engine/integrations/environment/environment.service';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class MicrosoftProviderEnabledGuard implements CanActivate {
|
export class MicrosoftProviderEnabledGuard implements CanActivate {
|
||||||
@ -11,7 +15,10 @@ export class MicrosoftProviderEnabledGuard implements CanActivate {
|
|||||||
|
|
||||||
canActivate(): boolean | Promise<boolean> | Observable<boolean> {
|
canActivate(): boolean | Promise<boolean> | Observable<boolean> {
|
||||||
if (!this.environmentService.get('AUTH_MICROSOFT_ENABLED')) {
|
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);
|
new MicrosoftStrategy(this.environmentService);
|
||||||
|
|||||||
@ -1,45 +1,43 @@
|
|||||||
import {
|
import { Injectable } from '@nestjs/common';
|
||||||
BadRequestException,
|
|
||||||
ForbiddenException,
|
|
||||||
Injectable,
|
|
||||||
NotFoundException,
|
|
||||||
} from '@nestjs/common';
|
|
||||||
import { InjectRepository } from '@nestjs/typeorm';
|
import { InjectRepository } from '@nestjs/typeorm';
|
||||||
|
|
||||||
import crypto from 'node:crypto';
|
import crypto from 'node:crypto';
|
||||||
|
|
||||||
import { Repository } from 'typeorm';
|
|
||||||
import { render } from '@react-email/components';
|
import { render } from '@react-email/components';
|
||||||
import { PasswordUpdateNotifyEmail } from 'twenty-emails';
|
|
||||||
import { addMilliseconds } from 'date-fns';
|
import { addMilliseconds } from 'date-fns';
|
||||||
import ms from 'ms';
|
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 { NodeEnvironment } from 'src/engine/integrations/environment/interfaces/node-environment.interface';
|
||||||
|
|
||||||
import { ChallengeInput } from 'src/engine/core-modules/auth/dto/challenge.input';
|
import {
|
||||||
import { assert } from 'src/utils/assert';
|
AppToken,
|
||||||
|
AppTokenType,
|
||||||
|
} from 'src/engine/core-modules/app-token/app-token.entity';
|
||||||
|
import {
|
||||||
|
AuthException,
|
||||||
|
AuthExceptionCode,
|
||||||
|
} from 'src/engine/core-modules/auth/auth.exception';
|
||||||
import {
|
import {
|
||||||
PASSWORD_REGEX,
|
PASSWORD_REGEX,
|
||||||
compareHash,
|
compareHash,
|
||||||
hashPassword,
|
hashPassword,
|
||||||
} from 'src/engine/core-modules/auth/auth.util';
|
} 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 { 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 { 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 { User } from 'src/engine/core-modules/user/user.entity';
|
||||||
import { Workspace } from 'src/engine/core-modules/workspace/workspace.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 { EmailService } from 'src/engine/integrations/email/email.service';
|
||||||
import { UpdatePassword } from 'src/engine/core-modules/auth/dto/update-password.entity';
|
import { EnvironmentService } from 'src/engine/integrations/environment/environment.service';
|
||||||
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 { TokenService } from './token.service';
|
import { TokenService } from './token.service';
|
||||||
|
|
||||||
@ -70,15 +68,31 @@ export class AuthService {
|
|||||||
email: challengeInput.email,
|
email: challengeInput.email,
|
||||||
});
|
});
|
||||||
|
|
||||||
assert(user, "This user doesn't exist", NotFoundException);
|
if (!user) {
|
||||||
assert(user.passwordHash, 'Incorrect login method', ForbiddenException);
|
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(
|
const isValid = await compareHash(
|
||||||
challengeInput.password,
|
challengeInput.password,
|
||||||
user.passwordHash,
|
user.passwordHash,
|
||||||
);
|
);
|
||||||
|
|
||||||
assert(isValid, 'Wrong password', ForbiddenException);
|
if (!isValid) {
|
||||||
|
throw new AuthException(
|
||||||
|
'Wrong password',
|
||||||
|
AuthExceptionCode.FORBIDDEN_EXCEPTION,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
return user;
|
return user;
|
||||||
}
|
}
|
||||||
@ -112,6 +126,13 @@ export class AuthService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async verify(email: string): Promise<Verify> {
|
async verify(email: string): Promise<Verify> {
|
||||||
|
if (!email) {
|
||||||
|
throw new AuthException(
|
||||||
|
'Email is required',
|
||||||
|
AuthExceptionCode.INVALID_INPUT,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
const user = await this.userRepository.findOne({
|
const user = await this.userRepository.findOne({
|
||||||
where: {
|
where: {
|
||||||
email,
|
email,
|
||||||
@ -119,13 +140,19 @@ export class AuthService {
|
|||||||
relations: ['defaultWorkspace', 'workspaces', 'workspaces.workspace'],
|
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(
|
if (!user.defaultWorkspace) {
|
||||||
user.defaultWorkspace,
|
throw new AuthException(
|
||||||
'User has no default workspace',
|
'User has no default workspace',
|
||||||
NotFoundException,
|
AuthExceptionCode.INVALID_DATA,
|
||||||
);
|
);
|
||||||
|
}
|
||||||
|
|
||||||
// passwordHash is hidden for security reasons
|
// passwordHash is hidden for security reasons
|
||||||
user.passwordHash = '';
|
user.passwordHash = '';
|
||||||
@ -165,18 +192,33 @@ export class AuthService {
|
|||||||
return { isValid: !!workspace };
|
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({
|
const user = await this.userRepository.findOne({
|
||||||
where: {
|
where: {
|
||||||
id: userId,
|
id: userIdToImpersonate,
|
||||||
},
|
},
|
||||||
relations: ['defaultWorkspace', 'workspaces', 'workspaces.workspace'],
|
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) {
|
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);
|
const accessToken = await this.tokenService.generateAccessToken(user.id);
|
||||||
@ -215,15 +257,24 @@ export class AuthService {
|
|||||||
const client = apps.find((app) => app.id === clientId);
|
const client = apps.find((app) => app.id === clientId);
|
||||||
|
|
||||||
if (!client) {
|
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) {
|
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) {
|
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');
|
const authorizationCode = crypto.randomBytes(42).toString('hex');
|
||||||
@ -272,13 +323,30 @@ export class AuthService {
|
|||||||
userId: string,
|
userId: string,
|
||||||
newPassword: string,
|
newPassword: string,
|
||||||
): Promise<UpdatePassword> {
|
): Promise<UpdatePassword> {
|
||||||
|
if (!userId) {
|
||||||
|
throw new AuthException(
|
||||||
|
'User ID is required',
|
||||||
|
AuthExceptionCode.INVALID_INPUT,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
const user = await this.userRepository.findOneBy({ id: userId });
|
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);
|
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);
|
const newPasswordHash = await hashPassword(newPassword);
|
||||||
|
|
||||||
@ -311,4 +379,21 @@ export class AuthService {
|
|||||||
|
|
||||||
return { success: true };
|
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 { HttpService } from '@nestjs/axios';
|
||||||
import {
|
import { Injectable } from '@nestjs/common';
|
||||||
BadRequestException,
|
|
||||||
ForbiddenException,
|
|
||||||
Injectable,
|
|
||||||
} from '@nestjs/common';
|
|
||||||
import { InjectRepository } from '@nestjs/typeorm';
|
import { InjectRepository } from '@nestjs/typeorm';
|
||||||
|
|
||||||
import FileType from 'file-type';
|
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 { FileFolder } from 'src/engine/core-modules/file/interfaces/file-folder.interface';
|
||||||
|
|
||||||
|
import {
|
||||||
|
AuthException,
|
||||||
|
AuthExceptionCode,
|
||||||
|
} from 'src/engine/core-modules/auth/auth.exception';
|
||||||
import {
|
import {
|
||||||
PASSWORD_REGEX,
|
PASSWORD_REGEX,
|
||||||
compareHash,
|
compareHash,
|
||||||
@ -26,7 +26,6 @@ import {
|
|||||||
WorkspaceActivationStatus,
|
WorkspaceActivationStatus,
|
||||||
} from 'src/engine/core-modules/workspace/workspace.entity';
|
} from 'src/engine/core-modules/workspace/workspace.entity';
|
||||||
import { EnvironmentService } from 'src/engine/integrations/environment/environment.service';
|
import { EnvironmentService } from 'src/engine/integrations/environment/environment.service';
|
||||||
import { assert } from 'src/utils/assert';
|
|
||||||
import { getImageBufferFromUrl } from 'src/utils/image';
|
import { getImageBufferFromUrl } from 'src/utils/image';
|
||||||
|
|
||||||
export type SignInUpServiceInput = {
|
export type SignInUpServiceInput = {
|
||||||
@ -66,12 +65,22 @@ export class SignInUpService {
|
|||||||
if (!firstName) firstName = '';
|
if (!firstName) firstName = '';
|
||||||
if (!lastName) lastName = '';
|
if (!lastName) lastName = '';
|
||||||
|
|
||||||
assert(email, 'Email is required', BadRequestException);
|
if (!email) {
|
||||||
|
throw new AuthException(
|
||||||
|
'Email is required',
|
||||||
|
AuthExceptionCode.INVALID_INPUT,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
if (password) {
|
if (password) {
|
||||||
const isPasswordValid = PASSWORD_REGEX.test(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;
|
const passwordHash = password ? await hashPassword(password) : undefined;
|
||||||
@ -89,7 +98,12 @@ export class SignInUpService {
|
|||||||
existingUser.passwordHash,
|
existingUser.passwordHash,
|
||||||
);
|
);
|
||||||
|
|
||||||
assert(isValid, 'Wrong password', ForbiddenException);
|
if (!isValid) {
|
||||||
|
throw new AuthException(
|
||||||
|
'Wrong password',
|
||||||
|
AuthExceptionCode.FORBIDDEN_EXCEPTION,
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (workspaceInviteHash) {
|
if (workspaceInviteHash) {
|
||||||
@ -137,17 +151,19 @@ export class SignInUpService {
|
|||||||
inviteHash: workspaceInviteHash,
|
inviteHash: workspaceInviteHash,
|
||||||
});
|
});
|
||||||
|
|
||||||
assert(
|
if (!workspace) {
|
||||||
workspace,
|
throw new AuthException(
|
||||||
'This workspace inviteHash is invalid',
|
'Invit hash is invalid',
|
||||||
ForbiddenException,
|
AuthExceptionCode.FORBIDDEN_EXCEPTION,
|
||||||
);
|
);
|
||||||
|
}
|
||||||
|
|
||||||
assert(
|
if (!(workspace.activationStatus === WorkspaceActivationStatus.ACTIVE)) {
|
||||||
workspace.activationStatus === WorkspaceActivationStatus.ACTIVE,
|
throw new AuthException(
|
||||||
'Workspace is not ready to welcome new members',
|
'Workspace is not ready to welcome new members',
|
||||||
ForbiddenException,
|
AuthExceptionCode.FORBIDDEN_EXCEPTION,
|
||||||
);
|
);
|
||||||
|
}
|
||||||
|
|
||||||
if (existingUser) {
|
if (existingUser) {
|
||||||
const updatedUser = await this.userWorkspaceService.addUserToWorkspace(
|
const updatedUser = await this.userWorkspaceService.addUserToWorkspace(
|
||||||
@ -203,11 +219,12 @@ export class SignInUpService {
|
|||||||
lastName: string;
|
lastName: string;
|
||||||
picture: SignInUpServiceInput['picture'];
|
picture: SignInUpServiceInput['picture'];
|
||||||
}) {
|
}) {
|
||||||
assert(
|
if (this.environmentService.get('IS_SIGN_UP_DISABLED')) {
|
||||||
!this.environmentService.get('IS_SIGN_UP_DISABLED'),
|
throw new AuthException(
|
||||||
'Sign up is disabled',
|
'Sign up is disabled',
|
||||||
ForbiddenException,
|
AuthExceptionCode.FORBIDDEN_EXCEPTION,
|
||||||
);
|
);
|
||||||
|
}
|
||||||
|
|
||||||
const workspaceToCreate = this.workspaceRepository.create({
|
const workspaceToCreate = this.workspaceRepository.create({
|
||||||
displayName: '',
|
displayName: '',
|
||||||
|
|||||||
@ -1,8 +1,3 @@
|
|||||||
import {
|
|
||||||
BadRequestException,
|
|
||||||
InternalServerErrorException,
|
|
||||||
NotFoundException,
|
|
||||||
} from '@nestjs/common';
|
|
||||||
import { Test, TestingModule } from '@nestjs/testing';
|
import { Test, TestingModule } from '@nestjs/testing';
|
||||||
import { getRepositoryToken } from '@nestjs/typeorm';
|
import { getRepositoryToken } from '@nestjs/typeorm';
|
||||||
|
|
||||||
@ -14,6 +9,7 @@ import {
|
|||||||
AppToken,
|
AppToken,
|
||||||
AppTokenType,
|
AppTokenType,
|
||||||
} from 'src/engine/core-modules/app-token/app-token.entity';
|
} 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 { JwtAuthStrategy } from 'src/engine/core-modules/auth/strategies/jwt.auth.strategy';
|
||||||
import { JwtWrapperService } from 'src/engine/core-modules/jwt/services/jwt-wrapper.service';
|
import { JwtWrapperService } from 'src/engine/core-modules/jwt/services/jwt-wrapper.service';
|
||||||
import { User } from 'src/engine/core-modules/user/user.entity';
|
import { User } from 'src/engine/core-modules/user/user.entity';
|
||||||
@ -106,7 +102,7 @@ describe('TokenService', () => {
|
|||||||
expect(result.passwordResetTokenExpiresAt).toBeDefined();
|
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 mockUser = { id: '1', email: 'test@example.com' } as User;
|
||||||
const mockToken = {
|
const mockToken = {
|
||||||
userId: '1',
|
userId: '1',
|
||||||
@ -120,18 +116,18 @@ describe('TokenService', () => {
|
|||||||
|
|
||||||
await expect(
|
await expect(
|
||||||
service.generatePasswordResetToken(mockUser.email),
|
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);
|
jest.spyOn(userRepository, 'findOneBy').mockResolvedValue(null);
|
||||||
|
|
||||||
await expect(
|
await expect(
|
||||||
service.generatePasswordResetToken('nonexistent@example.com'),
|
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;
|
const mockUser = { id: '1', email: 'test@example.com' } as User;
|
||||||
|
|
||||||
jest.spyOn(userRepository, 'findOneBy').mockResolvedValue(mockUser);
|
jest.spyOn(userRepository, 'findOneBy').mockResolvedValue(mockUser);
|
||||||
@ -139,7 +135,7 @@ describe('TokenService', () => {
|
|||||||
|
|
||||||
await expect(
|
await expect(
|
||||||
service.generatePasswordResetToken(mockUser.email),
|
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 });
|
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';
|
const resetToken = 'invalid-reset-token';
|
||||||
|
|
||||||
jest.spyOn(appTokenRepository, 'findOne').mockResolvedValue(null);
|
jest.spyOn(appTokenRepository, 'findOne').mockResolvedValue(null);
|
||||||
|
|
||||||
await expect(
|
await expect(
|
||||||
service.validatePasswordResetToken(resetToken),
|
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 resetToken = 'orphan-token';
|
||||||
const hashedToken = crypto
|
const hashedToken = crypto
|
||||||
.createHash('sha256')
|
.createHash('sha256')
|
||||||
@ -212,10 +208,10 @@ describe('TokenService', () => {
|
|||||||
|
|
||||||
await expect(
|
await expect(
|
||||||
service.validatePasswordResetToken(resetToken),
|
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 resetToken = 'revoked-token';
|
||||||
const hashedToken = crypto
|
const hashedToken = crypto
|
||||||
.createHash('sha256')
|
.createHash('sha256')
|
||||||
@ -234,7 +230,7 @@ describe('TokenService', () => {
|
|||||||
.mockResolvedValue(mockToken as AppToken);
|
.mockResolvedValue(mockToken as AppToken);
|
||||||
await expect(
|
await expect(
|
||||||
service.validatePasswordResetToken(resetToken),
|
service.validatePasswordResetToken(resetToken),
|
||||||
).rejects.toThrow(NotFoundException);
|
).rejects.toThrow(AuthException);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@ -1,12 +1,4 @@
|
|||||||
import {
|
import { Injectable } from '@nestjs/common';
|
||||||
BadRequestException,
|
|
||||||
ForbiddenException,
|
|
||||||
Injectable,
|
|
||||||
InternalServerErrorException,
|
|
||||||
NotFoundException,
|
|
||||||
UnauthorizedException,
|
|
||||||
UnprocessableEntityException,
|
|
||||||
} from '@nestjs/common';
|
|
||||||
import { InjectRepository } from '@nestjs/typeorm';
|
import { InjectRepository } from '@nestjs/typeorm';
|
||||||
|
|
||||||
import crypto from 'crypto';
|
import crypto from 'crypto';
|
||||||
@ -24,6 +16,10 @@ import {
|
|||||||
AppToken,
|
AppToken,
|
||||||
AppTokenType,
|
AppTokenType,
|
||||||
} from 'src/engine/core-modules/app-token/app-token.entity';
|
} 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 { 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 { 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';
|
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 { Workspace } from 'src/engine/core-modules/workspace/workspace.entity';
|
||||||
import { EmailService } from 'src/engine/integrations/email/email.service';
|
import { EmailService } from 'src/engine/integrations/email/email.service';
|
||||||
import { EnvironmentService } from 'src/engine/integrations/environment/environment.service';
|
import { EnvironmentService } from 'src/engine/integrations/environment/environment.service';
|
||||||
import { assert } from 'src/utils/assert';
|
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class TokenService {
|
export class TokenService {
|
||||||
@ -68,7 +63,13 @@ export class TokenService {
|
|||||||
): Promise<AuthToken> {
|
): Promise<AuthToken> {
|
||||||
const expiresIn = this.environmentService.get('ACCESS_TOKEN_EXPIRES_IN');
|
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 expiresAt = addMilliseconds(new Date().getTime(), ms(expiresIn));
|
||||||
|
|
||||||
const user = await this.userRepository.findOne({
|
const user = await this.userRepository.findOne({
|
||||||
@ -77,11 +78,17 @@ export class TokenService {
|
|||||||
});
|
});
|
||||||
|
|
||||||
if (!user) {
|
if (!user) {
|
||||||
throw new NotFoundException('User is not found');
|
throw new AuthException(
|
||||||
|
'User is not found',
|
||||||
|
AuthExceptionCode.INVALID_INPUT,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!user.defaultWorkspace) {
|
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 = {
|
const jwtPayload: JwtPayload = {
|
||||||
@ -99,7 +106,13 @@ export class TokenService {
|
|||||||
const secret = this.environmentService.get('REFRESH_TOKEN_SECRET');
|
const secret = this.environmentService.get('REFRESH_TOKEN_SECRET');
|
||||||
const expiresIn = this.environmentService.get('REFRESH_TOKEN_EXPIRES_IN');
|
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 expiresAt = addMilliseconds(new Date().getTime(), ms(expiresIn));
|
||||||
|
|
||||||
const refreshTokenPayload = {
|
const refreshTokenPayload = {
|
||||||
@ -130,7 +143,13 @@ export class TokenService {
|
|||||||
const secret = this.environmentService.get('LOGIN_TOKEN_SECRET');
|
const secret = this.environmentService.get('LOGIN_TOKEN_SECRET');
|
||||||
const expiresIn = this.environmentService.get('LOGIN_TOKEN_EXPIRES_IN');
|
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 expiresAt = addMilliseconds(new Date().getTime(), ms(expiresIn));
|
||||||
const jwtPayload = {
|
const jwtPayload = {
|
||||||
sub: email,
|
sub: email,
|
||||||
@ -155,7 +174,13 @@ export class TokenService {
|
|||||||
'SHORT_TERM_TOKEN_EXPIRES_IN',
|
'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 expiresAt = addMilliseconds(new Date().getTime(), ms(expiresIn));
|
||||||
const jwtPayload = {
|
const jwtPayload = {
|
||||||
sub: workspaceMemberId,
|
sub: workspaceMemberId,
|
||||||
@ -212,7 +237,10 @@ export class TokenService {
|
|||||||
const token = ExtractJwt.fromAuthHeaderAsBearerToken()(request);
|
const token = ExtractJwt.fromAuthHeaderAsBearerToken()(request);
|
||||||
|
|
||||||
if (!token) {
|
if (!token) {
|
||||||
throw new UnauthorizedException('missing authentication token');
|
throw new AuthException(
|
||||||
|
'missing authentication token',
|
||||||
|
AuthExceptionCode.FORBIDDEN_EXCEPTION,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
const decoded = await this.verifyJwt(
|
const decoded = await this.verifyJwt(
|
||||||
token,
|
token,
|
||||||
@ -257,22 +285,35 @@ export class TokenService {
|
|||||||
): Promise<AuthTokens> {
|
): Promise<AuthTokens> {
|
||||||
const userExists = await this.userRepository.findBy({ id: user.id });
|
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({
|
const workspace = await this.workspaceRepository.findOne({
|
||||||
where: { id: workspaceId },
|
where: { id: workspaceId },
|
||||||
relations: ['workspaceUsers'],
|
relations: ['workspaceUsers'],
|
||||||
});
|
});
|
||||||
|
|
||||||
assert(workspace, 'workspace doesnt exist', NotFoundException);
|
if (!workspace) {
|
||||||
|
throw new AuthException(
|
||||||
|
'workspace doesnt exist',
|
||||||
|
AuthExceptionCode.INVALID_INPUT,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
assert(
|
if (
|
||||||
workspace.workspaceUsers
|
!workspace.workspaceUsers
|
||||||
.map((userWorkspace) => userWorkspace.userId)
|
.map((userWorkspace) => userWorkspace.userId)
|
||||||
.includes(user.id),
|
.includes(user.id)
|
||||||
'user does not belong to workspace',
|
) {
|
||||||
ForbiddenException,
|
throw new AuthException(
|
||||||
);
|
'user does not belong to workspace',
|
||||||
|
AuthExceptionCode.FORBIDDEN_EXCEPTION,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
await this.userRepository.save({
|
await this.userRepository.save({
|
||||||
id: user.id,
|
id: user.id,
|
||||||
@ -293,29 +334,17 @@ export class TokenService {
|
|||||||
async verifyAuthorizationCode(
|
async verifyAuthorizationCode(
|
||||||
exchangeAuthCodeInput: ExchangeAuthCodeInput,
|
exchangeAuthCodeInput: ExchangeAuthCodeInput,
|
||||||
): Promise<ExchangeAuthCode> {
|
): Promise<ExchangeAuthCode> {
|
||||||
const { authorizationCode, codeVerifier, clientSecret } =
|
const { authorizationCode, codeVerifier } = exchangeAuthCodeInput;
|
||||||
exchangeAuthCodeInput;
|
|
||||||
|
|
||||||
assert(
|
if (!authorizationCode) {
|
||||||
authorizationCode,
|
throw new AuthException(
|
||||||
'Authorization code not found',
|
'Authorization code not found',
|
||||||
NotFoundException,
|
AuthExceptionCode.INVALID_INPUT,
|
||||||
);
|
);
|
||||||
|
}
|
||||||
assert(
|
|
||||||
!codeVerifier || !clientSecret,
|
|
||||||
'client secret or code verifier not found',
|
|
||||||
NotFoundException,
|
|
||||||
);
|
|
||||||
|
|
||||||
let userId = '';
|
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) {
|
if (codeVerifier) {
|
||||||
const authorizationCodeAppToken = await this.appTokenRepository.findOne({
|
const authorizationCodeAppToken = await this.appTokenRepository.findOne({
|
||||||
where: {
|
where: {
|
||||||
@ -323,17 +352,19 @@ export class TokenService {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
assert(
|
if (!authorizationCodeAppToken) {
|
||||||
authorizationCodeAppToken,
|
throw new AuthException(
|
||||||
'Authorization code does not exist',
|
'Authorization code does not exist',
|
||||||
NotFoundException,
|
AuthExceptionCode.INVALID_INPUT,
|
||||||
);
|
);
|
||||||
|
}
|
||||||
|
|
||||||
assert(
|
if (!(authorizationCodeAppToken.expiresAt.getTime() >= Date.now())) {
|
||||||
authorizationCodeAppToken.expiresAt.getTime() >= Date.now(),
|
throw new AuthException(
|
||||||
'Authorization code expired.',
|
'Authorization code expired.',
|
||||||
ForbiddenException,
|
AuthExceptionCode.FORBIDDEN_EXCEPTION,
|
||||||
);
|
);
|
||||||
|
}
|
||||||
|
|
||||||
const codeChallenge = crypto
|
const codeChallenge = crypto
|
||||||
.createHash('sha256')
|
.createHash('sha256')
|
||||||
@ -350,26 +381,32 @@ export class TokenService {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
assert(
|
if (!codeChallengeAppToken) {
|
||||||
codeChallengeAppToken,
|
throw new AuthException(
|
||||||
'code verifier doesnt match the challenge',
|
'code verifier doesnt match the challenge',
|
||||||
ForbiddenException,
|
AuthExceptionCode.FORBIDDEN_EXCEPTION,
|
||||||
);
|
);
|
||||||
|
}
|
||||||
|
|
||||||
assert(
|
if (!(codeChallengeAppToken.expiresAt.getTime() >= Date.now())) {
|
||||||
codeChallengeAppToken.expiresAt.getTime() >= Date.now(),
|
throw new AuthException(
|
||||||
'code challenge expired.',
|
'code challenge expired.',
|
||||||
ForbiddenException,
|
AuthExceptionCode.FORBIDDEN_EXCEPTION,
|
||||||
);
|
);
|
||||||
|
}
|
||||||
|
|
||||||
assert(
|
if (codeChallengeAppToken.userId !== authorizationCodeAppToken.userId) {
|
||||||
codeChallengeAppToken.userId === authorizationCodeAppToken.userId,
|
throw new AuthException(
|
||||||
'authorization code / code verifier was not created by same client',
|
'authorization code / code verifier was not created by same client',
|
||||||
ForbiddenException,
|
AuthExceptionCode.FORBIDDEN_EXCEPTION,
|
||||||
);
|
);
|
||||||
|
}
|
||||||
|
|
||||||
if (codeChallengeAppToken.revokedAt) {
|
if (codeChallengeAppToken.revokedAt) {
|
||||||
throw new ForbiddenException('Token has been revoked.');
|
throw new AuthException(
|
||||||
|
'Token has been revoked.',
|
||||||
|
AuthExceptionCode.FORBIDDEN_EXCEPTION,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
await this.appTokenRepository.save({
|
await this.appTokenRepository.save({
|
||||||
@ -386,13 +423,17 @@ export class TokenService {
|
|||||||
});
|
});
|
||||||
|
|
||||||
if (!user) {
|
if (!user) {
|
||||||
throw new NotFoundException(
|
throw new AuthException(
|
||||||
'User who generated the token does not exist',
|
'User who generated the token does not exist',
|
||||||
|
AuthExceptionCode.INVALID_INPUT,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!user.defaultWorkspace) {
|
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(
|
const accessToken = await this.generateAccessToken(
|
||||||
@ -414,24 +455,35 @@ export class TokenService {
|
|||||||
const coolDown = this.environmentService.get('REFRESH_TOKEN_COOL_DOWN');
|
const coolDown = this.environmentService.get('REFRESH_TOKEN_COOL_DOWN');
|
||||||
const jwtPayload = await this.verifyJwt(refreshToken, secret);
|
const jwtPayload = await this.verifyJwt(refreshToken, secret);
|
||||||
|
|
||||||
assert(
|
if (!(jwtPayload.jti && jwtPayload.sub)) {
|
||||||
jwtPayload.jti && jwtPayload.sub,
|
throw new AuthException(
|
||||||
'This refresh token is malformed',
|
'This refresh token is malformed',
|
||||||
UnprocessableEntityException,
|
AuthExceptionCode.INVALID_INPUT,
|
||||||
);
|
);
|
||||||
|
}
|
||||||
|
|
||||||
const token = await this.appTokenRepository.findOneBy({
|
const token = await this.appTokenRepository.findOneBy({
|
||||||
id: jwtPayload.jti,
|
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({
|
const user = await this.userRepository.findOne({
|
||||||
where: { id: jwtPayload.sub },
|
where: { id: jwtPayload.sub },
|
||||||
relations: ['appTokens'],
|
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
|
// Check if revokedAt is less than coolDown
|
||||||
if (
|
if (
|
||||||
@ -452,8 +504,9 @@ export class TokenService {
|
|||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
|
|
||||||
throw new ForbiddenException(
|
throw new AuthException(
|
||||||
'Suspicious activity detected, this refresh token has been revoked. All tokens has been revoked.',
|
'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;
|
accessToken: AuthToken;
|
||||||
refreshToken: AuthToken;
|
refreshToken: AuthToken;
|
||||||
}> {
|
}> {
|
||||||
|
if (!token) {
|
||||||
|
throw new AuthException(
|
||||||
|
'Refresh token not found',
|
||||||
|
AuthExceptionCode.INVALID_INPUT,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
const {
|
const {
|
||||||
user,
|
user,
|
||||||
token: { id },
|
token: { id },
|
||||||
@ -502,11 +562,20 @@ export class TokenService {
|
|||||||
);
|
);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (error instanceof TokenExpiredError) {
|
if (error instanceof TokenExpiredError) {
|
||||||
throw new UnauthorizedException('Token has expired.');
|
throw new AuthException(
|
||||||
|
'Token has expired.',
|
||||||
|
AuthExceptionCode.FORBIDDEN_EXCEPTION,
|
||||||
|
);
|
||||||
} else if (error instanceof JsonWebTokenError) {
|
} else if (error instanceof JsonWebTokenError) {
|
||||||
throw new UnauthorizedException('Token invalid.');
|
throw new AuthException(
|
||||||
|
'Token invalid.',
|
||||||
|
AuthExceptionCode.FORBIDDEN_EXCEPTION,
|
||||||
|
);
|
||||||
} else {
|
} else {
|
||||||
throw new UnprocessableEntityException();
|
throw new AuthException(
|
||||||
|
'Unknown token error.',
|
||||||
|
AuthExceptionCode.INVALID_INPUT,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -516,17 +585,23 @@ export class TokenService {
|
|||||||
email,
|
email,
|
||||||
});
|
});
|
||||||
|
|
||||||
assert(user, 'User not found', NotFoundException);
|
if (!user) {
|
||||||
|
throw new AuthException(
|
||||||
|
'User not found',
|
||||||
|
AuthExceptionCode.INVALID_INPUT,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
const expiresIn = this.environmentService.get(
|
const expiresIn = this.environmentService.get(
|
||||||
'PASSWORD_RESET_TOKEN_EXPIRES_IN',
|
'PASSWORD_RESET_TOKEN_EXPIRES_IN',
|
||||||
);
|
);
|
||||||
|
|
||||||
assert(
|
if (!expiresIn) {
|
||||||
expiresIn,
|
throw new AuthException(
|
||||||
'PASSWORD_RESET_TOKEN_EXPIRES_IN constant value not found',
|
'PASSWORD_RESET_TOKEN_EXPIRES_IN constant value not found',
|
||||||
InternalServerErrorException,
|
AuthExceptionCode.INTERNAL_SERVER_ERROR,
|
||||||
);
|
);
|
||||||
|
}
|
||||||
|
|
||||||
const existingToken = await this.appTokenRepository.findOne({
|
const existingToken = await this.appTokenRepository.findOne({
|
||||||
where: {
|
where: {
|
||||||
@ -543,10 +618,9 @@ export class TokenService {
|
|||||||
{ long: true },
|
{ long: true },
|
||||||
);
|
);
|
||||||
|
|
||||||
assert(
|
throw new AuthException(
|
||||||
false,
|
|
||||||
`Token has already been generated. Please wait for ${timeToWait} to generate again.`,
|
`Token has already been generated. Please wait for ${timeToWait} to generate again.`,
|
||||||
BadRequestException,
|
AuthExceptionCode.INVALID_INPUT,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -579,7 +653,12 @@ export class TokenService {
|
|||||||
email,
|
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 frontBaseURL = this.environmentService.get('FRONT_BASE_URL');
|
||||||
const resetLink = `${frontBaseURL}/reset-password/${resetToken.passwordResetToken}`;
|
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({
|
const user = await this.userRepository.findOneBy({
|
||||||
id: token.userId,
|
id: token.userId,
|
||||||
});
|
});
|
||||||
|
|
||||||
assert(user, 'Token is invalid', NotFoundException);
|
if (!user) {
|
||||||
|
throw new AuthException(
|
||||||
|
'User not found',
|
||||||
|
AuthExceptionCode.INVALID_INPUT,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
id: user.id,
|
id: user.id,
|
||||||
@ -657,7 +746,12 @@ export class TokenService {
|
|||||||
id: userId,
|
id: userId,
|
||||||
});
|
});
|
||||||
|
|
||||||
assert(user, 'User not found', NotFoundException);
|
if (!user) {
|
||||||
|
throw new AuthException(
|
||||||
|
'User not found',
|
||||||
|
AuthExceptionCode.INVALID_INPUT,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
await this.appTokenRepository.update(
|
await this.appTokenRepository.update(
|
||||||
{
|
{
|
||||||
|
|||||||
@ -1,8 +1,4 @@
|
|||||||
import {
|
import { Injectable } from '@nestjs/common';
|
||||||
ForbiddenException,
|
|
||||||
Injectable,
|
|
||||||
UnauthorizedException,
|
|
||||||
} from '@nestjs/common';
|
|
||||||
import { PassportStrategy } from '@nestjs/passport';
|
import { PassportStrategy } from '@nestjs/passport';
|
||||||
import { InjectRepository } from '@nestjs/typeorm';
|
import { InjectRepository } from '@nestjs/typeorm';
|
||||||
|
|
||||||
@ -10,12 +6,15 @@ import { ExtractJwt, Strategy } from 'passport-jwt';
|
|||||||
import { Repository } from 'typeorm';
|
import { Repository } from 'typeorm';
|
||||||
|
|
||||||
import { TypeORMService } from 'src/database/typeorm/typeorm.service';
|
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 { User } from 'src/engine/core-modules/user/user.entity';
|
||||||
import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity';
|
import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity';
|
||||||
import { EnvironmentService } from 'src/engine/integrations/environment/environment.service';
|
import { EnvironmentService } from 'src/engine/integrations/environment/environment.service';
|
||||||
import { DataSourceService } from 'src/engine/metadata-modules/data-source/data-source.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';
|
import { ApiKeyWorkspaceEntity } from 'src/modules/api-key/standard-objects/api-key.workspace-entity';
|
||||||
|
|
||||||
export type JwtPayload = { sub: string; workspaceId: string; jti?: string };
|
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;
|
let apiKey: ApiKeyWorkspaceEntity | null = null;
|
||||||
|
|
||||||
if (!workspace) {
|
if (!workspace) {
|
||||||
throw new UnauthorizedException();
|
throw new AuthException(
|
||||||
|
'Workspace not found',
|
||||||
|
AuthExceptionCode.INVALID_INPUT,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (payload.jti) {
|
if (payload.jti) {
|
||||||
// TODO: Check why it's not working
|
// TODO: Check why it's not working
|
||||||
// const apiKeyRepository =
|
// const apiKeyRepository =
|
||||||
@ -71,11 +74,12 @@ export class JwtAuthStrategy extends PassportStrategy(Strategy, 'jwt') {
|
|||||||
|
|
||||||
apiKey = res?.[0];
|
apiKey = res?.[0];
|
||||||
|
|
||||||
assert(
|
if (!apiKey || apiKey.revokedAt) {
|
||||||
apiKey && !apiKey.revokedAt,
|
throw new AuthException(
|
||||||
'This API Key is revoked',
|
'This API Key is revoked',
|
||||||
ForbiddenException,
|
AuthExceptionCode.FORBIDDEN_EXCEPTION,
|
||||||
);
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (payload.workspaceId) {
|
if (payload.workspaceId) {
|
||||||
@ -84,7 +88,10 @@ export class JwtAuthStrategy extends PassportStrategy(Strategy, 'jwt') {
|
|||||||
relations: ['defaultWorkspace'],
|
relations: ['defaultWorkspace'],
|
||||||
});
|
});
|
||||||
if (!user) {
|
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 { PassportStrategy } from '@nestjs/passport';
|
||||||
|
|
||||||
import { Request } from 'express';
|
import { Request } from 'express';
|
||||||
import { VerifyCallback } from 'passport-google-oauth20';
|
import { VerifyCallback } from 'passport-google-oauth20';
|
||||||
import { Strategy } from 'passport-microsoft';
|
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';
|
import { EnvironmentService } from 'src/engine/integrations/environment/environment.service';
|
||||||
|
|
||||||
export type MicrosoftRequest = Omit<
|
export type MicrosoftRequest = Omit<
|
||||||
@ -60,7 +63,10 @@ export class MicrosoftStrategy extends PassportStrategy(Strategy, 'microsoft') {
|
|||||||
const email = emails?.[0]?.value ?? null;
|
const email = emails?.[0]?.value ?? null;
|
||||||
|
|
||||||
if (!email) {
|
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'] = {
|
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';
|
import { GoogleAPIsRequest } from 'src/engine/core-modules/auth/types/google-api-request.type';
|
||||||
|
|
||||||
type GoogleAPIsRequestExtraParams = {
|
type GoogleAPIsRequestExtraParams = {
|
||||||
@ -19,7 +23,10 @@ export const setRequestExtraParams = (
|
|||||||
} = params;
|
} = params;
|
||||||
|
|
||||||
if (!transientToken) {
|
if (!transientToken) {
|
||||||
throw new Error('transientToken is required');
|
throw new AuthException(
|
||||||
|
'transientToken is required',
|
||||||
|
AuthExceptionCode.INVALID_INPUT,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
request.params.transientToken = transientToken;
|
request.params.transientToken = transientToken;
|
||||||
|
|||||||
Reference in New Issue
Block a user