fix: store login crash and dashboard misrouted as storefront
Some checks failed
CI / ruff (push) Successful in 10s
CI / pytest (push) Failing after 44m20s
CI / validate (push) Successful in 22s
CI / dependency-scanning (push) Successful in 27s
CI / docs (push) Has been skipped
CI / deploy (push) Has been skipped

- 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 <noreply@anthropic.com>
This commit is contained in:
2026-02-24 01:19:22 +01:00
parent 05d31a7fc5
commit cd935988c4
3 changed files with 61 additions and 7 deletions

View File

@@ -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

View File

@@ -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 = (

View File

@@ -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