Initial project

This commit is contained in:
warrenchen 2026-01-30 16:57:29 +09:00
commit 5d7b2e79ea
12 changed files with 1184 additions and 0 deletions

73
.gitignore vendored Normal file
View File

@ -0,0 +1,73 @@
# Build results
bin/
obj/
[Bb]uild/
[Bb]uilds/
# Visual Studio
.vs/
*.suo
*.user
*.userosscache
*.sln.docstates
*.vcxproj.user
*.vcxproj.filters
*.vcxproj.user
# Rider / JetBrains
.idea/
*.sln.iml
# VS Code
.vscode/
# Visual Studio Code local history
.history/
# .NET Core
project.lock.json
project.fragment.lock.json
artifacts/
# NuGet
*.nupkg
*.snupkg
# The packages folder can be ignored because of PackageReference
# Uncomment if using packages.config
#packages/
# Logs
*.log
# User-specific files
*.userprefs
# Resharper / DotCover
_ReSharper*/
*.[Rr]e[Ss]harper
*.DotSettings.user
# Test results
TestResults/
*.trx
# Coverage
*.coverage
*.coveragexml
coverage/
# ASP.NET Scaffolding
ScaffoldingReadMe.txt
# Secrets
secrets.json
appsettings.*.json
!appsettings.json
# OS
.DS_Store
Thumbs.db
# Others
*.swp
*.tmp

51
README.md Normal file
View File

@ -0,0 +1,51 @@
# 會員平台Member Center
此專案為多網站共用的會員登入中心與電子報訂閱管理平台。文件統一放在 `docs/`
## 目標MVP
- 多個網站共用的會員登入中心SSO
- OAuth2 + OIDC 登入API 與 Redirect 兩種方式)
- 電子報訂閱管理(含未註冊訂閱)
- 未註冊訂閱者可自行取消訂閱
- 未註冊訂閱者未來註冊可沿用訂閱資料
## 既定條件
- OAuth2 + OIDC
- 會員中心只負責 Email/訂閱管理,發送系統另建
- 訂閱採 double opt-in
- 各站各自設計 UI主要走 API少數狀況使用 redirect
- 會員資料跨站共享(邏輯隔離)
- 訂閱事件同步採 event/queue
- DBPostgreSQL
- 開發C# .NET Core + MVC + OpenIddict
## 範圍(第一階段)
- 多租戶(站點)概念與基本設定
- 會員與訂閱者 Email 身分識別
- OAuth2/OIDC 授權Authorization Code + PKCE
- 訂閱/取消訂閱流程(含未註冊)
- 基本管理後台(核心 CRUD
## 非目標(暫不處理)
- 第三方社群登入
- MFA
- 行銷自動化或行為追蹤
- 金流/付費會員
## 文件
- `docs/DESIGN.md`
- `docs/UI.md`
- `docs/USE_CASES.md`
- `docs/FLOWS.md`
- `docs/OPENAPI.md`
- `docs/openapi.yaml`
- `docs/SCHEMA.sql`
- `docs/TECH_STACK.md`
- `docs/SEED.sql`
- `docs/INSTALL.md`
## 待確認事項
- OAuth2 scopes 與最小 claimsemail/profile
- 事件系統選擇Kafka/RabbitMQ/SNS+SQS
- 取消訂閱的 UI 形式(純一鍵或提供偏好)
- GDPR/CCPA 資料匯出與刪除是否納入第一期

124
docs/DESIGN.md Normal file
View File

@ -0,0 +1,124 @@
# 系統設計草案Member Center
日期2026-01-30
## 1. 需求摘要(已確認)
1) 多個網站共用的會員登入中心SSO
2) 電子報訂閱管理(發送另建)
3) 未註冊會員可訂閱;註冊後沿用訂閱資料
4) 未註冊會員可取消訂閱(單一清單退訂)
5) 登入需支援 API 與 Redirect 兩種方式OAuth2 + OIDC
## 2. 架構原則
- OAuth2 + OIDCAuthorization Code + PKCE
- 會員中心只管理 Email 與訂閱狀態
- Double Opt-in
- 各站自行設計 UI主要走 API少數狀況使用 redirect
- 多租戶為邏輯隔離,但會員資料跨站共享
- 訂閱狀態同步採 event/queue
- PostgreSQL
- 實作C# .NET Core + MVC + OpenIddict
## 3. 使用者故事(精簡)
- 訪客:在任一站點未登入狀態下輸入 Email 訂閱電子報
- 訪客:在信件中點擊連結完成 double opt-in
- 訪客:點擊取消訂閱連結即可退訂(單一清單)
- 會員在任一站點登入後其他站點可無痛登入SSO
- 站點後台:可管理站點資訊、訂閱清單、會員基本資料
## 4. 核心模組
- Identity Service註冊、登入、密碼重設、Email 驗證
- OAuth2/OIDC Service授權流程、token 發放、ID Token
- Subscription Service訂閱/退訂/偏好管理
- Admin Console租戶與清單管理
- Mailer Integration驗證信/退訂信/確認信的發送介面(外部系統)
- Event Publisher訂閱事件發佈供發信系統或數據系統消費
## 5. 資料模型(概念)
- tenants
- id, name, domains, status, created_at
- users
- id, email, password_hash, email_verified_at, status, created_at
- oauth_clients
- id, tenant_id, name, redirect_uris, client_type, created_at
- oauth_codes
- id, client_id, user_id, code_hash, code_challenge, expires_at, consumed_at
- oauth_tokens
- id, user_id, client_id, access_token_hash, refresh_token_hash, expires_at, revoked_at
- oidc_id_tokens
- id, user_id, client_id, id_token_hash, expires_at
- newsletter_lists
- id, tenant_id, name, status, created_at
- newsletter_subscriptions
- id, list_id, tenant_id, email, user_id (nullable), status, preferences, created_at
- email_verifications
- id, email, tenant_id, token_hash, purpose, expires_at, consumed_at
- unsubscribe_tokens
- id, subscription_id, token_hash, expires_at, consumed_at
- audit_logs
- id, actor_type, actor_id, action, payload, created_at
關聯說明:
- newsletter_subscriptions.email 與 users.email 維持唯一性關聯
- 使用者註冊時,如 email 存在訂閱紀錄,補上 user_id
- 單一清單退訂unsubscribe token 綁定 subscription_id
## 6. 核心流程
### 6.1 OAuth2/OIDC Redirect 登入Authorization Code + PKCE
1) 站點導向 `/oauth/authorize`,帶 `client_id`, `redirect_uri`, `code_challenge`, `scope=openid email`
2) 使用者於會員中心登入
3) 成功後導回 `redirect_uri` 並附 `code`
4) 站點以 `code` + `code_verifier``/oauth/token` 換取 token + `id_token`
### 6.2 OAuth2 API 使用(站點自行 UI
1) 站點以 API 驗證使用者登入(會員中心提供 login API
2) 成功後取得 token含 ID Token 可選)
3) 站點以 access_token 呼叫其他會員中心 API
### 6.3 未登入狀態的訂閱流程(在獨立平台)
1) 使用者在各站點輸入 Email 並選擇訂閱清單
2) 站點呼叫 `POST /newsletter/subscribe`
3) 會員中心建立 `pending` 訂閱並發送驗證信(透過外部發信系統)
4) 使用者點擊信件連結 `/newsletter/confirm?token=...`
5) 訂閱狀態改為 `active`
6) 會員中心發出事件 `subscription.activated` 到 event/queue
### 6.4 未登入退訂(單一清單)
1) 信件提供「一鍵退訂」連結 `/newsletter/unsubscribe?token=...`
2) 驗證 token 後將該訂閱標記為 `unsubscribed`
3) 會員中心發出事件 `subscription.unsubscribed` 到 event/queue
### 6.5 註冊後銜接
1) 使用者完成註冊
2) 系統搜尋 `newsletter_subscriptions.email`
3) 將 `user_id` 補上並保留偏好
4) 可選:發出事件 `subscription.linked_to_user`
## 7. API 介面(草案)
- GET `/oauth/authorize`
- POST `/oauth/token`
- GET `/.well-known/openid-configuration`
- POST `/auth/login` (API-only login)
- POST `/auth/refresh`
- POST `/newsletter/subscribe`
- GET `/newsletter/confirm`
- POST `/newsletter/unsubscribe`
- GET `/newsletter/preferences`
- POST `/newsletter/preferences`
## 8. 安全與合規
- 密碼強度與防暴力破解rate limit + lockout
- Token rotation + refresh token revoke
- Redirect URI 白名單 + PKCE
- Double opt-in可配置
- Audit log
- GDPR/CCPA資料匯出與刪除規劃中
## 9. 其他文件
- `docs/UI.md`
- `docs/USE_CASES.md`
- `docs/FLOWS.md`
- `docs/OPENAPI.md`
- `docs/SCHEMA.sql`
- `docs/TECH_STACK.md`

47
docs/FLOWS.md Normal file
View File

@ -0,0 +1,47 @@
# Flows
流程以「API 自建 UI」與「會員中心統一 UI」兩種模式描述。
## F-01 註冊會員
- [API] 站點送出 `POST /auth/register`(待補 API
- [API] 會員中心建立 user、寄送驗證信
- [UI] 導向會員中心註冊頁完成註冊
- [UI] 會員中心寄送驗證信
## F-02 登入OAuth2 + OIDC
- [API] 站點送出 `POST /auth/login` 取得 access_token + id_token
- [API] 站點建立自身 session
- [UI] 導向 `/oauth/authorize` 完成授權碼流程
- [UI] 站點用 code 換 token + id_token
## F-03 忘記密碼 / 重設密碼
- [API] 站點送出 `POST /auth/password/forgot`
- [UI] 會員中心頁提交 email 並發送重設信
- [API/UI] 使用 token 進入重設密碼頁
## F-04 訂閱電子報(未登入)
- [API] 站點送出 `POST /newsletter/subscribe`
- [API] 會員中心建立 pending 訂閱並發送驗證信
- [UI] 使用者點擊驗證信連結 `/newsletter/confirm?token=...`
- [UI] 訂閱改為 active發出 event `subscription.activated`
## F-05 取消訂閱(單一清單)
- [UI] 使用者點擊退訂連結 `/newsletter/unsubscribe?token=...`
- [UI] 訂閱狀態改為 unsubscribed
- [API] 發出 event `subscription.unsubscribed`
## F-06 訂閱偏好管理(登入後)
- [API] 站點讀取 `/newsletter/preferences`
- [API] 站點更新 `/newsletter/preferences`
- [UI] 會員中心提供偏好頁(可選)
## F-07 會員資料查看
- [API] 站點讀取 `/user/profile`
- [UI] 會員中心提供個人資料頁
## F-08 管理者管理租戶/清單/Client
- [UI] 會員中心管理後台進行 CRUD
## F-09 訂閱與會員綁定
- [API] 使用者完成註冊後,會員中心將訂閱資料與 user_id 綁定
- [API] 發送事件 `subscription.linked_to_user`

16
docs/INSTALL.md Normal file
View File

@ -0,0 +1,16 @@
# 安裝與初始化Schema + Seed
提供一個簡單安裝流程,將 DB schema 與種子資料一起初始化。
## 內容
- Schema`docs/SCHEMA.sql`
- Seed`docs/SEED.sql`
## 建議流程(範例)
1) 建立資料庫
2) 執行 `SCHEMA.sql`
3) 執行 `SEED.sql`(包含預設 admin 角色與初始帳號)
## 注意事項
- `docs/SEED.sql``admin@example.com``<PASSWORD_HASH>` 為預設值,請部署前替換
- 若使用 ASP.NET Core Identity請使用對應的密碼雜湊格式

18
docs/OPENAPI.md Normal file
View File

@ -0,0 +1,18 @@
# OpenAPI 草案(完整)
已補上完整端點與資料結構,並提供 `docs/openapi.yaml` 作為可直接擴充的版本。
## 版本
- OpenAPI: 3.1.0
- 檔案:`docs/openapi.yaml`
## 核心資源
- OAuth2/OIDC授權、token、discovery、JWKS
- Auth註冊、登入、刷新、登出、忘記/重設密碼、Email 驗證
- User個人資料
- Newsletter訂閱/確認/退訂/偏好
- AdminTenants/Lists/OAuth ClientsMVP CRUD
## Security Schemes
- OAuth2 (Authorization Code + PKCE)
- Bearer JWTAPI 使用)

144
docs/SCHEMA.sql Normal file
View File

@ -0,0 +1,144 @@
-- PostgreSQL schema draft (MVP)
CREATE TABLE tenants (
id UUID PRIMARY KEY,
name TEXT NOT NULL,
domains TEXT[] NOT NULL DEFAULT '{}',
status TEXT NOT NULL DEFAULT 'active',
created_at TIMESTAMPTZ NOT NULL DEFAULT now()
);
CREATE TABLE users (
id UUID PRIMARY KEY,
email TEXT NOT NULL UNIQUE,
password_hash TEXT NOT NULL,
email_verified_at TIMESTAMPTZ,
status TEXT NOT NULL DEFAULT 'active',
created_at TIMESTAMPTZ NOT NULL DEFAULT now()
);
CREATE TABLE oauth_clients (
id UUID PRIMARY KEY,
tenant_id UUID NOT NULL REFERENCES tenants(id),
name TEXT NOT NULL,
redirect_uris TEXT[] NOT NULL DEFAULT '{}',
client_type TEXT NOT NULL DEFAULT 'confidential',
client_secret_hash TEXT,
created_at TIMESTAMPTZ NOT NULL DEFAULT now()
);
ALTER TABLE oauth_clients
ADD CONSTRAINT chk_oauth_clients_secret
CHECK (
(client_type = 'confidential' AND client_secret_hash IS NOT NULL)
OR (client_type <> 'confidential')
);
CREATE TABLE oauth_codes (
id UUID PRIMARY KEY,
client_id UUID NOT NULL REFERENCES oauth_clients(id),
user_id UUID NOT NULL REFERENCES users(id),
code_hash TEXT NOT NULL,
code_challenge TEXT NOT NULL,
code_challenge_method TEXT NOT NULL DEFAULT 'S256',
expires_at TIMESTAMPTZ NOT NULL,
consumed_at TIMESTAMPTZ
);
CREATE TABLE oauth_tokens (
id UUID PRIMARY KEY,
user_id UUID NOT NULL REFERENCES users(id),
client_id UUID NOT NULL REFERENCES oauth_clients(id),
access_token_hash TEXT NOT NULL,
refresh_token_hash TEXT NOT NULL,
expires_at TIMESTAMPTZ NOT NULL,
revoked_at TIMESTAMPTZ
);
CREATE TABLE oidc_id_tokens (
id UUID PRIMARY KEY,
user_id UUID NOT NULL REFERENCES users(id),
client_id UUID NOT NULL REFERENCES oauth_clients(id),
id_token_hash TEXT NOT NULL,
expires_at TIMESTAMPTZ NOT NULL
);
CREATE TABLE newsletter_lists (
id UUID PRIMARY KEY,
tenant_id UUID NOT NULL REFERENCES tenants(id),
name TEXT NOT NULL,
status TEXT NOT NULL DEFAULT 'active',
created_at TIMESTAMPTZ NOT NULL DEFAULT now()
);
CREATE TABLE newsletter_subscriptions (
id UUID PRIMARY KEY,
list_id UUID NOT NULL REFERENCES newsletter_lists(id),
email TEXT NOT NULL,
user_id UUID REFERENCES users(id),
status TEXT NOT NULL DEFAULT 'pending',
preferences JSONB NOT NULL DEFAULT '{}'::jsonb,
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
UNIQUE (list_id, email)
);
CREATE TABLE email_verifications (
id UUID PRIMARY KEY,
email TEXT NOT NULL,
tenant_id UUID NOT NULL REFERENCES tenants(id),
token_hash TEXT NOT NULL,
purpose TEXT NOT NULL,
expires_at TIMESTAMPTZ NOT NULL,
consumed_at TIMESTAMPTZ
);
CREATE TABLE unsubscribe_tokens (
id UUID PRIMARY KEY,
subscription_id UUID NOT NULL REFERENCES newsletter_subscriptions(id),
token_hash TEXT NOT NULL,
expires_at TIMESTAMPTZ NOT NULL,
consumed_at TIMESTAMPTZ
);
CREATE TABLE audit_logs (
id UUID PRIMARY KEY,
actor_type TEXT NOT NULL,
actor_id UUID,
action TEXT NOT NULL,
payload JSONB NOT NULL DEFAULT '{}'::jsonb,
created_at TIMESTAMPTZ NOT NULL DEFAULT now()
);
CREATE TABLE roles (
id UUID PRIMARY KEY,
name TEXT NOT NULL UNIQUE,
created_at TIMESTAMPTZ NOT NULL DEFAULT now()
);
CREATE TABLE user_roles (
user_id UUID NOT NULL REFERENCES users(id),
role_id UUID NOT NULL REFERENCES roles(id),
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
PRIMARY KEY (user_id, role_id)
);
CREATE INDEX idx_newsletter_subscriptions_email
ON newsletter_subscriptions(email);
CREATE INDEX idx_newsletter_subscriptions_list_id
ON newsletter_subscriptions(list_id);
CREATE INDEX idx_oauth_tokens_user_id
ON oauth_tokens(user_id);
CREATE INDEX idx_oauth_tokens_client_id
ON oauth_tokens(client_id);
CREATE INDEX idx_oauth_codes_client_id
ON oauth_codes(client_id);
CREATE INDEX idx_email_verifications_email
ON email_verifications(email);
CREATE INDEX idx_user_roles_role_id
ON user_roles(role_id);

28
docs/SEED.sql Normal file
View File

@ -0,0 +1,28 @@
-- Seed data for roles and initial admin
-- Roles
INSERT INTO roles (id, name)
VALUES
(gen_random_uuid(), 'admin'),
(gen_random_uuid(), 'support')
ON CONFLICT (name) DO NOTHING;
-- Initial admin user (placeholder password hash)
-- Replace password_hash with your identity provider hash.
INSERT INTO users (id, email, password_hash, email_verified_at, status)
VALUES (
gen_random_uuid(),
'admin@example.com',
'<PASSWORD_HASH>',
now(),
'active'
)
ON CONFLICT (email) DO NOTHING;
-- Assign admin role to the admin user
INSERT INTO user_roles (user_id, role_id)
SELECT u.id, r.id
FROM users u
JOIN roles r ON r.name = 'admin'
WHERE u.email = 'admin@example.com'
ON CONFLICT DO NOTHING;

15
docs/TECH_STACK.md Normal file
View File

@ -0,0 +1,15 @@
# 技術堆疊(已選定)
## 核心
- 語言/框架C# .NET Core + MVC
- OAuth2/OIDCOpenIddict
- DBPostgreSQL
- 事件同步Event/Queue
## 建議元件
- ORMEF Core + Npgsql
- AuthASP.NET Core Identity
- 背景任務Hangfire 或 Quartz.NET
- 事件匯流MassTransit + RabbitMQ/Kafka依部署環境擇一
- 測試xUnit + Testcontainers
- 可觀測性OpenTelemetry + Serilog

35
docs/UI.md Normal file
View File

@ -0,0 +1,35 @@
# UI 規劃(管理者 / 會員)
## 會員中心(統一 UI
### 會員端
- 註冊 / 登入 / 忘記密碼
- Email 驗證
- 個人資料Email 為主)
- 訂閱管理(清單與偏好)
- 退訂(單一清單)
- 連結外站(可選:回到來源站點)
### 管理者端
- 租戶管理Tenant CRUD
- OAuth Client 管理redirect_uris / scopes
- 電子報清單管理Lists CRUD
- 訂閱查詢 / 匯出
- 審計紀錄查詢
- 系統設定安全策略、token 時效)
## 各站自建 UIAPI
### 會員端
- 登入 / 註冊 / 忘記密碼
- Email 驗證頁(可自建或導回會員中心)
- 訂閱表單(未登入)
- 訂閱偏好管理(登入後)
- 退訂頁(從 email token 進來)
### 管理者端
- 站內顯示會員資料(只讀)
- 站內訂閱狀態顯示
## UI 核心原則
- 會員中心 UI 為統一入口(少數情境)
- 其餘皆走 API 與各站自建 UI
- 會員中心 UI 不承擔行銷內容或寄送

30
docs/USE_CASES.md Normal file
View File

@ -0,0 +1,30 @@
# Use Cases
標記:
- [API] 代表各站自建 UI透過 API 完成
- [UI] 代表使用會員中心統一 UI
## 會員端
- UC-01 註冊會員 [API/UI]
- UC-02 登入(取得 token [API/UI]
- UC-03 登出 [API/UI]
- UC-04 忘記密碼 / 重設密碼 [API/UI]
- UC-05 Email 驗證 [API/UI]
- UC-06 訂閱電子報(未登入) [API]
- UC-07 訂閱確認double opt-in [UI]
- UC-08 取消訂閱(單一清單) [UI]
- UC-09 訂閱偏好管理(登入後) [API/UI]
- UC-10 會員資料查看Email 為主) [API/UI]
## 管理者端
- UC-11 租戶管理 [UI]
- UC-12 OAuth Client 管理 [UI]
- UC-13 電子報清單管理 [UI]
- UC-14 訂閱查詢 / 匯出 [UI]
- UC-15 審計紀錄查詢 [UI]
- UC-16 安全策略設定token 時效等) [UI]
## 系統 / 整合
- UC-17 發送訂閱事件event/queue [API]
- UC-18 發送退訂事件event/queue [API]
- UC-19 訂閱與會員綁定 [API]

603
docs/openapi.yaml Normal file
View File

@ -0,0 +1,603 @@
openapi: 3.1.0
info:
title: Member Center API
version: 0.1.0
description: OAuth2/OIDC + Newsletter Subscription API
servers:
- url: /api
security:
- BearerAuth: []
paths:
/oauth/authorize:
get:
summary: OAuth2 Authorization Endpoint
description: Authorization Code + PKCE flow
parameters:
- in: query
name: client_id
required: true
schema: { type: string }
- in: query
name: redirect_uri
required: true
schema: { type: string }
- in: query
name: response_type
required: true
schema: { type: string, enum: [code] }
- in: query
name: scope
required: true
schema: { type: string }
- in: query
name: code_challenge
required: true
schema: { type: string }
- in: query
name: code_challenge_method
required: true
schema: { type: string, enum: [S256] }
- in: query
name: state
required: false
schema: { type: string }
responses:
'302':
description: Redirect to client with code
/oauth/token:
post:
summary: OAuth2 Token Endpoint
requestBody:
required: true
content:
application/x-www-form-urlencoded:
schema:
type: object
required: [grant_type, code, redirect_uri, client_id, code_verifier]
properties:
grant_type: { type: string, enum: [authorization_code, refresh_token] }
code: { type: string }
redirect_uri: { type: string }
client_id: { type: string }
code_verifier: { type: string }
refresh_token: { type: string }
responses:
'200':
description: Token response
content:
application/json:
schema:
$ref: '#/components/schemas/TokenResponse'
/.well-known/openid-configuration:
get:
summary: OIDC Discovery
responses:
'200':
description: OIDC discovery document
/.well-known/jwks.json:
get:
summary: JWKS
responses:
'200':
description: JSON Web Key Set
/auth/register:
post:
summary: Register user
requestBody:
required: true
content:
application/json:
schema:
$ref: '#/components/schemas/RegisterRequest'
responses:
'200':
description: Registered
content:
application/json:
schema:
$ref: '#/components/schemas/UserProfile'
/auth/login:
post:
summary: API login
requestBody:
required: true
content:
application/json:
schema:
$ref: '#/components/schemas/LoginRequest'
responses:
'200':
description: Token response
content:
application/json:
schema:
$ref: '#/components/schemas/TokenResponse'
/auth/refresh:
post:
summary: Refresh token
requestBody:
required: true
content:
application/json:
schema:
$ref: '#/components/schemas/RefreshRequest'
responses:
'200':
description: Token response
content:
application/json:
schema:
$ref: '#/components/schemas/TokenResponse'
/auth/logout:
post:
summary: Logout (revoke refresh token)
security:
- BearerAuth: []
requestBody:
required: true
content:
application/json:
schema:
type: object
properties:
refresh_token:
type: string
responses:
'204':
description: Logged out
/auth/password/forgot:
post:
summary: Request password reset
requestBody:
required: true
content:
application/json:
schema:
$ref: '#/components/schemas/ForgotPasswordRequest'
responses:
'204':
description: Email sent
/auth/password/reset:
post:
summary: Reset password
requestBody:
required: true
content:
application/json:
schema:
$ref: '#/components/schemas/ResetPasswordRequest'
responses:
'204':
description: Password reset
/auth/email/verify:
get:
summary: Verify email
parameters:
- in: query
name: token
required: true
schema: { type: string }
responses:
'200':
description: Email verified
/user/profile:
get:
summary: Get current user profile
security:
- BearerAuth: []
responses:
'200':
description: Profile
content:
application/json:
schema:
$ref: '#/components/schemas/UserProfile'
/newsletter/subscribe:
post:
summary: Subscribe (unauthenticated allowed)
requestBody:
required: true
content:
application/json:
schema:
$ref: '#/components/schemas/SubscribeRequest'
responses:
'200':
description: Pending subscription
content:
application/json:
schema:
$ref: '#/components/schemas/Subscription'
/newsletter/confirm:
get:
summary: Confirm subscription (double opt-in)
parameters:
- in: query
name: token
required: true
schema: { type: string }
responses:
'200':
description: Active subscription
content:
application/json:
schema:
$ref: '#/components/schemas/Subscription'
/newsletter/unsubscribe:
post:
summary: Unsubscribe single list
requestBody:
required: true
content:
application/json:
schema:
type: object
required: [token]
properties:
token: { type: string }
responses:
'200':
description: Unsubscribed
content:
application/json:
schema:
$ref: '#/components/schemas/Subscription'
/newsletter/preferences:
get:
summary: Get preferences
parameters:
- in: query
name: subscription_id
required: false
schema: { type: string }
- in: query
name: tenant_id
required: false
schema: { type: string }
- in: query
name: email
required: false
schema: { type: string, format: email }
responses:
'200':
description: Preferences
content:
application/json:
schema:
$ref: '#/components/schemas/Subscription'
post:
summary: Update preferences
requestBody:
required: true
content:
application/json:
schema:
type: object
required: [subscription_id, preferences]
properties:
subscription_id: { type: string }
preferences: { type: object }
responses:
'200':
description: Updated
content:
application/json:
schema:
$ref: '#/components/schemas/Subscription'
/admin/tenants:
get:
summary: List tenants
security: [{ BearerAuth: [] }]
responses:
'200':
description: List
post:
summary: Create tenant
security: [{ BearerAuth: [] }]
requestBody:
required: true
content:
application/json:
schema:
$ref: '#/components/schemas/Tenant'
responses:
'201':
description: Created
/admin/tenants/{id}:
get:
summary: Get tenant
security: [{ BearerAuth: [] }]
parameters:
- in: path
name: id
required: true
schema: { type: string }
responses:
'200':
description: Tenant
put:
summary: Update tenant
security: [{ BearerAuth: [] }]
parameters:
- in: path
name: id
required: true
schema: { type: string }
requestBody:
required: true
content:
application/json:
schema:
$ref: '#/components/schemas/Tenant'
responses:
'200':
description: Updated
delete:
summary: Delete tenant
security: [{ BearerAuth: [] }]
parameters:
- in: path
name: id
required: true
schema: { type: string }
responses:
'204':
description: Deleted
/admin/newsletter-lists:
get:
summary: List newsletter lists
security: [{ BearerAuth: [] }]
responses:
'200':
description: List
post:
summary: Create list
security: [{ BearerAuth: [] }]
requestBody:
required: true
content:
application/json:
schema:
$ref: '#/components/schemas/NewsletterList'
responses:
'201':
description: Created
/admin/newsletter-lists/{id}:
get:
summary: Get list
security: [{ BearerAuth: [] }]
parameters:
- in: path
name: id
required: true
schema: { type: string }
responses:
'200':
description: List
put:
summary: Update list
security: [{ BearerAuth: [] }]
parameters:
- in: path
name: id
required: true
schema: { type: string }
requestBody:
required: true
content:
application/json:
schema:
$ref: '#/components/schemas/NewsletterList'
responses:
'200':
description: Updated
delete:
summary: Delete list
security: [{ BearerAuth: [] }]
parameters:
- in: path
name: id
required: true
schema: { type: string }
responses:
'204':
description: Deleted
/admin/oauth-clients:
get:
summary: List OAuth clients
security: [{ BearerAuth: [] }]
responses:
'200':
description: List
post:
summary: Create OAuth client
security: [{ BearerAuth: [] }]
requestBody:
required: true
content:
application/json:
schema:
$ref: '#/components/schemas/OAuthClient'
responses:
'201':
description: Created
/admin/oauth-clients/{id}:
get:
summary: Get OAuth client
security: [{ BearerAuth: [] }]
parameters:
- in: path
name: id
required: true
schema: { type: string }
responses:
'200':
description: OAuth client
put:
summary: Update OAuth client
security: [{ BearerAuth: [] }]
parameters:
- in: path
name: id
required: true
schema: { type: string }
requestBody:
required: true
content:
application/json:
schema:
$ref: '#/components/schemas/OAuthClient'
responses:
'200':
description: Updated
delete:
summary: Delete OAuth client
security: [{ BearerAuth: [] }]
parameters:
- in: path
name: id
required: true
schema: { type: string }
responses:
'204':
description: Deleted
components:
securitySchemes:
OAuth2:
type: oauth2
flows:
authorizationCode:
authorizationUrl: /api/oauth/authorize
tokenUrl: /api/oauth/token
scopes:
openid: OpenID Connect
email: Email
profile: Basic profile
BearerAuth:
type: http
scheme: bearer
bearerFormat: JWT
schemas:
TokenResponse:
type: object
properties:
access_token: { type: string }
refresh_token: { type: string }
id_token: { type: string }
token_type: { type: string, example: Bearer }
expires_in: { type: integer }
RegisterRequest:
type: object
required: [email, password]
properties:
email: { type: string, format: email }
password: { type: string }
LoginRequest:
type: object
required: [email, password, client_id]
properties:
email: { type: string, format: email }
password: { type: string }
client_id: { type: string }
scope: { type: string }
RefreshRequest:
type: object
required: [refresh_token, client_id]
properties:
refresh_token: { type: string }
client_id: { type: string }
ForgotPasswordRequest:
type: object
required: [email]
properties:
email: { type: string, format: email }
ResetPasswordRequest:
type: object
required: [token, new_password]
properties:
token: { type: string }
new_password: { type: string }
UserProfile:
type: object
properties:
id: { type: string }
email: { type: string, format: email }
email_verified: { type: boolean }
created_at: { type: string, format: date-time }
SubscribeRequest:
type: object
required: [tenant_id, list_id, email]
properties:
tenant_id: { type: string }
list_id: { type: string }
email: { type: string, format: email }
preferences: { type: object }
source: { type: string }
Subscription:
type: object
properties:
id: { type: string }
tenant_id: { type: string }
list_id: { type: string }
email: { type: string, format: email }
status: { type: string, enum: [pending, active, unsubscribed] }
preferences: { type: object }
created_at: { type: string, format: date-time }
Tenant:
type: object
properties:
id: { type: string }
name: { type: string }
domains: { type: array, items: { type: string } }
status: { type: string }
NewsletterList:
type: object
properties:
id: { type: string }
tenant_id: { type: string }
name: { type: string }
status: { type: string }
OAuthClient:
type: object
properties:
id: { type: string }
tenant_id: { type: string }
name: { type: string }
redirect_uris: { type: array, items: { type: string } }
client_type: { type: string, enum: [public, confidential] }