Skip to content

マルチ主催組織 / アカウント体系

複数の 主催組織 が同じプラットフォーム上で大会を運営できるようにするための、アカウント・権限モデルと DB スキーマ計画。

ステータス(方針確定 / 実装前)

方針は確定。MVP では「土台(データ構造+権限スコープ+切替)」を入れ、重い機能(共同掲載UI・レベル対応表UI・項目別権限・決済分離)は受け皿だけ用意して後で有効化する。重い機能を「後で足せる」前提は、この土台(紐づけテーブル・organizer_idcan() 集約)を MVPで必ず入れ切る ことが条件。

前提・方針

  • 公開(参加者)側は1サイトに集約。主催組織で画面は分けない。分かれるのは 管理側だけ
  • 後から作り直すのが大変なもの(データの形・権限スコープ)は MVP から入れる。追加の画面・機能は後回し可
  • 既存データは仮。移行はせず、主催組織つきで再シードする
  • 公開時 Snapshot 方針(データモデル)は維持。主催組織スコープは主に live / draft に効き、公開済みの snapshot は不変

アカウント体系

原則

  1. 1人 = 1アカウント(システム共通・複数アカウント不可)
  2. 1アカウントが複数のロールを持てる(兼任OK。ロールは「紐づけ」として何件でも)

ロール

ロール範囲説明
一般ユーザ公開サイトで大会申込・マイページ(管理画面には入れない)。全アカウント共通の土台
スタッフ (staff)紐づく主催組織現場運営(MVPはアカウントのみ/操作機能・シフトは後)
主催組織の 管理者/編集/閲覧 (admin/editor/viewer)紐づく主催組織だけその組織を admin(全管理+メンバー管理)/editor(編集)/viewer(閲覧)のいずれかで
システムアドミン全主催組織全権・全機能を横断(users.is_system_admin

ロール(admin/editor/viewer/staff)は 「機能 × アクセス(編集/閲覧/なし)」のプリセット。詳細は後述の「権限判定」。

アカウントと主催組織の関係(多対多)

  • 1つの主催組織に、管理アカウントを 何人でも 紐づけられる
  • 1つのアカウントが 複数の主催組織を掛け持ち できる(ログインし直し不要
  • 1つの主催組織につきロールは1つadmin/editor/viewer/staff)。組織が違えば別のロールを持てる
  • システムアドミンは紐づけ不要で全主催組織を扱える

例(1アカウントの兼任):一般ユーザ + A組織は editor + B組織は viewer + C組織は staff

ログインと主催組織の切り替え

  • 1ログインのまま、操作する主催組織を画面で切り替える(再ログイン不要)
  • システムアドミン=どの主催組織でも/主催組織管理者・スタッフ=自分に紐づく主催組織の中から
  • 管理画面の各機能は 「いま選んでいる主催組織」のデータだけ を表示・編集

DB スキーマ計画

設計の要点

  • ロールは users.role(単一 enum)をやめ、「紐づけ」テーブル organization_members(アカウント×組織×ロール1つ)に移す
  • システム全権だけは users.is_system_admin フラグで持つ
  • 権限の判定は can(機能, 操作) に統一。ロールは「機能×アクセス」のプリセットで、機能ごとの上書きは organization_member_features(MVPは空・将来用)に入れる → “機能ごとON/OFF”を後付けでも呼び出し側を変えずに足せる
  • 大会の 主催tournaments.organizer_id(単一)を維持し、共同管理(掲載) は別テーブルで足す
  • 主催組織で分けるリソースは organizer_id を追加。ただし 規定/審査基準/ルールはレベル経由(自前の組織列は不要)。共通リソース(競技・会場)は据え置き

主催組織(既存テーブルを流用)

organizers(既存)を 主催組織 とする。id / code / name / short_name / kind / color / banner_id。変更なし。

新規テーブル

organization_members — アカウント × 主催組織 × ロール(このモデルの核。MVPで持つのはコレ)

説明
user_iduuid (FK users)アカウント
organizer_iduuid (FK organizers)主催組織
roleenum admin | editor | viewer | staffその組織でのロール(1組織1つ
PK (user_id, organizer_id)

organization_member_features — 人 × 組織 × 機能 → アクセス(機能ごとの上書き。MVPは空・将来用

説明
user_iduuid (FK users)
organizer_iduuid (FK organizers)
featureenum(tournaments/registrations/masters/reports/members …)機能
accessenum none | view | editこの機能のアクセス
PK (user_id, organizer_id, feature)。行があればロール既定より優先

tournament_co_organizers — 大会の共同管理(掲載)。主催は tournaments.organizer_id、ここは“追加で管理できる組織”

説明
tournament_iduuid (FK tournaments)対象大会
organizer_iduuid (FK organizers)共同管理する主催組織
PK (tournament_id, organizer_id)。MVPは空、掲載開始で行追加

organizer_venues — 主催組織が使える会場(会場は共通のまま、利用可リスト)

organizer_iduuid (FK organizers)
venue_iduuid (FK venues)
PK (organizer_id, venue_id)

level_tiers — 共通の「難易度の段」(レベル対応表の軸)

説明
iduuid PK
nametext例:中級
sort_indexint並び順

level_external_aliases — 段 ⇄ 外部サービスの呼び名(将来の labola 対応の器)

説明
iduuid PK
tier_iduuid (FK level_tiers)どの段か
systemtext例:labola
labeltext例:エンジョイ

権限判定(ロール=プリセット + can()

  • ロールは「機能 → アクセス」の既定表(プリセット)として展開する。テーブルではなく定数。
ts
// 機能ごとの既定アクセス(ロール → 機能 → none/view/edit)
const ROLE_PRESETS = {
  admin:  { tournaments:"edit", registrations:"edit", masters:"edit", reports:"edit", members:"edit" },
  editor: { tournaments:"edit", registrations:"edit", masters:"edit", reports:"view", members:"none" },
  viewer: { tournaments:"view", registrations:"view", masters:"view", reports:"view", members:"none" },
  staff:  { tournaments:"view", registrations:"view", masters:"none", reports:"none", members:"none" },
};

// 判定はすべてこの関数を通す(機能ごとの上書きがあれば優先、無ければロール既定)
function can(user, organizerId, feature, need /* "view"|"edit" */): boolean {
  if (user.isSystemAdmin) return true;
  const m = membershipOf(user, organizerId);          // organization_members の行
  if (!m) return false;
  const level = featureOverride(user, organizerId, feature)   // organization_member_features
             ?? ROLE_PRESETS[m.role][feature];
  return need === "edit" ? level === "edit" : level !== "none";
}
  • MVPorganization_member_features は空 → 判定はロール既定だけ
  • 将来(機能ごとON/OFF)organization_member_features に行を足すだけ。can() の呼び出し側は不変

既存テーブルへの変更

テーブル変更
users+ is_system_admin booleanrole(単一 enum)と organizer_id(単一 FK)は廃止予定 → ロールは organization_members へ。一般ユーザ=どの紐づけも持たないアカウント
tournamentsorganizer_id主催 として維持。共同管理は tournament_co_organizers
levels+ organizer_id(主催組織ごと)、+ tier_id(FK level_tiers, nullable)
prizes / discount_templates / media / announcements+ organizer_id(レベルに紐づかないので直接持つ)

一意制約の地雷(移行時に必須)

organizer_id を足すテーブルは、いま codeグローバル一意(.unique() で持っている(levels.code / discount_templates.code など)。このままだと別の主催組織が同じ code(例:早割の early)を作れない。code の一意制約は (organizer_id, code) の複合一意へ変更すること。 ※ 規定/審査基準/ルールの (sport_id, level_id) 一意は、レベルが主催組織ごとになるので自然に組織別になり変更不要。

| regulation_templates / judging_templates / rule_templates | 変更なしlevel_id でレベルを参照しているので、主催組織はレベル経由で決まる(一意制約 (競技 × レベル) は、レベルが主催組織ごとなので実質 (主催組織 × 競技 × レベル))| | sports | 変更なし(共通)| | venues | 変更なし(共通。利用可は organizer_venues)|

アクティブ主催組織(切替)と権限スコープ

  • 「操作中の主催組織」は セッション/Cookie に保持(テーブルではない)
  • 管理 API は全て 「操作中の主催組織」でスコープ + organization_members の権限チェックを通す(is_system_admin は全許可)
  • ※ これが一番“後付けが大変”なので、土台として最初から全エンドポイントに通す

レベル対応表のイメージ

段(level_tiers)A組織のレベルB組織のレベル外部(labola)
中級Lv.3エンジョイエンジョイ

levels 行が tier_id で「段」を指し、同じ段=同じ実力帯として横断対応。外部の呼び名は level_external_aliases

スコープの線引き(MVP / 後回し)

区分内容
MVPで作る上記スキーマ一式・organization_members による権限/スコープ・主催組織の切替・各リソースが主催組織ごとに見える/編集・レベル+段のデータ・会場の利用可リスト
器だけ(UI/有効化は後)大会の共同掲載(tournament_co_organizers はあるが投入は後)・レベル対応表の編集UI・項目別/機能別の細かい権限
Ver.2 / 後スタッフのシフト→名簿閲覧・決済分離(Stripe Connect)・LaBOLA連携

既存データ

仮データのため 移行しない。スキーマ変更後、主催組織つきで再シードする。

確定済みの判断(クライアント確認不要)

項目判断
共同掲載(tournament_co_organizersMVP は 器のみ(行は投入しない)。掲載UIは後
機能ごとの権限(項目別)MVP は ロール単位admin/editor/viewer/staff)。機能ごとON/OFFは organization_member_features で後付け
スタッフMVP は アカウントのみ。シフト→名簿閲覧は Ver.2
決済分離(Stripe Connect)MVP 後。organizer_id で受け皿だけ用意済み

関連