generated from ttipo/project-base
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:
@@ -0,0 +1,33 @@
|
||||
import { MigrationInterface, QueryRunner } from 'typeorm';
|
||||
|
||||
/**
|
||||
* 로그인 세션 표 (docs/mail/api-contract.md 1절 "세션 규칙") — 이 프로젝트의 첫 테이블.
|
||||
* 리프레시 토큰은 해시만 저장하고, 1회용 회전·재사용 감지·로그아웃 폐기를 이 표로 판단한다.
|
||||
* 인덱스: expires_at(만료 정리) · revoked_at 부분 인덱스(폐기된 행만 — 정리 작업의 두 번째 조건)
|
||||
* (자동 생성: npm run migration:generate — 엔티티는 modules/auth/auth-session.entity.ts)
|
||||
*/
|
||||
export class CreateAuthSessions1793010690903 implements MigrationInterface {
|
||||
name = 'CreateAuthSessions1793010690903';
|
||||
|
||||
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(
|
||||
`CREATE TABLE "auth_sessions" ("id" uuid NOT NULL, "user_email" character varying(254) NOT NULL, "token_hash" character(64) NOT NULL, "previous_token_hash" character(64), "rotated_at" TIMESTAMP WITH TIME ZONE, "created_at" TIMESTAMP WITH TIME ZONE NOT NULL, "expires_at" TIMESTAMP WITH TIME ZONE NOT NULL, "absolute_expires_at" TIMESTAMP WITH TIME ZONE NOT NULL, "revoked_at" TIMESTAMP WITH TIME ZONE, "revoked_reason" character varying(32), CONSTRAINT "PK_641507381f32580e8479efc36cd" PRIMARY KEY ("id"))`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`CREATE INDEX "IDX_auth_sessions_expires_at" ON "auth_sessions" ("expires_at") `,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`CREATE INDEX "IDX_auth_sessions_revoked_at" ON "auth_sessions" ("revoked_at") WHERE "revoked_at" IS NOT NULL`,
|
||||
);
|
||||
}
|
||||
|
||||
public async down(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(
|
||||
`DROP INDEX "public"."IDX_auth_sessions_revoked_at"`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`DROP INDEX "public"."IDX_auth_sessions_expires_at"`,
|
||||
);
|
||||
await queryRunner.query(`DROP TABLE "auth_sessions"`);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user