権限管理 (技術詳細)
マダミス.jp 店舗管理アプリ(javascript/mdms_jp/shop)における、ロール定義・全ページ別の利用可能機能・権限判定の実装をまとめる。
画面操作の目線でのまとめは 権限管理 を参照。
対象範囲は /shops/[id]/* および /shopGroups/[id]/* の全ページと、関連する GraphQL mutation。
ロール定義
enum 上の定義
javascript/mdms_jp/backend/prisma/schema.prisma
enum ShopMemberRole {
ADMIN
MANAGER
STAFF
}UI 上(権限管理画面)でユーザーに割り当てられる区分は 「管理者(ADMIN)」「運営責任者(MANAGER)」「一般スタッフ(STAFF)」の 3 つ。STAFF(一般スタッフ)が最小権限のロールで、MANAGER の UI 上の表示名は「運営責任者」。
実態としての権限レベル
enum は 3 値だが、実装上は以下の 5 段階 + 1(運営マスター、ctx.requester.isAdmin) が存在する。ランク判定は authorization.ts の SHOP_ROLE_RANK(ADMIN:3 / MANAGER:2 / STAFF:1、非メンバーは 0)で行い、各関所は「しきい値以上か」で判定する。
| レベル | 識別フラグ | 意味 |
|---|---|---|
| グループ管理者 | ShopGroupMember.isGroupAdmin = true | グループ全店舗で自動的に ADMIN 扱い + 店舗グループ自体の編集権限。新店舗が追加されても自動的に ADMIN 同等。 |
管理者(ADMIN) | ShopMember.role = 'ADMIN' | その店舗の管理者。 |
運営責任者(MANAGER) | ShopMember.role = 'MANAGER' | その店舗の運営責任者。日常運用 + 部屋・価格・店舗情報の編集 + 一般スタッフ招待・スタッフ紐付け。 |
一般スタッフ(STAFF) | ShopMember.role = 'STAFF' | その店舗の最小権限ロール。閲覧中心 + 公演・予約・顧客対応 + 自分に紐付いたスタッフ枠の編集 + 顧客メモ編集。 |
| 閲覧専用 ShopGroupMember | ShopMember レコードなし & isGroupAdmin = false | 招待受諾直後やレガシー状態で発生しうる中間状態。顧客名簿などの閲覧系のみ通る。 |
| 運営マスター | ctx.requester.isAdmin = true | すべての権限判定を通る。 |
ShopGroupMember.isGroupAdmin のコメント(schema.prisma:216-217)
これが true の場合は、ShopGroup 自体の編集権限があり、かつ新規店舗が追加されても自動的に admin 同等の権限が与えられる。また、ShopMember が作成されていなかったとしても、role=ADMIN かのように振る舞う
ShopGroup・ShopGroupMember・ShopMember のリレーション
User
└─ ShopGroupMember (shopGroupId × userId で unique)
├─ isGroupAdmin: Boolean
└─ ShopMember[] (shopGroupMemberId × shopId で unique)
└─ role: ADMIN | MANAGER | STAFF- 1 ユーザー × 1 店舗グループにつき
ShopGroupMemberが 1 件である - そのユーザーがグループ内のどの店舗に所属するかは
ShopMemberレコードで表現する isGroupAdmin = trueの場合、ShopMemberレコードがなくてもすべての店舗でADMINとして振る舞う
各権限の割り当て手順
権限がどのように付与されるかを、種類別に整理する。
最初の店舗・グループの作成
新しい店舗グループ(ShopGroup)と店舗(Shop)の作成は createShopWithGroup mutation(backend/src/schema/shop/mutations/create_shop_with_group.ts)で行う。実行には 運営マスターの権限が必須(ctx.requester.isAdmin)。
この mutation は店舗グループと店舗だけを作成し、ShopGroupMember を作らない。つまり、店舗作成直後の状態では誰も権限を持たない。最初のグループ管理者は次項「招待リンクによる加入」で割り当てる。
既存グループへの 2 店舗目以降の追加は createShopInGroup mutation を使う(これも運営マスター専用)。
グループ管理者の割り当て
| 経路 | 操作 | 必要権限 | 備考 |
|---|---|---|---|
| 招待リンクによる加入 | /shopGroups/[id]/members または /shops/[id]/members で「招待リンクを作成」→ shopGroupId 指定で発行されたリンクを /invite/[token] で受諾 | リンク発行: isShopGroupAdmin / 受諾: ログインのみ | 受諾時に 自動で isGroupAdmin: true の ShopGroupMember が作成される(verify_invitation_link.ts) |
| 既存メンバーの昇格 | /shopGroups/[id]/members で対象メンバーの「グループ管理者」チェックボックスを ON → 保存 | isShopGroupAdmin(既存のグループ管理者のみ実行可) | updateShopGroupMember mutation で isGroupAdmin: true に更新(update_shop_group_member.ts:48)。同時に shopMembers[].role は 強制的に 'ADMIN' に上書きされる(同 85 行) |
| DB 直接操作 | サポート・開発者が psql などで ShopGroupMember.isGroupAdmin = true をセット | DB アクセス権限 | 初回のグループ管理者の設定など |
管理者(ADMIN)の割り当て
ShopMember.role = 'ADMIN' を持つ状態。以下のいずれかで作られる。
| 経路 | 操作 | 必要権限 | 備考 |
|---|---|---|---|
| 権限管理画面(グループ) | /shopGroups/[id]/members で対象メンバーの店舗別ロール選択を「管理者」に変更 → 保存 | isShopGroupAdmin(グループ管理者) | updateShopGroupMember の shopMembers[].role で指定(update_shop_group_member.ts:84-86) |
| 権限管理画面(店舗) | /shops/[id]/members で対象メンバーのロールを「管理者」に変更 | isShopAdmin | updateShopMember mutation(backend/src/schema/shop_member/mutations/update_shop_group_member.ts)を使用。その店舗の管理者になるだけで、isGroupAdmin は変わらない |
招待リンクで直接 ADMIN / MANAGER として加入させる経路は 無い。店舗の招待リンク経由は必ず STAFF(一般スタッフ)として登録される。運営責任者・管理者への昇格は加入後に管理者がロール変更する。
運営責任者(MANAGER)の割り当て
ShopMember.role = 'MANAGER' を持つ状態。
| 経路 | 操作 | 必要権限 | 備考 |
|---|---|---|---|
| 招待リンクで加入した一般スタッフの昇格 | /shops/[id]/members で「招待リンクを作成」→ 該当店舗のリンクを /invite/[token] で受諾したあと、管理者がロールを変更 | リンク発行: isShopManager / 受諾: ログインのみ / 昇格: isShopAdmin | 受諾時に ShopGroupMember(isGroupAdmin: false)と ShopMember (role: 'STAFF') が同時作成される。運営責任者(MANAGER)への昇格は加入後に管理者がロール変更(updateShopMember)で行う |
| 権限管理画面で降格・追加店舗付与 | /shopGroups/[id]/members で対象メンバーの店舗別ロールを「運営責任者」に設定 → 保存 | isShopGroupAdmin(グループ管理者) | updateShopGroupMember。グループ管理者のチェックが ON のときは ADMIN に強制上書きされるため、MANAGER にしたい場合はグループ管理者のチェックを OFF のままにする |
| 権限管理画面(店舗) | /shops/[id]/members で対象メンバーのロールを「運営責任者」に変更 | isShopAdmin | updateShopMember mutation |
閲覧専用 ShopGroupMember の割り当て
ShopGroupMember は存在するが isGroupAdmin = false かつ ShopMember が一件も無い状態。明示的な UI 経路は無い。以下のような経緯で発生しうる。
- 招待リンクは店舗か店舗グループのどちらかに紐付いた状態で発行されるため、通常運用では発生しないはずである
- ただし
createShopGroupMembermutation(backend/src/schema/shop_group_member/mutations/create_shop_group_member.ts)はuserIdとshopGroupIdだけを受け取りisGroupAdmin: falseのShopGroupMemberを直接作成するため、これが呼ばれた直後(まだupdateShopGroupMemberでロールを付ける前)の中間状態として発生する updateShopGroupMemberで全ShopMemberを外しつつグループ管理者のチェックも OFF にすれば手動で作れる
運営マスターの割り当て
ctx.requester.isAdmin は マダミス.jp 管轄外 で、UZU 側の運営マスターのフラグ。マダミス.jp 店舗管理 UI からは付与・剥奪できない。バックエンドの Context 構築時点で決まる。
権限の剥奪
| 操作 | mutation | 必要権限 |
|---|---|---|
| グループからメンバーを完全削除 | deleteShopGroupMember | isShopGroupAdmin |
| グループ管理者を降格 | updateShopGroupMember(isGroupAdmin: false で保存) | isShopGroupAdmin |
| ある店舗からだけ外す | updateShopGroupMember の shopMembers 配列から該当 shopId を除外して保存 | isShopGroupAdmin |
ロール変更(ADMIN ↔ MANAGER) | updateShopGroupMember または updateShopMember | isShopGroupAdmin / isShopAdmin |
権限判定の実装
バックエンドの認可ヘルパー
javascript/mdms_jp/backend/src/logic/authorization.ts に集約されている。
権限判定は SHOP_ROLE_RANK(ADMIN:3 / MANAGER:2 / STAFF:1、非メンバー 0)によるランク比較で行う。isShopAdmin / isShopManager / isShopStaff はいずれも findShopRole でロールを引き、関数名と同じしきい値(それぞれ ADMIN / MANAGER / STAFF)以上かを返す。isShopStaff は STAFF 以上 = その店舗のメンバー全員。上位ロールが下位の権限を必ず内包する(例: ADMIN は当然 isShopManager も true)ため、ロールを増やしてもしきい値の行だけで表現できる。
| 関数 | 戻り値 | 許可条件 |
|---|---|---|
findShopGroupMember({ ctx, userId, shopId }) | ShopGroupMember | null | isGroupAdmin = true または当該 shopId の ShopMember が存在 |
canViewShopGroupSensitiveData({ ctx, shopGroupId }) | boolean | 運営マスター / isGroupAdmin = true / 同 group 配下に ShopMember を 1 件以上保有 |
findShopRole({ ctx, userId, shopId }) | 'ADMIN' | 'MANAGER' | 'STAFF' | null | isGroupAdmin = true なら 'ADMIN' を返す。それ以外は ShopMember.role |
canAccessReservation(ctx, reservationId) | boolean | 運営マスター / 予約者本人(customer.userId)/ 当該店舗のメンバー |
canAccessCustomer(ctx, customerId) | boolean | 運営マスター / 顧客本人 / 顧客が属する店舗グループのメンバー(閲覧用。一般スタッフ STAFF でも true) |
canManageCustomer({ ctx, customerId }) | boolean | 運営マスター / isGroupAdmin / 顧客の属するグループ配下で MANAGER 以上。本人特例なし・一般スタッフ STAFF のみは false(顧客情報編集用) |
canAccessUserContact(ctx, userContactId) | boolean | 運営マスター / 本人 / そのユーザーの customer が属するグループのメンバー |
isShopAdmin(ctx, shopId) | boolean | 運営マスター / ランク >= ADMIN(isGroupAdmin / ShopMember.role = 'ADMIN') |
isShopManager(ctx, shopId) | boolean | 運営マスター / ランク >= MANAGER(ADMIN・グループ管理者も当然 true) |
isShopStaff(ctx, shopId) | boolean | 運営マスター / ランク >= STAFF(= その店舗のメンバーであれば true)。閲覧系の関所 |
canManageShopStaff({ ctx, shopStaffId }) | boolean | 運営マスター / 当該店舗で MANAGER 以上は全スタッフ枠 / それ未満は ShopMember.shopStaffId で自分に紐付いた枠のみ |
isShopGroupAdmin(ctx, shopGroupId) | boolean | 運営マスター / ShopGroupMember.isGroupAdmin = true |
これらは Mutation のリゾルバーの冒頭で呼ばれ、false なら ForbiddenError('この操作には ... 権限が必要です')を投げる。ctx.requester が無い場合は UnauthenticatedError。
フロント側のロール取得経路
GraphQL の Shop 型に以下のフィールドが定義されている(javascript/mdms_jp/backend/src/schema/shop/object.ts)。
| フィールド | 行 | 解決ロジック |
|---|---|---|
Shop.myShopRole: ShopMemberRole | 337-350 | findShopRole を呼ぶ。グループ管理者なら 'ADMIN'。 |
Shop.hasPermission: Boolean! | 323-335 | findShopGroupMember の結果を boolean 化。 |
フロントは currentUser.belongingShops[].myShopRole を Apollo のキャッシュから取り出して UI 制御に使う。
フロント側のレイアウトガード
/shops/[id]/* のレイアウト(javascript/mdms_jp/shop/src/app/shops/[id]/layout.tsx)は、所属チェックに加えてページキー単位のロールガード を持つ。usePathname() でページキー(/shops/[id]/<key>)を取り出し、共有 CONST SHOP_MENU_MIN_ROLE の最小ロールと自分の myShopRole を hasShopRole で比較する。
const pageKey = pathname.split('/')[3];
const minRole = pageKey ? SHOP_MENU_MIN_ROLE[pageKey] : undefined;
if (minRole && !currentUser.isSallyAdmin) {
if (!hasShopRole(belongingShop?.myShopRole ?? undefined, minRole)) {
return <div>このページにアクセスする権限がありません</div>;
}
}運営マスターは全ページ許可。CONST に無いキー(トップ等)は制約なし。共有 CONST は src/features/auth/shopMenuPermissions.ts で、サイドバーと同一ソース。
WARNING
このページガードはあくまで UX 用であり、真の関所はバックエンドの認可である。
/shopGroups/[id]/* のレイアウト(javascript/mdms_jp/shop/src/app/shopGroups/[id]/layout.tsx)は、isGroupAdminOf(src/features/auth/shopGroupAdmin.ts)でグループ管理者(運営マスターを含む)かどうかを判定し、グループ管理者でなければ「この店舗グループを管理する権限がありません」を返す。顧客詳細(/shopGroups/[id]/customers/[customerId])だけは店舗の予約画面からも開く共有画面のためガードの対象外で、顧客の個人情報はバックエンドの canAccessCustomer が守る。サイドメニュー(ShopGroupSideMenu.tsx)はロールによる出し分けをせず、アクセス制御はこのレイアウトに任せる。
サイドバーとページガードの共有 CONST
ページキー → 最小ロールの対応表 SHOP_MENU_MIN_ROLE(src/features/auth/shopMenuPermissions.ts)を唯一の定義とし、サイドバー(shopSideMenu.tsx)とページガード(layout.tsx)が同じ CONST を参照する。
// shopMenuPermissions.ts
export const SHOP_MENU_MIN_ROLE: Record<string, ShopMemberRole | undefined> = {
bookingSlots: 'STAFF', catalogs: 'STAFF', staffs: 'STAFF',
customers: 'STAFF', 'phone-blocklist': 'STAFF', coupons: 'STAFF',
rooms: 'MANAGER', 'pricing-rules': 'MANAGER', overview: 'MANAGER', members: 'MANAGER',
settings: 'ADMIN', billings: 'ADMIN',
};サイドバーは canShowMenu(path) で hasShopRole(myShopRole, SHOP_MENU_MIN_ROLE[path]) || isSallyAdmin を判定し、表に無いキーは非表示にする(判定できないときは閉じる側に倒す)。一方ページガードは、表に無いキーには制限を課さない(判定できないときは開く側に倒す)。coupons だけは事前決済が有効な店舗のみメニューに載せる条件と AND で評価する。これにより一般スタッフ(STAFF)からは rooms / pricing-rules / overview / members が非表示になる。
ページ別の必要権限
ルート・認証系
| URL | 機能 | 表示制御(フロント) | 主要操作 / 必要権限(バックエンド) | ファイル |
|---|---|---|---|---|
/ | 所属店舗一覧 | ログインのみ | (Query のみ) | app/page.tsx |
/google-auth-callback | Google OAuth コールバック | ログインのみ | OAuth リダイレクト | app/google-auth-callback/page.tsx |
/invite/[token] | 招待リンク受諾 | ログインのみ | verifyInvitationLink(認証のみ) | app/invite/[token]/page.tsx |
店舗単位(/shops/[id]/*)
凡例: ⚠️ = 課題あり(リンク先の節を参照)
| URL | 機能 | フロントの表示制御 | 主要操作(Mutation) | 必要権限 |
|---|---|---|---|---|
/shops/[id]/bookingSlots | 公演一覧 | STAFF 以上に表示 | createPublicBookingSlot / createPrivateBookingSlot / cancelBookingSlot / deleteBookingSlot / duplicateBookingSlots / publishDraftBookingSlots / restoreBookingSlot / ShopEventSlot CRUD | isShopManager ⚠️ 公演系 mutation のしきい値と運用方針のずれ |
/shops/[id]/bookingSlots/[slotId] | 公演詳細編集 | STAFF 以上に表示 | updatePublicBookingSlot / updatePrivateBookingSlot / assignBookingSlot / confirmPrivateBookingSlot / rejectPrivateBookingSlot / sendBookingSlotNotification | isShopManager |
/shops/[id]/catalogs | カタログ一覧 | STAFF 以上に表示 | createShopCatalog / deleteShopCatalog / reorderShopCatalog | isShopManager(reorder は対象から shopId を導出し、導出できなければ拒否する) |
/shops/[id]/catalogs/[shopCatalogId] | カタログ詳細編集 | STAFF 以上に表示 | updateShopCatalog / upsertShopCatalogPrice / deleteShopCatalogPrice | isShopManager |
/shops/[id]/catalogs/archived | アーカイブ済カタログ | STAFF 以上に表示 | (一覧のみ) | - |
/shops/[id]/staffs | スタッフ管理 | STAFF 以上に表示 | createShopStaff / deleteShopStaff | isShopManager |
reorderShopStaff | isShopManager(対象から shopId を導出し、導出できなければ拒否する) | |||
updateShopStaff | canManageShopStaff(MANAGER 以上は全枠 / STAFF は自分の枠のみ) | |||
/shops/[id]/staffs/[staffId] | スタッフ詳細(スキル編集) | STAFF 以上に表示 | updateShopStaff / createShopStaffCatalogSkill / deleteShopStaffCatalogSkill | canManageShopStaff |
/shops/[id]/customers | 顧客名簿 | STAFF 以上に表示 | paginatedCustomers(Query) | findShopGroupMember(任意のメンバー) |
顧客メモ編集(updateCustomerMemo) | canAccessCustomer(一般スタッフ STAFF も可) | |||
顧客情報編集(updateCustomer: 氏名・電話・メール) | canManageCustomer(MANAGER 以上。本人特例なし) | |||
/shops/[id]/members | 権限管理(店舗) | MANAGER 以上に表示 | createInvitationLink(shop) | isShopManager |
updateShopMember(スタッフ紐付けのみ / ロール据え置き) | isShopManager | |||
updateShopMember(ロール変更 / role='ADMIN') | isShopAdmin(isGroupAdmin には触れない) | |||
deleteShopGroupMember(削除ボタン) | isShopGroupAdmin(グループ管理者のみ) | |||
/shops/[id]/phone-blocklist | 電話番号ブロックリスト | STAFF 以上に表示(一覧閲覧)。登録・削除ボタンは MANAGER 以上のみ表示 | addPhoneToBlocklist / removePhoneFromBlocklist | isShopManager(= MANAGER 以上) |
/shops/[id]/pricing-rules | 価格設定ルール | MANAGER 以上に表示 | query / createShopPricingRule / updateShopPricingRule / deleteShopPricingRule / updateShopPricingRulesPriority | isShopManager(query も同じ) |
/shops/[id]/rooms | 部屋管理 | MANAGER 以上に表示 | createShopRoom / updateShopRoom / deleteShopRoom / reorderShopRooms | isShopManager |
/shops/[id]/coupons | クーポン(事前決済 ON 時のみメニュー表示) | STAFF 以上かつ事前決済 ON | query(coupon / coupons) | isShopStaff(一般スタッフを含む全メンバーが閲覧可) |
createCoupon / updateCouponDescription / updateCouponDiscount / deleteCoupon | isShopManager | |||
/shops/[id]/billings | 売上・手数料 | ADMIN のみ表示 | monthlyShopBilling の生成 | 運営マスター専用 ⚠️ 運営マスター専用の monthlyShopBilling の生成 |
/shops/[id]/overview | 店舗情報(基本情報編集) | MANAGER 以上に表示 | updateShop(handleUpdateShop) | isShopManager |
/shops/[id]/settings | その他設定(Google Calendar 連携) | ADMIN のみ表示 | updateShopHide* / generateAuthUrl / saveAuthToken / registerCalendar / disconnectCalendar / disconnectAccount / syncAllEvents | isShopAdmin |
グループ単位(/shopGroups/[id]/*)
フロントの表示制御はいずれもフロント側のレイアウトガードによる。
凡例: ⚠️ = 課題あり(リンク先の節を参照)
| URL | 機能 | フロントの表示制御 | 主要操作(Mutation) | 必要権限 |
|---|---|---|---|---|
/shopGroups/[id]/shops | グループ内店舗一覧 | グループ管理者のみ | createShopInGroup | 運営マスター専用 |
/shopGroups/[id]/customers | グループ顧客一覧 | グループ管理者のみ | (Query、Customer 型は canAccessCustomer で守られる) | - |
/shopGroups/[id]/customers/[customerId] | 顧客詳細 | ガード対象外(店舗の予約画面からも開く) | updateCustomer | canManageCustomer |
/shopGroups/[id]/overview | グループ情報 | グループ管理者のみ | updateShopGroup / 法人情報 CRUD | isShopGroupAdmin(グループ管理者) |
/shopGroups/[id]/billings | グループ売上 | グループ管理者のみ | monthlyShopBilling の生成 | 運営マスター専用 ⚠️ 運営マスター専用の monthlyShopBilling の生成 |
/shopGroups/[id]/members | 権限管理(グループ) | グループ管理者のみ | createShopGroupMember / updateShopGroupMember / deleteShopGroupMember / createInvitationLink / reorderShopGroupMember | isShopGroupAdmin(グループ管理者)。reorderShopGroupMember は input に shopGroupId が無いため対象から導出し、取得件数と ids の件数の一致・全行が同じ shopGroupId であることを確かめ、満たさなければ拒否する |
ロール別の制限
一般スタッフ(STAFF)の制限
一般スタッフは閲覧中心の最小権限。バックエンドの mutation で ForbiddenError になる主な操作は次のとおり。
- カタログ作成・編集・削除・並び替え(
isShopManager) - カタログ価格(
upsert/deleteShopCatalogPrice)(isShopManager) - スタッフ追加・削除・並び替え(
isShopManager)、他人のスタッフ枠の編集・スキル設定(canManageShopStaff。自分に紐付いた枠のみ可) - 部屋 CRUD・並び替え(
isShopManager) - 価格設定ルール CRUD・query(
isShopManager) - 店舗情報(
handleUpdateShop)(isShopManager) - 電話番号ブロックリストの登録・削除(
addPhoneToBlocklist/removePhoneFromBlocklist)(isShopManager。一覧閲覧は可) - 顧客情報編集(氏名・電話・メール)(
canManageCustomer。メモのみ編集可) - 権限管理メニュー(サイドバー非表示・ページガードで遮断)、招待リンク発行・スタッフ紐付け(
isShopManager) - 部屋・価格設定・店舗情報メニュー(サイドバー非表示・ページガードで遮断)
- その他設定・売上(
isShopAdmin/ 運営マスター)
一般スタッフが実行できる主なもの(= 上位ロールも当然可能)は次のとおり。
- 公演(
BookingSlot)系の CRUD・スタッフ割当(マトリクスは「公演 = 一般スタッフ STAFF 閲覧 + 操作可」) - 予約のキャンセル・人数変更・クーポン適用(
canAccessReservation) - クーポン閲覧(
isShopStaff)、電話ブロックリスト閲覧(登録・削除は運営責任者以上) - 顧客名簿閲覧・顧客メモ編集(
findShopGroupMember/canAccessCustomer) - 自分に紐付いたスタッフ枠の編集・スキル設定(
canManageShopStaff) - 支払い返金・同期
INFO
公演(BookingSlot)系 mutation の認可しきい値は isShopManager(= MANAGER 以上)だが、マトリクス上は一般スタッフにも公演操作を開放する方針(現場運用)。一般スタッフ(STAFF)のしきい値との整合は残課題である(公演系 mutation のしきい値と運用方針のずれ)。
運営責任者(MANAGER)の制限
運営責任者は日常運用 + 店舗構成・設定の編集まで可能。管理者との差は以下の 3 点のみ。
- その他設定(
updateShopHide*/ Google カレンダー連携) …isShopAdmin必須 - 売上・手数料ページ … 閲覧含め管理者・運営マスターのみ(サイドバー非表示)
- メンバーのロール変更・削除 …
updateShopMemberのロール変更経路はisShopAdmin、deleteShopGroupMemberはisShopGroupAdmin。運営責任者は スタッフ紐付け(ロール据え置き)のみisShopManagerで可能(updateShopMemberはロールが変わるかどうかで必要権限を分ける)
加えてグループレベルの操作(店舗グループの編集 / 法人情報 / グループメンバー CRUD)は isShopGroupAdmin 必須で運営責任者も不可。
管理者(ADMIN)の制限
ShopMember.role = 'ADMIN'(= isGroupAdmin: false)のユーザーでもできない操作は次のとおり。
ShopGroupMemberCRUD:createShopGroupMember/updateShopGroupMember/deleteShopGroupMember/reorderShopGroupMember(isShopGroupAdmin必須)- 店舗グループの編集(
updateShopGroup、isShopGroupAdmin) - 法人情報 CRUD(
isShopGroupAdmin) - グループレベルの月次集計(
monthlyShopBillingの生成は運営マスター専用)
管理者でも実行できる権限管理操作(混同しやすいため明記)は次のとおり。
- 招待リンクの発行(
createInvitationLink。店舗指定はisShopManager) - 加入済みメンバーのロール変更(
updateShopMember、isShopAdmin)。店舗でADMINにしてもその店舗の管理者になるだけで、isGroupAdminには触れない
「ShopMember.role = 'ADMIN' かつ isGroupAdmin = false」の状態は、店舗権限管理画面(/shops/[id]/members)で「管理者」に変更した場合と、グループ権限管理画面(/shopGroups/[id]/members)でグループ管理者のチェックを OFF にしたまま店舗ロールを「管理者」に設定した場合に生じる。
enum の外にある権限
- グループ管理者(
ShopGroupMember.isGroupAdmin: Boolean)— enum 上は別ロールではないが、実態としては最上位。フロントの「権限管理」画面では「グループ管理者」ラベルで表示される(features/members/members.tsx:286) - 閲覧専用 ShopGroupMember —
ShopMemberを持たずisGroupAdmin = false。招待リンクの種別ミスや旧来データで発生しうる中間状態。canViewShopGroupSensitiveDataでは false になり、顧客名簿などの個人情報は閲覧できない - 運営マスター(
ctx.requester.isAdmin)— UZU 側の運営マスター。フロントではisSallyAdminで参照され、すべての判定を通る(shopSideMenu.tsx:114など)
設計上の問題点・セキュリティリスク
公演系 mutation のしきい値と運用方針のずれ
公演系 mutation(booking_slot/* / public_booking_slot/* / private_booking_slot/* / shop_event_slot/*)は isShopManager(= MANAGER 以上)で判定する。一方、マトリクス上は一般スタッフ(STAFF)にも公演の操作を開放する方針で、公演ページ自体は一般スタッフも開ける。一般スタッフのしきい値をどう揃えるかは決まっていない。
運営マスター専用の reservationByCampaignId Query
backend/src/schema/reservation/query.ts:10-25 で ctx.requester?.isAdmin を要求。マーケティング集計用途のため意図的だが、ドキュメント上は明記が必要。
運営マスター専用の monthlyShopBilling の生成
backend/src/schema/monthly_shop_billing/mutation.ts:16:
authorize: (_parent, _args, ctx) => ctx.requester?.isAdmin ?? false,UI 上、店舗管理側の「売上・手数料」ページから呼べる生成系の操作は、グループ管理者であっても自分で実行できない。実運用では別 cron・サポート対応の想定と思われるが、ボタンを出している以上ロールを揃えるか UI から外すか整理が必要。
shop / shopByDisplayId / shopGroup 系 Query の認可
Shop型のmembers/shopGroupMembersフィールドはauthorize: isShopManagerで、権限管理(メンバー一覧 + ロール)は運営責任者以上でないと取得できない。reservationNotificationEmailはisShopAdminで判定するShop.shopEventSlotsフィールドの読み取りはisShopStaff(= その店舗のメンバーであること)。公演ページ(/shops/[id]/bookingSlots)は一般スタッフも閲覧可のため、MANAGER以上にすると一般スタッフが公演ページを開いた時にこのフィールドだけForbiddenErrorになりページ全体が表示できなくなるBookingSlot.operationRecord(公演の作成者・最終更新者)はisShopStaffで判定し、その公演を操作した人の氏名に限って一般スタッフにも返す。ShopGroupMember.displayName自体の読み取りは運営責任者以上(canManageShopGroupMemberDisplayName)で、氏名を返す経路はこの 2 つ。BookingSlotはお客様向けサイトからも取得されるため、権限がない場合はForbiddenErrorではなくnullを返す(エラーにすると公演ごと取得できなくなる)。実装はbackend/src/logic/bookingSlotOperator.ts。氏名の公開範囲を見直すときは両方の経路を確認するmonthlyShopBillingsはisShopAdminで判定する- 未対応(要レビュー):
shop/shopByDisplayIdQuery 自体やshopGroupQuery は認可指定なし(公開店舗ページのため意図的な部分もある)。catalogs/staffs/bookingSlotsの読み取りフィールドは「メンバーかどうかすら見ていない」(一般スタッフSTAFFも閲覧可という方針と方向は一致するが、最小ロールの強制はしていない) - 未対応(優先): 財務系 query の
accountingSummaries(accountancy/query.ts)は無認可で、任意ユーザーがプラットフォーム全体の売上を取得できる
残課題
- 財務系 query の無認可(優先):
accountingSummariesを認可で塞ぐ(shop/shopByDisplayId/shopGroup系 Query の認可) - 公演系 mutation のしきい値: 一般スタッフ(
STAFF)に公演操作を開放する方針とisShopManagerのずれを揃える(公演系 mutation のしきい値と運用方針のずれ) - 読み取りフィールドの網羅的な認可:
catalogs/staffs/bookingSlots等の読み取りを最小ロールで強制する(一般スタッフSTAFF= 閲覧可で方向は一致するため実害は薄い) updateShopMemberの API 直叩きによる越境: staffId の所属検証がなく、id だけを条件に更新する。通常 UI では踏まない- スタッフとアカウントの紐付け UI の改善: 紐付けは権限管理画面の編集モーダルのみ(運営責任者も使える)。スタッフページでの未紐付け警告・アカウント選択ドロップダウンは整っていない
変更履歴
| 日付 | 変更 | PR |
|---|---|---|
| 2026-06-19 | 最小権限ロール STAFF(一般スタッフ)を新設し、それまでの STAFF(運営スタッフ)を MANAGER(運営責任者)にリネーム。Menu.onlyShopAdmin を廃止し、SHOP_MENU_MIN_ROLE に一本化 | #21677 |