← ブログ一覧に戻る
MIRAISFastAPINext.js認証・認可

MIRAIS 設計・実装記録 #3|サーバと画面 — バックエンドとフロントエンドの実装

第 3 回 サーバと画面

設計をコードにする回です。レイヤー分離、3 経路の認証設計、API のドメイン構成、そしてフロントエンドの構成と、認証必須のポスター PDF をブラウザに表示するための実装を扱います。

第 VII 部 バックエンド


22. アプリ構成

22-1. レイヤー分離

app/
├── main.py       エントリポイント、CORS、起動処理、ルータ登録
├── models.py     データモデル(永続化層)
├── schemas.py    API 契約スキーマ
├── cli.py        管理 CLI
├── core/         設定 / DB セッション / JWT
├── api/          ルーティング層(17 モジュール)— HTTP と認可のみ
└── services/     ビジネスロジック層(13 モジュール)— 純粋な処理

原則:業務ルールはルーターではなくサービス層に集約する。 実際、表彰アルゴリズムやスライド生成といった重い処理はすべてサービス層にあり、ルーティング層は入出力の変換と認可に徹しています。

この分離を守った効果は、テストのしやすさよりもむしろ読みやすさに現れました。「この機能はどこにあるか」を探すとき、HTTP の話なら api/、業務ロジックなら services/ と迷わず辿れます。

22-2. 起動時の処理

起動時にテーブルの自動作成を行い、開発モードの場合のみデモデータを投入します。本番モードではシードは一切走りません。本番でデータを投入する必要がある場合は、独立した管理 CLI を使う設計にしています。開発用フラグと本番のデータ投入を別の経路に分けることで、「開発フラグを立てないとデータが入れられない」という危険な依存を避けました。


23. 認証・認可の設計

23-1. 3 つの認証経路

① 学内ユーザー(学生・教員・事務)
   ブラウザ → Google Identity → ID Token
   → バックエンドで ID Token を検証
   → メールアドレスからロールを解決、ドメインを検証
   → ユーザーを作成または更新 → 自前 JWT を発行

② 参加企業
   運営が管理画面から招待リンクを発行(有効期限つきトークン)
   → 企業が URL を開くだけでログイン
   → トークンの期限と使用履歴を確認、未承認企業は拒否
   → 自前 JWT を発行

③ 開発用(本番では無効)
   ロールを指定した即時ログイン。デモと検証の高速化のため

企業向けにパスワードを設定させない設計にしたのは、参加企業に新しいアカウントを作らせないという方針からです。年に一度しか使わないサービスのためにパスワードを管理させるのは、利用者にとって負担でしかありません。

23-2. 単一のトークン形式で 2 種類の主体を扱う

JWT のペイロードに「主体の種別」を持たせ、それが「ユーザーテーブルを引くか企業テーブルを引くか」を決めます。これにより、学内ユーザーと企業という異なるエンティティを 1 つのトークン形式で扱えるようになりました。

API 層は抽象化された「principal」オブジェクトだけを見ればよく、「ユーザーか企業か」の分岐が 1 箇所に閉じています。この抽象を最初に置いたおかげで、その後に増えた企業向け機能を、認証まわりを触らずに追加できました。

23-3. 認可の依存関係

認証必須              未認証なら拒否
学内ユーザー限定       企業トークンなら拒否
企業限定              ユーザートークンなら拒否
ロール限定            指定ロール以外は拒否

ロール解決には、運営・教員として登録されたメールアドレスを自動的に昇格させる仕組みがあります。既存ユーザーのロールは昇格方向にのみ自動更新され、降格は起きません。

23-4. 「現在のイベント」の解決

現在のイベント=年度が最大のもの、という単純な規則にしました。年度切替は「新しい年度のイベントレコードを作る」だけで完了します。日付による判定や手動フラグも検討しましたが、規則が単純であるほど間違いが起きにくいと考え、これを採りました。


24. API の構成

エンドポイントの網羅的な一覧は公開版では省略し、機能ドメイン単位の要約を記載します。

ドメイン提供内容認可
公開イベントの公開属性、研究のタイトルと抄録のみ認証不要
認証Google ログイン、招待リンクログイン、自身の情報取得一部認証必須
イベントイベント CRUD、タイムテーブル、FAQ、お知らせ、ゼミ、当日の「現在の部」参照は公開/更新は運営
提出物一覧・詳細、自分の提出物の取得と更新、受賞情報認証必須
企業運営向け CRUD と招待リンク発行、企業自身の関心・推薦・ブックマーク運営/企業
コメント属性バッジ付きの一覧と投稿認証必須
投票順位投票・評価投票、集計認証必須
マッチング意味検索、埋め込みの再構築一部運営限定
オファーマッチング希望の送信とステータス変更企業/学生
ブース企業ブースの取得・更新、3D 描画用の一覧企業/認証必須
ポスターPDF の配信とアップロード認証必須/学生限定
運営ダッシュボード、学生一覧、リマインド、設定、表彰選定、企業詳細、ブース割当、スライド出力運営限定
同期スプレッドシート書き出し、過年度 CSV 取り込み、ジョブ履歴運営限定
教員自ゼミの進捗、学科賞推薦、投票ランキング教員限定
学生所属年度の判定と誘導先の指示学生限定
開発シード、フェーズ切替、接続状況本番では無効

24-1. 提出ステータスの自動判定

学生に「提出しました」ボタンを押させる代わりに、入力内容から自動でステータスを決めています。

タイトル + 抄録 + ポスター が揃っている → 完了
タイトル のみ                            → タイトル済
どちらもない                             → 未提出

「2 週間前までにタイトルと概要、その後にポスター」という実際の運用が 2 段階であることに対応しています。学生からすると、入力するだけで状態が進むので、提出したつもりが未提出だった、という事故が起きません。

24-2. ポスターアップロードの検証

学生からのファイル受け取りは、システムの中で最も慎重に扱うべき箇所です。以下を多段で確認しています。

  • ロールが学生であること
  • 学籍番号が登録済みで、数字のみであること
  • 現在のフェーズが受付中であること
  • Content-Type と拡張子が PDF であること
  • ファイル先頭のバイト列が PDF の署名であること
  • ファイルサイズが上限以内であること

保存先のファイル名はログイン中のユーザーの学籍番号から生成され、リクエストパラメータには由来しません。他人のファイルを上書きする経路を、そもそも作らないという設計です。



第 VIII 部 フロントエンド


25. 構成と API 契約

25-1. App Router とサイトマップの一致

設計したサイトマップが、そのままディレクトリ構造に対応します。この 1 対 1 対応は設計段階から意図していました。設計書とコードの構造が一致していると、「この画面を直したい」から「このファイルを開く」までの距離がゼロになります。

すべてのページをクライアントコンポーネントとし、データ取得は SWR で行っています。サーバーサイドレンダリングを使っていない理由は、①認証トークンをブラウザ側で保持しているためサーバー側でセッションを持てない、②単一コンテナ構成でシンプルさを優先した、の 2 点です。

25-2. API クライアント

型定義と HTTP クライアントを 1 ファイルに集約しました。バックエンドのスキーマと対になる TypeScript 型を定義しており、レスポンス型は 30 種類以上あります。

クライアントの挙動は以下の通りです。

- 保存されたトークンを Authorization ヘッダに自動付与
- JSON ボディには Content-Type を自動設定(ファイル送信時は除く)
- 常に最新を取得(キャッシュしない)
- 非 2xx はカスタムエラーとして throw し、サーバーのエラーメッセージを採用

エラーメッセージをサーバー側の文言そのまま表示する設計にしたのは、バックエンドで日本語のわかりやすいメッセージ(「PDF ファイルのみアップロードできます」など)を返しているためです。フロント側で英語のステータスコードを翻訳し直すより、発生源で書いたほうが正確になります。

25-3. 認証状態の管理

セッションは SWR のキーとして自身の情報取得エンドポイントを参照し、フォーカス時の再検証を無効にしています。ログアウトはトークンの破棄とキャッシュの無効化のみです。サーバー側にセッションを持たないステートレス設計のため、これで完結します。


26. コンポーネント設計

コンポーネント役割
ナビゲーションバーロール別にリンクを動的切替。ログイン状態で識別チップとログアウトを表示
ロールガード許可ロール以外に「アクセス権限がありません」を表示
フェーズバッジ運用フェーズを日本語ラベルで表示
統計グリッドKPI カードのグリッド
研究カードゼミ・ブース番号・マッチ率をピルで表示
ドロップゾーンドラッグ&ドロップによるファイル受け取り
ポスタービューア認証付きで PDF を取得し、blob 経由で表示
コメントスレッド属性バッジ付きのスレッド表示
投票パネル企業は順位投票、それ以外は評価投票

26-1. ポスタービューアの設計判断

ポスター PDF は認証ヘッダが必須です。しかし <img><iframe> の src 属性はカスタムヘッダを送れません。そこで以下の手順を取りました。

① 認証ヘッダを付けて PDF を取得
② 取得したデータから blob URL を生成
③ その blob URL を iframe に流し込む

認証必須の資産をブラウザで表示するための定石であり、第 15 章 / 第 2 回の公開遮断ポリシーを崩さずに認証ユーザーへ表示するための実装上の要になっています。「ポスターを認証必須にする」という判断は、こういう実装コストとセットで引き受けるものだ、というのが学びでした。

26-2. デザインシステム

グローバル CSS に自前のクラスを定義しています。カード、ボタン、バッジ、ピル、フェーズ表示、入力欄、ヘッダ——いずれも用途から名前を付けました。テーマカラーはイベント設定から動的に注入されるため、運営が管理画面から変更した色が全ページに波及します。

スライド資料

MIRAIS 発表スライド / 「プロダクト紹介」がこの回に対応しますタップしてPDFを開く