From adb992e6d87bab8f494438ed44bdbe6b2d6a3242 Mon Sep 17 00:00:00 2001 From: ttipo Date: Sat, 31 Oct 2026 10:57:14 +0900 Subject: [PATCH] =?UTF-8?q?docs:=2012=EB=8B=A8=EA=B3=84=20=EA=B3=84?= =?UTF-8?q?=EC=A0=95=20=EB=B3=B4=EC=95=88=20=E2=80=94=20=EA=B3=84=EC=95=BD?= =?UTF-8?q?=205=EC=A0=88=20=C2=B7=20=EB=94=94=EC=9E=90=EC=9D=B8=20?= =?UTF-8?q?=EB=B3=B4=EB=93=9C=20=EB=84=B7=20=C2=B7=20README?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 2단계 인증(인증 앱 TOTP) · 앱 비밀번호 · 로그인 기록을 엔진(Stalwart 0.16.27) 실험 결과에 맞춰 정했다. 엔진이 코드 확인 · 메일 프로그램 막기 · 앱 비밀번호를 맡고, 서버는 첫 코드 확인 · 다시 쓰기 막기 · 실패 한도 · 기록을 맡는다. 사용자 결정(2026-10-31): 비밀번호를 바꾸면 앱 비밀번호도 지우고, 끊은 세션은 바로 막는다. Co-Authored-By: Claude Opus 5.5 --- docs/design/webmail/LoginTwoFactor.dc.html | 229 ++++++++ docs/design/webmail/README.md | 97 ++++ .../webmail/SettingsAppPasswords.dc.html | 464 ++++++++++++++++ .../webmail/SettingsLoginHistory.dc.html | 501 ++++++++++++++++++ docs/design/webmail/SettingsTwoFactor.dc.html | 493 +++++++++++++++++ docs/design/webmail/canvas.json | 11 +- docs/mail/api-contract.md | 310 ++++++++++- 7 files changed, 2101 insertions(+), 4 deletions(-) create mode 100644 docs/design/webmail/LoginTwoFactor.dc.html create mode 100644 docs/design/webmail/SettingsAppPasswords.dc.html create mode 100644 docs/design/webmail/SettingsLoginHistory.dc.html create mode 100644 docs/design/webmail/SettingsTwoFactor.dc.html diff --git a/docs/design/webmail/LoginTwoFactor.dc.html b/docs/design/webmail/LoginTwoFactor.dc.html new file mode 100644 index 0000000..1624a8c --- /dev/null +++ b/docs/design/webmail/LoginTwoFactor.dc.html @@ -0,0 +1,229 @@ + + + + +로그인 · 2단계 인증 + + + + + + + +
+
+ +
+ ARA SYSTEM +
+

사내 업무를
한 계정으로

+

메일부터 시작해 캘린더, 파일 저장소, ERP까지 같은 계정으로 이어집니다.

+
+
+ +
+
+
+

2단계 인증

+

비밀번호를 확인했습니다. 휴대폰 인증 앱에 표시된 코드를 넣어 주세요.

+
+ +
+ + ara.kim@arasystem.it.kr + 다른 계정으로 로그인 +
+ +
+
+ + +
+ +
+ +

인증 앱을 쓸 수 없나요? 관리자에게 문의해 주세요.

+

Outlook · 휴대폰 메일 앱은 이 코드를 묻지 않습니다. 설정 › 앱 비밀번호에서 만든 비밀번호로 연결합니다.

+
+
+
+ +
+ +
+

로그인 · 2단계 인증

+

2단계 인증을 켠 직원은 비밀번호가 맞으면 위 화면으로 옵니다. 켜지 않은 직원은 지금처럼 바로 메일함으로 갑니다. 화면 틀 · 칸 · 단추는 로그인 화면과 같고, 오류 문구는 에러 코드 사전에서 가져옵니다. 2026-10-31 확정.

+
+ +
+ +
+
+

코드 틀림

+

칸 아래 빨간 글 · 칸을 비우고 초점은 그 칸에 — 알림 없음

+
+
+
+
+

2단계 인증

+

비밀번호를 확인했습니다. 휴대폰 인증 앱에 표시된 코드를 넣어 주세요.

+
+ +
+ + ara.kim@arasystem.it.kr + 다른 계정으로 로그인 +
+ +
+
+ + +

+ + 코드가 올바르지 않습니다. 휴대폰의 날짜와 시간이 「자동」으로 맞춰져 있는지 확인해 주세요. +

+
+ +
+ +

인증 앱을 쓸 수 없나요? 관리자에게 문의해 주세요.

+
+
+

인증 앱의 코드는 30초마다 바뀌고, 휴대폰 시계가 어긋나면 맞게 넣어도 틀립니다. 틀린 코드는 비밀번호 틀림과 따로 세고(같은 주소 15분 5회 · 하루 20회), 로그인 기록에 「실패 — 코드 틀림」으로 남습니다.

+
+ +
+
+

시도가 너무 많음

+

위쪽 빨간 상자 — 로그인 오류와 같은 자리 · 같은 모양

+
+
+
+
+

2단계 인증

+

비밀번호를 확인했습니다. 휴대폰 인증 앱에 표시된 코드를 넣어 주세요.

+
+ +
+ + ara.kim@arasystem.it.kr + 다른 계정으로 로그인 +
+ +

+ + 요청이 너무 많습니다. 잠시 후 다시 시도해 주세요. +

+ +
+
+ + +
+ +
+ +

인증 앱을 쓸 수 없나요? 관리자에게 문의해 주세요.

+
+
+

코드 실패 한도(같은 주소 15분 5회 · 하루 20회)를 넘었을 때입니다 — 문구는 사전의 「요청이 너무 많습니다」입니다. 막힌 동안 [확인]은 같은 안내를 다시 띄우고, 칸 · 단추는 그대로 둡니다 — 화면은 언제 풀리는지 모릅니다.

+
+ +
+
+

확인 중

+

단추 바쁨 · 칸은 읽기만

+
+
+
+
+

2단계 인증

+

비밀번호를 확인했습니다. 휴대폰 인증 앱에 표시된 코드를 넣어 주세요.

+
+ +
+ + ara.kim@arasystem.it.kr + 다른 계정으로 로그인 +
+ +
+
+ + +
+ +
+ +

인증 앱을 쓸 수 없나요? 관리자에게 문의해 주세요.

+
+
+

로그인 단추와 같은 모양입니다(「로그인하는 중…」처럼 흐리게 · aria-busy). 맞으면 받은편지함으로 — 로그인 전에 가려던 주소가 있으면 그곳으로 갑니다.

+
+
+ +
+ +
+
+

흐름 · 규칙

+
+
    +
  • 비밀번호가 틀리면 지금처럼 「이메일 또는 비밀번호가 올바르지 않습니다.」 — 이 화면으로 오지 않습니다. 2단계 인증을 켰는지는 비밀번호가 맞은 뒤에야 드러납니다.
  • +
  • [다른 계정으로 로그인] → 처음 로그인 화면(주소 칸은 비움). 확인한 비밀번호는 버립니다.
  • +
  • 칸은 숫자 6자리만 받습니다 — 붙여 넣은 글의 띄어쓰기 · 하이픈은 지웁니다. 휴대폰 · 암호 관리자의 코드 자동 채우기(one-time-code)를 켭니다.
  • +
  • 이 화면에 10분 넘게 머물면 [확인] 때 「다시 로그인해 주세요」와 함께 처음 화면으로 갑니다. 새로고침도 처음 화면으로 갑니다.
  • +
  • 이 브라우저 기억 · 복구 코드 · 문자 · 메일 코드는 두지 않습니다 — 로그인할 때마다 코드를 묻습니다.
  • +
+
+ +
+
+

메일 프로그램 · 휴대폰을 잃었을 때

+
+
    +
  • Outlook · 썬더버드 · 휴대폰 메일 앱(IMAP · SMTP)은 이 단계를 거치지 않습니다. 2단계 인증을 켜면 웹메일 비밀번호로는 새로 연결할 수 없고 앱 비밀번호로만 연결합니다(설정 › 앱 비밀번호).
  • +
  • 휴대폰을 잃었거나 인증 앱을 지웠으면 관리자에게 문의합니다 — 관리자가 메일 서버 관리 화면에서 그 직원의 2단계 인증을 끄면, 비밀번호만으로 로그인한 뒤 새 휴대폰으로 다시 켭니다.
  • +
  • 로그인 기록: 코드까지 맞으면 「성공 · 2단계 인증」, 코드가 틀리면 「실패 — 코드 틀림」.
  • +
+
+
+
+
+
+ + + diff --git a/docs/design/webmail/README.md b/docs/design/webmail/README.md index c3b1e90..24cb0eb 100644 --- a/docs/design/webmail/README.md +++ b/docs/design/webmail/README.md @@ -662,6 +662,103 @@ API 와 서버 규칙은 계약 2절 "실시간 알림". - 확정본이 미룬 것: 기간 고르기 · 연장, 다시 보내기, 받는 사람 확인 코드 · 비밀번호, 사내만, 받는 사람별 링크 · 기록, 처음 받음 · 만료 전 알림, 미리 보기, zip 모두 받기, 폴더 올리기, 관리자 화면 · 퇴사자 처리, 보낸 메일을 지울 때 파일도, 모바일 앱, 파일 저장소 연동. +## 12단계 — 계정 보안 (2026-10-31 확정 — `LoginTwoFactor` · `SettingsTwoFactor` · `SettingsAppPasswords` · `SettingsLoginHistory`) + +2단계 인증(인증 앱 TOTP) · 앱 비밀번호 · 로그인 기록. 직원이 스스로 켠다 — 관리자가 전원에게 강제하는 것은 13단계다. +데이터 · 규칙은 계약 5절(`docs/mail/api-contract.md` "계정 보안 — 12단계")이 정하고, 새 에러 코드(`AUTH_006`~`AUTH_009` — 계약 5절 "에러 코드")의 사전 문구 초안도 거기 있다. +설정 메뉴의 계정 묶음: 비밀번호 · 2단계 인증 · 앱 비밀번호 · 로그인 기록(새 항목 아이콘: 방패(체크) · 열쇠 · 시계(되감기)). +새 토큰은 없다 — 키 [복사] · 표의 [지우기] · [로그아웃] · 안내 상자 안 단추는 차단한 주소의 [차단 풀기]와 같은 밀도 예외 36(`--size-notice-action`)이다. +각 보드 위쪽은 실제 크기 화면(설정 셸은 `SettingsAccount` 와 같다 — 메뉴 208 · 콘텐츠 열 800), 아래는 상태 카드다. + +### 로그인 — 코드 단계 (`LoginTwoFactor`) + +- **누가 오나**: 비밀번호가 맞고 2단계 인증을 켠 직원만. 켜지 않은 직원은 지금처럼 바로 메일함으로 간다. + 비밀번호가 틀리면 지금처럼 「이메일 또는 비밀번호가 올바르지 않습니다.」 — 2단계를 켰는지는 비밀번호가 맞은 뒤에야 드러난다. +- **틀**은 로그인 화면과 같다. 위에서부터: + - 제목 "2단계 인증" / "비밀번호를 확인했습니다. 휴대폰 인증 앱에 표시된 코드를 넣어 주세요." + - 주소 + [다른 계정으로 로그인] — 처음 화면으로(주소 칸은 비움), 확인한 비밀번호는 버린다. + - 칸 "인증 앱에 표시된 6자리 코드" — 숫자 6자리만, 붙여 넣은 글의 띄어쓰기 · 하이픈은 지운다. `inputmode="numeric"` · `autocomplete="one-time-code"`. + - [확인] → "인증 앱을 쓸 수 없나요? 관리자에게 문의해 주세요." → "Outlook · 휴대폰 메일 앱은 이 코드를 묻지 않습니다. 설정 › 앱 비밀번호에서 만든 비밀번호로 연결합니다." +- **상태** + - 코드 틀림 — 칸 아래 빨간 글(`AUTH_006` 사전 문구 "코드가 올바르지 않습니다. 휴대폰의 날짜와 시간이 「자동」으로 맞춰져 있는지 확인해 주세요."). 칸을 비우고 초점은 그 칸, 알림은 없다. + - 시도 과다 — 위쪽 빨간 상자(로그인 오류와 같은 자리 · 같은 모양)에 `REQ_001` 사전 문구. 코드 실패는 비밀번호 틀림과 따로 센다(같은 주소 15분 5회 · 하루 20회). 칸 · 단추는 그대로 둔다. + - 확인 중 — 「확인하는 중…」(로그인 단추와 같은 모양 · `aria-busy`), 칸은 읽기만. 맞으면 받은편지함으로 — 로그인 전에 가려던 주소가 있으면 그곳으로. +- 이 화면에 10분 넘게 머물면 [확인] 때 「다시 로그인해 주세요」와 함께 처음 화면으로 간다. 새로고침도 처음 화면이다(비밀번호는 화면 메모리에만 있다). +- 로그인 기록에는 코드까지 맞으면 「성공 · 2단계 인증」, 틀리면 「실패 — 코드 틀림」으로 남는다. + +### 설정 › 계정 › 2단계 인증 (`SettingsTwoFactor`) + +- 머리 "2단계 인증" + "로그인할 때 비밀번호와 함께 휴대폰 인증 앱의 코드를 확인합니다. 비밀번호가 새어 나가도 휴대폰이 없으면 로그인할 수 없습니다." +- **상태는 글자로만**(뱃지 · 스위치 없음) — 테두리 상자 하나: + - 「꺼짐」(회색 굵게) / "지금은 로그인할 때 비밀번호만 확인합니다." + [2단계 인증 켜기] + - 「켜짐 — 2026년 10월 31일부터」(초록 #067647 굵게 — 켠 때를 모르면 「켜짐」) / "로그인할 때마다 비밀번호와 인증 앱의 코드를 확인합니다." + [2단계 인증 끄기] +- **꺼져 있을 때** 상자 아래: + - 갈색 경고 "켜면 메일 프로그램은 앱 비밀번호로 다시 연결해야 합니다" / "Outlook · 썬더버드 · 휴대폰 메일 앱은 웹메일 비밀번호로 새로 연결할 수 없고, 이미 연결해 둔 프로그램도 다음에 다시 연결할 때 막힙니다. 켜기 전에 앱 비밀번호를 만들어 두면 편합니다."(앱 비밀번호 링크) + - 「켜는 차례」 1 · 2 · 3(청록 굵은 숫자 — 동그라미 없음): 인증 앱 설치(Google · Microsoft Authenticator 처럼 6자리 코드를 만드는 앱) → [2단계 인증 켜기]를 눌러 QR 찍기 → 코드와 지금 비밀번호를 넣으면 켜진다. +- **켜져 있을 때** 상자 아래: "휴대폰을 바꿀 때는 2단계 인증을 끈 뒤 새 휴대폰으로 다시 켜 주세요 — 끌 때 지금 휴대폰의 코드가 필요합니다." · "휴대폰을 잃어버렸거나 인증 앱을 지웠으면 관리자에게 문의해 주세요." · "메일 프로그램은 앱 비밀번호로 연결합니다." +- **켜기 창**(모달 620 — `wide`, 필터 고치기 창과 같은 폭 · 처음 초점은 코드 칸) + - ① "인증 앱으로 QR 코드를 찍어 주세요" / "인증 앱에서 [+] → 「QR 코드 스캔」을 고르고 이 그림을 찍습니다." + QR 그림 + - "찍을 수 없으면 이 키를 입력하세요" + 32자 키(4자씩 띄움, 고정폭) + [복사](36 — 「키를 복사했습니다.」) + "앱에는 「ARA SYSTEM (ara.kim@arasystem.it.kr)」로 저장됩니다." + - ② "앱에 표시된 6자리 코드" ③ "지금 비밀번호" → [켜기] [취소] + - 칸 아래 오류(알림 없이, 첫 잘못된 칸에 초점): 코드 `AUTH_006` · 지금 비밀번호 `AUTH_005`("지금 비밀번호가 올바르지 않습니다."). 둘 다 틀렸으면 코드부터 — 서버가 코드를 먼저 본다. + 틀린 코드는 지우고 그 칸에 초점을 둔다. QR · 키는 그대로라 다시 찍지 않아도 된다. 비밀번호 틀림은 로그인 실패와 함께, 코드 틀림은 코드 실패로 따로 센다. + - 켜는 동안 「켜는 중…」 · [취소]. 맞으면 창을 닫고 화면이 「켜짐」이 되며, 위에 안내 상자 "2단계 인증을 켰습니다" / "메일 프로그램(Outlook · 썬더버드 · 휴대폰 메일 앱)은 다시 연결할 때 앱 비밀번호가 필요합니다." [앱 비밀번호 만들기](36) — 이 화면을 떠나면 사라진다. + - 키는 창을 열 때마다 새로 만든다(10분 안에 켠다 — 지나면 `AUTH_008` 로 처음부터). 켜지 않고 닫으면 그 키는 버린다 — 앱에 먼저 저장한 항목은 지워 달라고 안내한다. QR · 키는 이 창에만 보이고 로그에 남기지 않는다. + - 켜도 다른 곳의 웹메일 로그인은 끊지 않는다 — 끊으려면 로그인 기록에서. +- **끄기 창**(alertdialog 400 · 처음 초점 「취소」, Esc 도 같다): "2단계 인증을 끌까요?" / "끄면 비밀번호만으로 로그인합니다. 만들어 둔 앱 비밀번호는 그대로 남습니다." + 지금 비밀번호 · 인증 앱 코드 → [끄기](빨강) [취소]. + 끄면 「2단계 인증을 껐습니다.」 — 화면은 「꺼짐」. 틀리면 켜기 창과 같은 칸 아래 글이다. +- **비밀번호 화면**(설정 › 비밀번호) + - 2단계 인증을 켠 직원: 지금 양식 맨 아래에 「인증 앱 코드」 칸 + "코드는 30초마다 바뀝니다 — 다른 칸을 다 쓴 뒤 넣어 주세요." + 머리 설명은 "웹메일에 로그인하는 비밀번호입니다. 2단계 인증이 켜져 있어 메일 프로그램은 앱 비밀번호로 연결합니다." 다른 탭에서 켜서 `AUTH_007` 이 오면 2단계 상태를 다시 받아 코드 칸을 보인다. + - 모든 직원: 단추 아래 "바꾸면 이 브라우저를 뺀 다른 기기 · 브라우저의 웹메일 로그인이 끊깁니다." "앱 비밀번호도 모두 지워집니다 — 메일 프로그램은 새 앱 비밀번호로 다시 연결해 주세요." + +### 설정 › 계정 › 앱 비밀번호 (`SettingsAppPasswords`) + +- 머리 "앱 비밀번호" + "Outlook · 썬더버드 · 휴대폰 메일 앱 같은 메일 프로그램에 넣는 비밀번호입니다. 프로그램마다 따로 만들어 두면, 기기를 잃었을 때 그것만 지울 수 있습니다." + 오른쪽 "3 / 5개" + [앱 비밀번호 만들기]. +- **표**(구현은 실제 table): 이름 · 만든 날 · 마지막 사용 · [지우기](36, 빨간 글). 최근에 만든 것이 위, 긴 이름은 말줄임. + 마지막 사용은 "오늘 오전 9:12 · IMAP · 211.186.95.252" — 쓰지 않았으면 "아직 쓰지 않음"(회색, 오래 쓰지 않은 줄은 지워도 된다는 신호). +- **메일 프로그램 설정값 상자**: 받는 메일(IMAP) `mx1.arasystem.it.kr` · 포트 993 · SSL/TLS / 보내는 메일(SMTP) `mx1.arasystem.it.kr` · 포트 465 · SSL/TLS / + 사용자 이름 = 메일 주소 전체 / 비밀번호 = 여기서 만든 앱 비밀번호 — 웹메일 비밀번호가 아닙니다. 호스트 · 포트는 화면 상수 하나에 둔다(전 직원 전환 때 다시 본다). +- **아래 안내**: "앱 비밀번호는 메일 읽기 · 보내기(IMAP · SMTP)에만 쓰입니다. 웹메일 로그인에는 쓸 수 없습니다." · "2단계 인증을 켜면 메일 프로그램은 앱 비밀번호로만 연결합니다. 켜지 않아도 만들어 쓸 수 있습니다." · + "웹메일 비밀번호를 바꾸면 앱 비밀번호도 모두 지워집니다 — 메일 프로그램마다 새로 만들어 주세요." · "휴대폰이나 PC 를 잃어버렸으면 그 기기의 앱 비밀번호를 바로 지워 주세요." +- **5개를 모두 만들었을 때**: [앱 비밀번호 만들기]는 `aria-disabled` + 머리 아래 한 줄 "5개까지 만들 수 있습니다. 쓰지 않는 앱 비밀번호를 지운 뒤 만들어 주세요." — 누르면 창을 열지 않고 까닭을 읽어 준다(role=status). +- **빈 상태**: "0 / 5개" + "만든 앱 비밀번호가 없습니다" / "Outlook · 휴대폰 메일 앱에서 메일을 보려면 앱 비밀번호를 만들어 주세요. 2단계 인증을 켰다면 메일 프로그램은 앱 비밀번호로만 연결합니다." + 설정값 상자 · 아래 안내는 그대로. 목록을 받지 못하면 지금의 목록 오류와 같은 모양(사전 문구 + [다시 시도]) — 그동안 [만들기]는 막는다(개수를 모르므로). +- **만들기 창**(400 · 처음 초점 이름 칸): 「이름」 + "어느 프로그램 · 기기에서 쓰는지 알아볼 이름입니다. 40자까지." → [만들기] [취소]. + 칸 아래: "이름을 넣어 주세요." · "같은 이름의 앱 비밀번호가 있습니다."(앞뒤 공백을 지운 값으로 화면이 먼저 본다) · 그사이 다른 창에서 5개를 채웠으면 `AUTH_009` 사전 문구. +- **만든 결과 창**(620 · 처음 초점 [복사]): "앱 비밀번호를 만들었습니다" / "‘회사 PC 아웃룩’ — 메일 프로그램의 비밀번호 칸에 아래 값을 넣어 주세요." + - 값은 고정폭으로 4자씩 떨어뜨려 보이지만 띄어쓰기는 없다 — [복사](36)는 40자 그대로. "40자입니다. 보기 좋게 4자씩 띄웠지만 띄어쓰기는 없습니다." + - 경고 "이 창을 닫으면 다시 볼 수 없습니다. 잊어버리면 지우고 새로 만들어 주세요." + 설정값 요약 + [닫기]. + - 복사 「앱 비밀번호를 복사했습니다.」 / 못 하면 「앱 비밀번호를 복사하지 못했습니다. 값을 직접 골라 복사해 주세요.」 + - [닫기] · Esc 로 닫으면 목록 맨 위에 새 줄이 생기고 값은 화면 메모리에서도 지운다. 복사하지 않고 닫아도 다시 묻지 않는다. +- **지우기 확인**(alertdialog 400 · 처음 초점 「취소」): "‘휴대폰 메일 앱’ 앱 비밀번호를 지울까요?" / "이 비밀번호로 연결한 메일 프로그램은 다음 연결부터 로그인하지 못합니다. 지운 비밀번호는 되돌릴 수 없습니다." + [지우기](빨강) [취소] → 「‘휴대폰 메일 앱’ 앱 비밀번호를 지웠습니다.」 — 줄이 빠지고 개수가 준다(되돌리기 없음). + +### 설정 › 계정 › 로그인 기록 (`SettingsLoginHistory`) + +- 머리 "로그인 기록" + "최근 90일 동안의 로그인입니다. 모르는 기록이 있으면 바로 비밀번호를 바꾸고, 2단계 인증이 꺼져 있으면 켜 주세요."(두 링크). +- **로그인된 브라우저**: 「로그인된 브라우저」 + "웹메일 3곳" · 오른쪽 [다른 곳 모두 로그아웃](44). + - 줄마다 PC · 휴대폰 아이콘(16) · 브라우저 · 운영체제 / IP / 마지막 활동 / [로그아웃](36). 맨 위는 지금 이 브라우저 — 단추 대신 「지금 이 브라우저」 글자만. 모르면 "알 수 없는 브라우저". + - 아래 "웹메일 로그인만 보입니다. 메일 프로그램은 앱 비밀번호를 지워 끊습니다."(링크) + - 줄의 [로그아웃]은 묻지 않고 바로 끊는다 → 「‘Edge · Windows’의 로그인을 끊었습니다.」(되돌리기 없음). 끊은 줄은 빠지고, 다른 곳이 없으면 [다른 곳 모두 로그아웃]을 그리지 않는다. + - [다른 곳 모두 로그아웃] → 확인(alertdialog 400 · 처음 초점 「취소」) "다른 곳의 로그인을 모두 끊을까요?" / + "이 브라우저를 뺀 2곳(Edge · Windows, Safari · iPhone)의 웹메일 로그인을 끊습니다. 끊긴 곳은 다음 동작에서 바로 로그인 화면으로 돌아갑니다." "메일 프로그램은 끊기지 않습니다 — 앱 비밀번호를 지워 끊어 주세요." + [모두 로그아웃](청록 주 단추 — 데이터를 지우는 일이 아니다) [취소] → 「다른 곳의 로그인 2곳을 끊었습니다.」 + - 끊긴 브라우저는 다음 요청에서 바로 로그인 화면으로 간다(열린 실시간 알림도 30초 안에) — 계약 5절 "로그인된 브라우저". +- **최근 로그인**: 「최근 로그인」 + "최근 90일 · 웹메일과 메일 프로그램" · 오른쪽 전환 [전체 · 실패만](`.segmented`). 표: 시각 · 방식 · IP · 기기 · 결과. + - 방식: "웹메일" / "메일 프로그램 · IMAP" · "메일 프로그램 · SMTP" + 앱 비밀번호로 들어왔으면 아래 줄 "앱 비밀번호 ‘회사 PC 아웃룩’"(지운 앱 비밀번호도 그 이름 그대로). + - 기기: 웹메일은 "Chrome · Windows", 메일 프로그램은 "—". 나라 · 지역은 보이지 않는다. + - 결과: 「성공」 · 「성공 · 2단계 인증」 · 「실패 — 비밀번호 틀림」 · 「실패 — 코드 틀림」 · 「실패 — 앱 비밀번호 필요」(2단계를 켠 계정에 메일 프로그램이 웹메일 비밀번호로 연결하려 함). 실패는 빨간 굵은 글자만 — 뱃지 없음. + - 최근 순서 · 20개씩 [더 보기](44). 「실패만」이면 "최근 90일 · 실패 4번", [더 보기]도 실패만 이어서. + - 빈 상태(실패만): "최근 90일 동안 실패한 로그인이 없습니다" / "[전체]를 누르면 성공한 로그인도 봅니다." 기록은 이 기능을 배포한 날부터 쌓인다. +- 목록을 받지 못하면 지금의 목록 오류와 같은 모양(사전 문구 + [다시 시도]). + +### 처음 판에서 미룬 것 + +복구 코드, 이 브라우저 기억, 문자 · 메일 코드, 보안 키 · 패스키, 관리자가 2단계 인증을 강제하기(13단계), 켜진 채 휴대폰 바꾸기, 앱 비밀번호 이름 바꾸기 · 만료, +새 기기 로그인 알림 메일, 계정 변경 기록(비밀번호 바꾸기 · 2단계 켜기 등), 로그인 기록의 나라 · 지역. + ## 구현하며 정한 보완 (2026-10-26 — 캔버스에는 아직 없다) - 찾지 못한 첨부(보관 시간이 지난 업로드·지워진 원본)는 실패로 표시하고, 전달 원본에는 "빼기"만 둔다(다시 올릴 수 없다). diff --git a/docs/design/webmail/SettingsAppPasswords.dc.html b/docs/design/webmail/SettingsAppPasswords.dc.html new file mode 100644 index 0000000..2e5ffad --- /dev/null +++ b/docs/design/webmail/SettingsAppPasswords.dc.html @@ -0,0 +1,464 @@ + + + + +설정 · 앱 비밀번호 + + + + + + + +
+
+ + + +
+ +
+ 설정 +
+ + +
+
+ +
+ 김아라 + ara.kim@arasystem.it.kr +
+ + + 로그아웃 + +
+
+ +
+ + + +
+
+ +
+
+

앱 비밀번호

+

Outlook · 썬더버드 · 휴대폰 메일 앱 같은 메일 프로그램에 넣는 비밀번호입니다. 프로그램마다 따로 만들어 두면, 기기를 잃었을 때 그것만 지울 수 있습니다.

+
+
+ 3 / 5개 + +
+
+ +
+ +
    +
  • + 노트북 썬더버드 + 10월 31일 + 아직 쓰지 않음 + +
  • +
  • + 회사 PC 아웃룩 + 10월 27일 + 오늘 오전 9:12 · IMAP · 211.186.95.252 + +
  • +
  • + 휴대폰 메일 앱 + 10월 20일 + 어제 오후 6:40 · SMTP · 223.38.120.77 + +
  • +
+
+ +
+

메일 프로그램 설정값

+
+
받는 메일 (IMAP)
+
mx1.arasystem.it.kr · 포트 993 · SSL/TLS
+
보내는 메일 (SMTP)
+
mx1.arasystem.it.kr · 포트 465 · SSL/TLS
+
사용자 이름
+
ara.kim@arasystem.it.kr — 메일 주소 전체
+
비밀번호
+
여기서 만든 앱 비밀번호 — 웹메일 비밀번호가 아닙니다
+
+
+ +
+

앱 비밀번호는 메일 읽기 · 보내기(IMAP · SMTP)에만 쓰입니다. 웹메일 로그인에는 쓸 수 없습니다.

+

2단계 인증을 켜면 메일 프로그램은 앱 비밀번호로만 연결합니다. 켜지 않아도 만들어 쓸 수 있습니다.

+

웹메일 비밀번호를 바꾸면 앱 비밀번호도 모두 지워집니다 — 메일 프로그램마다 새로 만들어 주세요.

+

휴대폰이나 PC 를 잃어버렸으면 그 기기의 앱 비밀번호를 바로 지워 주세요.

+
+ +
+
+
+
+
+ +
+ +
+

설정 · 앱 비밀번호

+

위 화면은 3개를 만든 상태입니다. 값은 메일 서버가 만든 40자(app_ 로 시작)이고 만들 때 한 번만 보입니다. 이름은 꼭 붙이고 5개까지 둡니다. 메일 읽기 · 보내기(IMAP · SMTP)에만 쓰이고, 2단계 인증을 켜지 않은 직원도 만들 수 있습니다. 메뉴는 계정 묶음의 「앱 비밀번호」(2단계 인증 아래). 2026-10-31 확정.

+
+ +
+ +
+
+

5개를 모두 만들었을 때

+ [앱 비밀번호 만들기]는 aria-disabled — 까닭은 머리 아래 한 줄 · 콘텐츠 열 800 +
+
+
+
+

앱 비밀번호

+

Outlook · 썬더버드 · 휴대폰 메일 앱 같은 메일 프로그램에 넣는 비밀번호입니다. 프로그램마다 따로 만들어 두면, 기기를 잃었을 때 그것만 지울 수 있습니다.

+
+
+ 5 / 5개 + +
+
+ +

5개까지 만들 수 있습니다. 쓰지 않는 앱 비밀번호를 지운 뒤 만들어 주세요.

+ +
+ +
    +
  • + 태블릿 메일 앱 + 10월 31일 + 아직 쓰지 않음 + +
  • +
  • + 노트북 썬더버드 + 10월 31일 + 오늘 오전 9:40 · IMAP · 211.186.95.252 + +
  • +
  • + 회사 PC 아웃룩 + 10월 27일 + 오늘 오전 9:12 · IMAP · 211.186.95.252 + +
  • +
  • + 휴대폰 메일 앱 + 10월 20일 + 어제 오후 6:40 · SMTP · 223.38.120.77 + +
  • +
  • + 집 PC 아웃룩 + 9월 2일 + 10월 3일 오후 8:15 · IMAP · 121.134.7.88 + +
  • +
+
+
+
+ +
+
+

빈 상태

+

아직 하나도 만들지 않았을 때 — 설정값 상자 · 아래 안내는 그대로

+
+
+ 앱 비밀번호 +
+ 0 / 5개 + +
+
+
+ +

만든 앱 비밀번호가 없습니다

+

Outlook · 휴대폰 메일 앱에서 메일을 보려면 앱 비밀번호를 만들어 주세요. 2단계 인증을 켰다면 메일 프로그램은 앱 비밀번호로만 연결합니다.

+
+

목록을 받지 못하면 지금의 목록 오류와 같은 모양 — 사전 문구 + [다시 시도]. 그동안 [만들기]는 막습니다(개수를 모르므로).

+
+
+ +
+ +
+
+

만든 결과 창

+

모달 620 · 값은 이 창에서 한 번만 · 처음 초점 [복사]

+
+
+
+

앱 비밀번호를 만들었습니다

+

‘회사 PC 아웃룩’ — 메일 프로그램의 비밀번호 칸에 아래 값을 넣어 주세요.

+
+ app_7qm2xk9vtn3wpl1srb3hcy7djf2gaz1euk0n + +
+

40자입니다. 보기 좋게 4자씩 띄웠지만 띄어쓰기는 없습니다.

+
+ + 이 창을 닫으면 다시 볼 수 없습니다. 잊어버리면 지우고 새로 만들어 주세요. +
+
+
받는 메일 (IMAP)
+
mx1.arasystem.it.kr · 993 · SSL/TLS
+
보내는 메일 (SMTP)
+
mx1.arasystem.it.kr · 465 · SSL/TLS
+
사용자 이름
+
ara.kim@arasystem.it.kr
+
+
+ +
+
+
+
+ + 앱 비밀번호를 복사했습니다. +
+

[닫기] · Esc 로 닫으면 목록 맨 위에 새 줄이 생기고 값은 다시 볼 수 없습니다 — 복사하지 않고 닫아도 다시 묻지 않습니다. 복사하지 못하면 「앱 비밀번호를 복사하지 못했습니다. 값을 직접 골라 복사해 주세요.」

+
+ +
+
+
+

만들기 창

+

모달 400 · 처음 초점은 이름 칸 · 이름은 꼭

+
+
+
+

앱 비밀번호 만들기

+
+ + +

어느 프로그램 · 기기에서 쓰는지 알아볼 이름입니다. 40자까지.

+
+
+ + +
+
+
+
+
이름이 비었을 때 (칸 아래)
이름을 넣어 주세요.
+
같은 이름이 있을 때 (칸 아래)
같은 이름의 앱 비밀번호가 있습니다.
+
그사이 다른 창에서 5개를 채웠을 때
5개까지 만들 수 있습니다. 쓰지 않는 앱 비밀번호를 지운 뒤 만들어 주세요.
+
+
+
+
+

지우기 확인 창

+

모달 400 · 되돌릴 수 없는 일 — 처음 초점 「취소」

+
+
+
+

‘휴대폰 메일 앱’ 앱 비밀번호를 지울까요?

+

이 비밀번호로 연결한 메일 프로그램은 다음 연결부터 로그인하지 못합니다. 지운 비밀번호는 되돌릴 수 없습니다.

+
+ + +
+
+
+
+ + ‘휴대폰 메일 앱’ 앱 비밀번호를 지웠습니다. +
+

줄이 사라지고 개수가 줄어듭니다(되돌리기 없음). 이미 연결된 프로그램은 다음에 메일을 받거나 보낼 때 비밀번호를 다시 묻습니다.

+
+
+
+ +
+ +
+
+

줄 · 값

+
+
    +
  • 줄마다 이름 · 만든 날 · 마지막 사용 · [지우기]. 최근에 만든 것이 위입니다. 구현은 실제 표(table) — 캔버스만 grid 로 그렸습니다.
  • +
  • 마지막 사용 = 그 앱 비밀번호로 메일 서버에 로그인한 마지막 때(시각 · IMAP 또는 SMTP · IP). 쓰지 않았으면 「아직 쓰지 않음」(회색) — 오래 쓰지 않은 줄은 지워도 된다는 신호입니다.
  • +
  • 값은 메일 서버(엔진)가 만든 40자(app_ 로 시작). 만들 때 한 번만 보이고, 서버도 다시 보여 주지 못합니다.
  • +
  • 결과 창의 값은 4자씩 떨어뜨려 보이지만 띄어쓰기는 없습니다 — [복사]는 40자 그대로 복사합니다.
  • +
  • [지우기] 36 — 차단한 주소의 [차단 풀기]와 같은 밀도 예외(새 토큰 없음). 긴 이름은 줄 안에서 말줄임.
  • +
+
+ +
+
+

누르면

+
+
    +
  • [앱 비밀번호 만들기] → 이름 창 → [만들기] → 결과 창(값 · [복사] · 설정값) → [닫기] → 목록 맨 위에 새 줄, 「4 / 5개」.
  • +
  • 5개면 [만들기]는 창을 열지 않고 까닭을 읽어 줍니다(aria-disabled · role=status).
  • +
  • [지우기] → 확인 창 → 줄이 사라지고 개수가 줄어듭니다.
  • +
  • 로그인 기록의 메일 프로그램 줄에 앱 비밀번호 이름이 함께 남습니다. 지운 앱 비밀번호의 기록도 그 이름 그대로 남습니다.
  • +
  • 2단계 인증 화면의 「켰습니다」 안내 · 경고의 링크가 이 화면으로 옵니다.
  • +
+
+
+
+
+
+ + + diff --git a/docs/design/webmail/SettingsLoginHistory.dc.html b/docs/design/webmail/SettingsLoginHistory.dc.html new file mode 100644 index 0000000..3a95610 --- /dev/null +++ b/docs/design/webmail/SettingsLoginHistory.dc.html @@ -0,0 +1,501 @@ + + + + +설정 · 로그인 기록 + + + + + + + +
+
+ + + +
+ +
+ 설정 +
+ + +
+
+ +
+ 김아라 + ara.kim@arasystem.it.kr +
+ + + 로그아웃 + +
+
+ +
+ + + +
+
+ +
+

로그인 기록

+

최근 90일 동안의 로그인입니다. 모르는 기록이 있으면 바로 비밀번호를 바꾸고, 2단계 인증이 꺼져 있으면 켜 주세요.

+
+ +
+
+
+

로그인된 브라우저

+ 웹메일 3곳 +
+ +
+
+ +
    +
  • + + Chrome · Windows + 211.186.95.252 + 지금 이 브라우저 + +
  • +
  • + + Edge · Windows + 211.186.95.252 + 오늘 오전 9:05 + +
  • +
  • + + Safari · iPhone + 223.38.120.77 + 어제 오후 10:31 + +
  • +
+
+

웹메일 로그인만 보입니다. 메일 프로그램은 앱 비밀번호를 지워 끊습니다.

+
+ +
+
+
+

최근 로그인

+ 최근 90일 · 웹메일과 메일 프로그램 +
+
+ + +
+
+
+ +
    +
  • + 오늘 오전 9:30 + 웹메일 + 211.186.95.252 + Chrome · Windows + 성공 · 2단계 인증 +
  • +
  • + 오늘 오전 9:29 + 웹메일 + 211.186.95.252 + Chrome · Windows + 실패 — 코드 틀림 +
  • +
  • + 오늘 오전 9:12 + + 메일 프로그램 · IMAP + 앱 비밀번호 ‘회사 PC 아웃룩’ + + 211.186.95.252 + — + 성공 +
  • +
  • + 오늘 오전 8:47 + 웹메일 + 211.186.95.252 + Edge · Windows + 성공 +
  • +
  • + 어제 오후 11:58 + 메일 프로그램 · SMTP + 45.133.10.21 + — + 실패 — 비밀번호 틀림 +
  • +
  • + 어제 오후 10:05 + 웹메일 + 223.38.120.77 + Safari · iPhone + 성공 +
  • +
  • + 어제 오후 6:40 + + 메일 프로그램 · SMTP + 앱 비밀번호 ‘휴대폰 메일 앱’ + + 223.38.120.77 + — + 성공 +
  • +
  • + 10월 29일 오후 3:21 + 웹메일 + 185.220.101.4 + Chrome · Linux + 실패 — 비밀번호 틀림 +
  • +
+
+
+ +
+ + 최근 순서 · 20개씩. 90일이 지난 기록은 지웁니다. +
+ +
+
+
+
+
+ +
+ +
+

설정 · 로그인 기록

+

위 화면은 2단계 인증을 켠 직원의 기록입니다(2단계 인증 · 앱 비밀번호 보드와 같은 날). 웹메일과 메일 프로그램(IMAP · SMTP)의 로그인을 성공 · 실패 모두 보이고, 지금 로그인된 브라우저를 하나씩 또는 한꺼번에 끊습니다. 메뉴는 계정 묶음의 「로그인 기록」(맨 아래). 2026-10-31 확정.

+
+ +
+ +
+
+

최근 로그인 — 실패만

+ [실패만]을 고르면 실패한 줄만 · [더 보기]도 실패만 이어서 · 콘텐츠 열 800 +
+
+
+
+
+

최근 로그인

+ 최근 90일 · 실패 4번 +
+
+ + +
+
+
+ +
    +
  • + 오늘 오전 9:29 + 웹메일 + 211.186.95.252 + Chrome · Windows + 실패 — 코드 틀림 +
  • +
  • + 어제 오후 11:58 + 메일 프로그램 · SMTP + 45.133.10.21 + — + 실패 — 비밀번호 틀림 +
  • +
  • + 10월 29일 오후 3:21 + 웹메일 + 185.220.101.4 + Chrome · Linux + 실패 — 비밀번호 틀림 +
  • +
  • + 10월 12일 오전 4:02 + 메일 프로그램 · IMAP + 91.240.118.30 + — + 실패 — 비밀번호 틀림 +
  • +
+
+
+
+
+ +
+
+

다른 곳 모두 로그아웃 — 확인 창

+

모달 400 · 되돌릴 수 없는 일 — 처음 초점 「취소」

+
+
+
+

다른 곳의 로그인을 모두 끊을까요?

+

이 브라우저를 뺀 2곳(Edge · Windows, Safari · iPhone)의 웹메일 로그인을 끊습니다. 끊긴 곳은 다음 동작에서 바로 로그인 화면으로 돌아갑니다.

+

메일 프로그램은 끊기지 않습니다 — 앱 비밀번호를 지워 끊어 주세요.

+
+ + +
+
+
+
+ + 다른 곳의 로그인 2곳을 끊었습니다. +
+
+ + ‘Edge · Windows’의 로그인을 끊었습니다. +
+

위 알림은 확인 창 뒤, 아래 알림은 줄의 [로그아웃] 뒤입니다(묻지 않고 바로 끊음 · 되돌리기 없음). 끊은 줄은 목록에서 빠지고, 다른 곳이 없으면 [다른 곳 모두 로그아웃]을 그리지 않습니다.

+
+
+ +
+ +
+
+

빈 상태

+ 다른 곳의 로그인이 없고, 실패한 로그인도 없을 때 · 콘텐츠 열 800 +
+
+
+
+
+

로그인된 브라우저

+ 웹메일 1곳 +
+
+
+ +
    +
  • + + Chrome · Windows + 211.186.95.252 + 지금 이 브라우저 + +
  • +
+
+

다른 곳에 로그인된 브라우저가 없습니다. 웹메일 로그인만 보입니다. 메일 프로그램은 앱 비밀번호를 지워 끊습니다.

+
+ +
+
+
+

최근 로그인

+ 최근 90일 · 웹메일과 메일 프로그램 +
+
+ + +
+
+
+ +

최근 90일 동안 실패한 로그인이 없습니다

+

[전체]를 누르면 성공한 로그인도 봅니다.

+
+
+
+
+ +
+
+
+

줄 · 값

+
+
    +
  • 로그인된 브라우저 — 웹메일 로그인(세션)마다 한 줄: PC · 휴대폰 아이콘 · 브라우저 · 운영체제 · IP · 마지막 활동 · [로그아웃]. 맨 위는 지금 이 브라우저 — 단추 대신 「지금 이 브라우저」 글자만.
  • +
  • 브라우저 · 운영체제는 로그인할 때 브라우저가 알려 준 값으로 짐작합니다(틀릴 수 있음). 마지막 활동은 그 브라우저가 로그인을 마지막으로 이어 간 때입니다.
  • +
  • 최근 로그인 — 최근 순서. 메일 프로그램 줄은 기기를 알 수 없어 「—」, 대신 방식 아래에 앱 비밀번호 이름(맞는 앱 비밀번호가 없던 실패는 이름 없이).
  • +
  • 결과 글: 성공 · 성공 · 2단계 인증 · 실패 — 비밀번호 틀림 · 실패 — 코드 틀림 · 실패 — 앱 비밀번호 필요(2단계 인증을 켠 계정에 메일 프로그램이 웹메일 비밀번호로 연결하려 함). 실패는 빨간 굵은 글자만 — 뱃지 없음.
  • +
  • IP 는 앞단 프록시를 지난 실제 주소입니다. 나라 · 지역은 보이지 않습니다. 구현은 실제 표(table) — 캔버스만 grid 로 그렸습니다.
  • +
+
+
+
+

알아 둘 것

+
+
    +
  • 줄의 [로그아웃]은 36 — 차단한 주소의 [차단 풀기]와 같은 밀도 예외(새 토큰 없음). [다른 곳 모두 로그아웃] · [더 보기]는 44.
  • +
  • 끊은 브라우저는 바로 막힙니다 — 다음 요청에서 로그인 화면으로 가고, 열린 실시간 알림 연결도 30초 안에 끊깁니다.
  • +
  • 비밀번호를 바꾸면 지금처럼 다른 곳의 로그인을 모두 끊습니다 — 이 목록에도 이 브라우저만 남습니다.
  • +
  • 목록을 받지 못하면 지금의 목록 오류와 같은 모양(사전 문구 + [다시 시도]).
  • +
  • 기록은 이 기능을 배포한 날부터 쌓입니다 — 그 전 로그인은 보이지 않습니다.
  • +
+
+
+
+
+
+
+ + + diff --git a/docs/design/webmail/SettingsTwoFactor.dc.html b/docs/design/webmail/SettingsTwoFactor.dc.html new file mode 100644 index 0000000..5254c1a --- /dev/null +++ b/docs/design/webmail/SettingsTwoFactor.dc.html @@ -0,0 +1,493 @@ + + + + +설정 · 2단계 인증 + + + + + + + +
+
+ + + +
+ +
+ 설정 +
+ + +
+
+ +
+ 김아라 + ara.kim@arasystem.it.kr +
+ + + 로그아웃 + +
+
+ +
+ + + +
+
+ +
+

2단계 인증

+

로그인할 때 비밀번호와 함께 휴대폰 인증 앱의 코드를 확인합니다. 비밀번호가 새어 나가도 휴대폰이 없으면 로그인할 수 없습니다.

+
+ +
+
+ 꺼짐 + 지금은 로그인할 때 비밀번호만 확인합니다. +
+ +
+ +
+ +
+ 켜면 메일 프로그램은 앱 비밀번호로 다시 연결해야 합니다 + Outlook · 썬더버드 · 휴대폰 메일 앱은 웹메일 비밀번호로 새로 연결할 수 없고, 이미 연결해 둔 프로그램도 다음에 다시 연결할 때 막힙니다. 켜기 전에 앱 비밀번호를 만들어 두면 편합니다. +
+
+ +
+

켜는 차례

+
    +
  1. + + 휴대폰에 인증 앱을 설치합니다 — Google Authenticator, Microsoft Authenticator 처럼 6자리 코드를 만드는(TOTP) 앱이면 됩니다. +
  2. +
  3. + + [2단계 인증 켜기]를 누르고 화면의 QR 코드를 인증 앱으로 찍습니다. +
  4. +
  5. + + 앱에 표시된 6자리 코드와 지금 비밀번호를 넣으면 켜집니다. 그다음부터 로그인할 때마다 코드를 묻습니다. +
  6. +
+
+ +
+
+
+
+
+ +
+ +
+

설정 · 2단계 인증

+

위 화면은 꺼진 상태입니다. 인증 앱(TOTP) 하나로만 켭니다 — 이 브라우저 기억 · 복구 코드 · 문자 · 메일 코드는 없고, 휴대폰을 잃으면 관리자에게 문의합니다. 메뉴는 계정 묶음의 「2단계 인증」(비밀번호 아래). 2026-10-31 확정.

+
+ +
+ +
+
+

켜기 창

+

모달 620(필터 고치기 창과 같은 폭) · 처음 초점은 코드 칸

+
+
+
+

2단계 인증 켜기

+
    +
  1. + +
    + 인증 앱으로 QR 코드를 찍어 주세요 +
    +
    + +
    +
    +

    인증 앱에서 [+] → 「QR 코드 스캔」을 고르고 이 그림을 찍습니다.

    +
    + 찍을 수 없으면 이 키를 입력하세요 +
    + K7QF 2M4X HB6R TZ3N PW5C YL7D JQ2V MGAS + +
    + 앱에는 「ARA SYSTEM (ara.kim@arasystem.it.kr)」로 저장됩니다. +
    +
    +
    +
    +
  2. +
  3. + +
    + + +
    +
  4. +
  5. + +
    +
    + +
    + + +
    +
    +
    +
  6. +
+
+ + +
+
+
+
+ +
+
+
+

칸 아래 오류

+

[켜기]를 누른 뒤 — 알림 없이 칸 아래에만, 첫 잘못된 칸에 초점

+
+
+ + +

+ + 코드가 올바르지 않습니다. 휴대폰의 날짜와 시간이 「자동」으로 맞춰져 있는지 확인해 주세요. +

+
+
+
+ +
+ + +
+

+ + 지금 비밀번호가 올바르지 않습니다. +

+
+
+

틀린 코드는 지우고 그 칸에 초점을 둡니다. 둘 다 틀렸으면 코드부터 알립니다. 틀려도 QR · 키는 그대로라 다시 찍지 않아도 됩니다. 비밀번호 틀림은 로그인 실패와 함께, 코드 틀림은 코드 실패로 따로 셉니다(로그인과 같은 한도) — 너무 많으면 「요청이 너무 많습니다…」 알림.

+
+
+
+

켜는 동안 · 창을 닫으면

+
+
+ 켜기 창 아래 단추 줄 + + +
+
    +
  • [켜기]는 코드와 지금 비밀번호를 함께 확인합니다. 맞으면 창을 닫고 화면이 「켜짐」으로 바뀝니다(아래 왼쪽 — 위에 안내 상자).
  • +
  • 키는 창을 열 때마다 새로 만듭니다. 켜지 않고 닫으면 그 키는 버립니다 — 앱에 먼저 저장한 항목은 지워 주세요.
  • +
  • 켜도 다른 곳의 웹메일 로그인은 끊지 않습니다 — 끊으려면 로그인 기록에서.
  • +
  • QR · 키는 이 창에만 보이고 로그에 남기지 않습니다.
  • +
+
+
+
+ +
+ +
+
+

켜진 상태 — 켠 직후

+ 콘텐츠 열 800 · 위 안내 상자는 켠 직후에만 보이고 이 화면을 떠나면 사라집니다 +
+
+
+

2단계 인증

+

로그인할 때 비밀번호와 함께 휴대폰 인증 앱의 코드를 확인합니다. 비밀번호가 새어 나가도 휴대폰이 없으면 로그인할 수 없습니다.

+
+ +
+ +
+
+ 2단계 인증을 켰습니다 + 메일 프로그램(Outlook · 썬더버드 · 휴대폰 메일 앱)은 다시 연결할 때 앱 비밀번호가 필요합니다. +
+ 앱 비밀번호 만들기 +
+
+ +
+
+ 켜짐 — 2026년 10월 31일부터 + 로그인할 때마다 비밀번호와 인증 앱의 코드를 확인합니다. +
+ +
+ +
+

휴대폰을 바꿀 때는 2단계 인증을 끈 뒤 새 휴대폰으로 다시 켜 주세요 — 끌 때 지금 휴대폰의 코드가 필요합니다.

+

휴대폰을 잃어버렸거나 인증 앱을 지웠으면 관리자에게 문의해 주세요.

+

메일 프로그램은 앱 비밀번호로 연결합니다.

+
+
+
+ +
+
+

끄기 창

+

모달 400 · 처음 초점 「취소」(Esc 도 같음)

+
+
+
+

2단계 인증을 끌까요?

+

끄면 비밀번호만으로 로그인합니다. 만들어 둔 앱 비밀번호는 그대로 남습니다.

+
+ +
+ + +
+
+
+ + +
+
+ + +
+
+
+
+ + 2단계 인증을 껐습니다. +
+

틀리면 켜기 창과 같은 칸 아래 글입니다. 끄면 화면은 「꺼짐」(위 화면)으로 돌아가고, 메일 프로그램은 웹메일 비밀번호로도 다시 연결할 수 있습니다.

+
+
+ +
+ +
+
+

비밀번호 — 2단계 인증을 켠 직원

+ 지금 양식(설정 · 비밀번호)에 「인증 앱 코드」 칸을 맨 아래 더합니다 · 콘텐츠 열 800 +
+
+
+

비밀번호

+

웹메일에 로그인하는 비밀번호입니다. 2단계 인증이 켜져 있어 메일 프로그램은 앱 비밀번호로 연결합니다.

+
+ +
+
+ +
+ + +
+
+
+ +
+ + +
+

8자 이상 · 지금 비밀번호와 달라야 합니다. 영문 · 숫자 · 기호를 섞으면 더 안전합니다.

+
+
+ +
+ +
+
+
+ + +

코드는 30초마다 바뀝니다 — 다른 칸을 다 쓴 뒤 넣어 주세요.

+
+
+ +
+
+ +
+ +
+ 바꾸면 이 브라우저를 뺀 다른 기기 · 브라우저의 웹메일 로그인이 끊깁니다. + 앱 비밀번호도 모두 지워집니다 — 메일 프로그램은 새 앱 비밀번호로 다시 연결해 주세요. +
+
+
+
+ +
+
+

정한 것 · 확인할 것

+
+
    +
  • 메뉴: 계정 › 비밀번호 · 2단계 인증 · 앱 비밀번호 · 로그인 기록. 새 항목 셋의 아이콘은 방패(체크) · 열쇠 · 시계(되감기).
  • +
  • 상태는 글자로만 — 「꺼짐」(회색 굵게) · 「켜짐 — 2026년 10월 31일부터」(초록 굵게). 뱃지 · 스위치 없이 [켜기] · [끄기] 단추.
  • +
  • 켜기 창은 필터 고치기 창과 같은 폭(620), 끄기 창은 확인 창 폭(400). 키의 [복사]는 36 — 차단 풀기와 같은 밀도 예외(새 토큰 없음). 복사하면 알림 「키를 복사했습니다.」
  • +
  • 비밀번호 화면의 코드 칸은 2단계 인증을 켠 직원에게만, 맨 아래에 둡니다 — 코드가 30초마다 바뀌어 먼저 넣으면 다른 칸을 쓰는 동안 지날 수 있습니다. 틀리면 그 칸 아래 「코드가 올바르지 않습니다. …」.
  • +
  • 비밀번호를 바꾸면 앱 비밀번호도 모두 지웁니다 — 비밀번호가 샌 동안 누군가 만든 앱 비밀번호가 남지 않게.
  • +
+
+
+
+
+
+ + + diff --git a/docs/design/webmail/canvas.json b/docs/design/webmail/canvas.json index dc28efa..dff128e 100644 --- a/docs/design/webmail/canvas.json +++ b/docs/design/webmail/canvas.json @@ -38,9 +38,13 @@ "LargeFileBlock.dc.html": { "x": 1520, "y": 18920, "w": 1440, "h": 2020, "title": "본문 덩이 — 바깥 메일 프로그램에서" }, "MailReadLargeFiles.dc.html": { "x": 3040, "y": 18920, "w": 1440, "h": 1940, "title": "읽기 화면 · 대용량 첨부" }, "SharePage.dc.html": { "x": 4560, "y": 18920, "w": 1440, "h": 2130, "title": "내려받기 화면 (files.arasystem.it.kr)" }, - "SettingsLargeFiles.dc.html": { "x": 6080, "y": 18920, "w": 1440, "h": 2200, "title": "설정 · 대용량 첨부" } + "SettingsLargeFiles.dc.html": { "x": 6080, "y": 18920, "w": 1440, "h": 2200, "title": "설정 · 대용량 첨부" }, + "LoginTwoFactor.dc.html": { "x": 0, "y": 21880, "w": 1440, "h": 1990, "title": "로그인 · 2단계 인증" }, + "SettingsTwoFactor.dc.html": { "x": 1520, "y": 21880, "w": 1440, "h": 3130, "title": "설정 · 2단계 인증" }, + "SettingsAppPasswords.dc.html": { "x": 3040, "y": 21880, "w": 1440, "h": 2810, "title": "설정 · 앱 비밀번호" }, + "SettingsLoginHistory.dc.html": { "x": 4560, "y": 21880, "w": 1440, "h": 2300, "title": "설정 · 로그인 기록" } }, - "order": ["Main.dc.html", "Login.dc.html", "Compose.dc.html", "States.dc.html", "MainList.dc.html", "MainListRead.dc.html", "ComposeEditor.dc.html", "Settings.dc.html", "ComposeAssist.dc.html", "Search.dc.html", "BulkSelect.dc.html", "ListStates.dc.html", "SettingsAccount.dc.html", "SettingsVacation.dc.html", "MailboxManage.dc.html", "MailActions4.dc.html", "SettingsNotify.dc.html", "MailRealtime.dc.html", "SettingsFilter.dc.html", "MailFilterStates.dc.html", "MailFilterApply.dc.html", "MailFilterApplyStates.dc.html", "Contacts.dc.html", "ContactsEdit.dc.html", "ContactsMail.dc.html", "MailReadTools.dc.html", "MailPrint.dc.html", "MailSendLater.dc.html", "SettingsSendRead.dc.html", "ComposeLargeFiles.dc.html", "LargeFileBlock.dc.html", "MailReadLargeFiles.dc.html", "SharePage.dc.html", "SettingsLargeFiles.dc.html"], + "order": ["Main.dc.html", "Login.dc.html", "Compose.dc.html", "States.dc.html", "MainList.dc.html", "MainListRead.dc.html", "ComposeEditor.dc.html", "Settings.dc.html", "ComposeAssist.dc.html", "Search.dc.html", "BulkSelect.dc.html", "ListStates.dc.html", "SettingsAccount.dc.html", "SettingsVacation.dc.html", "MailboxManage.dc.html", "MailActions4.dc.html", "SettingsNotify.dc.html", "MailRealtime.dc.html", "SettingsFilter.dc.html", "MailFilterStates.dc.html", "MailFilterApply.dc.html", "MailFilterApplyStates.dc.html", "Contacts.dc.html", "ContactsEdit.dc.html", "ContactsMail.dc.html", "MailReadTools.dc.html", "MailPrint.dc.html", "MailSendLater.dc.html", "SettingsSendRead.dc.html", "ComposeLargeFiles.dc.html", "LargeFileBlock.dc.html", "MailReadLargeFiles.dc.html", "SharePage.dc.html", "SettingsLargeFiles.dc.html", "LoginTwoFactor.dc.html", "SettingsTwoFactor.dc.html", "SettingsAppPasswords.dc.html", "SettingsLoginHistory.dc.html"], "notes": { "flow": { "x": 0, "y": -300, "text": "arasystem 웹메일 — 1단계 화면 (로그인 → 메일함 → 쓰기)", "kind": "title1", "maxW": 4480 }, "phase2": { "x": 0, "y": 2180, "text": "2단계 화면 — 임시 저장 · 받는 사람 자동 완성 · 서명 (2026-10-27 확정)", "kind": "title1", "maxW": 2960 }, @@ -51,7 +55,8 @@ "phase7": { "x": 0, "y": 12400, "text": "7단계 화면 — 이미 받은 메일에 필터 적용 · 필터 창 나가기 확인 · 부재중 안내 — 2026-10-29 확정", "kind": "title1", "maxW": 2960 }, "phase8": { "x": 0, "y": 14280, "text": "8단계 화면 — 주소록 (내 연락처 · 그룹 · 사내 목록 · 메일에서 쓰기) — 2026-10-29 확정", "kind": "title1", "maxW": 4480 }, "phase9": { "x": 0, "y": 16520, "text": "9단계 화면 — 보내기 취소 · 예약 발송 · 외부 이미지 보기 · 인쇄 · 보낸 사람 차단 — 2026-10-29 확정", "kind": "title1", "maxW": 6000 }, - "phase11": { "x": 0, "y": 18620, "text": "11단계 화면 — 대용량 첨부 (쓰기 · 본문 덩이 · 읽기 · 내려받기 화면 · 설정) — 초안, 사용자 확인 전", "kind": "title1", "maxW": 7520 } + "phase11": { "x": 0, "y": 18620, "text": "11단계 화면 — 대용량 첨부 (쓰기 · 본문 덩이 · 읽기 · 내려받기 화면 · 설정) — 2026-10-30 확정", "kind": "title1", "maxW": 7520 }, + "phase12": { "x": 0, "y": 21580, "text": "12단계 화면 — 계정 보안(2단계 인증 · 앱 비밀번호 · 로그인 기록) — 2026-10-31 확정", "kind": "title1", "maxW": 6000 } }, "designSystems": [] } diff --git a/docs/mail/api-contract.md b/docs/mail/api-contract.md index a4c65a6..c881e2e 100644 --- a/docs/mail/api-contract.md +++ b/docs/mail/api-contract.md @@ -9,7 +9,7 @@ - 성공 `{ "success": true, "data": … }` / 실패 `{ "success": false, "error": { "code", "message", "traceId" } }` - 날짜·시각은 ISO 8601 UTC 문자열, 목록은 `{ items, pageInfo }` - 이 단계에서 쓰는 에러 코드: `AUTH_001` `AUTH_002`(클라이언트 전용) `AUTH_003` `AUTH_004` `AUTH_005` `VAL_001` `RES_001` `REQ_001` `FILE_001` `FILE_002` `MAIL_001` `MAIL_002` `MAIL_003` `MAIL_004` `MAIL_005` `MAIL_006` `MAIL_007` `MAIL_008` `MAIL_009` `MAIL_010` `MAIL_011` `MAIL_012` `MAIL_013` `MAIL_014` `MAIL_015` `CONTACT_001` `CONTACT_002` `SYS_001` - (11단계 — 4절 "대용량 첨부"가 `FILE_003` ~ `FILE_008` · `MAIL_016` 을 더한다) + (11단계 — 4절 "대용량 첨부"가 `FILE_003` ~ `FILE_008` · `MAIL_016` 을 더한다. 12단계 — 5절 "계정 보안"이 `AUTH_006` ~ `AUTH_009` 를 더한다) - **적용 범위: 1단계는 웹(브라우저) 전용이다.** 인증은 HttpOnly 쿠키뿐이고 토큰을 본문으로 주지 않는다. 모바일 앱(Flutter)의 인증 방식(토큰 발급·`flutter_secure_storage`)은 앱 착수 때 이 문서에 추가한다. - **배포 형태: 화면과 API 는 같은 주소(origin)를 쓴다.** 운영은 프론트 nginx 가 `/api` 를 백엔드로 넘기고, @@ -35,6 +35,8 @@ ### `POST /api/auth/login` +> 12단계: 응답에 `status` 가 붙고, 2단계 인증을 켠 직원은 코드 단계를 거친다 — 5절 "로그인". + ```jsonc // 요청 { "email": "ara.kim@arasystem.it.kr", "password": "…" } @@ -74,6 +76,7 @@ 두 번 이상 지난 토큰이나 30초가 지난 이전 토큰은 위 규칙대로 세션을 끊는다. - 세션은 로그인 후 **최대 30일**(절대 만료)이다. 그 안에서도 리프레시 토큰을 14일 동안 쓰지 않으면 끝난다. - 액세스 토큰은 서버 조회 없이 검증한다. 그래서 세션을 끊어도 이미 발급된 액세스 토큰은 만료(최대 1시간)까지 유효하다. + → 12단계부터는 끊은 세션을 바로 막는다(액세스 토큰의 세션 id + 서버가 기억한 끊은 세션 — 5절 "로그인된 브라우저"). ### `POST /api/auth/logout` @@ -81,6 +84,8 @@ ### `PUT /api/auth/password` — 4단계 +> 12단계: 2단계 인증을 켠 직원은 `otpCode` 를 함께 보낸다 — 5절 "비밀번호 바꾸기". + ```jsonc // 요청 (로그인 필요) { "currentPassword": "…", "newPassword": "…" } @@ -102,6 +107,7 @@ - 바뀌면 그 직원의 **다른 로그인 세션을 모두 끊는다** — 이 요청의 리프레시 쿠키(Path=`/api/auth` 라 이 요청에도 온다)가 가리키는 세션만 남긴다. 쿠키가 없거나 다른 직원 것이면 이 세션도 끊긴다(다음 갱신에서 로그인 화면으로). 끊긴 기기의 액세스 토큰은 만료(최대 1시간)까지 쓰인다(위 세션 규칙). 휴대폰 · PC 메일 프로그램(IMAP)은 새 비밀번호를 다시 넣어야 한다. + → 12단계부터: 끊긴 기기는 바로 막히고, 앱 비밀번호도 모두 지운다(5절 "비밀번호 바꾸기"). - 비밀번호는 바뀌었는데 세션 끊기가 실패하면 200 으로 답하고 오류 로그를 남긴다 — 실패로 답하면 사용자가 옛 비밀번호로 다시 시도하기 때문이다. - 비밀번호는 로그 · 응답에 남기지 않는다. 바꾼 일은 info 로그에 주소(가린 것)만 남긴다. - **클라이언트**: 같은 규칙(8자 이상 · 지금과 다름 · 확인 칸 일치)을 먼저 보고, `AUTH_005` 는 지금 비밀번호 칸 아래에 사전 문구로 보인다. 성공하면 세 칸을 비운다. @@ -2050,3 +2056,305 @@ interface FileUploadCreate { 9. **`FILE_006` 의 HTTP 상태** — 추천: 507(저장 공간을 정확히 말하고, 클라이언트의 자동 다시 시도 대상(502 · 503 · 504)과 상태로 갈린다). 대안 503. 10. **형식 검사의 MIME** — 추천: 확장자 허용 목록만(위 "올리기" 1). 보안 규칙(`.claude/rules/security.md` "확장자와 MIME 을 둘 다")의 예외라 규칙 문서에 까닭과 함께 적는다(rules 담당). 11. **인용 속 링크 상태를 묻는 길** — 추천: 주소 목록을 보내는 `POST /api/share-link-lookups`(이 계약 — 저장하지 않은 쓰기 화면의 인용에도 쓴다). 대안 — 원본 메일 id 로 서버가 본문에서 찾기(`GET /api/messages/{id}/…`) — 클라이언트가 주소를 고르지 않아도 되지만 임시 저장본 · 고친 인용은 따로 다뤄야 한다. + +--- + +## 5. 계정 보안 — 12단계 (2026-10-31 확정) + +> 2단계 인증(인증 앱 TOTP) · 앱 비밀번호 · 로그인 기록. 근거는 Stalwart 0.16.27 개발 엔진 실험(2026-10-31 — 엔진 TOTP · 앱 비밀번호 · 웹훅 · 자동 차단). +> - 엔진이 하는 것: 코드 확인(앞뒤 30초까지), 2단계를 켠 계정의 비밀번호만 하는 IMAP · SMTP 로그인 막기, 앱 비밀번호 만들기(값은 엔진이 정한다), 로그인 이벤트 보내기(웹훅). +> - 서버가 하는 것(엔진이 하지 않는 것): 등록 때 첫 코드 확인, 같은 코드 다시 쓰기 막기, 코드 실패 한도, 웹메일 로그인에서 앱 비밀번호 거절, 로그인 기록 저장 · 보존. +> - 웹(브라우저)만이다. 모바일 앱은 앱 착수 때 이 절에 더한다. + +### 공유 계약을 바꾸는 곳 (한데 모음) + +- `POST /api/auth/login`: 요청에 `otpCode`(선택), 응답에 `status`(`signedIn` · `twoFactorRequired`). `twoFactorRequired` 는 200 이고 쿠키가 없다 — `email` · `name` 은 `null`. +- `PUT /api/auth/password`: 요청에 `otpCode` — 2단계 인증을 켠 직원만 필수. +- 새 API: `/api/two-factor` · `/api/two-factor/enrollments` · `/api/two-factor/activation` · `/api/two-factor/deactivation` · `/api/app-passwords` · `/api/auth/sessions` · `/api/login-events`. +- 엔진만 부르는 길 `POST /api/internal/mail-engine-events` — 앞단 nginx 가 막는다(404 `RES_001`). +- 에러 코드 `AUTH_006` ~ `AUTH_009`. +- 루트 env `MAIL_WEBHOOK_SECRET`(setup · backend 공용 — 운영 필수). + +### 한도 · 규칙 + +| 항목 | 값 | +| --- | --- | +| 코드 | 숫자 6자리 · 30초 · SHA-1(인증 앱 기본값). 엔진은 앞뒤 한 단계(±30초)까지 받는다 | +| 같은 코드 다시 쓰기 | 같은 직원이 성공에 쓴 코드는 2분 동안 다시 받지 않는다(코드가 받아지는 90초보다 길게 — 로그인 · 설정 모두) → `AUTH_006` | +| 코드 실패 | 같은 주소 15분 5회 · 24시간 20회. 같은 IP 는 로그인 실패 한도(1분 20회)에 함께 센다 → 429 `REQ_001` | +| 등록 시간 | 등록을 시작하고 10분 안에 켠다. 직원마다 기다리는 등록은 하나다(새로 시작하면 앞의 것은 버린다) | +| 앱 비밀번호 | 엔진 상한(기본 5개 — `x:Authentication.maxAppPasswords`). 이름 1~40자(앞뒤 공백을 지운 값, JS 문자열 길이). 권한은 메일 프로그램만 | +| 로그인 기록 | 90일 보존(날마다 정리) | +| 로그인된 브라우저 | 끝나지 않은 웹메일 세션 — 1절 세션 규칙 그대로(절대 30일 · 미사용 14일) | + +### 로그인 (`POST /api/auth/login`) — 1절에 더하는 것 + +```jsonc +// 1) 비밀번호 +{ "email": "ara.kim@arasystem.it.kr", "password": "…" } +// 2단계 인증을 켜지 않은 직원 → 200 + Set-Cookie (지금과 같다) +{ "status": "signedIn", "email": "ara.kim@arasystem.it.kr", "name": "김아라" } +// 켠 직원이고 비밀번호가 맞음 → 200, 쿠키 없음 +{ "status": "twoFactorRequired", "email": null, "name": null } + +// 2) 코드 — 비밀번호를 한 번 더 싣는다(서버는 비밀번호를 들고 있지 않는다) +{ "email": "ara.kim@arasystem.it.kr", "password": "…", "otpCode": "123456" } +// → 200 + Set-Cookie { "status": "signedIn", "email": "…", "name": "…" } +``` + +| 실패 | 코드 | +| --- | --- | +| 비밀번호 틀림(코드 단계에서도 — 그사이 바뀜) · **앱 비밀번호 모양**(`app` + 1자 + 36자 · 40자 — 엔진에 묻지 않고 실패로 센다) · 그 밖의 1절 규칙 | 401 `AUTH_004` | +| 코드 틀림 · 2분 안에 다시 쓴 코드 | 400 `AUTH_006` — 401 이 아닌 까닭은 `AUTH_005` 와 같다(같은 코드를 로그인한 채 쓰는 설정에서도 쓴다) | +| `otpCode` 가 숫자 6자리가 아님 | 400 `VAL_001` | +| 코드 실패 한도 · 1절의 한도 | 429 `REQ_001` | + +- 서버의 차례 + 1. 앱 비밀번호 모양이면 거절한다. + 2. 1절처럼 실패 한 번을 차지하고 엔진에 본인 Basic 으로 묻는다(지금의 `verifyCredentials`). + 3. 200 → 2단계 인증이 꺼진 직원이다. 로그인한다(코드가 와도 보지 않는다). + 4. **402**(비밀번호는 맞고 엔진이 2단계를 요구) → 차지한 비밀번호 실패를 돌려준다. `otpCode` 가 없으면 `twoFactorRequired` 로 답한다. + 있으면: 다시 쓴 코드인지 보고 → 코드 실패 한 번을 차지하고 → 엔진 `POST /api/auth`(`accountSecret` + `mfaToken`)에 묻는다. + `authenticated` 면 로그인한다(차지한 것을 돌려주고 코드를 2분 기억). `failure` 면 `AUTH_006`. + 5. 401 → `AUTH_004`. +- **지금은 402 를 엔진 장애(503 `MAIL_001`)로 다룬다 — 고친다.** 운영자가 엔진 관리 화면에서 OTP 를 켠 직원은 지금 웹메일에 들어오지 못한다. 4단계 비밀번호 바꾸기도 같다. +- 코드 단계에서도 비밀번호를 엔진 Basic 으로 먼저 보는 까닭: `/api/auth` 의 `failure` 는 비밀번호 틀림과 코드 틀림을 가르지 않는다. 비밀번호를 추측하는 요청이 코드 실패로 세지지 않게 한다. +- `twoFactorRequired` 는 비밀번호가 맞았다는 뜻이다(2단계 로그인의 성질 — 코드 없이는 들어오지 못한다). 비밀번호 추측은 1절의 실패 한도가 그대로 막는다. +- 엔진 `/api/auth` 가 성공하면 엔진 메모리에 1회용 OAuth 코드가 남는다(쓰지 않으면 10분 뒤 사라진다) — 쓰지 않는다. + `redirectUri` 는 https 주소여야 한다(`https:///`). `clientId` 는 고정 이름(`arasystem-webmail`)이다. +- **클라이언트** + - 코드 단계는 비밀번호를 화면 메모리에만 둔다. 성공 · [다른 계정으로 로그인] · 화면을 떠날 때 지운다. + 코드 단계에 10분 넘게 머물면 비밀번호를 지우고 첫 단계로 돌아간다(새로고침도 첫 단계). + - 코드 칸은 `inputmode="numeric"` · `autocomplete="one-time-code"` 이다. 붙여 넣은 글에서 숫자만 남긴다(공백 · `-`). + - `AUTH_006` 은 칸 아래에 보이고(알림 없음) 칸을 비운다. `AUTH_004` 는 첫 단계로 돌아가 비밀번호 칸 아래에 보인다. + +### 비밀번호 바꾸기 (`PUT /api/auth/password`) — 4단계에 더하는 것 + +```jsonc +{ "currentPassword": "…", "newPassword": "…", "otpCode": "123456" } // otpCode — 2단계 인증을 켠 직원만 +``` + +- 비밀번호를 먼저 본인 Basic 으로 확인한다 — 엔진은 지금 비밀번호 틀림과 코드 틀림을 같은 `forbidden` 으로 거절하기 때문이다. + - 200(2단계 꺼짐) → 지금처럼 본인 자격으로 바꾼다. + - 402(2단계 켜짐, 비밀번호 맞음) → 게이트웨이 대리 접근으로 `x:AccountPassword/set`(`currentSecret` + `secret` + `otpAuth.otpCode`)을 보낸다. 2단계 인증은 그대로 남는다. +- `otpCode` 가 없으면 400 `AUTH_007`(다른 탭에서 켠 경우 — 화면이 코드 칸을 보인다). 틀리거나 다시 쓴 코드면 400 `AUTH_006`(코드 실패로 센다). +- 앱 비밀번호 모양의 지금 비밀번호는 엔진에 묻지 않고 `AUTH_005` 다(세지 않는다 — 4단계 규칙과 같다). +- **바뀌면 그 직원의 앱 비밀번호를 모두 지운다**(2단계 인증과 상관없이 — 대리 접근 `x:AppPassword/set` 의 destroy, Google 과 같다). + - 비밀번호가 샌 동안 누군가 웹메일에서 만든 앱 비밀번호가 남지 않게 한다. + - 지우기에 실패해도 200 이고 오류 로그를 남긴다. 결과를 모르는 실패(503)에도 지운다 — 다른 세션을 끊는 규칙과 같다. + - 화면이 미리 안내한다(메일 프로그램은 새 앱 비밀번호로 다시 연결한다). +- 구현 때 확인: 2단계를 켠 채 대리 접근으로 바꾸기의 성공 경로(조사 때는 실패 경로만 돌렸다), 바꾼 뒤 `otpUrl` 이 남는지. + +### 2단계 인증 (`/api/two-factor`) + +```jsonc +// GET /api/two-factor → 200 +{ "isEnabled": true, "enabledAt": "2026-10-31T00:12:00.000Z" } // enabledAt — 웹메일에서 켠 때. 엔진 관리 화면에서 켰으면 null + +// POST /api/two-factor/enrollments → 201 (꺼져 있을 때만, Cache-Control: no-store) +{ "enrollmentId": "…", "secret": "JBSWY3DPEHPK3PXP…(32자)", + "otpauthUrl": "otpauth://totp/ARA%20SYSTEM:ara.kim%40arasystem.it.kr?secret=…&issuer=ARA%20SYSTEM&algorithm=SHA1&digits=6&period=30", + "expiresAt": "2026-10-31T00:22:00.000Z" } + +// POST /api/two-factor/activation → 200 (GET 과 같은 모양) +{ "enrollmentId": "…", "otpCode": "123456", "currentPassword": "…" } + +// POST /api/two-factor/deactivation → 200 (GET 과 같은 모양) +{ "currentPassword": "…", "otpCode": "123456" } +``` + +- **엔진이 기준이다.** 켜짐은 엔진 `x:AccountPassword/get` 에 `otpAuth.otpUrl` 이 있는지로 본다(대리 접근). 서버는 켠 때(`enabledAt`)만 둔다 — 엔진에서 꺼졌으면 지운다. +- 비밀(`secret`): 서버가 만든 160비트를 base32(RFC 4648, 채움 없음 — 32자)로 적는다. 등록을 기다리는 동안(10분)만 서버 메모리에 둔다. + 켠 뒤에는 엔진에만 있다 — 서버 · DB · 로그 어디에도 두지 않는다. +- QR 은 화면이 `otpauthUrl` 로 그린다. 새 프론트 의존성이 든다 — 작은 MIT QR 부호기 하나, `v-html` 없이 SVG 로 그린다(고르는 것은 구현 때). +- 켜기 + 1. 받은 코드를 등록의 비밀로 먼저 확인한다(±1 단계). 엔진은 등록 때 코드를 보지 않는다 — 잘못 등록하면 직원이 끄지도 못한다. + 2. 맞으면 대리 접근으로 `currentSecret` + `otpAuth.otpUrl` 을 보낸다. + 3. 지금 비밀번호가 틀리면 `AUTH_005` 다(로그인 실패 한도로 센다 — 비밀번호 바꾸기와 같다). 켠 코드는 2분 기억한다. +- 끄기: 비밀번호를 본인 Basic 으로 먼저 확인(402) → 다시 쓴 코드인지 확인 → 대리 접근으로 `currentSecret` + `otpAuth: { otpCode, otpUrl: null }`. 이미 꺼져 있으면 200 이다(같은 결과). +- 켜고 끄는 일은 웹메일 세션을 끊지 않는다 — 로그인 기록 화면에 [다른 곳 모두 로그아웃]이 따로 있다. 이미 열린 IMAP 연결은 다시 연결할 때까지 남는다(알려진 한계). +- 같은 직원의 2단계 인증 바꾸기(등록 · 켜기 · 끄기 · 2단계를 켠 직원의 비밀번호 바꾸기)는 한 번에 하나다 — 처리 중이면 429 `REQ_001`. +- 휴대폰 바꾸기: 끈 뒤 다시 켠다(켜진 채 다시 등록하기는 처음 판에 없다). +- 휴대폰을 잃었으면 운영자가 엔진 관리 화면에서 그 직원의 OTP 를 지운다(직원 비밀번호 없이 된다 — 절차는 mail/README). + +| 실패 | 코드 | +| --- | --- | +| 코드 틀림 · 다시 쓴 코드 | 400 `AUTH_006` | +| 지금 비밀번호 틀림 · 앱 비밀번호 모양 | 400 `AUTH_005` | +| 등록이 없음 · 10분 지남 · 더 새 등록으로 바뀜 · 그사이 다른 곳에서 켜짐(등록 · 켜기) | 409 `AUTH_008` — 화면은 처음부터 다시 | +| 형식 오류 | 400 `VAL_001` | +| 코드 · 비밀번호 실패 한도, 처리 중 | 429 `REQ_001` | +| 웹메일로 다루지 않는 계정 | 401 `AUTH_001` | +| 엔진 장애 · 결과를 모르는 실패 | 503 `MAIL_001` — 켜기 · 끄기의 결과를 모르면 엔진을 다시 읽어 맞춘다(바뀌었으면 성공으로) | + +### 앱 비밀번호 (`/api/app-passwords`) + +```jsonc +// GET /api/app-passwords → 200 (한 쪽 — 엔진 상한이 작다) +{ "items": [ { "id": "b", "name": "회사 PC 아웃룩", "createdAt": "2026-10-31T00:20:00.000Z", + "lastUsed": { "at": "2026-10-31T00:41:00.000Z", "method": "imap", "ip": "211.186.95.252" } } ], + "pageInfo": { "hasNext": false, "nextCursor": null, "total": 1 } } + +// POST /api/app-passwords → 201 (Cache-Control: no-store) — password 는 이 응답에만 +{ "name": "회사 PC 아웃룩" } +→ { "id": "c", "name": "회사 PC 아웃룩", "createdAt": "…", "lastUsed": null, "password": "app_…(40자)" } + +// DELETE /api/app-passwords/{id} → 200 (data: null) — 이미 없으면 200 +``` + +- 만들기는 대리 접근 `x:AppPassword/set` 의 create 다: + `{ description: 이름, permissions: { "@type": "Replace", "permissions": { "authenticate": true, "imapAuthenticate": true, "emailSend": true } } }`. + 기본(`Inherit`)이면 앱 비밀번호로 JMAP 을 쓰고 앱 비밀번호를 또 만들 수 있다 — 그래서 늘 좁힌다. 만료 · 허용 IP 는 두지 않는다. +- 값(`password`)은 엔진이 만들어 만들 때 한 번만 준다. 서버는 응답에 실어 넘기기만 한다(로그 · DB · 캐시 없음). +- 상한을 넘으면 엔진이 `overQuota` 로 거절한다 → 409 `AUTH_009`. 이름이 비었거나 40자를 넘거나 이미 있는 이름(앞뒤 공백을 지운 값이 같음)이면 400 `VAL_001` — 화면이 먼저 보고 칸 아래에 알린다. +- 목록은 최근에 만든 것이 위다. + 화면의 "n / 5개"는 엔진 기본값 5 를 상수로 쓴다 — 엔진 상한을 바꾸면 화면 상수도 함께 바꾼다. +- 2단계 인증을 켜지 않은 직원도 만든다(기기마다 따로 끊기 편하다). + - 쓰는 곳: IMAP 993 · SMTP 465(사용자 이름 = 메일 주소). + - 쓰이지 않는 곳: 웹메일 로그인 · 비밀번호 바꾸기 · 2단계 끄기. + - 웹메일 비밀번호를 바꾸면 모두 지워진다(위 "비밀번호 바꾸기"). +- 이름 바꾸기 · 만료 · 허용 IP 는 처음 판에 없다. +- **마지막 사용**: 엔진에 칸이 없어 서버가 웹훅의 `auth.success`(앱 비밀번호 — `id` 가 자격 번호)로 만든다. + 엔진은 지운 번호를 다시 쓴다 — 서버 기록은 같은 번호 · 같은 `createdAt` 일 때만 잇는다(만들 때 서버가 기록을 새로 둔다). +- 지운 앱 비밀번호로 이미 열린 IMAP 연결은 엔진이 끊지 않는다(알려진 한계 — 다음 연결부터 막힌다). + +### 로그인된 브라우저 (`/api/auth/sessions`) + +경로가 `/api/auth` 아래인 까닭: 리프레시 쿠키(Path=`/api/auth`)로 이 요청의 세션을 안다(비밀번호 바꾸기와 같다). + +```jsonc +// GET /api/auth/sessions → 200 (지금 세션이 맨 앞, 그다음 최근 활동 차례) +{ "items": [ { "id": "…", "createdAt": "…", "lastActiveAt": "…", "ip": "211.186.95.252", + "device": { "browser": "Chrome", "os": "Windows" }, "isCurrent": true } ], + "pageInfo": { "hasNext": false, "nextCursor": null, "total": 3 } } + +// DELETE /api/auth/sessions/{id} → 200 (data: null) +// 지금 세션 → 400 VAL_001 (로그아웃을 쓴다) · 남의 것 · 없는 것 · 끝난 것 → 404 RES_001 +// DELETE /api/auth/sessions → 200 { "ended": 2 } — 이 요청의 세션만 남기고 모두 끊는다 +``` + +- `lastActiveAt` 은 마지막 갱신(회전) 때다 — 없으면 로그인 때. +- `ip` · `device` 는 로그인 · 갱신 때 서버가 둔다. + - IP 는 `TRUST_PROXY` 기준 클라이언트 IP 다. + - 브라우저는 서버가 User-Agent 를 짧은 이름(`Edge` · `Chrome` · `Firefox` · `Safari` · `Whale` · `Samsung Internet` / `Windows` · `macOS` · `iOS` · `Android` · `Linux` · `ChromeOS`)으로 바꾼다 — 서버 순수 함수 하나, 모르면 `null`. + - 12단계 전에 연 세션은 둘 다 `null` 이다. +- **끊은 세션은 바로 막는다.** 액세스 토큰에 세션 id(`sid`)를 싣는다. 서버는 끊은 세션 id 를 1시간(액세스 토큰의 가장 긴 수명) 기억하고, 가드가 그 토큰을 401 `AUTH_001` 로 막는다 — DB 를 묻지 않는다. + - 끊긴 브라우저는 다음 요청에서 갱신이 실패해(세션이 끊김) 로그인 화면으로 간다. 열린 실시간 알림 연결은 다음 ping(25초) 때 끝낸다. + - 로그아웃 · 비밀번호 바꾸기 · 이미 쓴 리프레시 토큰 감지로 세션을 끊을 때도 같다 — 1절의 "끊어도 만료까지 유효"를 바꾼다. + - 서버가 다시 뜨면 최근 1시간 안에 끊은 세션을 DB 에서 읽어 채운다. 12단계 전에 발급한 토큰(`sid` 없음)은 지금처럼 만료까지 쓰인다. +- 리프레시 쿠키가 없거나 남의 것이면 `DELETE /api/auth/sessions` 는 모두 끊는다(비밀번호 바꾸기와 같다). 그때 `isCurrent` 는 모두 false 다. +- 페이지: 기본 20 · 최대 100(`limit`), 위치 커서(`shared/utils/offset-cursor.ts`). + +### 로그인 기록 (`GET /api/login-events`) + +```jsonc +// GET /api/login-events?limit=20&cursor=…&isSuccess=false → 200 (최근 차례) +{ "items": [ + { "id": "…", "occurredAt": "…", "method": "webmail", "isSuccess": true, "isTwoFactor": true, "failureReason": null, + "ip": "211.186.95.252", "device": { "browser": "Edge", "os": "Windows" }, "appPassword": null }, + { "id": "…", "occurredAt": "…", "method": "imap", "isSuccess": true, "isTwoFactor": false, "failureReason": null, + "ip": "…", "device": null, "appPassword": { "id": "b", "name": "회사 PC 아웃룩" } }, + { "id": "…", "occurredAt": "…", "method": "smtp", "isSuccess": false, "isTwoFactor": false, "failureReason": "appPasswordRequired", + "ip": "…", "device": null, "appPassword": null } ], + "pageInfo": { "hasNext": true, "nextCursor": "…", "total": 143 } } +``` + +| 칸 | 값 | +| --- | --- | +| `method` | `webmail` · `imap`(메일 프로그램 받기) · `smtp`(메일 프로그램 보내기) | +| `isTwoFactor` | 웹메일 성공이 코드까지 거쳤는지 | +| `failureReason` | `wrongPassword`(비밀번호 틀림 — 웹메일에 넣은 앱 비밀번호 포함) · `wrongCode`(코드 틀림 · 다시 쓴 코드) · `appPasswordRequired`(2단계를 켠 계정에 메일 프로그램이 비밀번호로 — 엔진 `auth.mfa-required`) | +| `appPassword` | 메일 프로그램이 앱 비밀번호로 들어온 경우. `name` 은 그때의 이름이다(지운 뒤에도 남는다) | + +- 남기는 것 + - 웹메일: 서버가 판정한 로그인 시도(성공 · `AUTH_004` · `AUTH_006`). 한도로 막은 요청(429) · 사내 도메인이 아닌 주소 · 막은 계정은 남기지 않는다. + - 메일 프로그램: 엔진 웹훅(아래)의 IMAP · SMTP 이벤트. +- 남기지 않는 것: 갱신(`/auth/refresh`), 게이트웨이 대리 접근, 엔진 HTTP 리스너의 이벤트(웹메일이 백엔드 IP 로 남기는 것 — 서버 기록과 겹친다). +- 90일 보존, 커서 페이지(기본 20 · 최대 100). `isSuccess=false` 면 실패만 준다. +- 비밀번호 · 코드 · 앱 비밀번호 값은 기록에 없다. User-Agent 원문은 512자까지 둔다(화면에는 짧은 이름만). + +### 엔진 웹훅 받기 (`POST /api/internal/mail-engine-events`) + +- 엔진만 부른다. 세 겹으로 막는다: + 1. 앞단 nginx 가 `/api/internal/` 을 404 `RES_001` 로 닫는다(두 server 모두). + 2. 서버는 접속 주소(`X-Forwarded-For` 가 아니라 소켓)가 앱 네트워크(`APP_NETWORK_SUBNET`)인지 본다. + 3. 본문 원문의 HMAC-SHA256(`X-Signature`, base64)이 `MAIL_WEBHOOK_SECRET` 으로 맞는지 본다(상수 시간 비교). + 하나라도 아니면 401 `AUTH_001` 이다 — 본문을 쓰지 않고, warn 로그에 접속 주소만 남긴다. +- 본문은 `{"events":[{ "id", "createdAt", "type", "data": {…} }]}` 이다. 서버가 쓰는 것: + - `type`: `auth.success` · `auth.failed` · `auth.mfa-required` + - `data.listenerId`: `imap…` → imap, `submission…` · `smtp` → smtp. 그 밖(`http` 등)은 버린다. + - `data.remoteIp` · `data.accountName`(주소, 앱 비밀번호면 로컬 이름) · `data.details` + - `data.id`: 앱 비밀번호 번호. `details` 가 앱 비밀번호 표시일 때만 쓴다 — IMAP 실패의 `id` 는 명령 태그다. +- 사내 도메인의 직원만 남긴다. 사내 목록에 없는 주소의 실패와 막은 계정은 버린다(엔진은 서버 전체의 이벤트를 보낸다). +- 엔진 이벤트 `id` 로 겹침을 거른다(엔진이 다시 보낸 묶음). 곧바로 200 으로 답한다(엔진이 5초 기다린다). +- 이 경로만 다르게 둔다 + - 요청 수 한도가 없다(`@SkipThrottle`). 출처 검사는 `Origin` 이 없어 통과한다. + - JSON 한도를 따로 둔다(본문 4MB · 배열 하나 10,000 · 값 200,000) — 공격이 오면 1초 묶음이 커진다. +- 엔진의 대기열은 메모리에 있다. 서버가 5분 넘게 받지 못하거나 엔진이 다시 뜨면 그동안의 메일 프로그램 기록은 빠진다(알려진 한계). + +### 엔진 설정 (setup — release-ops) + +- `x:WebHook` 하나를 둔다(setup 표시로 찾는다). + - url `http://backend:/api/internal/mail-engine-events`, `signatureKey` = `MAIL_WEBHOOK_SECRET` + - `eventsPolicy: include` + `auth.success` · `auth.failed` · `auth.mfa-required` — 기본(`exclude` + 빈 목록)은 모든 이벤트를 보낸다. + - `level: trace` — 이 판은 웹훅에 수준을 쓰지 않지만 새 판 문서는 쓴다(기본 info 면 실패 이벤트가 빠진다). + - throttle 1000ms · timeout 5000ms · discardAfter 300000ms · lossy false. 바꾼 뒤 리로드한다. +- `MAIL_WEBHOOK_SECRET` 이 비면(개발) 웹훅을 만들지 않고 `[주의]` 를 낸다(있던 것은 끈다). 운영은 비면 setup 이 멈춘다. +- 엔진 `/api/auth` 는 백엔드가 앱 네트워크로(`MAIL_JMAP_URL` 의 `/api/auth`) 묻는다. 공개 443 은 지금처럼 403 이다. +- 앱 비밀번호 상한(`maxAppPasswords`)은 엔진 기본 5 그대로 둔다. + +### 환경 변수 + +- `MAIL_WEBHOOK_SECRET` — 루트 env. 영문 · 숫자 32자 이상(Node `--env-file` 이 `#` 부터 자른다). +- setup 과 backend(compose `environment`)가 읽는다. 운영 필수이고 예시 값은 거부한다(`env.validation.ts`). +- 바꾸면 setup 을 다시 돌리고 backend 를 다시 띄운다. + +### 로그 + +- 코드 · 비밀(`secret` · `otpauthUrl`) · 앱 비밀번호 값 · 비밀번호는 어떤 수준에도 남기지 않는다. 앱 비밀번호 이름(사용자 글)도 남기지 않는다(id 만). +- info: 2단계 켜기 · 끄기, 앱 비밀번호 만들기 · 지우기, 다른 세션 끊기 — 가린 주소 + id. +- warn: 코드 틀림, 서명 · 주소가 맞지 않는 웹훅. +- 웹훅 수신은 남기지 않는다(반복되는 정상 흐름). 버린 수 · 남긴 수는 debug. +- 엔진 오류의 설명 문장은 남기지 않는다(지금 규칙). + +### 에러 코드 (12단계에 더함 — 제안, 문구는 레지스트리에서 정한다) + +| 코드 | HTTP | 상황 | 문구(제안) | +| --- | --- | --- | --- | +| `AUTH_006` | 400 | 2단계 인증 코드가 틀림 · 2분 안에 다시 씀(로그인 · 비밀번호 바꾸기 · 켜기 · 끄기) | "코드가 올바르지 않습니다. 휴대폰의 날짜와 시간이 「자동」으로 맞춰져 있는지 확인해 주세요." | +| `AUTH_007` | 400 | 2단계 인증을 켠 직원의 요청에 코드가 없음(다른 곳에서 켠 뒤의 화면) | "인증 앱의 코드를 입력해 주세요." | +| `AUTH_008` | 409 | 등록이 없음 · 시간이 지남 · 그사이 다른 곳에서 바뀜 | "2단계 인증 설정이 그사이 바뀌었거나 시간이 지났습니다. 처음부터 다시 해 주세요." | +| `AUTH_009` | 409 | 앱 비밀번호 상한 | "5개까지 만들 수 있습니다. 쓰지 않는 앱 비밀번호를 지운 뒤 만들어 주세요." — 엔진 상한을 바꾸면 이 문구와 화면 상수도 | + +웹이 분기하는 코드: 넷 모두. + +### 확장 때 옮길 것 (nestjs 스킬의 Redis 목록에 더한다) + +기다리는 등록(비밀 10분), 다시 쓴 코드 기억(2분), 코드 실패 카운터, 같은 직원의 2단계 바꾸기 잠금, 끊은 세션 id 기억(1시간) — 모두 프로세스 메모리다. 인스턴스를 늘리면 공유 저장소로 옮긴다. + +### 운영 확인 + +- setup 뒤 `[확인]` 줄에 웹훅이 켜졌는지 본다. 메일 프로그램으로 한 번 로그인해 로그인 기록에 IMAP 줄이 생기는지 본다. +- 휴대폰을 잃은 직원: 엔진 관리 화면 › 그 계정 › 자격 증명(Password)의 OTP 를 지운다(mail/README 에 절차 — 구현 때 실물로 확인). +- 직원에게 알릴 것: 2단계 인증을 켜면 아웃룩 · 휴대폰 메일 앱은 앱 비밀번호로 다시 연결해야 한다. + +### 알려진 한계 + +- IMAP · SMTP 에서 2단계를 켠 계정에 맞는 비밀번호를 넣으면 "2단계 필요"가 나와 비밀번호가 맞았는지 알 수 있다. 엔진 자동 차단은 틀린 시도만 센다(들어오지는 못한다). +- 2단계를 켜거나 앱 비밀번호를 지워도 이미 열린 IMAP 연결은 엔진이 끊지 않는다(지운 앱 비밀번호의 연결은 다음 명령에서 멈춘다). +- 웹훅이 잃는 기록이 있다(엔진 재시작 · 서버가 5분 넘게 받지 못함). +- 엔진 관리 화면에서 켠 2단계 인증은 `enabledAt` 이 없다. +- 복구 코드가 없다 — 휴대폰을 잃으면 운영자에게 문의한다. + +### 이 단계에서 하지 않는 것 + +복구 코드, 이 브라우저 기억, 문자 · 메일 코드, 보안 키(WebAuthn) · 패스키, 관리자가 2단계를 강제하기(13단계), 켜진 채 휴대폰 바꾸기, +앱 비밀번호 이름 바꾸기 · 만료 · 허용 IP, 새 기기 로그인 알림 메일, 계정 변경 기록(비밀번호 바꾸기 · 2단계 켜기 등), 메일 프로그램의 OAuth 로그인, 모바일 앱. + +### 정한 것 (2026-10-31 추천대로) + +1. 인증 앱에 보일 이름은 "ARA SYSTEM" 이다(앱에는 「ARA SYSTEM (주소)」). 엔진이 받는지 구현 때 확인한다 — 안 되면 "ARASYSTEM". +2. 코드 실패 한도는 같은 주소 15분 5회 · 24시간 20회다(위 "한도 · 규칙"). 비밀번호를 아는 공격자가 1년 내내 시도해도 맞힐 확률이 약 2% 다. +3. 2단계를 켜거나 꺼도 다른 세션을 끊지 않는다 — 로그인 기록 화면에서 따로 끊는다. +4. 비밀번호를 바꾸면 앱 비밀번호도 모두 지운다(위 "비밀번호 바꾸기"). +5. 다른 곳 로그아웃은 바로 적용한다 — 끊은 세션 id 를 가드가 막는다(위 "로그인된 브라우저"). 로그아웃 · 비밀번호 바꾸기의 세션 끊기도 같다.