feat(backend): 직원 인증(쿠키 JWT·DB 세션)과 웹메일 API(Stalwart JMAP 중계)

첫 사내 모듈인 웹메일의 백엔드다. 메일 엔진은 외부(Stalwart)에 두고 백엔드가 JMAP 으로 중계한다.

- 직원 로그인 = 메일 계정: 엔진으로 비밀번호를 확인한 뒤 HttpOnly 쿠키 JWT 를 발급한다.
  리프레시 세션은 DB 에 해시로 저장하고 1회용으로 회전한다 (이미 쓴 토큰이 오면 세션 폐기)
- 로그인 실패 한도는 엔진에 묻기 전에 원자적으로 차지하고, IPv6 는 /64 로 묶어 센다
- 상태를 바꾸는 요청은 출처(Sec-Fetch-Site·Origin)를 검사하고, 클라이언트 IP 는 TRUST_PROXY 로만 해석한다
- 메일함·목록·읽기·발송·첨부 API: 받은 HTML 은 정화해서 내보내고, 발송은 Idempotency-Key 로 중복을 막는다
- 메일 엔진 관리자 계정은 로그인·대리 접근 대상에서 막는다 (게이트웨이가 관리자까지 대리할 수 있다)
- 에러 코드 AUTH_004·MAIL_001~006 을 추가하고 클라이언트 에러 사전을 다시 생성했다

계약: docs/mail/api-contract.md

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-27 01:31:38 +09:00
parent 238d0ba838
commit 71f2e01d83
115 changed files with 15985 additions and 174 deletions
+20 -10
View File
@@ -1,13 +1,16 @@
import { MiddlewareConsumer, Module, NestModule } from '@nestjs/common';
import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { ConfigModule } from '@nestjs/config';
import { ScheduleModule } from '@nestjs/schedule';
import { ThrottlerGuard, ThrottlerModule } from '@nestjs/throttler';
import { ThrottlerModule } from '@nestjs/throttler';
import { CacheModule } from '@nestjs/cache-manager';
import { APP_GUARD } from '@nestjs/core';
import { RequestIdMiddleware } from '@/common/middleware/request-id.middleware';
import { ClientIpThrottlerGuard } from '@/common/guards/client-ip-throttler.guard';
import { OriginGuard } from '@/common/guards/origin.guard';
import { AuthModule } from '@/modules/auth/auth.module';
import { HealthModule } from '@/modules/health/health.module';
import { MailModule } from '@/modules/mail/mail.module';
import { UploadModule } from '@/modules/upload/upload.module';
import { UserModule } from '@/modules/user/user.module';
import { parsePositiveInt } from '@/shared/utils/env';
@@ -58,20 +61,27 @@ const THROTTLE_DEFAULT_LIMIT = 100; // 구간당 IP별 최대 요청 수
}),
}),
HealthModule,
// 직원 로그인(쿠키 JWT) — 자격 확인은 MailModule 의 메일 엔진 포트로
AuthModule,
// 웹메일 API (Stalwart JMAP 어댑터)
MailModule,
UploadModule,
UserModule,
],
providers: [
ShutdownLogger,
// 전역 가드는 등록 순서대로 돈다 — 다른 사이트에서 온 상태 변경 요청은 요청 수를 세기 전에 막는다
{
provide: APP_GUARD,
useClass: ThrottlerGuard,
useClass: OriginGuard,
},
// 요청 수 한도 — ThrottlerGuard 그대로, 클라이언트 키만 IPv6 /64 로 묶는다 (두 번째 limiter 가 아니다)
{
provide: APP_GUARD,
useClass: ClientIpThrottlerGuard,
},
],
})
export class AppModule implements NestModule {
// 모든 요청에 추적 ID 를 부여한다 (로그·에러 응답 traceId 와 동일 값)
configure(consumer: MiddlewareConsumer): void {
consumer.apply(RequestIdMiddleware).forRoutes('*');
}
}
// 요청 추적 ID 는 app.setup.ts 의 configureApp() 이 **맨 앞** express 미들웨어로 붙인다 —
// 본문 파서 오류(413·400·415)에도 traceId 가 남도록 Nest 미들웨어(본문 파서 뒤)로 두지 않는다
export class AppModule {}