シフリー — 業務用シフト管理システムのセキュリティと設計判断
背景と課題
飲食店や学習塾など小規模店舗のシフト管理は、多くの場合LINEグループやExcelで行われています。管理者がシフト表を手作業で作成し、従業員が個別にスケジュールを確認するという運用には以下の問題があります。
- 従業員の空き時間の把握に毎回やりとりが必要
- シフト表の作成・修正・共有が手作業
- 確定したシフトを各自のカレンダーに転記する手間
- 承認フローが口頭やメッセージベースで記録が残らない
本システムは、これらの課題をGoogle Calendar連携と承認ワークフローで解決する業務用Webアプリケーションです。
サイト: https://shifree.vercel.app
ワークフロー設計
本システムは3つのロール(ワーカー、管理者、オーナー)による承認ワークフローを中心に設計しています。
管理者: シフト期間を作成 → 公開
↓
ワーカー: Googleカレンダーから空き時間を自動計算 → シフト希望を提出
↓
管理者: 提出状況を確認 → シフト表を作成 → オーナーへ承認申請
↓
オーナー: 確認 → 承認 or 差し戻し(コメント付き)
↓
管理者: 確定 → ワーカーのGoogleカレンダーに自動登録
シフト期間のステータス遷移(draft → open → closed → confirmed)はShiftPeriodモデルで管理し、各遷移時にバリデーションを実行します。承認・差し戻しの履歴はApprovalHistoryとして記録し、監査ログとして保持しています。
技術構成
| カテゴリ | 技術 |
|---|---|
| バックエンド | Python 3.9+ / Flask 3.1 |
| ORM / マイグレーション | Flask-SQLAlchemy / Flask-Migrate (Alembic) |
| DB | SQLite(ローカル) / PostgreSQL(本番) |
| 認証 | Google OAuth 2.0 |
| 外部API | Google Calendar API v3 |
| テスト | pytest(153件) |
| フロントエンド | HTML5 / CSS3 / JavaScript(ロール別SPA) |
| デプロイ | Vercel(Serverless Python + Cron Jobs) |
全体で7,586行・45ファイルの構成です。
| 種別 | ファイル数 | 行数 |
|---|---|---|
| Python | 29 | 2,312 |
| JavaScript | 8 | 2,823 |
| CSS | 4 | 1,936 |
| HTML | 4 | 515 |
Google Calendar双方向同期
本システムの技術的な核は、Google Calendar APIとの双方向同期です。
営業時間の同期と衝突解決
管理者が設定した営業時間(曜日別デフォルト + 例外日)をGoogleカレンダーにエクスポートし、逆にGoogleカレンダーの予定から営業時間をインポートすることもできます。
双方向同期で最も難しいのは衝突解決のルール設計です。本システムでは「どちらが真実の源か」をイベント単位で管理し、データソースの優先順位に基づいて同期の方向を制御しています。
べき等性を確保するため、同期状態を追跡し、既にカレンダーに存在するイベントは変更時のみ更新します。同期操作はすべてログに記録され、運用時の問題調査に使用できます。
ワーカーの空き時間自動計算
ワーカーがシフト希望を提出する際、Googleカレンダーの既存予定を取得して空き時間を自動計算します。手動で時間を調整することも可能ですが、多くのケースで自動計算の結果をそのまま使えるため、提出作業を大幅に短縮しています。
確定シフトのカレンダー登録
承認されたシフトは、各ワーカーのGoogleカレンダーに自動でイベントを作成します。この処理は非同期タスクキューで実行されます。
RBAC(ロールベースアクセス制御)
DB駆動のRBAC(Role-Based Access Control)を実装しています。
ミドルウェアによる強制
ミドルウェア層でデコレータベースの認証・権限チェックを実装し、すべてのAPIエンドポイントで強制しています。組織に未参加のユーザーは業務APIへのアクセスが遮断され、専用の案内ページに誘導されます。
マルチテナント設計
マルチテナント化に伴い、権限の正規化ソースをユーザー単位からメンバーシップ単位に移行しました。すべてのデータアクセスに組織スコープを適用し、他組織のデータへのアクセス(IDOR)を防止しています。最後の管理者を降格・削除できないガードも実装しています。
招待フロー
管理者はQRコード、共有リンク、メール招待の3つの方法でワーカーを組織に招待できます。招待なしの新規ユーザーは自動的に新組織を作成し管理者になるマルチ組織対応の設計です。
招待フローの実装では、モバイルブラウザでのOAuth認証時にセッション情報が消失する問題が発生しました。複数の受け渡し方法を併用するフォールバック設計で対応しています。
6フェーズのセキュリティ強化
初期リリース後、体系的にセキュリティ強化を実施しました。優先度に基づき6フェーズに分けて段階的に対応しています。
| Phase | 対象領域 | 概要 |
|---|---|---|
| 1 | セキュリティ基盤 | 開発時の残留物の除去、シークレット管理の見直し、依存パッケージのCVE対応 |
| 2 | 認可・データ保護 | IDOR対策、入力値の検証強化、DB操作の安全性向上 |
| 3 | インフラ | セキュリティヘッダー、CORS制御、レート制限、セッション管理の改善 |
| 4 | フロントエンド | XSS対策、出力エスケープの徹底、エラーレスポンスの無害化 |
| 5 | 認証・承認フロー | セッション固定攻撃対策、トークン管理の強化、監査ログの追加 |
| 6 | CSP完全準拠 | インラインスクリプトの完全排除、CDNリソースの整合性検証 |
各フェーズでは対応するテストケースを追加し、回帰を防止しています。セキュリティ対応の詳細は非公開としていますが、OWASP Top 10を基準に、認証・認可・入力検証・出力エスケープ・暗号化・ログの各領域を網羅しています。
非同期タスクキュー
メール通知とカレンダー同期はバックグラウンドで実行する必要があります。Vercelのサーバーレス環境ではCeleryのようなワーカープロセスを常駐できないため、PostgreSQLをキューとして使うDB駆動の非同期タスクキューを自前で実装しました。
タスクにステータス管理と指数バックオフ付きリトライを実装し、サーバーレス環境の制約(Cron実行回数の制限等)に対応するため、1回の呼び出しで複数種類の処理を統合しています。また、キューへの登録が失敗した場合は同期送信にフォールバックする設計にしており、タスクキュー障害がユーザー体験に影響しないようにしています。
運用ダッシュボードからタスクの成功率と履歴を確認できます。
テスト戦略
12ファイル・191テスト関数のテストスイートを維持しています。テストは技術レイヤーではなくビジネスルール単位で整理しています。
| テストカテゴリ | テスト数 | 検証内容 |
|---|---|---|
| 入力バリデーション | 32 | 不正日付、ステータス改ざん、空body |
| 招待機能 | 23 | トークン検証、Cookie/sessionフォールバック |
| 非同期タスク | 20 | キュー処理、リトライ、フォールバック |
| マルチテナント | 19 | 組織間分離、IDOR防止 |
| RBAC | 19 | 全エンドポイントのロールベースアクセス |
| 欠員補充 | 15 | レースコンディション、トークン応答 |
| 承認ワークフロー | 10 | ステート遷移、差し戻し→再提出 |
| 認証 | 10 | OAuth、セッション管理 |
| その他 | 43 | エラーフォーマット、監査ログ、リマインダー、ダッシュボード |
レースコンディションのテスト(test_race_condition_second_accept_blocked)では、欠員補充で2人が同時にacceptした場合に2人目が拒否されることを検証しています。また、stale organization_idによるリダイレクトループのバグ修正を検証する回帰テストも含まれています。
セキュリティ強化の各フェーズで対応するテストを追加し、回帰を防止しています。
Vercelサーバーレスデプロイでの課題
マイグレーション実行タイミング
当初はビルド時にAlembicマイグレーションを実行する設計でしたが、Vercelのビルド環境からNeon PostgreSQLへの接続が不安定なケースがありました。コールドスタート時に実行する方式に変更しています。
PostgreSQL互換性
ローカル開発はSQLite、本番はPostgreSQLという構成のため、マイグレーションスクリプトの互換性問題が発生しました。Booleanカラムのデフォルト値をsa.false()に変更するなど、PostgreSQL固有の制約に対応しています。
App Factoryパターン
Vercelのエントリポイント(api/index.py)でcreate_app()ファクトリを正しく呼び出す形に修正しました。from app import appのような直接インポートではサーバーレス環境で初期化が失敗するケースがありました。
技術スタック
| 技術 | 用途 |
|---|---|
| Python 3.9+ / Flask 3.1 | バックエンドAPI |
| Flask-SQLAlchemy / Flask-Migrate | ORM / DBマイグレーション |
| PostgreSQL (Neon) | 本番DB |
| Google OAuth 2.0 | 認証 |
| Google Calendar API v3 | カレンダー双方向同期 |
| Flask-Limiter | レート制限 |
| Flask-Session | サーバーサイドセッション |
| Fernet (cryptography) | トークン暗号化 |
| pytest (153件) | 自動テスト |
| Vercel (Serverless + Cron) | デプロイ・非同期タスク実行 |
| PWA (Service Worker) | モバイル対応 |