generated from ttipo/project-base
docs: 계약 1 · 5 · 6절과 디자인 README 를 앱 로그인 서버에 맞춘다
- 계약 1절 "앱 로그인": 서버 구현, 갱신 · 로그아웃의 400 / 401 경계(빈 · 너무 긴 토큰), private, no-store, Bearer 는 cli 와 sid 가 있는 앱 토큰만, 웹 · 앱 로그인 요청 수 한 칸, 앱 세션의 갱신 유예는 세션 끝(서버도 끊는다), 앱 이름은 진짜 클라이언트로만, 알려진 한계 - 5절: "로그인된 기기"(세션 목록 client 칸 · 앱 기기 이름 규칙), 로그인 기록 method app · device 가 앱 기록에도 - 6절: 운영 도구 현황의 마지막 로그인(웹메일 · 앱), 재설정 · 이전 마치기의 세션 끊기가 앱도 - 디자인 README: 12단계 로그인된 기기 · 최근 로그인 · 비밀번호 화면 글, 13단계 직원 알림 메일의 로그아웃 줄, 2단계 켜기에서 다른 곳이 먼저 켠 경우(서버가 이 세션을 끊는다 — 계약 6절) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -315,7 +315,7 @@ API 와 서버 규칙은 계약 1절 "`PUT /api/auth/password`", 2절 "메일함
|
||||
- 새 비밀번호 아래 안내 "8자 이상이고 지금 비밀번호와 달라야 합니다. 영문, 숫자, 기호를 섞으면 더 안전합니다." (12.5px #667085).
|
||||
- 오류는 그 칸 아래 빨간 글(#B42318, 아이콘 alert-circle) · 칸 테두리도 빨강 — "지금 비밀번호가 올바르지 않습니다." · "새 비밀번호가 서로 다릅니다."
|
||||
- [비밀번호 바꾸기] → "비밀번호를 바꿨습니다. 다른 기기와 브라우저에서는 로그아웃했습니다."(#067647), 세 칸을 비운다.
|
||||
- 아래 안내 상자(바탕 #F3F8F8): 다른 기기의 웹메일 로그인이 끊기는 것과 메일 프로그램(IMAP)에 새 비밀번호를 다시 넣어야 하는 것.
|
||||
- 아래 안내 상자(바탕 #F3F8F8): 다른 곳의 로그인(웹메일 · ARA SYSTEM 앱)이 끊기는 것과 메일 프로그램(IMAP)에 새 비밀번호를 다시 넣어야 하는 것.
|
||||
|
||||
### 부재중 자동 응답
|
||||
|
||||
@@ -743,13 +743,13 @@ API 와 서버 규칙은 계약 2절 "실시간 알림".
|
||||
틀린 코드는 지우고 그 칸에 초점을 둔다. QR · 키는 그대로라 다시 찍지 않아도 된다. 비밀번호 틀림은 로그인 실패와 함께, 코드 틀림은 코드 실패로 따로 센다.
|
||||
- 켜는 동안 「켜는 중…」 · [취소]. 맞으면 창을 닫고 화면이 「켜짐」이 되며, 위에 안내 상자 "2단계 인증을 켰습니다" / "Outlook, 썬더버드, 휴대폰 메일 앱 같은 메일 프로그램은 다시 연결할 때 앱 비밀번호가 필요합니다." [앱 비밀번호 만들기](36) — 이 화면을 떠나면 사라진다.
|
||||
- 키는 창을 열 때마다 새로 만든다(10분 안에 켠다 — 지나면 `AUTH_008` 로 처음부터). 켜지 않고 닫으면 그 키는 버린다 — 앱에 먼저 저장한 항목은 지워 달라고 안내한다. QR · 키는 이 창에만 보이고 로그에 남기지 않는다.
|
||||
- 켜도 다른 곳의 웹메일 로그인은 끊지 않는다 — 끊으려면 로그인 기록에서.
|
||||
- 켜도 다른 곳의 로그인(웹메일 · ARA SYSTEM 앱)은 끊지 않는다 — 끊으려면 로그인 기록에서.
|
||||
- **끄기 창**(alertdialog 400 · 처음 초점 「취소」, Esc 도 같다): "2단계 인증을 끌까요?" / "끄면 비밀번호만으로 로그인합니다. 만들어 둔 앱 비밀번호는 그대로 남습니다." + 지금 비밀번호 · 인증 앱 코드 → [끄기](빨강) [취소].
|
||||
끄면 「2단계 인증을 껐습니다.」 — 화면은 「꺼짐」. 틀리면 켜기 창과 같은 칸 아래 글이다.
|
||||
- **비밀번호 화면**(설정 › 비밀번호)
|
||||
- 2단계 인증을 켠 직원: 지금 양식 맨 아래에 「인증 앱 코드」 칸 + "코드는 30초마다 바뀌니 다른 칸을 다 채운 뒤에 넣어 주세요."
|
||||
머리 설명은 "웹메일에 로그인하는 비밀번호입니다. 2단계 인증이 켜져 있어 메일 프로그램은 앱 비밀번호로 연결합니다." 다른 탭에서 켜서 `AUTH_007` 이 오면 2단계 상태를 다시 받아 코드 칸을 보인다.
|
||||
- 모든 직원: 단추 아래 "바꾸면 이 브라우저를 빼고 다른 기기와 브라우저의 웹메일에서 로그아웃됩니다." "앱 비밀번호도 모두 지워지니, 메일 프로그램은 새 앱 비밀번호로 다시 연결해 주세요."
|
||||
- 모든 직원: 단추 아래 "바꾸면 이 브라우저를 빼고 다른 기기와 브라우저에서 모두 로그아웃됩니다."(2026-11-06 — 앱 세션도 끊긴다) "앱 비밀번호도 모두 지워지니, 메일 프로그램은 새 앱 비밀번호로 다시 연결해 주세요."
|
||||
|
||||
### 설정 › 계정 › 앱 비밀번호 (`SettingsAppPasswords`)
|
||||
|
||||
@@ -777,17 +777,17 @@ API 와 서버 규칙은 계약 2절 "실시간 알림".
|
||||
### 설정 › 계정 › 로그인 기록 (`SettingsLoginHistory`)
|
||||
|
||||
- 머리 "로그인 기록" + "최근 90일 동안의 로그인입니다. 모르는 기록이 있으면 바로 비밀번호를 바꾸고, 2단계 인증이 꺼져 있으면 켜 주세요."(두 링크).
|
||||
- **로그인된 브라우저**: 「로그인된 브라우저」 + "웹메일 3곳" · 오른쪽 [다른 곳 모두 로그아웃](44).
|
||||
- 줄마다 PC · 휴대폰 아이콘(16) · 브라우저 · 운영체제 / IP / 마지막 활동 / [로그아웃](36). 맨 위는 지금 이 브라우저 — 단추 대신 「지금 이 브라우저」 글자만. 모르면 "알 수 없는 브라우저".
|
||||
- 아래 "웹메일 로그인만 보입니다. 메일 프로그램의 연결을 끊으려면 앱 비밀번호를 지워 주세요."(링크)
|
||||
- **로그인된 기기**(2026-11-06 — 앱 세션이 함께 들어가 「로그인된 브라우저」에서 바꿨다): 「로그인된 기기」 + "웹메일 3곳"(앱 세션이 있으면 "웹메일 2곳 · ARA SYSTEM 앱 1곳" — 세션의 `client` 로 세고 0곳인 쪽은 쓰지 않는다. "앱 N곳"으로 줄이지 않는다 — 앱 비밀번호를 쓰는 "휴대폰 메일 앱"과 헷갈리지 않게) · 오른쪽 [다른 곳 모두 로그아웃](44).
|
||||
- 줄마다 PC · 휴대폰 아이콘(16) · 기기(칸 머리 "기기" — 아이콘 칸의 화면 낭독 이름은 "종류") / IP / 마지막 활동 / [로그아웃](36). 앱 세션은 "ARA SYSTEM 앱 · Android"(운영체제를 모르면 "ARA SYSTEM 앱")이고 아이콘은 늘 휴대폰이다 — 앱인지는 `client` 로만 가린다(앱 꼴 User-Agent 의 웹 세션은 웹메일로 센다). 맨 위는 지금 이 브라우저 — 단추 대신 「지금 이 브라우저」 글자만. 모르면 "알 수 없는 브라우저".
|
||||
- 아래 "메일 프로그램은 여기에 보이지 않습니다. 연결을 끊으려면 [앱 비밀번호]를 지워 주세요."(링크 — 2026-11-06 앱 세션이 함께 보여 고침)
|
||||
- 줄의 [로그아웃]은 묻지 않고 바로 끊는다 → 「‘Edge · Windows’에서 로그아웃했습니다.」(되돌리기 없음). 끊은 줄은 빠지고, 다른 곳이 없으면 [다른 곳 모두 로그아웃]을 그리지 않는다.
|
||||
- [다른 곳 모두 로그아웃] → 확인(alertdialog 400 · 처음 초점 「취소」) "다른 곳에서 모두 로그아웃할까요?" /
|
||||
"이 브라우저를 뺀 2곳(Edge · Windows, Safari · iPhone)의 웹메일에서 로그아웃합니다. 그곳에서 다음에 무엇이든 누르면 바로 로그인 화면이 나옵니다." "메일 프로그램은 로그아웃되지 않습니다. 연결을 끊으려면 앱 비밀번호를 지워 주세요."
|
||||
"이 브라우저를 뺀 2곳(Edge · Windows, ARA SYSTEM 앱 · Android)에서 로그아웃합니다. 그곳에서 다음에 무엇이든 누르면 바로 로그인 화면이 나옵니다." "메일 프로그램은 로그아웃되지 않습니다. 연결을 끊으려면 앱 비밀번호를 지워 주세요."
|
||||
[모두 로그아웃](청록 주 단추 — 데이터를 지우는 일이 아니다) [취소] → 「다른 2곳에서 로그아웃했습니다.」
|
||||
- 끊긴 브라우저는 다음 요청에서 바로 로그인 화면으로 간다(열린 실시간 알림도 30초 안에) — 계약 5절 "로그인된 브라우저".
|
||||
- **최근 로그인**: 「최근 로그인」 + "최근 90일 · 웹메일과 메일 프로그램" · 오른쪽 전환 [전체 · 실패만](`.segmented--outline` — role=group + `aria-pressed`). 표: 시각 · 방식 · IP · 기기 · 결과.
|
||||
- 방식: "웹메일" / "메일 프로그램 · IMAP" · "메일 프로그램 · SMTP" + 앱 비밀번호로 들어왔으면 아래 줄 "앱 비밀번호 ‘회사 PC 아웃룩’"(지운 앱 비밀번호도 그 이름 그대로).
|
||||
- 기기: 웹메일은 "Chrome · Windows", 메일 프로그램은 "—". 나라 · 지역은 보이지 않는다.
|
||||
- 끊긴 브라우저 · 앱은 다음 요청에서 바로 로그인 화면으로 간다(열린 실시간 알림도 30초 안에) — 계약 5절 "로그인된 기기".
|
||||
- **최근 로그인**: 「최근 로그인」 + "최근 90일 · 웹메일, ARA SYSTEM 앱, 메일 프로그램" · 오른쪽 전환 [전체 · 실패만](`.segmented--outline` — role=group + `aria-pressed`). 표: 시각 · 방식 · IP · 기기 · 결과.
|
||||
- 방식: "웹메일" / "ARA SYSTEM 앱" / "메일 프로그램 · IMAP" · "메일 프로그램 · SMTP" + 앱 비밀번호로 들어왔으면 아래 줄 "앱 비밀번호 ‘회사 PC 아웃룩’"(지운 앱 비밀번호도 그 이름 그대로).
|
||||
- 기기: 웹메일은 "Chrome · Windows", 앱은 운영체제만 "Android"(방식 칸이 이미 앱 이름이고 기기 칸 120 에서 잘리지 않게 — 모르면 "—"), 메일 프로그램은 "—". 나라 · 지역은 보이지 않는다.
|
||||
- 결과: 「성공」 · 「2단계 인증으로 성공」 · 「비밀번호가 틀려 실패」 · 「코드가 틀려 실패」 · 「앱 비밀번호가 필요해 실패」(2단계를 켠 계정에 메일 프로그램이 웹메일 비밀번호로 연결하려 함). 실패는 빨간 굵은 글자만 — 뱃지 없음.
|
||||
- 최근 순서 · 20개씩 [더 보기](44). 「실패만」이면 "최근 90일 · 실패 4번", [더 보기]도 실패만 이어서.
|
||||
- 빈 상태(실패만): "최근 90일 동안 실패한 로그인이 없습니다" / "[전체]를 누르면 성공한 로그인도 볼 수 있습니다." 전체가 비었으면 "이 기능이 생긴 날부터 기록이 쌓입니다."
|
||||
@@ -838,7 +838,7 @@ API 와 서버 규칙은 계약 2절 "실시간 알림".
|
||||
- 바꾼 뒤 넘기지 못함(새 토큰 받기 `/auth/refresh` 가 장애) — 먼저 `me`(세션 표의 지금 값)를 묻는다. 제한이 남아 있으면 그 단계로, 없으면 가려던 주소로.
|
||||
`me` 도 받지 못하면 이 단계에 머물며 위쪽 빨간 상자에 사전 문구 + [다시 시도](36) — 칸 · 단추는 막고 [다른 계정으로 로그인]만 누를 수 있다. 2단계 켜기 단계도 같다.
|
||||
- **끝나면**: 서버가 제한을 지우고 화면은 `/auth/refresh` 로 새 토큰을 받아 받은편지함으로 간다(로그인 전에 가려던 주소가 있으면 그곳) — 알림 "새 비밀번호를 정했습니다."
|
||||
2단계 인증이 필수인데 켜지 않았으면 받은편지함 대신 「2단계 인증 켜기」 단계로. 다른 곳의 웹메일 로그인 끊기 · 앱 비밀번호 지우기는 설정 › 비밀번호와 같다(계약 5절) — 맨 아래 안내 줄이 그 까닭이다.
|
||||
2단계 인증이 필수인데 켜지 않았으면 받은편지함 대신 「2단계 인증 켜기」 단계로. 다른 곳의 로그인(웹메일 · ARA SYSTEM 앱) 끊기 · 앱 비밀번호 지우기는 설정 › 비밀번호와 같다(계약 5절) — 맨 아래 안내 줄이 그 까닭이다.
|
||||
|
||||
### 로그인 — 2단계 인증 켜기(필수) (`LoginTwoFactorSetup`)
|
||||
|
||||
@@ -862,7 +862,7 @@ API 와 서버 규칙은 계약 2절 "실시간 알림".
|
||||
- **상태**:
|
||||
- 코드 틀림 — 칸 아래 `AUTH_006` 사전 문구, 칸을 비우고 초점. QR · 키는 그대로라 다시 찍지 않는다.
|
||||
- 시간이 지남 · 그사이 바뀜(`AUTH_008`) — 위쪽 빨간 상자에 사전 문구("2단계 인증 설정이 그사이 바뀌었거나 시간이 지났습니다. 처음부터 다시 해 주세요.") + 새 QR · 키로 다시 그린다(코드 칸은 비움).
|
||||
다른 곳에서 이미 켰으면 이 단계를 건너뛰고 받은편지함으로.
|
||||
다른 곳에서 이미 켰으면 이 세션은 서버가 끊는다(계약 6절 — 2단계를 거치지 않은 세션) — 다음 요청에서 세션 만료로 로그인 화면에 돌아가고, 다시 로그인하면 코드 단계다(2026-11-06 — 앞에 적었던 "건너뛰고 받은편지함으로"는 서버가 먼저 끊어 일어나지 않는다).
|
||||
- ③ 지금 비밀번호가 빔 — 보내지 않고 그 칸의 안내 글이 그대로 빨간 오류가 된다(새 비밀번호 단계의 임시 비밀번호와 같다).
|
||||
- 지금 비밀번호 틀림(③ 이 있을 때) — 칸 아래 `AUTH_005`. 둘 다 틀렸으면 코드부터(서버가 코드를 먼저 본다 — 12단계). 화면 메모리에서 쓴 비밀번호가 거절되면 기억을 지우고 ③ 칸을 이 오류와 함께 보인다.
|
||||
- 시도 과다 — 위쪽 상자에 `REQ_001`(코드 실패는 같은 주소 15분 5회 · 하루 20회 — 12단계 그대로).
|
||||
@@ -990,7 +990,7 @@ API 와 서버 규칙은 계약 2절 "실시간 알림".
|
||||
- **① 관리자가 비밀번호를 재설정했습니다** — 운영 도구의 「비밀번호 재설정」 뒤.
|
||||
- 제목 "관리자가 비밀번호를 재설정했습니다"
|
||||
- 인사 "{이름} 님, 관리자가 비밀번호를 임시 비밀번호로 바꿨습니다."
|
||||
- 표: 시각 — `{시각}` / 바뀐 것 — "비밀번호가 임시 비밀번호로 바뀌었고, 처음 로그인할 때 새 비밀번호를 정하게 됩니다" / 로그아웃 — "모든 기기와 브라우저의 웹메일에서 로그아웃되었습니다" /
|
||||
- 표: 시각 — `{시각}` / 바뀐 것 — "비밀번호가 임시 비밀번호로 바뀌었고, 처음 로그인할 때 새 비밀번호를 정하게 됩니다" / 로그아웃 — "모든 기기의 웹메일과 앱에서 로그아웃되었습니다"(2026-11-06 — 앱 세션도 끊는다) /
|
||||
앱 비밀번호 — "모두 지워져서 메일 프로그램은 새 앱 비밀번호로 다시 연결해야 합니다"
|
||||
- 할 일 "아웃룩이나 휴대폰 메일 앱을 쓰면 메일 프로그램마다 앱 비밀번호가 필요합니다. 웹메일의 **[설정] → [앱 비밀번호]**에서 새로 만들어 주세요."
|
||||
- 확인 문장 "직접 요청한 일이 아니면 바로 IT 담당에게 알려 주세요."
|
||||
|
||||
+30
-23
@@ -78,7 +78,7 @@
|
||||
두 번 이상 지난 토큰이나 30초가 지난 이전 토큰은 위 규칙대로 세션을 끊는다.
|
||||
- 세션은 로그인 후 **최대 30일**(절대 만료)이다. 그 안에서도 리프레시 토큰을 14일 동안 쓰지 않으면 끝난다.
|
||||
- 액세스 토큰은 서버 조회 없이 검증한다. 그래서 세션을 끊어도 이미 발급된 액세스 토큰은 만료(최대 1시간)까지 유효하다.
|
||||
→ 12단계부터는 끊은 세션을 바로 막는다(액세스 토큰의 세션 id + 서버가 기억한 끊은 세션 — 5절 "로그인된 브라우저").
|
||||
→ 12단계부터는 끊은 세션을 바로 막는다(액세스 토큰의 세션 id + 서버가 기억한 끊은 세션 — 5절 "로그인된 기기").
|
||||
|
||||
### `POST /api/auth/logout`
|
||||
|
||||
@@ -131,22 +131,24 @@
|
||||
성공하면 복원 완료, 실패하면 로그아웃 상태로 시작한다.
|
||||
→ 백엔드는 **액세스 쿠키가 없거나 만료돼도** 리프레시 쿠키만으로 `/auth/refresh` 가 동작해야 한다.
|
||||
|
||||
### 앱 로그인 (앱 전용 토큰 경로) — 앱 1단계 (2026-11-05 정함 · **서버 미구현**)
|
||||
### 앱 로그인 (앱 전용 토큰 경로) — 앱 1단계 (2026-11-05 정함 · 2026-11-06 서버)
|
||||
|
||||
> 2026-11-05 사용자가 앱 전용 토큰 경로를 정했다(웹의 쿠키를 앱에 쓰지 않는다). 앱(`flutter/`)은 이 절대로 만들고 가짜 서버로 시험한다 — 서버는 다음 차례에 이 절대로 만든다.
|
||||
> 2026-11-05 사용자가 앱 전용 토큰 경로를 정했다(웹의 쿠키를 앱에 쓰지 않는다). 앱(`flutter/`)과 서버(`modules/auth` 의 `app-auth.controller.ts`)가 이 절대로 만들었다.
|
||||
> **세션 규칙은 웹과 같다**: 1회용 회전 · 해시만 저장 · 재사용 감지 · 30초 유예 · 끊은 세션은 `sid` 로 바로 막기 · 절대 30일 · 미사용 14일 · 로그인 실패 카운터 · 2단계 코드 가드 · 앱 비밀번호 모양 거절 · 제한된 세션(6절). 다른 것은 토큰을 나르는 길뿐이다.
|
||||
|
||||
| 경로 | 본문 | 응답 |
|
||||
| --- | --- | --- |
|
||||
| `POST /api/auth/app/login` | 웹 login 과 같다(`email` · `password` · `otpCode?`) | 웹 login 응답과 같은 평평한 모양(`status` · `email` · `name` · `restriction` · `twoFactorDeadline`) + `tokens`. `signedIn`(웹이 쿠키를 심는 바로 그 경우)에만 `tokens` 가 차고, 그 밖(`twoFactorRequired` 등)은 `tokens: null` |
|
||||
| `POST /api/auth/app/refresh` | `{ refreshToken }` | 웹 refresh 와 같은 칸(`email` · `name` · `restriction` · `twoFactorDeadline` — 6절: 늘 싣는다) + `tokens`. 30초 유예 안의 다시 쓰기는 웹처럼 새 토큰 없이 200(`tokens: null`). 끝난 세션 · 틀린 토큰은 401 `AUTH_001` |
|
||||
| `POST /api/auth/app/logout` | `{ refreshToken? }`(보안 저장소를 읽지 못했으면 빈 본문 `{}`) + 머리 `Authorization: Bearer`(있으면) | 웹 logout 과 같다 — 리프레시 토큰으로 세션을 찾고, 없거나 틀리면 액세스 토큰의 `sid` 로 끝낸다. 어느 쪽도 맞지 않아도 성공으로 답한다 |
|
||||
| `POST /api/auth/app/refresh` | `{ refreshToken }`(글 — 없거나 글이 아니면 400 `VAL_001`) | 웹 refresh 와 같은 칸(`email` · `name` · `restriction` · `twoFactorDeadline` — 6절: 늘 싣는다) + `tokens`. 30초 유예 안의 다시 쓰기는 웹처럼 새 토큰 없이 200(`tokens: null`) — **앱 세션은 그때 서버가 그 세션을 끝낸다**(아래). 끝난 세션 · 틀린 토큰(빈 글 · 2,048자 넘는 글 포함 — 서명을 보기 전에)은 401 `AUTH_001` |
|
||||
| `POST /api/auth/app/logout` | `{ refreshToken? }`(보안 저장소를 읽지 못했으면 빈 본문 `{}`) + 머리 `Authorization: Bearer`(있으면) | 웹 logout 과 같다 — 리프레시 토큰으로 세션을 찾고, 없거나 틀리면(빈 글 · 2,048자 넘는 글 포함) 액세스 토큰의 `sid` 로 끝낸다(액세스 토큰은 만료됐어도 서명이 맞으면 — 세션을 끝내기만 한다). 어느 쪽도 맞지 않아도 200. 본문 모양이 틀리면(`refreshToken` 이 글이 아님 · 모르는 칸) 400 `VAL_001` |
|
||||
|
||||
- `tokens = { accessToken, accessExpiresAt, refreshToken, refreshExpiresAt }`(시각은 ISO 8601 UTC) — 토큰이 든 응답은 `Cache-Control: no-store`.
|
||||
- 보호 API 는 `Authorization: Bearer <accessToken>`. **앱 토큰에만 표시 `cli: 'app'`** 를 싣고, Bearer 는 그 표시가 있는 토큰만 · 쿠키는 표시가 없는(웹) 토큰만 받는다(서로 바꿔 쓰지 못하게). `GET /api/auth/me` · 6절의 제한 허용 경로도 Bearer 로 그대로 — 제한(`rst`)과 403 `AUTH_010` 도 웹과 같다.
|
||||
- 실패 카운터 · 요청 수 한도 · 2단계 코드 가드 · 앱 비밀번호 모양 거절은 웹 login 과 같은 함수를 지난다(앱 경로가 한도를 비켜 가지 않게). 출처 검사(OriginGuard)는 앱(`Origin` · `Sec-Fetch-Site` 없음)을 지금 규칙 그대로 통과시킨다.
|
||||
- `tokens = { accessToken, accessExpiresAt, refreshToken, refreshExpiresAt }`(시각은 ISO 8601 UTC) — 로그인 · 갱신 응답은 `Cache-Control: private, no-store`(처리기가 낸 오류 응답도 — 가드 · 본문 파서가 먼저 막은 응답(403 출처 · 429 한도 · 400 · 413 · 415)에는 토큰이 없어 이 머리가 없다).
|
||||
- 보호 API 는 `Authorization: Bearer <accessToken>`. **앱 토큰에만 표시 `cli: 'app'`** 를 싣고, Bearer 는 그 표시와 `sid` 가 있는 토큰만 · 쿠키는 표시가 없는(웹) 토큰만 받는다(서로 바꿔 쓰지 못하게 — 리프레시 토큰도, 세션 표의 `client` 도 맞아야 한다. 다르면 401 이고 재사용으로 보지 않는다). `Authorization: Bearer` 가 있으면 그것만 보고 쿠키는 보지 않는다. `GET /api/auth/me` · 6절의 제한 허용 경로도 Bearer 로 그대로 — 제한(`rst`)과 403 `AUTH_010` 도 웹과 같다.
|
||||
- 실패 카운터 · 요청 수 한도 · 2단계 코드 가드 · 앱 비밀번호 모양 거절은 웹 login 과 같은 함수를 지난다(앱 경로가 한도를 비켜 가지 않게 — 로그인 요청 수 한도는 웹 · 앱이 IP 마다 1분 60회 한 칸을 함께 쓴다). 출처 검사(OriginGuard)는 앱(`Origin` · `Sec-Fetch-Site` 없음)을 지금 규칙 그대로 통과시킨다.
|
||||
- **"지금 세션"**: 앱 요청은 리프레시 쿠키가 없으므로 액세스 토큰의 `sid` 로 안다 — `PUT /api/auth/password`(이 세션만 남기기) · `/api/auth/sessions`(isCurrent · 다른 곳 모두 끊기) · 앱 logout.
|
||||
- 세션 표에 `client`(`web` | `app`) 칸을 둔다. 앱은 User-Agent `ARASYSTEM-App/<앱 판> (Android <판>; <기종>)` 을 보내고, 서버는 로그인된 기기 · 로그인 기록에 "ARA SYSTEM 앱 · Android" 꼴로 보이고 로그인 기록 `method` 는 `app` 이다.
|
||||
- 세션 표에 `client`(`web` | `app`) 칸을 둔다(마이그레이션 `AddAppSessionClient` — 기본 `web`). 앱은 User-Agent `ARASYSTEM-App/<앱 판> (Android <판>; <기종>)` 을 보내고, 서버는 로그인된 기기 · 로그인 기록에 "ARA SYSTEM 앱 · Android" 꼴로 보이고 로그인 기록 `method` 는 `app` 이다.
|
||||
**앱 이름은 진짜 클라이언트(세션의 `client` · 로그인 기록의 `method`)로만 붙인다** — 웹 세션이 앱 꼴 User-Agent 를 보내도 앱 이름을 받지 않는다(비밀번호를 훔친 사람이 직원 본인의 앱처럼 보이지 않게). 세션 목록 항목에 `client` 칸을 싣는다(5절).
|
||||
- **앱 세션의 갱신 유예는 그 세션을 끝낸다**(2026-11-06 리뷰): 앱은 갱신을 한 번에 하나만 하므로 앱 세션의 유예는 "회전 응답을 잃음" 아니면 "훔친 토큰을 누가 먼저 씀"이다. 서버는 그 세션을 끊고(사유 `app-refresh-grace`) 200 `tokens: null` 로 답한다 — 앱이 다음 실행 직후라 조용한 logout 을 보내지 못해도 남는 세션이 없다. 웹의 유예는 그대로다(탭 여러 개).
|
||||
|
||||
**앱이 할 일**
|
||||
- 리프레시 토큰은 `flutter_secure_storage`(키 `kRefreshTokenKey`)에만, 액세스 토큰은 메모리에만 둔다. 새 리프레시 토큰은 받자마자 저장한다 — 회전 직후 앱이 죽으면 다음 실행의 옛 토큰이 30초 뒤 재사용으로 세션을 끊는다(알려진 한계).
|
||||
@@ -154,7 +156,9 @@
|
||||
- 보호 API 의 401 이면 갱신을 **한 번에 하나**(동시 401 을 한 갱신에 묶는다) → 원래 요청 한 번 다시. 앱 login · refresh · logout 은 갱신하지 않는다. 갱신의 401 이면 토큰을 지우고 `AUTH_002`(클라이언트 전용) + 로그인 화면, 5xx · 연결 끊김은 세션을 그대로 둔다.
|
||||
`accessExpiresAt` 30초 전에는 미리 갱신한다.
|
||||
- **유예 응답(`tokens: null`)은 세션 끝으로 다룬다** — 앱은 갱신을 한 번에 하나만 하므로 그 응답은 "앞 갱신의 응답을 잃었다"는 뜻이고 이어 쓸 리프레시 토큰이 없다(서버는 해시만 두어 같은 토큰을 다시 줄 수 없다).
|
||||
메모리의 액세스 토큰이 아직 있으면 그것을 Bearer 로 실어 조용히 앱 logout 을 부른다 — 서버의 그 세션이 "로그인된 기기"에 14일 남지 않게(`sid` 로 끝낸다).
|
||||
메모리의 액세스 토큰이 아직 있으면 그것을 Bearer 로 실어 조용히 앱 logout 을 부른다(서버도 그 세션을 이미 끝냈다 — 남는 것이 없게 두 겹).
|
||||
나중에 푸시를 따로 받는 백그라운드 작업이 생겨도 갱신은 앱과 같은 자물쇠를 함께 쓴다 — 유예가 세션 끝이므로 둘이 따로 갱신하면 서로의 세션을 끊는다.
|
||||
- **알려진 한계**: 회전 응답을 잃고 30초가 지난 뒤 같은 토큰을 다시 보내면 재사용으로 세션이 끊긴다(서버 로그에 탈취 의심 warn) — 모바일 망에서는 웹보다 잦을 수 있다.
|
||||
- `AUTH_010` 은 알림 없이 me 를 다시 받아 그 단계로 — me 에 제한이 없으면 웹처럼 갱신한 뒤 원래 요청을 한 번 다시 보낸다.
|
||||
- 로그아웃 · 다른 계정으로 로그인 · 세션 끝 뒤에 도착한 응답(로그인 · 코드 · 갱신 · me)은 쓰지 않는다 — 버린 로그인 결과에 토큰이 있으면 그 토큰으로 조용히 앱 logout 을 부른다(웹의 "로그아웃 뒤 늦은 갱신"과 같은 결).
|
||||
- 코드 단계 · 제한 단계에서 다시 쓸 비밀번호는 화면 메모리에만 10분 — 성공 · 다른 계정 · 떠남 · 제한이 풀릴 때 지운다(웹과 같다).
|
||||
@@ -861,7 +865,7 @@ data: {"reason":"token-expiring"}
|
||||
그래서 서버와 엔진 사이가 10분 넘게 끊겼다 다시 이으면, 그동안 온 메일은 `state` 로만 알린다(목록에는 나타나고 바탕화면 알림은 없다).
|
||||
"보낸 사람이 나"는 토큰의 주소와 발신 ID(별칭) 주소로 본다(대소문자 무시).
|
||||
- 오래 열린 엔진 연결은 엔진의 동시 요청 한도(4)를 잡아먹지 않는다 — 한 직원의 스트림 8개를 연 채 그 직원 · 다른 직원의 보통 요청과 동시 요청 6개가 모두 됐다(2026-10-28 확인).
|
||||
- 연결은 액세스 토큰이 끝날 때 닫는다(최대 1시간 — 그 전에 `end`). 12단계부터 로그아웃 · 비밀번호 변경 · 세션 끊기로 끊은 세션의 연결은 다음 ping 때 `end` 없이 닫는다(계정을 더 쓸 수 없을 때와 같은 길 — 5절 "로그인된 브라우저").
|
||||
- 연결은 액세스 토큰이 끝날 때 닫는다(최대 1시간 — 그 전에 `end`). 12단계부터 로그아웃 · 비밀번호 변경 · 세션 끊기로 끊은 세션의 연결은 다음 ping 때 `end` 없이 닫는다(계정을 더 쓸 수 없을 때와 같은 길 — 5절 "로그인된 기기").
|
||||
서버가 종료될 때는 열린 연결을 `end` 로 먼저 닫는다 — 오래 열린 연결이 종료를 붙잡지 않게.
|
||||
알려진 한계: 잠자기 · VPN 재연결 · 망 바꿈으로 반쯤 열린 연결은 서버가 ping 을 쓰다 실패할 때(OS 기본 15분 안팎)까지 자리를 잡는다 —
|
||||
그사이 그 직원의 연결이 5개에 닿으면 새 연결은 429 이고 화면은 1분 확인으로 이어진다.
|
||||
@@ -2119,7 +2123,7 @@ interface FileUploadCreate {
|
||||
| 등록 시간 | 등록을 시작하고 10분 안에 켠다. 직원마다 기다리는 등록은 하나다(새로 시작하면 앞의 것은 버린다) |
|
||||
| 앱 비밀번호 | 엔진 상한(기본 5개 — `x:Authentication.maxAppPasswords`). 이름 1~40자(앞뒤 공백을 지운 값, JS 문자열 길이). 권한은 메일 프로그램만 |
|
||||
| 로그인 기록 | 90일 보존(날마다 정리) |
|
||||
| 로그인된 브라우저 | 끝나지 않은 웹메일 세션과 앱 세션(같은 세션 표 — 1절 "앱 로그인"의 `client`) — 1절 세션 규칙 그대로(절대 30일 · 미사용 14일). 앱 서버를 만들 때 화면 이름도 "로그인된 기기"로 함께 정한다 |
|
||||
| 로그인된 기기 | 끝나지 않은 웹메일 세션과 앱 세션(같은 세션 표 — 1절 "앱 로그인"의 `client`) — 1절 세션 규칙 그대로(절대 30일 · 미사용 14일). 화면 이름은 앱 서버와 함께 "로그인된 브라우저"에서 바꿨다(2026-11-06) |
|
||||
|
||||
### 로그인 (`POST /api/auth/login`) — 1절에 더하는 것
|
||||
|
||||
@@ -2258,14 +2262,15 @@ interface FileUploadCreate {
|
||||
엔진은 지운 번호를 다시 쓴다 — 서버 기록은 같은 번호 · 같은 `createdAt` 일 때만 잇는다(만들 때 서버가 기록을 새로 둔다).
|
||||
- 지운 앱 비밀번호로 이미 열린 IMAP 연결은 엔진이 끊지 않는다(알려진 한계 — 다음 연결부터 막힌다).
|
||||
|
||||
### 로그인된 브라우저 (`/api/auth/sessions`)
|
||||
### 로그인된 기기 (`/api/auth/sessions`)
|
||||
|
||||
경로가 `/api/auth` 아래인 까닭: 리프레시 쿠키(Path=`/api/auth`)로 이 요청의 세션을 안다(비밀번호 바꾸기와 같다).
|
||||
경로가 `/api/auth` 아래인 까닭: 리프레시 쿠키(Path=`/api/auth`)로 이 요청의 세션을 안다(비밀번호 바꾸기와 같다 — 앱은 액세스 토큰의 `sid`).
|
||||
12단계에 "로그인된 브라우저"로 만들었고, 앱 세션이 함께 들어가 2026-11-06 화면 이름을 바꿨다.
|
||||
|
||||
```jsonc
|
||||
// GET /api/auth/sessions → 200 (지금 세션이 맨 앞, 그다음 최근 활동 차례)
|
||||
{ "items": [ { "id": "…", "createdAt": "…", "lastActiveAt": "…", "ip": "211.186.95.252",
|
||||
"device": { "browser": "Chrome", "os": "Windows" }, "isCurrent": true } ],
|
||||
"device": { "browser": "Chrome", "os": "Windows" }, "client": "web", "isCurrent": true } ],
|
||||
"pageInfo": { "hasNext": false, "nextCursor": null, "total": 3 } }
|
||||
|
||||
// DELETE /api/auth/sessions/{id} → 200 (data: null)
|
||||
@@ -2277,7 +2282,9 @@ interface FileUploadCreate {
|
||||
- `ip` · `device` 는 로그인 · 갱신 때 서버가 둔다.
|
||||
- IP 는 `TRUST_PROXY` 기준 클라이언트 IP 다.
|
||||
- 브라우저는 서버가 User-Agent 를 짧은 이름(`Edge` · `Chrome` · `Firefox` · `Safari` · `Whale` · `Samsung Internet` / `Windows` · `macOS` · `iOS` · `Android` · `Linux` · `ChromeOS`)으로 바꾼다 — 서버 순수 함수 하나, 모르면 `null`.
|
||||
앱 세션(User-Agent `ARASYSTEM-App/<판> (Android <판>; <기종>)` — 1절 "앱 로그인")은 `ARA SYSTEM 앱` · `Android` 다.
|
||||
앱 세션(`client: "app"` — 1절 "앱 로그인")은 브라우저가 `ARA SYSTEM 앱`, 운영체제는 User-Agent `ARASYSTEM-App/<판> (Android <판>; <기종>)` 의 괄호 첫 칸(`Android` · `iOS` — 모르거나 User-Agent 가 없으면 `null`)이다.
|
||||
**앱 이름은 `client` 로만 붙인다** — 웹 세션이 앱 꼴 User-Agent 를 보내면 브라우저는 `null` 이다(꾸민 User-Agent 로 직원 본인의 앱처럼 보이지 않게).
|
||||
- `client`: `"web"` | `"app"`(2026-11-06 더함 — 화면은 이것으로 앱 세션을 세고 휴대폰 아이콘을 보인다).
|
||||
- 12단계 전에 연 세션은 둘 다 `null` 이다.
|
||||
- **끊은 세션은 바로 막는다.** 액세스 토큰에 세션 id(`sid`)를 싣는다. 서버는 끊은 세션 id 를 1시간(액세스 토큰의 가장 긴 수명) 기억하고, 가드가 그 토큰을 401 `AUTH_001` 로 막는다 — DB 를 묻지 않는다.
|
||||
- 끊긴 브라우저는 다음 요청에서 갱신이 실패해(세션이 끊김) 로그인 화면으로 간다. 열린 실시간 알림 연결은 다음 ping(25초) 때 끝낸다.
|
||||
@@ -2303,7 +2310,7 @@ interface FileUploadCreate {
|
||||
|
||||
| 칸 | 값 |
|
||||
| --- | --- |
|
||||
| `method` | `webmail` · `app`(앱 로그인 — 1절 "앱 로그인", 서버를 만들 때 더한다) · `imap`(메일 프로그램 받기) · `smtp`(메일 프로그램 보내기). 웹은 모르는 값을 그대로 보이므로 `app` 을 더할 때 화면의 이름 글도 함께 더한다 |
|
||||
| `method` | `webmail` · `app`(앱 로그인 — 1절 "앱 로그인", 2026-11-06 — 기기는 앱 세션과 같은 규칙) · `imap`(메일 프로그램 받기) · `smtp`(메일 프로그램 보내기) |
|
||||
| `isTwoFactor` | 웹메일 · 앱 성공이 코드까지 거쳤는지 |
|
||||
| `failureReason` | `wrongPassword`(비밀번호 틀림 — 웹메일에 넣은 앱 비밀번호 포함) · `wrongCode`(코드 틀림 · 다시 쓴 코드) · `appPasswordRequired`(2단계를 켠 계정에 메일 프로그램이 비밀번호로 — 엔진 `auth.mfa-required`) |
|
||||
| `appPassword` | 메일 프로그램이 앱 비밀번호로 들어온 경우. `name` 은 그때의 이름이다(지운 뒤에도 남는다) |
|
||||
@@ -2392,7 +2399,7 @@ interface FileUploadCreate {
|
||||
- 2단계를 켜거나 앱 비밀번호를 지워도 이미 열린 IMAP 연결은 엔진이 끊지 않는다(지운 앱 비밀번호의 연결은 다음 명령에서 멈춘다).
|
||||
- 웹훅이 잃는 기록이 있다(엔진 재시작 · 서버가 5분 넘게 받지 못함 · 한도를 넘는 묶음). 같은 초 · 같은 연결의 똑같은 이벤트는 하나로 합쳐진다.
|
||||
- 성공에 쓴 코드는 2분 동안 다시 받지 않으므로, 30초 안에 두 곳에서 연달아 로그인하면 두 번째는 `AUTH_006` 이고 다음 코드를 기다려야 한다.
|
||||
- 로그인 기록의 `appPassword.name` 은 웹메일 밖(엔진 관리 화면)에서 만든 앱 비밀번호면 `null` 이다. `device` 는 웹메일 기록에만 있고, User-Agent 가 없거나 알아보지 못하면 `null` 이다.
|
||||
- 로그인 기록의 `appPassword.name` 은 웹메일 밖(엔진 관리 화면)에서 만든 앱 비밀번호면 `null` 이다. `device` 는 웹메일 · 앱 기록에만 있고(앱은 로그인된 기기와 같은 규칙), User-Agent 가 없거나 알아보지 못하면 `null` 이다.
|
||||
- 끊은 세션 `DELETE /api/auth/sessions/{id}` 의 모양이 틀린 id 는 404 `RES_001`(없는 것과 같다).
|
||||
- 엔진 관리 화면에서 켠 2단계 인증은 `enabledAt` 이 없다.
|
||||
- 복구 코드가 없다 — 휴대폰을 잃으면 운영자에게 문의한다.
|
||||
@@ -2408,7 +2415,7 @@ interface FileUploadCreate {
|
||||
2. 코드 실패 한도는 같은 주소 15분 5회 · 24시간 20회다(위 "한도 · 규칙"). 비밀번호를 아는 공격자가 1년 내내 시도해도 맞힐 확률이 약 2% 다.
|
||||
3. 2단계를 켜거나 꺼도 다른 세션을 끊지 않는다 — 로그인 기록 화면에서 따로 끊는다.
|
||||
4. 비밀번호를 바꾸면 앱 비밀번호도 모두 지운다(위 "비밀번호 바꾸기").
|
||||
5. 다른 곳 로그아웃은 바로 적용한다 — 끊은 세션 id 를 가드가 막는다(위 "로그인된 브라우저"). 로그아웃 · 비밀번호 바꾸기의 세션 끊기도 같다.
|
||||
5. 다른 곳 로그아웃은 바로 적용한다 — 끊은 세션 id 를 가드가 막는다(위 "로그인된 기기"). 로그아웃 · 비밀번호 바꾸기의 세션 끊기도 같다.
|
||||
|
||||
---
|
||||
|
||||
@@ -2554,18 +2561,18 @@ interface FileUploadCreate {
|
||||
| 퇴사 마무리 | 잠긴 계정만. 엔진 계정 지우기(메일 영구 삭제 — 주소를 다시 쳐서 확인) + 우리 DB 의 그 주소 행 정리 — 같은 주소를 다시 쓸 때 섞이지 않게 |
|
||||
| 링크 끊기 | 신고된 공개 링크 주소로 → `revoked_by = operator` |
|
||||
| 공용 주소 | MailingList 만들기 · 구성원 넣기 / 빼기 · 목록(구성원 받은편지함으로 복사) |
|
||||
| 현황 | 직원마다 잠금 · 2단계 · 앱 비밀번호 수 · 마지막 웹메일 로그인 · 메일함 사용량 · 이전 중(끝나는 날) |
|
||||
| 현황 | 직원마다 잠금 · 2단계 · 앱 비밀번호 수 · 마지막 로그인(웹메일 · 앱 — 2026-11-06 부터 앱도 센다) · 메일함 사용량 · 이전 중(끝나는 날) |
|
||||
| 정책 · 설정 | 2단계 필수(켜기 · 시작일 · 예외), 문의 연락처 |
|
||||
| 메일 이전 (2026-11-02 더함) | 이전 현황(이전 중인 계정 · 끝나는 때 · 메일함 사용량) · 이전용 비밀번호 다시 만들기(한 사람 또는 이전 중인 계정 모두 — 잃었거나 30일을 늘릴 때) · 이전 마치기(한 사람 또는 이전 중인 계정 모두 → 직원용 임시 비밀번호) |
|
||||
|
||||
- 차례가 있는 명령
|
||||
- 비밀번호 재설정: 앱 비밀번호 지우기(대리 접근) → 웹메일 세션 끊기(DB) → 알림 메일 넣기(대리 접근) → 엔진에 임시 비밀번호 + 만료 → 세션 한 번 더 끊기.
|
||||
- 비밀번호 재설정: 앱 비밀번호 지우기(대리 접근) → 웹메일 · 앱 세션 끊기(DB) → 알림 메일 넣기(대리 접근) → 엔진에 임시 비밀번호 + 만료 → 세션 한 번 더 끊기.
|
||||
만료 뒤에는 대리 접근 메서드가 막히고, 알림이 말하는 일(로그아웃 · 앱 비밀번호 지움)은 넣기 전에 끝나 있어야 하기 때문이다. 첫 세션 끊기가 실패하면 알림을 넣기 전에 멈추고,
|
||||
엔진 바꾸기가 실패하면 넣은 알림 메일을 지운다. 마지막 끊기는 그사이 옛 비밀번호로 새로 로그인한 세션을 위한 것이다.
|
||||
- 잠금: 받는 메일 넘기기 · 자동 응답(대리 접근으로 Sieve)을 먼저 걸고 → `authenticate` 빼기 → 세션 끊기. 뺀 뒤에는 대리 접근이 막혀 Sieve 를 새로 걸 수 없다(이미 건 Sieve 는 계속 돈다).
|
||||
- 퇴사 마무리: 엔진 계정을 지운 뒤 우리 DB 에서 그 주소의 행을 한 트랜잭션으로 지운다 — 로그인 기록 · 앱 비밀번호 사용 · 2단계 설정 · 서명 · 설정 · 사내 목록 기록 · 대용량 파일 · 링크,
|
||||
그리고 2단계 필수의 예외 주소 목록과 공용 주소(MailingList)의 구성원에서도 뺀다(실행 전 보여 주는 바뀔 것에 든다).
|
||||
세션 행은 바로 지우지 않는다 — 끊은 기록을 서버가 15초 동기화로 읽어야 하므로 끝난 세션 정리 cron 에 맡긴다(로그인된 브라우저 목록은 살아 있는 세션만 보인다).
|
||||
세션 행은 바로 지우지 않는다 — 끊은 기록을 서버가 15초 동기화로 읽어야 하므로 끝난 세션 정리 cron 에 맡긴다(로그인된 기기 목록은 살아 있는 세션만 보인다).
|
||||
디스크 파일(대용량 · 서명 그림)은 그 뒤에 지우고 못 지운 것은 정리 cron 에 맡긴다. 실행 기록(`ops_audit_log`)은 남기고, 읽기만 하는 현황은 기록하지 않는다.
|
||||
- 비밀번호 재설정은 잠긴 계정을 거절한다(대리 접근이 막혀 있다 — 잠금 풀기가 재설정을 함께 권한다). 이미 만료된 계정을 다시 재설정하면 앱 비밀번호 지우기 · 알림 넣기를 건너뛴다
|
||||
(엔진이 막는다 — 엔진 관리 화면에서 만료를 걸어 앱 비밀번호가 남은 드문 경우는 `[못 함]` 으로 알리고 이어 간다).
|
||||
@@ -2593,7 +2600,7 @@ interface FileUploadCreate {
|
||||
비밀번호를 바꾸기 전에 그 주소의 웹메일 세션을 끊는다 — 임시 비밀번호(만료)로 들어와 새 비밀번호 단계에 묶인 세션이, 만료가 미래로 바뀌면 다음 갱신에서 제한이 풀리기 때문이다(끊지 못하면 바꾸지 않는다).
|
||||
한 사람일 때는 이전 중이거나 비밀번호가 만료된 계정(30일이 지난 이전 계정 · 아직 바꾸지 않은 임시 비밀번호 — 엔진에서는 가를 수 없다 — 바뀔 것에 "지금 비밀번호는 더 쓰지 못합니다")만 받는다. 만료된 계정은 "모두"에 넣지 않는다.
|
||||
직원이 이미 비밀번호를 정한 계정(만료 없음)은 막는다 — 걸면 직원이 로그인하지 못한다. 잠긴 계정 · 2단계 인증을 켠 계정(비밀번호로는 IMAP 에 들어가지 못한다)도 막는다.
|
||||
- 이전 마치기의 차례(계정마다): 앱 비밀번호가 있으면 지우기 → 웹메일 세션 끊기 → **이전 중 로그인 기록 지우기**(우리 DB `login_events` — imapsync 의 IMAP 로그인이 엔진 웹훅으로 쌓인다. 직원이 첫날 모르는 IMAP 로그인 수십 줄을 보지 않게)
|
||||
- 이전 마치기의 차례(계정마다): 앱 비밀번호가 있으면 지우기 → 웹메일 · 앱 세션 끊기 → **이전 중 로그인 기록 지우기**(우리 DB `login_events` — imapsync 의 IMAP 로그인이 엔진 웹훅으로 쌓인다. 직원이 첫날 모르는 IMAP 로그인 수십 줄을 보지 않게)
|
||||
→ 엔진에 직원용 임시 비밀번호 + 만료 → 세션 한 번 더 끊기. **받은편지함 알림 메일은 넣지 않는다**(재설정 알림은 "관리자가 비밀번호를 바꿈"이라 새 직원이 놀란다).
|
||||
한 계정이 실패해도 나머지를 이어 하고, 끝에 주소 · 임시 비밀번호 표를 한 번 보인다(앞에서 만든 것은 실패가 나도 반드시 보인다). 로그인 기록을 지우지 못한 계정은 비밀번호를 바꾸지 않고 멈춘다(넘긴 뒤에는 지울 명령이 없다).
|
||||
잠긴 계정은 마치지 않는다. y/N 뒤 계정마다 다시 읽어 그사이 비밀번호 처지가 바뀌었으면(다른 운영자가 먼저 마침 등) 하지 않는다. 세션 끊는 사유는 운영 도구의 `ended-by-operator` 그대로다.
|
||||
|
||||
Reference in New Issue
Block a user