feat: RBAC Phase 1 — consolidate user roles into 4-value enum
Some checks failed
Some checks failed
Consolidate User.role (2-value: admin/store) + User.is_super_admin (boolean) into a single 4-value UserRole enum: super_admin, platform_admin, merchant_owner, store_member. Drop stale StoreUser.user_type column. Fix role="user" bug in merchant creation. Key changes: - Expand UserRole enum from 2 to 4 values with computed properties (is_admin, is_super_admin, is_platform_admin, is_merchant_owner, is_store_user) - Add Alembic migration (tenancy_003) for data migration + column drops - Remove is_super_admin from JWT token payload - Update all auth dependencies, services, routes, templates, JS, and tests - Update all RBAC documentation 66 files changed, 1219 unit tests passing. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -58,16 +58,16 @@
|
||||
<!-- Admin Type -->
|
||||
<div class="flex items-center p-4 bg-white rounded-lg shadow-xs dark:bg-gray-800">
|
||||
<div class="p-3 mr-4 rounded-full"
|
||||
:class="adminUser?.is_super_admin
|
||||
:class="adminUser?.role === 'super_admin'
|
||||
? 'text-orange-500 bg-orange-100 dark:text-orange-100 dark:bg-orange-500'
|
||||
: 'text-purple-500 bg-purple-100 dark:text-purple-100 dark:bg-purple-500'">
|
||||
<span x-html="$icon(adminUser?.is_super_admin ? 'star' : 'shield', 'w-5 h-5')"></span>
|
||||
<span x-html="$icon(adminUser?.role === 'super_admin' ? 'star' : 'shield', 'w-5 h-5')"></span>
|
||||
</div>
|
||||
<div>
|
||||
<p class="mb-2 text-sm font-medium text-gray-600 dark:text-gray-400">
|
||||
Admin Type
|
||||
Role
|
||||
</p>
|
||||
<p class="text-lg font-semibold text-gray-700 dark:text-gray-200" x-text="adminUser?.is_super_admin ? 'Super Admin' : 'Platform Admin'">
|
||||
<p class="text-lg font-semibold text-gray-700 dark:text-gray-200" x-text="adminUser?.role === 'super_admin' ? 'Super Admin' : 'Platform Admin'">
|
||||
-
|
||||
</p>
|
||||
</div>
|
||||
@@ -98,7 +98,7 @@
|
||||
<p class="mb-2 text-sm font-medium text-gray-600 dark:text-gray-400">
|
||||
Platforms
|
||||
</p>
|
||||
<p class="text-lg font-semibold text-gray-700 dark:text-gray-200" x-text="adminUser?.is_super_admin ? 'All' : ((adminUser?.platforms || []).length || 0)">
|
||||
<p class="text-lg font-semibold text-gray-700 dark:text-gray-200" x-text="adminUser?.role === 'super_admin' ? 'All' : ((adminUser?.platforms || []).length || 0)">
|
||||
0
|
||||
</p>
|
||||
</div>
|
||||
@@ -170,7 +170,7 @@
|
||||
</div>
|
||||
|
||||
<!-- Platform Access (for platform admins) -->
|
||||
<template x-if="!adminUser?.is_super_admin">
|
||||
<template x-if="adminUser?.role !== 'super_admin'">
|
||||
<div class="px-4 py-3 mb-8 bg-white rounded-lg shadow-md dark:bg-gray-800">
|
||||
<h3 class="mb-4 text-lg font-semibold text-gray-700 dark:text-gray-200">
|
||||
Platform Access
|
||||
@@ -197,7 +197,7 @@
|
||||
</template>
|
||||
|
||||
<!-- Super Admin Notice -->
|
||||
<template x-if="adminUser?.is_super_admin">
|
||||
<template x-if="adminUser?.role === 'super_admin'">
|
||||
<div class="px-4 py-3 mb-8 bg-orange-50 dark:bg-orange-900/20 rounded-lg border border-orange-200 dark:border-orange-800">
|
||||
<div class="flex items-center">
|
||||
<span x-html="$icon('star', 'w-5 h-5 text-orange-500 mr-3')"></span>
|
||||
|
||||
@@ -37,23 +37,23 @@
|
||||
<!-- Toggle Super Admin -->
|
||||
<button
|
||||
@click="showToggleSuperAdminModal = true"
|
||||
:disabled="saving || (adminUser?.id === currentUserId && adminUser?.is_super_admin)"
|
||||
:disabled="saving || (adminUser?.id === currentUserId && adminUser?.role === 'super_admin')"
|
||||
class="flex items-center px-4 py-2 text-sm font-medium leading-5 text-white transition-colors duration-150 rounded-lg focus:outline-none disabled:opacity-50"
|
||||
:class="adminUser?.is_super_admin ? 'bg-yellow-600 hover:bg-yellow-700' : 'bg-purple-600 hover:bg-purple-700'"
|
||||
:title="adminUser?.id === currentUserId && adminUser?.is_super_admin ? 'Cannot demote yourself' : ''">
|
||||
<span x-html="$icon(adminUser?.is_super_admin ? 'shield-x' : 'shield-check', 'w-4 h-4 mr-2')"></span>
|
||||
<span x-text="adminUser?.is_super_admin ? 'Demote from Super Admin' : 'Promote to Super Admin'"></span>
|
||||
:class="adminUser?.role === 'super_admin' ? 'bg-yellow-600 hover:bg-yellow-700' : 'bg-purple-600 hover:bg-purple-700'"
|
||||
:title="adminUser?.id === currentUserId && adminUser?.role === 'super_admin' ? 'Cannot demote yourself' : ''">
|
||||
<span x-html="$icon(adminUser?.role === 'super_admin' ? 'shield-x' : 'shield-check', 'w-4 h-4 mr-2')"></span>
|
||||
<span x-text="adminUser?.role === 'super_admin' ? 'Demote from Super Admin' : 'Promote to Super Admin'"></span>
|
||||
</button>
|
||||
|
||||
<!-- Status Badges -->
|
||||
<div class="ml-auto flex items-center gap-2">
|
||||
<span
|
||||
x-show="adminUser?.is_super_admin"
|
||||
x-show="adminUser?.role === 'super_admin'"
|
||||
class="inline-flex items-center px-3 py-1 text-xs font-semibold leading-tight text-yellow-700 bg-yellow-100 rounded-full dark:bg-yellow-700 dark:text-yellow-100">
|
||||
Super Admin
|
||||
</span>
|
||||
<span
|
||||
x-show="!adminUser?.is_super_admin"
|
||||
x-show="adminUser?.role !== 'super_admin'"
|
||||
class="inline-flex items-center px-3 py-1 text-xs font-semibold leading-tight text-purple-700 bg-purple-100 rounded-full dark:bg-purple-700 dark:text-purple-100">
|
||||
Platform Admin
|
||||
</span>
|
||||
@@ -107,7 +107,7 @@
|
||||
</div>
|
||||
|
||||
<!-- Platform Assignments Card (Only for Platform Admins) -->
|
||||
<template x-if="!adminUser?.is_super_admin">
|
||||
<template x-if="adminUser?.role !== 'super_admin'">
|
||||
<div class="px-4 py-3 mb-6 bg-white rounded-lg shadow-md dark:bg-gray-800">
|
||||
<div class="flex items-center justify-between mb-4">
|
||||
<h3 class="text-lg font-semibold text-gray-700 dark:text-gray-200">
|
||||
@@ -160,7 +160,7 @@
|
||||
</template>
|
||||
|
||||
<!-- Super Admin Notice -->
|
||||
<template x-if="adminUser?.is_super_admin">
|
||||
<template x-if="adminUser?.role === 'super_admin'">
|
||||
<div class="px-4 py-3 mb-6 bg-yellow-50 dark:bg-yellow-900/20 border border-yellow-200 dark:border-yellow-800 rounded-lg">
|
||||
<div class="flex items-center">
|
||||
<span x-html="$icon('shield-check', 'w-6 h-6 text-yellow-600 dark:text-yellow-400 mr-3')"></span>
|
||||
@@ -270,7 +270,7 @@
|
||||
{{ confirm_modal_dynamic(
|
||||
'toggleSuperAdminModal',
|
||||
'Toggle Super Admin',
|
||||
"'Are you sure you want to ' + (adminUser?.is_super_admin ? 'demote' : 'promote') + ' \"' + (adminUser?.username || '') + '\" ' + (adminUser?.is_super_admin ? 'from' : 'to') + ' super admin?'",
|
||||
"'Are you sure you want to ' + (adminUser?.role === 'super_admin' ? 'demote' : 'promote') + ' \"' + (adminUser?.username || '') + '\" ' + (adminUser?.role === 'super_admin' ? 'from' : 'to') + ' super admin?'",
|
||||
'toggleSuperAdmin()',
|
||||
'showToggleSuperAdminModal',
|
||||
'Confirm',
|
||||
|
||||
@@ -101,15 +101,15 @@
|
||||
|
||||
<!-- Filters -->
|
||||
<div class="flex flex-wrap gap-3">
|
||||
<!-- Admin Type Filter -->
|
||||
<!-- Admin Role Filter -->
|
||||
<select
|
||||
x-model="filters.is_super_admin"
|
||||
x-model="filters.role"
|
||||
@change="pagination.page = 1; loadAdminUsers()"
|
||||
class="px-4 py-2 text-sm text-gray-700 dark:text-gray-300 bg-white dark:bg-gray-700 border border-gray-300 dark:border-gray-600 rounded-lg focus:border-purple-400 focus:outline-none"
|
||||
>
|
||||
<option value="">All Admin Types</option>
|
||||
<option value="true">Super Admins</option>
|
||||
<option value="false">Platform Admins</option>
|
||||
<option value="">All Admin Roles</option>
|
||||
<option value="super_admin">Super Admins</option>
|
||||
<option value="platform_admin">Platform Admins</option>
|
||||
</select>
|
||||
|
||||
<!-- Status Filter -->
|
||||
@@ -139,7 +139,7 @@
|
||||
<!-- Admin Users Table -->
|
||||
<div x-show="!loading">
|
||||
{% call table_wrapper() %}
|
||||
{{ table_header(['Admin', 'Email', 'Type', 'Platforms', 'Status', 'Last Login', 'Actions']) }}
|
||||
{{ table_header(['Admin', 'Email', 'Role', 'Platforms', 'Status', 'Last Login', 'Actions']) }}
|
||||
<tbody class="bg-white divide-y dark:divide-gray-700 dark:bg-gray-800">
|
||||
<!-- Empty State -->
|
||||
<template x-if="adminUsers.length === 0">
|
||||
@@ -162,7 +162,7 @@
|
||||
<div class="flex items-center text-sm">
|
||||
<div class="relative hidden w-8 h-8 mr-3 rounded-full md:block">
|
||||
<div class="absolute inset-0 rounded-full flex items-center justify-center text-white font-semibold text-sm"
|
||||
:class="admin.is_super_admin ? 'bg-orange-500' : 'bg-purple-500'"
|
||||
:class="admin.role === 'super_admin' ? 'bg-orange-500' : 'bg-purple-500'"
|
||||
x-text="(admin.username || 'A').charAt(0).toUpperCase()">
|
||||
</div>
|
||||
</div>
|
||||
@@ -176,22 +176,22 @@
|
||||
<!-- Email -->
|
||||
<td class="px-4 py-3 text-sm" x-text="admin.email"></td>
|
||||
|
||||
<!-- Type -->
|
||||
<!-- Role -->
|
||||
<td class="px-4 py-3 text-xs">
|
||||
<span class="px-2 py-1 font-semibold leading-tight rounded-full"
|
||||
:class="admin.is_super_admin
|
||||
:class="admin.role === 'super_admin'
|
||||
? 'text-orange-700 bg-orange-100 dark:bg-orange-700 dark:text-orange-100'
|
||||
: 'text-purple-700 bg-purple-100 dark:bg-purple-700 dark:text-purple-100'"
|
||||
x-text="admin.is_super_admin ? 'Super Admin' : 'Platform Admin'">
|
||||
x-text="admin.role === 'super_admin' ? 'Super Admin' : 'Platform Admin'">
|
||||
</span>
|
||||
</td>
|
||||
|
||||
<!-- Platforms -->
|
||||
<td class="px-4 py-3 text-sm">
|
||||
<template x-if="admin.is_super_admin">
|
||||
<template x-if="admin.role === 'super_admin'">
|
||||
<span class="text-gray-500 dark:text-gray-400 italic">All platforms</span>
|
||||
</template>
|
||||
<template x-if="!admin.is_super_admin">
|
||||
<template x-if="admin.role !== 'super_admin'">
|
||||
<div class="flex flex-wrap gap-1">
|
||||
<template x-for="platform in (admin.platforms || []).slice(0, 3)" :key="platform.id">
|
||||
<span class="px-2 py-0.5 text-xs bg-gray-100 dark:bg-gray-700 rounded" x-text="platform.code"></span>
|
||||
|
||||
@@ -216,7 +216,7 @@
|
||||
<div class="flex items-center justify-between text-xs text-gray-500 dark:text-gray-400">
|
||||
<span x-text="membership.store_code"></span>
|
||||
<span class="inline-flex items-center px-2 py-0.5 rounded-full bg-gray-100 dark:bg-gray-700 text-gray-600 dark:text-gray-300"
|
||||
x-text="membership.user_type"></span>
|
||||
x-text="membership.is_owner ? 'Owner' : 'Member'"></span>
|
||||
</div>
|
||||
</a>
|
||||
</template>
|
||||
|
||||
@@ -78,24 +78,26 @@
|
||||
<div class="mt-4 p-4 bg-purple-50 dark:bg-purple-900/20 rounded-lg border border-purple-200 dark:border-purple-800">
|
||||
<h4 class="text-sm font-medium text-purple-800 dark:text-purple-300 mb-3">Admin Settings</h4>
|
||||
|
||||
<!-- Super Admin Toggle -->
|
||||
<label class="flex items-center mb-4">
|
||||
<input
|
||||
type="checkbox"
|
||||
x-model="formData.is_super_admin"
|
||||
:disabled="saving"
|
||||
class="w-4 h-4 text-purple-600 border-gray-300 rounded focus:ring-purple-500 dark:border-gray-600 dark:bg-gray-700"
|
||||
>
|
||||
<span class="ml-2 text-sm text-gray-700 dark:text-gray-300">
|
||||
Super Admin
|
||||
<!-- Admin Role Selection -->
|
||||
<label class="block mb-4 text-sm">
|
||||
<span class="text-gray-700 dark:text-gray-400">
|
||||
Admin Role <span class="text-red-600">*</span>
|
||||
</span>
|
||||
<select
|
||||
x-model="formData.role"
|
||||
:disabled="saving"
|
||||
class="block w-full mt-1 text-sm dark:text-gray-300 dark:border-gray-600 dark:bg-gray-700 focus:border-purple-400 focus:outline-none focus:shadow-outline-purple form-select"
|
||||
>
|
||||
<option value="platform_admin">Platform Admin</option>
|
||||
<option value="super_admin">Super Admin</option>
|
||||
</select>
|
||||
</label>
|
||||
<p class="text-xs text-gray-600 dark:text-gray-400 mb-4 -mt-2 ml-6">
|
||||
<p class="text-xs text-gray-600 dark:text-gray-400 mb-4 -mt-2">
|
||||
Super admins have access to all platforms and can manage other admins.
|
||||
</p>
|
||||
|
||||
<!-- Platform Assignment (only if not super admin) -->
|
||||
<template x-if="!formData.is_super_admin">
|
||||
<template x-if="formData.role !== 'super_admin'">
|
||||
<div>
|
||||
<label class="block text-sm">
|
||||
<span class="text-gray-700 dark:text-gray-400">
|
||||
|
||||
Reference in New Issue
Block a user