Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
68 commits
Select commit Hold shift + click to select a range
1602386
feat: organization 모듈 생성
liveforpresent Nov 1, 2025
a8eb5a9
chore: 패키지 설치
liveforpresent Nov 1, 2025
4df3532
chore: auth 디렉토리 구조 변경
liveforpresent Nov 1, 2025
4f588b8
chore: 대이동
liveforpresent Nov 2, 2025
6300d03
fix: 카카오 연동 해제 로직 내에서 repository 사용하지 않도록 수정
liveforpresent Nov 2, 2025
13db423
feat: auth repository에 deleteById 구현
liveforpresent Nov 2, 2025
36d7ca8
feat: auth 삭제 로직 및 이벤트 구현
liveforpresent Nov 2, 2025
10dafa0
feat: 일반 유저 oauth 로그인 연동 해제 usecase 구현
liveforpresent Nov 2, 2025
a0e2648
fix: 일반 유저 정보 삭제 시 oAuth 로직을 직접 사용하지 않도록 수정
liveforpresent Nov 2, 2025
bddb9f3
fix: 일반 유저 정보 삭제를 위해 인증 정보 삭제 이벤트 listener 구현
liveforpresent Nov 2, 2025
da932f1
feat: 회원탈퇴 api auth로 이동 및 쿠키 삭제 로직 추가
liveforpresent Nov 2, 2025
5b638ce
docs: 회원탈퇴 api swagger 문서화
liveforpresent Nov 2, 2025
7afb996
chore: auth organization 에러 코드 정리
liveforpresent Nov 3, 2025
baf2d7e
feat: organization 모듈 생성
liveforpresent Nov 1, 2025
eb31f3d
chore: 패키지 설치
liveforpresent Nov 1, 2025
bd33c14
chore: auth 디렉토리 구조 변경
liveforpresent Nov 1, 2025
2cc15a3
chore: 대이동
liveforpresent Nov 2, 2025
28f1a04
fix: 카카오 연동 해제 로직 내에서 repository 사용하지 않도록 수정
liveforpresent Nov 2, 2025
f1fa22a
feat: auth repository에 deleteById 구현
liveforpresent Nov 2, 2025
0a4599b
feat: auth 삭제 로직 및 이벤트 구현
liveforpresent Nov 2, 2025
7846fea
feat: 일반 유저 oauth 로그인 연동 해제 usecase 구현
liveforpresent Nov 2, 2025
577651c
fix: 일반 유저 정보 삭제 시 oAuth 로직을 직접 사용하지 않도록 수정
liveforpresent Nov 2, 2025
e071d5f
fix: 일반 유저 정보 삭제를 위해 인증 정보 삭제 이벤트 listener 구현
liveforpresent Nov 2, 2025
d2738c3
feat: 회원탈퇴 api auth로 이동 및 쿠키 삭제 로직 추가
liveforpresent Nov 2, 2025
f84a0af
docs: 회원탈퇴 api swagger 문서화
liveforpresent Nov 2, 2025
bd9f274
chore: auth organization 에러 코드 정리
liveforpresent Nov 3, 2025
06bb689
Merge branch 'feature/organization' of https://github.com/DevKor-gith…
liveforpresent Nov 4, 2025
5671a4d
feat: AccountId 구현
liveforpresent Nov 4, 2025
8c1cf83
feat: RawPassword 구현
liveforpresent Nov 4, 2025
6c7a53e
feat: PasswordHash 구현
liveforpresent Nov 4, 2025
ab31981
feat: AuthOrganization 구현
liveforpresent Nov 4, 2025
e521d3c
feat: 비밀번호 hash 관련 기능 구현
liveforpresent Nov 4, 2025
85f985c
feat: AuthOrganization repository 구현
liveforpresent Nov 4, 2025
5da2dd6
feat: AuthOrganization orm entity 구현
liveforpresent Nov 4, 2025
47a6076
feat: AuthOrganiztion 도메인과 인프라 간 mapper 구현
liveforpresent Nov 4, 2025
032f116
feat: AuthOrganization 모듈 구현
liveforpresent Nov 4, 2025
2f5cbeb
feat: Organization 도메인 객체 구현
liveforpresent Nov 4, 2025
a7e7295
feat: Organization orm entity 구현
liveforpresent Nov 4, 2025
c8e6f93
feat: Organization 도메인 <-> 인프라 간 mapper 구현
liveforpresent Nov 4, 2025
d1d584c
feat: Organization repository 구현
liveforpresent Nov 4, 2025
35969f1
feat: organization model 구현
liveforpresent Nov 5, 2025
22959ca
feat: organization view entity 구현
liveforpresent Nov 5, 2025
5999e32
feat: organization repository 구현
liveforpresent Nov 5, 2025
606b4b3
feat: organization 생성 로직 구현
liveforpresent Nov 6, 2025
ffa33f9
feat: orgnization 삭제 로직 구현
liveforpresent Nov 6, 2025
b5e8ec4
feat: organization repository 추가 구현
liveforpresent Nov 6, 2025
591799d
feat: update organization 로직 구현
liveforpresent Nov 9, 2025
f68db46
feat: organization 인증 생성 로직 구현
liveforpresent Nov 9, 2025
ed84cd4
feat: 회원가입 api 구현
liveforpresent Nov 9, 2025
82aad0a
feat: organization 로그아웃 구현
liveforpresent Nov 9, 2025
1356c70
feat: organization 로그인 로직 구현
liveforpresent Nov 9, 2025
64b5668
feat: organization 토큰 갱신 로직 구현
liveforpresent Nov 9, 2025
9ad9f63
feat: organization 로그인, 토큰 갱신 api 구현
liveforpresent Nov 9, 2025
8943e31
feat: auth-organization 예외 코드 정리
liveforpresent Nov 9, 2025
42bdb51
docs: auth-organization swagger 문서 작성
liveforpresent Nov 9, 2025
e829001
docs: organization swagger 문서 작성
liveforpresent Nov 9, 2025
d6e11f8
fix: 아이디/비밀번호 첫 번째와 마지막에 공백문자 무효화
liveforpresent Nov 9, 2025
25b3b53
feat: repository에 아이디 존재 여부 확인 구현
liveforpresent Nov 9, 2025
b33a133
feat: id 중복 체크 로직 구현
liveforpresent Nov 9, 2025
897536a
feat: id 중복 체크 api 구현
liveforpresent Nov 9, 2025
287c80a
docs: 아이디 중복 체크 api swagger 문서 작성
liveforpresent Nov 9, 2025
66b681c
fix: 기관 토큰 쿠키명 수정
liveforpresent Nov 10, 2025
66c5550
fix: typo 수정
liveforpresent Nov 10, 2025
2700c3f
fix: 유저 삭제 기능 수정
liveforpresent Nov 10, 2025
3827dae
feat: 유저 삭제 시 존재 여부 확인 기능 구현
liveforpresent Nov 10, 2025
0a4f6d4
fix: 회원 탈퇴 시 쿠키 삭제
liveforpresent Nov 10, 2025
7da6407
fix: auth entity 수정
liveforpresent Nov 10, 2025
a11db7f
docs: swagger 문서 수정
liveforpresent Nov 13, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@
"@sapphire/snowflake": "^3.5.5",
"@types/passport-jwt": "^4.0.1",
"axios": "^1.11.0",
"bcrypt": "^6.0.0",
"class-transformer": "^0.5.1",
"class-validator": "^0.14.2",
"cookie-parser": "^1.4.7",
Expand All @@ -60,6 +61,7 @@
"@nestjs/testing": "^11.0.1",
"@swc/cli": "^0.7.7",
"@swc/core": "^1.10.7",
"@types/bcrypt": "^6.0.0",
"@types/cookie-parser": "^1.4.8",
"@types/express": "^5.0.0",
"@types/jest": "^29.5.14",
Expand Down
4 changes: 2 additions & 2 deletions src/analytics/application/auth-event.handler.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { EventsHandler, IEventHandler } from '@nestjs/cqrs';
import { AuthCreatedEvent } from 'src/auth/domain/event/auth-created.event';
import { AuthCreatedEvent } from 'src/auth/auth-user/domain/event/auth-created.event';
import { AnalyticsService } from '../infrastructure/analytics.service';
import { LoginSucceededEvent } from 'src/auth/domain/event/login-succeeded.event';
import { LoginSucceededEvent } from 'src/auth/auth-user/domain/event/login-succeeded.event';

@EventsHandler(AuthCreatedEvent)
export class AuthCreatedEventHandler implements IEventHandler<AuthCreatedEvent> {
Expand Down
2 changes: 2 additions & 0 deletions src/app.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import { ScrapModule } from './scrap/scrap.module';
import mikroOrmConfig from './shared/config/mikro-orm.config';
import config from 'src/shared/config/configuration';
import { AnalyticsModule } from './analytics/analytics.module';
import { OrganizationModule } from './organization/organization.module';

@Module({
imports: [
Expand All @@ -29,6 +30,7 @@ import { AnalyticsModule } from './analytics/analytics.module';
TagModule,
MediaModule,
ScrapModule,
OrganizationModule,
],
controllers: [AppController],
providers: [AppService],
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import { ICommand } from '@nestjs/cqrs';

export class CheckAccountIdCommand implements ICommand {
constructor(public readonly accountId: string) {}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import { Inject, Injectable } from '@nestjs/common';
import { AUTH_ORGANIZATION_STORE, AuthOrganizationStore } from '../../domain/auth-organization.store';
import { CommandHandler } from '@nestjs/cqrs';
import { CheckAccountIdCommand } from './check-account-id.command';

@Injectable()
@CommandHandler(CheckAccountIdCommand)
export class CheckAccountIdUseCase {
constructor(
@Inject(AUTH_ORGANIZATION_STORE)
private readonly authOrganizationStore: AuthOrganizationStore,
) {}

async execute(command: CheckAccountIdCommand): Promise<boolean> {
const { accountId } = command;
const exists = await this.authOrganizationStore.existsByAccountId(accountId.trim());
return exists;
}
}
10 changes: 10 additions & 0 deletions src/auth/auth-organization/application/create/create.command.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import { ICommand } from '@nestjs/cqrs';

export class CreateAuthOrganizationCommand implements ICommand {
constructor(
public readonly accountId: string,
public readonly password: string,
public readonly name: string,
public readonly contact: string,
) {}
}
50 changes: 50 additions & 0 deletions src/auth/auth-organization/application/create/create.use-case.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import { Inject, Injectable } from '@nestjs/common';
import { CreateAuthOrganizationCommand } from './create.command';
import { AUTH_ORGANIZATION_STORE, AuthOrganizationStore } from '../../domain/auth-organization.store';
import { CustomException } from 'src/shared/exception/custom-exception';
import { CustomExceptionCode } from 'src/shared/exception/custom-exception-code';
import { Identifier } from 'src/shared/core/domain/identifier';
import { AuthOrganization } from '../../domain/auth-organization';
import { AccountId } from '../../domain/vo/account-id';
import { PasswordHash } from '../../domain/vo/password-hash';
import { EventBus } from '@nestjs/cqrs';
import { AuthOrganizationCreatedEvent } from '../../domain/event/auth-organization-created.event';
import { PASSWORD_HASHER, PasswordHasher } from '../../domain/password-hasher';
import { RawPassword } from '../../domain/vo/raw-password';

@Injectable()
export class CreateAuthOrganizationUseCase {
constructor(
@Inject(AUTH_ORGANIZATION_STORE)
private readonly authOrganizationStore: AuthOrganizationStore,
@Inject(PASSWORD_HASHER)
private readonly passwordHasher: PasswordHasher,
private readonly eventBus: EventBus,
) {}

async execute(command: CreateAuthOrganizationCommand) {
const existingAuth = await this.authOrganizationStore.loadByAccountId(command.accountId.trim());
if (existingAuth) throw new CustomException(CustomExceptionCode.AUTH_ORGANIZATION_ACCOUNT_ID_ALREADY_EXISTS);

const rawPassword = RawPassword.create(command.password.trim());
const hashedPassword = await this.passwordHasher.hash(rawPassword.value);

const authOrganization = AuthOrganization.create({
id: Identifier.create(),
accountId: AccountId.create(command.accountId.trim()),
passwordHash: PasswordHash.create(hashedPassword),
refreshToken: null,
organizationId: Identifier.create(),
createdAt: new Date(),
updatedAt: new Date(),
isDeleted: false,
deletedAt: null,
});

await this.authOrganizationStore.save(authOrganization);

await this.eventBus.publish(
new AuthOrganizationCreatedEvent(authOrganization.organizationId.value, command.name, command.contact),
);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import { IsNotEmpty, IsString } from 'class-validator';

export class LoginResponseDto {
@IsString()
@IsNotEmpty()
accessToken: string;

@IsString()
@IsNotEmpty()
refreshToken: string;
}
8 changes: 8 additions & 0 deletions src/auth/auth-organization/application/login/login.command.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import { ICommand } from '@nestjs/cqrs';

export class LoginCommand implements ICommand {
constructor(
public readonly accountId: string,
public readonly password: string,
) {}
}
57 changes: 57 additions & 0 deletions src/auth/auth-organization/application/login/login.use-case.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
import { Inject, Injectable } from '@nestjs/common';
import { CommandHandler } from '@nestjs/cqrs';
import { LoginCommand } from './login.command';
import { AUTH_ORGANIZATION_STORE, AuthOrganizationStore } from '../../domain/auth-organization.store';
import { PASSWORD_HASHER, PasswordHasher } from '../../domain/password-hasher';
import { CustomException } from 'src/shared/exception/custom-exception';
import { CustomExceptionCode } from 'src/shared/exception/custom-exception-code';
import { JwtProvider } from 'src/auth/core/infrastructure/jwt/jwt.provider';
import { TokenType } from 'src/auth/core/infrastructure/jwt/jwt.factory';
import { LoginResponseDto } from './dto/login.response.dto';
import { AuthOrganization } from '../../domain/auth-organization';

@Injectable()
@CommandHandler(LoginCommand)
export class LoginUseCase {
constructor(
@Inject(AUTH_ORGANIZATION_STORE)
private readonly authOrganizationStore: AuthOrganizationStore,
@Inject(PASSWORD_HASHER)
private readonly passwordHasher: PasswordHasher,
private readonly jwtProvider: JwtProvider,
) {}

async execute(command: LoginCommand): Promise<LoginResponseDto> {
const { accountId, password } = command;

const authOrganization = await this.validateAccount(accountId.trim(), password.trim());
const { accessToken, refreshToken, jti } = await this.generateTokens(authOrganization.organizationId.value);
await this.saveRefreshToken(authOrganization, jti);

return { accessToken, refreshToken };
}

private async validateAccount(accountId: string, password: string): Promise<AuthOrganization> {
const authOrganization = await this.authOrganizationStore.loadByAccountId(accountId);
if (!authOrganization) throw new CustomException(CustomExceptionCode.AUTH_ORGANIZATION_NOT_FOUND);

const isPasswordValid = await this.passwordHasher.compare(password, authOrganization.passwordHash.value);
if (!isPasswordValid) throw new CustomException(CustomExceptionCode.AUTH_ORGANIZATION_INVALID_PASSWORD);

return authOrganization;
}

private async generateTokens(
organizationId: string,
): Promise<{ accessToken: string; refreshToken: string; jti: string }> {
const { token: accessToken } = await this.jwtProvider.generateToken(TokenType.ACCESS, organizationId);
const { token: refreshToken, jti } = await this.jwtProvider.generateToken(TokenType.REFRESH, organizationId);

return { accessToken, refreshToken, jti };
}

private async saveRefreshToken(authOrganization: AuthOrganization, jti: string): Promise<void> {
authOrganization.updateRefreshToken(jti);
await this.authOrganizationStore.update(authOrganization);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import { ICommand } from '@nestjs/cqrs';

export class LogoutCommand implements ICommand {
constructor(public readonly organizationId: string) {}
}
25 changes: 25 additions & 0 deletions src/auth/auth-organization/application/logout/logout.use-case.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import { Inject, Injectable } from '@nestjs/common';
import { CommandHandler } from '@nestjs/cqrs';
import { LogoutCommand } from './logout.command';
import { AUTH_ORGANIZATION_STORE, AuthOrganizationStore } from '../../domain/auth-organization.store';
import { CustomException } from 'src/shared/exception/custom-exception';
import { CustomExceptionCode } from 'src/shared/exception/custom-exception-code';

@Injectable()
@CommandHandler(LogoutCommand)
export class LogoutUseCase {
constructor(
@Inject(AUTH_ORGANIZATION_STORE)
private readonly authOrganizationStore: AuthOrganizationStore,
) {}

async execute(command: LogoutCommand): Promise<void> {
const { organizationId } = command;

const authOrganization = await this.authOrganizationStore.loadByOrganizationId(organizationId);
if (!authOrganization) throw new CustomException(CustomExceptionCode.AUTH_ORGANIZATION_INVALID_ACCESS_TOKEN);

authOrganization.updateRefreshToken(null);
await this.authOrganizationStore.update(authOrganization);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import { ICommand } from '@nestjs/cqrs';

export class RenewTokenCommand implements ICommand {
constructor(
public readonly organizationId: string,
public readonly jti: string,
) {}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
import { Inject, Injectable } from '@nestjs/common';
import { JwtProvider } from 'src/auth/core/infrastructure/jwt/jwt.provider';
import { AUTH_ORGANIZATION_STORE, AuthOrganizationStore } from '../../domain/auth-organization.store';
import { CommandHandler, ICommandHandler } from '@nestjs/cqrs';
import { RenewTokenCommand } from './renew-token.command';
import { TokenType } from 'src/auth/core/infrastructure/jwt/jwt.factory';
import { CustomException } from 'src/shared/exception/custom-exception';
import { CustomExceptionCode } from 'src/shared/exception/custom-exception-code';
import { RenewTokenResponseDto } from './dto/renew-token.response.dto';
import { AuthOrganization } from '../../domain/auth-organization';

@Injectable()
@CommandHandler(RenewTokenCommand)
export class RenewTokenUseCase implements ICommandHandler<RenewTokenCommand> {
constructor(
@Inject(AUTH_ORGANIZATION_STORE)
private readonly authOrganizationStore: AuthOrganizationStore,
private readonly jwtProvider: JwtProvider,
) {}

async execute(command: RenewTokenCommand): Promise<RenewTokenResponseDto> {
const { organizationId, jti } = command;

const authOrganization = await this.validateRefreshToken(organizationId, jti);
const { accessToken, refreshToken, jti: newJti } = await this.generateTokens(organizationId);
await this.saveRefreshToken(authOrganization, newJti);

return { accessToken, refreshToken };
}

private async validateRefreshToken(organizationId: string, jti: string): Promise<AuthOrganization> {
const authOrganization = await this.authOrganizationStore.loadByRefreshToken(jti);
if (!authOrganization || authOrganization.organizationId.value !== organizationId) {
throw new CustomException(CustomExceptionCode.AUTH_ORGANIZATION_INVALID_REFRESH_TOKEN);
}

return authOrganization;
}

private async generateTokens(
organizationId: string,
): Promise<{ accessToken: string; refreshToken: string; jti: string }> {
const { token: accessToken } = await this.jwtProvider.generateToken(TokenType.ACCESS, organizationId);
const { token: refreshToken, jti } = await this.jwtProvider.generateToken(TokenType.REFRESH, organizationId);

return { accessToken, refreshToken, jti };
}

private async saveRefreshToken(authOrganization: AuthOrganization, jti: string): Promise<void> {
authOrganization.updateRefreshToken(jti);
await this.authOrganizationStore.update(authOrganization);
}
}
36 changes: 36 additions & 0 deletions src/auth/auth-organization/auth-organization.module.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import { MikroOrmModule } from '@mikro-orm/nestjs';
import { Module } from '@nestjs/common';
import { SharedModule } from 'src/shared/shared.module';
import { AuthOrganizationController } from './presentation/auth-organization.controller';
import { AUTH_ORGANIZATION_STORE } from './domain/auth-organization.store';
import { AuthOrganizationStoreImpl } from './infrastructure/auth-organization.store.impl';
import { AuthCoreModule } from '../core/auth-core.module';
import { AuthOrganizationEntity } from './infrastructure/auth-organization.entity';
import { CreateAuthOrganizationUseCase } from './application/create/create.use-case';
import { CqrsModule } from '@nestjs/cqrs';
import { PASSWORD_HASHER } from './domain/password-hasher';
import { PasswordHasherImpl } from './infrastructure/password-hasher.impl';
import { RenewTokenUseCase } from './application/renew-token/renew-token.use-case';
import { LogoutUseCase } from './application/logout/logout.use-case';
import { LoginUseCase } from './application/login/login.use-case';
import { CheckAccountIdUseCase } from './application/check-account-id/check-account-id.use-case';

const usecases = [CreateAuthOrganizationUseCase, RenewTokenUseCase, LoginUseCase, LogoutUseCase, CheckAccountIdUseCase];

@Module({
imports: [SharedModule, MikroOrmModule.forFeature([AuthOrganizationEntity]), AuthCoreModule, CqrsModule],
providers: [
{
provide: AUTH_ORGANIZATION_STORE,
useClass: AuthOrganizationStoreImpl,
},
{
provide: PASSWORD_HASHER,
useClass: PasswordHasherImpl,
},
...usecases,
],
controllers: [AuthOrganizationController],
exports: [],
})
export class AuthOrganizationModule {}
12 changes: 12 additions & 0 deletions src/auth/auth-organization/domain/auth-organization.store.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import { AuthOrganization } from './auth-organization';

export interface AuthOrganizationStore {
save(authOrganization: AuthOrganization): Promise<void>;
update(authOrganization: AuthOrganization): Promise<void>;
loadByOrganizationId(organizationId: string): Promise<AuthOrganization | null>;
loadByRefreshToken(refreshToken: string): Promise<AuthOrganization | null>;
loadByAccountId(accountId: string): Promise<AuthOrganization | null>;
existsByAccountId(accountId: string): Promise<boolean>;
}

export const AUTH_ORGANIZATION_STORE = Symbol('AUTH_ORGANIZATION_STORE');
Loading
Loading