Skip to content

fix: /api/score と学習記録APIにレート制限・乱用対策を導入する#280

Open
keisato848 wants to merge 1 commit into
mainfrom
fix/270-rate-limiting
Open

fix: /api/score と学習記録APIにレート制限・乱用対策を導入する#280
keisato848 wants to merge 1 commit into
mainfrom
fix/270-rate-limiting

Conversation

@keisato848

Copy link
Copy Markdown
Owner

概要

Issue #270 対応。AI採点API (/api/score) および学習記録API (/api/learning-records) へのレート制限を実装する。

変更内容

新規ファイル

  • �pps/web/lib/rate-limit.ts: スライディングウィンドウ方式のインメモリ レートリミッター

修正ファイル

  • �pps/web/app/api/score/route.ts: レート制限チェック追加
  • �pps/web/app/api/learning-records/route.ts: レート制限チェック追加
  • �pps/web/components/features/exam/AIAnswerBox.tsx: 429 レスポンス時の UI メッセージ + 型安全性改善
  • �pps/web/tests/api/score.test.ts: next-auth モック追加でテストを修正

制限値

エンドポイント 対象 上限 ウィンドウ
/api/score 認証ユーザー 10回 5分
/api/score ゲスト (IP 単位) 3回 5分
/api/learning-records POST 認証ユーザー 120回 1分

受け入れ基準チェック

  • /api/score の短時間連続呼び出しが制限される
  • 学習記録APIの過剰書き込みが制限される
  • 429時のUIメッセージがある(「AI採点の利用上限に達しました。約N分後に再度お試しください。」)
  • 正常な受験体験を阻害しない上限値が設計される
  • レート制限イベントがTelemetryで確認できる(console.warn('[RateLimit] ...') → App Insights トレース)

テスト

  • ユニットテスト全 64 ファイル・98 テストケースパス確認済み

- apps/web/lib/rate-limit.ts を新規作成(スライディングウィンドウ方式)
- /api/score: 認証ユーザー10回/5分、ゲスト3回/5分の制限を追加
- /api/learning-records POST: 認証ユーザー120回/分の制限を追加
- AIAnswerBox.tsx: 429 レスポンス時に分単位の再試行メッセージを表示
- catch(err: any) → catch(err: unknown) で型安全性も改善
- テスト: next-auth モックを追加し全テストパスを確認
Copilot AI review requested due to automatic review settings May 28, 2026 13:39

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Issue #270 対応として、AI採点 API(/api/score)と学習記録 API(/api/learning-records)にレート制限を導入し、429 時のユーザー体験(UIメッセージ)とテストの安定化を図るPRです。

Changes:

  • インメモリのレートリミッター(checkRateLimit / IP取得 / 制限定数)を新規追加
  • /api/score/api/learning-records にレート制限チェックと 429 応答(Retry-After 等)を追加
  • フロント側で 429 を明示的に扱い、エラーハンドリングの型安全性を改善

Reviewed changes

Copilot reviewed 5 out of 5 changed files in this pull request and generated 5 comments.

Show a summary per file
File Description
apps/web/lib/rate-limit.ts インメモリレートリミッター実装と制限値定義を追加
apps/web/app/api/score/route.ts セッション/IPベースで /api/score のレート制限と 429 応答を追加
apps/web/app/api/learning-records/route.ts 学習記録POSTにユーザーIDベースのレート制限と 429 応答を追加
apps/web/components/features/exam/AIAnswerBox.tsx 429時のUIメッセージ表示と catch の unknown
apps/web/tests/api/score.test.ts getServerSession をゲスト固定でモックしてテストを安定化

Comment on lines +19 to +25
function maybeCleanup(windowMs: number): void {
const now = Date.now();
if (now - lastCleanup < CLEANUP_INTERVAL_MS) return;
lastCleanup = now;
for (const [key, entry] of store.entries()) {
if (now - entry.windowStart > windowMs * 2) {
store.delete(key);
Comment on lines +39 to +43
/**
* レート制限チェック(スライディングウィンドウ方式)
*
* @param key レート制限キー(例: `score:user:xxx`)
* @param maxRequests ウィンドウ内の最大リクエスト数
const ip = getClientIp(req.headers);
const rateLimitKey = userId ? `score:user:${userId}` : `score:ip:${ip}`;
const limit = userId ? RATE_LIMITS.SCORE_AUTH : RATE_LIMITS.SCORE_GUEST;
const { allowed, remaining, retryAfter } = checkRateLimit(rateLimitKey, limit.maxRequests, limit.windowMs);
Comment on lines +94 to +100
// 学習記録の過剰書き込み防止
const rateLimitKey = `learning-records:user:${session.user.id}`;
const { allowed, retryAfter } = checkRateLimit(
rateLimitKey,
RATE_LIMITS.LEARNING_RECORDS.maxRequests,
RATE_LIMITS.LEARNING_RECORDS.windowMs,
);
Comment on lines +46 to +50
export function checkRateLimit(
key: string,
maxRequests: number,
windowMs: number,
): RateLimitResult {
@github-actions

Copy link
Copy Markdown

✅ Staging デプロイ完了

項目
🌐 Staging URL Stagingで確認する
🌿 ブランチ fix/270-rate-limiting
📝 コミット 6ca281d

⚠️ 複数PRが同時進行している場合は最後にデプロイされたPRが反映されています。
確認後は本PRのレビューを進めてください。

@keisato848 keisato848 force-pushed the fix/270-rate-limiting branch from 6ca281d to 1fbb691 Compare June 18, 2026 12:09
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants