From cd935988c4ef0101ba549b744134e02ddc628a3f Mon Sep 17 00:00:00 2001 From: Samir Boulahtit Date: Tue, 24 Feb 2026 01:19:22 +0100 Subject: [PATCH] fix: store login crash and dashboard misrouted as storefront - Seed default RBAC roles per store and assign role_id to StoreUser records (was never implemented after RBAC Phase 1 cleanup) - Handle nullable role in auth_service find_user_store and get_user_store_role to prevent NoneType crash on login - Use platform_clean_path instead of clean_path in FrontendTypeMiddleware so /store/X/dashboard is detected as STORE, not STOREFRONT Co-Authored-By: Claude Opus 4.6 --- app/modules/core/services/auth_service.py | 6 ++- middleware/frontend_type.py | 7 ++- scripts/seed/seed_demo.py | 55 +++++++++++++++++++++-- 3 files changed, 61 insertions(+), 7 deletions(-) diff --git a/app/modules/core/services/auth_service.py b/app/modules/core/services/auth_service.py index b7535461..9f34de85 100644 --- a/app/modules/core/services/auth_service.py +++ b/app/modules/core/services/auth_service.py @@ -131,7 +131,8 @@ class AuthService: ) if store_user: - return True, store_user.role.name + role_name = store_user.role.name if store_user.role else "staff" + return True, role_name return False, None @@ -213,7 +214,8 @@ class AuthService: (vm for vm in user.store_memberships if vm.is_active), None ) if active_membership: - return active_membership.store, active_membership.role.name + role_name = active_membership.role.name if active_membership.role else "staff" + return active_membership.store, role_name return None, None diff --git a/middleware/frontend_type.py b/middleware/frontend_type.py index 9d4082e6..5d6a2f9c 100644 --- a/middleware/frontend_type.py +++ b/middleware/frontend_type.py @@ -43,8 +43,11 @@ class FrontendTypeMiddleware(BaseHTTPMiddleware): async def dispatch(self, request: Request, call_next): """Detect frontend type and inject into request state.""" host = request.headers.get("host", "") - # Use clean_path if available (from store_context_middleware), else original path - path = getattr(request.state, "clean_path", None) or request.url.path + # Use platform_clean_path (platform prefix stripped, store prefix retained) + # so FrontendDetector can distinguish /store/ from /storefront/. + # Do NOT use clean_path here — it has the store prefix stripped too, + # which makes /store/X/dashboard indistinguishable from /storefront/X/products. + path = getattr(request.state, "platform_clean_path", None) or request.url.path # Check if store context exists (set by StoreContextMiddleware) has_store_context = ( diff --git a/scripts/seed/seed_demo.py b/scripts/seed/seed_demo.py index d7ac326d..1cedb74b 100644 --- a/scripts/seed/seed_demo.py +++ b/scripts/seed/seed_demo.py @@ -221,6 +221,7 @@ DEMO_SUBSCRIPTIONS = [ ] # Demo team members (linked to merchants by index, assigned to stores by store_code) +# Role must be one of: manager, staff, support, viewer, marketing (see ROLE_PRESETS) DEMO_TEAM_MEMBERS = [ # WizaCorp team { @@ -229,6 +230,7 @@ DEMO_TEAM_MEMBERS = [ "password": "password123", # noqa: SEC001 "first_name": "Alice", "last_name": "Manager", + "role": "manager", "store_codes": ["WIZATECH", "WIZAGADGETS"], # manages two stores }, { @@ -237,6 +239,7 @@ DEMO_TEAM_MEMBERS = [ "password": "password123", # noqa: SEC001 "first_name": "Charlie", "last_name": "Staff", + "role": "staff", "store_codes": ["WIZAHOME"], }, # Fashion Group team @@ -246,6 +249,7 @@ DEMO_TEAM_MEMBERS = [ "password": "password123", # noqa: SEC001 "first_name": "Diana", "last_name": "Stylist", + "role": "manager", "store_codes": ["FASHIONHUB", "FASHIONOUTLET"], }, { @@ -254,6 +258,7 @@ DEMO_TEAM_MEMBERS = [ "password": "password123", # noqa: SEC001 "first_name": "Eric", "last_name": "Sales", + "role": "staff", "store_codes": ["FASHIONOUTLET"], }, # BookWorld team @@ -263,6 +268,7 @@ DEMO_TEAM_MEMBERS = [ "password": "password123", # noqa: SEC001 "first_name": "Fiona", "last_name": "Editor", + "role": "manager", "store_codes": ["BOOKSTORE", "BOOKDIGITAL"], }, ] @@ -853,10 +859,37 @@ def create_demo_stores( return stores +def _ensure_store_roles(db: Session, store: Store) -> dict[str, Role]: + """Ensure default roles exist for a store, return name→Role lookup.""" + from app.modules.tenancy.services.permission_discovery_service import ( + permission_discovery_service, + ) + + existing = db.query(Role).filter(Role.store_id == store.id).all() + if existing: + return {r.name: r for r in existing} + + role_names = ["manager", "staff", "support", "viewer", "marketing"] + roles = {} + for name in role_names: + permissions = list(permission_discovery_service.get_preset_permissions(name)) + role = Role( + store_id=store.id, + name=name, + permissions=permissions, + ) + db.add(role) # noqa: PERF006 + roles[name] = role + + db.flush() + print_success(f" Created default roles for {store.name}: {', '.join(role_names)}") + return roles + + def create_demo_team_members( db: Session, stores: list[Store], auth_manager: AuthManager ) -> list[User]: - """Create demo team member users and assign them to stores.""" + """Create demo team member users and assign them to stores with roles.""" if SEED_MODE == "minimal": return [] @@ -865,6 +898,15 @@ def create_demo_team_members( # Build a store_code → Store lookup from the created stores store_lookup = {s.store_code: s for s in stores} + # Pre-create default roles for all stores that team members will be assigned to + store_roles: dict[str, dict[str, Role]] = {} + for member_data in DEMO_TEAM_MEMBERS: + for store_code in member_data["store_codes"]: + if store_code not in store_roles: + store = store_lookup.get(store_code) + if store: + store_roles[store_code] = _ensure_store_roles(db, store) + for member_data in DEMO_TEAM_MEMBERS: # Check if user already exists user = db.execute( @@ -894,7 +936,8 @@ def create_demo_team_members( team_users.append(user) - # Assign user to stores + # Assign user to stores with role + role_name = member_data["role"] for store_code in member_data["store_codes"]: store = store_lookup.get(store_code) if not store: @@ -912,14 +955,20 @@ def create_demo_team_members( if existing_link: continue + # Look up role for this store + role = store_roles.get(store_code, {}).get(role_name) + store_user = StoreUser( store_id=store.id, user_id=user.id, + role_id=role.id if role else None, is_active=True, created_at=datetime.now(UTC), ) db.add(store_user) # noqa: PERF006 - print_success(f" Assigned {user.first_name} to {store.name} as team member") + print_success( + f" Assigned {user.first_name} to {store.name} as {role_name}" + ) db.flush() return team_users