From 1dcb0e6c332a3caf801d7541d7c056bca6fb5ec4 Mon Sep 17 00:00:00 2001 From: Samir Boulahtit Date: Thu, 19 Feb 2026 22:44:29 +0100 Subject: [PATCH] =?UTF-8?q?feat:=20RBAC=20Phase=201=20=E2=80=94=20consolid?= =?UTF-8?q?ate=20user=20roles=20into=204-value=20enum?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- app/api/deps.py | 22 +- .../analytics/services/stats_service.py | 2 +- .../tests/integration/test_admin_routes.py | 2 +- .../tests/integration/test_merchant_routes.py | 4 +- .../tests/integration/test_store_routes.py | 7 +- .../core/static/admin/js/init-alpine.js | 4 +- app/modules/core/static/admin/js/login.js | 7 +- .../marketplace_import_job_service.py | 4 +- .../services/platform_signup_service.py | 13 +- app/modules/messaging/models/message.py | 4 +- ...ancy_003_rbac_cleanup_consolidate_roles.py | 134 +++++++++++ app/modules/tenancy/models/__init__.py | 3 +- app/modules/tenancy/models/admin_platform.py | 4 +- app/modules/tenancy/models/store.py | 45 +--- app/modules/tenancy/models/user.py | 74 +++--- app/modules/tenancy/routes/api/admin_auth.py | 2 +- .../routes/api/admin_platform_users.py | 4 +- app/modules/tenancy/routes/api/admin_users.py | 24 +- app/modules/tenancy/routes/api/store_auth.py | 2 +- app/modules/tenancy/schemas/team.py | 1 - .../services/admin_platform_service.py | 29 ++- app/modules/tenancy/services/admin_service.py | 4 +- .../tenancy/services/merchant_service.py | 2 +- app/modules/tenancy/services/store_service.py | 10 +- .../tenancy/services/store_team_service.py | 6 +- .../tenancy/services/tenancy_metrics.py | 4 +- .../static/admin/js/admin-user-detail.js | 2 +- .../static/admin/js/admin-user-edit.js | 21 +- .../tenancy/static/admin/js/admin-users.js | 16 +- .../static/admin/js/select-platform.js | 2 +- .../tenancy/static/admin/js/user-create.js | 10 +- .../tenancy/admin/admin-user-detail.html | 14 +- .../tenancy/admin/admin-user-edit.html | 20 +- .../templates/tenancy/admin/admin-users.html | 24 +- .../tenancy/admin/merchant-user-detail.html | 2 +- .../templates/tenancy/admin/user-create.html | 26 +- .../test_email_verification_routes.py | 4 +- .../integration/test_merchant_auth_routes.py | 6 +- .../test_merchant_password_reset.py | 4 +- .../tests/integration/test_merchant_routes.py | 4 +- .../integration/test_store_password_reset.py | 4 +- .../tests/unit/test_admin_platform_model.py | 15 +- .../tests/unit/test_admin_platform_service.py | 10 +- .../unit/test_email_verification_token.py | 2 +- .../tests/unit/test_store_team_service.py | 10 +- .../tests/unit/test_user_auth_service.py | 8 +- .../tenancy/tests/unit/test_user_model.py | 6 +- .../unit/test_user_password_reset_token.py | 2 +- app/templates/admin/partials/header.html | 2 +- docs/api/rbac-visual-guide.md | 75 ++++-- docs/api/rbac.md | 225 +++++++++++------- docs/architecture/auth-rbac.md | 159 ++++++++----- docs/architecture/user-context-pattern.md | 51 ++-- docs/backend/rbac-quick-reference.md | 37 ++- docs/backend/store-rbac.md | 66 +++-- middleware/auth.py | 35 +-- models/schema/auth.py | 32 +-- scripts/seed/seed_demo.py | 33 +-- static/shared/js/api-client.js | 6 +- tests/fixtures/admin_platform_fixtures.py | 3 +- tests/fixtures/auth_fixtures.py | 18 +- tests/fixtures/store_fixtures.py | 5 +- .../api/v1/admin/test_admin_users.py | 15 +- .../api/v1/store/test_dashboard.py | 5 +- tests/unit/api/test_deps.py | 18 +- tests/unit/middleware/test_auth.py | 54 +++-- tests/unit/models/schema/test_auth.py | 22 +- 67 files changed, 874 insertions(+), 616 deletions(-) create mode 100644 app/modules/tenancy/migrations/versions/tenancy_003_rbac_cleanup_consolidate_roles.py diff --git a/app/api/deps.py b/app/api/deps.py index 271db1ba..5c0a6a86 100644 --- a/app/api/deps.py +++ b/app/api/deps.py @@ -200,7 +200,7 @@ def get_current_admin_from_cookie_or_header( user = _validate_user_token(token, db) # Verify user is admin - if user.role != "admin": + if not user.is_admin: logger.warning( f"Non-admin user {user.username} attempted admin route: {request.url.path}" ) @@ -235,7 +235,7 @@ def get_current_admin_api( user = _validate_user_token(credentials.credentials, db) - if user.role != "admin": + if not user.is_admin: logger.warning(f"Non-admin user {user.username} attempted admin API") raise AdminRequiredException("Admin privileges required") @@ -399,7 +399,7 @@ def get_admin_with_platform_context( user = _validate_user_token(token, db) - if user.role != "admin": + if not user.is_admin: raise AdminRequiredException("Admin privileges required") # Super admins bypass platform context @@ -702,7 +702,7 @@ def get_current_store_from_cookie_or_header( user = _validate_user_token(token, db) # CRITICAL: Block admins from store routes - if user.role == "admin": + if user.is_admin: logger.warning( f"Admin user {user.username} attempted store route: {request.url.path}" ) @@ -710,8 +710,8 @@ def get_current_store_from_cookie_or_header( "Store access only - admins cannot use store portal" ) - # Verify user is store - if user.role != "store": + # Verify user is store user (merchant_owner or store_member) + if not user.is_store_user: logger.warning( f"Non-store user {user.username} attempted store route: {request.url.path}" ) @@ -749,11 +749,11 @@ def get_current_store_api( user = _validate_user_token(credentials.credentials, db) # Block admins from store API - if user.role == "admin": + if user.is_admin: logger.warning(f"Admin user {user.username} attempted store API") raise InsufficientPermissionsException("Store access only") - if user.role != "store": + if not user.is_store_user: logger.warning(f"Non-store user {user.username} attempted store API") raise InsufficientPermissionsException("Store privileges required") @@ -1570,7 +1570,7 @@ def get_current_admin_optional( user = _validate_user_token(token, db) # Verify user is admin - if user.role == "admin": + if user.is_admin: return UserContext.from_user(user, include_store_context=False) except Exception: # Invalid token or other error @@ -1616,8 +1616,8 @@ def get_current_store_optional( # Validate token and get user user = _validate_user_token(token, db) - # Verify user is store - if user.role == "store": + # Verify user is a store user + if user.is_store_user: return UserContext.from_user(user) except Exception: # Invalid token or other error diff --git a/app/modules/analytics/services/stats_service.py b/app/modules/analytics/services/stats_service.py index 7201ef3c..d753dcba 100644 --- a/app/modules/analytics/services/stats_service.py +++ b/app/modules/analytics/services/stats_service.py @@ -420,7 +420,7 @@ class StatsService: total_users = db.query(User).count() active_users = db.query(User).filter(User.is_active == True).count() inactive_users = total_users - active_users - admin_users = db.query(User).filter(User.role == "admin").count() + admin_users = db.query(User).filter(User.role.in_(["super_admin", "platform_admin"])).count() return { "total_users": total_users, diff --git a/app/modules/billing/tests/integration/test_admin_routes.py b/app/modules/billing/tests/integration/test_admin_routes.py index 186e478c..71a81ed6 100644 --- a/app/modules/billing/tests/integration/test_admin_routes.py +++ b/app/modules/billing/tests/integration/test_admin_routes.py @@ -80,7 +80,7 @@ def rt_merchant(db, rt_platform): email=f"merchant_{uuid.uuid4().hex[:8]}@test.com", username=f"merchant_{uuid.uuid4().hex[:8]}", hashed_password=auth.hash_password("pass123"), - role="store", + role="merchant_owner", is_active=True, ) db.add(owner) diff --git a/app/modules/billing/tests/integration/test_merchant_routes.py b/app/modules/billing/tests/integration/test_merchant_routes.py index 018cc234..8b29ca87 100644 --- a/app/modules/billing/tests/integration/test_merchant_routes.py +++ b/app/modules/billing/tests/integration/test_merchant_routes.py @@ -43,7 +43,7 @@ def merch_owner(db): email=f"merchowner_{uuid.uuid4().hex[:8]}@test.com", username=f"merchowner_{uuid.uuid4().hex[:8]}", hashed_password=auth.hash_password("merchpass123"), - role="store", + role="merchant_owner", is_active=True, ) db.add(user) @@ -160,7 +160,7 @@ def merch_auth_headers(merch_owner, merch_merchant): id=merch_owner.id, email=merch_owner.email, username=merch_owner.username, - role="store", + role="merchant_owner", is_active=True, ) diff --git a/app/modules/billing/tests/integration/test_store_routes.py b/app/modules/billing/tests/integration/test_store_routes.py index af5c5ac8..86cb5df1 100644 --- a/app/modules/billing/tests/integration/test_store_routes.py +++ b/app/modules/billing/tests/integration/test_store_routes.py @@ -24,7 +24,7 @@ from app.modules.billing.models import ( SubscriptionTier, ) from app.modules.tenancy.models import Merchant, Platform, Store, User -from app.modules.tenancy.models.store import StoreUser, StoreUserType +from app.modules.tenancy.models.store import StoreUser from app.modules.tenancy.models.store_platform import StorePlatform # ============================================================================ @@ -65,7 +65,7 @@ def store_full_setup(db, store_platform): email=f"storeowner_{uid}@test.com", username=f"storeowner_{uid}", hashed_password=auth.hash_password("storepass123"), - role="store", + role="merchant_owner", is_active=True, is_email_verified=True, ) @@ -98,11 +98,10 @@ def store_full_setup(db, store_platform): db.commit() db.refresh(store) - # 4. Create StoreUser (owner association) + # 4. Create StoreUser (ownership determined via Merchant.owner_user_id) store_user = StoreUser( store_id=store.id, user_id=owner.id, - user_type=StoreUserType.OWNER.value, is_active=True, ) db.add(store_user) diff --git a/app/modules/core/static/admin/js/init-alpine.js b/app/modules/core/static/admin/js/init-alpine.js index 226c9bf1..2e774abc 100644 --- a/app/modules/core/static/admin/js/init-alpine.js +++ b/app/modules/core/static/admin/js/init-alpine.js @@ -175,7 +175,7 @@ function data() { adminProfile: getAdminProfileFromStorage(), get isSuperAdmin() { - return this.adminProfile?.is_super_admin === true; + return this.adminProfile?.role === 'super_admin'; }, // ───────────────────────────────────────────────────────────────── @@ -398,4 +398,4 @@ const PlatformSettings = { }; // Export to window -window.PlatformSettings = PlatformSettings; \ No newline at end of file +window.PlatformSettings = PlatformSettings; diff --git a/app/modules/core/static/admin/js/login.js b/app/modules/core/static/admin/js/login.js index 58ed5516..f46edda7 100644 --- a/app/modules/core/static/admin/js/login.js +++ b/app/modules/core/static/admin/js/login.js @@ -147,7 +147,7 @@ function adminLogin() { throw new Error('Invalid response from server - no token'); } - if (response.user && response.user.role !== 'admin') { + if (response.user && !['super_admin', 'platform_admin'].includes(response.user.role)) { loginLog.error('Authorization failed: User is not admin', { actualRole: response.user.role }); @@ -166,8 +166,7 @@ function adminLogin() { loginLog.debug('User data stored:', { username: response.user.username, role: response.user.role, - id: response.user.id, - is_super_admin: response.user.is_super_admin + id: response.user.id }); } @@ -243,4 +242,4 @@ function adminLogin() { } } -loginLog.info('Login module loaded'); \ No newline at end of file +loginLog.info('Login module loaded'); diff --git a/app/modules/marketplace/services/marketplace_import_job_service.py b/app/modules/marketplace/services/marketplace_import_job_service.py index 06a1a1ab..83887bd3 100644 --- a/app/modules/marketplace/services/marketplace_import_job_service.py +++ b/app/modules/marketplace/services/marketplace_import_job_service.py @@ -87,7 +87,7 @@ class MarketplaceImportJobService: raise ImportJobNotFoundException(job_id) # Users can only see their own jobs, admins can see all - if user.role != "admin" and job.user_id != user.id: + if not user.is_admin and job.user_id != user.id: raise ImportJobNotOwnedException(job_id, user.id) return job @@ -160,7 +160,7 @@ class MarketplaceImportJobService: ) # Users can only see their own jobs, admins can see all store jobs - if user.role != "admin": + if not user.is_admin: query = query.filter(MarketplaceImportJob.user_id == user.id) # Apply marketplace filter diff --git a/app/modules/marketplace/services/platform_signup_service.py b/app/modules/marketplace/services/platform_signup_service.py index 63b306ac..53247e3b 100644 --- a/app/modules/marketplace/services/platform_signup_service.py +++ b/app/modules/marketplace/services/platform_signup_service.py @@ -38,8 +38,6 @@ from app.modules.tenancy.models import ( Platform, Store, StorePlatform, - StoreUser, - StoreUserType, User, ) from middleware.auth import AuthManager @@ -338,7 +336,7 @@ class PlatformSignupService: hashed_password=self.auth_manager.hash_password(password), first_name=first_name, last_name=last_name, - role="store", + role="merchant_owner", is_active=True, ) db.add(user) @@ -373,14 +371,7 @@ class PlatformSignupService: db.add(store) db.flush() - # Create StoreUser (owner) - store_user = StoreUser( - store_id=store.id, - user_id=user.id, - user_type=StoreUserType.OWNER.value, - is_active=True, - ) - db.add(store_user) + # Owner relationship is via Merchant.owner_user_id — no StoreUser needed # Create StoreOnboarding record onboarding_service = OnboardingService(db) diff --git a/app/modules/messaging/models/message.py b/app/modules/messaging/models/message.py index c21408a5..bb2d7a96 100644 --- a/app/modules/messaging/models/message.py +++ b/app/modules/messaging/models/message.py @@ -42,8 +42,8 @@ class ConversationType(str, enum.Enum): class ParticipantType(str, enum.Enum): """Type of participant in a conversation.""" - ADMIN = "admin" # User with role="admin" - STORE = "store" # User with role="store" (via StoreUser) + ADMIN = "admin" # Platform admin user (super_admin or platform_admin) + STORE = "store" # Store team user (merchant_owner or store_member) CUSTOMER = "customer" # Customer model diff --git a/app/modules/tenancy/migrations/versions/tenancy_003_rbac_cleanup_consolidate_roles.py b/app/modules/tenancy/migrations/versions/tenancy_003_rbac_cleanup_consolidate_roles.py new file mode 100644 index 00000000..7819db2f --- /dev/null +++ b/app/modules/tenancy/migrations/versions/tenancy_003_rbac_cleanup_consolidate_roles.py @@ -0,0 +1,134 @@ +"""tenancy: RBAC cleanup — consolidate user roles and drop legacy columns + +Migrates users.role from old 3-value scheme (admin/store/user) to new +4-value scheme (super_admin/platform_admin/merchant_owner/store_member). +Drops the now-redundant users.is_super_admin and store_users.user_type columns. + +Revision ID: tenancy_003 +Revises: tenancy_002 +Create Date: 2026-02-19 +""" +import sqlalchemy as sa + +from alembic import op + +revision = "tenancy_003" +down_revision = "tenancy_002" +branch_labels = None +depends_on = None + + +def upgrade() -> None: + # ------------------------------------------------------------------ + # Step 1: Data migration — remap role values using is_super_admin + # and merchant ownership to determine the new role. + # + # Order matters: the 'admin' rows must be split before we touch + # 'store' or 'user' rows, and 'merchant_owner' for role='user' + # (bug fix) must come before the catch-all 'store_member' update. + # ------------------------------------------------------------------ + + # admin + is_super_admin=true -> super_admin + op.execute( + "UPDATE users SET role = 'super_admin' " + "WHERE role = 'admin' AND is_super_admin = true" + ) + + # admin + is_super_admin=false -> platform_admin + op.execute( + "UPDATE users SET role = 'platform_admin' " + "WHERE role = 'admin' AND is_super_admin = false" + ) + + # store users who own a merchant -> merchant_owner + op.execute( + "UPDATE users SET role = 'merchant_owner' " + "WHERE role = 'store' " + "AND id IN (SELECT owner_user_id FROM merchants)" + ) + + # legacy 'user' role (bug — should have been 'store') -> merchant_owner + op.execute( + "UPDATE users SET role = 'merchant_owner' " + "WHERE role = 'user'" + ) + + # remaining store users (not merchant owners) -> store_member + op.execute( + "UPDATE users SET role = 'store_member' " + "WHERE role = 'store' " + "AND id NOT IN (SELECT owner_user_id FROM merchants)" + ) + + # ------------------------------------------------------------------ + # Step 2: Drop legacy columns that are now redundant + # ------------------------------------------------------------------ + + # users.is_super_admin is replaced by role = 'super_admin' + op.drop_column("users", "is_super_admin") + + # store_users.user_type is replaced by users.role (merchant_owner vs store_member) + op.drop_column("store_users", "user_type") + + +def downgrade() -> None: + # ------------------------------------------------------------------ + # Step 1: Re-add dropped columns + # ------------------------------------------------------------------ + + op.add_column( + "users", + sa.Column( + "is_super_admin", + sa.Boolean(), + nullable=False, + server_default="false", + ), + ) + + op.add_column( + "store_users", + sa.Column( + "user_type", + sa.String(), + nullable=False, + server_default="member", + ), + ) + + # ------------------------------------------------------------------ + # Step 2: Reverse data migration — map new roles back to old scheme + # ------------------------------------------------------------------ + + # Mark super admins + op.execute( + "UPDATE users SET is_super_admin = true " + "WHERE role = 'super_admin'" + ) + + # super_admin + platform_admin -> admin + op.execute( + "UPDATE users SET role = 'admin' " + "WHERE role IN ('super_admin', 'platform_admin')" + ) + + # merchant_owner + store_member -> store + op.execute( + "UPDATE users SET role = 'store' " + "WHERE role IN ('merchant_owner', 'store_member')" + ) + + # ------------------------------------------------------------------ + # Step 3: Restore store_users.user_type based on merchant ownership + # + # If the user owns the merchant that owns the store, they are + # an 'owner'; otherwise 'member'. + # ------------------------------------------------------------------ + op.execute( + "UPDATE store_users SET user_type = 'owner' " + "WHERE user_id IN (" + " SELECT m.owner_user_id FROM merchants m" + " JOIN stores s ON s.merchant_id = m.id" + " WHERE s.id = store_users.store_id" + ")" + ) diff --git a/app/modules/tenancy/models/__init__.py b/app/modules/tenancy/models/__init__.py index c3c5b2d9..7fada486 100644 --- a/app/modules/tenancy/models/__init__.py +++ b/app/modules/tenancy/models/__init__.py @@ -33,7 +33,7 @@ from app.modules.tenancy.models.merchant import Merchant from app.modules.tenancy.models.merchant_domain import MerchantDomain from app.modules.tenancy.models.platform import Platform from app.modules.tenancy.models.platform_module import PlatformModule -from app.modules.tenancy.models.store import Role, Store, StoreUser, StoreUserType +from app.modules.tenancy.models.store import Role, Store, StoreUser from app.modules.tenancy.models.store_domain import StoreDomain from app.modules.tenancy.models.store_platform import StorePlatform from app.modules.tenancy.models.user import User, UserRole @@ -62,7 +62,6 @@ __all__ = [ # Store "Store", "StoreUser", - "StoreUserType", "Role", # Merchant configuration "MerchantDomain", diff --git a/app/modules/tenancy/models/admin_platform.py b/app/modules/tenancy/models/admin_platform.py index 77144a46..607c09e3 100644 --- a/app/modules/tenancy/models/admin_platform.py +++ b/app/modules/tenancy/models/admin_platform.py @@ -3,8 +3,8 @@ AdminPlatform junction table for many-to-many relationship between Admin Users and Platforms. This enables platform-scoped admin access: -- Super Admins: Have is_super_admin=True on User model, bypass this table -- Platform Admins: Assigned to specific platforms via this junction table +- Super Admins: Have role='super_admin' on User model, bypass this table +- Platform Admins: Have role='platform_admin', assigned to specific platforms via this junction table A platform admin CAN be assigned to multiple platforms (e.g., both OMS and Loyalty). """ diff --git a/app/modules/tenancy/models/store.py b/app/modules/tenancy/models/store.py index 10e82a79..4f477c14 100644 --- a/app/modules/tenancy/models/store.py +++ b/app/modules/tenancy/models/store.py @@ -8,8 +8,6 @@ other models such as User (owner), Product, Customer, and Order. Note: MarketplaceImportJob relationships are owned by the marketplace module. """ -import enum - from sqlalchemy import ( JSON, Boolean, @@ -420,19 +418,14 @@ class Store(Base, TimestampMixin): } -class StoreUserType(str, enum.Enum): - """Types of store users.""" - - OWNER = "owner" # Store owner (full access to store area) - TEAM_MEMBER = "member" # Team member (role-based access to store area) - - class StoreUser(Base, TimestampMixin): """ - Represents a user's membership in a store. + Represents a user's team membership in a store. - - Owner: Created automatically when store is created - - Team Member: Invited by owner via email + Ownership is determined via User.is_owner_of(store_id) which checks + Merchant.owner_user_id, NOT a field on this table. + + This table is for team members only (invited by owner). """ __tablename__ = "store_users" @@ -446,10 +439,7 @@ class StoreUser(Base, TimestampMixin): user_id = Column(Integer, ForeignKey("users.id"), nullable=False) """Foreign key linking to the associated User.""" - # Distinguish between owner and team member - user_type = Column(String, nullable=False, default=StoreUserType.TEAM_MEMBER.value) - - # Role for team members (NULL for owners - they have all permissions) + # Role for team members (determines granular permissions) role_id = Column(Integer, ForeignKey("roles.id"), nullable=True) """Foreign key linking to the associated Role.""" @@ -480,22 +470,14 @@ class StoreUser(Base, TimestampMixin): """Relationship to the Role model, representing the role held by the store user.""" def __repr__(self) -> str: - """Return a string representation of the StoreUser instance. - - Returns: - str: A string that includes the store_id, the user_id and the user_type of the StoreUser instance. - """ - return f"" + """Return a string representation of the StoreUser instance.""" + role_name = self.role.name if self.role else "no-role" + return f"" @property def is_owner(self) -> bool: - """Check if this is an owner membership.""" - return self.user_type == StoreUserType.OWNER.value - - @property - def is_team_member(self) -> bool: - """Check if this is a team member (not owner).""" - return self.user_type == StoreUserType.TEAM_MEMBER.value + """Check if this user is the owner of the store (via merchant ownership).""" + return self.user.is_owner_of(self.store_id) @property def is_invitation_pending(self) -> bool: @@ -506,7 +488,7 @@ class StoreUser(Base, TimestampMixin): """ Check if user has a specific permission. - Owners always have all permissions. + Owners (merchant owners) always have all permissions. Team members check their role's permissions. """ # Owners have all permissions @@ -526,7 +508,6 @@ class StoreUser(Base, TimestampMixin): def get_all_permissions(self) -> list: """Get all permissions this user has.""" if self.is_owner: - # Return all possible permissions from discovery service from app.modules.tenancy.services.permission_discovery_service import ( permission_discovery_service, ) @@ -571,4 +552,4 @@ class Role(Base, TimestampMixin): return f"" -__all__ = ["Store", "StoreUser", "StoreUserType", "Role"] +__all__ = ["Store", "StoreUser", "Role"] diff --git a/app/modules/tenancy/models/user.py b/app/modules/tenancy/models/user.py index 38f38288..7504ee93 100644 --- a/app/modules/tenancy/models/user.py +++ b/app/modules/tenancy/models/user.py @@ -2,13 +2,15 @@ """ User model with authentication support. -ROLE CLARIFICATION: -- User.role should ONLY contain platform-level roles: - * "admin" - Platform administrator (full system access) - * "store" - Any user who owns or is part of a store team +ROLE SYSTEM (Phase 1 — Consolidated 4-value enum): +- User.role contains one of 4 values: + * "super_admin" — Platform super administrator (all platforms, all settings) + * "platform_admin" — Platform admin scoped to assigned platforms + * "merchant_owner" — Owns merchant(s) and all their stores + * "store_member" — Team member on specific store(s) -- Store-specific roles (manager, staff, etc.) are stored in StoreUser.role -- Customers are NOT in the User table - they use the Customer model +- Store-specific granular permissions are in StoreUser.role_id -> Role.permissions +- Customers are NOT in the User table — they use the Customer model """ import enum @@ -23,12 +25,14 @@ from models.database.base import TimestampMixin class UserRole(str, enum.Enum): """Platform-level user roles.""" - ADMIN = "admin" # Platform administrator - STORE = "store" # Store owner or team member + SUPER_ADMIN = "super_admin" # Platform super administrator + PLATFORM_ADMIN = "platform_admin" # Platform admin (scoped to assigned platforms) + MERCHANT_OWNER = "merchant_owner" # Owns merchant(s) and all their stores + STORE_MEMBER = "store_member" # Team member on specific store(s) class User(Base, TimestampMixin): - """Represents a platform user (admins and stores only).""" + """Represents a platform user (admins, merchant owners, and store team).""" __tablename__ = "users" @@ -39,18 +43,13 @@ class User(Base, TimestampMixin): last_name = Column(String) hashed_password = Column(String, nullable=False) - # Platform-level role only (admin or store) - role = Column(String, nullable=False, default=UserRole.STORE.value) + # Consolidated role — one of: super_admin, platform_admin, merchant_owner, store_member + role = Column(String, nullable=False, default=UserRole.STORE_MEMBER.value) is_active = Column(Boolean, default=True, nullable=False) is_email_verified = Column(Boolean, default=False, nullable=False) last_login = Column(DateTime, nullable=True) - # Super admin flag (only meaningful when role='admin') - # Super admins have access to ALL platforms and global settings - # Platform admins (is_super_admin=False) are assigned to specific platforms - is_super_admin = Column(Boolean, default=False, nullable=False) - # Language preference (NULL = use context default: store dashboard_language or system default) # Supported: en, fr, de, lb preferred_language = Column(String(5), nullable=True) @@ -92,15 +91,34 @@ class User(Base, TimestampMixin): return f"{self.first_name} {self.last_name}" return self.username - @property - def is_admin(self) -> bool: - """Check if user is a platform admin.""" - return self.role == UserRole.ADMIN.value + # ========================================================================= + # Role-checking properties + # ========================================================================= @property - def is_store(self) -> bool: - """Check if user is a store (owner or team member).""" - return self.role == UserRole.STORE.value + def is_super_admin(self) -> bool: + """Check if user is a super admin.""" + return self.role == UserRole.SUPER_ADMIN.value + + @property + def is_admin(self) -> bool: + """Check if user is any kind of platform admin (super or scoped).""" + return self.role in (UserRole.SUPER_ADMIN.value, UserRole.PLATFORM_ADMIN.value) + + @property + def is_platform_admin(self) -> bool: + """Check if user is a scoped platform admin (not super admin).""" + return self.role == UserRole.PLATFORM_ADMIN.value + + @property + def is_merchant_owner(self) -> bool: + """Check if user is a merchant owner.""" + return self.role == UserRole.MERCHANT_OWNER.value + + @property + def is_store_user(self) -> bool: + """Check if user is a merchant owner or store member.""" + return self.role in (UserRole.MERCHANT_OWNER.value, UserRole.STORE_MEMBER.value) def is_owner_of(self, store_id: int) -> bool: """ @@ -155,16 +173,6 @@ class User(Base, TimestampMixin): # Admin Platform Access Methods # ========================================================================= - @property - def is_super_admin_user(self) -> bool: - """Check if user is a super admin (can access all platforms).""" - return self.role == UserRole.ADMIN.value and self.is_super_admin - - @property - def is_platform_admin(self) -> bool: - """Check if user is a platform admin (access to assigned platforms only).""" - return self.role == UserRole.ADMIN.value and not self.is_super_admin - def can_access_platform(self, platform_id: int) -> bool: """ Check if admin can access a specific platform. diff --git a/app/modules/tenancy/routes/api/admin_auth.py b/app/modules/tenancy/routes/api/admin_auth.py index 3cbe64d6..b09cb13e 100644 --- a/app/modules/tenancy/routes/api/admin_auth.py +++ b/app/modules/tenancy/routes/api/admin_auth.py @@ -57,7 +57,7 @@ def admin_login( login_result = auth_service.login_user(db=db, user_credentials=user_credentials) # Verify user is admin - if login_result["user"].role != "admin": + if not login_result["user"].is_admin: logger.warning( f"Non-admin user attempted admin login: {user_credentials.email_or_username}" ) diff --git a/app/modules/tenancy/routes/api/admin_platform_users.py b/app/modules/tenancy/routes/api/admin_platform_users.py index 428ad699..8d7b499c 100644 --- a/app/modules/tenancy/routes/api/admin_platform_users.py +++ b/app/modules/tenancy/routes/api/admin_platform_users.py @@ -92,7 +92,7 @@ def get_all_users( store_id=su.store_id, store_code=su.store.store_code, store_name=su.store.name, - user_type=su.user_type, + role=su.user.role, is_active=su.is_active, ) for su in (user.store_memberships or []) @@ -299,7 +299,7 @@ def get_user_details( store_id=su.store_id, store_code=su.store.store_code, store_name=su.store.name, - user_type=su.user_type, + role=su.user.role, is_active=su.is_active, ) for su in (user.store_memberships or []) diff --git a/app/modules/tenancy/routes/api/admin_users.py b/app/modules/tenancy/routes/api/admin_users.py index d7a520bd..aa41cc68 100644 --- a/app/modules/tenancy/routes/api/admin_users.py +++ b/app/modules/tenancy/routes/api/admin_users.py @@ -57,7 +57,7 @@ class AdminUserResponse(BaseModel): first_name: str | None = None last_name: str | None = None is_active: bool - is_super_admin: bool + role: str platform_assignments: list[PlatformAssignmentResponse] = [] created_at: datetime updated_at: datetime @@ -82,7 +82,7 @@ class CreateAdminUserRequest(BaseModel): password: str first_name: str | None = None last_name: str | None = None - is_super_admin: bool = False + role: str = "platform_admin" platform_ids: list[int] = [] @@ -93,9 +93,9 @@ class AssignPlatformRequest(BaseModel): class ToggleSuperAdminRequest(BaseModel): - """Request to toggle super admin status.""" + """Request to change admin role (super_admin or platform_admin).""" - is_super_admin: bool + role: str # "super_admin" or "platform_admin" # ============================================================================ @@ -125,7 +125,7 @@ def _build_admin_response(admin: User) -> AdminUserResponse: first_name=admin.first_name, last_name=admin.last_name, is_active=admin.is_active, - is_super_admin=admin.is_super_admin, + role=admin.role, platform_assignments=assignments, created_at=admin.created_at, updated_at=admin.updated_at, @@ -174,14 +174,14 @@ def create_admin_user( Super admin only. """ - # Validate platform_ids required for non-super admin - if not request.is_super_admin and not request.platform_ids: + # Validate platform_ids required for platform admins + if request.role != "super_admin" and not request.platform_ids: raise ValidationException( "Platform admins must be assigned to at least one platform", field="platform_ids", ) - if request.is_super_admin: + if request.role == "super_admin": # Create super admin using service user = admin_platform_service.create_super_admin( db=db, @@ -202,7 +202,7 @@ def create_admin_user( first_name=user.first_name, last_name=user.last_name, is_active=user.is_active, - is_super_admin=user.is_super_admin, + role=user.role, platform_assignments=[], ) # Create platform admin with assignments using service @@ -306,17 +306,17 @@ def toggle_super_admin_status( user = admin_platform_service.toggle_super_admin( db=db, user_id=user_id, - is_super_admin=request.is_super_admin, + is_super_admin=(request.role == "super_admin"), current_admin_id=current_admin.id, ) db.commit() - action = "promoted to" if request.is_super_admin else "demoted from" + action = "promoted to" if request.role == "super_admin" else "demoted from" return { "message": f"Admin {action} super admin successfully", "user_id": user_id, - "is_super_admin": user.is_super_admin, + "role": user.role, } diff --git a/app/modules/tenancy/routes/api/store_auth.py b/app/modules/tenancy/routes/api/store_auth.py index 891e1322..d0628dfa 100644 --- a/app/modules/tenancy/routes/api/store_auth.py +++ b/app/modules/tenancy/routes/api/store_auth.py @@ -77,7 +77,7 @@ def store_login( user = login_result["user"] # CRITICAL: Prevent admin users from using store login - if user.role == "admin": + if user.is_admin: logger.warning(f"Admin user attempted store login: {user.username}") raise InvalidCredentialsException( "Admins cannot access store portal. Please use admin portal." diff --git a/app/modules/tenancy/schemas/team.py b/app/modules/tenancy/schemas/team.py index 38ceb95b..f68975ae 100644 --- a/app/modules/tenancy/schemas/team.py +++ b/app/modules/tenancy/schemas/team.py @@ -118,7 +118,6 @@ class TeamMemberResponse(BaseModel): first_name: str | None last_name: str | None full_name: str - user_type: str = Field(..., description="'owner' or 'member'") role_name: str = Field(..., description="Role name") role_id: int | None permissions: list[str] = Field( diff --git a/app/modules/tenancy/services/admin_platform_service.py b/app/modules/tenancy/services/admin_platform_service.py index 1b2ce95b..ea153b92 100644 --- a/app/modules/tenancy/services/admin_platform_service.py +++ b/app/modules/tenancy/services/admin_platform_service.py @@ -344,7 +344,7 @@ class AdminPlatformService: reason="User must be an admin to be promoted to super admin", ) - user.is_super_admin = is_super_admin + user.role = "super_admin" if is_super_admin else "platform_admin" user.updated_at = datetime.now(UTC) db.flush() db.refresh(user) @@ -402,9 +402,8 @@ class AdminPlatformService: hashed_password=auth_manager.hash_password(password), first_name=first_name, last_name=last_name, - role="admin", + role="platform_admin", is_active=True, - is_super_admin=False, # Platform admin, not super admin ) db.add(user) db.flush() @@ -459,10 +458,12 @@ class AdminPlatformService: Returns: Tuple of (list of User objects, total count) """ - query = db.query(User).filter(User.role == "admin") + query = db.query(User).filter( + User.role.in_(["super_admin", "platform_admin"]) + ) if not include_super_admins: - query = query.filter(User.is_super_admin == False) + query = query.filter(User.role == "platform_admin") if is_active is not None: query = query.filter(User.is_active == is_active) @@ -508,7 +509,10 @@ class AdminPlatformService: admin = ( db.query(User) .options(joinedload(User.admin_platforms)) - .filter(User.id == user_id, User.role == "admin") + .filter( + User.id == user_id, + User.role.in_(["super_admin", "platform_admin"]), + ) .first() ) @@ -560,8 +564,7 @@ class AdminPlatformService: hashed_password=get_password_hash(password), first_name=first_name, last_name=last_name, - role="admin", - is_super_admin=True, + role="super_admin", is_active=True, ) db.add(user) @@ -601,7 +604,10 @@ class AdminPlatformService: operation="deactivate own account", ) - admin = db.query(User).filter(User.id == user_id, User.role == "admin").first() + admin = db.query(User).filter( + User.id == user_id, + User.role.in_(["super_admin", "platform_admin"]), + ).first() if not admin: raise UserNotFoundException(str(user_id)) @@ -642,7 +648,10 @@ class AdminPlatformService: operation="delete own account", ) - admin = db.query(User).filter(User.id == user_id, User.role == "admin").first() + admin = db.query(User).filter( + User.id == user_id, + User.role.in_(["super_admin", "platform_admin"]), + ).first() if not admin: raise UserNotFoundException(str(user_id)) diff --git a/app/modules/tenancy/services/admin_service.py b/app/modules/tenancy/services/admin_service.py index 40392118..869b0c28 100644 --- a/app/modules/tenancy/services/admin_service.py +++ b/app/modules/tenancy/services/admin_service.py @@ -69,7 +69,7 @@ class AdminService: raise CannotModifySelfException(user_id, "deactivate account") # Check if user is another admin - if user.role == "admin" and user.id != current_admin_id: + if user.is_admin and user.id != current_admin_id: raise UserStatusChangeException( user_id=user_id, current_status="admin", @@ -248,7 +248,7 @@ class AdminService: user = self._get_user_by_id_or_raise(db, user_id) # Prevent changing own admin status - if user.id == current_admin_id and role and role != "admin": + if user.id == current_admin_id and role and role not in ("super_admin", "platform_admin"): raise UserRoleChangeException( user_id=user_id, current_role=user.role, diff --git a/app/modules/tenancy/services/merchant_service.py b/app/modules/tenancy/services/merchant_service.py index a6db0580..174d3eb4 100644 --- a/app/modules/tenancy/services/merchant_service.py +++ b/app/modules/tenancy/services/merchant_service.py @@ -69,7 +69,7 @@ class MerchantService: username=merchant_data.owner_email.split("@")[0], email=merchant_data.owner_email, hashed_password=auth_manager.hash_password(temp_password), - role="user", + role="merchant_owner", is_active=True, is_email_verified=False, ) diff --git a/app/modules/tenancy/services/store_service.py b/app/modules/tenancy/services/store_service.py index 8352b0fa..7ef29c0a 100644 --- a/app/modules/tenancy/services/store_service.py +++ b/app/modules/tenancy/services/store_service.py @@ -80,7 +80,7 @@ class StoreService: # Check if user is merchant owner or admin if ( - current_user.role != "admin" + not current_user.is_admin and merchant.owner_user_id != current_user.id ): raise UnauthorizedStoreAccessException( @@ -105,7 +105,7 @@ class StoreService: letzshop_csv_url_en=store_data.letzshop_csv_url_en, letzshop_csv_url_de=store_data.letzshop_csv_url_de, is_active=True, - is_verified=(current_user.role == "admin"), + is_verified=current_user.is_admin, ) db.add(new_store) @@ -153,7 +153,7 @@ class StoreService: query = db.query(Store) # Non-admin users can only see active and verified stores, plus their own - if current_user.role != "admin": + if not current_user.is_admin: # Get store IDs the user owns through merchants from app.modules.tenancy.models import Merchant @@ -457,7 +457,7 @@ class StoreService: def _can_access_store(self, store: Store, user: User) -> bool: """Check if user can access store.""" # Admins can always access - if user.role == "admin": + if user.is_admin: return True # Merchant owners can access their stores @@ -481,7 +481,7 @@ class StoreService: - Team members with appropriate role (owner role in StoreUser) """ # Admins can always update - if user.role == "admin": + if user.is_admin: return True # Check if user is store owner via merchant diff --git a/app/modules/tenancy/services/store_team_service.py b/app/modules/tenancy/services/store_team_service.py index 3c4b7cc6..3e4ff120 100644 --- a/app/modules/tenancy/services/store_team_service.py +++ b/app/modules/tenancy/services/store_team_service.py @@ -33,7 +33,7 @@ from app.modules.tenancy.exceptions import ( TeamMemberAlreadyExistsException, UserNotFoundException, ) -from app.modules.tenancy.models import Role, Store, StoreUser, StoreUserType, User +from app.modules.tenancy.models import Role, Store, StoreUser, User from middleware.auth import AuthManager logger = logging.getLogger(__name__) @@ -134,7 +134,7 @@ class StoreTeamService: email=email, username=username, hashed_password=self.auth_manager.hash_password(temp_password), - role="store", # Platform role + role="store_member", is_active=False, # Will be activated when invitation accepted is_email_verified=False, ) @@ -157,7 +157,6 @@ class StoreTeamService: store_user = StoreUser( store_id=store.id, user_id=user.id, - user_type=StoreUserType.TEAM_MEMBER.value, role_id=role.id, invited_by=inviter.id, invitation_token=invitation_token, @@ -416,7 +415,6 @@ class StoreTeamService: "first_name": vu.user.first_name, "last_name": vu.user.last_name, "full_name": vu.user.full_name, - "user_type": vu.user_type, "role_name": vu.role.name if vu.role else "owner", "role_id": vu.role.id if vu.role else None, "permissions": vu.get_all_permissions(), diff --git a/app/modules/tenancy/services/tenancy_metrics.py b/app/modules/tenancy/services/tenancy_metrics.py index fbaf2634..bcaf7939 100644 --- a/app/modules/tenancy/services/tenancy_metrics.py +++ b/app/modules/tenancy/services/tenancy_metrics.py @@ -216,7 +216,7 @@ class TenancyMetricsProvider: db.query(User) .filter( User.id.in_(platform_user_ids), - User.role == "admin", + User.role.in_(["super_admin", "platform_admin"]), ) .count() ) @@ -230,11 +230,9 @@ class TenancyMetricsProvider: ) # Team members: distinct StoreUser users who are NOT merchant owners - # Uses user_type="member" AND excludes owner user IDs to avoid overlap team_members = ( db.query(func.count(func.distinct(StoreUser.user_id))) .filter( - StoreUser.user_type == "member", ~StoreUser.user_id.in_(db.query(Merchant.owner_user_id)), ) .scalar() or 0 diff --git a/app/modules/tenancy/static/admin/js/admin-user-detail.js b/app/modules/tenancy/static/admin/js/admin-user-detail.js index eaaa10bd..83302b32 100644 --- a/app/modules/tenancy/static/admin/js/admin-user-detail.js +++ b/app/modules/tenancy/static/admin/js/admin-user-detail.js @@ -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); diff --git a/app/modules/tenancy/static/admin/js/admin-user-edit.js b/app/modules/tenancy/static/admin/js/admin-user-edit.js index b6fcd468..f61863bd 100644 --- a/app/modules/tenancy/static/admin/js/admin-user-edit.js +++ b/app/modules/tenancy/static/admin/js/admin-user-edit.js @@ -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`); diff --git a/app/modules/tenancy/static/admin/js/admin-users.js b/app/modules/tenancy/static/admin/js/admin-users.js index e6497dac..6bb96d8f 100644 --- a/app/modules/tenancy/static/admin/js/admin-users.js +++ b/app/modules/tenancy/static/admin/js/admin-users.js @@ -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 }; diff --git a/app/modules/tenancy/static/admin/js/select-platform.js b/app/modules/tenancy/static/admin/js/select-platform.js index 50e6fb0a..337233dd 100644 --- a/app/modules/tenancy/static/admin/js/select-platform.js +++ b/app/modules/tenancy/static/admin/js/select-platform.js @@ -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) { diff --git a/app/modules/tenancy/static/admin/js/user-create.js b/app/modules/tenancy/static/admin/js/user-create.js index 2c424de2..3fe28159 100644 --- a/app/modules/tenancy/static/admin/js/user-create.js +++ b/app/modules/tenancy/static/admin/js/user-create.js @@ -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); diff --git a/app/modules/tenancy/templates/tenancy/admin/admin-user-detail.html b/app/modules/tenancy/templates/tenancy/admin/admin-user-detail.html index 799cd0c7..b664db80 100644 --- a/app/modules/tenancy/templates/tenancy/admin/admin-user-detail.html +++ b/app/modules/tenancy/templates/tenancy/admin/admin-user-detail.html @@ -58,16 +58,16 @@
- +

- Admin Type + Role

-

+

-

@@ -98,7 +98,7 @@

Platforms

-

+

0

@@ -170,7 +170,7 @@ -