Skip to content

2026-08-10 画面のサインイン状態をサーバーのセッションから決める(Issue #40)

  • 課題: components/SignInWithEthereum.tsx がサインイン済みかどうかをReactの状態(signedInAs)だけで持ち、サーバーのセッションを読んでいなかった。GET /api/auth/me は実装済みだが呼び出し元がゼロだった。結果として、(1) 再読み込みすると有効なセッションがあっても「署名してサインイン」に戻る、(2) 期限切れ後もタブを開いたままなら「サインイン済み」と表示し続ける、(3) ヘッダーの「運営」リンクが全員に見える(/adminnotFound() で存在を隠す設計なのに導線だけ出ている)、という3つのずれがあった。また、利用者が明示的にサインアウトする手段がなかった。

決定

1. セッションはクライアントから /api/auth/me で読む。Server Component では読まない

/setup とヘッダーのどちらも、cookies() を読むのではなく GET /api/auth/me の結果を使う。

  • ヘッダーはルートレイアウトにあるため、ここで cookies() を読むと全ページが動的になる。 トップページが静的でなくなるのは、外部から見える公開ビュー(#16)を考えると避けたい。実際、この変更後も //setup は静的なまま(npm run build の出力で確認)。
  • /setup の他のカード(ConnectWallet / WalletSetup)は元々クライアント側でウォレットの状態を見ている。サインイン表示だけサーバー由来にすると、同じ画面に真実の源が2つ混ざる。
  • 代償は、初回描画で軽量なリクエストが1回増えることと、確定するまでの短い未確定状態。未確定の間は「サインインしていない」表示を出さず、何も出さないか確認中と表示する。サインイン済みの人に状態が揺れて見えるのを避けるため。

取得は lib/useSession.ts へ集約し、react-query の同じキー(["session"])を共有する。ヘッダーと /setup が同時に使っても取得は1回。

2. サインアウトはヘッダーに常設する

/setup だけに置く案は採らない。サインアウトしたくなるのは /setup にいるときとは限らず、共用端末での利用を考えるとどのページからでも終われるほうがよい。

ヘッダーがサインアウトのためにセッションを知る必要がある以上、「運営」リンクの出し分けは追加コストなしで付いてくる。 逆に、リンクの出し分けだけのために全ページで取得を1回増やすのは割に合わない。この2つは一体の判断として決めた。

3. 「運営」リンクは管理者にだけ表示する

GET /api/auth/me の応答に isAdmin を加えた。返すのは要求者自身の権限だけで、管理者の一覧は返さない。

これはアクセス制御ではない。 アクセス制御は従来どおりサーバー側の requireAdmin() で、/admin は認可に失敗すれば404を返す。リンクを隠す価値は「権限のない人に、押しても404になる導線を見せない」ことであって、/admin というパスの存在は隠せない(notFound() による否認であり、URLの秘匿ではない)。この点は architecture.md にも明記した。

4. ウォレット未接続のときはセッションを破棄しない

アカウント・チェーンの変更を検知してサインアウトする既存の挙動は残す。ただし判定を次のように変えた。

  • 表示の根拠がサーバー側になったため、ウォレットが未接続(addressundefined)の状態でも比較が走るようになった。 wagmi の再接続が終わる前はこの状態を通るので、そのまま破棄すると有効なセッションを消してしまう。未接続のときは何もしない。
  • セッションのアドレスは正規化済みの小文字、wagmi 側はチェックサム表記なので、比較の前に小文字へ揃える。揃えないと常に不一致となり、サインインした直後にサインアウトされる。

含めなかったもの

  • 自動サインイン(ページを開いただけで署名を求める挙動)
  • ウォレット接続状態の表示の作り直し
  • セッションの有効期限の変更(#41 で14日として文書化した)
  • 検知を全ページへ広げること。 検知の useEffectSignInWithEthereum の中にあり、このコンポーネントを置いているのは /setup だけなので、自動破棄が効くのは /setup を開いている間に限られる。ここで直したのは「再読み込みすると検知が効かなくなる」ほうで、「どのページでも効く」にはなっていない。SessionStatus がルートレイアウト経由で全ページに乗ったので置き場所としてはそちらが自然だが、範囲が広がるため #44 で扱う

残る重複

shortenAddresslib/domain/address.ts に置いた。#39(監査ログ閲覧)が lib/applicationEventLabels.ts に同じ処理を持っていたため、#39 の側で lib/domain/address.ts へ寄せた(#34 / PR #39)。applicationEventLabels.ts に残るのは fieldLabel だけになる。

秘密情報(SESSION_PASSWORD、Supabaseキー、Safeの認証情報)はドキュメント・Issue・ログへ貼らないでください。