Crowny Today 회원 인증 구현 (2026-07-17)
개요
today.crowny.org(:9604, 서버=/Users/ef/crowny-today/서버.한선)에 크라우니회원/플렉서블그룹 aimed 회원+관리자 대상 인증을 구현했다.
명부 SSOT 판정 근거
/Users/ef/crowny-auth (users.json, crowny_id/tier): 크라우니 전체 회원 가입 시스템 — aimed 조직 소속 여부 필드 없음. 범용 회원 정본이지 aimed 명부는 아님.
/Users/ef/crowny-flexible/crowny-flex: flex.crowny.org의 "플렉서블그룹" 회원.psv(id|계정|해시|이름|그룹|역할|가입에포크)에 aimed 그룹은 존재하지 않음(grep 0건). 회원 그룹 필드는 "플렉서블" 뿐.
/Users/ef/crowny-erp/instances/aimed/data/조직/조직도.psv: 17명 실제 AIMED 직원 명부(사번/이름/팀/직급/역할/견적권한). 이것이 aimed 조직의 유일한 SSOT — 직급에 "총괄" 포함 2명(E01 이선우/기술팀총괄, E02 이동훈/영업총괄)을 관리자로 판정.
- 결론: aimed 회원 명부 SSOT = ERP aimed 조직도.psv. 단, ERP data/members/.json은 비밀번호 원문/공유 비밀을 노출하지 않아 today가 직접 대조 불가 → today 로컬
data/멤버.psv에 조직도 인원을 부트스트랩 시드(초기비번 aimed2026, 로그인은 사번 id)하는 방식으로 구현. ERP core/libs/인증.한선의 SHA256(id+pw+salt)+토큰=id.해시 패턴을 재사용.
구현 내용
서버.한선 (/Users/ef/crowny-today/서버.한선)
data/멤버.psv (id|pwhash|name|role|team): 부트스트랩 시 ERP 조직도 17명 + admin 계정 시드 (_멤버부트스트랩)
POST /api/login {id, password}: 명부 대조(SHA256 해시) → 토큰 발급 {ok,token,id,name,role}. 토큰=id.SHA256(id+ts+rand+salt)
GET /api/me: Bearer 토큰 검증 → {ok,id,name,role}
- 기존 API 전체(posts/todos/report/sync/briefing)에 Bearer 게이트 적용 (
_인증확인). /health, OPTIONS, /, /api/login은 무인증.
- 게시물 생성 시
name을 클라이언트 JSON 대신 세션의 name으로 서버 강제 — 스푸핑 실측 차단 확인.
data/세션.psv (token|id|role|name|ts), TTL 86400초(24h). 재기동 후에도 파일 기반이라 세션 유지.
GET /: 토큰(Bearer 헤더 또는 crownyTodayToken 쿠키) 없으면 인라인 로그인 HTML, 있으면 /Users/ef/CrownyBrowser/bundle/today/app/index.html을 파일소켓전송으로 스트리밍(4.2MB, 16KB 캡 우회 — 플렉스서버.한선의 파일소켓전송 패턴 재사용).
- 신규 헬퍼:
_한줄트림(한줄트림 builtin 미존재로 자체구현), _베어러(Authorization: Bearer 추출, 통합서버.한선 패턴), _비번해시/_토큰생성/_로그인처리/_세션조회/_인증확인.
브릿지 (/Users/ef/CrownyBrowser/bundle/today/bridge/todayBridge.js)
LS_TOKEN/LS_NAME localStorage 키, getToken/setAuth/clearAuth
showLoginOverlay(): 크림아이보리×다크초콜릿 톤 인라인 로그인 오버레이(아이디/비번) → /api/login → 토큰 저장 → 오버레이 제거 → hydrate/flush
fetchWithTimeout가 모든 fetch에 Authorization: Bearer <token> 자동 첨부, 응답 401 감지 시 토큰 삭제+오버레이 재표시
boot()에서 토큰 없으면 최초 로그인 오버레이 표시
검증 증거 (실측, curl 원문)
$ curl -s -o /dev/null -w "%{http_code}" http://127.0.0.1:9604/api/posts
401
$ curl -s -X POST http://127.0.0.1:9604/api/login -d '{"id":"E01","password":"aimed2026"}'
{"ok":true,"token":"E01.7482b3824d4615d26c7e64394d04de005e0b90a568cf0d8b47174147fc741504","id":"E01","name":"이선우","role":"admin"}
$ curl -s -X POST http://127.0.0.1:9604/api/login -d '{"id":"E05","password":"aimed2026"}'
{"ok":true,"token":"E05.5a78f7d33f78023b1ed2b09348f21024b9c0d3ab5011aa20b843de74cf57837a","id":"E05","name":"김민수","role":"member"}
$ curl -s -o /dev/null -w "wrongpw status=%{http_code}" -X POST http://127.0.0.1:9604/api/login -d '{"id":"E05","password":"WRONG"}'
wrongpw status=401
$ curl -s http://127.0.0.1:9604/api/me -H "Authorization: Bearer $TOKEN_E01"
{"ok":true,"id":"E01","name":"이선우","role":"admin"}
$ curl -s -o /dev/null -w "status=%{http_code}" http://127.0.0.1:9604/api/posts -H "Authorization: Bearer $TOKEN_E01"
status=200
$ curl -s -X POST http://127.0.0.1:9604/api/posts -H "Authorization: Bearer $TOKEN_E01" -d '{"type":"now","name":"홍길동(스푸핑시도)","text":"오늘 인증 테스트"}'
{"ok":true,"id":"P1784260591_2867"}
# 목록 재조회 → name:"이선우" (스푸핑 차단, 서버강제 확인)
# 재기동 후 세션 유지
$ launchctl unload/load org.crowny.today.plist
$ curl -s http://127.0.0.1:9604/api/me -H "Authorization: Bearer $TOKEN_E01"
{"ok":true,"id":"E01","name":"이선우","role":"admin"}
# 게이트웨이(https://today.crowny.org) 경유
$ curl -s -o /dev/null -w "health status=%{http_code}" https://today.crowny.org/health
health status=200
$ curl -s -X POST https://today.crowny.org/api/login -d '{"id":"E05","password":"aimed2026"}'
{"ok":true,"token":"E05.60c192a0e7099ad625d98016bc3ecac08f7d260bc51490f6221616a861d79799","id":"E05","name":"김민수","role":"member"}
$ curl -s -o /dev/null -w "status=%{http_code}" https://today.crowny.org/api/posts -H "Authorization: Bearer $TOKEN_GW"
status=200
$ curl -s -X POST https://today.crowny.org/api/posts -H "Authorization: Bearer $TOKEN_GW" -d '{"type":"now","name":"스푸핑","text":"게이트웨이 경유 인증 검증"}'
{"ok":true,"id":"P1784260668_1126"}
$ curl -s -o /dev/null -w "status=%{http_code}" https://today.crowny.org/api/posts
status=401
잔여 이슈
- 초기 시드 비밀번호(
aimed2026)는 평문 고정값 — 실사용 전 개별 변경/강제 리셋 플로우 필요.
GET / 로그인 HTML은 CrownyBrowser 앱 index.html 서빙과 별개(단순 폴백) — 실제 오버레이는 todayBridge.js가 주입하는 방식이 주 경로.
- ERP aimed data/members/.json과 today data/멤버.psv가 별도 자격증명 저장소로 이원화됨(설계상 의도, ERP 비번 원문 미노출 때문) — 추후 crowny-auth 또는 ERP에 SSO 위임 API가 열리면 로컬 비번 저장소 제거 가능.
- 관리자 판정은 조직도 "총괄" 직급 2명으로 최소 정의 — 세분화된 권한 매트릭스(권한매트릭스.psv) 연동은 미구현.
관련 파일
- /Users/ef/crowny-today/서버.한선
- /Users/ef/crowny-today/서버.toau
- /Users/ef/crowny-today/data/멤버.psv
- /Users/ef/crowny-today/data/세션.psv
- /Users/ef/CrownyBrowser/bundle/today/bridge/todayBridge.js
- /Users/ef/crowny-erp/instances/aimed/data/조직/조직도.psv (참조, 미수정)
- /Users/ef/crowny-erp/core/libs/인증.한선 (패턴 참조, 미수정)
- /Users/ef/crowny-flex/state/회원.psv (참조, 미수정 — aimed 그룹 부재 확인용)
크라우니코드
lookup/search MISS 1건(신규 도메인, "http bearer token auth 게이트") → learn add 1건 + crowny-knowledge brain learn 1건(intent=today_인증_게이트, 판정=타/직접 신규 구현).