会員登録のLINE通知を実装したら、管理者権限が3ヶ月前から消えていた

開発eurekapu-nuxt4

万が一、会員登録が入ったら、自分に通知が飛ぶようにしたい。 朝いちばんに出した要望はそれだけだった。

その日のうちに、自分の管理画面に自分が入れない理由を追いかけていた。

通知をどこに仕掛けるか

登録の入口はメール登録とGoogleログインの2つある。 調べさせたところ、どちらを通っても better-auth の databaseHooks.user.create.after を踏むので、通知はそこ1箇所に置けば足りると分かった。 迷う余地があるのは送り先だけだった。

メールで飛ばせば、いずれ要る配信基盤とまとめられる。 LINEなら通知が手元まで来るので、見落とさない。 自分にとっては後者のほうが重い。LINEにした。

ついでに、前から気になっていた3点も洗わせた。 退会機能は無かった。ログアウトは全レイアウトのヘッダーに入っていた。決済は動かず、原因はPrice IDの環境変数が未設定だったことにある。 決済は有料化するときの話なので、そのまま置いた。退会だけは計画を1本立てさせた。

計画をCodexに潰させる

通知と退会で計画書を2本書かせて、Codexにレビューさせた。

1回目で刺さったのは、通知の送り方だった。 最初の案は broadcast API を使うもので、これは友だち全員に飛ぶ。自分だけに届く保証がない。 避けた理由は「userId の取得が面倒だから」だった。 調べ直させたら、userId は LINE Developers コンソールのチャネル基本設定にそのまま表示されていた。前提のほうが間違っていた。

2回目のレビューでもCriticalが5件出た。

退会の判断では、こちらが何度も同じところを回った。 物理削除か、論理削除か、匿名化か。個人情報保護法の努力義務を持ち出した説明を受けても、判断材料にならなかった。 要件を「削除機能はある」「同じメールアドレスで再入会できる」の2つに絞ったら、答えは1つしか残らない。 論理削除では行が残るので、メールアドレスの一意制約に当たって再登録できない。物理削除しかない。 法令ではなく、再入会が理由だった。

HTTP 200 が返るのに、通知が来ない

Messaging APIのチャネルは、コンソールから直接作れなくなっていた。LINE公式アカウントを先に作る必要がある。 フォームを入力させた。業種の選択がずれていたのを直させ、会社名を正式名称に書き換えさせた。 営業案内の友だち追加がデフォルトでオンになっていたので、それも外させた。

トークンの発行まで来たところで、Channel secret が会話ログに出てしまった。 以降はトークン類をファイル経由で扱い、ログに出さない方針に切り替えさせた。

テスト送信は HTTP 200 を返した。 LINEのトーク画面には、何も増えていなかった。

bot/info を叩かせたら、別のボットの名前が返ってきた。既存の通知ボットのトークンが入っている。 .env のキー重複を疑ったが、重複は無い。書き込まれた値そのものが違っていた。 トークンの取得ロジックを、候補を全部拾ってそれぞれAPIで検証し、正解を選ぶ方式に変えさせた。正しい値に更新できた。

送ると、まだ別のボットから届く。

Node の --env-file は、既に存在する環境変数を上書きしない。 システム環境変数に同じ名前のトークンが設定されていて、それが既存ボットのものだった。 .env の値は最初から正しく、システム側の値が勝っていただけだった。 これを外して、ようやく狙ったチャネルへ届いた。

管理画面に入れない

ログインしたのに AdminUsers に入れない。自分は管理者のはずだった。

判定ロジックを調べさせた。 admin_email テーブルは空。つまり管理者判定は環境変数 BOOTSTRAP_ADMIN_EMAILS だけにぶら下がっていた。 本番の暗号化環境変数は5つ。その中にその名前は無かった。

原因を辿らせたら、設定ファイルのコメントに「旧実装ではメールアドレスをコード内にハードコードしていた」と書いてあった。 3ヶ月前のコミットで、ハードコードをやめて環境変数へ移していた。 そして DEPLOY.md には、その環境変数が「設定済み」と書かれていた。

書いただけで、Cloudflare 側には入れていなかった。 ドキュメントと実態がずれたまま、3ヶ月誰も気づかなかった。自分も気づかなかった。

調査のあいだに会員の一覧も見えた。自分の想定にない登録が混じっていた。

勝手に消えないようにする

「フックで効かせられますかね」と聞いた。 やらせたのは3つ。

  • 管理者を admin_email テーブルに登録し、環境変数1本に依存する状態をやめた
  • デプロイスクリプトの前段に管理者の整合性チェックを入れた。✅ 管理者 2 名を確認 が出ないと先へ進まない
  • 削除APIに保護を入れ、管理者を消せないようにした

厳密にはフックではなく、デプロイ前のゲートになった。 権限のように「無いと詰むもの」は、消えたときにデプロイが止まるほうがいい。

Windowsでは npx.cmdexecFile で実行できないので、チェックスクリプトはシェル経由になる。lintが警告を出したが、このスクリプトは定数しか使わず外部入力を一切補間しない。インジェクションの余地がない旨をコードに書かせて残した。

既存の管理画面テストが、追加したフィールドで落ちた。テストのほうを更新させて、130件を通した。

そこまで済ませてから、devで実機確認に移った。 テスト登録を1件入れると、通知の失敗記録は0件のまま、LINEに着信した。 テストユーザーを消したついでにCASCADEを実測させたら、孤児レコードは0件だった。退会計画で前提にしていた挙動がここで裏付けられた。

このとき、DBから消したのにログイン状態が続いた。cookieCache の5分残存が、計画書で書いたとおりに目の前で起きた。 better-auth の cookie は httpOnly なのでJSからは消せない。独立したブラウザコンテキストを立てて確認させた。

退会機能も通した。 確認欄が空のあいだはボタンが無効になる。実行すると user、session、account の3テーブルから行が消える。 同じメールアドレスで再入会できることまで確かめて、要件を満たした。全3155テストがパスした。

宿題を書き戻して、別セッションへ渡す

残った宿題を計画書に書き戻させて、別のセッションへ引き継がせた。

引き継ぎ先では、こちらが渡した診断のうち1件を「再現しない」と判定させた。 理由は、提案どおり直すと防御が1枚剥がれるからだった。渡した前提を鵜呑みにせず実機で確かめてから断ってきたので、こちらの指示のほうが間違っていたことになる。

secret の登録でつまずく

Cloudflare Pages のシークレットは、値を画面に出さずに入れたかった。 .env からパイプで流し込む方式を試させたところ、.env へのアクセスがガードでブロックされた。想定どおりの動きなので、これでいい。 本番D1への書き込みも、ハーネス側のガードで止まった。

読み取り専用の確認だけ先に済ませた。本番の6テーブルはすべて on_delete: CASCADE になっていた。適用対象のマイグレーションは1本で、中身はテーブル作成とインデックス追加しかない。 そのうえで、シークレットの投入は自分の手で実行した。

デプロイ後、本番で登録テストを1件流した。通知失敗が1件記録された。

LINE push failed: 401

配線自体は動いている。環境変数が届いていなければ何も記録されないからだ。トークンだけがLINEに拒否されている。 devで同じトークンを使って送ると成功した。手元の値は有効で、本番に入った値だけが弾かれている。 自分でもう一度入れ直し、再デプロイした。Pages はデプロイ時点の環境変数構成を取り込むので、入れ直しただけでは反映されない。

今度は通った。失敗件数は増えないままだった。 PowerShellのネイティブコマンド間パイプが末尾に改行を足したりエンコーディングを変えたりする、という指摘は受けていた。ただ通ってしまったので、原因は特定できていない。

最後にCodexへ見せる

その日書いたコードを、パスと観点だけ渡してCodexに読ませた。ファイルの中身は引数に載せない。

「stale cookie を5分より長く延命できる」という指摘が、自分の書いた記録と食い違った。 鵜呑みにせず実装で裏を取らせたら、Codexが正しく、記録のほうが間違っていた。criticalは0件で、本番で起きている障害も無かった。

3件を直させた。決済設定があるときに退会を止めるガードと、テストの穴と、計画書やコード内コメントの記述訂正だった。 テストの穴というのは、disableCookieCache 付きで呼ばれたかを検証していなかったという指摘で、追加した assertion が空振りでないことまで確かめさせた。 一時的に素の getSession へ戻すと、追加分だけが落ちて他の6件は通ったまま残る。捕まえるべきものを捕まえている。

テストは3,170件になった。コミットとデプロイまで通した。学習ゲートにはスキップの指示を出した。

今日の学び

  • --env-file は既存の環境変数を上書きしない。.env を直したのに変わらないときは、システム側に同名の変数が居ないかを先に見る
  • ドキュメントに「設定済み」と書いた瞬間に、設定した気になる。書く人と入れる人が同じだと誰も検証しない。だからデプロイのたびに実物を数えさせることにした
  • 権限のような「無いと詰むもの」を、環境変数1本にぶら下げない。DBに持たせて、消えたら止める
  • 追加した assertion は、一度わざと壊して落ちることを見ないと、通っている意味が分からない
#LINE Messaging API #better-auth #Cloudflare Pages #環境変数#管理者権限