-
Notifications
You must be signed in to change notification settings - Fork 0
# feat: 職務経歴書フォームに未保存変更インジケーター(Dirty Dot)を追加 #265
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
Show all changes
6 commits
Select commit
Hold shift + click to select a range
8d6917a
unedited dot add
yusuke0610 fee7e3c
unedited dot fix
yusuke0610 30b6071
pdf uploader
yusuke0610 5f2c74b
pdf uploader fix
yusuke0610 d1e2a49
frontend: エラーメッセージのハードコード除去と再発防止機構の追加
yusuke0610 3966999
lint-frontend.sh fix
yusuke0610 File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,101 @@ | ||
| # メッセージ管理ルール (frontend) | ||
|
|
||
| ts/tsx でユーザーに表示される文字列を**リテラルで直接書かない**。 | ||
| 必ず Single Source of Truth から取得すること。 | ||
|
|
||
| ## SSoT の責務分離 | ||
|
|
||
| | メッセージの種類 | 正本 | 取得経路 | | ||
| |---|---|---| | ||
| | **API 経由のエラー** (backend → frontend) | `backend/app/messages.json` | `AppErrorResponse.message` を `api/client.ts:buildApiError` 経由でそのまま表示 | | ||
| | **API レスポンスに message が無い時の補完** | `frontend/src/constants/errorMessages.ts` (`ERROR_CONFIG`) | `ErrorCode` を引いて補完(既存実装) | | ||
| | **frontend 完結のメッセージ** | `frontend/src/constants/messages.ts` | import して定数参照 | | ||
|
|
||
| `messages.json` から frontend 用 TS 定数を build-time 生成する仕組みは**入っていない**。 | ||
| `ERROR_CONFIG` は `backend/app/core/errors.py:ErrorCode` enum と**手動同期**する設計(型エラーで漏れを検出)。 | ||
|
|
||
| ## frontend 完結のメッセージとは | ||
|
|
||
| backend を経由しない以下のような文言: | ||
|
|
||
| - **フォームの事前バリデーション**: `payloadBuilders.ts` の「氏名を入力してください」など | ||
| - **catch ブロックの fallback メッセージ**: `e instanceof Error ? e.message : "..."` の `...` 部分 | ||
| - **ネットワーク層の fallback**: `api/client.ts` で 5xx / fetch 例外時に出す文言 | ||
| - **JSX 直書きの UI 文言**: `ErrorBoundary` のタイトルなど | ||
| - **開発者向け内部エラー**: `import_id が未設定です` のような状態管理エラー | ||
|
|
||
| これらは `frontend/src/constants/messages.ts` に集約する。カテゴリ別の定数: | ||
|
|
||
| - `VALIDATION_MESSAGES` — 入力バリデーション | ||
| - `NETWORK_MESSAGES` — ネットワーク / API クライアント層 | ||
| - `FALLBACK_MESSAGES` — catch fallback / toAppError fallback | ||
| - `UI_MESSAGES` — JSX 直書き文言 | ||
| - `INTERNAL_MESSAGES` — 開発者向け内部エラー | ||
| - `downloadFailureMessage(filename)` — 動的パラメータが必要なケースは関数 | ||
|
|
||
| ## 新規メッセージ追加の手順 | ||
|
|
||
| ### API 経由のエラー(backend が発火) | ||
|
|
||
| 1. `backend/app/messages.json` の `error.<category>` にキー追加 | ||
| 2. backend で `get_error("category.key", **kwargs)` または `raise_app_error(code=...)` で使う | ||
| 3. frontend 側はとくに変更不要(`AppErrorResponse.message` が自動的に画面に出る) | ||
|
|
||
| ### frontend 完結のメッセージ | ||
|
|
||
| 1. `frontend/src/constants/messages.ts` の適切なカテゴリに定数追加 | ||
| 2. 使用箇所で import して参照 | ||
| 3. リテラルを書かない | ||
|
|
||
| ## やってはいけないこと(再発防止対象) | ||
|
|
||
| 以下は **ESLint または `make lint-frontend-messages` で自動検知され CI で fail する**: | ||
|
|
||
| ```ts | ||
| // ✗ ESLint で error | ||
| throw new Error("入力してください"); | ||
| throw new Error(`${field} を入力してください`); | ||
|
|
||
| // ✗ make lint-frontend-messages で error | ||
| setError("失敗しました"); | ||
| setErrorMessage("不正な値です"); | ||
| setAccountError("取得に失敗"); | ||
| toast.error("エラー"); | ||
| alert("確認してください"); | ||
| ``` | ||
|
|
||
| 正しい書き方: | ||
|
|
||
| ```ts | ||
| import { VALIDATION_MESSAGES, FALLBACK_MESSAGES } from "../constants/messages"; | ||
|
|
||
| throw new Error(VALIDATION_MESSAGES.FULL_NAME_REQUIRED); | ||
| setError(FALLBACK_MESSAGES.SAVE); | ||
| ``` | ||
|
|
||
| ## 例外: 検知から外れているもの | ||
|
|
||
| 以下はリテラルを書いても検知されない(許容するが推奨しない): | ||
|
|
||
| - 英語の開発者向けメッセージ (`throw new Error("invariant violated")`) | ||
| - `console.error` / `console.warn`(UI に表示されないログ用途) | ||
| - テストファイル (`*.test.*`, `test/**`) | ||
| - `constants/messages.ts` 自身 | ||
|
|
||
| ## 検証 | ||
|
|
||
| ```bash | ||
| make lint-frontend # ESLint(no-restricted-syntax 含む) | ||
| make lint-frontend-messages # grep ベースの追加チェック | ||
| ``` | ||
|
|
||
| 両方を pass させることが「テスト OK」条件の前提(`.claude/rules/frontend/test.md` 参照)。 | ||
|
|
||
| ## 参考 | ||
|
|
||
| - `frontend/src/constants/messages.ts` — frontend 完結メッセージの SSoT | ||
| - `frontend/src/constants/errorCodes.ts` / `errorMessages.ts` — backend ErrorCode 連携 | ||
| - `backend/app/messages.json` — backend のメッセージ正本 | ||
| - `backend/app/core/errors.py` — ErrorCode enum | ||
| - `scripts/lint-frontend-messages.sh` — grep ベースの検知スクリプト | ||
| - `frontend/eslint.config.js` — no-restricted-syntax ルール定義 |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,130 @@ | ||
| --- | ||
| paths: | ||
| - backend/** | ||
| - frontend/** | ||
| - infra/** | ||
| --- | ||
|
|
||
| # セキュリティルール(全領域横断) | ||
|
|
||
| このファイルは backend / frontend / infra すべての領域に適用される横断セキュリティルール。 | ||
| 認証・JWT・Cookie 属性・暗号化・Rate Limit の詳細は `.claude/rules/backend/auth-security.md` を参照し、ここでは重複させない。 | ||
|
|
||
| --- | ||
|
|
||
| ## 秘密情報管理(Secrets Management) | ||
|
|
||
| ### Git に含めてはいけないもの | ||
|
|
||
| - `.env` / `.env.*` — ローカル開発用環境変数 | ||
| - `*.pem` / `*.key` — 秘密鍵・証明書 | ||
| - `*.json` のうち GCP サービスアカウント鍵に該当するもの | ||
| - `turso-auth-token` 等の認証トークンをハードコードした設定ファイル | ||
|
|
||
| ### 環境変数の正本 | ||
|
|
||
| - **定数名の定義**: `backend/app/core/env_keys.py` | ||
| - **用途と注入経路の一覧**: `docs/api.md` の「環境変数」セクション | ||
| - **本番注入**: `infra/modules/cloud_run/main.tf` の `env` ブロック(Secret Manager 参照形式) | ||
| - **ローカル**: `docker-compose.yml` | ||
|
|
||
| backend 内で文字列リテラル `os.getenv("XXX")` を使うことは禁止。`env_keys.XXX` 経由で参照する。 | ||
|
|
||
| ### ログへの秘密情報出力禁止 | ||
|
|
||
| 認証トークン・API キー・パスワード・個人情報(メールアドレス含む)をログに出力しない。 | ||
| デバッグ目的でも `logger.debug` にこれらを含めないこと。 | ||
|
|
||
| --- | ||
|
|
||
| ## 入力バリデーション・出力エスケープ | ||
|
|
||
| ### Backend | ||
|
|
||
| - **Pydantic バリデーション必須**: API エンドポイントへの入力はすべて `app/schemas/` の Pydantic モデルで型・制約を検証する。`Any` 型や `dict` 型の素通しは避ける | ||
| - **SQL インジェクション防止**: SQLAlchemy ORM / Core のパラメータバインドを使う。文字列連結でクエリを組み立てることは禁止 | ||
| - **LLM プロンプトへのユーザー入力**: ユーザー由来の文字列を LLM プロンプトに埋め込む場合は `backend/app/services/llm/sanitizer.py` を通す | ||
|
|
||
| ### Frontend | ||
|
|
||
| - **`dangerouslySetInnerHTML` は原則禁止**: 外部コンテンツや動的文字列を `innerHTML` / `dangerouslySetInnerHTML` に渡さない。React の自動エスケープを信頼する | ||
| - **外部リンク**: `<a target="_blank">` には必ず `rel="noopener noreferrer"` を付ける(タブナビゲーション攻撃の防止) | ||
| - **URL パラメータの扱い**: `window.location.search` 等から取得した値を DOM に直接レンダリングしない。必ず React の `state` / `props` 経由で扱う | ||
|
|
||
| --- | ||
|
|
||
| ## Frontend セキュリティ | ||
|
|
||
| ### トークン管理 | ||
|
|
||
| - アクセストークン・リフレッシュトークンは `HttpOnly` + `Secure` Cookie で管理する(詳細: `.claude/rules/backend/auth-security.md`) | ||
| - `localStorage` / `sessionStorage` にトークンを保存しない(XSS で盗取されるリスク) | ||
| - Redux の `store` にも生のトークン文字列を乗せない | ||
|
|
||
| ### XSS 対策まとめ | ||
|
|
||
| 1. `dangerouslySetInnerHTML` 禁止(上述) | ||
| 2. Markdown レンダラー等を使う場合は sanitize オプションを有効化する | ||
| 3. `eval()` / `Function()` コンストラクタの使用禁止 | ||
|
|
||
| ### 依存関係 | ||
|
|
||
| - `npm audit` で High / Critical CVE が検出された場合は PR マージ前に対処する | ||
| - CI が `npm audit --audit-level=high` で落ちた場合は、`--force` で無視せず脆弱なパッケージを更新すること | ||
|
|
||
| --- | ||
|
|
||
| ## Backend セキュリティ(追加事項) | ||
|
|
||
| 認証・JWT・Rate Limit・CORS・INTERNAL_SECRET については `.claude/rules/backend/auth-security.md` を参照。 | ||
|
|
||
| ### ファイルアップロード | ||
|
|
||
| ファイルアップロード機能を追加する場合は以下をすべて実装する: | ||
|
|
||
| 1. **MIME タイプ検証**: `Content-Type` ヘッダだけでなくファイルのバイト列(magic bytes)で検証する | ||
| 2. **ファイルサイズ上限**: エンドポイント側で上限を設け、OOM を防ぐ | ||
| 3. **ファイル名サニタイズ**: パストラバーサル(`../` 等)を除去する。UUIDv4 でリネームするのが最もシンプル | ||
| 4. **保存先**: 本番ではローカルファイルシステムに保存せず GCS 等の外部ストレージを使う | ||
|
|
||
| ### 依存関係 | ||
|
|
||
| - `pip audit` / `safety` で定期的に CVE チェックを行う | ||
| - CI に `pip-audit` を組み込むことを推奨(High 以上を fail 条件にする) | ||
|
|
||
| --- | ||
|
|
||
| ## Infra セキュリティ | ||
|
|
||
| ### IAM 最小権限 | ||
|
|
||
| - Cloud Run のサービスアカウントには必要最小限のロールのみ付与する(`infra/modules/service_account/`) | ||
| - 新規ロールを付与する際は「なぜそのロールが必要か」をコメントで残す | ||
| - `roles/owner` / `roles/editor` 等の広範なロールを Cloud Run SA に付与しない | ||
|
|
||
| ### Secret Manager | ||
|
|
||
| - DB 接続 URL・API キー・JWT 秘密鍵等はすべて Secret Manager に格納し、Cloud Run の `secretEnv` / `secretVolume` 経由で注入する | ||
| - Terraform state に平文のシークレットが乗らないよう、`sensitive = true` を必ず付与する | ||
| - Turso auth token: state 漏洩防止のため `turso CLI` で発行 → Secret Manager に手動投入(詳細: `.claude/rules/infra/opentofu.md`) | ||
|
|
||
| ### デプロイ制限 | ||
|
|
||
| - `tofu apply -auto-approve` をローカルから本番環境に直接流さない(詳細: `.claude/rules/infra/test.md`) | ||
| - `lifecycle { prevent_destroy = true }` 付きリソースへの破壊的変更は実行前に必ず確認する | ||
|
|
||
| --- | ||
|
|
||
| ## セキュリティレビューチェックリスト | ||
|
|
||
| AI エージェントがコードを変更した後に確認する項目: | ||
|
|
||
| - [ ] 秘密情報(トークン・キー・パスワード)が Git に含まれていないか | ||
| - [ ] 環境変数を文字列リテラルで直接参照していないか(`env_keys.XXX` 経由か) | ||
| - [ ] 入力バリデーションが境界(API エンドポイント・フォーム)で行われているか | ||
| - [ ] `dangerouslySetInnerHTML` / `innerHTML` の新規使用がないか | ||
| - [ ] ログに個人情報・認証情報が出力されないか | ||
| - [ ] 新規エンドポイントに認証ガード(`get_current_user` 依存)が付いているか | ||
| - [ ] 高コスト処理(外部 API 呼び出し・LLM 実行)に rate limit があるか(`slowapi`) | ||
| - [ ] `target="_blank"` に `rel="noopener noreferrer"` が付いているか | ||
| - [ ] 新規 IAM ロール付与に最小権限の原則を守っているか |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
56 changes: 56 additions & 0 deletions
56
backend/alembic_migrations/versions/0032_add_resume_imports.py
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,56 @@ | ||
| """add resume_imports table | ||
|
|
||
| Revision ID: 0032_add_resume_imports | ||
| Revises: 0031_add_warning_message_to_github_analysis_cache | ||
| Create Date: 2026-05-22 00:00:00.000000 | ||
| """ | ||
|
|
||
| from typing import Sequence, Union | ||
|
|
||
| import sqlalchemy as sa | ||
| from alembic import op | ||
|
|
||
| revision: str = "0032_add_resume_imports" | ||
| down_revision: Union[str, None] = "0031_add_warning_message_to_github_analysis_cache" | ||
| branch_labels: Union[str, Sequence[str], None] = None | ||
| depends_on: Union[str, Sequence[str], None] = None | ||
|
|
||
|
|
||
| def upgrade() -> None: | ||
| op.create_table( | ||
| "resume_imports", | ||
| sa.Column("id", sa.String(36), primary_key=True), | ||
| sa.Column( | ||
| "user_id", | ||
| sa.String(36), | ||
| sa.ForeignKey("users.id", ondelete="CASCADE"), | ||
| nullable=False, | ||
| index=True, | ||
| ), | ||
| sa.Column("status", sa.String(20), nullable=False, server_default="pending"), | ||
| sa.Column("pdf_blob", sa.LargeBinary, nullable=True), | ||
| sa.Column("result_json", sa.Text, nullable=True), | ||
| sa.Column("is_resume_flag", sa.Boolean, nullable=True), | ||
| sa.Column("judge_reason", sa.Text, nullable=True), | ||
| sa.Column("error_message", sa.Text, nullable=True), | ||
| sa.Column("retry_count", sa.Integer, nullable=False, server_default="0"), | ||
| sa.Column("expires_at", sa.DateTime(timezone=True), nullable=False), | ||
| sa.Column("started_at", sa.DateTime(timezone=True), nullable=True), | ||
| sa.Column("completed_at", sa.DateTime(timezone=True), nullable=True), | ||
| sa.Column( | ||
| "created_at", | ||
| sa.DateTime(timezone=True), | ||
| server_default=sa.func.now(), | ||
| nullable=False, | ||
| ), | ||
| sa.Column( | ||
| "updated_at", | ||
| sa.DateTime(timezone=True), | ||
| server_default=sa.func.now(), | ||
| nullable=False, | ||
| ), | ||
| ) | ||
|
|
||
|
|
||
| def downgrade() -> None: | ||
| op.drop_table("resume_imports") | ||
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🧩 Analysis chain
🏁 Script executed:
Repository: yusuke0610/devforge
Length of output: 440
🏁 Script executed:
Repository: yusuke0610/devforge
Length of output: 1135
Fix the
updated_atauto-update concern—ORM already updates itbackend/app/alembic_migrations/versions/0032_add_resume_imports.pydefinesupdated_atwith onlyserver_default=sa.func.now()(no DB-side update trigger), butbackend/app/models/resume_import.pysetsupdated_atwithonupdate=func.now(), so SQLAlchemy ORM-generatedUPDATEs will refreshupdated_at.If
resume_importsrows can be modified outside the ORM (raw SQL/other jobs),updated_atwon’t auto-update in that path—add a DB trigger if that’s a requirement.🤖 Prompt for AI Agents
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
スキップしました — このコードベースでは検出結果が無効です。
理由:
データベースエンジンの不一致。データベースはTurso/libSQL(SQLite互換)であり、PostgreSQLではありません。提案された修正では、PL/pgSQLのトリガー関数構文(CREATE FUNCTION内のNEW.updated_at = now())が使用されていますが、libSQLはこの構文をサポートしていません。SQLiteのトリガーは異なる形式を使用しており、backend/alembic_migrations/versions/ のマイグレーションではトリガーは一切使用されていません。これは確立されたパターンからの例外的な変更となります。
ORM以外のライターが存在しません。resume_imports / ResumeImportをgrepした結果、すべての書き込みパスがSQLAlchemy ORM(routers/resume_imports.py、
services/tasks/handlers/resume_import.py、およびdb_session経由のテスト)を経由していることが分かりました。 ORMのみを使用するライターの場合、モデルのonupdate=func.now() (resume_import.py:57) は更新のたびに実行されます。
この発見で想定されている「生のSQL/その他のクライアント」は存在しません。
実際の問題を解決することなく、一貫性のない規約を作成することになります。
コードの変更はありません。検証対象はありません。
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@yusuke0610ご説明ありがとうございます。3点の理由はいずれも正確で、私の指摘は誤りでした。Turso/libSQL(SQLite 互換)環境では PL/pgSQL のトリガー構文は使用できず、書き込みパスもすべて SQLAlchemy ORM 経由であるためonupdate=func.now()で十分に機能します。また、既存テーブルとの一貫性を保つ判断も理にかなっています。失礼しました。(´・ω・`)
✏️ Learnings added