feat(backend): 13단계 전환 준비 — 제한된 세션 · 2단계 필수 · 운영 도구 · 받은편지함 알림 · 문의 연락처

전 직원 전환 때 관리자가 계정을 다루고(입사 · 비밀번호 재설정 · 2단계 해제 · 잠금 · 퇴사 마무리 · 공용 주소) 회사가 2단계 인증을 필수로 할 수 있게 했다.
- 제한된 세션(AUTH_010): 임시 비밀번호(엔진 만료) · 2단계 필수인 직원은 로그인은 되지만 새 비밀번호 · 2단계 켜기만 지난다. 푸는 요청은 그 세션만 다음 제한으로 바꾸고,
  2단계를 켜면 2단계를 거친 적 없는 다른 세션은 끊는다. 만료는 게이트웨이 대리 접근 세션의 권한(다섯)으로 로그인 · 갱신 때 알아챈다.
- 운영 도구 CLI(dist/cli/ops.js): 바뀔 것 → y/N → 실행 → ops_audit_log. 관리자 비밀번호는 TTY 에서만 · 입력 기록 0, 재설정은 앱 비밀번호 → 세션 → 알림 → 만료 차례(만료 뒤 대리 접근이 막힌다).
- 받은편지함 알림 메일(Email/import — 링크 없음): 재설정 · 2단계 해제 · 위험 파일 판정. 끊은 세션 기억은 15초마다 DB 에서 채운다.
- 2단계를 켠 잠긴 계정도 로그인은 AUTH_004 — 엔진이 잠긴 계정에도 2단계를 먼저 봐, 게이트웨이 자신의 권한으로 잠김과 설정 문제를 가른다(실사용 시험에서 찾음).
검증: lint 0 · 단위 3,138 · e2e 266 · 리뷰 두 번 · 실사용 시험(브라우저 + 운영 도구) 89/89.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-11-02 15:37:03 +09:00
parent 73babeaa2c
commit 452d9fef1d
107 changed files with 10903 additions and 192 deletions
+40
View File
@@ -0,0 +1,40 @@
import 'reflect-metadata';
import { NestFactory } from '@nestjs/core';
import { OpsCli } from '@/modules/ops/ops-cli';
import { OPS_ALLOW_NO_TTY_ENV, TerminalIo } from '@/modules/ops/ops-io';
import { OpsModule } from '@/modules/ops/ops.module';
/**
* 운영 도구 (13단계 — 계약 6절 "운영 도구") — 서버 안에서 사람이 돌리는 명령:
* sudo docker exec -it <PROJECT_NAME>-backend node dist/cli/ops.js
* 한국어 메뉴로 묻고, 바뀔 것을 보여 주고, y/N 으로 확인받은 뒤 실행한다. 한 일은 ops_audit_log 에 남는다.
* Nest 앱 컨텍스트로 띄운다 — HTTP 서버 · cron · 실시간 엔진 연결 · 악성 검사 · 마이그레이션 없이 (modules/ops/ops.module.ts).
* 화면에는 경고 · 오류 로그만 섞인다(info 로그는 끈다)
*/
async function main(): Promise<number> {
const app = await NestFactory.createApplicationContext(OpsModule, {
logger: ['error', 'warn'],
});
// 표준 입력이 터미널이 아니면 도구가 비밀번호를 묻기 전에 멈춘다(-it 를 빠뜨림). 자동 시험만 OPS_ALLOW_NO_TTY=1 로 파이프를 받는다
const io = new TerminalIo(process.stdin, process.stdout, {
allowNonInteractive: process.env[OPS_ALLOW_NO_TTY_ENV] === '1',
});
try {
return await app.get(OpsCli).run(io);
} finally {
io.close();
await app.close();
}
}
main()
.then((code) => {
process.exitCode = code;
})
.catch((error: unknown) => {
process.stderr.write(
`운영 도구를 시작하지 못했습니다: ${error instanceof Error ? error.message : String(error)}\n`,
);
process.exitCode = 1;
});