# CertRemind 開発ガイド ## 概要 - アプリケーション名: CertRemind - 目的: ユーザーが登録した Web サイトの TLS/SSL 証明書の有効期限を監視し、期限切れ前にアプリ内アラート、Webhook、Push 通知で知らせる。 - 現在の実装状況: `development_plan.md` のフェーズ 8 までの機能を実装済み。MVP の主要機能に加え、基本的な品質向上、テスト、運用準備も整備済み。 ## 技術スタック - Node.js v22 - pnpm - Hono.js - React.js / Vite - Radix UI - lucide-react - PostgreSQL - Docker Compose - OpenSSL - Argon2id - TOTP: `otplib` - Push 通知: `web-push` - QR コード: `qrcode.react` - テスト: Vitest - Lint / Format: ESLint / Prettier ## 主要コマンド ```text pnpm install docker compose up -d postgres docker compose up -d --build docker compose -f docker-compose.yml -f docker-compose.dev.yml up --build pnpm dev pnpm monitor:once pnpm monitor:worker pnpm lint pnpm test pnpm exec vite build pnpm format ``` 開発サーバー: ```text Frontend: http://127.0.0.1:5173/ API: http://127.0.0.1:3000 ``` Docker Compose: ```text 本番相当: docker compose up -d --build 開発用: docker compose -f docker-compose.yml -f docker-compose.dev.yml up --build 本番 URL: http://127.0.0.1:3000/ 開発 URL: http://127.0.0.1:5173/ ``` - `docker-compose.yml` は本番相当の標準 Compose として、PostgreSQL、Web/API アプリ、`monitor-worker` を起動する。 - `docker-compose.dev.yml` は開発用 override として、アプリを bind mount し `pnpm dev` で Vite と API を起動する。 - Docker CLI が Compose v2 サブコマンドに対応していない環境では、同じ引数で `docker-compose` を使う。 - Compose のアプリ公開ポートは `127.0.0.1` に限定し、外部 NIC へ公開しない。 - Compose の PostgreSQL はホストに公開せず、`app` / `monitor-worker` から内部ネットワーク経由でのみアクセスする。 - コンテナ内では `HOST=0.0.0.0` を使い、ホスト側への公開範囲は Compose の `ports` で制御する。 ## ディレクトリ構成 ```text db/schema.sql Dockerfile docker-compose.yml docker-compose.dev.yml README.md public/push-sw.js src/client src/client/api/client.js src/client/components src/client/components/Toast.jsx src/client/routes src/client/styles/app.css src/server src/server/app.js src/server/index.js src/server/config/env.js src/server/db/pool.js src/server/middleware src/server/modules src/server/jobs/monitorCertificates.js src/server/jobs/monitorWorker.js src/server/utils/logger.js tests/apiSecurity.test.js tests/monitoring.test.js tests/urlPolicy.test.js ``` ## 環境変数 `.env.example` を参照すること。 主な項目: - `NODE_ENV` - `HOST` - `PORT` - `DATABASE_URL` - `COOKIE_SECRET` - `VAPID_PUBLIC_KEY` - `VAPID_PRIVATE_KEY` - `VAPID_SUBJECT` - `OPENSSL_PATH` 補足: - `HOST` 未設定時は `127.0.0.1` で待ち受ける。Docker Compose ではコンテナ内到達性のため `0.0.0.0` を指定する。 - ホスト実行時の `DATABASE_URL` は通常 `localhost:5432`、Compose 内のサービスでは `postgres:5432` を使う。ただし Compose 起動中の PostgreSQL はホストに公開しない。 - `OPENSSL_PATH` 未設定時は `openssl` を使う。 - Windows では Git 付属の `openssl.exe` を自動検出する実装がある。 - VAPID private key が未設定の場合、Push 実送信は失敗として `delivery_result` に記録される。 ## データベース DDL は `db/schema.sql` に保管している。 実装済みテーブル: - `users` - `user_totp` - `sessions` - `sites` - `notification_methods` - `site_alert_conditions` - `alert_history` 設計メモ: - 主キーは UUID。 - 全テーブルに `created_at` / `updated_at` を持つ。 - `updated_at` はトリガーで更新する。 - `sites` は最新の証明書期限、確認日時、取得失敗内容を保持する。 - ユーザー関連データは `users` 削除を起点に CASCADE で削除する。 - サイト削除時、通知条件は CASCADE で削除する。 - アラート履歴の `site_id` はサイト削除時に `NULL` へ変更する。 - アラート重複抑制は `alert_history.dedupe_key` を使う。 注意: - `docker-compose.yml` は初回 DB 起動時に `db/schema.sql` を読み込む。既存 volume がある場合、schema の変更は自動再適用されない。 - `docker-compose.yml` はアプリ本体と監視 worker も起動するため、DB だけを起動したい場合は `docker compose up -d postgres` を使う。 - 開発用 Compose は `docker-compose.dev.yml` を重ねて使い、Vite の公開ポートも `127.0.0.1:5173` に限定する。 - Compose の PostgreSQL へホストから直接接続する前提の作業は行わない。DB 操作が必要な場合はコンテナ内実行や一時的な明示設定を検討する。 ## 実装済み API ```text GET /api/health GET /api/auth/csrf POST /api/auth/register POST /api/auth/login POST /api/auth/logout GET /api/auth/me GET /api/sites POST /api/sites GET /api/sites/:siteId PATCH /api/sites/:siteId DELETE /api/sites/:siteId GET /api/sites/:siteId/settings PUT /api/sites/:siteId/settings DELETE /api/sites/:siteId/settings GET /api/alerts PATCH /api/alerts/:alertId/read DELETE /api/alerts/:alertId GET /api/notification-methods POST /api/notification-methods/webhooks PATCH /api/notification-methods/webhooks/:methodId DELETE /api/notification-methods/webhooks/:methodId POST /api/notification-methods/push-subscriptions GET /api/account PATCH /api/account/profile PATCH /api/account/password POST /api/account/totp/setup POST /api/account/totp/verify DELETE /api/account/totp DELETE /api/account ``` ## 実装済み画面 認証前: - 登録画面 - ログイン画面 - 2 段階認証コード入力 認証後: - URL ルーティング - `/sites` - `/sites/:siteId/settings` - `/alerts` - `/notifications` - `/account` - `/login` - `/register` - 直リンク、初期読込み、ブラウザバック / フォワードに対応 - 未認証で保護画面 URL を開いた場合はログイン画面を表示し、ログイン後に元の画面へ復帰する - 左サイドメニュー - PC 幅では展開表示 - 720px 以下ではアイコンのみの畳み表示 - サイト一覧、アラート履歴、通知方法、アカウント、ログアウトに対応 - 共通トースト通知 - 認証後画面の正常メッセージ、エラーメッセージを右上に表示 - 5 秒後の自動非表示と手動クローズに対応 - ログイン / 登録画面のエラーはインライン表示を維持 - サイト一覧 - サイト追加 - 最新監視結果に基づく証明書期限表示 - 確認ダイアログ付きサイト削除 - サイト設定 - エイリアス編集 - 通知タイミング設定 - 時間 / 日 / 週間の単位指定 - 複数タイミングの追加 - 確認ダイアログ付き通知タイミング削除 - アプリ内アラート必須表示 - Webhook 選択 - Push 通知フラグ設定 - 確認ダイアログ付き設定削除 - アラート一覧 - サイト絞り込み - アラート種類絞り込み - 開始日時 / 終了日時絞り込み - 既読更新 - 確認ダイアログ付き履歴削除 - 通知方法管理 - Webhook 登録 - Webhook 編集 - 確認ダイアログ付き Webhook 削除 - ブラウザ Push 通知の許可状態表示 - VAPID public key がある場合の Push 購読登録 - アカウント設定 - 表示名更新 - ダイアログでのパスワード更新 - パスワード更新後の全セッション無効化 - ステップ式ポップアップでの 2 段階認証セットアップ - 2 段階認証 QR コード表示 - シークレット文字列表示 - 確認ダイアログ付き 2 段階認証解除 - 確認ダイアログ付きアカウント削除 ## 入力検証 サーバー側: - Zod でリクエストを検証する。 - クライアントから送られる `user_id` は信用しない。 - 認証済み API はセッションのユーザー ID を使う。 - SQL はプレースホルダを使う。 クライアント側: - 登録 / ログイン、サイト、Webhook、アラート絞り込み、アカウント設定で入力必須チェックと形式チェックを実装済み。 - クライアント側検証は UX 用であり、サーバー側検証を省略してはいけない。 ## 認証・セッション - パスワードは Argon2id でハッシュ化する。 - セッションは `sessions` テーブルで管理する。 - セッション Cookie は HttpOnly / SameSite=Lax。 - 本番環境では Secure Cookie を有効化する。 - CSRF トークンを状態変更 API で必須にする。 - パスワード更新時は対象ユーザーの全セッションを削除し、現在の Cookie も削除する。 - TOTP が有効なユーザーはログイン時に OTP 検証が必須。 ## サイト管理 - サイト URL は HTTPS のみ許可する。 - URL は `normalizeHttpsUrl` で正規化する。 - `localhost`、private IPv4、loopback IPv4 は拒否する。 - サイト API は常にセッションユーザーの所有データのみ扱う。 - サイト設定の通知タイミングは内部的に時間単位で保存する。 - 通知タイミングは 1 時間以上、17520 時間以内。 - 同じサイトに同じ通知タイミングを重複登録できない。 ## アラート履歴 - アラート履歴は `alert_history` に保存する。 - アラート一覧はログインユーザーの履歴のみ返す。 - 既読更新、削除もログインユーザーの履歴のみ対象にする。 - 絞り込み条件: - サイト - アラート種類 - 開始日時 - 終了日時 - アラート重複抑制には `dedupe_key` を使う。 ## 通知方法 Webhook: - HTTPS のみ許可する。 - `normalizeHttpsUrl` を通し、localhost / private IPv4 / loopback IPv4 を拒否する。 - Slack 互換 Webhook として送信する。 - 更新・削除はログインユーザーの通知方法のみ対象。 Push: - ブラウザ Push 購読情報を `notification_methods` に保存する。 - endpoint は HTTPS のみ許可する。 - Service Worker は `public/push-sw.js`。 - VAPID key は環境変数で管理する。 ## 証明書監視ジョブ 一回実行: ```text pnpm monitor:once ``` 定期実行 worker: ```text pnpm monitor:worker ``` 処理内容: - 登録サイトを取得する。 - OpenSSL で証明書期限を取得する。 - 成功時は `sites.certificate_expires_at` / `certificate_checked_at` を更新し、取得失敗内容をクリアする。 - 失敗時は `sites.certificate_checked_at` / `certificate_check_error` を更新し、既存の期限値は残す。 - サイトごとの通知条件を評価する。 - 条件一致時にアラート履歴を作成する。 - Webhook / Push 通知を送信する。 - 証明書取得失敗も `certificate_check_failed` としてアラート化する。 - サイト単位の失敗で全体を止めない。 - 外部通信を伴う監視処理は並列数を制限する。 - Webhook / Push の送信失敗は `delivery_result` に記録する。 - 監視ジョブの開始、終了、失敗は構造化ログで出力する。 - `pnpm monitor:worker` は 1 時間ごとに監視ジョブを実行する長時間起動プロセス。 - 本番相当の Docker Compose では `monitor-worker` サービスとして `pnpm monitor:worker` を常駐起動する。 注意: - サンドボックス内では外部 TLS 接続が `Permission denied` になる場合がある。その場合は権限付き実行が必要。 - 定期実行は `pnpm monitor:worker` で提供済み。運用要件に応じて cron やタスクスケジューラから `pnpm monitor:once` を実行する構成も選択できる。 ## セキュリティ方針 - リクエスト改ざんに注意する。 - クライアントから送信されてきたデータを信頼しない。 - CSRF トークンを使用する。 - パスワードを平文保存しない。 - SQL インジェクションを避けるためプレースホルダを使う。 - 認可はセッションユーザーを基準に判定する。 - URL ルーティングは表示状態の復元用であり、認可は必ず API 側のセッションユーザー基準で判定する。 - 未認証時の復帰先 URL はアプリ内の許可済みパスのみ扱い、外部 URL へのリダイレクトに使わない。 - 1 リクエストがサーバー全体の処理をブロックしないようにする。 - 外部通信はタイムアウトや並列数制限を設定する。 - 削除操作には確認ダイアログを置く。 - API の未捕捉エラーは構造化ログで記録する。 ## テスト・運用準備 - `tests/urlPolicy.test.js` で URL 正規化と基本的な SSRF 対策を検証する。 - `tests/apiSecurity.test.js` で CSRF、認証必須、セッションユーザー基準の認可、Webhook URL 検証、通知条件の所有者確認を検証する。 - `tests/monitoring.test.js` で証明書監視成功 / 失敗時の DB 更新、アラート作成、通知呼び出しを検証する。 - `README.md` に開発環境起動手順、検証コマンド、DB 注意点、環境変数一覧を記載済み。 - サンドボックス環境では Vite / Vitest の設定解決で権限付き実行が必要になる場合がある。 ## 既知の強化候補 - API 単体テスト / 統合テストをさらに増やす。 - ログイン試行回数制限を追加する。 - SSRF 対策として DNS 解決後の IP チェックを追加する。 - IPv6 private / link-local / unique local address の検証を追加する。 - セッションローテーションを追加する。 - E2E テストを追加する。 ## 開発時の注意 - `development_status.md` は作業後に更新する。 - DB 変更を行う場合は `db/schema.sql` も更新する。 - API を追加したら、認証、CSRF、認可、入力検証を確認する。 - UI の削除操作には確認ダイアログを追加する。 - 既存の左サイドメニューとレスポンシブ挙動を崩さない。 - フロントエンドの入力チェックを追加しても、サーバー側検証を必ず維持する。 - Docker Compose のアプリポートを変更する場合も、ホスト側 bind は `127.0.0.1` に限定する。 - 変更後は少なくとも以下を実行する。 ```text pnpm lint pnpm test pnpm exec vite build ```