コンテンツにスキップ

公開オンボーディング (メール招待 → 組織登録 / signup)

運営が宛先メールに 6 桁の認証コードを送信し、受信者がそのコードを使って

  • 組織 (tenant) 1 つ
  • その最初の管理者 (admin) アカウント 1 つ

を作成する、半クローズドな公開オンボーディング機能です。公開ページから誰でもコードを 要求することはできません (運営がメール宛に招待を発行する方式)。


1. 構成 (DB / API / Web / 運営スクリプト)

実体 役割
DB public.signup_invite_codes (migration 00000000000048 + 00000000000049) 招待台帳。email / code_hash / attempts を持つ。service_role のみ読み書き可 (RLS ロックダウン)
運営 scripts/invite-org.mjs 6 桁コードを生成 → code_hash を保存 → Resend で宛先にコードをメール送信
API POST /signup (apps/api/src/routes/signup.ts) 公開エンドポイント。email + code を検証 → tenant / admin / profiles 作成 → 招待を消費
Web /register (apps/web/src/pages/RegisterOrgPage.tsx) 公開ページ。メール + コード + 組織情報を入力 → POST /signup

セキュリティモデル (主防御は単一の鍵ではなく多層)

  • メール所有: 運営がそのアドレス宛にコードを送るため、メールを受信できる人だけがコードを知り得る。公開ページから任意のメールへコードを送る導線は無い。
  • 6 桁コードのハッシュ照合: 平文コードは DB に保存せず、code_hash = sha256(id:code) のみ保存する (salt = 招待行の id)。平文はメール本文にだけ出す。
  • 試行上限: コード照合の失敗が attempts >= 5 に達すると、その招待行では以後照合できない。
  • 短い有効期限: 既定 10 分 (--expires-min で調整可)。期限切れは登録不可。
  • 単回使用: 消費は used_at is null をガードにしたアトミック更新。同時実行でも成功は 1 件だけ。
  • signup_invite_codesservice_role (= API サーバ / 運営スクリプト) だけが読み書きする。anon / authenticated 向け RLS ポリシーは作っていないため、ブラウザのクライアントから台帳は読めない。
  • Resend の API キーは Worker (API) に置かない。コード発行はこの運営スクリプトだけが行うため、RESEND_API_KEY はスクリプト実行環境の env にのみ置く。Worker には SUPABASE_SERVICE_ROLE_KEY 等のみ。
  • 新規 tenant_id を採番し admin の app_metadata に埋めることでテナント分離
  • 作成途中で失敗した場合は作成済みリソースをロールバックし、部分作成を残さない。
  • エラーメッセージは汎用化し、存在 / 期限 / 種別 / コードのどれで弾いたかをリークしない。

レート制限について

Cloudflare Workers 単体 (追加ストレージ無し) で厳密なレート制限を実装するのは難しいため、 上記の 「メール所有 + 6 桁 + 試行上限 5 + 10 分 + 単回使用」の多層が主たる不正登録対策です。 さらに強化する場合は Cloudflare の WAF・Rate Limiting Rules の併用を検討してください。

collector と discharger の差

collector (処理業者) discharger (排出事業者)
tenants 1 行 type=collector type=discharger
admin auth ユーザー app_metadata={tenant_id, tenant_type, role:'admin'} ✅ 同左
profiles 1 行 ✅ 作成 (login_id / display_name / role='admin') ❌ 作らない (app_metadata だけで解決)

ログイン方式は既存と同じ ログインID + パスワード (email = <login_id>@members.waste-link.local)。 招待行に種別 (tenant_type) が固定されている場合、/register で選ぶ種別が一致しないと 403 になります (受信者にはメールで種別を案内してください)。


2. 運営の準備 (本番で機能させる前提) ⚠️

2-1. Worker 側 (/signup 用)

POST /signupSUPABASE_SERVICE_ROLE_KEY が無いと 503 を返します。本番 / staging の Worker に設定済みであること。

cd apps/api
pnpm exec wrangler secret put SUPABASE_SERVICE_ROLE_KEY --env production
# staging を試すなら --env staging

SUPABASE_URL / SUPABASE_ANON_KEY も同様に設定済みであること (既存機能と共通)。 RESEND_API_KEY は Worker には設定しません (発行は運営スクリプト側のみ)。

2-2. Resend 側 (コード送信用)

  1. Resend アカウントを作成する (https://resend.com)。
  2. 送信ドメインを登録し、DNS を検証する: Resend が指示する SPF / DKIM レコード (および推奨の DMARC) を DNS に追加し、ドメインを Verified にする。検証が済まないと 到達率が著しく落ちる / 送信できない。
  3. 検証済みドメインの送信元アドレスを MAIL_FROM に使う (例 onboarding@<verified-domain>)。
  4. Resend ダッシュボードで API キーを発行し、運営スクリプト実行環境の RESEND_API_KEY に置く。

3. 招待の発行 (運営が実行)

宛先メールに 6 桁コードを送る運営スクリプト。code_hash のみ DB に保存し、平文コードは メール本文にだけ出ます (ログにも出力しません)。

SUPABASE_URL="https://<project-ref>.supabase.co" \
SUPABASE_SERVICE_ROLE_KEY="<service_role キー>" \
RESEND_API_KEY="<Resend API キー>" \
MAIL_FROM="onboarding@<verified-domain>" \
node scripts/invite-org.mjs \
  --email owner@example.com \
  --tenant-type collector \
  --org-name "サンプル運輸" \
  --expires-min 10
引数 必須 説明
--email <addr> 招待先メール。この宛先にコードが届く
--tenant-type <collector\|discharger> 種別を固定 (省略するとどちらの種別でも使える)
--org-name <text> 組織名ヒント (note に保存。任意)
--expires-min <int> 有効期限の分数 (既定 10)
--dry-run DB / メール送信せず、計画だけ表示 (env 不要)

--dry-run を付ければ env 無しで内容を確認できます (コードは生成・表示しません)。

再送は、クールダウンを考慮しつつ同じコマンドを再実行します。同一メールの「未使用」招待行は 置換され (delete → insert)、常に最新の 1 件だけが有効になります。

招待コード・SUPABASE_SERVICE_ROLE_KEYRESEND_API_KEY秘密情報です。Slack 等に 平文で貼らず、ログにも残さないでください。

(参考) 旧 out-of-band 方式

メールを介さず運営が手入力する平文招待コード (signup_invite_codes.code) を発行する旧方式は 廃止しました (bootstrap-org.mjs --issue-invite-code は削除済み)。招待は必ず scripts/invite-org.mjs で「宛先メール + code_hash」として発行してください (新フロー: メール + 6 桁)。 code 列は後方互換のため残しています。


4. 登録フロー (受信者視点)

  1. 運営から届いた招待メールを開き、6 桁の認証コードを確認する。
  2. Web アプリの /register を開く (ログイン画面の「招待メールをお持ちの方の組織登録」からも遷移可)。
  3. 招待メールアドレス / 認証コード (6桁) / 組織名 / 事業者種別 / 管理者ログインID / 管理者名 / パスワードを入力して送信。
  4. 成功すると /signin に遷移し、ログインID がプリフィルされる。指定したログインID + パスワードでログイン。
  5. collector admin → /collector
  6. discharger → /discharger

エラーの意味 (API レスポンス)

status 意味
400 入力不備 (メール形式・コードが6桁でない・組織名・ログインID 形式・パスワード長など)
403 招待が無効 (存在しない / 期限切れ / 試行上限 / コード不一致 / 種別不一致)。原因は区別せず汎用表示
409 そのログインID が既に使われている
503 サーバに SUPABASE_SERVICE_ROLE_KEY 等が未設定 (本番前提の §2-1 を確認)

5. 関連ドキュメント