[{"data":1,"prerenderedAt":321},["ShallowReactive",2],{"content-/member-signup-notification-admin-guard":3,"all-pages-for-dir":299,"related-/member-signup-notification-admin-guard":300,"og-image-/member-signup-notification-admin-guard":320},{"id":4,"title":5,"body":6,"category":279,"concepts":280,"description":281,"extension":282,"meta":283,"navigation":284,"ogImage":280,"path":285,"project_name":286,"published":287,"publishedAt":288,"seo":289,"source":290,"stem":291,"tags":292,"todo":280,"unpublished":287,"updatedAt":280,"__hash__":298},"pages/2026-08/2026-08-12/member-signup-notification-admin-guard.md","会員登録のLINE通知を実装したら、管理者権限が3ヶ月前から消えていた",{"type":7,"value":8,"toc":267},"minimark",[9,13,16,20,28,31,34,38,41,44,47,50,54,57,60,63,73,76,86,89,92,103,110,117,120,123,126,146,149,160,163,166,169,172,176,179,182,186,195,202,205,215,218,221,225,228,231,242,245,248],[10,11,12],"p",{},"万が一、会員登録が入ったら、自分に通知が飛ぶようにしたい。\n朝いちばんに出した要望はそれだけだった。",[10,14,15],{},"その日のうちに、自分の管理画面に自分が入れない理由を追いかけていた。",[17,18,19],"h2",{"id":19},"通知をどこに仕掛けるか",[10,21,22,23,27],{},"登録の入口はメール登録とGoogleログインの2つある。\n調べさせたところ、どちらを通っても better-auth の ",[24,25,26],"code",{},"databaseHooks.user.create.after"," を踏むので、通知はそこ1箇所に置けば足りると分かった。\n迷う余地があるのは送り先だけだった。",[10,29,30],{},"メールで飛ばせば、いずれ要る配信基盤とまとめられる。\nLINEなら通知が手元まで来るので、見落とさない。\n自分にとっては後者のほうが重い。LINEにした。",[10,32,33],{},"ついでに、前から気になっていた3点も洗わせた。\n退会機能は無かった。ログアウトは全レイアウトのヘッダーに入っていた。決済は動かず、原因はPrice IDの環境変数が未設定だったことにある。\n決済は有料化するときの話なので、そのまま置いた。退会だけは計画を1本立てさせた。",[17,35,37],{"id":36},"計画をcodexに潰させる","計画をCodexに潰させる",[10,39,40],{},"通知と退会で計画書を2本書かせて、Codexにレビューさせた。",[10,42,43],{},"1回目で刺さったのは、通知の送り方だった。\n最初の案は broadcast API を使うもので、これは友だち全員に飛ぶ。自分だけに届く保証がない。\n避けた理由は「userId の取得が面倒だから」だった。\n調べ直させたら、userId は LINE Developers コンソールのチャネル基本設定にそのまま表示されていた。前提のほうが間違っていた。",[10,45,46],{},"2回目のレビューでもCriticalが5件出た。",[10,48,49],{},"退会の判断では、こちらが何度も同じところを回った。\n物理削除か、論理削除か、匿名化か。個人情報保護法の努力義務を持ち出した説明を受けても、判断材料にならなかった。\n要件を「削除機能はある」「同じメールアドレスで再入会できる」の2つに絞ったら、答えは1つしか残らない。\n論理削除では行が残るので、メールアドレスの一意制約に当たって再登録できない。物理削除しかない。\n法令ではなく、再入会が理由だった。",[17,51,53],{"id":52},"http-200-が返るのに通知が来ない","HTTP 200 が返るのに、通知が来ない",[10,55,56],{},"Messaging APIのチャネルは、コンソールから直接作れなくなっていた。LINE公式アカウントを先に作る必要がある。\nフォームを入力させた。業種の選択がずれていたのを直させ、会社名を正式名称に書き換えさせた。\n営業案内の友だち追加がデフォルトでオンになっていたので、それも外させた。",[10,58,59],{},"トークンの発行まで来たところで、Channel secret が会話ログに出てしまった。\n以降はトークン類をファイル経由で扱い、ログに出さない方針に切り替えさせた。",[10,61,62],{},"テスト送信は HTTP 200 を返した。\nLINEのトーク画面には、何も増えていなかった。",[10,64,65,68,69,72],{},[24,66,67],{},"bot/info"," を叩かせたら、別のボットの名前が返ってきた。既存の通知ボットのトークンが入っている。\n",[24,70,71],{},".env"," のキー重複を疑ったが、重複は無い。書き込まれた値そのものが違っていた。\nトークンの取得ロジックを、候補を全部拾ってそれぞれAPIで検証し、正解を選ぶ方式に変えさせた。正しい値に更新できた。",[10,74,75],{},"送ると、まだ別のボットから届く。",[10,77,78,79,82,83,85],{},"Node の ",[24,80,81],{},"--env-file"," は、既に存在する環境変数を上書きしない。\nシステム環境変数に同じ名前のトークンが設定されていて、それが既存ボットのものだった。\n",[24,84,71],{}," の値は最初から正しく、システム側の値が勝っていただけだった。\nこれを外して、ようやく狙ったチャネルへ届いた。",[17,87,88],{"id":88},"管理画面に入れない",[10,90,91],{},"ログインしたのに AdminUsers に入れない。自分は管理者のはずだった。",[10,93,94,95,98,99,102],{},"判定ロジックを調べさせた。\n",[24,96,97],{},"admin_email"," テーブルは空。つまり管理者判定は環境変数 ",[24,100,101],{},"BOOTSTRAP_ADMIN_EMAILS"," だけにぶら下がっていた。\n本番の暗号化環境変数は5つ。その中にその名前は無かった。",[10,104,105,106,109],{},"原因を辿らせたら、設定ファイルのコメントに「旧実装ではメールアドレスをコード内にハードコードしていた」と書いてあった。\n3ヶ月前のコミットで、ハードコードをやめて環境変数へ移していた。\nそして ",[24,107,108],{},"DEPLOY.md"," には、その環境変数が「設定済み」と書かれていた。",[10,111,112,116],{},[113,114,115],"strong",{},"書いただけで、Cloudflare 側には入れていなかった","。\nドキュメントと実態がずれたまま、3ヶ月誰も気づかなかった。自分も気づかなかった。",[10,118,119],{},"調査のあいだに会員の一覧も見えた。自分の想定にない登録が混じっていた。",[17,121,122],{"id":122},"勝手に消えないようにする",[10,124,125],{},"「フックで効かせられますかね」と聞いた。\nやらせたのは3つ。",[127,128,129,136,143],"ul",{},[130,131,132,133,135],"li",{},"管理者を ",[24,134,97],{}," テーブルに登録し、環境変数1本に依存する状態をやめた",[130,137,138,139,142],{},"デプロイスクリプトの前段に管理者の整合性チェックを入れた。",[24,140,141],{},"✅ 管理者 2 名を確認"," が出ないと先へ進まない",[130,144,145],{},"削除APIに保護を入れ、管理者を消せないようにした",[10,147,148],{},"厳密にはフックではなく、デプロイ前のゲートになった。\n権限のように「無いと詰むもの」は、消えたときにデプロイが止まるほうがいい。",[10,150,151,152,155,156,159],{},"Windowsでは ",[24,153,154],{},"npx.cmd"," を ",[24,157,158],{},"execFile"," で実行できないので、チェックスクリプトはシェル経由になる。lintが警告を出したが、このスクリプトは定数しか使わず外部入力を一切補間しない。インジェクションの余地がない旨をコードに書かせて残した。",[10,161,162],{},"既存の管理画面テストが、追加したフィールドで落ちた。テストのほうを更新させて、130件を通した。",[10,164,165],{},"そこまで済ませてから、devで実機確認に移った。\nテスト登録を1件入れると、通知の失敗記録は0件のまま、LINEに着信した。\nテストユーザーを消したついでにCASCADEを実測させたら、孤児レコードは0件だった。退会計画で前提にしていた挙動がここで裏付けられた。",[10,167,168],{},"このとき、DBから消したのにログイン状態が続いた。cookieCache の5分残存が、計画書で書いたとおりに目の前で起きた。\nbetter-auth の cookie は httpOnly なのでJSからは消せない。独立したブラウザコンテキストを立てて確認させた。",[10,170,171],{},"退会機能も通した。\n確認欄が空のあいだはボタンが無効になる。実行すると user、session、account の3テーブルから行が消える。\n同じメールアドレスで再入会できることまで確かめて、要件を満たした。全3155テストがパスした。",[17,173,175],{"id":174},"宿題を書き戻して別セッションへ渡す","宿題を書き戻して、別セッションへ渡す",[10,177,178],{},"残った宿題を計画書に書き戻させて、別のセッションへ引き継がせた。",[10,180,181],{},"引き継ぎ先では、こちらが渡した診断のうち1件を「再現しない」と判定させた。\n理由は、提案どおり直すと防御が1枚剥がれるからだった。渡した前提を鵜呑みにせず実機で確かめてから断ってきたので、こちらの指示のほうが間違っていたことになる。",[17,183,185],{"id":184},"secret-の登録でつまずく","secret の登録でつまずく",[10,187,188,189,191,192,194],{},"Cloudflare Pages のシークレットは、値を画面に出さずに入れたかった。\n",[24,190,71],{}," からパイプで流し込む方式を試させたところ、",[24,193,71],{}," へのアクセスがガードでブロックされた。想定どおりの動きなので、これでいい。\n本番D1への書き込みも、ハーネス側のガードで止まった。",[10,196,197,198,201],{},"読み取り専用の確認だけ先に済ませた。本番の6テーブルはすべて ",[24,199,200],{},"on_delete: CASCADE"," になっていた。適用対象のマイグレーションは1本で、中身はテーブル作成とインデックス追加しかない。\nそのうえで、シークレットの投入は自分の手で実行した。",[10,203,204],{},"デプロイ後、本番で登録テストを1件流した。通知失敗が1件記録された。",[206,207,212],"pre",{"className":208,"code":210,"language":211},[209],"language-text","LINE push failed: 401\n","text",[24,213,210],{"__ignoreMap":214},"",[10,216,217],{},"配線自体は動いている。環境変数が届いていなければ何も記録されないからだ。トークンだけがLINEに拒否されている。\ndevで同じトークンを使って送ると成功した。手元の値は有効で、本番に入った値だけが弾かれている。\n自分でもう一度入れ直し、再デプロイした。Pages はデプロイ時点の環境変数構成を取り込むので、入れ直しただけでは反映されない。",[10,219,220],{},"今度は通った。失敗件数は増えないままだった。\nPowerShellのネイティブコマンド間パイプが末尾に改行を足したりエンコーディングを変えたりする、という指摘は受けていた。ただ通ってしまったので、原因は特定できていない。",[17,222,224],{"id":223},"最後にcodexへ見せる","最後にCodexへ見せる",[10,226,227],{},"その日書いたコードを、パスと観点だけ渡してCodexに読ませた。ファイルの中身は引数に載せない。",[10,229,230],{},"「stale cookie を5分より長く延命できる」という指摘が、自分の書いた記録と食い違った。\n鵜呑みにせず実装で裏を取らせたら、Codexが正しく、記録のほうが間違っていた。criticalは0件で、本番で起きている障害も無かった。",[10,232,233,234,237,238,241],{},"3件を直させた。決済設定があるときに退会を止めるガードと、テストの穴と、計画書やコード内コメントの記述訂正だった。\nテストの穴というのは、",[24,235,236],{},"disableCookieCache"," 付きで呼ばれたかを検証していなかったという指摘で、追加した assertion が空振りでないことまで確かめさせた。\n一時的に素の ",[24,239,240],{},"getSession"," へ戻すと、追加分だけが落ちて他の6件は通ったまま残る。捕まえるべきものを捕まえている。",[10,243,244],{},"テストは3,170件になった。コミットとデプロイまで通した。学習ゲートにはスキップの指示を出した。",[17,246,247],{"id":247},"今日の学び",[127,249,250,258,261,264],{},[130,251,252,254,255,257],{},[24,253,81],{}," は既存の環境変数を上書きしない。",[24,256,71],{}," を直したのに変わらないときは、システム側に同名の変数が居ないかを先に見る",[130,259,260],{},"ドキュメントに「設定済み」と書いた瞬間に、設定した気になる。書く人と入れる人が同じだと誰も検証しない。だからデプロイのたびに実物を数えさせることにした",[130,262,263],{},"権限のような「無いと詰むもの」を、環境変数1本にぶら下げない。DBに持たせて、消えたら止める",[130,265,266],{},"追加した assertion は、一度わざと壊して落ちることを見ないと、通っている意味が分からない",{"title":214,"searchDepth":268,"depth":268,"links":269},2,[270,271,272,273,274,275,276,277,278],{"id":19,"depth":268,"text":19},{"id":36,"depth":268,"text":37},{"id":52,"depth":268,"text":53},{"id":88,"depth":268,"text":88},{"id":122,"depth":268,"text":122},{"id":174,"depth":268,"text":175},{"id":184,"depth":268,"text":185},{"id":223,"depth":268,"text":224},{"id":247,"depth":268,"text":247},"dev",null,"会員登録が入ったら自分へLINE通知を飛ばす仕組みを入れた。その途中で管理画面に入れなくなり、管理者判定が環境変数1本にぶら下がったまま3ヶ月放置されていたことが分かるまでの記録。","md",{},true,"/member-signup-notification-admin-guard","eurekapu-nuxt4",false,"2026-08-12T00:00:00.000Z",{"title":5,"description":281},"make-diary","2026-08/2026-08-12/member-signup-notification-admin-guard",[293,294,295,296,297],"LINE Messaging API","better-auth","Cloudflare Pages","環境変数","管理者権限","wnf9ydPh1ra46EAyVxK80C5j7abicZlnY4hyutlwphQ",[],[301,305,309,313,316],{"title":302,"path":303,"publishedAt":304},"壊れて見えた投稿ヒートマップと、別マシンに置き去りだった10コミット","/blog-heatmap-and-stale-branch","2026-08-03T00:00:00.000Z",{"title":306,"path":307,"publishedAt":308},"Herokuを止める前に導線を移す — app.eurekapu.com 静的化の本番切替とUdemyリンク62本の移設","/app-cutover-udemy-links-finish","2026-07-28T00:00:00.000Z",{"title":310,"path":311,"publishedAt":312},"Herokuを止められるか、nuxt generate を4回回して確かめた — 旧Nuxt 2アプリ静的化の実測","/app-static-build-heroku-retirement","2026-07-27T00:00:00.000Z",{"title":314,"path":315,"publishedAt":312},"移行サイトのSEO・AI-SEO改善を1日で回した — 画像altを480件、記事の要点を203本、OG画像を署名付きに","/wordpress-nuxt-ai-seo-phase-c",{"title":317,"path":318,"publishedAt":319},"本番切替の前に移行先サイトを仕上げた日 — 画像の謎、フォームの文字化け、トップページの作り直し","/wordpress-site-final-polish","2026-07-26T00:00:00.000Z","https://log.eurekapu.com/og/blog/member-signup-notification-admin-guard.png?v=2026-08-12T00%3A00%3A00.000Z&title=%E4%BC%9A%E5%93%A1%E7%99%BB%E9%8C%B2%E3%81%AELINE%E9%80%9A%E7%9F%A5%E3%82%92%E5%AE%9F%E8%A3%85%E3%81%97%E3%81%9F%E3%82%89%E3%80%81%E7%AE%A1%E7%90%86%E8%80%85%E6%A8%A9%E9%99%90%E3%81%8C3%E3%83%B6%E6%9C%88%E5%89%8D%E3%81%8B%E3%82%89%E6%B6%88%E3%81%88%E3%81%A6%E3%81%84%E3%81%9F&author=Kei%20Komatsu&sig=64896ff33a15f146",1786603504374]