HBSA100_Signin (운영자 로그인)
(Hereby) 시스템 관리 > [[HBSA000]] > Signin
| 앱 | System Admin (포트 4100) |
| 부모 화면 | [[HBSA000]] |
| 대상 사용자 | 플랫폼 운영자(OPERATOR) — 본인 이메일 계정 |
| 프로토타입 | TBU |
| 관련 정책 | [[POL_권한(RBAC)]] |
| Status | draft (코드 기반 — 라이브 실사 불가: dev 라우트 AccessDenied 격리, 2026-08-25 확인) |
| Owner | 카나 |
개요
IP allowlist 통과 후, 콘솔 JWT가 없을 때 노출되는 로그인 화면(!isLoading && !authenticated). redesign Phase 3에서 env 단일 계정을 폐기하고 운영자 본인 이메일 계정 로그인으로 전환됐다. 검증 = 대상 엔드포인트의 account.login + platform.systemWhoAmI로 OPERATOR 확인.
문서 히스토리
| 날짜 | 작성자 | 팀 | 변경 내용 |
|---|---|---|---|
| 2026-08-25 | 카나 | 콸 | 최초 작성 (redesign PR #583 코드 census 기반, 라이브 미실사) |
| 2026-09-01 | 카나 | PM | 로그인 비밀번호 RSA-OAEP+일회용 챌린지 암호화 반영(cf552d4, 8/29 머지) — 코드 기준 |
| 2026-09-21 | 카나 | PM | 최초 운영자 계정 부트스트랩 단계 신설 — 계정 0개일 때만 로그인 폼 자리에 「최초 운영자 계정 만들기」 노출, Root·SUPER_ADMIN 생성 + 역할·권한 카탈로그 동일 트랜잭션 시드, 409 BOOTSTRAP_NOT_AVAILABLE·Origin 검사·레이트리밋 반영(85df625b, #800) — 커밋 근거, 라이브 실사 이월(TBU) |
| 2026-09-23 | 카나 | PM | 「로그인 상태 유지」 체크박스 신설 — 켜면 유휴 30분 판정을 건너뛰는 30일 persistent 세션 발급(admin_sessions.persistent), 이메일 로그인은 인증 앱 확인 단계·Google 로그인은 /google/start?remember=1 로 선택값 전달, 승인 대기 계정 제외·8시간 재인증 유지(27c905a, #825) — 커밋 근거, 라이브 실사 이월(TBU) |
| 2026-10-02 | 카나 | PM | 잠긴 계정 로그인 안내 분리 — 잠금 중 시도는 423 ACCOUNT_LOCKED "로그인 시도가 너무 많아 계정이 잠시 잠겼어요. {N}분 뒤 다시 시도해 주세요."로 응답, 잠금 중 시도로 잠금이 연장되지 않음·재인증 동일(dbecf4b5, #996) — 커밋 근거, 라이브 실사 이월(TBU) |
Data I/O
Client → Server
| 액션 | 전달 값 | 콘솔 REST | 권한 게이트 |
|---|---|---|---|
| "로그인" 제출 | { identifier(email), password, endpoint? } | POST /api/auth/login | IP allowlist + OPERATOR |
| 세션 확인(init) | 콘솔 JWT | GET /api/auth/me | JWT 유효성 |
Server → Client
| 변수 | 소스 | 값 없을 때 | 비고 |
|---|---|---|---|
token | JWT sign (12h) | - | localStorage 저장 추정(TBU) |
username | operator.email | - | 헤더에 @{username} 표기 |
operator | systemWhoAmI 결과 | - | {lastName+firstName} = operatorName |
loginError | 실패 메시지 | hide | 아래 상태 분기 |
코드 근거: src/views/signin.html, server/index.ts /api/auth/login.
화면 플로우
Signin 사양표
| 번호 | 항목/기능명 | 타입 | 사양 및 기능 설명 | 연관/영향 범위 | 비고 |
|---|---|---|---|---|---|
| 00 | 화면 진입 조건 | - | 조건1 [!isLoading && !authenticated] 시 노출. 조건2 [isLoading] 시 [[HBSA200]] 로딩("연결 중...") 노출, 조건3 [authenticated] 시 [[HBSA200]] Shell | init() GET /api/auth/me | IP allowlist 미통과 시 이 화면 자체 도달 불가(403) |
| 01 | 초기 상태 | Output | 카드 중앙 정렬. 상단 방패 아이콘("ic-shield-check", orange) + 타이틀 "System Admin" + 부제 "운영자 본인 계정으로 로그인하세요" | - | |
| 10 | 이메일 입력 | Input Box | • 라벨 "이메일", type=email, placeholder "operator@example.com"• x-model=loginUsername, required, autocomplete=email• 로딩 중 disabled | loginUsername → identifier | |
| 20 | 비밀번호 입력 | Input Box | • 라벨 "비밀번호", type=password, required, autocomplete=current-password• 로딩 중 disabled • 전송 시 평문이 아니라 RSA-OAEP + 일회용 챌린지로 암호화해 전송( login-crypto.ts 클라이언트·서버 쌍, cf552d4 — 8/29 머지) | loginPassword → 암호화 payload | 인터페이스 계약 변경(8/29) — 코드 기준 |
| 25 | 로그인 상태 유지 | Checkbox | • 로그인 폼에 「로그인 상태 유지」 체크박스 신설 • 켜면 유휴 30분 판정을 건너뛰는 30일 persistent 세션 발급( admin_sessions.persistent, 마이그레이션 1894000000010)• 선택값 전달 경로: 이메일 로그인은 인증 앱 확인 단계( rememberMe), Google 로그인은 /google/start?remember=1 → 쿠키• 승인 대기 계정은 대상 아님, 8시간 재인증은 그대로 유지 | admin_sessions.persistent · [[HBSA200]] 「로그인된 브라우저」 | 신설 (코드 반영: 27c905a, #825) · 인증 앱 확인 단계·Google 로그인 경로는 본 사양서 미기술 — 갱신 대기 · TBU: 라이브 실사 이월 |
| 30 | 로그인 버튼 | Button | • 라벨 "로그인" (orange solid) • 클릭 → doLogin() → POST /api/auth/login• 로딩 중: 스피너 + "로그인 중..." 표시, disabled | doLogin() | 엔드포인트 미지정 시 레지스트리 첫 엔드포인트 대상 |
| 30-Err | ㄴ 에러 배너 | Alert | loginError 존재 시 red 박스. 문구는 상태별 분기(40) | loginError | 실측 토스트 아님, 인라인 배너 |
| 40 | 로그인 결과 분기 | - | case01 [자격증명 오류] → 401 "아이디 또는 비밀번호가 올바르지 않아요." / case02 [OPERATOR 아님] → 403 "시스템 운영자(OPERATOR) 계정이 아니에요." / case03 [엔드포인트 연결불가] → 502 "엔드포인트에 연결할 수 없어요: {message}" / case04 [성공] → JWT 발급 + 그 엔드포인트 세션 setCredentials 자동등록 → Shell | server/index.ts | 입력 누락 시 400 "이메일과 비밀번호를 입력해 주세요." |
| ㄴ41 | 계정 잠김 | - | • 로그인 실패가 누적돼 잠긴 계정의 로그인 시도는 비밀번호가 맞든 틀리든 「잠금」으로 응답(틀린 비밀번호 문구와 구분) - CASE05 [계정 잠김]: 423 ACCOUNT_LOCKED "로그인 시도가 너무 많아 계정이 잠시 잠겼어요. {N}분 뒤 다시 시도해 주세요." (남은 시간 분 단위 올림, 최소 1분)• 잠금 중 시도는 실패 횟수·잠금 시각을 늘리지 않음(예전엔 시도마다 15분씩 연장) — 감사 auth.local.locked_attempt만 기록• 재인증(8시간)도 동일: 감사 auth.local.reauth_locked_attempt | local-auth.service.ts · security/password.ts | 신설 (코드 반영: dbecf4b5, #996) · 없는 이메일·틀린 비밀번호는 기존 401 문구 유지 · TBU: 라이브 실사 이월 |
| 45 | 최초 운영자 계정 만들기 (부트스트랩) | Step | • 컨트롤플레인 DB에 시스템 관리자 계정이 하나도 없을 때만 로그인 폼 자리에 나타나는 「최초 운영자 계정 만들기」 단계 • 그 자리에서 Root · SUPER_ADMIN(global) 계정을 생성. 역할·권한 카탈로그가 비어 있으면 같은 트랜잭션에서 시드 - 효과: 수동 SQL( bootstrap-local.sql)·reconcile CLI 없이 서버 기동 직후 바로 접속해 계정 생성 가능• 진입 판정: init에서 세션이 없을 때만 GET /api/auth/bootstrap/status 조회 → { required } true면 authStep "bootstrap"- required는 계정 0개일 때만 true (삭제된 계정도 점유로 간주)- CASE01 [성공]: 201 생성 → 이메일 프리필로 로그인 화면 복귀 - CASE02 [이미 계정 있음]: 409 BOOTSTRAP_NOT_AVAILABLE• POST /api/auth/bootstrap에 Origin 검사·레이트리밋 적용. 이후 계정은 기존대로 접근 관리에서 생성- 로컬 dev 프록시 changeOrigin 제거(Host가 바뀌어 assertRequestOrigin이 로그인 POST를 ORIGIN_INVALID로 막던 문제 동반 수정) | GET/POST /api/auth/bootstrap · [[HBSA210_Organizations (목록·상세·설정·역할·관리자)]] | 신설 (코드 반영: 85df625b, #800) · TBU: 라이브 실사 이월 |
| 50 | 푸터 | Text | "hereby System Admin" (패밀리 서비스명 미부착 — 마스터 영역 주석) | - | |
| 60 | 권한별 차이 | - | 조건1 [OPERATOR] 로그인 성공 / 조건2 [일반 회원/SUPPORT] → 403 거부(콘솔 로그인은 OPERATOR 게이트). SUPPORT의 콘솔 진입 가능 여부는 코드상 systemWhoAmI OPERATOR 판정만 확인됨 | - | TBU: SUPPORT 콘솔 로그인 허용 여부 |
권한 / 상태 요약
| 시나리오 | 트리거 | 결과 | 비고 |
|---|---|---|---|
case01 [자격증명 오류] | 틀린 이메일/비번 | 401 배너 | |
case02 [비-OPERATOR] | 일반 계정 로그인 | 403 배너 | |
case03 [엔드포인트 다운] | 연결 실패 | 502 배너 | |
case04 [성공] | OPERATOR 검증 | Shell 진입 + 세션 자동연결 | 볼트 설정 시 자격증명 영속 |
CASE05 [계정 잠김] | 잠긴 계정으로 로그인·재인증 | 423 배너 "…{N}분 뒤 다시 시도해 주세요." | 잠금 연장 없음 (#996) |
OQ
- 성공/실패 시 실제 UI 전환·토스트 실측 = TBU (라이브 격리)
- 다중 엔드포인트 환경에서 로그인 대상 엔드포인트 선택 UI 존재 여부 = 코드상 body.endpoint 지원하나 signin.html에 셀렉터 없음 TBU