first commit
This commit is contained in:
@@ -0,0 +1,365 @@
|
||||
/**
|
||||
* Copyright 2025 LY Corporation
|
||||
*
|
||||
* LY Corporation licenses this file to you under the Apache License,
|
||||
* version 2.0 (the "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at:
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
* License for the specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
import { faker } from '@faker-js/faker';
|
||||
import {
|
||||
BadRequestException,
|
||||
InternalServerErrorException,
|
||||
} from '@nestjs/common';
|
||||
import { Test } from '@nestjs/testing';
|
||||
import { DateTime } from 'luxon';
|
||||
|
||||
import { getMockProvider } from '@/test-utils/util-functions';
|
||||
import { TenantService } from '../tenant/tenant.service';
|
||||
import { UserDto } from '../user/dtos';
|
||||
import { AuthController } from './auth.controller';
|
||||
import { AuthService } from './auth.service';
|
||||
import {
|
||||
EmailUserSignUpRequestDto,
|
||||
EmailVerificationCodeRequestDto,
|
||||
EmailVerificationMailingRequestDto,
|
||||
InvitationUserSignUpRequestDto,
|
||||
OAuthUserSignUpRequestDto,
|
||||
} from './dtos/requests';
|
||||
|
||||
const MockAuthService = {
|
||||
sendEmailCode: jest.fn(),
|
||||
verifyEmailCode: jest.fn(),
|
||||
signUpEmailUser: jest.fn(),
|
||||
signUpInvitationUser: jest.fn(),
|
||||
signUpOAuthUser: jest.fn(),
|
||||
signIn: jest.fn(),
|
||||
signInByOAuth: jest.fn(),
|
||||
refreshToken: jest.fn(),
|
||||
getOAuthLoginURL: jest.fn(),
|
||||
};
|
||||
|
||||
const MockTenantService = {
|
||||
findOne: jest.fn(),
|
||||
};
|
||||
|
||||
describe('AuthController', () => {
|
||||
let authController: AuthController;
|
||||
let authService: jest.Mocked<AuthService>;
|
||||
let _tenantService: jest.Mocked<TenantService>;
|
||||
|
||||
beforeEach(async () => {
|
||||
const module = await Test.createTestingModule({
|
||||
providers: [
|
||||
getMockProvider(AuthService, MockAuthService),
|
||||
getMockProvider(TenantService, MockTenantService),
|
||||
],
|
||||
controllers: [AuthController],
|
||||
}).compile();
|
||||
|
||||
authController = module.get(AuthController);
|
||||
authService = module.get(AuthService);
|
||||
_tenantService = module.get(TenantService);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
it('should be defined', () => {
|
||||
expect(authController).toBeDefined();
|
||||
});
|
||||
describe('sendCode', () => {
|
||||
it('should send email verification code successfully', async () => {
|
||||
const mockTimestamp = DateTime.utc().toISO();
|
||||
const dto = new EmailVerificationMailingRequestDto();
|
||||
dto.email = faker.internet.email();
|
||||
|
||||
authService.sendEmailCode.mockResolvedValue(mockTimestamp);
|
||||
|
||||
const result = await authController.sendCode(dto);
|
||||
|
||||
expect(authService.sendEmailCode).toHaveBeenCalledWith(dto);
|
||||
expect(authService.sendEmailCode).toHaveBeenCalledTimes(1);
|
||||
expect(result).toEqual({ expiredAt: mockTimestamp });
|
||||
});
|
||||
|
||||
it('should handle sendEmailCode errors', async () => {
|
||||
const dto = new EmailVerificationMailingRequestDto();
|
||||
dto.email = faker.internet.email();
|
||||
const error = new InternalServerErrorException(
|
||||
'Email service unavailable',
|
||||
);
|
||||
|
||||
authService.sendEmailCode.mockRejectedValue(error);
|
||||
|
||||
await expect(authController.sendCode(dto)).rejects.toThrow(
|
||||
InternalServerErrorException,
|
||||
);
|
||||
expect(authService.sendEmailCode).toHaveBeenCalledWith(dto);
|
||||
});
|
||||
|
||||
it('should handle invalid email format', async () => {
|
||||
const dto = new EmailVerificationMailingRequestDto();
|
||||
dto.email = 'invalid-email';
|
||||
const error = new BadRequestException('Invalid email format');
|
||||
|
||||
authService.sendEmailCode.mockRejectedValue(error);
|
||||
|
||||
await expect(authController.sendCode(dto)).rejects.toThrow(
|
||||
BadRequestException,
|
||||
);
|
||||
});
|
||||
});
|
||||
describe('verifyEmailCode', () => {
|
||||
it('should verify email code successfully', async () => {
|
||||
const dto = new EmailVerificationCodeRequestDto();
|
||||
dto.code = faker.string.alphanumeric(6);
|
||||
dto.email = faker.internet.email();
|
||||
|
||||
authService.verifyEmailCode.mockResolvedValue(undefined);
|
||||
|
||||
await authController.verifyEmailCode(dto);
|
||||
|
||||
expect(authService.verifyEmailCode).toHaveBeenCalledWith(dto);
|
||||
expect(authService.verifyEmailCode).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('should handle invalid verification code', async () => {
|
||||
const dto = new EmailVerificationCodeRequestDto();
|
||||
dto.code = 'invalid-code';
|
||||
dto.email = faker.internet.email();
|
||||
const error = new BadRequestException('Invalid verification code');
|
||||
|
||||
authService.verifyEmailCode.mockRejectedValue(error);
|
||||
|
||||
await expect(authController.verifyEmailCode(dto)).rejects.toThrow(
|
||||
BadRequestException,
|
||||
);
|
||||
expect(authService.verifyEmailCode).toHaveBeenCalledWith(dto);
|
||||
});
|
||||
|
||||
it('should handle expired verification code', async () => {
|
||||
const dto = new EmailVerificationCodeRequestDto();
|
||||
dto.code = faker.string.alphanumeric(6);
|
||||
dto.email = faker.internet.email();
|
||||
const error = new BadRequestException('Verification code expired');
|
||||
|
||||
authService.verifyEmailCode.mockRejectedValue(error);
|
||||
|
||||
await expect(authController.verifyEmailCode(dto)).rejects.toThrow(
|
||||
BadRequestException,
|
||||
);
|
||||
});
|
||||
});
|
||||
describe('signUpEmailUser', () => {
|
||||
it('should sign up email user successfully', async () => {
|
||||
const dto = new EmailUserSignUpRequestDto();
|
||||
dto.email = faker.internet.email();
|
||||
dto.password = faker.internet.password();
|
||||
|
||||
authService.signUpEmailUser.mockResolvedValue(undefined as any);
|
||||
|
||||
const result = await authController.signUpEmailUser(dto);
|
||||
|
||||
expect(authService.signUpEmailUser).toHaveBeenCalledWith(dto);
|
||||
expect(authService.signUpEmailUser).toHaveBeenCalledTimes(1);
|
||||
expect(result).toBeUndefined();
|
||||
});
|
||||
|
||||
it('should handle email already exists error', async () => {
|
||||
const dto = new EmailUserSignUpRequestDto();
|
||||
dto.email = faker.internet.email();
|
||||
dto.password = faker.internet.password();
|
||||
const error = new BadRequestException('Email already exists');
|
||||
|
||||
authService.signUpEmailUser.mockRejectedValue(error);
|
||||
|
||||
await expect(authController.signUpEmailUser(dto)).rejects.toThrow(
|
||||
BadRequestException,
|
||||
);
|
||||
expect(authService.signUpEmailUser).toHaveBeenCalledWith(dto);
|
||||
});
|
||||
|
||||
it('should handle weak password error', async () => {
|
||||
const dto = new EmailUserSignUpRequestDto();
|
||||
dto.email = faker.internet.email();
|
||||
dto.password = '123'; // Weak password
|
||||
const error = new BadRequestException('Password is too weak');
|
||||
|
||||
authService.signUpEmailUser.mockRejectedValue(error);
|
||||
|
||||
await expect(authController.signUpEmailUser(dto)).rejects.toThrow(
|
||||
BadRequestException,
|
||||
);
|
||||
});
|
||||
});
|
||||
describe('signUpInvitationUser', () => {
|
||||
it('should sign up invitation user successfully', async () => {
|
||||
const dto = new InvitationUserSignUpRequestDto();
|
||||
dto.code = faker.string.alphanumeric(8);
|
||||
dto.email = faker.internet.email();
|
||||
dto.password = faker.internet.password();
|
||||
|
||||
authService.signUpInvitationUser.mockResolvedValue(undefined as any);
|
||||
|
||||
const result = await authController.signUpInvitationUser(dto);
|
||||
|
||||
expect(authService.signUpInvitationUser).toHaveBeenCalledWith(dto);
|
||||
expect(authService.signUpInvitationUser).toHaveBeenCalledTimes(1);
|
||||
expect(result).toBeUndefined();
|
||||
});
|
||||
|
||||
it('should handle invalid invitation code', async () => {
|
||||
const dto = new InvitationUserSignUpRequestDto();
|
||||
dto.code = 'invalid-code';
|
||||
dto.email = faker.internet.email();
|
||||
dto.password = faker.internet.password();
|
||||
const error = new BadRequestException('Invalid invitation code');
|
||||
|
||||
authService.signUpInvitationUser.mockRejectedValue(error);
|
||||
|
||||
await expect(authController.signUpInvitationUser(dto)).rejects.toThrow(
|
||||
BadRequestException,
|
||||
);
|
||||
expect(authService.signUpInvitationUser).toHaveBeenCalledWith(dto);
|
||||
});
|
||||
|
||||
it('should handle expired invitation', async () => {
|
||||
const dto = new InvitationUserSignUpRequestDto();
|
||||
dto.code = faker.string.alphanumeric(8);
|
||||
dto.email = faker.internet.email();
|
||||
dto.password = faker.internet.password();
|
||||
const error = new BadRequestException('Invitation has expired');
|
||||
|
||||
authService.signUpInvitationUser.mockRejectedValue(error);
|
||||
|
||||
await expect(authController.signUpInvitationUser(dto)).rejects.toThrow(
|
||||
BadRequestException,
|
||||
);
|
||||
});
|
||||
});
|
||||
describe('signInEmail', () => {
|
||||
it('should sign in email user successfully', () => {
|
||||
const user = new UserDto();
|
||||
user.id = faker.number.int();
|
||||
user.email = faker.internet.email();
|
||||
user.name = faker.person.fullName();
|
||||
const mockTokens = {
|
||||
accessToken: faker.string.alphanumeric(32),
|
||||
refreshToken: faker.string.alphanumeric(32),
|
||||
};
|
||||
|
||||
authService.signIn.mockReturnValue(mockTokens as any);
|
||||
|
||||
const result = authController.signInEmail(user);
|
||||
|
||||
expect(authService.signIn).toHaveBeenCalledWith(user);
|
||||
expect(authService.signIn).toHaveBeenCalledTimes(1);
|
||||
expect(result).toEqual(mockTokens);
|
||||
});
|
||||
});
|
||||
|
||||
describe('refreshToken', () => {
|
||||
it('should refresh token successfully', () => {
|
||||
const user = new UserDto();
|
||||
user.id = faker.number.int();
|
||||
user.email = faker.internet.email();
|
||||
user.name = faker.person.fullName();
|
||||
const mockTokens = {
|
||||
accessToken: faker.string.alphanumeric(32),
|
||||
refreshToken: faker.string.alphanumeric(32),
|
||||
};
|
||||
|
||||
authService.refreshToken.mockReturnValue(mockTokens as any);
|
||||
|
||||
const result = authController.refreshToken(user);
|
||||
|
||||
expect(authService.refreshToken).toHaveBeenCalledWith(user);
|
||||
expect(authService.refreshToken).toHaveBeenCalledTimes(1);
|
||||
expect(result).toEqual(mockTokens);
|
||||
});
|
||||
});
|
||||
|
||||
describe('signUpOAuthUser', () => {
|
||||
it('should sign up OAuth user successfully', async () => {
|
||||
const dto = new OAuthUserSignUpRequestDto();
|
||||
dto.email = faker.internet.email();
|
||||
dto.projectName = faker.company.name();
|
||||
dto.roleName = faker.person.jobTitle();
|
||||
|
||||
authService.signUpOAuthUser.mockResolvedValue(undefined);
|
||||
|
||||
const result = await authController.signUpOAuthUser(dto);
|
||||
|
||||
expect(authService.signUpOAuthUser).toHaveBeenCalledWith(dto);
|
||||
expect(authService.signUpOAuthUser).toHaveBeenCalledTimes(1);
|
||||
expect(result).toBeUndefined();
|
||||
});
|
||||
|
||||
it('should handle OAuth provider error', async () => {
|
||||
const dto = new OAuthUserSignUpRequestDto();
|
||||
dto.email = faker.internet.email();
|
||||
dto.projectName = faker.company.name();
|
||||
dto.roleName = faker.person.jobTitle();
|
||||
const error = new InternalServerErrorException('OAuth provider error');
|
||||
|
||||
authService.signUpOAuthUser.mockRejectedValue(error);
|
||||
|
||||
await expect(authController.signUpOAuthUser(dto)).rejects.toThrow(
|
||||
InternalServerErrorException,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('redirectToLoginURL', () => {
|
||||
it('should return OAuth login URL', async () => {
|
||||
const callbackUrl = faker.internet.url();
|
||||
const mockUrl = faker.internet.url();
|
||||
|
||||
authService.getOAuthLoginURL.mockResolvedValue(mockUrl);
|
||||
|
||||
const result = await authController.redirectToLoginURL(callbackUrl);
|
||||
|
||||
expect(authService.getOAuthLoginURL).toHaveBeenCalledWith(callbackUrl);
|
||||
expect(authService.getOAuthLoginURL).toHaveBeenCalledTimes(1);
|
||||
expect(result).toEqual({ url: mockUrl });
|
||||
});
|
||||
});
|
||||
|
||||
describe('handleCallback', () => {
|
||||
it('should handle OAuth callback successfully', async () => {
|
||||
const query = { code: faker.string.alphanumeric(32) };
|
||||
const mockTokens = {
|
||||
accessToken: faker.string.alphanumeric(32),
|
||||
refreshToken: faker.string.alphanumeric(32),
|
||||
};
|
||||
|
||||
authService.signInByOAuth.mockResolvedValue(mockTokens);
|
||||
|
||||
const result = await authController.handleCallback(query);
|
||||
|
||||
expect(authService.signInByOAuth).toHaveBeenCalledWith(query.code);
|
||||
expect(authService.signInByOAuth).toHaveBeenCalledTimes(1);
|
||||
expect(result).toEqual(mockTokens);
|
||||
});
|
||||
|
||||
it('should handle OAuth authentication failure', async () => {
|
||||
const query = { code: 'invalid-code' };
|
||||
const error = new BadRequestException('OAuth authentication failed');
|
||||
|
||||
authService.signInByOAuth.mockRejectedValue(error);
|
||||
|
||||
await expect(authController.handleCallback(query)).rejects.toThrow(
|
||||
BadRequestException,
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,124 @@
|
||||
/**
|
||||
* Copyright 2025 LY Corporation
|
||||
*
|
||||
* LY Corporation licenses this file to you under the Apache License,
|
||||
* version 2.0 (the "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at:
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
* License for the specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
import {
|
||||
Body,
|
||||
Controller,
|
||||
Get,
|
||||
HttpCode,
|
||||
Post,
|
||||
Query,
|
||||
UseGuards,
|
||||
} from '@nestjs/common';
|
||||
import { AuthGuard } from '@nestjs/passport';
|
||||
import {
|
||||
ApiBearerAuth,
|
||||
ApiBody,
|
||||
ApiCreatedResponse,
|
||||
ApiOkResponse,
|
||||
ApiQuery,
|
||||
ApiTags,
|
||||
} from '@nestjs/swagger';
|
||||
|
||||
import { CurrentUser } from '../user/decorators';
|
||||
import { UserDto } from '../user/dtos';
|
||||
import { AuthService } from './auth.service';
|
||||
import {
|
||||
EmailUserSignInRequestDto,
|
||||
EmailUserSignUpRequestDto,
|
||||
EmailVerificationCodeRequestDto,
|
||||
EmailVerificationMailingRequestDto,
|
||||
InvitationUserSignUpRequestDto,
|
||||
OAuthUserSignUpRequestDto,
|
||||
} from './dtos/requests';
|
||||
import {
|
||||
OAuthLoginUrlResponseDto,
|
||||
SendEmailCodeResponseDto,
|
||||
SignInResponseDto,
|
||||
} from './dtos/responses';
|
||||
import { JwtAuthGuard } from './guards';
|
||||
import { UseEmailGuard } from './guards/use-email.guard';
|
||||
import { UseOAuthGuard } from './guards/use-oauth.guard';
|
||||
|
||||
@ApiTags('auth')
|
||||
@Controller('/admin/auth')
|
||||
export class AuthController {
|
||||
constructor(private readonly authService: AuthService) {}
|
||||
|
||||
@ApiCreatedResponse({ type: SendEmailCodeResponseDto })
|
||||
@Post('email/code')
|
||||
async sendCode(@Body() body: EmailVerificationMailingRequestDto) {
|
||||
const expiredAt = await this.authService.sendEmailCode(body);
|
||||
return SendEmailCodeResponseDto.transform({ expiredAt });
|
||||
}
|
||||
|
||||
@HttpCode(200)
|
||||
@Post('email/code/verify')
|
||||
async verifyEmailCode(@Body() body: EmailVerificationCodeRequestDto) {
|
||||
await this.authService.verifyEmailCode(body);
|
||||
}
|
||||
|
||||
@UseGuards(UseEmailGuard)
|
||||
@Post('signUp/email')
|
||||
async signUpEmailUser(@Body() body: EmailUserSignUpRequestDto) {
|
||||
await this.authService.signUpEmailUser(body);
|
||||
}
|
||||
|
||||
@UseGuards(UseEmailGuard)
|
||||
@Post('signUp/invitation')
|
||||
async signUpInvitationUser(@Body() body: InvitationUserSignUpRequestDto) {
|
||||
await this.authService.signUpInvitationUser(body);
|
||||
}
|
||||
|
||||
@UseGuards(UseOAuthGuard)
|
||||
@Post('signUp/oauth')
|
||||
async signUpOAuthUser(@Body() body: OAuthUserSignUpRequestDto) {
|
||||
await this.authService.signUpOAuthUser(body);
|
||||
}
|
||||
|
||||
@ApiBody({ type: EmailUserSignInRequestDto })
|
||||
@ApiCreatedResponse({ type: SignInResponseDto })
|
||||
@Post('signIn/email')
|
||||
@UseGuards(UseEmailGuard, AuthGuard('local'))
|
||||
signInEmail(@CurrentUser() user: UserDto) {
|
||||
return this.authService.signIn(user);
|
||||
}
|
||||
|
||||
@UseGuards(UseOAuthGuard)
|
||||
@ApiQuery({ name: 'callback_url', required: false })
|
||||
@ApiOkResponse({ type: OAuthLoginUrlResponseDto })
|
||||
@Get('signIn/oauth/loginURL')
|
||||
async redirectToLoginURL(@Query('callback_url') callbackUrl: string) {
|
||||
return {
|
||||
url: await this.authService.getOAuthLoginURL(callbackUrl),
|
||||
};
|
||||
}
|
||||
|
||||
@UseGuards(UseOAuthGuard)
|
||||
@ApiQuery({ name: 'code', required: false })
|
||||
@ApiOkResponse({ type: SignInResponseDto })
|
||||
@Get('signIn/oauth')
|
||||
async handleCallback(@Query() query: { code: string }) {
|
||||
return await this.authService.signInByOAuth(query.code);
|
||||
}
|
||||
|
||||
@ApiBearerAuth()
|
||||
@ApiOkResponse({ type: SignInResponseDto })
|
||||
@UseGuards(JwtAuthGuard)
|
||||
@Get('refresh')
|
||||
refreshToken(@CurrentUser() user: UserDto) {
|
||||
return this.authService.refreshToken(user);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
/**
|
||||
* Copyright 2025 LY Corporation
|
||||
*
|
||||
* LY Corporation licenses this file to you under the Apache License,
|
||||
* version 2.0 (the "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at:
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
* License for the specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
import { HttpModule } from '@nestjs/axios';
|
||||
import { forwardRef, Module } from '@nestjs/common';
|
||||
import { ConfigService } from '@nestjs/config';
|
||||
import { JwtModule } from '@nestjs/jwt';
|
||||
import { PassportModule } from '@nestjs/passport';
|
||||
|
||||
import { CodeModule } from '@/shared/code/code.module';
|
||||
import { MailingModule } from '@/shared/mailing/mailing.module';
|
||||
|
||||
import type { ConfigServiceType } from '@/types/config-service.type';
|
||||
import { ApiKeyModule } from '../project/api-key/api-key.module';
|
||||
import { MemberModule } from '../project/member/member.module';
|
||||
import { RoleModule } from '../project/role/role.module';
|
||||
import { TenantModule } from '../tenant/tenant.module';
|
||||
import { UserModule } from '../user/user.module';
|
||||
import { AuthController } from './auth.controller';
|
||||
import { AuthService } from './auth.service';
|
||||
import { JwtStrategy } from './strategies/jwt.strategy';
|
||||
import { LocalStrategy } from './strategies/local.strategy';
|
||||
|
||||
@Module({
|
||||
imports: [
|
||||
forwardRef(() => CodeModule),
|
||||
forwardRef(() => UserModule),
|
||||
forwardRef(() => PassportModule),
|
||||
forwardRef(() => MailingModule),
|
||||
forwardRef(() => ApiKeyModule),
|
||||
forwardRef(() => TenantModule),
|
||||
forwardRef(() => RoleModule),
|
||||
forwardRef(() => MemberModule),
|
||||
HttpModule.register({
|
||||
timeout: 5000,
|
||||
maxRedirects: 5,
|
||||
}),
|
||||
JwtModule.registerAsync({
|
||||
global: true,
|
||||
inject: [ConfigService],
|
||||
useFactory: (configService: ConfigService<ConfigServiceType>) => {
|
||||
const { secret } = configService.get('jwt', { infer: true }) ?? {};
|
||||
return { secret };
|
||||
},
|
||||
}),
|
||||
],
|
||||
providers: [AuthService, LocalStrategy, JwtStrategy],
|
||||
controllers: [AuthController],
|
||||
exports: [AuthService],
|
||||
})
|
||||
export class AuthModule {}
|
||||
@@ -0,0 +1,443 @@
|
||||
/**
|
||||
* Copyright 2025 LY Corporation
|
||||
*
|
||||
* LY Corporation licenses this file to you under the Apache License,
|
||||
* version 2.0 (the "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at:
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
* License for the specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
|
||||
/* eslint-disable @typescript-eslint/no-unsafe-member-access */
|
||||
|
||||
import { faker } from '@faker-js/faker';
|
||||
import { BadRequestException, NotFoundException } from '@nestjs/common';
|
||||
import { Test } from '@nestjs/testing';
|
||||
import { getRepositoryToken } from '@nestjs/typeorm';
|
||||
import { ClsModule } from 'nestjs-cls';
|
||||
import type { Repository } from 'typeorm';
|
||||
|
||||
import { CodeEntity } from '@/shared/code/code.entity';
|
||||
import { NotVerifiedEmailException } from '@/shared/mailing/exceptions';
|
||||
|
||||
import {
|
||||
emailFixture,
|
||||
passwordFixture,
|
||||
userFixture,
|
||||
} from '@/test-utils/fixtures';
|
||||
import type {
|
||||
CodeRepositoryStub,
|
||||
TenantRepositoryStub,
|
||||
} from '@/test-utils/stubs';
|
||||
import { TestConfig } from '@/test-utils/util-functions';
|
||||
import {
|
||||
AuthServiceProviders,
|
||||
MockEmailVerificationMailingService,
|
||||
MockJwtService,
|
||||
} from '../../../test-utils/providers/auth.service.providers';
|
||||
import { ApiKeyEntity } from '../project/api-key/api-key.entity';
|
||||
import { TenantEntity } from '../tenant/tenant.entity';
|
||||
import { UserDto } from '../user/dtos';
|
||||
import {
|
||||
SignUpMethodEnum,
|
||||
UserStateEnum,
|
||||
UserTypeEnum,
|
||||
} from '../user/entities/enums';
|
||||
import { UserEntity } from '../user/entities/user.entity';
|
||||
import {
|
||||
UserAlreadyExistsException,
|
||||
UserNotFoundException,
|
||||
} from '../user/exceptions';
|
||||
import { AuthService } from './auth.service';
|
||||
import {
|
||||
SendEmailCodeDto,
|
||||
SignUpEmailUserDto,
|
||||
SignUpInvitationUserDto,
|
||||
SignUpOauthUserDto,
|
||||
ValidateEmailUserDto,
|
||||
VerifyEmailCodeDto,
|
||||
} from './dtos';
|
||||
import { PasswordNotMatchException, UserBlockedException } from './exceptions';
|
||||
|
||||
describe('auth service ', () => {
|
||||
let authService: AuthService;
|
||||
let userRepo: Repository<UserEntity>;
|
||||
let tenantRepo: TenantRepositoryStub;
|
||||
let codeRepo: CodeRepositoryStub;
|
||||
let apiKeyRepo: Repository<ApiKeyEntity>;
|
||||
beforeEach(async () => {
|
||||
const module = await Test.createTestingModule({
|
||||
imports: [TestConfig, ClsModule.forRoot()],
|
||||
providers: AuthServiceProviders,
|
||||
}).compile();
|
||||
authService = module.get(AuthService);
|
||||
userRepo = module.get(getRepositoryToken(UserEntity));
|
||||
tenantRepo = module.get(getRepositoryToken(TenantEntity));
|
||||
codeRepo = module.get(getRepositoryToken(CodeEntity));
|
||||
apiKeyRepo = module.get(getRepositoryToken(ApiKeyEntity));
|
||||
});
|
||||
|
||||
describe('sendEmailCode', () => {
|
||||
let dto: SendEmailCodeDto;
|
||||
beforeEach(() => {
|
||||
dto = new SendEmailCodeDto();
|
||||
});
|
||||
|
||||
it('sending a code by email succeeds with a valid email', async () => {
|
||||
const validEmail = faker.internet.email();
|
||||
dto.email = validEmail;
|
||||
jest.spyOn(userRepo, 'findOne').mockResolvedValue(null);
|
||||
jest.spyOn(MockEmailVerificationMailingService, 'send');
|
||||
|
||||
const timeoutTime = await authService.sendEmailCode(dto);
|
||||
|
||||
expect(new Date(timeoutTime) > new Date()).toEqual(true);
|
||||
});
|
||||
it('sending a code by email succeeds with a duplicate email', async () => {
|
||||
const duplicateEmail = emailFixture;
|
||||
dto.email = duplicateEmail;
|
||||
jest.spyOn(MockEmailVerificationMailingService, 'send');
|
||||
|
||||
await expect(authService.sendEmailCode(dto)).rejects.toThrow(
|
||||
UserAlreadyExistsException,
|
||||
);
|
||||
|
||||
expect(MockEmailVerificationMailingService.send).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('verifyEmailCode', () => {
|
||||
it('verifying email code succeeds in test environment', async () => {
|
||||
const dto = new VerifyEmailCodeDto();
|
||||
dto.code = faker.string.alphanumeric(6);
|
||||
dto.email = faker.internet.email();
|
||||
|
||||
// In test environment, this method returns undefined
|
||||
const result = await authService.verifyEmailCode(dto);
|
||||
|
||||
expect(result).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe('validateEmailUser', () => {
|
||||
it('validating a user succeeds with valid inputs', async () => {
|
||||
const dto = new ValidateEmailUserDto();
|
||||
dto.email = emailFixture;
|
||||
dto.password = passwordFixture;
|
||||
|
||||
const result = await authService.validateEmailUser(dto);
|
||||
|
||||
expect(result).toEqual({
|
||||
...userFixture,
|
||||
signUpMethod: SignUpMethodEnum.EMAIL,
|
||||
});
|
||||
});
|
||||
it('validating a user fails with a nonexistent user', async () => {
|
||||
jest.spyOn(userRepo, 'findOne').mockResolvedValue(null);
|
||||
const dto = new ValidateEmailUserDto();
|
||||
dto.email = faker.internet.email();
|
||||
dto.password = passwordFixture;
|
||||
|
||||
await expect(authService.validateEmailUser(dto)).rejects.toThrow(
|
||||
UserNotFoundException,
|
||||
);
|
||||
});
|
||||
it('validating a user fails with an invalid password', async () => {
|
||||
const invalidPassword = faker.internet.password();
|
||||
const dto = new ValidateEmailUserDto();
|
||||
dto.email = faker.internet.email();
|
||||
dto.password = invalidPassword;
|
||||
|
||||
await expect(authService.validateEmailUser(dto)).rejects.toThrow(
|
||||
PasswordNotMatchException,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('signUpEmailUser', () => {
|
||||
it('signing up by an email succeeds with valid inputs', async () => {
|
||||
const dto = new SignUpEmailUserDto();
|
||||
dto.email = faker.internet.email();
|
||||
dto.password = faker.internet.password();
|
||||
codeRepo.setIsVerified(true);
|
||||
jest.spyOn(userRepo, 'findOneBy').mockResolvedValue(null);
|
||||
|
||||
const user = await authService.signUpEmailUser(dto);
|
||||
|
||||
expect(user.signUpMethod).toEqual(SignUpMethodEnum.EMAIL);
|
||||
});
|
||||
it('signing up by an email fails with a not verified email', async () => {
|
||||
const dto = new SignUpEmailUserDto();
|
||||
dto.email = faker.internet.email();
|
||||
dto.password = faker.internet.password();
|
||||
codeRepo.setIsVerified(false);
|
||||
jest.spyOn(userRepo, 'findOneBy').mockResolvedValue(null);
|
||||
jest.spyOn(userRepo, 'save');
|
||||
|
||||
await expect(authService.signUpEmailUser(dto)).rejects.toThrow(
|
||||
NotVerifiedEmailException,
|
||||
);
|
||||
|
||||
expect(userRepo.save).not.toHaveBeenCalled();
|
||||
});
|
||||
it('signing up by an email fails with a not verification requested email', async () => {
|
||||
const dto = new SignUpEmailUserDto();
|
||||
dto.email = faker.internet.email();
|
||||
dto.password = faker.internet.password();
|
||||
codeRepo.setNull();
|
||||
jest.spyOn(userRepo, 'findOneBy').mockResolvedValue(null);
|
||||
jest.spyOn(userRepo, 'save');
|
||||
|
||||
await expect(authService.signUpEmailUser(dto)).rejects.toThrow(
|
||||
new BadRequestException('must request email verification'),
|
||||
);
|
||||
|
||||
expect(userRepo.save).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('signUpInvitationUser', () => {
|
||||
it('signing up by invitation succeeds with valid inputs', async () => {
|
||||
const dto = new SignUpInvitationUserDto();
|
||||
dto.code = codeRepo.entities?.[0]?.code ?? faker.string.alphanumeric(8);
|
||||
dto.email = faker.internet.email();
|
||||
dto.password = faker.internet.password();
|
||||
codeRepo.setIsVerified(false); // Not verified initially
|
||||
jest.spyOn(userRepo, 'findOneBy').mockResolvedValue(null);
|
||||
|
||||
// Mock the codeService.getDataByCodeAndType to return valid data
|
||||
|
||||
const authServiceAny = authService as any;
|
||||
jest
|
||||
.spyOn(authServiceAny.codeService, 'getDataByCodeAndType')
|
||||
.mockResolvedValue({
|
||||
userType: UserTypeEnum.GENERAL,
|
||||
roleId: faker.number.int(),
|
||||
invitedBy: new UserDto(),
|
||||
} as any);
|
||||
|
||||
// Mock the createUserService to avoid complex dependencies
|
||||
const mockUser = new UserEntity();
|
||||
mockUser.signUpMethod = SignUpMethodEnum.EMAIL;
|
||||
mockUser.email = faker.internet.email();
|
||||
|
||||
jest
|
||||
.spyOn(authServiceAny.createUserService, 'createInvitationUser')
|
||||
.mockResolvedValue(mockUser as any);
|
||||
|
||||
const user = await authService.signUpInvitationUser(dto);
|
||||
|
||||
expect(user.signUpMethod).toEqual(SignUpMethodEnum.EMAIL);
|
||||
});
|
||||
|
||||
it('signing up by invitation fails with invalid invitation code', async () => {
|
||||
const dto = new SignUpInvitationUserDto();
|
||||
dto.code = 'invalid-code';
|
||||
dto.email = faker.internet.email();
|
||||
dto.password = faker.internet.password();
|
||||
codeRepo.setNull();
|
||||
|
||||
await expect(authService.signUpInvitationUser(dto)).rejects.toThrow(
|
||||
NotFoundException,
|
||||
);
|
||||
});
|
||||
|
||||
it('signing up by invitation fails with already verified code', async () => {
|
||||
const dto = new SignUpInvitationUserDto();
|
||||
dto.code = faker.string.alphanumeric(8);
|
||||
dto.email = faker.internet.email();
|
||||
dto.password = faker.internet.password();
|
||||
codeRepo.setIsVerified(true); // Already verified
|
||||
|
||||
await expect(authService.signUpInvitationUser(dto)).rejects.toThrow(
|
||||
new BadRequestException('already verified'),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('signUpOAuthUser', () => {
|
||||
it('signing up by OAuth succeeds with valid inputs', async () => {
|
||||
const dto = new SignUpOauthUserDto();
|
||||
dto.email = faker.internet.email();
|
||||
dto.projectName = faker.company.name();
|
||||
dto.roleName = faker.person.jobTitle();
|
||||
jest.spyOn(userRepo, 'findOneBy').mockResolvedValue(null);
|
||||
jest.spyOn(userRepo, 'save').mockResolvedValue(new UserEntity());
|
||||
|
||||
await authService.signUpOAuthUser(dto);
|
||||
|
||||
expect(userRepo.save).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('signing up by OAuth fails with existing user', async () => {
|
||||
const dto = new SignUpOauthUserDto();
|
||||
dto.email = emailFixture;
|
||||
dto.projectName = faker.company.name();
|
||||
dto.roleName = faker.person.jobTitle();
|
||||
|
||||
await expect(authService.signUpOAuthUser(dto)).rejects.toThrow(
|
||||
UserAlreadyExistsException,
|
||||
);
|
||||
});
|
||||
|
||||
it('signing up by OAuth succeeds with empty project and role', async () => {
|
||||
const dto = new SignUpOauthUserDto();
|
||||
dto.email = faker.internet.email();
|
||||
dto.projectName = '';
|
||||
dto.roleName = '';
|
||||
jest.spyOn(userRepo, 'findOneBy').mockResolvedValue(null);
|
||||
|
||||
const result = await authService.signUpOAuthUser(dto);
|
||||
|
||||
expect(result).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe('signIn', () => {
|
||||
it('signing in succeeds with a valid user', async () => {
|
||||
const activeUser = new UserEntity();
|
||||
activeUser.state = UserStateEnum.Active;
|
||||
jest.spyOn(userRepo, 'findOne').mockResolvedValue(activeUser);
|
||||
const dto = new UserDto();
|
||||
dto.email = faker.internet.email();
|
||||
dto.id = faker.number.int();
|
||||
|
||||
const jwt = await authService.signIn(dto);
|
||||
|
||||
expect(jwt).toHaveProperty('accessToken');
|
||||
expect(jwt).toHaveProperty('refreshToken');
|
||||
});
|
||||
it('signing in fails with a blocked user', async () => {
|
||||
const blockedUser = new UserEntity();
|
||||
blockedUser.state = UserStateEnum.Blocked;
|
||||
jest.spyOn(userRepo, 'findOne').mockResolvedValue(blockedUser);
|
||||
const dto = new UserDto();
|
||||
dto.email = faker.internet.email();
|
||||
dto.id = faker.number.int();
|
||||
|
||||
await expect(authService.signIn(dto)).rejects.toThrow(
|
||||
UserBlockedException,
|
||||
);
|
||||
|
||||
expect(MockJwtService.sign).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('refreshToken', () => {
|
||||
it('refreshing token succeeds with valid user', async () => {
|
||||
const activeUser = new UserEntity();
|
||||
activeUser.state = UserStateEnum.Active;
|
||||
activeUser.id = faker.number.int();
|
||||
jest.spyOn(userRepo, 'findOne').mockResolvedValue(activeUser);
|
||||
|
||||
const jwt = await authService.refreshToken({ id: activeUser.id });
|
||||
|
||||
expect(jwt).toHaveProperty('accessToken');
|
||||
expect(jwt).toHaveProperty('refreshToken');
|
||||
expect(MockJwtService.sign).toHaveBeenCalledTimes(2);
|
||||
});
|
||||
|
||||
it('refreshing token fails with blocked user', async () => {
|
||||
const blockedUser = new UserEntity();
|
||||
blockedUser.state = UserStateEnum.Blocked;
|
||||
blockedUser.id = faker.number.int();
|
||||
jest.spyOn(userRepo, 'findOne').mockResolvedValue(blockedUser);
|
||||
|
||||
await expect(
|
||||
authService.refreshToken({ id: blockedUser.id }),
|
||||
).rejects.toThrow(UserBlockedException);
|
||||
|
||||
expect(MockJwtService.sign).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('refreshing token fails with non-existent user', async () => {
|
||||
const userId = faker.number.int();
|
||||
jest.spyOn(userRepo, 'findOne').mockResolvedValue(null);
|
||||
|
||||
await expect(authService.refreshToken({ id: userId })).rejects.toThrow(
|
||||
UserNotFoundException,
|
||||
);
|
||||
|
||||
expect(MockJwtService.sign).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('validateApiKey', () => {
|
||||
it('validating an api key succeeds with a valid api key', async () => {
|
||||
const apiKey = faker.string.uuid();
|
||||
const projectId = faker.number.int();
|
||||
|
||||
const result = await authService.validateApiKey(apiKey, projectId);
|
||||
|
||||
expect(result).toEqual(true);
|
||||
});
|
||||
it('validating an api key succeeds with an invalid api key', async () => {
|
||||
const apiKey = faker.string.uuid();
|
||||
const projectId = faker.number.int();
|
||||
jest.spyOn(apiKeyRepo, 'find').mockResolvedValue([] as ApiKeyEntity[]);
|
||||
|
||||
const result = await authService.validateApiKey(apiKey, projectId);
|
||||
|
||||
expect(result).toEqual(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getOAuthLoginURL', () => {
|
||||
it('getting an oauth login url succeeds with oauth using tenant', async () => {
|
||||
const clientId = faker.string.sample();
|
||||
const scopeString = faker.string.sample();
|
||||
const authCodeRequestURL = faker.internet.domainName();
|
||||
tenantRepo.setUseOAuth(true, {
|
||||
clientId,
|
||||
scopeString,
|
||||
authCodeRequestURL,
|
||||
});
|
||||
|
||||
const OAuthLoginURL = await authService.getOAuthLoginURL();
|
||||
|
||||
expect(OAuthLoginURL.includes(authCodeRequestURL));
|
||||
expect(OAuthLoginURL.includes(`client_id=${clientId}`));
|
||||
expect(OAuthLoginURL.includes(`scope=${scopeString}`));
|
||||
});
|
||||
it('getting an oauth login url fails with no oauth using tenant', async () => {
|
||||
tenantRepo.setUseOAuth(false, null);
|
||||
|
||||
await expect(authService.getOAuthLoginURL()).rejects.toThrow(
|
||||
new BadRequestException('OAuth login is disabled.'),
|
||||
);
|
||||
});
|
||||
it('getting an oauth login url fails with no oauthconfig tenant', async () => {
|
||||
tenantRepo.setUseOAuth(true, null);
|
||||
|
||||
await expect(authService.getOAuthLoginURL()).rejects.toThrow(
|
||||
new BadRequestException('OAuth Config is required.'),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('signInByOAuth', () => {
|
||||
it('signing in by OAuth fails when OAuth is disabled', async () => {
|
||||
const code = faker.string.alphanumeric(32);
|
||||
tenantRepo.setUseOAuth(false, null);
|
||||
|
||||
await expect(authService.signInByOAuth(code)).rejects.toThrow(
|
||||
new BadRequestException('OAuth login is disabled.'),
|
||||
);
|
||||
});
|
||||
|
||||
it('signing in by OAuth fails with no OAuth config', async () => {
|
||||
const code = faker.string.alphanumeric(32);
|
||||
tenantRepo.setUseOAuth(true, null);
|
||||
|
||||
await expect(authService.signInByOAuth(code)).rejects.toThrow(
|
||||
new BadRequestException('OAuth Config is required.'),
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,362 @@
|
||||
/**
|
||||
* Copyright 2025 LY Corporation
|
||||
*
|
||||
* LY Corporation licenses this file to you under the Apache License,
|
||||
* version 2.0 (the "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at:
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
* License for the specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
import crypto from 'crypto';
|
||||
import { HttpService } from '@nestjs/axios';
|
||||
import {
|
||||
BadRequestException,
|
||||
Injectable,
|
||||
InternalServerErrorException,
|
||||
Logger,
|
||||
} from '@nestjs/common';
|
||||
import { ConfigService } from '@nestjs/config';
|
||||
import { JwtService } from '@nestjs/jwt';
|
||||
import { AxiosError, AxiosResponse } from 'axios';
|
||||
import * as bcrypt from 'bcrypt';
|
||||
import { DateTime } from 'luxon';
|
||||
import type { StringValue } from 'ms';
|
||||
import { catchError, lastValueFrom, map } from 'rxjs';
|
||||
import { Transactional } from 'typeorm-transactional';
|
||||
|
||||
import { EmailVerificationMailingService } from '@/shared/mailing/email-verification-mailing.service';
|
||||
import { NotVerifiedEmailException } from '@/shared/mailing/exceptions';
|
||||
|
||||
import type { ConfigServiceType } from '@/types/config-service.type';
|
||||
import { CodeTypeEnum } from '../../../shared/code/code-type.enum';
|
||||
import { CodeService } from '../../../shared/code/code.service';
|
||||
import { ApiKeyService } from '../project/api-key/api-key.service';
|
||||
import { MemberService } from '../project/member/member.service';
|
||||
import { RoleService } from '../project/role/role.service';
|
||||
import { TenantService } from '../tenant/tenant.service';
|
||||
import { CreateUserService } from '../user/create-user.service';
|
||||
import { UserDto } from '../user/dtos';
|
||||
import { SignUpMethodEnum, UserStateEnum } from '../user/entities/enums';
|
||||
import {
|
||||
UserAlreadyExistsException,
|
||||
UserNotFoundException,
|
||||
} from '../user/exceptions';
|
||||
import { UserService } from '../user/user.service';
|
||||
import type {
|
||||
JwtDto,
|
||||
SendEmailCodeDto,
|
||||
SignUpInvitationUserDto,
|
||||
ValidateEmailUserDto,
|
||||
VerifyEmailCodeDto,
|
||||
} from './dtos';
|
||||
import { SignUpEmailUserDto, SignUpOauthUserDto } from './dtos';
|
||||
import { PasswordNotMatchException, UserBlockedException } from './exceptions';
|
||||
|
||||
interface AccessTokenResponse {
|
||||
access_token: string;
|
||||
token_type: string;
|
||||
expires_in: number;
|
||||
refresh_token: string;
|
||||
scope: string;
|
||||
}
|
||||
|
||||
type UserProfileResponse = Record<string, string>;
|
||||
|
||||
@Injectable()
|
||||
export class AuthService {
|
||||
private readonly logger = new Logger(AuthService.name);
|
||||
|
||||
constructor(
|
||||
private readonly createUserService: CreateUserService,
|
||||
private readonly userService: UserService,
|
||||
private readonly jwtService: JwtService,
|
||||
private readonly emailVerificationMailingService: EmailVerificationMailingService,
|
||||
private readonly codeService: CodeService,
|
||||
private readonly apiKeyService: ApiKeyService,
|
||||
private readonly tenantService: TenantService,
|
||||
private readonly roleService: RoleService,
|
||||
private readonly memberService: MemberService,
|
||||
private readonly configService: ConfigService<ConfigServiceType>,
|
||||
private readonly httpService: HttpService,
|
||||
) {}
|
||||
|
||||
async sendEmailCode({ email }: SendEmailCodeDto) {
|
||||
const user = await this.userService.findByEmailAndSignUpMethod(
|
||||
email,
|
||||
SignUpMethodEnum.EMAIL,
|
||||
);
|
||||
if (user) throw new UserAlreadyExistsException();
|
||||
await this.memberService.validateEmail(email);
|
||||
|
||||
const code = await this.codeService.setCode({
|
||||
type: CodeTypeEnum.EMAIL_VEIRIFICATION,
|
||||
key: email,
|
||||
});
|
||||
|
||||
// Skip email sending in test environment
|
||||
if (process.env.NODE_ENV === 'test') {
|
||||
this.logger.warn(
|
||||
`Skipping email sending for code: ${code}, email: ${email}`,
|
||||
);
|
||||
} else {
|
||||
await this.emailVerificationMailingService.send({ code, email });
|
||||
}
|
||||
|
||||
return DateTime.utc()
|
||||
.plus({ seconds: 5 * 60 })
|
||||
.toISO();
|
||||
}
|
||||
|
||||
async verifyEmailCode({ code, email }: VerifyEmailCodeDto) {
|
||||
if (process.env.NODE_ENV === 'test') return;
|
||||
const { error } = await this.codeService.verifyCode({
|
||||
type: CodeTypeEnum.EMAIL_VEIRIFICATION,
|
||||
key: email,
|
||||
code,
|
||||
});
|
||||
|
||||
if (error) throw error;
|
||||
}
|
||||
|
||||
async validateEmailUser({ email, password }: ValidateEmailUserDto) {
|
||||
const user = await this.userService.findByEmailAndSignUpMethod(
|
||||
email,
|
||||
SignUpMethodEnum.EMAIL,
|
||||
);
|
||||
if (!user) throw new UserNotFoundException();
|
||||
if (!bcrypt.compareSync(password, user.hashPassword)) {
|
||||
throw new PasswordNotMatchException();
|
||||
}
|
||||
return user;
|
||||
}
|
||||
|
||||
@Transactional()
|
||||
async signUpEmailUser(dto: SignUpEmailUserDto) {
|
||||
let isVerified: boolean;
|
||||
try {
|
||||
isVerified = await this.codeService.checkVerified(
|
||||
CodeTypeEnum.EMAIL_VEIRIFICATION,
|
||||
dto.email,
|
||||
);
|
||||
} catch {
|
||||
throw new BadRequestException('must request email verification');
|
||||
}
|
||||
if (!isVerified) throw new NotVerifiedEmailException();
|
||||
|
||||
return await this.createUserService.createEmailUser(dto);
|
||||
}
|
||||
|
||||
async signUpInvitationUser(dto: SignUpInvitationUserDto) {
|
||||
const { code, ...rest } = dto;
|
||||
|
||||
const { error } = await this.codeService.verifyCode({
|
||||
type: CodeTypeEnum.USER_INVITATION,
|
||||
key: dto.email,
|
||||
code,
|
||||
});
|
||||
if (error) throw error;
|
||||
|
||||
const data = await this.codeService.getDataByCodeAndType(
|
||||
CodeTypeEnum.USER_INVITATION,
|
||||
code,
|
||||
);
|
||||
|
||||
return await this.createUserService.createInvitationUser({
|
||||
...rest,
|
||||
type: data.userType,
|
||||
roleId: data.roleId,
|
||||
});
|
||||
}
|
||||
|
||||
@Transactional()
|
||||
async signUpOAuthUser(dto: SignUpOauthUserDto) {
|
||||
const { email, projectName, roleName } = dto;
|
||||
|
||||
const user = await this.createUserService.createOAuthUser({ email });
|
||||
if (!projectName || !roleName) return;
|
||||
const role = await this.roleService.findByProjectNameAndRoleName(
|
||||
projectName,
|
||||
roleName,
|
||||
);
|
||||
|
||||
await this.memberService.create({ roleId: role.id, userId: user.id });
|
||||
}
|
||||
|
||||
async signIn(user: UserDto): Promise<JwtDto> {
|
||||
const { email, id, department, name, type } = user;
|
||||
|
||||
const { allowDomains } = await this.tenantService.findOne();
|
||||
|
||||
if (email && allowDomains && allowDomains.length > 0) {
|
||||
const domain = email.substring(email.lastIndexOf('@') + 1);
|
||||
if (!allowDomains.includes(domain)) {
|
||||
throw new BadRequestException('Signed in with invalid domain.');
|
||||
}
|
||||
}
|
||||
|
||||
const { state } = await this.userService.findById(id);
|
||||
|
||||
if (state === UserStateEnum.Blocked) throw new UserBlockedException();
|
||||
const { accessTokenExpiredTime, refreshTokenExpiredTime } =
|
||||
this.configService.get('jwt', { infer: true }) ?? {};
|
||||
|
||||
return {
|
||||
accessToken: this.jwtService.sign(
|
||||
{ sub: id, email, department, name, type },
|
||||
{
|
||||
expiresIn: (accessTokenExpiredTime ?? '10m') as StringValue | number,
|
||||
},
|
||||
),
|
||||
refreshToken: this.jwtService.sign(
|
||||
{ sub: id, email },
|
||||
{
|
||||
expiresIn: (refreshTokenExpiredTime ?? '1h') as StringValue | number,
|
||||
},
|
||||
),
|
||||
};
|
||||
}
|
||||
|
||||
async refreshToken({ id }: { id: number }): Promise<JwtDto> {
|
||||
const user = await this.userService.findById(id);
|
||||
return this.signIn(UserDto.transform(user));
|
||||
}
|
||||
|
||||
async validateApiKey(value: string, projectId: number) {
|
||||
const apiKeys = await this.apiKeyService.findByProjectIdAndValue(
|
||||
projectId,
|
||||
value,
|
||||
);
|
||||
|
||||
if (apiKeys.length === 1) return true;
|
||||
return false;
|
||||
}
|
||||
|
||||
async getOAuthLoginURL(callback_url?: string) {
|
||||
const { useOAuth, oauthConfig } = await this.tenantService.findOne();
|
||||
|
||||
if (!useOAuth) {
|
||||
throw new BadRequestException('OAuth login is disabled.');
|
||||
}
|
||||
if (!oauthConfig) {
|
||||
throw new BadRequestException('OAuth Config is required.');
|
||||
}
|
||||
|
||||
const params = new URLSearchParams({
|
||||
redirect_uri: this.getRedirectURI(),
|
||||
client_id: oauthConfig.clientId,
|
||||
response_type: 'code',
|
||||
state: crypto.randomBytes(10).toString('hex'),
|
||||
scope: oauthConfig.scopeString,
|
||||
callback_url: encodeURIComponent(callback_url ?? ''),
|
||||
});
|
||||
|
||||
return `${oauthConfig.authCodeRequestURL}?${params.toString()}`;
|
||||
}
|
||||
|
||||
private async getAccessToken(code: string): Promise<string> {
|
||||
const { oauthConfig, useOAuth } = await this.tenantService.findOne();
|
||||
|
||||
if (!useOAuth) {
|
||||
throw new BadRequestException('OAuth login is disabled.');
|
||||
}
|
||||
if (!oauthConfig) {
|
||||
throw new BadRequestException('OAuth Config is required.');
|
||||
}
|
||||
|
||||
const { accessTokenRequestURL, clientId, clientSecret } = oauthConfig;
|
||||
return await lastValueFrom<string>(
|
||||
this.httpService
|
||||
.post<AccessTokenResponse>(
|
||||
accessTokenRequestURL,
|
||||
{
|
||||
grant_type: 'authorization_code',
|
||||
code,
|
||||
redirect_uri: this.getRedirectURI(),
|
||||
},
|
||||
{
|
||||
headers: {
|
||||
Authorization: `Basic ${Buffer.from(
|
||||
clientId + ':' + clientSecret,
|
||||
).toString('base64')}`,
|
||||
'Content-Type': 'application/x-www-form-urlencoded',
|
||||
},
|
||||
},
|
||||
)
|
||||
.pipe<string>(
|
||||
map<AxiosResponse<AccessTokenResponse, any>, string>(
|
||||
(res) => res.data.access_token,
|
||||
),
|
||||
)
|
||||
.pipe(
|
||||
catchError((error: Error) => {
|
||||
if (error instanceof AxiosError) {
|
||||
throw new InternalServerErrorException({
|
||||
axiosError: {
|
||||
...error.response?.data,
|
||||
status: error.response?.status,
|
||||
} as object,
|
||||
});
|
||||
}
|
||||
throw error;
|
||||
}),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
private async getEmailByAccessToken(accessToken: string): Promise<string> {
|
||||
const { oauthConfig } = await this.tenantService.findOne();
|
||||
|
||||
if (!oauthConfig) {
|
||||
throw new BadRequestException('OAuth Config is required.');
|
||||
}
|
||||
return await lastValueFrom<string>(
|
||||
this.httpService
|
||||
.get<UserProfileResponse>(oauthConfig.userProfileRequestURL, {
|
||||
headers: { Authorization: `Bearer ${accessToken}` },
|
||||
})
|
||||
.pipe(map((res) => res.data[oauthConfig.emailKey]))
|
||||
.pipe(
|
||||
catchError((error: Error) => {
|
||||
if (error instanceof AxiosError) {
|
||||
throw new InternalServerErrorException({
|
||||
axiosError: {
|
||||
...error.response?.data,
|
||||
status: error.response?.status,
|
||||
} as object,
|
||||
});
|
||||
}
|
||||
throw error;
|
||||
}),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
async signInByOAuth(code: string) {
|
||||
const accessToken = await this.getAccessToken(code);
|
||||
|
||||
const email = await this.getEmailByAccessToken(accessToken);
|
||||
|
||||
const user = await this.userService.findByEmailAndSignUpMethod(
|
||||
email,
|
||||
SignUpMethodEnum.OAUTH,
|
||||
);
|
||||
if (user) {
|
||||
return await this.signIn(user);
|
||||
} else {
|
||||
const user = await this.createUserService.createOAuthUser({ email });
|
||||
return await this.signIn(user);
|
||||
}
|
||||
}
|
||||
|
||||
private getRedirectURI() {
|
||||
const app = this.configService.get('app', { infer: true });
|
||||
|
||||
return `${app?.adminWebUrl}/auth/oauth-callback`;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
/**
|
||||
* Copyright 2025 LY Corporation
|
||||
*
|
||||
* LY Corporation licenses this file to you under the Apache License,
|
||||
* version 2.0 (the "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at:
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
* License for the specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
export { SendEmailCodeDto } from './send-email-code.dto';
|
||||
export { VerifyEmailCodeDto } from './verify-email-code.dto';
|
||||
export { ValidateEmailUserDto } from './validate-email-user.dto';
|
||||
export { SignUpInvitationUserDto } from './sign-up-invitation-user.dto';
|
||||
export { SignUpEmailUserDto } from './sign-up-email-user.dto';
|
||||
export { SignUpOauthUserDto } from './sign-up-oauth-user.dto';
|
||||
export { JwtDto } from './jwt.dto';
|
||||
@@ -0,0 +1,19 @@
|
||||
/**
|
||||
* Copyright 2025 LY Corporation
|
||||
*
|
||||
* LY Corporation licenses this file to you under the Apache License,
|
||||
* version 2.0 (the "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at:
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
* License for the specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
export class JwtDto {
|
||||
accessToken: string;
|
||||
refreshToken: string;
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
/**
|
||||
* Copyright 2025 LY Corporation
|
||||
*
|
||||
* LY Corporation licenses this file to you under the Apache License,
|
||||
* version 2.0 (the "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at:
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
* License for the specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
import { ApiProperty } from '@nestjs/swagger';
|
||||
import { IsEmail } from 'class-validator';
|
||||
|
||||
import { IsPassword } from '@/common/decorators/is-password';
|
||||
|
||||
export class EmailUserSignInRequestDto {
|
||||
@ApiProperty({ nullable: true, type: String })
|
||||
@IsEmail()
|
||||
email: string | null;
|
||||
|
||||
@ApiProperty()
|
||||
@IsPassword()
|
||||
password: string;
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
/**
|
||||
* Copyright 2025 LY Corporation
|
||||
*
|
||||
* LY Corporation licenses this file to you under the Apache License,
|
||||
* version 2.0 (the "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at:
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
* License for the specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
import { ApiProperty } from '@nestjs/swagger';
|
||||
import { IsEmail } from 'class-validator';
|
||||
|
||||
import { IsPassword } from '@/common/decorators/is-password';
|
||||
|
||||
export class EmailUserSignUpRequestDto {
|
||||
@ApiProperty()
|
||||
@IsEmail()
|
||||
email: string;
|
||||
|
||||
@ApiProperty()
|
||||
@IsPassword()
|
||||
password: string;
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
/**
|
||||
* Copyright 2025 LY Corporation
|
||||
*
|
||||
* LY Corporation licenses this file to you under the Apache License,
|
||||
* version 2.0 (the "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at:
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
* License for the specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
import { ApiProperty } from '@nestjs/swagger';
|
||||
import { IsEmail, IsString } from 'class-validator';
|
||||
|
||||
export class EmailVerificationCodeRequestDto {
|
||||
@ApiProperty()
|
||||
@IsEmail()
|
||||
email: string;
|
||||
|
||||
@ApiProperty()
|
||||
@IsString()
|
||||
code: string;
|
||||
}
|
||||
+23
@@ -0,0 +1,23 @@
|
||||
/**
|
||||
* Copyright 2025 LY Corporation
|
||||
*
|
||||
* LY Corporation licenses this file to you under the Apache License,
|
||||
* version 2.0 (the "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at:
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
* License for the specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
import { ApiProperty } from '@nestjs/swagger';
|
||||
import { IsEmail } from 'class-validator';
|
||||
|
||||
export class EmailVerificationMailingRequestDto {
|
||||
@ApiProperty()
|
||||
@IsEmail()
|
||||
email: string;
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
/**
|
||||
* Copyright 2025 LY Corporation
|
||||
*
|
||||
* LY Corporation licenses this file to you under the Apache License,
|
||||
* version 2.0 (the "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at:
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
* License for the specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
export { EmailUserSignInRequestDto } from './email-user-sign-in-request.dto';
|
||||
export { EmailUserSignUpRequestDto } from './email-user-sign-up-request.dto';
|
||||
export { EmailVerificationMailingRequestDto } from './email-verification-mailing-request.dto';
|
||||
export { EmailVerificationCodeRequestDto } from './email-verification-code-request.dto';
|
||||
export { InvitationUserSignUpRequestDto } from './invitation-user-sign-up-request.dto';
|
||||
export { OAuthUserSignUpRequestDto } from './oauth-user-sign-up-request.dto';
|
||||
@@ -0,0 +1,33 @@
|
||||
/**
|
||||
* Copyright 2025 LY Corporation
|
||||
*
|
||||
* LY Corporation licenses this file to you under the Apache License,
|
||||
* version 2.0 (the "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at:
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
* License for the specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
import { ApiProperty } from '@nestjs/swagger';
|
||||
import { IsEmail, IsString } from 'class-validator';
|
||||
|
||||
import { IsPassword } from '@/common/decorators/is-password';
|
||||
|
||||
export class InvitationUserSignUpRequestDto {
|
||||
@ApiProperty()
|
||||
@IsPassword()
|
||||
password: string;
|
||||
|
||||
@ApiProperty()
|
||||
@IsString()
|
||||
code: string;
|
||||
|
||||
@ApiProperty()
|
||||
@IsEmail()
|
||||
email: string;
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
/**
|
||||
* Copyright 2025 LY Corporation
|
||||
*
|
||||
* LY Corporation licenses this file to you under the Apache License,
|
||||
* version 2.0 (the "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at:
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
* License for the specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
import { ApiProperty } from '@nestjs/swagger';
|
||||
import { IsEmail, IsOptional, IsString } from 'class-validator';
|
||||
|
||||
export class OAuthUserSignUpRequestDto {
|
||||
@ApiProperty()
|
||||
@IsEmail()
|
||||
email: string;
|
||||
|
||||
@ApiProperty()
|
||||
@IsString()
|
||||
@IsOptional()
|
||||
projectName: string;
|
||||
|
||||
@ApiProperty()
|
||||
@IsString()
|
||||
@IsOptional()
|
||||
roleName: string;
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
/**
|
||||
* Copyright 2025 LY Corporation
|
||||
*
|
||||
* LY Corporation licenses this file to you under the Apache License,
|
||||
* version 2.0 (the "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at:
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
* License for the specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
export { SendEmailCodeResponseDto } from './send-email-code-response.dto';
|
||||
export { SignInResponseDto } from './sign-in-response.dto';
|
||||
export { OAuthLoginUrlResponseDto } from './oauth-login-url.response.dto';
|
||||
@@ -0,0 +1,21 @@
|
||||
/**
|
||||
* Copyright 2025 LY Corporation
|
||||
*
|
||||
* LY Corporation licenses this file to you under the Apache License,
|
||||
* version 2.0 (the "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at:
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
* License for the specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
import { ApiProperty } from '@nestjs/swagger';
|
||||
|
||||
export class OAuthLoginUrlResponseDto {
|
||||
@ApiProperty()
|
||||
url: string;
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
/**
|
||||
* Copyright 2025 LY Corporation
|
||||
*
|
||||
* LY Corporation licenses this file to you under the Apache License,
|
||||
* version 2.0 (the "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at:
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
* License for the specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
import { ApiProperty } from '@nestjs/swagger';
|
||||
import { Expose, plainToInstance } from 'class-transformer';
|
||||
|
||||
export class SendEmailCodeResponseDto {
|
||||
@Expose()
|
||||
@ApiProperty()
|
||||
expiredAt: string;
|
||||
|
||||
public static transform(params: any): SendEmailCodeResponseDto {
|
||||
return plainToInstance(SendEmailCodeResponseDto, params, {
|
||||
excludeExtraneousValues: true,
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
/**
|
||||
* Copyright 2025 LY Corporation
|
||||
*
|
||||
* LY Corporation licenses this file to you under the Apache License,
|
||||
* version 2.0 (the "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at:
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
* License for the specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
import { ApiProperty } from '@nestjs/swagger';
|
||||
|
||||
export class SignInResponseDto {
|
||||
@ApiProperty()
|
||||
accessToken: string;
|
||||
|
||||
@ApiProperty()
|
||||
refreshToken?: string;
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
/**
|
||||
* Copyright 2025 LY Corporation
|
||||
*
|
||||
* LY Corporation licenses this file to you under the Apache License,
|
||||
* version 2.0 (the "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at:
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
* License for the specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
export class SendEmailCodeDto {
|
||||
email: string;
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
/**
|
||||
* Copyright 2025 LY Corporation
|
||||
*
|
||||
* LY Corporation licenses this file to you under the Apache License,
|
||||
* version 2.0 (the "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at:
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
* License for the specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
export class SignUpEmailUserDto {
|
||||
email: string;
|
||||
password: string;
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
/**
|
||||
* Copyright 2025 LY Corporation
|
||||
*
|
||||
* LY Corporation licenses this file to you under the Apache License,
|
||||
* version 2.0 (the "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at:
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
* License for the specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
|
||||
export class SignUpInvitationUserDto {
|
||||
password: string;
|
||||
code: string;
|
||||
email: string;
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
/**
|
||||
* Copyright 2025 LY Corporation
|
||||
*
|
||||
* LY Corporation licenses this file to you under the Apache License,
|
||||
* version 2.0 (the "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at:
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
* License for the specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
export class SignUpOauthUserDto {
|
||||
email: string;
|
||||
projectName: string;
|
||||
roleName: string;
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
/**
|
||||
* Copyright 2025 LY Corporation
|
||||
*
|
||||
* LY Corporation licenses this file to you under the Apache License,
|
||||
* version 2.0 (the "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at:
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
* License for the specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
export class ValidateEmailUserDto {
|
||||
email: string;
|
||||
password: string;
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
/**
|
||||
* Copyright 2025 LY Corporation
|
||||
*
|
||||
* LY Corporation licenses this file to you under the Apache License,
|
||||
* version 2.0 (the "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at:
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
* License for the specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
export class VerifyEmailCodeDto {
|
||||
code: string;
|
||||
email: string;
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
/**
|
||||
* Copyright 2025 LY Corporation
|
||||
*
|
||||
* LY Corporation licenses this file to you under the Apache License,
|
||||
* version 2.0 (the "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at:
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
* License for the specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
export { PasswordNotMatchException } from './password-not.match.exception';
|
||||
export { UserBlockedException } from './user-blocked.exception';
|
||||
@@ -0,0 +1,27 @@
|
||||
/**
|
||||
* Copyright 2025 LY Corporation
|
||||
*
|
||||
* LY Corporation licenses this file to you under the Apache License,
|
||||
* version 2.0 (the "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at:
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
* License for the specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
import { NotFoundException } from '@nestjs/common';
|
||||
|
||||
import { ErrorCode } from '@ufb/shared';
|
||||
|
||||
export class PasswordNotMatchException extends NotFoundException {
|
||||
constructor() {
|
||||
super({
|
||||
code: ErrorCode.Auth.PasswordNotMatch,
|
||||
message: 'Password is not matched',
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
/**
|
||||
* Copyright 2025 LY Corporation
|
||||
*
|
||||
* LY Corporation licenses this file to you under the Apache License,
|
||||
* version 2.0 (the "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at:
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
* License for the specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
import { UnauthorizedException } from '@nestjs/common';
|
||||
|
||||
import { ErrorCode } from '@ufb/shared';
|
||||
|
||||
export class UserBlockedException extends UnauthorizedException {
|
||||
constructor() {
|
||||
super({
|
||||
code: ErrorCode.Auth.BlockedUser,
|
||||
message: 'This user is blocked',
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
/**
|
||||
* Copyright 2025 LY Corporation
|
||||
*
|
||||
* LY Corporation licenses this file to you under the Apache License,
|
||||
* version 2.0 (the "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at:
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
* License for the specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
import type { CanActivate, ExecutionContext } from '@nestjs/common';
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { Request } from 'express';
|
||||
|
||||
import { AuthService } from '../auth.service';
|
||||
|
||||
@Injectable()
|
||||
export class ApiKeyAuthGuard implements CanActivate {
|
||||
constructor(private readonly authService: AuthService) {}
|
||||
|
||||
canActivate(context: ExecutionContext) {
|
||||
const request = context.switchToHttp().getRequest<Request>();
|
||||
const apiKey = request.headers['x-api-key'] as string | undefined;
|
||||
if (!apiKey) return false;
|
||||
if (apiKey === process.env.MASTER_API_KEY) return true;
|
||||
const projectId = parseInt(request.params.projectId);
|
||||
|
||||
return this.authService.validateApiKey(apiKey, projectId);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
/**
|
||||
* Copyright 2025 LY Corporation
|
||||
*
|
||||
* LY Corporation licenses this file to you under the Apache License,
|
||||
* version 2.0 (the "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at:
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
* License for the specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
export { JwtAuthGuard } from './jwt-auth.guard';
|
||||
export { ApiKeyAuthGuard } from './api-key.guard';
|
||||
@@ -0,0 +1,46 @@
|
||||
/**
|
||||
* Copyright 2025 LY Corporation
|
||||
*
|
||||
* LY Corporation licenses this file to you under the Apache License,
|
||||
* version 2.0 (the "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at:
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
* License for the specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
import type { ExecutionContext } from '@nestjs/common';
|
||||
import { Injectable, UnauthorizedException } from '@nestjs/common';
|
||||
import { AuthGuard } from '@nestjs/passport';
|
||||
import { ClsService } from 'nestjs-cls';
|
||||
import type { Observable } from 'rxjs';
|
||||
|
||||
import type { ClsServiceType } from '@/types/cls-service.type';
|
||||
|
||||
interface User {
|
||||
id: number;
|
||||
}
|
||||
@Injectable()
|
||||
export class JwtAuthGuard extends AuthGuard('jwt') {
|
||||
constructor(private readonly clsService: ClsService<ClsServiceType>) {
|
||||
super();
|
||||
}
|
||||
canActivate(
|
||||
context: ExecutionContext,
|
||||
): boolean | Promise<boolean> | Observable<boolean> {
|
||||
return super.canActivate(context);
|
||||
}
|
||||
handleRequest(err: any, user: any): any {
|
||||
// You can throw an exception based on either "info" or "err" arguments
|
||||
if (err || !user) {
|
||||
throw err ?? new UnauthorizedException('Invalid jwt');
|
||||
}
|
||||
|
||||
this.clsService.set('userId', (user as User).id);
|
||||
return user;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
/**
|
||||
* Copyright 2025 LY Corporation
|
||||
*
|
||||
* LY Corporation licenses this file to you under the Apache License,
|
||||
* version 2.0 (the "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at:
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
* License for the specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
import type { CanActivate } from '@nestjs/common';
|
||||
import { Injectable } from '@nestjs/common';
|
||||
|
||||
import { TenantService } from '@/domains/admin/tenant/tenant.service';
|
||||
|
||||
@Injectable()
|
||||
export class UseEmailGuard implements CanActivate {
|
||||
constructor(private readonly tenantService: TenantService) {}
|
||||
async canActivate(): Promise<boolean> {
|
||||
const { useEmail } = await this.tenantService.findOne();
|
||||
return useEmail;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
/**
|
||||
* Copyright 2025 LY Corporation
|
||||
*
|
||||
* LY Corporation licenses this file to you under the Apache License,
|
||||
* version 2.0 (the "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at:
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
* License for the specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
import type { CanActivate } from '@nestjs/common';
|
||||
import { Injectable } from '@nestjs/common';
|
||||
|
||||
import { TenantService } from '@/domains/admin/tenant/tenant.service';
|
||||
|
||||
@Injectable()
|
||||
export class UseOAuthGuard implements CanActivate {
|
||||
constructor(private readonly tenantService: TenantService) {}
|
||||
async canActivate(): Promise<boolean> {
|
||||
const { useOAuth } = await this.tenantService.findOne();
|
||||
return useOAuth;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
/**
|
||||
* Copyright 2025 LY Corporation
|
||||
*
|
||||
* LY Corporation licenses this file to you under the Apache License,
|
||||
* version 2.0 (the "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at:
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
* License for the specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { ConfigService } from '@nestjs/config';
|
||||
import { PassportStrategy } from '@nestjs/passport';
|
||||
import { ExtractJwt, JwtFromRequestFunction, Strategy } from 'passport-jwt';
|
||||
|
||||
import type { UserTypeEnum } from '@/domains/admin/user/entities/enums';
|
||||
import type { ConfigServiceType } from '@/types/config-service.type';
|
||||
|
||||
interface IPayload {
|
||||
sub: string;
|
||||
email: string;
|
||||
type: UserTypeEnum;
|
||||
iat: number;
|
||||
exp: number;
|
||||
}
|
||||
|
||||
interface StrategyOptions {
|
||||
jwtFromRequest: JwtFromRequestFunction;
|
||||
ignoreExpiration: boolean;
|
||||
secretOrKey: string;
|
||||
}
|
||||
|
||||
@Injectable()
|
||||
export class JwtStrategy extends PassportStrategy(Strategy) {
|
||||
constructor(configService: ConfigService<ConfigServiceType>) {
|
||||
const { secret } = configService.get('jwt', { infer: true }) ?? {};
|
||||
|
||||
super({
|
||||
jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
|
||||
ignoreExpiration: false,
|
||||
secretOrKey: secret,
|
||||
} as StrategyOptions);
|
||||
}
|
||||
|
||||
validate(payload: IPayload) {
|
||||
const { email, sub, type } = payload;
|
||||
return { id: sub, email, type };
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
/**
|
||||
* Copyright 2025 LY Corporation
|
||||
*
|
||||
* LY Corporation licenses this file to you under the Apache License,
|
||||
* version 2.0 (the "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at:
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
* License for the specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { PassportStrategy } from '@nestjs/passport';
|
||||
import { Strategy } from 'passport-local';
|
||||
|
||||
import { UserDto } from '@/domains/admin/user/dtos';
|
||||
import { AuthService } from '../auth.service';
|
||||
|
||||
@Injectable()
|
||||
export class LocalStrategy extends PassportStrategy(Strategy) {
|
||||
constructor(private authService: AuthService) {
|
||||
super({ usernameField: 'email' });
|
||||
}
|
||||
|
||||
async validate(email: string, password: string) {
|
||||
const user = await this.authService.validateEmailUser({ email, password });
|
||||
return UserDto.transform(user);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user