Skip to content

Commit 6abcd78

Browse files
authored
Merge pull request #60 from kc3hack/feature/issue-51-user-api
Issue #51 の実装と、レビュー対応を含む。 ## 実装内容 - POST/GET/PUT/DELETE /users(CRUD) - GET/PUT /users/{id}/profile(Upsert) - GET /users/{id}/badges / skill-trees / quest-progress - update_user_rank()(AI専用内部CRUD、HTTP非公開) - ADR 010(rank管理)・ADR 011(エンドポイント設計)追加 ## レビュー対応 - fix: PUT /users/{id} の ValueError を 400 に変換 - fix: update_user_rank() に rank 範囲(0–9)バリデーション追加 - fix: username 重複の事前チェック、null username の無視 - test: 異常系・バリデーション系テスト4件追加(計131 passed) - docs: ADR 010/011 を実装と整合させる(rankフロー・user_id記述・IDORセキュリティ注記) Closes #51
2 parents 4225e16 + f42e314 commit 6abcd78

File tree

18 files changed

+847
-53
lines changed

18 files changed

+847
-53
lines changed

.github/decisions/009-database-design-strategy.md

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ issue #31で6テーブル(Profile, OAuthAccount, Badge, Quest, QuestProgress,
3333
- **変更理由**:
3434
- **Langchain/フロントエンドとの統一的な操作**: JSONB型(バイナリ形式)で一貫した扱いが必要
3535
- **テスト環境の統一**: SQLite(BLOB型)/PostgreSQL(JSONB型)両対応
36-
- **将来の拡張性**: JSON内部検索の可能性を考慮
36+
- **スキルツリーのクリア状態更新・検索**: SkillTree.tree_data 内の各ノード(`completed` フラグ)をノード単位で更新・検索する予定があるため、JSONB の GIN インデックスによる JSON 内部検索(`@>`, `?` 演算子等)が必要になる(Issue #54 参照)
3737
- **実装方針**:
3838
- PostgreSQL: JSONB型として保存
3939
- SQLite: BLOB型として保存(JSONBのバイナリ表現)
@@ -152,8 +152,8 @@ issue #31で6テーブル(Profile, OAuthAccount, Badge, Quest, QuestProgress,
152152
- **対処法**: SQLAlchemy ORM の `relationship()` で簡潔に記述
153153
- **パフォーマンス**: 非正規化より若干遅い
154154
- **対処法**: ハッカソン規模(数百ユーザー)では影響なし、将来的にインデックス最適化
155-
- **JSONB型未使用**: PostgreSQL固有の高速検索機能を使えない
156-
- **対処法**: MVP段階ではJSON内部検索が不要、必要になったら移行
155+
- **JSONB型採用による書き込みコスト**: JSON型より書き込み時のパース処理が若干重い
156+
- **対処法**: ハッカソン規模では影響なし。スキルツリーはユーザー分析時のみ更新するため書き込み頻度が低い
157157

158158
### 実装ガイドライン
159159

Lines changed: 128 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,128 @@
1+
# ADR 010: User API設計 - rankフィールドの管理方針
2+
3+
## ステータス
4+
5+
- [x] **決定**
6+
7+
## コンテキスト (課題と背景)
8+
9+
Issue #51`PUT /users/{user_id}` エンドポイントを実装する際、`rank` フィールドの扱いについて以下の問題が判明した。
10+
11+
### 問題1: PUT /users/{id} に rank を公開するとユーザーが改ざん可能
12+
13+
`PUT /users/{id}``rank` フィールドを含めた場合、任意のユーザーが `rank=9`(世界樹)を自由にセットできる。AI分析を経ずにランクを操作できることになり、ゲームバランスを破壊する。
14+
15+
### 問題2: rankとexpの整合性バリデータとMVPフローの矛盾
16+
17+
`app/schemas/user.py``User` レスポンススキーマに以下のバリデータが存在していた:
18+
19+
```python
20+
@model_validator(mode="after")
21+
def validate_rank_consistency(self) -> "User":
22+
expected_rank = calculate_rank(self.exp)
23+
if self.rank != expected_rank:
24+
raise ValueError(...)
25+
```
26+
27+
product-spec のMVPフローでは、AIが `rank=4` と判定した状態で `exp=0` のままになるため、このバリデータは **500エラー** を引き起こす。
28+
29+
product-spec 4.2:
30+
> **MVP段階**: 初回分析時のランク決定のみ実装。
31+
> **Future**: ポイント制や特定条件達成によるランクアップ機能。
32+
33+
MVPではrankはAIが直接決定し、expの蓄積によるrankupはFutureスコープ。
34+
35+
### rankとexpの関係
36+
37+
`rank = calculate_rank(exp)` という関係性は定義されているが(`RANK_THRESHOLDS`)、それはFutureのexp蓄積ランクアップのための設計。MVPではrankはAI判定で決まり、expとの連動はない。
38+
39+
また、rankが更新されるとスキルツリーの再生成フローが走る可能性もある(Issue #54)。このような複合的な処理はHTTPエンドポイントから直接操作できるべきではなく、サービス層・CRUD層で制御すべき。
40+
41+
## 決定 (Decision)
42+
43+
### 1. `PUT /users/{user_id}` から `rank` を除外
44+
45+
```python
46+
class UserUpdate(BaseModel):
47+
username: str | None = None
48+
# rank / level / exp はサーバー側で管理するため含めない
49+
```
50+
51+
### 2. AI専用のCRUD関数 `update_user_rank` を追加
52+
53+
AI サービス(LangChain / rank_service.py)からのみ呼び出される内部関数として `app/crud/user.py` に実装。HTTPエンドポイントには公開しない。
54+
55+
```python
56+
def update_user_rank(db: Session, user_id: int, rank: int) -> User | None:
57+
"""AI分析結果のランク保存専用。エンドポイントには公開しない。"""
58+
```
59+
60+
### 3. `validate_rank_consistency` バリデータを削除
61+
62+
MVPではrankとexpの連動はFutureスコープ。バリデータはFutureのランクアップ実装時に改めて検討する。
63+
64+
### rankの更新フロー(MVP)
65+
66+
```
67+
POST /analyze/rank (github_username, ...)
68+
└─ analyze_user_rank() が LLM でpercentile・rank を判定して結果を返す
69+
└─ フロントはレスポンス(rank, rank_name, reasoning)を表示するだけ
70+
71+
PUT /users/{id} では rank は変更不可
72+
```
73+
74+
> **Note**: 現在の `analyze_user_rank()``app/services/rank_service.py`)は `db` / `user_id` を受け取らず、LLM 判定結果を返すだけの純粋な分析ツールとして実装している。`update_user_rank(db, user_id, rank)` の呼び出しは AI サービス統合(Issue #54)のタイミングで追加する。
75+
76+
### rankの更新フロー(Future: Issue #54 実装時)
77+
78+
**設計原則**: rank の UPDATE はエンドポイントがサーバー内部で完結させる。フロントエンドは analyze 結果を受け取るだけでよく、別途 `PUT /users/{id}` で保存する必要はない。Issue #54`generate_skill_tree_ai(user_id, category, db)` が SkillTree テーブルを直接更新するパターンと同じ。
79+
80+
```
81+
POST /analyze/rank (user_id, github_username, ...)
82+
└─ analyze_user_rank(db, user_id, ...) を呼び出し
83+
└─ LLM が percentile から rank 判定
84+
└─ 内部で update_user_rank(db, user_id, rank) を呼び出して DB 保存
85+
└─ フロントはレスポンスを表示するだけ(DB 保存はサーバー側で完結)
86+
87+
PUT /users/{id} では rank は変更不可(変わらず)
88+
```
89+
90+
## 代替案との比較 (Options)
91+
92+
### 1. PUT /users/{id} に rank を含め、フロントが保存する
93+
94+
- **Good**: シンプル、実装が容易
95+
- **Bad**: ユーザーが rank を自由に改ざん可能。MVP段階でOAuth認証が未実装のため、現時点で安全に実装できない
96+
- **却下理由**: セキュリティリスク
97+
98+
### 2. POST /users/{id}/rank-init 専用エンドポイントを追加
99+
100+
- **Good**: 意図が明確
101+
- **Bad**: エンドポイントが増えてAPIが複雑になる。AI内部でCRUD関数を呼べば同じことが実現できる
102+
- **却下理由**: 不要な複雑性の追加(YAGNI)
103+
104+
## 結果 (Consequences)
105+
106+
### Positive
107+
108+
- **セキュリティ**: ユーザーによるrank改ざんが不可能
109+
- **MVP優先**: 技術負債を増やさず、動くものを優先して提供
110+
- **拡張性**: Future(ランクアップ・スキルツリー再生成連携)実装時は `update_user_rank` を拡張するだけでよい
111+
112+
### Negative
113+
114+
- **validate_rank_consistency の削除**: バリデータによる自動チェックがなくなる
115+
- **対処法**: `update_user_rank` を内部専用CRUDに限定し、rank変更経路を制御する
116+
- **expとrankの非連動(MVP)**: expが増えてもrankは変わらない
117+
- **対処法**: Future実装時(Issue #XX)に連動ロジックを追加する
118+
119+
## 関連
120+
121+
- Issue #51: User API エンドポイント実装
122+
- Issue #54: AI実装Phase 3 - スキルツリー生成(`update_user_rank` の利用元候補)
123+
- ADR 005: APEX Legendsスタイルランク分布(rankの意味・定義)
124+
- product-spec 4.2: ランクアップ方針(MVP vs Future)
125+
126+
## 変更履歴
127+
128+
- 2026-02-20: 決定(Issue #51 設計議論)
Lines changed: 165 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,165 @@
1+
# ADR 011: User API エンドポイント設計
2+
3+
## ステータス
4+
5+
- [x] **決定**
6+
7+
## コンテキスト (課題と背景)
8+
9+
Issue #51`/users` 配下の RESTful API を設計する際、以下の判断が必要になった。
10+
11+
1. ネストされたリソース(Badge, SkillTree 等)のレスポンスに `user_id` を含めるか
12+
2. 各エンドポイントの Request / Response の確定
13+
14+
## 決定 (Decision)
15+
16+
### 1. ネストリソースの `user_id` を除去(コミット eaed96e)
17+
18+
当初は既存スキーマを流用して `user_id` を含める方針だったが、
19+
その後のリファクタリング(eaed96e)でレスポンススキーマから冗長な `user_id` を除去した。
20+
21+
除去した理由:
22+
- クライアントはパスから `user_id` を知っているため不要
23+
- RESTful 的に冗長情報を排除
24+
25+
**Future**: API 公開・外部連携が必要になった段階で専用レスポンススキーマへ切り出しを検討。
26+
27+
### 2. 確定エンドポイント仕様
28+
29+
#### User
30+
31+
| Method | Path | Status | 概要 |
32+
|---|---|---|---|
33+
| POST | `/users` | 201 | ユーザー登録(仮実装・username のみ) |
34+
| GET | `/users/{user_id}` | 200 | ユーザー情報取得 |
35+
| PUT | `/users/{user_id}` | 200 | username 更新のみ(ADR 010参照) |
36+
| DELETE | `/users/{user_id}` | 204 | ユーザー削除 |
37+
38+
**POST /users**
39+
```
40+
Request: { "username": "string" }
41+
Response: { id, username, level=1, exp=0, rank=0, created_at, updated_at }
42+
Error: 400 username重複
43+
Note: SkillTree 6カテゴリが自動初期化される(CRUD層実装済み)
44+
```
45+
46+
**GET /users/{user_id}**
47+
```
48+
Response: { id, username, level, exp, rank, created_at, updated_at }
49+
Error: 404
50+
```
51+
52+
**PUT /users/{user_id}**
53+
```
54+
Request: { "username"?: "string" }
55+
※ level / exp / rank はサーバー管理のため除外(ADR 010)
56+
Response: { id, username, level, exp, rank, created_at, updated_at }
57+
Error: 404
58+
```
59+
60+
**DELETE /users/{user_id}**
61+
```
62+
Response: 204 No Content
63+
Error: 404
64+
```
65+
66+
#### Profile
67+
68+
| Method | Path | Status | 概要 |
69+
|---|---|---|---|
70+
| GET | `/users/{user_id}/profile` | 200 | プロフィール取得 |
71+
| PUT | `/users/{user_id}/profile` | 200 | プロフィール更新(Upsert) |
72+
73+
**GET /users/{user_id}/profile**
74+
```
75+
Response: { id, user_id, github_username, qiita_id, connpass_id,
76+
portfolio_url, portfolio_text, last_analyzed_at }
77+
Error: 404 (user not found / profile not found)
78+
```
79+
80+
**PUT /users/{user_id}/profile**
81+
```
82+
Request: { "github_username"?: str, "qiita_id"?: str, "connpass_id"?: str,
83+
"portfolio_url"?: str, "portfolio_text"?: str } ← 全て optional
84+
Response: { id, user_id, github_username, qiita_id, connpass_id,
85+
portfolio_url, portfolio_text, last_analyzed_at }
86+
Error: 404 (user not found)
87+
Note: profile が存在しない場合は作成(Upsert)
88+
```
89+
90+
#### Badge
91+
92+
| Method | Path | Status | 概要 |
93+
|---|---|---|---|
94+
| GET | `/users/{user_id}/badges` | 200 | バッジ一覧取得 |
95+
96+
**GET /users/{user_id}/badges**
97+
```
98+
Response: [ { id, category, tier, earned_at } ]
99+
※ 空配列を返す(404にしない)
100+
Error: 404 (user not found)
101+
```
102+
103+
#### SkillTree
104+
105+
| Method | Path | Status | 概要 |
106+
|---|---|---|---|
107+
| GET | `/users/{user_id}/skill-trees` | 200 | スキルツリー一覧取得 |
108+
109+
**GET /users/{user_id}/skill-trees**
110+
```
111+
Response: [ { id, category, tree_data, generated_at } ]
112+
※ ユーザー作成時に6カテゴリ自動初期化のため必ず6件
113+
Error: 404 (user not found)
114+
```
115+
116+
#### QuestProgress
117+
118+
| Method | Path | Status | 概要 |
119+
|---|---|---|---|
120+
| GET | `/users/{user_id}/quest-progress` | 200 | クエスト進捗一覧取得 |
121+
122+
**GET /users/{user_id}/quest-progress**
123+
```
124+
Response: [ { id, quest_id, status, started_at, completed_at } ]
125+
※ 空配列を返す(404にしない)
126+
Error: 404 (user not found)
127+
```
128+
129+
## 代替案との比較 (Options)
130+
131+
### ネストリソースから user_id を除外する
132+
133+
```python
134+
class BadgeResponse(BaseModel):
135+
id: int
136+
category: BadgeCategory
137+
tier: int
138+
earned_at: datetime
139+
# user_id は除外
140+
```
141+
142+
- **Good**: RESTful 的に冗長情報を排除、ペイロード削減
143+
- **Bad**: 既存 `Badge` スキーマと別に `BadgeResponse` を作る必要がある。全リソース分(Badge, SkillTree, QuestProgress)で作業が増える
144+
- **却下理由**: MVP のスピード感に合わない。フロントが無視すれば実害なし
145+
146+
## 結果 (Consequences)
147+
148+
### Positive
149+
150+
- 既存スキーマをそのまま流用でき、実装が高速
151+
- エンドポイント仕様が ADR として記録され、チーム内で認識統一
152+
153+
### Negative
154+
155+
- **認証・認可未実装(MVP)**: `/users/{user_id}` は IDOR(Insecure Direct Object Reference)リスクあり
156+
- **対処法**: OAuth 認証実装(Future)後にミドルウェアで認証・認可チェックを追加予定
157+
158+
## 関連
159+
160+
- Issue #51: User API エンドポイント実装
161+
- ADR 010: rankフィールドの管理方針(PUT /users/{id} の設計判断)
162+
163+
## 変更履歴
164+
165+
- 2026-02-20: 決定(Issue #51 設計議論)

backend/app/api/api.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,10 @@
11
"""ルーター集約"""
22

33
from fastapi import APIRouter
4-
from app.api.endpoints import analyze
4+
from app.api.endpoints import analyze, users
55

66

77
api_router = APIRouter()
88

99
api_router.include_router(analyze.router, prefix="/analyze", tags=["analyze"])
10+
api_router.include_router(users.router, prefix="/users", tags=["users"])

0 commit comments

Comments
 (0)