Skip to content

自分のシフトだけをGoogleカレンダーに表示できるようにする - #73

Merged
champierre merged 1 commit into
mainfrom
feature/my-shifts-ical-subscription
Sep 15, 2026
Merged

champierre merged 1 commit into
mainfrom
feature/my-shifts-ical-subscription

Conversation

@champierre

@champierre champierre commented Sep 15, 2026 •

Copy link
Copy Markdown
Contributor

概要

通常ユーザーの「自分のシフト一覧」タブに、自分のシフトだけの iCal 購読URLを表示するようにしました。Googleカレンダーの「URLで追加」に貼り付けると、以後シフトを申請・削除するたびにカレンダー側へ自動で反映されます。

あわせて、購読トークンの設計上の問題を修正しています。

調査でわかったこと

iCal 配信の仕組み(ICalService / routes/ical.ts)と、特定ユーザーのシフトだけを返すエンドポイントはすでに実装済みでした。そのため本 PR は新規実装ではなく、既存の仕組みを一般ユーザーに露出させる作業が主になっています。

ただし、そのまま露出させると問題がありました。

修正したセキュリティ上の問題

これまで /api/ical/user/:user_id の検証は .env の ICAL_TOKEN 1個だけを見ており、しかもそのトークンは /api/config 経由でログイン前の全ブラウザへ配布されていました。

購読URLはカレンダーアプリに貼られてユーザーの手元に長く残るため、この状態で学生に配ると次のことができてしまいます。

  • 購読URLの user_id を書き換えるだけで他人のシフトを閲覧できる
  • /ical/all に変えれば全員分のシフトを取得できる

対応

ICAL_TOKEN をサーバー秘密鍵として扱い、各ユーザーの購読トークンを HMAC-SHA256(ICAL_TOKEN, "ical-user:" + user_id) で導出する方式に変更しました。トークンは user_id と対でのみ有効です。

  • DB マイグレーション不要(保存せず都度導出)
  • ICAL_TOKEN を再生成すれば全ユーザーの購読URLを一括失効できる
  • 比較は timingSafeEqual を使用

変更内容

サーバー

  • server/src/utils/icalToken.ts (新規) — HMAC によるトークン導出・検証
  • GET /api/ical/my-url?userId= (新規) — 本人の購読URLを発行
  • GET /api/ical/all-url?email= (新規) — 管理者用の購読URLを発行(AUTHORIZED_EMAILS で認可)
  • /api/config から icalToken の配布を廃止
  • Content-Disposition を attachment → inline に変更(購読用途のため)

フロントエンド

  • 「自分のシフト一覧」タブ上部に購読URL欄と「コピー」「使い方」を追加
  • 0件時の早期 return に影響されないよう、index.html に静的配置
  • 初回ログイン時の描画パス(auth.js)とタブ切替パス(loadMyShifts)の両方から初期化
  • クリップボードが使えない環境では手動コピーを促すフォールバックを追加

UI の変更点

「自分のシフト一覧」タブの上部に、自分のシフトだけの購読URL欄を追加しました。

「自分のシフト一覧」タブ上部に追加した購読URL欄

「コピー」ボタンを押すと文言が「コピーしました」に変わります。

コピーボタンを押した状態

ドキュメント

  • help.html に第6章「Googleカレンダーに表示する」を追加(学生向け・キャプチャ付き)
    • 反映まで最大24時間かかる点を明記(問い合わせが予想されるため)
    • URLを他人に共有しないよう注意書きを追加
  • README.md のエンドポイント表・トークン設計・セキュリティ注意を更新

回帰修正

config.ICAL_TOKEN を廃止した結果、管理者の「iCal 購読URL(全シフト)」欄が常に「ICAL_TOKEN が未設定です」表示になる不具合を作り込んでいました。既存テストでは検出できなかったため、サーバーから取得する方式に変更したうえで再発防止テストを追加しています。

テスト

CLAUDE.md の TDD 方針に従い、失敗するテストを先に書いてから実装しました。

Test Suites: 30 passed, 30 total
Tests:       412 passed, 412 total

新規テスト:

  • server/src/__tests__/utils/icalToken.test.ts — トークン導出・検証
  • server/src/__tests__/routes/ical.test.ts — 他人の user_id で 401 になること等
  • server/src/__tests__/routes/config.test.ts — 共通トークンを配布しないこと
  • test/my-shifts-ical-url.test.js — URL組み立て・コピー
  • test/admin-ical-url.test.js — 上記回帰の再発防止

テストが実装を実際に検証していることを確認するため、ロジックを一時的に旧挙動へ戻して該当テストが落ちることも確認済みです。

実サーバーでの動作確認

自分のトークンで自分のシフト        → 200 text/calendar
自分のトークンで他人の user_id      → 401
共通トークンで個人のエンドポイント   → 401
ユーザートークンで /ical/all        → 401
未認可メールで /ical/all-url        → 403(トークン非露出)

確認をお願いしたい点

  • /api/ical/all-url の認可はメールアドレスをクエリで受け取る方式にしました。既存 API に認可チェックがないため最小限に留めていますが、本来はサーバー側で JWT 検証をするのが筋だと思います(別 PR 向け)。
  • help.html の章番号を振り直しています(6〜8 → 7〜9)。

🤖 Generated with Claude Code

通常ユーザーの「自分のシフト一覧」タブに、自分のシフトだけのiCal購読URLを
表示する。Googleカレンダーの「URLで追加」に貼り付けると、以後シフトの増減が
自動で反映される。

あわせて、購読トークンの設計上の問題を修正する。

これまで /api/ical/user/:user_id は .env の ICAL_TOKEN 1個だけで検証しており、
そのトークンは /api/config からログイン前の全ブラウザへ配布されていた。
このため購読URLの user_id を書き換えるだけで他人のシフトを閲覧でき、
/ical/all で全員分も取得できる状態だった。

変更点:

- ICAL_TOKEN をサーバー秘密鍵として扱い、/api/config からの配布を廃止
- 購読トークンを HMAC-SHA256(ICAL_TOKEN, "ical-user:" + user_id) で導出し、
  user_id と対でのみ有効にする(DBマイグレーション不要)
- GET /api/ical/my-url で本人の購読URLを発行
- GET /api/ical/all-url で管理者用の購読URLを発行(AUTHORIZED_EMAILS で認可)
- 購読を想定し Content-Disposition を attachment から inline に変更
- クリップボードが使えない環境では手動コピーを促すフォールバックを追加
- help.html に学生向けの手順(キャプチャ付き)を第6章として追加

回帰修正:
config.ICAL_TOKEN の廃止により、管理者の「iCal 購読URL(全シフト)」欄が
常に「未設定」表示になる不具合が発生したため、サーバーから取得する方式へ変更。
再発防止テストを test/admin-ical-url.test.js に追加した。

テスト: 412 passed (30 suites)

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@champierre
champierre merged commit 45f9e60 into main Sep 15, 2026
4 checks passed
@champierre
champierre deleted the feature/my-shifts-ical-subscription branch September 15, 2026 07:55
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.

1 participant