first commit
CI / typecheck (push) Successful in 1m8s
CI / format (push) Failing after 1m6s
CI / lint (push) Failing after 49s
CI / test (push) Failing after 1m8s

This commit is contained in:
SDI
2026-07-15 18:05:12 +09:00
commit 12e4f17b62
4633 changed files with 817125 additions and 0 deletions
@@ -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;
}
@@ -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);
}
}