feat(auth, deployment): id,pw or OIDC auth / docker, k8s deployment docs
This commit is contained in:
parent
9f3b4a4613
commit
d9a132c824
41 changed files with 2352 additions and 237 deletions
132
docs/admin-auth.md
Normal file
132
docs/admin-auth.md
Normal file
|
|
@ -0,0 +1,132 @@
|
|||
# Admin Authentication
|
||||
|
||||
관리자 영역은 `/admin/**` API와 관리자 프론트엔드 전체를 포함한다.
|
||||
`/v1/**` 와 `/health` 는 기존 사용자 API 키 인증 또는 공개 엔드포인트 계약을 유지한다.
|
||||
|
||||
## Authentication Modes
|
||||
|
||||
`ADMIN_AUTH_MODE` 로 관리자 로그인 방식을 제어한다.
|
||||
|
||||
| Mode | Description |
|
||||
|------|-------------|
|
||||
| `env` | ENV 기반 관리자 아이디/비밀번호 로그인만 허용 |
|
||||
| `oidc` | OpenID Connect 로그인만 허용 |
|
||||
| `both` | ENV 로그인과 OIDC 로그인을 모두 허용 |
|
||||
|
||||
기본 추천값은 `both`.
|
||||
|
||||
## Protected Surface
|
||||
|
||||
아래 경로는 관리자 인증이 필요하다.
|
||||
|
||||
- `/admin/users`
|
||||
- `/admin/backends`
|
||||
- `/admin/permissions`
|
||||
- `/admin/scripts`
|
||||
- `/admin/analytics/*`
|
||||
- `/admin/health`
|
||||
|
||||
예외 공개 경로:
|
||||
|
||||
- `POST /admin/auth/login`
|
||||
- `GET /admin/auth/session`
|
||||
- `GET /admin/auth/oidc/start`
|
||||
- `GET /admin/auth/oidc/callback`
|
||||
|
||||
## Session Model
|
||||
|
||||
브라우저 로그인은 서버사이드 세션 + `HttpOnly` 쿠키를 사용한다.
|
||||
|
||||
- 쿠키 이름: `kyush_admin_session`
|
||||
- `SameSite=Lax`
|
||||
- `Secure`: production 환경에서 활성화
|
||||
- 세션 TTL: `ADMIN_SESSION_TTL_HOURS`
|
||||
|
||||
세션 데이터는 `core.db` 의 `admin_sessions` 테이블에 저장된다.
|
||||
|
||||
## CSRF
|
||||
|
||||
세션 기반 관리자 요청은 CSRF 보호를 적용한다.
|
||||
|
||||
- `GET /admin/auth/session` 응답에 `csrfToken` 이 포함된다.
|
||||
- 프론트엔드는 `POST`, `PUT`, `DELETE` 요청마다 `X-CSRF-Token` 헤더를 보낸다.
|
||||
- Bearer 관리자 API 토큰 요청에는 CSRF를 적용하지 않는다.
|
||||
|
||||
## Admin API Tokens
|
||||
|
||||
자동화나 서비스 연동은 관리자 API 토큰을 사용할 수 있다.
|
||||
|
||||
- 발급 API: `POST /admin/auth/tokens`
|
||||
- 조회 API: `GET /admin/auth/tokens`
|
||||
- 폐기 API: `DELETE /admin/auth/tokens/:id`
|
||||
|
||||
토큰은 최초 발급 시 1회만 원문이 반환된다.
|
||||
DB에는 hash 와 prefix 만 저장된다.
|
||||
|
||||
대상 테이블:
|
||||
|
||||
- `admin_api_tokens`
|
||||
|
||||
## ENV Login
|
||||
|
||||
ENV 로그인은 아래 설정이 필요하다.
|
||||
|
||||
| Variable | Description |
|
||||
|----------|-------------|
|
||||
| `ADMIN_USERNAME` | 관리자 로그인 아이디 |
|
||||
| `ADMIN_PASSWORD_HASH` | 비밀번호 hash |
|
||||
| `ADMIN_SESSION_SECRET` | 세션/토큰 hash salt 및 비밀값 |
|
||||
|
||||
현재 구현은 아래 hash 형식을 지원한다.
|
||||
|
||||
- `sha256$<hex>`
|
||||
- `scrypt$<salt_hex>$<derived_hex>`
|
||||
|
||||
`ADMIN_PASSWORD_HASH` 는 평문 비밀번호 대신 hash 값으로만 저장하는 것을 전제로 한다.
|
||||
|
||||
## Admin Session API
|
||||
|
||||
### `GET /admin/auth/session`
|
||||
|
||||
현재 관리자 세션 정보를 반환한다.
|
||||
|
||||
```json
|
||||
{
|
||||
"authenticated": true,
|
||||
"authMode": "both",
|
||||
"csrfToken": "base64url-token",
|
||||
"principal": {
|
||||
"provider": "env",
|
||||
"subject": "env:admin",
|
||||
"username": "admin",
|
||||
"displayName": "admin"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### `POST /admin/auth/login`
|
||||
|
||||
요청 본문:
|
||||
|
||||
```json
|
||||
{
|
||||
"username": "admin",
|
||||
"password": "your-password"
|
||||
}
|
||||
```
|
||||
|
||||
성공 시 세션 쿠키를 발급하고 `AdminSessionResponse` 를 반환한다.
|
||||
|
||||
### `POST /admin/auth/logout`
|
||||
|
||||
현재 세션을 무효화하고 세션 쿠키를 제거한다.
|
||||
|
||||
## Deployment Notes
|
||||
|
||||
운영 배포는 관리자 영역과 공개 라우팅을 분리하는 구성이 권장된다.
|
||||
|
||||
- public gateway: `/v1/**`, `/health`
|
||||
- admin gateway: 관리자 SPA + `/admin/**`
|
||||
|
||||
관리자 프론트는 내부 전용 origin 에서 `/admin/**` 상대 경로만 호출한다.
|
||||
이 구조로 브라우저 CORS 복잡도를 줄이고 관리자 라우팅을 외부 공개하지 않을 수 있다.
|
||||
27
docs/api.md
27
docs/api.md
|
|
@ -9,7 +9,7 @@
|
|||
| Method | Path | Description |
|
||||
|--------|------|-------------|
|
||||
| GET | `/health` | 서버 상태 확인 (status, timestamp) |
|
||||
| GET | `/admin/health` | Admin 라우터 상태 확인 (status, timestamp) |
|
||||
| GET | `/admin/health` | 관리자 인증이 필요한 Admin 라우터 상태 확인 (status, timestamp) |
|
||||
|
||||
## OpenAI-Compatible Proxy (인증 필요)
|
||||
|
||||
|
|
@ -20,8 +20,27 @@
|
|||
| POST | `/v1/chat/completions` | Chat completions 프록시 (스크립트 적용, 분석 로깅) |
|
||||
| GET | `/v1/models` | 사용 가능한 모델 목록 |
|
||||
|
||||
`/v1/**`는 기존 사용자 API 키 인증을 유지하며 관리자 인증과 분리된다.
|
||||
|
||||
## Admin API
|
||||
|
||||
`/admin/**`는 기본적으로 관리자 인증이 필요하다. 브라우저는 세션 쿠키, 자동화는 `Authorization: Bearer <admin_api_token>` 방식으로 접근한다.
|
||||
|
||||
세션 기반 요청에서 `POST`, `PUT`, `DELETE`를 호출할 때는 `GET /admin/auth/session`에서 받은 CSRF 토큰을 `X-CSRF-Token` 헤더로 함께 보내야 한다.
|
||||
|
||||
### Auth
|
||||
|
||||
| Method | Path | Description |
|
||||
|--------|------|-------------|
|
||||
| GET | `/admin/auth/session` | 현재 관리자 로그인 상태, principal, auth mode, CSRF 토큰 조회 |
|
||||
| POST | `/admin/auth/login` | ENV 관리자 계정 로그인 후 세션 쿠키 발급 |
|
||||
| POST | `/admin/auth/logout` | 현재 관리자 세션 종료 |
|
||||
| GET | `/admin/auth/oidc/start` | OIDC 로그인 시작 |
|
||||
| GET | `/admin/auth/oidc/callback` | OIDC code exchange 후 세션 생성, 관리자 화면으로 redirect |
|
||||
| GET | `/admin/auth/tokens` | 현재 관리자 principal이 발급한 API 토큰 목록 조회 |
|
||||
| POST | `/admin/auth/tokens` | 새 관리자 API 토큰 발급 |
|
||||
| DELETE | `/admin/auth/tokens/:id` | 관리자 API 토큰 폐기 |
|
||||
|
||||
### Users
|
||||
|
||||
| Method | Path | Description |
|
||||
|
|
@ -51,7 +70,7 @@
|
|||
| GET | `/admin/permissions/user/:userId` | 사용자별 권한 조회 |
|
||||
| GET | `/admin/permissions/backend/:backendId` | 백엔드별 권한 조회 |
|
||||
| POST | `/admin/permissions` | 권한 부여 (user_id, backend_id) |
|
||||
| DELETE | `/admin/permissions?user_id=X&backend_id=Y` | 권한 삭제 |
|
||||
| DELETE | `/admin/permissions?user_id=X&backend_id=Y` | 권한 해제 |
|
||||
|
||||
### Scripts
|
||||
|
||||
|
|
@ -77,3 +96,7 @@
|
|||
| GET | `/admin/analytics/metrics` | backendId, days | 백엔드 성능 메트릭 |
|
||||
|
||||
상세 로그는 `users.detail_logging=1` 또는 `backends.detail_logging=1`일 때만 request/response header/body가 저장된다.
|
||||
|
||||
참고:
|
||||
- 관리자 인증과 세션/토큰 정책은 [docs/admin-auth.md](./admin-auth.md) 참고
|
||||
- OpenID Connect 설정은 [docs/oidc.md](./oidc.md) 참고
|
||||
|
|
|
|||
|
|
@ -1,29 +1,31 @@
|
|||
# Client (Solid.js + Vite)
|
||||
|
||||
Admin 대시보드. 진입점: `client/src/index.tsx`
|
||||
Admin 대시보드 진입점: `client/src/index.tsx`
|
||||
|
||||
## Directory Structure
|
||||
|
||||
```
|
||||
client/src/
|
||||
index.tsx # DOM 렌더링 진입점
|
||||
App.tsx # 라우터 정의 (7개 라우트)
|
||||
App.tsx # 관리자 인증 부트스트랩 + 라우트 정의
|
||||
auth.tsx # 관리자 세션 컨텍스트, session bootstrap, login/logout/token helpers
|
||||
api/
|
||||
client.ts # Admin API 클라이언트 (users, backends, permissions, scripts, analytics)
|
||||
client.ts # Admin API 클라이언트 (credentials: include, /admin same-origin 호출)
|
||||
types/
|
||||
index.ts # TypeScript 타입 정의
|
||||
routes/
|
||||
Dashboard.tsx # 홈 — 운영 요약 스트립 + 최근 요청 테이블
|
||||
Users.tsx # 사용자 관리 — CRUD, API 키 재발급, 검색, 상세 로깅 토글
|
||||
Backends.tsx # 백엔드 관리 — CRUD (name, base_url, api_key, detail_logging, is_active)
|
||||
Permissions.tsx # 권한 관리 — user-backend 매핑, 추가/회수
|
||||
Analytics.tsx # 분석 — 최근 요청, 사용량 집계, 백엔드 메트릭 패널
|
||||
DetailLogs.tsx # 상세 로그 탐색 — 월/일/검색/사용자/백엔드 필터 + 페이징 + 페이로드 인스펙터
|
||||
Scripts.tsx # 스크립트 관리 — 목록, 요약, 편집기, 테스트, 활성화/비활성화
|
||||
Dashboard.tsx # 메인 운영 요약 스트립 + 최근 요청 테이블 + 관리자 토큰 관리
|
||||
Users.tsx # 사용자 관리, CRUD, API 키 재발급, 개별 상세 로그 토글
|
||||
Backends.tsx # 백엔드 관리, CRUD (name, base_url, api_key, detail_logging, is_active)
|
||||
Permissions.tsx # 권한 관리, user-backend 매핑, 추가/해제
|
||||
Analytics.tsx # 분석 탭, 최근 요청, 사용량 통계, 백엔드 메트릭 차트/표
|
||||
DetailLogs.tsx # 상세 로그 검색 뷰, 텍스트 검색, 사용자/백엔드 필터 + 페이지 + 페이로드 인스펙터
|
||||
Scripts.tsx # 스크립트 관리, 목록, 요약, 편집기, 테스트, 활성/비활성화
|
||||
components/
|
||||
Layout.tsx # AppShell 래퍼
|
||||
EditModal.tsx # 레거시 범용 편집 모달
|
||||
ScriptEditor.tsx # Monaco 에디터 래퍼 (TypeScript 하이라이팅)
|
||||
ScriptEditor.tsx # Monaco 에디터 래퍼 (TypeScript 하이라이트)
|
||||
LoginGate.tsx # 로그인 화면, ENV 로그인 폼, OIDC 시작 버튼
|
||||
ui/
|
||||
index.ts # primitives/patterns 재수출
|
||||
primitives/ # Button, Dialog, Select, Tabs, TextField 등
|
||||
|
|
@ -41,24 +43,26 @@ client/src/
|
|||
| `/backends` | Backends | 백엔드 관리 |
|
||||
| `/permissions` | Permissions | 권한 관리 |
|
||||
| `/analytics` | Analytics | 집계 기반 분석 대시보드 |
|
||||
| `/detail-logs` | DetailLogs | 상세 요청 로그 탐색/인스펙션 |
|
||||
| `/detail-logs` | DetailLogs | 상세 요청 로그 검색/인스펙션 |
|
||||
| `/scripts` | Scripts | 스크립트 관리 |
|
||||
|
||||
모든 관리자 라우트는 로그인 게이트 아래에서만 렌더링된다.
|
||||
|
||||
## Styling
|
||||
|
||||
공통 UI 레이어는 `client/src/ui/styles.css` 에서 시작하며, 내부적으로 `tokens.css`, `base.css`, `layout.css`, `patterns.css`, `pages.css` 를 불러온다.
|
||||
공통 UI 레이어는 `client/src/ui/styles.css` 에서 시작하고, 내부적으로 `tokens.css`, `base.css`, `layout.css`, `patterns.css`, `pages.css` 를 불러온다.
|
||||
|
||||
- `@kobalte/core` 기반 primitive wrapper와 공통 pattern을 사용한다.
|
||||
- 페이지는 `AppShell`, `PageHeader`, `Panel`, `DataGrid`, `SummaryStrip`, `FormDialog`, `ConfirmDialog` 조합으로 구성된다.
|
||||
- Storybook(`client/.storybook`)에서 같은 스타일 레이어를 사용해 workbench를 유지한다.
|
||||
- 라이트/다크 테마와 dense 콘솔형 레이아웃을 기본 전제로 한다.
|
||||
- Storybook(`client/.storybook`)에서 같은 스타일 레이어를 사용하는 workbench를 운영한다.
|
||||
- 라이트 테마와 dense 콘솔형 레이아웃을 기본 전제로 둔다.
|
||||
|
||||
## Dependencies
|
||||
|
||||
| Package | Purpose |
|
||||
|---------|---------|
|
||||
| solid-js | UI 프레임워크 |
|
||||
| @solidjs/router | 클라이언트 사이드 라우팅 |
|
||||
| @solidjs/router | 클라이언트 사이드 라우터 |
|
||||
| @kobalte/core | headless UI primitive |
|
||||
| lucide-solid | 아이콘 세트 |
|
||||
| solid-monaco | Monaco 에디터 통합 |
|
||||
|
|
@ -66,10 +70,21 @@ client/src/
|
|||
|
||||
## Dev Server
|
||||
|
||||
포트: 3002 (vite.config.ts), API 프록시: `/api` → `http://localhost:3000`
|
||||
포트: 3002 (`vite.config.ts`), 개발 중 API 프록시 `/admin` → `http://localhost:3000`
|
||||
|
||||
운영 배포에서는 관리자 프론트가 admin gateway 뒤에서 same-origin `/admin/**`를 호출한다. 브라우저가 내부 서버 주소를 직접 알 필요는 없다.
|
||||
|
||||
## Admin Auth Notes
|
||||
|
||||
- 앱 시작 시 `GET /admin/auth/session`으로 현재 로그인 상태와 CSRF 토큰을 불러온다.
|
||||
- 인증되지 않은 상태에서는 관리자 화면 대신 `LoginGate`가 렌더링된다.
|
||||
- ENV 로그인 폼과 OIDC 로그인 버튼을 함께 제공한다.
|
||||
- 세션 기반 변경 요청은 `X-CSRF-Token` 헤더를 자동으로 포함한다.
|
||||
- 401 응답은 재로그인 흐름으로, 403 응답은 권한 오류 표시로 처리한다.
|
||||
- 관리자 API 토큰 생성/폐기는 Dashboard에서 수행한다.
|
||||
|
||||
## Analytics Notes
|
||||
|
||||
- `Analytics` 화면은 최근 요청/사용량/메트릭의 집계 뷰를 보여준다.
|
||||
- 상세 요청 로그 탐색은 별도 `DetailLogs` 화면에서 처리한다.
|
||||
- `Analytics` 화면은 최근 요청/사용량 메트릭의 집계 뷰를 보여준다.
|
||||
- 상세 요청 로그 검색은 별도 `DetailLogs` 화면에서 처리한다.
|
||||
- analytics API 클라이언트는 `month`, `date`, `q`, `limit`, `offset`, `userId`, `backendId`, `endpoint`, `detailLogged` 필터를 지원한다.
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@ DB는 `DB_DIR` 하위에 분리 저장된다.
|
|||
|
||||
---
|
||||
|
||||
## Core Database (core.db)
|
||||
## Core Database (`core.db`)
|
||||
|
||||
### users
|
||||
|
||||
|
|
@ -17,7 +17,7 @@ DB는 `DB_DIR` 하위에 분리 저장된다.
|
|||
| name | TEXT | NOT NULL |
|
||||
| email | TEXT | |
|
||||
| is_active | BOOLEAN | DEFAULT 1 |
|
||||
| detail_logging | INTEGER | DEFAULT 0 |
|
||||
| detail_logging | INTEGER | NOT NULL DEFAULT 0 |
|
||||
| created_at | TIMESTAMP | DEFAULT CURRENT_TIMESTAMP |
|
||||
| updated_at | TIMESTAMP | DEFAULT CURRENT_TIMESTAMP |
|
||||
|
||||
|
|
@ -32,22 +32,23 @@ Indexes: `idx_users_api_key(api_key)`
|
|||
| base_url | TEXT | NOT NULL |
|
||||
| api_key | TEXT | |
|
||||
| is_active | BOOLEAN | DEFAULT 1 |
|
||||
| detail_logging | INTEGER | DEFAULT 0 |
|
||||
| detail_logging | INTEGER | NOT NULL DEFAULT 0 |
|
||||
| created_at | TIMESTAMP | DEFAULT CURRENT_TIMESTAMP |
|
||||
| updated_at | TIMESTAMP | DEFAULT CURRENT_TIMESTAMP |
|
||||
|
||||
### permissions
|
||||
|
||||
users와 backends의 many-to-many 관계.
|
||||
`users`와 `backends`의 many-to-many 관계.
|
||||
|
||||
| Column | Type | Constraints |
|
||||
|--------|------|-------------|
|
||||
| id | INTEGER | PRIMARY KEY AUTOINCREMENT |
|
||||
| user_id | INTEGER | NOT NULL, FK → users(id) |
|
||||
| backend_id | INTEGER | NOT NULL, FK → backends(id) |
|
||||
| user_id | INTEGER | NOT NULL, FK → `users(id)` |
|
||||
| backend_id | INTEGER | NOT NULL, FK → `backends(id)` |
|
||||
| created_at | TIMESTAMP | DEFAULT CURRENT_TIMESTAMP |
|
||||
|
||||
Unique: `(user_id, backend_id)`
|
||||
|
||||
Indexes: `idx_permissions_user(user_id)`, `idx_permissions_backend(backend_id)`
|
||||
|
||||
### user_scripts
|
||||
|
|
@ -56,9 +57,9 @@ Indexes: `idx_permissions_user(user_id)`, `idx_permissions_backend(backend_id)`
|
|||
|--------|------|-------------|
|
||||
| id | INTEGER | PRIMARY KEY AUTOINCREMENT |
|
||||
| name | TEXT | UNIQUE NOT NULL |
|
||||
| script_type | TEXT | NOT NULL, CHECK IN ('per-user-backend', 'per-backend', 'per-user') |
|
||||
| target_user_id | INTEGER | FK → users(id) |
|
||||
| target_backend_id | INTEGER | FK → backends(id) |
|
||||
| script_type | TEXT | NOT NULL, CHECK IN (`'per-user-backend'`, `'per-backend'`, `'per-user'`) |
|
||||
| target_user_id | INTEGER | FK → `users(id)` |
|
||||
| target_backend_id | INTEGER | FK → `backends(id)` |
|
||||
| script_code | TEXT | NOT NULL |
|
||||
| is_active | BOOLEAN | DEFAULT 1 |
|
||||
| created_at | TIMESTAMP | DEFAULT CURRENT_TIMESTAMP |
|
||||
|
|
@ -66,15 +67,58 @@ Indexes: `idx_permissions_user(user_id)`, `idx_permissions_backend(backend_id)`
|
|||
|
||||
Indexes: `idx_user_scripts_type`, `idx_user_scripts_active`, `idx_user_scripts_target_user`, `idx_user_scripts_target_backend`
|
||||
|
||||
---
|
||||
### admin_sessions
|
||||
|
||||
## Analytics Database (analytics.db)
|
||||
|
||||
### usage_stats
|
||||
관리자 브라우저 로그인 세션 저장소. 세션 쿠키 원문은 저장하지 않고 `session_token_hash`를 저장한다.
|
||||
|
||||
| Column | Type | Constraints |
|
||||
|--------|------|-------------|
|
||||
일별 집계 테이블. 날짜 경계는 `TZ`, 저장 timestamp는 UTC 기준.
|
||||
| id | INTEGER | PRIMARY KEY AUTOINCREMENT |
|
||||
| session_token_hash | TEXT | UNIQUE NOT NULL |
|
||||
| provider | TEXT | NOT NULL, CHECK IN (`'env'`, `'oidc'`) |
|
||||
| subject | TEXT | NOT NULL |
|
||||
| username | TEXT | |
|
||||
| email | TEXT | |
|
||||
| display_name | TEXT | NOT NULL |
|
||||
| csrf_token | TEXT | NOT NULL |
|
||||
| expires_at | TEXT | NOT NULL |
|
||||
| last_used_at | TEXT | |
|
||||
| revoked_at | TEXT | |
|
||||
| created_at | TEXT | NOT NULL |
|
||||
| updated_at | TEXT | NOT NULL |
|
||||
|
||||
Indexes: `idx_admin_sessions_subject(subject)`, `idx_admin_sessions_expires_at(expires_at)`
|
||||
|
||||
### admin_api_tokens
|
||||
|
||||
자동화/서비스 연동용 관리자 API 토큰 저장소. DB에는 토큰 원문이 아니라 `token_hash`와 `token_prefix`만 저장한다.
|
||||
|
||||
| Column | Type | Constraints |
|
||||
|--------|------|-------------|
|
||||
| id | INTEGER | PRIMARY KEY AUTOINCREMENT |
|
||||
| token_hash | TEXT | UNIQUE NOT NULL |
|
||||
| name | TEXT | NOT NULL |
|
||||
| provider | TEXT | NOT NULL, CHECK IN (`'env'`, `'oidc'`) |
|
||||
| subject | TEXT | NOT NULL |
|
||||
| username | TEXT | |
|
||||
| email | TEXT | |
|
||||
| display_name | TEXT | NOT NULL |
|
||||
| token_prefix | TEXT | NOT NULL |
|
||||
| expires_at | TEXT | NOT NULL |
|
||||
| last_used_at | TEXT | |
|
||||
| revoked_at | TEXT | |
|
||||
| created_at | TEXT | NOT NULL |
|
||||
| updated_at | TEXT | NOT NULL |
|
||||
|
||||
Indexes: `idx_admin_api_tokens_subject(subject)`, `idx_admin_api_tokens_expires_at(expires_at)`
|
||||
|
||||
---
|
||||
|
||||
## Analytics Database (`analytics.db`)
|
||||
|
||||
일별 집계 테이블. 날짜 경계는 `TZ`, 저장 timestamp는 UTC 기준이다.
|
||||
|
||||
### usage_stats
|
||||
|
||||
| Column | Type | Constraints |
|
||||
|--------|------|-------------|
|
||||
|
|
@ -86,6 +130,7 @@ Indexes: `idx_user_scripts_type`, `idx_user_scripts_active`, `idx_user_scripts_t
|
|||
| total_tokens | INTEGER | DEFAULT 0 |
|
||||
|
||||
Unique: `(user_id, backend_id, date)`
|
||||
|
||||
Indexes: `idx_usage_stats_user`, `idx_usage_stats_date`
|
||||
|
||||
### backend_metrics
|
||||
|
|
@ -104,6 +149,7 @@ Indexes: `idx_usage_stats_user`, `idx_usage_stats_date`
|
|||
| success_rate | REAL | DEFAULT 1.0 |
|
||||
|
||||
Unique: `(backend_id, date)`
|
||||
|
||||
Indexes: `idx_backend_metrics_backend`, `idx_backend_metrics_date`
|
||||
|
||||
## Monthly Request Logs (`request_logs/request_logs_YYYY-MM.db`)
|
||||
|
|
@ -126,7 +172,7 @@ Indexes: `idx_backend_metrics_backend`, `idx_backend_metrics_date`
|
|||
| status_code | INTEGER | |
|
||||
| response_time_ms | INTEGER | |
|
||||
| error_message | TEXT | |
|
||||
| detail_logged | INTEGER | DEFAULT 0 |
|
||||
| detail_logged | INTEGER | NOT NULL DEFAULT 0 |
|
||||
| local_date | TEXT | `TZ` 기준 `YYYY-MM-DD` |
|
||||
| request_headers | TEXT | JSON 문자열 |
|
||||
| request_body | TEXT | JSON 또는 raw 문자열 |
|
||||
|
|
@ -134,4 +180,4 @@ Indexes: `idx_backend_metrics_backend`, `idx_backend_metrics_date`
|
|||
| response_body | TEXT | JSON 또는 raw 문자열 |
|
||||
| created_at | TEXT | UTC ISO timestamp |
|
||||
|
||||
Indexes: `created_at`, `local_date`, `user_id`, `backend_id`, `endpoint`, `detail_logged`
|
||||
Indexes: `idx_request_logs_created_at`, `idx_request_logs_local_date`, `idx_request_logs_user`, `idx_request_logs_backend`, `idx_request_logs_endpoint`, `idx_request_logs_detail_logged`
|
||||
|
|
|
|||
387
docs/k8s-traefik.md
Normal file
387
docs/k8s-traefik.md
Normal file
|
|
@ -0,0 +1,387 @@
|
|||
# Kubernetes Deployment with Traefik
|
||||
|
||||
`kyush-llm-router`를 Kubernetes에 배포하면서, 외부에는 라우터 API만 공개하고 관리자 프론트와 `/admin/**`는 내부망에서만 접근하도록 구성하는 예시다.
|
||||
|
||||
이 문서는 ingress controller로 Traefik을 사용하고, 관리자 경로에는 `IngressRoute`와 `Middleware.ipAllowList`를 적용하는 상황을 가정한다.
|
||||
|
||||
## Goals
|
||||
|
||||
- 공개 진입점
|
||||
- `/v1/**`
|
||||
- `/health`
|
||||
- 내부 전용 진입점
|
||||
- 관리자 프론트
|
||||
- `/admin/**`
|
||||
- 브라우저 기준 관리자 프론트와 관리자 API는 same-origin으로 동작
|
||||
- Traefik에서 admin host에 IP allowlist를 적용
|
||||
|
||||
## Assumptions
|
||||
|
||||
- Traefik CRD가 이미 설치되어 있다.
|
||||
- 예시는 `apiVersion: traefik.io/v1alpha1` 기준이다.
|
||||
- public host는 `router.example.com`, admin host는 `router-admin.internal.example.com` 을 사용한다.
|
||||
- admin host는 사내망, VPN, 프라이빗 DNS 같은 내부 경로에서만 해석되거나 접근된다.
|
||||
- 앱 이미지는 역할별로 분리되어 있다고 가정한다.
|
||||
- `server` 이미지: Express 서버
|
||||
- `admin-client` 이미지: 관리자 프론트 정적 파일을 nginx로 서빙하는 이미지
|
||||
|
||||
## Recommended Topology
|
||||
|
||||
```text
|
||||
Internet
|
||||
-> Traefik
|
||||
-> public IngressRoute
|
||||
-> router-server Service:3000
|
||||
|
||||
Internal network / VPN
|
||||
-> Traefik
|
||||
-> admin frontend IngressRoute + ipAllowList middleware
|
||||
-> admin-client Service:80
|
||||
|
||||
Admin frontend
|
||||
-> same-origin /admin/**
|
||||
-> admin api IngressRoute + ipAllowList middleware
|
||||
-> router-server Service:3000
|
||||
```
|
||||
|
||||
핵심은 public/admin을 서로 다른 host로 분리하고, admin 쪽만 Traefik middleware로 한 번 더 제한하는 것이다.
|
||||
|
||||
## Environment And Secrets
|
||||
|
||||
아래 예시는 서버에 필요한 주요 ENV만 담는다.
|
||||
|
||||
```yaml
|
||||
apiVersion: v1
|
||||
kind: Secret
|
||||
metadata:
|
||||
name: router-server-secret
|
||||
namespace: llm-router
|
||||
type: Opaque
|
||||
stringData:
|
||||
ADMIN_PASSWORD_HASH: "scrypt$..."
|
||||
ADMIN_SESSION_SECRET: "replace-with-long-random-secret"
|
||||
OIDC_CLIENT_SECRET: ""
|
||||
---
|
||||
apiVersion: v1
|
||||
kind: ConfigMap
|
||||
metadata:
|
||||
name: router-server-config
|
||||
namespace: llm-router
|
||||
data:
|
||||
NODE_ENV: "production"
|
||||
SERVER_PORT: "3000"
|
||||
DB_DIR: "/data"
|
||||
TZ: "Asia/Seoul"
|
||||
CORS_ORIGINS: "https://router-admin.internal.example.com"
|
||||
ADMIN_AUTH_MODE: "both"
|
||||
ADMIN_USERNAME: "admin"
|
||||
ADMIN_SESSION_TTL_HOURS: "12"
|
||||
ADMIN_API_TOKEN_TTL_DAYS: "30"
|
||||
ADMIN_TRUSTED_PROXY_IPS: "10.0.0.0/8,192.168.0.0/16"
|
||||
OIDC_ISSUER_URL: ""
|
||||
OIDC_CLIENT_ID: ""
|
||||
OIDC_REDIRECT_URI: "https://router-admin.internal.example.com/admin/auth/oidc/callback"
|
||||
OIDC_ALLOWED_EMAILS: "admin1@example.com,admin2@example.com"
|
||||
OIDC_SCOPES: "openid profile email"
|
||||
```
|
||||
|
||||
## Server Deployment
|
||||
|
||||
```yaml
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: router-server
|
||||
namespace: llm-router
|
||||
spec:
|
||||
replicas: 1
|
||||
selector:
|
||||
matchLabels:
|
||||
app: router-server
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
app: router-server
|
||||
spec:
|
||||
containers:
|
||||
- name: server
|
||||
image: ghcr.io/example/kyush-llm-router-server:latest
|
||||
ports:
|
||||
- containerPort: 3000
|
||||
envFrom:
|
||||
- configMapRef:
|
||||
name: router-server-config
|
||||
- secretRef:
|
||||
name: router-server-secret
|
||||
volumeMounts:
|
||||
- name: router-data
|
||||
mountPath: /data
|
||||
readinessProbe:
|
||||
httpGet:
|
||||
path: /health
|
||||
port: 3000
|
||||
initialDelaySeconds: 5
|
||||
periodSeconds: 10
|
||||
livenessProbe:
|
||||
httpGet:
|
||||
path: /health
|
||||
port: 3000
|
||||
initialDelaySeconds: 15
|
||||
periodSeconds: 20
|
||||
volumes:
|
||||
- name: router-data
|
||||
persistentVolumeClaim:
|
||||
claimName: router-data
|
||||
---
|
||||
apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
name: router-server
|
||||
namespace: llm-router
|
||||
spec:
|
||||
selector:
|
||||
app: router-server
|
||||
ports:
|
||||
- name: http
|
||||
port: 3000
|
||||
targetPort: 3000
|
||||
```
|
||||
|
||||
```yaml
|
||||
apiVersion: v1
|
||||
kind: PersistentVolumeClaim
|
||||
metadata:
|
||||
name: router-data
|
||||
namespace: llm-router
|
||||
spec:
|
||||
accessModes:
|
||||
- ReadWriteOnce
|
||||
resources:
|
||||
requests:
|
||||
storage: 10Gi
|
||||
```
|
||||
|
||||
## Admin Frontend Deployment
|
||||
|
||||
정적 파일 서빙이라면 nginx를 사용해도 충분하다. 여기서는 빌드된 관리자 프론트를 nginx가 서빙하는 전용 이미지를 사용한다고 가정한다.
|
||||
|
||||
```yaml
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: admin-client
|
||||
namespace: llm-router
|
||||
spec:
|
||||
replicas: 1
|
||||
selector:
|
||||
matchLabels:
|
||||
app: admin-client
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
app: admin-client
|
||||
spec:
|
||||
containers:
|
||||
- name: admin-client
|
||||
image: ghcr.io/example/kyush-llm-router-admin:latest
|
||||
ports:
|
||||
- containerPort: 80
|
||||
readinessProbe:
|
||||
httpGet:
|
||||
path: /
|
||||
port: 80
|
||||
initialDelaySeconds: 5
|
||||
periodSeconds: 10
|
||||
---
|
||||
apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
name: admin-client
|
||||
namespace: llm-router
|
||||
spec:
|
||||
selector:
|
||||
app: admin-client
|
||||
ports:
|
||||
- name: http
|
||||
port: 80
|
||||
targetPort: 80
|
||||
```
|
||||
|
||||
중요한 점은 nginx를 ingress 대신 내부 정적 파일 서버로만 쓰고, 외부 라우팅과 접근 제어는 Traefik이 담당하게 두는 것이다.
|
||||
|
||||
## Traefik Middleware
|
||||
|
||||
관리자용 host에만 내부망 IP allowlist를 적용한다.
|
||||
|
||||
```yaml
|
||||
apiVersion: traefik.io/v1alpha1
|
||||
kind: Middleware
|
||||
metadata:
|
||||
name: admin-ip-allowlist
|
||||
namespace: llm-router
|
||||
spec:
|
||||
ipAllowList:
|
||||
sourceRange:
|
||||
- 10.0.0.0/8
|
||||
- 172.16.0.0/12
|
||||
- 192.168.0.0/16
|
||||
- 100.64.0.0/10
|
||||
ipStrategy:
|
||||
depth: 1
|
||||
```
|
||||
|
||||
`ipStrategy.depth` 는 Traefik 앞단에 L4/L7 프록시가 하나 더 있는 환경에서만 맞춰야 한다. 직접 노출된 Traefik이라면 생략하는 편이 안전하다.
|
||||
|
||||
## Public IngressRoute
|
||||
|
||||
외부 공개는 `/v1` 과 `/health` 만 노출한다.
|
||||
|
||||
```yaml
|
||||
apiVersion: traefik.io/v1alpha1
|
||||
kind: IngressRoute
|
||||
metadata:
|
||||
name: router-public
|
||||
namespace: llm-router
|
||||
spec:
|
||||
entryPoints:
|
||||
- websecure
|
||||
routes:
|
||||
- match: Host(`router.example.com`) && (PathPrefix(`/v1`) || Path(`/health`))
|
||||
kind: Rule
|
||||
services:
|
||||
- name: router-server
|
||||
port: 3000
|
||||
tls:
|
||||
secretName: router-example-com-tls
|
||||
```
|
||||
|
||||
이 라우트에는 `/admin` 이나 관리자 프론트 경로를 넣지 않는다.
|
||||
|
||||
## Admin Frontend IngressRoute
|
||||
|
||||
관리자 프론트는 내부 전용 host로 따로 분리한다.
|
||||
|
||||
```yaml
|
||||
apiVersion: traefik.io/v1alpha1
|
||||
kind: IngressRoute
|
||||
metadata:
|
||||
name: router-admin-frontend
|
||||
namespace: llm-router
|
||||
spec:
|
||||
entryPoints:
|
||||
- websecure
|
||||
routes:
|
||||
- match: Host(`router-admin.internal.example.com`) && Path(`/`)
|
||||
kind: Rule
|
||||
middlewares:
|
||||
- name: admin-ip-allowlist
|
||||
services:
|
||||
- name: admin-client
|
||||
port: 80
|
||||
- match: Host(`router-admin.internal.example.com`) && PathPrefix(`/assets`)
|
||||
kind: Rule
|
||||
middlewares:
|
||||
- name: admin-ip-allowlist
|
||||
services:
|
||||
- name: admin-client
|
||||
port: 80
|
||||
tls:
|
||||
secretName: router-admin-internal-tls
|
||||
```
|
||||
|
||||
단일 페이지 앱 경로 재작성은 admin-client 이미지 내부 nginx 설정에서 처리하는 편이 단순하다.
|
||||
|
||||
## Admin API IngressRoute
|
||||
|
||||
관리자 프론트가 same-origin `/admin/**` 를 직접 호출할 수 있도록 같은 host 아래에서 관리자 API를 서버로 보낸다.
|
||||
|
||||
```yaml
|
||||
apiVersion: traefik.io/v1alpha1
|
||||
kind: IngressRoute
|
||||
metadata:
|
||||
name: router-admin-api
|
||||
namespace: llm-router
|
||||
spec:
|
||||
entryPoints:
|
||||
- websecure
|
||||
routes:
|
||||
- match: Host(`router-admin.internal.example.com`) && PathPrefix(`/admin`)
|
||||
kind: Rule
|
||||
middlewares:
|
||||
- name: admin-ip-allowlist
|
||||
services:
|
||||
- name: router-server
|
||||
port: 3000
|
||||
tls:
|
||||
secretName: router-admin-internal-tls
|
||||
```
|
||||
|
||||
이 방식이면 prefix 제거용 middleware가 필요 없고, 브라우저 요청:
|
||||
|
||||
```text
|
||||
GET https://router-admin.internal.example.com/admin/auth/session
|
||||
```
|
||||
|
||||
은 서버에 그대로 아래처럼 전달된다.
|
||||
|
||||
```text
|
||||
GET /admin/auth/session
|
||||
```
|
||||
|
||||
## Optional Hardening
|
||||
|
||||
- `router-server` Service를 `ClusterIP`로만 두고 외부 노출은 Traefik만 담당하게 한다.
|
||||
- `NetworkPolicy`로 Traefik namespace에서만 `router-server` 와 `admin-client` 에 접근 가능하게 제한한다.
|
||||
- 서버의 `ADMIN_TRUSTED_PROXY_IPS` 에 Traefik Pod CIDR 또는 Service CIDR 범위를 넣어 추가 방어선을 둔다.
|
||||
- admin host는 공인 DNS에 올리지 않고 split-horizon DNS 또는 내부 DNS로만 배포한다.
|
||||
- `/health` 와 `/admin/health` 의 공개 범위를 운영 정책에 맞게 다시 점검한다.
|
||||
|
||||
## Minimal NetworkPolicy Example
|
||||
|
||||
```yaml
|
||||
apiVersion: networking.k8s.io/v1
|
||||
kind: NetworkPolicy
|
||||
metadata:
|
||||
name: router-server-ingress
|
||||
namespace: llm-router
|
||||
spec:
|
||||
podSelector:
|
||||
matchLabels:
|
||||
app: router-server
|
||||
policyTypes:
|
||||
- Ingress
|
||||
ingress:
|
||||
- from:
|
||||
- namespaceSelector:
|
||||
matchLabels:
|
||||
kubernetes.io/metadata.name: traefik
|
||||
ports:
|
||||
- protocol: TCP
|
||||
port: 3000
|
||||
---
|
||||
apiVersion: networking.k8s.io/v1
|
||||
kind: NetworkPolicy
|
||||
metadata:
|
||||
name: admin-client-ingress
|
||||
namespace: llm-router
|
||||
spec:
|
||||
podSelector:
|
||||
matchLabels:
|
||||
app: admin-client
|
||||
policyTypes:
|
||||
- Ingress
|
||||
ingress:
|
||||
- from:
|
||||
- namespaceSelector:
|
||||
matchLabels:
|
||||
kubernetes.io/metadata.name: traefik
|
||||
ports:
|
||||
- protocol: TCP
|
||||
port: 80
|
||||
```
|
||||
|
||||
## Notes
|
||||
|
||||
- 이 문서는 예시이며, 실제 이미지 이름, TLS secret 이름, CIDR 범위, namespace 이름은 환경에 맞게 바꿔야 한다.
|
||||
- Traefik CRD 버전에 따라 구버전 클러스터는 `traefik.containo.us/v1alpha1` 를 사용할 수 있다. 현재 예시는 `traefik.io/v1alpha1` 기준이다.
|
||||
- IP allowlist는 강한 방어선이지만, 관리자 인증 자체를 대체하지 않는다. 현재 서버의 관리자 세션/OIDC/토큰 인증은 그대로 유지해야 한다.
|
||||
65
docs/oidc.md
Normal file
65
docs/oidc.md
Normal file
|
|
@ -0,0 +1,65 @@
|
|||
# OpenID Connect Setup
|
||||
|
||||
관리자 인증은 generic OpenID Connect discovery 기반으로 동작한다.
|
||||
특정 공급자 전용 분기 없이 issuer metadata 와 authorization code flow 를 사용한다.
|
||||
|
||||
## Required Environment Variables
|
||||
|
||||
| Variable | Description |
|
||||
|----------|-------------|
|
||||
| `ADMIN_AUTH_MODE` | `oidc` 또는 `both` |
|
||||
| `ADMIN_SESSION_SECRET` | state, nonce, session 보호용 비밀값 |
|
||||
| `OIDC_ISSUER_URL` | issuer URL |
|
||||
| `OIDC_CLIENT_ID` | client id |
|
||||
| `OIDC_CLIENT_SECRET` | client secret |
|
||||
| `OIDC_REDIRECT_URI` | callback URL |
|
||||
| `OIDC_ALLOWED_EMAILS` | 관리자 허용 이메일 목록 |
|
||||
| `OIDC_SCOPES` | 기본값 `openid profile email` |
|
||||
|
||||
## Minimal Example
|
||||
|
||||
```env
|
||||
ADMIN_AUTH_MODE=both
|
||||
ADMIN_SESSION_SECRET=replace-with-long-random-secret
|
||||
OIDC_ISSUER_URL=https://your-issuer.example.com
|
||||
OIDC_CLIENT_ID=your-client-id
|
||||
OIDC_CLIENT_SECRET=your-client-secret
|
||||
OIDC_REDIRECT_URI=http://localhost:3002/admin/auth/oidc/callback
|
||||
OIDC_ALLOWED_EMAILS=admin1@example.com,admin2@example.com
|
||||
OIDC_SCOPES=openid profile email
|
||||
```
|
||||
|
||||
## Production Example
|
||||
|
||||
```env
|
||||
ADMIN_AUTH_MODE=both
|
||||
ADMIN_SESSION_SECRET=replace-with-long-random-secret
|
||||
OIDC_ISSUER_URL=https://auth.example.com/realms/main
|
||||
OIDC_CLIENT_ID=kyush-router-admin
|
||||
OIDC_CLIENT_SECRET=replace-with-client-secret
|
||||
OIDC_REDIRECT_URI=https://admin.internal.example.com/admin/auth/oidc/callback
|
||||
OIDC_ALLOWED_EMAILS=admin1@example.com,admin2@example.com
|
||||
OIDC_SCOPES=openid profile email
|
||||
```
|
||||
|
||||
## Flow
|
||||
|
||||
1. 브라우저가 `GET /admin/auth/oidc/start` 로 이동한다.
|
||||
2. 서버는 discovery metadata 를 읽고 authorization endpoint 로 redirect 한다.
|
||||
3. IdP 로그인 후 `OIDC_REDIRECT_URI` 로 callback 된다.
|
||||
4. 서버는 code exchange 를 수행하고 ID token / userinfo 에서 principal 을 구성한다.
|
||||
5. 이메일이 `OIDC_ALLOWED_EMAILS` 에 포함되면 관리자 세션을 생성한다.
|
||||
6. 이후 브라우저는 세션 쿠키로 `/admin/**` 를 호출한다.
|
||||
|
||||
## Allowlist Policy
|
||||
|
||||
- 관리자 승인은 이메일 allowlist 로 제한한다.
|
||||
- allowlist 에 없는 계정은 인증에 성공해도 관리자 권한을 얻지 못한다.
|
||||
- 이메일 비교는 운영 중 표기 흔들림을 막기 위해 소문자 정규화를 전제로 하는 편이 좋다.
|
||||
|
||||
## Notes
|
||||
|
||||
- `OIDC_REDIRECT_URI` 는 실제 브라우저가 접근하는 관리자 origin 기준이어야 한다.
|
||||
- 관리자 프론트가 same-origin `/admin/**` 를 사용하므로 callback 도 같은 origin 아래 두는 구성이 가장 단순하다.
|
||||
- `OIDC_ALLOWED_EMAILS` 가 비어 있으면 운영 환경에서는 사실상 관리자 승인이 열려버릴 수 있으므로 명시적으로 설정하는 편이 안전하다.
|
||||
- OIDC 는 관리자 인증 수단일 뿐이며, 내부망 접근 제어와 세션/토큰 정책을 대체하지 않는다.
|
||||
|
|
@ -6,56 +6,73 @@
|
|||
|
||||
```
|
||||
server/src/
|
||||
index.ts # Express 앱 팩토리 (CORS, 라우트 마운트, health 엔드포인트)
|
||||
index.ts # Express 엔트리포인트(CORS, 라우터 마운트, health 핸들러, 관리자 인증 적용)
|
||||
config/
|
||||
db-paths.ts # DB_DIR 기준 파일 경로 계산
|
||||
database.ts # Core SQLite 연결 및 스키마 초기화
|
||||
analytics-db.ts # Analytics SQLite 연결 및 스키마 초기화
|
||||
request-logs-db.ts # 월별 request_logs SQLite 연결 및 스키마 초기화
|
||||
db-paths.ts # DB_DIR 기준 파일 경로 계산
|
||||
database.ts # Core SQLite 연결 및 스키마 초기화
|
||||
analytics-db.ts # Analytics SQLite 연결 및 스키마 초기화
|
||||
request-logs-db.ts # 월별 request_logs SQLite 연결 및 스키마 초기화
|
||||
admin-auth.ts # 관리자 인증 ENV 파싱, auth mode / OIDC / TTL 설정
|
||||
models/
|
||||
User.ts # 사용자 CRUD (create, findById, findByApiKey, update, delete, regenerateApiKey)
|
||||
Backend.ts # 백엔드 CRUD (create, findById, findAll, update, delete)
|
||||
Permission.ts # 권한 관리 (user-backend 매핑)
|
||||
Script.ts # 스크립트 CRUD (타입별 필터링, 활성화/비활성화)
|
||||
User.ts # 사용자 CRUD (create, findById, findByApiKey, update, delete, regenerateApiKey)
|
||||
Backend.ts # 백엔드 CRUD (create, findById, findAll, update, delete)
|
||||
Permission.ts # 권한 관리 (user-backend 매핑)
|
||||
Script.ts # 스크립트 CRUD (타입별 필터링, 활성화/비활성화)
|
||||
AdminSession.ts # 관리자 세션 저장/조회/만료 처리
|
||||
AdminApiToken.ts # 관리자 API 토큰 발급/조회/폐기
|
||||
routes/
|
||||
auth.ts # Bearer 토큰 인증 미들웨어 (API 키 검증, 권한 로드)
|
||||
api.ts # OpenAI 호환 프록시 엔드포인트 (/v1/chat/completions, /v1/models)
|
||||
admin.ts # Admin CRUD 엔드포인트 (users, backends, permissions, /admin/health, /admin/scripts 마운트)
|
||||
scripts.ts # Script 관리/테스트 엔드포인트
|
||||
analytics.ts # Analytics 조회 엔드포인트
|
||||
auth.ts # Bearer 토큰 인증 미들웨어 (사용자 API 키 검증, 권한 로드)
|
||||
api.ts # OpenAI 호환 프록시 핸들러 (/v1/chat/completions, /v1/models)
|
||||
admin-auth.ts # 관리자 로그인, 세션, OIDC, 관리자 토큰 API
|
||||
admin.ts # Admin CRUD 핸들러 (users, backends, permissions, /admin/health, /admin/scripts 마운트)
|
||||
scripts.ts # Script 관리/테스트 핸들러
|
||||
analytics.ts # Analytics 조회 핸들러
|
||||
services/
|
||||
RouterService.ts # 활성 백엔드 선택, HTTP 요청 포워딩, header/body 정규화
|
||||
AnalyticsService.ts # 일별 사용량/메트릭 집계 + request_logs 조회
|
||||
RouterService.ts # 활성 백엔드 선택, HTTP 요청 포워딩, header/body 정규화
|
||||
AnalyticsService.ts # 일별 사용량 메트릭 집계 + request_logs 조회
|
||||
RequestLogService.ts # 월별 request_logs 기록/조회
|
||||
ScriptEngine.ts # 스크립트 체인 오케스트레이션 (onRequest/onResponse 훅 적용)
|
||||
ScriptExecutor.ts # isolated-vm 기반 스크립트 컴파일/실행 (5s timeout, 50MB memory)
|
||||
ScriptEngine.ts # 스크립트 체인 오케스트레이션 (onRequest/onResponse 적용)
|
||||
ScriptExecutor.ts # isolated-vm 기반 스크립트 컴파일/실행 (5s timeout, 50MB memory)
|
||||
utils/
|
||||
apiKey.ts # API 키 생성 (sk-{timestamp}-{random}, crypto.randomBytes)
|
||||
logger.ts # 컬러 콘솔 로거
|
||||
time.ts # TZ 기준 날짜/월 계산, UTC timestamp 생성
|
||||
apiKey.ts # API 키 생성 (sk-{timestamp}-{random}, crypto.randomBytes)
|
||||
adminAuth.ts # 관리자 principal 추출, 세션/토큰 인증 미들웨어, CSRF 검증
|
||||
adminSecurity.ts # 비밀번호 hash 검증, 토큰 생성/hash, OIDC state/nonce 처리
|
||||
logger.ts # 컬러 콘솔 로거
|
||||
time.ts # TZ 기준 날짜/월 계산, UTC timestamp 생성
|
||||
```
|
||||
|
||||
## Request Flow
|
||||
|
||||
```
|
||||
Client → auth.ts (API 키 검증, 권한 로드)
|
||||
→ RouterService.selectBackend (허용된 활성 백엔드 중 1개 선택)
|
||||
→ ScriptEngine.applyOnRequestScripts (요청 변조)
|
||||
→ RouterService.forwardRequest (백엔드 프록시)
|
||||
→ ScriptEngine.applyOnResponseScripts (응답 컨텍스트 후처리/검사)
|
||||
→ AnalyticsService.logRequest (집계 + 월별 request_logs 기록)
|
||||
→ Response
|
||||
```text
|
||||
Client -> auth.ts (사용자 API 키 검증, 권한 로드)
|
||||
-> RouterService.selectBackend (허용된 활성 백엔드 중 1개 선택)
|
||||
-> ScriptEngine.applyOnRequestScripts (요청 변조)
|
||||
-> RouterService.forwardRequest (백엔드로 프록시)
|
||||
-> ScriptEngine.applyOnResponseScripts (응답 컨텍스트 후처리 결과)
|
||||
-> AnalyticsService.logRequest (집계 + 월별 request_logs 기록)
|
||||
-> Response
|
||||
```
|
||||
|
||||
참고:
|
||||
- 라우트 마운트는 `server/src/index.ts` 에서 직접 수행한다.
|
||||
- `onResponse` 훅은 실행되지만, 현재 구현에서는 훅 반환값을 최종 HTTP 응답에 다시 반영하지 않는다.
|
||||
- 라우터 마운트는 `server/src/index.ts` 에서 직접 수행한다.
|
||||
- `/admin/**` 는 별도 관리자 인증 레이어를 거친 뒤 각 CRUD/analytics 라우트로 들어간다.
|
||||
- `/admin/auth/*` 만 관리자 영역 내 공개 예외 엔드포인트다.
|
||||
- `onResponse` 훅은 실행되지만 현재 구현에서는 반환값을 최종 HTTP 응답에 다시 반영하지 않는다.
|
||||
|
||||
## Time & Storage
|
||||
|
||||
- DB 루트는 `DB_DIR`로 설정한다. 파일은 `core.db`, `analytics.db`, `request_logs/request_logs_YYYY-MM.db` 구조로 생성된다.
|
||||
- 일/월 경계 계산은 `TZ` 기준으로 수행한다.
|
||||
- 저장되는 timestamp 문자열은 UTC ISO 형식으로 통일한다.
|
||||
- `core.db`에는 관리자 세션과 관리자 API 토큰을 위한 `admin_sessions`, `admin_api_tokens` 테이블도 생성된다.
|
||||
- 일/월 경계 계산은 `TZ` 기준으로 수행된다.
|
||||
- 저장되는 timestamp 문자열은 UTC ISO 형식으로 통일된다.
|
||||
|
||||
## Admin Auth Notes
|
||||
|
||||
- 관리자 브라우저 인증은 서버사이드 세션 + `HttpOnly` 쿠키를 사용한다.
|
||||
- 자동화용 관리자 인증은 Bearer 관리자 API 토큰을 사용한다.
|
||||
- 세션 기반 `POST/PUT/DELETE` 요청에는 CSRF 검사가 적용된다.
|
||||
- OIDC는 generic discovery 방식으로 동작하며, 허용 이메일은 `OIDC_ALLOWED_EMAILS`로 제한한다.
|
||||
- 권장 배포는 public/admin 게이트웨이 분리 구조다. public 쪽은 `/v1/**`, `/health`만 노출하고 admin 쪽은 관리자 프론트와 `/admin/**`를 same-origin으로 제공한다.
|
||||
|
||||
## Dependencies
|
||||
|
||||
|
|
@ -71,14 +88,17 @@ Client → auth.ts (API 키 검증, 권한 로드)
|
|||
## Tests
|
||||
|
||||
통합 테스트: `server/tests/integration/`
|
||||
- `api.test.ts` — 인증, 인가, 프록시 엔드포인트
|
||||
- `admin.test.ts` — Admin CRUD
|
||||
- `routing.test.ts` — 백엔드 선택, 요청 포워딩
|
||||
- `scripts.test.ts` — 스크립트 생성, 실행, 훅
|
||||
- `api.test.ts` - 인증, 에러, 프록시 핸들러
|
||||
- `admin-auth.test.ts` - 관리자 로그인, 세션, CSRF, 관리자 토큰
|
||||
- `admin.test.ts` - Admin CRUD
|
||||
- `routing.test.ts` - 백엔드 선택, 요청 포워딩
|
||||
- `scripts.test.ts` - 스크립트 생성, 실행, 검증
|
||||
|
||||
테스트 유틸: `server/tests/utils/` (`testApp.ts`, `mockBackend.ts`)
|
||||
테스트 유틸: `server/tests/utils/` (`testApp.ts`, `mockBackend.ts`, `adminClient.ts`)
|
||||
|
||||
벤치마크: `server/benchmarks/` (`index.ts`, `runner.ts`, `scenarios.ts`, `report.ts`, `stats.ts`)
|
||||
|
||||
사용 문서:
|
||||
- [docs/benchmarks.md](./benchmarks.md) - benchmark CLI usage, modes, output, caveats
|
||||
- [docs/admin-auth.md](./admin-auth.md) - 관리자 인증, 세션, CSRF, 관리자 토큰
|
||||
- [docs/oidc.md](./oidc.md) - OpenID Connect 설정과 allowlist 정책
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue