refactor: align backend with company-site-member schema and system-level RBAC groups
This commit is contained in:
@@ -1,98 +1,37 @@
|
||||
# memberapi.ose.tw 後端架構(FastAPI)
|
||||
# memberapi.ose.tw 後端架構(公司/品牌站台/會員)
|
||||
|
||||
## 1. 目標與邊界
|
||||
- 網域:`memberapi.ose.tw`
|
||||
- 角色:會員中心後端真相來源(Member + Organization + Permission)
|
||||
- 範圍:
|
||||
- user/member 管理(以 `authentik_sub` 為跨系統主鍵)
|
||||
- organization 管理
|
||||
- member-organization 關聯管理
|
||||
- permission grant/revoke 與 snapshot
|
||||
- internal 查詢 API 提供其他系統
|
||||
- 不在本服務處理:
|
||||
- 前端 UI/互動流程
|
||||
- Authentik hosted login page 本身
|
||||
## 核心主檔(對齊 DB Schema)
|
||||
- `users`:會員
|
||||
- `companies`:公司
|
||||
- `sites`:品牌站台(隸屬 company)
|
||||
- `systems`:系統層(member/mkt/...)
|
||||
- `modules`:模組(使用 `system.module` key)
|
||||
|
||||
## 2. 技術棧
|
||||
- Python 3.12
|
||||
- FastAPI
|
||||
- SQLAlchemy 2.0
|
||||
- PostgreSQL(psycopg)
|
||||
- Pydantic Settings
|
||||
## 權限模型
|
||||
- 直接權限:`user_scope_permissions`
|
||||
- 群組權限:`permission_groups` + `permission_group_members` + `permission_group_permissions`
|
||||
- Snapshot 回傳:合併「user 直接 + group」去重
|
||||
|
||||
## 3. 後端目錄
|
||||
- `backend/app/main.py`
|
||||
- `backend/app/api/`
|
||||
- `auth.py`
|
||||
- `me.py`
|
||||
- `admin.py`(permission)
|
||||
- `admin_entities.py`(member/org)
|
||||
- `internal.py`
|
||||
- `internal_entities.py`
|
||||
- `backend/app/models/`
|
||||
- `user.py`
|
||||
- `permission.py`
|
||||
- `organization.py`
|
||||
- `member_organization.py`
|
||||
- `api_client.py`
|
||||
- `backend/app/repositories/`
|
||||
- `users_repo.py`
|
||||
- `permissions_repo.py`
|
||||
- `organizations_repo.py`
|
||||
- `member_organizations_repo.py`
|
||||
## 授權層級
|
||||
- `system` 必填
|
||||
- `module` 選填
|
||||
- 有值:`{system}.{module}`(例:`mkt.campaign`)
|
||||
- 無值:系統層權限,使用 `system.__system__`
|
||||
|
||||
## 4. 資料模型
|
||||
- `users`
|
||||
- `id`, `authentik_sub`(unique), `authentik_user_id`, `email`, `display_name`, `is_active`, timestamps
|
||||
- `permissions`
|
||||
- `id`, `user_id`, `scope_type`, `scope_id`, `module`, `action`, `created_at`
|
||||
- unique: `(user_id, scope_type, scope_id, module, action)`
|
||||
- `organizations`
|
||||
- `id`, `org_code`(unique), `name`, `tax_id`, `status`, timestamps
|
||||
- `member_organizations`
|
||||
- `id`, `member_id`, `organization_id`, `created_at`
|
||||
- unique: `(member_id, organization_id)`
|
||||
- `api_clients`(由 `docs/API_CLIENTS_SQL.sql` 建立)
|
||||
- `client_key`, `api_key_hash`, `status`, allowlist, expires/rate-limit 欄位
|
||||
## 主要 API
|
||||
- `GET /me`
|
||||
- `GET /me/permissions/snapshot`
|
||||
- `POST /admin/permissions/grant|revoke`
|
||||
- `GET|POST /admin/systems`
|
||||
- `GET|POST /admin/modules`
|
||||
- `GET|POST /admin/companies`
|
||||
- `GET|POST /admin/sites`
|
||||
- `GET /admin/members`
|
||||
- `GET|POST /admin/permission-groups`
|
||||
- `POST|DELETE /admin/permission-groups/{group_key}/members/{authentik_sub}`
|
||||
- `POST /admin/permission-groups/{group_key}/permissions/grant|revoke`
|
||||
- `GET /internal/systems|modules|companies|sites|members`
|
||||
|
||||
## 5. API 設計
|
||||
- 健康檢查
|
||||
- `GET /healthz`
|
||||
- 登入
|
||||
- `GET /auth/oidc/url`
|
||||
- `POST /auth/oidc/exchange`
|
||||
- 使用者路由(Bearer)
|
||||
- `GET /me`
|
||||
- `GET /me/permissions/snapshot`
|
||||
- 管理路由(`X-Client-Key` + `X-API-Key`)
|
||||
- `POST /admin/permissions/grant`
|
||||
- `POST /admin/permissions/revoke`
|
||||
- `GET|POST|PATCH /admin/organizations...`
|
||||
- `GET|POST|PATCH /admin/members...`
|
||||
- `GET|POST|DELETE /admin/members/{member_id}/organizations...`
|
||||
- 內部路由(`X-Internal-Secret`)
|
||||
- `POST /internal/users/upsert-by-sub`
|
||||
- `GET /internal/permissions/{authentik_sub}/snapshot`
|
||||
- `POST /internal/authentik/users/ensure`
|
||||
- `GET /internal/members`
|
||||
- `GET /internal/members/by-sub/{authentik_sub}`
|
||||
- `GET /internal/organizations`
|
||||
- `GET /internal/organizations/by-code/{org_code}`
|
||||
- `GET /internal/members/{member_id}/organizations`
|
||||
|
||||
## 6. 安全策略
|
||||
- `admin` 路由:API client 驗證(status/過期/hash/allowlist)
|
||||
- `internal` 路由:`X-Internal-Secret`
|
||||
- `me` 路由:Auth token + JWKS 驗簽
|
||||
- `/me` 補值:若 token 無 `email/name`,會呼叫 `userinfo` 補齊
|
||||
|
||||
## 7. 與其他系統資料流
|
||||
1. 其他系統登入後,以 token `sub` 呼叫 `/internal/users/upsert-by-sub`
|
||||
2. 需要組織/會員資料時,走 `/internal/members*`、`/internal/organizations*`
|
||||
3. 權限查詢走 `/internal/permissions/{sub}/snapshot`
|
||||
4. 後台人員調整權限走 `/admin/permissions/grant|revoke`
|
||||
|
||||
## 8. 下一階段建議
|
||||
- 導入 Alembic migration
|
||||
- 加 audit log(誰在何時做了 member/org/permission 變更)
|
||||
- 補上整合測試與 rate-limit metrics
|
||||
## DB Migration
|
||||
- 初始化:`backend/scripts/init_schema.sql`
|
||||
- 舊庫補齊:`backend/scripts/migrate_align_company_site_member_system.sql`
|
||||
|
||||
@@ -2,229 +2,99 @@
|
||||
|
||||
Base URL:`https://memberapi.ose.tw`
|
||||
|
||||
## 0. OIDC 登入(目前主流程)
|
||||
### GET `/auth/oidc/url?redirect_uri=<frontend-callback-url>`
|
||||
200 Response:
|
||||
```json
|
||||
{
|
||||
"authorize_url": "https://auth.ose.tw/application/o/authorize/..."
|
||||
}
|
||||
```
|
||||
|
||||
### POST `/auth/oidc/exchange`
|
||||
Request:
|
||||
```json
|
||||
{
|
||||
"code": "authorization-code",
|
||||
"redirect_uri": "http://localhost:5173/auth/callback"
|
||||
}
|
||||
```
|
||||
|
||||
200 Response:
|
||||
```json
|
||||
{
|
||||
"access_token": "<jwt>",
|
||||
"token_type": "Bearer",
|
||||
"expires_in": 3600,
|
||||
"scope": "openid profile email"
|
||||
}
|
||||
```
|
||||
## 0. OIDC 登入
|
||||
- `GET /auth/oidc/url?redirect_uri=...`
|
||||
- `POST /auth/oidc/exchange`
|
||||
|
||||
## 1. 使用者資訊
|
||||
### GET `/me`
|
||||
Headers:
|
||||
- `Authorization: Bearer <access_token>`
|
||||
- `GET /me`
|
||||
- `GET /me/permissions/snapshot`
|
||||
|
||||
200 Response:
|
||||
`permissions` item:
|
||||
```json
|
||||
{
|
||||
"sub": "authentik-sub-123",
|
||||
"email": "user@example.com",
|
||||
"display_name": "User Name"
|
||||
"scope_type": "company|site",
|
||||
"scope_id": "company_key_or_site_key",
|
||||
"system": "mkt",
|
||||
"module": "mkt.campaign",
|
||||
"action": "view"
|
||||
}
|
||||
```
|
||||
|
||||
401 Error:
|
||||
```json
|
||||
{ "detail": "missing_bearer_token" }
|
||||
```
|
||||
或
|
||||
```json
|
||||
{ "detail": "invalid_bearer_token" }
|
||||
```
|
||||
|
||||
## 2. 我的權限快照
|
||||
### GET `/me/permissions/snapshot`
|
||||
## 2. 權限(User 直接授權)
|
||||
Headers:
|
||||
- `Authorization: Bearer <access_token>`
|
||||
- `X-Client-Key`
|
||||
- `X-API-Key`
|
||||
|
||||
200 Response:
|
||||
```json
|
||||
{
|
||||
"authentik_sub": "authentik-sub-123",
|
||||
"permissions": [
|
||||
{
|
||||
"scope_type": "site",
|
||||
"scope_id": "tw-main",
|
||||
"module": "campaign",
|
||||
"action": "view"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
## 3. Grant 權限
|
||||
### POST `/admin/permissions/grant`
|
||||
Headers:
|
||||
- `X-Client-Key: <client_key>`
|
||||
- `X-API-Key: <plain_api_key>`
|
||||
|
||||
Request:
|
||||
```json
|
||||
{
|
||||
"authentik_sub": "authentik-sub-123",
|
||||
"authentik_sub": "authentik-sub",
|
||||
"email": "user@example.com",
|
||||
"display_name": "User Name",
|
||||
"scope_type": "site",
|
||||
"scope_id": "tw-main",
|
||||
"display_name": "User",
|
||||
"scope_type": "company",
|
||||
"scope_id": "ose-main",
|
||||
"system": "mkt",
|
||||
"module": "campaign",
|
||||
"action": "view"
|
||||
}
|
||||
```
|
||||
|
||||
200 Response:
|
||||
```json
|
||||
{
|
||||
"permission_id": "uuid",
|
||||
"result": "granted"
|
||||
}
|
||||
```
|
||||
|
||||
## 4. Revoke 權限
|
||||
### POST `/admin/permissions/revoke`
|
||||
Headers:
|
||||
- `X-Client-Key: <client_key>`
|
||||
- `X-API-Key: <plain_api_key>`
|
||||
|
||||
Request:
|
||||
```json
|
||||
{
|
||||
"authentik_sub": "authentik-sub-123",
|
||||
"authentik_sub": "authentik-sub",
|
||||
"scope_type": "site",
|
||||
"scope_id": "tw-main",
|
||||
"system": "mkt",
|
||||
"module": "campaign",
|
||||
"action": "view"
|
||||
}
|
||||
```
|
||||
|
||||
200 Response:
|
||||
```json
|
||||
{
|
||||
"deleted": 1,
|
||||
"result": "revoked"
|
||||
}
|
||||
```
|
||||
說明:
|
||||
- `module` 可省略,代表系統層權限,後端會使用 `system.__system__`。
|
||||
- `module` 有值時會組成 `{system}.{module}` 存入(例如 `mkt.campaign`)。
|
||||
|
||||
404 Response:
|
||||
```json
|
||||
{ "detail": "user_not_found" }
|
||||
```
|
||||
|
||||
## 5. Health Check
|
||||
### GET `/healthz`
|
||||
200 Response:
|
||||
```json
|
||||
{ "status": "ok" }
|
||||
```
|
||||
|
||||
## 6. 組織管理(admin)
|
||||
### GET `/admin/organizations`
|
||||
## 3. 主資料管理(admin)
|
||||
Headers:
|
||||
- `X-Client-Key: <client_key>`
|
||||
- `X-API-Key: <plain_api_key>`
|
||||
- `X-Client-Key`
|
||||
- `X-API-Key`
|
||||
|
||||
Query:
|
||||
- `keyword` (optional)
|
||||
- `status` (optional: `active|inactive`)
|
||||
- `limit` (default `50`)
|
||||
- `offset` (default `0`)
|
||||
- `GET/POST /admin/systems`
|
||||
- `GET/POST /admin/modules`
|
||||
- `GET/POST /admin/companies`
|
||||
- `GET/POST /admin/sites`
|
||||
- `GET /admin/members`
|
||||
|
||||
### POST `/admin/organizations`
|
||||
```json
|
||||
{
|
||||
"org_code": "ose-main",
|
||||
"name": "Ose Main",
|
||||
"tax_id": "12345678",
|
||||
"status": "active"
|
||||
}
|
||||
```
|
||||
|
||||
### PATCH `/admin/organizations/{org_id}`
|
||||
```json
|
||||
{
|
||||
"name": "Ose Main Updated",
|
||||
"status": "inactive"
|
||||
}
|
||||
```
|
||||
|
||||
### POST `/admin/organizations/{org_id}/activate`
|
||||
### POST `/admin/organizations/{org_id}/deactivate`
|
||||
|
||||
## 7. 會員管理(admin)
|
||||
### GET `/admin/members`
|
||||
## 4. 權限群組(一組權限綁多個 user)
|
||||
Headers:
|
||||
- `X-Client-Key: <client_key>`
|
||||
- `X-API-Key: <plain_api_key>`
|
||||
- `X-Client-Key`
|
||||
- `X-API-Key`
|
||||
|
||||
Query:
|
||||
- `keyword` (optional)
|
||||
- `is_active` (optional: `true|false`)
|
||||
- `limit` (default `50`)
|
||||
- `offset` (default `0`)
|
||||
- `GET/POST /admin/permission-groups`
|
||||
- `POST /admin/permission-groups/{group_key}/members/{authentik_sub}`
|
||||
- `DELETE /admin/permission-groups/{group_key}/members/{authentik_sub}`
|
||||
- `POST /admin/permission-groups/{group_key}/permissions/grant`
|
||||
- `POST /admin/permission-groups/{group_key}/permissions/revoke`
|
||||
|
||||
### POST `/admin/members`
|
||||
```json
|
||||
{
|
||||
"authentik_sub": "authentik-sub-123",
|
||||
"email": "user@example.com",
|
||||
"display_name": "User Name",
|
||||
"is_active": true
|
||||
}
|
||||
```
|
||||
群組授權 payload 與 user 授權 payload 相同(用 `system/module/scope/action`)。
|
||||
|
||||
### PATCH `/admin/members/{member_id}`
|
||||
```json
|
||||
{
|
||||
"display_name": "New Name",
|
||||
"is_active": false
|
||||
}
|
||||
```
|
||||
|
||||
### POST `/admin/members/{member_id}/activate`
|
||||
### POST `/admin/members/{member_id}/deactivate`
|
||||
|
||||
## 8. 會員/組織關聯(admin)
|
||||
### GET `/admin/members/{member_id}/organizations`
|
||||
### POST `/admin/members/{member_id}/organizations/{org_id}`
|
||||
### DELETE `/admin/members/{member_id}/organizations/{org_id}`
|
||||
|
||||
## 9. 系統對系統查詢(internal)
|
||||
## 5. Internal 查詢 API(其他系統)
|
||||
Headers:
|
||||
- `X-Internal-Secret: <internal_shared_secret>`
|
||||
- `X-Internal-Secret`
|
||||
|
||||
Endpoints:
|
||||
- `GET /internal/systems`
|
||||
- `GET /internal/modules`
|
||||
- `GET /internal/companies`
|
||||
- `GET /internal/sites`
|
||||
- `GET /internal/members`
|
||||
- `GET /internal/members/by-sub/{authentik_sub}`
|
||||
- `GET /internal/organizations`
|
||||
- `GET /internal/organizations/by-code/{org_code}`
|
||||
- `GET /internal/members/{member_id}/organizations`
|
||||
- `GET /internal/permissions/{authentik_sub}/snapshot`
|
||||
|
||||
## 10. 常見錯誤碼
|
||||
## 6. 常見錯誤
|
||||
- `401 invalid_client`
|
||||
- `401 invalid_api_key`
|
||||
- `401 client_expired`
|
||||
- `403 origin_not_allowed`
|
||||
- `403 ip_not_allowed`
|
||||
- `403 path_not_allowed`
|
||||
- `503 internal_secret_not_configured`
|
||||
- `503 authentik_admin_not_configured`
|
||||
- `401 invalid_internal_secret`
|
||||
- `404 system_not_found`
|
||||
- `404 company_not_found`
|
||||
- `404 site_not_found`
|
||||
|
||||
Reference in New Issue
Block a user