feat: RBAC Phase 1 — consolidate user roles into 4-value enum
Some checks failed
CI / ruff (push) Successful in 11s
CI / validate (push) Has been cancelled
CI / dependency-scanning (push) Has been cancelled
CI / docs (push) Has been cancelled
CI / deploy (push) Has been cancelled
CI / pytest (push) Has been cancelled

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:
2026-02-19 22:44:29 +01:00
parent ef21d47533
commit 1dcb0e6c33
67 changed files with 874 additions and 616 deletions

View File

@@ -86,7 +86,7 @@ function adminUserDetailPage() {
adminUserDetailLog.info(`Admin user loaded in ${duration}ms`, {
id: this.adminUser.id,
username: this.adminUser.username,
is_super_admin: this.adminUser.is_super_admin,
role: this.adminUser.role,
is_active: this.adminUser.is_active
});
adminUserDetailLog.debug('Full admin user data:', this.adminUser);

View File

@@ -101,7 +101,7 @@ function adminUserEditPage() {
adminUserEditLog.info(`Admin user loaded in ${duration}ms`, {
id: this.adminUser.id,
username: this.adminUser.username,
is_super_admin: this.adminUser.is_super_admin
role: this.adminUser.role
});
} catch (error) {
@@ -128,7 +128,7 @@ function adminUserEditPage() {
// Get available platforms (not yet assigned)
get availablePlatformsForAssignment() {
if (!this.adminUser || this.adminUser.is_super_admin) return [];
if (!this.adminUser || this.adminUser.role === 'super_admin') return [];
const assignedIds = (this.adminUser.platforms || []).map(p => p.id);
return this.platforms.filter(p => !assignedIds.includes(p.id));
},
@@ -143,12 +143,13 @@ function adminUserEditPage() {
// Toggle super admin status
async toggleSuperAdmin() {
const newStatus = !this.adminUser.is_super_admin;
const action = newStatus ? 'promote to' : 'demote from';
const isSuperAdmin = this.adminUser.role === 'super_admin';
const newRole = isSuperAdmin ? 'platform_admin' : 'super_admin';
const action = !isSuperAdmin ? 'promote to' : 'demote from';
adminUserEditLog.info(`Toggle super admin: ${action}`);
// Prevent self-demotion
if (this.adminUser.id === this.currentUserId && !newStatus) {
if (this.adminUser.id === this.currentUserId && isSuperAdmin) {
Utils.showToast(I18n.t('tenancy.messages.you_cannot_demote_yourself_from_super_ad'), 'error');
return;
}
@@ -156,19 +157,19 @@ function adminUserEditPage() {
this.saving = true;
try {
const url = `/admin/admin-users/${this.userId}/super-admin`;
window.LogConfig.logApiCall('PUT', url, { is_super_admin: newStatus }, 'request');
window.LogConfig.logApiCall('PUT', url, { role: newRole }, 'request');
const response = await apiClient.put(url, { is_super_admin: newStatus });
const response = await apiClient.put(url, { role: newRole });
window.LogConfig.logApiCall('PUT', url, response, 'response');
this.adminUser.is_super_admin = response.is_super_admin;
this.adminUser.role = response.role;
// Clear platforms if promoted to super admin
if (response.is_super_admin) {
if (response.role === 'super_admin') {
this.adminUser.platforms = [];
}
const actionDone = newStatus ? 'promoted to' : 'demoted from';
const actionDone = newRole === 'super_admin' ? 'promoted to' : 'demoted from';
Utils.showToast(`Admin ${actionDone} super admin successfully`, 'success');
adminUserEditLog.info(`Admin ${actionDone} super admin successfully`);

View File

@@ -22,7 +22,7 @@ function adminUsersPage() {
adminToDelete: null,
filters: {
search: '',
is_super_admin: '',
role: '',
is_active: ''
},
stats: {
@@ -143,7 +143,7 @@ function adminUsersPage() {
if (this.filters.search) {
params.append('search', this.filters.search);
}
if (this.filters.is_super_admin === 'false') {
if (this.filters.role === 'platform_admin') {
params.append('include_super_admins', 'false');
}
if (this.filters.is_active !== '') {
@@ -174,9 +174,11 @@ function adminUsersPage() {
);
}
// Filter by super admin status
if (this.filters.is_super_admin === 'true') {
admins = admins.filter(admin => admin.is_super_admin);
// Filter by role
if (this.filters.role === 'super_admin') {
admins = admins.filter(admin => admin.role === 'super_admin');
} else if (this.filters.role === 'platform_admin') {
admins = admins.filter(admin => admin.role === 'platform_admin');
}
// Filter by active status
@@ -227,8 +229,8 @@ function adminUsersPage() {
// Compute stats from the data
this.stats = {
total_admins: admins.length,
super_admins: admins.filter(a => a.is_super_admin).length,
platform_admins: admins.filter(a => !a.is_super_admin).length,
super_admins: admins.filter(a => a.role === 'super_admin').length,
platform_admins: admins.filter(a => a.role === 'platform_admin').length,
active_admins: admins.filter(a => a.is_active).length
};

View File

@@ -46,7 +46,7 @@ function selectPlatform() {
const response = await apiClient.get('/admin/auth/accessible-platforms');
platformLog.debug('Platforms response:', response);
this.isSuperAdmin = response.is_super_admin;
this.isSuperAdmin = response.role === 'super_admin';
this.platforms = response.platforms || [];
if (this.isSuperAdmin) {

View File

@@ -17,7 +17,7 @@ function adminUserCreate() {
password: '',
first_name: '',
last_name: '',
is_super_admin: false,
role: 'platform_admin',
platform_ids: []
},
platforms: [],
@@ -72,7 +72,7 @@ function adminUserCreate() {
}
// Platform admin validation: must have at least one platform
if (!this.formData.is_super_admin) {
if (this.formData.role !== 'super_admin') {
if (!this.formData.platform_ids || this.formData.platform_ids.length === 0) {
this.errors.platform_ids = 'Platform admins must be assigned to at least one platform';
}
@@ -103,8 +103,8 @@ function adminUserCreate() {
password: this.formData.password,
first_name: this.formData.first_name || null,
last_name: this.formData.last_name || null,
is_super_admin: this.formData.is_super_admin,
platform_ids: this.formData.is_super_admin ? [] : this.formData.platform_ids.map(id => parseInt(id))
role: this.formData.role,
platform_ids: this.formData.role === 'super_admin' ? [] : this.formData.platform_ids.map(id => parseInt(id))
};
window.LogConfig.logApiCall('POST', url, { ...payload, password: '[REDACTED]' }, 'request');
@@ -116,7 +116,7 @@ function adminUserCreate() {
window.LogConfig.logApiCall('POST', url, response, 'response');
window.LogConfig.logPerformance('Create Admin User', duration);
const userType = this.formData.is_super_admin ? 'Super admin' : 'Platform admin';
const userType = this.formData.role === 'super_admin' ? 'Super admin' : 'Platform admin';
Utils.showToast(`${userType} created successfully`, 'success');
userCreateLog.info(`${userType} created successfully in ${duration}ms`, response);