アーキテクチャ Architecture
オーナーポータルのクライアントは React + Vite + TypeScript の SPA で、
BFF 側の requireOwner ミドルウェアが owners テーブルを参照し、
「ログインユーザーがオーナー登録済みか」を確認します。
スポット管理・料金グループ・ブースト購入など、駐車場オーナーの自己管理に必要な全機能を提供します。
The owner portal client is a React + Vite + TypeScript SPA. The BFF middleware
requireOwner checks the owners table to verify that the authenticated
user is a registered owner before allowing access to any protected endpoint.
The portal covers everything a parking-lot owner needs to self-manage: spots, pricing groups,
applications, review replies, boosts, and credits.
オーナーポータルのチャネル接続 Owner portal channel view
flowchart LR OP["Owner Portal
React + Vite
dev-owner.parky.co.jp"] subgraph BFF["Cloudflare Workers BFF"] MW["requireOwner
middleware"] API["/v1/owner/* endpoints"] UF["/v1/storage/upload-url
(R2 presign)"] end subgraph Supabase["Supabase"] AUTH["Auth"] DB[("PostgreSQL
public schema")] end subgraph Storage["Object storage"] R2["Cloudflare R2
parky bucket"] CDN["cdn.parky.co.jp
(public GET)"] end OP -->|"JWT + request"| MW MW -->|"owners テーブル確認"| DB MW --> API API --> DB API --> UF UF -->|"presigned URL"| OP OP -->|"PUT (image)"| R2 R2 --> CDN OP -->|"認証"| AUTH
requireOwner 認可フロー requireOwner authorization flow
sequenceDiagram
participant C as Owner Portal (SPA)
participant B as BFF (Cloudflare Workers)
participant DB as Supabase DB
C->>B: リクエスト + Supabase JWT
B->>B: JWT 検証 (Supabase secret)
B->>DB: SELECT id FROM public.owners WHERE user_id = $uid
alt オーナー未登録
DB-->>B: 0 rows
B-->>C: 403 Forbidden
else オーナー登録済み
DB-->>B: owner_id
B->>B: ctx.owner = { id, user_id }
B->>DB: ビジネスロジック実行
B-->>C: 200 OK
end
関連テーブル Key tables
| テーブルTable | Schema | 用途Purpose |
|---|---|---|
owners | public | オーナーレコード(個人/法人)。requireOwner の参照先。Owner records (individual / business). requireOwner checks here. |
owner_applications | public | オーナー申請。管理者審査→承認で owners レコードが生成される。Owner applications. Admin approval creates the owners record. |
parking_lots | public | 駐車場の基本情報。オーナーは自分に紐づく lot のみ編集可。Parking lot master. Owners can only edit lots linked to them. |
parking_lot_owners | public | 駐車場とオーナーの多対多リンクテーブル。Many-to-many link between lots and owners. |
parking_spots | public | 駐車場内の個別スポット(番号・タイプ・ステータス)。Individual spots inside a lot (number, type, status). |
pricing_groups | public | スポット群の料金グループ。時間帯・曜日別料金ルールを保持。Pricing groups for spot sets. Holds time/day rate rules. |
parking_reviews | public | ユーザーレビュー。オーナーは返信を投稿できる。User reviews. Owners can post replies. |
parking_boosts | public | 掲載ブースト期間の記録。Boost period records for listing promotion. |
credit_transactions | public | クレジット残高変動の履歴(purchase / consumption / refund / adjustment / gift)。Stripe Checkout 経由の購入は stripe_payment_intent_id に PaymentIntent ID を保持し、PARTIAL UNIQUE で二重入金を弾く。Credit balance change log (purchase / consumption / refund / adjustment / gift). Stripe purchases store the PaymentIntent ID in stripe_payment_intent_id, guarded by a PARTIAL UNIQUE index against double-charging. |
stripe_webhook_events | public | Stripe webhook 受信ログ。event_id PRIMARY KEY で重複配送を弾く idempotency 担保用。service_role のみアクセス可。Stripe webhook receipt log. event_id PRIMARY KEY guards against duplicate deliveries; service_role only. |
assets | public | 駐車場画像のメタデータ。実体は Cloudflare R2 に保存。Image metadata. Actual files live in Cloudflare R2. |
オーナー発行・認証フロー (LP → admin → 72h トークン) Owner provisioning & auth flow (LP → admin → 72h token)
2026-04-26 確定の統一フロー。新規招待もパスワードリセットも、72 時間有効な使い捨てトークン付きの「設定リンク」に一本化した。
メール本文に平文パスワードを載せる旧フロー、Supabase invite magic-link 系(/invite/accept)はすべて撤去済み。
Unified flow finalized on 2026-04-26. Both new invites and password resets now run through a single 72-hour single-use "setup link" token.
The legacy plaintext-password emails and the Supabase invite magic-link path (/invite/accept) have been fully removed.
sequenceDiagram
participant Owner as Future owner
participant LP as LP form
participant Admin as Admin portal
participant API as BFF
participant Email as Resend (email)
participant OP as Owner portal
Owner->>LP: 駐車場掲載問い合わせ
LP->>API: POST /v1/web/owner-inquiries
Admin->>API: POST /v1/admin/owner-registrations/{id}/approve
Note over API: Auth user 作成 (placeholder pw)
+ owners + parking_lot_owners 作成
+ token 発行 (SHA-256 hash 保存, 72h)
+ 既存 active を invalidate
API->>Email: 設定リンクメール送信
(/password-setup?token=<plain>)
Email-->>Owner: メール受信
Owner->>OP: リンククリック
OP->>API: POST /v1/owner-public/password-setup/verify
API-->>OP: ready (or 410 Gone if expired/used/invalidated)
Owner->>OP: 新パスワード入力
OP->>API: POST /v1/owner-public/password-setup/complete
API-->>OP: 200 (token consume + Supabase Auth pw 更新 + invite 時 owners.status='active')
Note over OP: 既存セッションは張らず
LoginPage に誘導
Owner->>OP: メール + 新 PW でログイン
OP->>API: GET /v1/owner/me (RequireAuth + role check)
| ステップStep | エンドポイント / 画面Endpoint / page | 内容Details |
|---|---|---|
| ① LP 問い合わせ① LP inquiry | POST /v1/web/owner-inquiries |
LP の申込フォームから owner_inquiries に登録(未認証)。書類提出は POST /v1/web/owner-inquiry-uploadsLP form writes to owner_inquiries (no auth). Document upload uses POST /v1/web/owner-inquiry-uploads |
| ② 管理者承認② Admin approve | POST /v1/admin/owner-registrations/{id}/approve |
Auth user(placeholder PW)+ owners + parking_lot_owners + 設定リンクトークン発行 + Resend メール送信Creates the Auth user (placeholder PW) + owners + parking_lot_owners + setup-link token, sends via Resend |
| ③ トークン検証③ Verify token | POST /v1/owner-public/password-setup/verify |
未認証・SHA-256 突合・410 Gone で失効通知Unauthenticated; SHA-256 match; 410 Gone on expiry |
| ④ パスワード確定④ Set password | POST /v1/owner-public/password-setup/complete |
トークン消費&Auth ユーザーのパスワード更新、Auth セッション返却Consumes the token, updates the Auth user password, returns an Auth session |
| ⑤ ロール検証⑤ Role check | GET /v1/owner/me |
RequireAuth + AuthContext.signIn で owners を確認、非オーナーは lastSignOutReason='not_owner' でログイン画面の banner に誘導SPA's RequireAuth + AuthContext.signIn hits this; non-owners surface the not_owner banner on LoginPage |
| ⑥ パスワードリセット⑥ Password reset | POST /v1/admin/owners/{id}/password-reset |
既存オーナーに新トークン発行(同テーブル purpose='reset')。新発行時は既存 active を invalidate し「最新 1 本のみ有効」を担保Issues a fresh reset token (purpose='reset') for an existing owner; previous active tokens are invalidated so only the latest is valid |
public.owner_password_setup_tokens: SHA-256 ハッシュのみ保管、平文は DB に残らないpublic.owner_password_setup_tokens: stores only SHA-256 hashes — plaintext is never persistedpurpose=invite/resetの 2 種類、有効期限 72 時間、使い捨て、service_role 専用purpose∈ {invite,reset}, 72-hour expiry, single-use, service_role only- 同一オーナーで新トークン発行時は 既存 active を invalidate し「最新 1 本のみ有効」
- Issuing a new token invalidates the previous active one — only the latest is valid
- Admin 側 OwnersPage の「認証」タブで履歴 10 件(active / used / expired / invalidated)を確認・再送信ボタンから再発行可
- The Admin OwnersPage "Auth" tab lists the last 10 token states (active / used / expired / invalidated) and has a resend button
エラー監視 (Sentry) Error monitoring (Sentry)
Owner Portal SPA は @sentry/react でフロント側の未処理例外・ErrorBoundary 捕捉・ルーティング Breadcrumb を Sentry に送信します。
DSN は Vite ビルドの環境変数 (VITE_SENTRY_DSN) から注入し、release 識別には git short SHA を使います。
BFF 側 (/v1/owner/*) の 5xx は Workers の toucan-js 連携で別途 capture されるため、SPA からは クライアント発生の例外のみを送る役割分担です。
セットアップ・DSN の入手・release tracking の手順は ops / sentry-setup を参照。
The Owner Portal SPA wires @sentry/react to forward unhandled exceptions, ErrorBoundary captures, and routing breadcrumbs to Sentry.
The DSN is injected at Vite build time via VITE_SENTRY_DSN and the git short SHA is used as the release identifier.
BFF-side 5xx responses on /v1/owner/* are captured separately through toucan-js on Workers, so the SPA only owns client-originated exceptions.
See ops / sentry-setup for DSN provisioning and release-tracking steps.
デプロイ経路 Deployment pipeline
| 環境Env | URL | トリガーTrigger |
|---|---|---|
| Dev | dev-owner.parky.co.jp | dev ブランチへの push → GitHub Actions → Cloudflare PagesPush to dev branch → GitHub Actions → Cloudflare Pages |
| Production | owner.parky.co.jp | main ブランチへの push → GitHub Actions → Cloudflare PagesPush to main branch → GitHub Actions → Cloudflare Pages |