# Role-Based Access Control (RBAC) Developer Guide

**Version:** 2.0
**Last Updated:** February 2026
**Audience:** Development Team

---

## Table of Contents

1. [Introduction](#introduction)
2. [RBAC Overview](#rbac-overview)
3. [System Architecture](#system-architecture)
4. [User Types & Contexts](#user-types-contexts)
5. [Database Schema](#database-schema)
6. [Permission System](#permission-system)
7. [Authentication Flow](#authentication-flow)
8. [Authorization Implementation](#authorization-implementation)
9. [Team Management](#team-management)
10. [Code Examples](#code-examples)
11. [Best Practices](#best-practices)
12. [Testing Guidelines](#testing-guidelines)
13. [Troubleshooting](#troubleshooting)

---

## Introduction

This guide documents the Role-Based Access Control (RBAC) system implemented in our multi-tenant e-commerce platform. The system provides granular access control across three distinct contexts: Platform Administration, Store Management, and Customer Shopping.

### Purpose

The RBAC system ensures that:
- Users can only access resources they're authorized to see
- Permissions are granular and context-specific
- Multi-tenancy is enforced at the database and application level
- Team collaboration is secure and auditable

### Key Principles

1. **Context Isolation** - Admin, store, and customer contexts are completely isolated
2. **Least Privilege** - Users have only the permissions they need
3. **Owner Authority** - Store owners have complete control over their store
4. **Team Flexibility** - Store teams can be structured with various role types
5. **Security First** - Cookie path isolation and role enforcement prevent unauthorized access

---

## RBAC Overview

### Two-Tier Permission Model

```
┌─────────────────────────────────────────────────────────────┐
│                     PLATFORM LEVEL                            │
│                     User.role (4-value enum)                  │
│                                                               │
│  ┌──────────────┐  ┌────────────────┐                       │
│  │ Super Admin  │  │ Platform Admin │   Platform admins      │
│  │(super_admin) │  │(platform_admin)│   (is_admin = True)    │
│  └──────────────┘  └────────────────┘                       │
│                                                               │
│  ┌────────────────┐  ┌──────────────┐                       │
│  │ Merchant Owner │  │ Store Member │   Store users          │
│  │(merchant_owner)│  │(store_member)│   (is_store_user=True) │
│  └────────────────┘  └──────────────┘                       │
└─────────────────────────────────────────────────────────────┘
                                        │
                                        ▼
┌─────────────────────────────────────────────────────────────┐
│                   STORE PERMISSION LEVEL                      │
│                    Role.permissions                           │
│                                                               │
│  Merchant owners bypass permission checks (all permissions)  │
│  Store members get permissions from assigned Role             │
│                                                               │
│  Manager, Staff, Support, Viewer, Marketing, Custom          │
└─────────────────────────────────────────────────────────────┘
```

### Context Separation

The application operates in three isolated contexts:

| Context | Routes | Authentication | User Roles |
|---------|--------|----------------|------------|
| **Admin** | `/admin/*` | `admin_token` cookie | `super_admin`, `platform_admin` |
| **Store** | `/store/*` | `store_token` cookie | `merchant_owner`, `store_member` |
| **Storefront** | `/storefront/account/*` | `customer_token` cookie | Customers |

**Important:** These contexts are security boundaries. Admin users cannot access store routes, store users cannot access admin routes, and customers are entirely separate.

---

## System Architecture

### High-Level Architecture

```
┌─────────────────────────────────────────────────────────────┐
│                        Request                               │
└────────────────┬────────────────────────────────────────────┘
                 │
                 ▼
┌─────────────────────────────────────────────────────────────┐
│                  Middleware Layer                            │
│                                                              │
│  • StoreContextMiddleware                                  │
│  • StoreDetectionMiddleware                                │
│  • AuthenticationMiddleware                                 │
└────────────────┬────────────────────────────────────────────┘
                 │
                 ▼
┌─────────────────────────────────────────────────────────────┐
│              FastAPI Route Handler                           │
│                                                              │
│  Dependencies:                                               │
│  • get_current_admin_from_cookie_or_header()                │
│  • get_current_store_from_cookie_or_header()               │
│  • require_store_permission("permission.name")             │
│  • require_store_owner()                                   │
└────────────────┬────────────────────────────────────────────┘
                 │
                 ▼
┌─────────────────────────────────────────────────────────────┐
│                    Service Layer                             │
│                                                              │
│  • store_team_service                                      │
│  • auth_service                                             │
│  • customer_service                                         │
└────────────────┬────────────────────────────────────────────┘
                 │
                 ▼
┌─────────────────────────────────────────────────────────────┐
│                   Database Layer                             │
│                                                              │
│  • User, StoreUser, Role, Customer                         │
└─────────────────────────────────────────────────────────────┘
```

### Component Responsibilities

#### Authentication Layer
- Validates JWT tokens
- Verifies cookie paths match routes
- Manages token lifecycle (creation, refresh, expiry)
- Handles dual storage (cookies + headers)

#### Authorization Layer
- Checks user roles and permissions
- Enforces store ownership rules
- Validates team member access
- Blocks cross-context access

#### Service Layer
- Implements business logic
- Manages team invitations
- Handles role assignments
- Provides reusable authorization checks

---

## User Types & Contexts

### Super Admins

**Characteristics:**
- `User.role = "super_admin"`
- Full access to `/admin/*` routes
- Manage all platforms, stores, and users
- Cannot access store or customer portals
- `User.is_super_admin` property returns `True` (computed from `role == "super_admin"`)

**Use Cases:**
- Full platform configuration
- Multi-platform management
- Super-level user management
- System monitoring

**Authentication:**
```python
# Login endpoint
POST /api/v1/admin/auth/login

# Cookie set
admin_token (path=/admin, httponly=true)

# Access routes
GET /admin/dashboard
GET /admin/stores
POST /admin/users/{user_id}/suspend
```

### Platform Admins

**Characteristics:**
- `User.role = "platform_admin"`
- Access to `/admin/*` routes scoped to assigned platforms
- `User.is_admin` property returns `True` (shared with super_admin)
- `User.is_platform_admin` property returns `True`
- Cannot access store or customer portals

**Use Cases:**
- Platform-scoped configuration
- Store approval/verification
- User management within assigned platforms
- System monitoring

### Merchant Owners

**Characteristics:**
- `User.role = "merchant_owner"`
- Automatic full permissions within their stores
- Ownership determined via `Merchant.owner_user_id` (not a column on StoreUser)
- Can invite and manage team members
- Cannot be removed from their store
- `User.is_merchant_owner` property returns `True`
- `User.is_store_user` property returns `True`

**Use Cases:**
- Complete store management
- Team administration
- Financial oversight
- Settings configuration

**Special Privileges:**
```python
# Automatic permissions - owners bypass permission checks
def has_permission(self, permission: str) -> bool:
    if self.is_owner:
        return True  # Owners bypass permission checks
```

### Store Team Members

**Characteristics:**
- `User.role = "store_member"`
- Permissions defined by `Role.permissions` via `StoreUser.role_id`
- Invited by merchant owner via email
- Can be assigned different roles (Manager, Staff, etc.)
- `User.is_store_user` property returns `True`

**Use Cases:**
- Day-to-day operations based on role
- Collaborative store management
- Specialized functions (marketing, support)

**Role Examples:**
```python
# Manager - Nearly full access
permissions = [
    "products.view", "products.create", "products.edit",
    "orders.view", "orders.edit", "orders.cancel",
    "customers.view", "customers.edit",
    "reports.view", "reports.financial"
]

# Staff - Operational access
permissions = [
    "products.view", "products.create", "products.edit",
    "orders.view", "orders.edit",
    "customers.view"
]

# Support - Customer service focus
permissions = [
    "orders.view", "orders.edit",
    "customers.view", "customers.edit"
]
```

### Customers

**Characteristics:**
- Separate `Customer` model (not in `User` table)
- Store-scoped authentication
- Can self-register on store shops
- Access only their own account + shop catalog

**Use Cases:**
- Browse store products
- Place orders
- Manage account information
- View order history

**Important:** Customers are NOT in the User table. They use a separate authentication system and cannot access admin or store portals.

---

## Database Schema

### Entity Relationship Diagram

```
┌──────────────────┐
│      users       │
│                  │
│  id (PK)         │◄──────┐
│  email           │       │
│  username        │       │
│  role            │       │  owner_user_id
│  ('super_admin'| │       │
│  'platform_     │       │
│   admin' |       │       │
│  'merchant_     │       │
│   owner' |       │       │
│  'store_member') │       │
│  is_active       │       │
│  is_email_       │       │
│  verified        │       │
└──────────────────┘       │
        │                  │
        │                  │
        │                  │
        ▼                  │
┌──────────────────┐       │
│  store_users    │       │
│                  │       │
│  id (PK)         │       │
│  store_id (FK) ─┼───┐   │
│  user_id (FK) ───┼─┐ │   │
│  role_id (FK)    │ │ │   │
│  invitation_     │ │ │   │
│  token           │ │ │   │
│  invitation_     │ │ │   │
│  sent_at         │ │ │   │
│  invitation_     │ │ │   │
│  accepted_at     │ │ │   │
│  invited_by (FK) │ │ │   │
│  is_active       │ │ │   │
└──────────────────┘ │ │   │
        │            │ │   │
        │ role_id    │ │   │
        │            │ │   │
        ▼            │ │   │
┌──────────────────┐ │ │   │
│      roles       │ │ │   │
│                  │ │ │   │
│  id (PK)         │ │ │   │
│  store_id (FK) ─┼─┘ │   │
│  name            │   │   │
│  permissions     │   │   │
│  (JSONB)         │   │   │
└──────────────────┘   │   │
                       │   │
                       ▼   │
┌──────────────────┐       │
│     stores      │       │
│                  │       │
│  id (PK)         │       │
│  store_code     │       │
│  subdomain       │       │
│  name            │       │
│  owner_user_id ──┼───────┘
│  is_active       │
│  is_verified     │
└──────────────────┘
        │
        │
        ▼
┌──────────────────┐
│    customers     │
│ (SEPARATE AUTH)  │
│                  │
│  id (PK)         │
│  store_id (FK)  │
│  email           │
│  hashed_password │
│  customer_number │
│  is_active       │
└──────────────────┘
```

**Note:** The `store_users` table no longer has a `user_type` column. Ownership is determined by `Merchant.owner_user_id` and `User.role == "merchant_owner"`, not by a field on StoreUser.

### Key Tables

#### users

Primary platform user table for all user types.

```python
class User(Base):
    __tablename__ = "users"

    id = Column(Integer, primary_key=True)
    email = Column(String, unique=True, nullable=False)
    username = Column(String, unique=True, nullable=False)
    hashed_password = Column(String, nullable=False)
    role = Column(String, nullable=False)
    # 'super_admin', 'platform_admin', 'merchant_owner', or 'store_member'
    is_active = Column(Boolean, default=True)
    is_email_verified = Column(Boolean, default=False)

    # Computed properties (not database columns):
    # is_super_admin: bool  -> role == "super_admin"
    # is_admin: bool        -> role in ("super_admin", "platform_admin")
    # is_platform_admin: bool -> role == "platform_admin"
    # is_merchant_owner: bool -> role == "merchant_owner"
    # is_store_user: bool   -> role in ("merchant_owner", "store_member")
```

**Important Fields:**
- `role`: Contains one of 4 values: `"super_admin"`, `"platform_admin"`, `"merchant_owner"`, or `"store_member"`
- `is_email_verified`: Required for team member invitations

**Note:** The `is_super_admin` column was removed. It is now a computed property: `self.role == "super_admin"`. Similarly, `is_admin` checks `role in ("super_admin", "platform_admin")`.

#### stores

Store entities representing businesses on the platform.

```python
class Store(Base):
    __tablename__ = "stores"

    id = Column(Integer, primary_key=True)
    store_code = Column(String, unique=True, nullable=False)
    subdomain = Column(String, unique=True, nullable=False)
    name = Column(String, nullable=False)
    owner_user_id = Column(Integer, ForeignKey("users.id"), nullable=False)
    is_active = Column(Boolean, default=True)
    is_verified = Column(Boolean, default=False)
```

**Important Fields:**
- `owner_user_id`: The user who owns this store (full permissions)
- `store_code`: Used in URLs for store context
- `subdomain`: For subdomain-based routing

#### store_users

Junction table linking users to stores with role information.

```python
class StoreUser(Base):
    __tablename__ = "store_users"

    id = Column(Integer, primary_key=True)
    store_id = Column(Integer, ForeignKey("stores.id"), nullable=False)
    user_id = Column(Integer, ForeignKey("users.id"), nullable=False)
    role_id = Column(Integer, ForeignKey("roles.id"), nullable=True)
    invited_by = Column(Integer, ForeignKey("users.id"))
    invitation_token = Column(String, nullable=True)
    invitation_sent_at = Column(DateTime, nullable=True)
    invitation_accepted_at = Column(DateTime, nullable=True)
    is_active = Column(Boolean, default=False)  # Activated on acceptance
```

**Important Fields:**
- `role_id`: NULL for merchant owners (they have all permissions), set for store members
- `invitation_*`: Fields for tracking invitation workflow
- `is_active`: FALSE until invitation accepted (for team members)

**Note:** The `user_type` column was removed from StoreUser. Ownership is now determined by `User.role == "merchant_owner"` and `Merchant.owner_user_id`, not by a field on this table. Use `User.is_owner_of(store_id)` to check ownership.

#### roles

Store-specific role definitions with permissions.

```python
class Role(Base):
    __tablename__ = "roles"

    id = Column(Integer, primary_key=True)
    store_id = Column(Integer, ForeignKey("stores.id"), nullable=False)
    name = Column(String, nullable=False)
    permissions = Column(JSONB, default=[])  # PostgreSQL JSONB
```

**Important Fields:**
- `store_id`: Roles are store-scoped, not platform-wide
- `name`: Role name (e.g., "Manager", "Staff", "Support")
- `permissions`: Array of permission strings (e.g., `["products.view", "products.create"]`)

#### customers

Separate customer authentication system (store-scoped).

```python
class Customer(Base):
    __tablename__ = "customers"

    id = Column(Integer, primary_key=True)
    store_id = Column(Integer, ForeignKey("stores.id"), nullable=False)
    email = Column(String, nullable=False)  # Unique within store
    hashed_password = Column(String, nullable=False)
    customer_number = Column(String, nullable=False)
    is_active = Column(Boolean, default=True)
```

**Important Note:** Customers are NOT in the `users` table. They have a completely separate authentication system and are scoped to individual stores.

---

## Permission System

### Permission Structure

Permissions follow a hierarchical naming convention: `resource.action`

```python
# Format
"{resource}.{action}"

# Examples
"products.view"      # View products
"products.create"    # Create new products
"products.edit"      # Edit existing products
"products.delete"    # Delete products
"orders.cancel"      # Cancel orders
"team.invite"        # Invite team members (owner only)
"settings.edit"      # Edit store settings
"reports.financial"  # View financial reports
```

### Available Permissions

#### Dashboard
```python
"dashboard.view"     # View dashboard
```

#### Products
```python
"products.view"      # View product list
"products.create"    # Create new products
"products.edit"      # Edit products
"products.delete"    # Delete products
"products.import"    # Import products from CSV/marketplace
"products.export"    # Export products
```

#### Stock/Inventory
```python
"stock.view"         # View stock levels
"stock.edit"         # Adjust stock quantities
"stock.transfer"     # Transfer stock between locations
```

#### Orders
```python
"orders.view"        # View orders
"orders.edit"        # Edit order details
"orders.cancel"      # Cancel orders
"orders.refund"      # Process refunds
```

#### Customers
```python
"customers.view"     # View customer list
"customers.edit"     # Edit customer details
"customers.delete"   # Delete customers
"customers.export"   # Export customer data
```

#### Marketing
```python
"marketing.view"     # View marketing campaigns
"marketing.create"   # Create campaigns
"marketing.send"     # Send marketing emails
```

#### Reports
```python
"reports.view"       # View basic reports
"reports.financial"  # View financial reports
"reports.export"     # Export report data
```

#### Settings
```python
"settings.view"      # View settings
"settings.edit"      # Edit basic settings
"settings.theme"     # Edit theme/branding
"settings.domains"   # Manage custom domains
```

#### Team Management
```python
"team.view"          # View team members
"team.invite"        # Invite new members (owner only)
"team.edit"          # Edit member roles (owner only)
"team.remove"        # Remove members (owner only)
```

#### Marketplace Imports
```python
"imports.view"       # View import jobs
"imports.create"     # Create import jobs
"imports.cancel"     # Cancel import jobs
```

### Permission Constants

All permissions are defined in `app/core/permissions.py`:

```python
from enum import Enum

class StorePermissions(str, Enum):
    """All available store permissions."""

    # Dashboard
    DASHBOARD_VIEW = "dashboard.view"

    # Products
    PRODUCTS_VIEW = "products.view"
    PRODUCTS_CREATE = "products.create"
    PRODUCTS_EDIT = "products.edit"
    PRODUCTS_DELETE = "products.delete"
    PRODUCTS_IMPORT = "products.import"
    PRODUCTS_EXPORT = "products.export"

    # ... (see permissions.py for complete list)
```

### Role Presets

Pre-configured role templates for common team structures:

```python
class PermissionGroups:
    """Pre-defined permission sets for common roles."""

    # Owner - All permissions (automatic)
    OWNER = set(p.value for p in StorePermissions)

    # Manager - Most permissions except team management
    MANAGER = {
        "dashboard.view",
        "products.view", "products.create", "products.edit", "products.delete",
        "stock.view", "stock.edit", "stock.transfer",
        "orders.view", "orders.edit", "orders.cancel", "orders.refund",
        "customers.view", "customers.edit", "customers.export",
        "marketing.view", "marketing.create", "marketing.send",
        "reports.view", "reports.financial", "reports.export",
        "settings.view", "settings.theme",
        "imports.view", "imports.create"
    }

    # Staff - Day-to-day operations
    STAFF = {
        "dashboard.view",
        "products.view", "products.create", "products.edit",
        "stock.view", "stock.edit",
        "orders.view", "orders.edit",
        "customers.view"
    }

    # Support - Customer service focused
    SUPPORT = {
        "dashboard.view",
        "products.view",
        "orders.view", "orders.edit",
        "customers.view", "customers.edit"
    }

    # Viewer - Read-only access
    VIEWER = {
        "dashboard.view",
        "products.view",
        "stock.view",
        "orders.view",
        "customers.view",
        "reports.view"
    }

    # Marketing - Marketing and customer communication
    MARKETING = {
        "dashboard.view",
        "customers.view", "customers.export",
        "marketing.view", "marketing.create", "marketing.send",
        "reports.view"
    }
```

### Custom Roles

Owners can create custom roles with specific permission sets:

```python
# Creating a custom role
custom_permissions = [
    "products.view",
    "products.create",
    "orders.view",
    "customers.view"
]

role = Role(
    store_id=store.id,
    name="Product Manager",
    permissions=custom_permissions
)
```

---

## Authentication Flow

### Admin Authentication

```
┌─────────────┐
│   Client    │
└──────┬──────┘
       │
       │ POST /api/v1/admin/auth/login
       │ { username, password }
       ▼
┌─────────────────────────────┐
│  Admin Auth Endpoint        │
│                             │
│  1. Validate credentials    │
│  2. Check is_admin          │
│     (super_admin or         │
│      platform_admin)        │
│  3. Generate JWT            │
└──────┬──────────────────────┘
       │
       │ Set-Cookie: admin_token=<JWT>
       │             Path=/admin
       │             HttpOnly=true
       │             Secure=true (prod)
       │             SameSite=Lax
       │
       │ Response: { access_token, user }
       ▼
┌─────────────┐
│   Client    │
│             │
│ 🍪 admin_token (path=/admin)      │
│ 💾 localStorage.token              │
└─────────────┘
```

### Store Authentication

```
┌─────────────┐
│   Client    │
└──────┬──────┘
       │
       │ POST /api/v1/store/auth/login
       │ { username, password }
       ▼
┌─────────────────────────────┐
│  Store Auth Endpoint       │
│                             │
│  1. Validate credentials    │
│  2. Block if is_admin       │
│  3. Find store membership  │
│  4. Determine ownership     │
│     via Merchant.           │
│     owner_user_id           │
│  5. Generate JWT            │
└──────┬──────────────────────┘
       │
       │ Set-Cookie: store_token=<JWT>
       │             Path=/store
       │             HttpOnly=true
       │             Secure=true (prod)
       │             SameSite=Lax
       │
       │ Response: { access_token, user, store, role }
       ▼
┌─────────────┐
│   Client    │
│             │
│ 🍪 store_token (path=/store)    │
│ 💾 localStorage.token              │
└─────────────┘
```

### Customer Authentication

```
┌─────────────┐
│   Client    │
└──────┬──────┘
       │
       │ POST /api/v1/platform/stores/{id}/customers/login
       │ { username, password }
       ▼
┌─────────────────────────────┐
│  Customer Auth Endpoint     │
│                             │
│  1. Validate store         │
│  2. Validate credentials    │
│  3. Generate JWT            │
└──────┬──────────────────────┘
       │
       │ Set-Cookie: customer_token=<JWT>
       │             Path=/shop
       │             HttpOnly=true
       │             Secure=true (prod)
       │             SameSite=Lax
       │
       │ Response: { access_token, user }
       ▼
┌─────────────┐
│   Client    │
│             │
│ 🍪 customer_token (path=/storefront)    │
│ 💾 localStorage.token              │
└─────────────┘
```

### Cookie Path Isolation

**Critical Security Feature:**

Cookies are restricted by path to prevent cross-context authentication:

```python
# Admin cookie
response.set_cookie(
    key="admin_token",
    value=jwt_token,
    path="/admin",  # Only sent to /admin/* routes
    httponly=True,
    secure=True,
    samesite="lax"
)

# Store cookie
response.set_cookie(
    key="store_token",
    value=jwt_token,
    path="/store",  # Only sent to /store/* routes
    httponly=True,
    secure=True,
    samesite="lax"
)

# Customer cookie
response.set_cookie(
    key="customer_token",
    value=jwt_token,
    path="/storefront",  # Only sent to /storefront/* routes
    httponly=True,
    secure=True,
    samesite="lax"
)
```

**Why This Matters:**
- Admin cookies are never sent to store routes
- Store cookies are never sent to admin routes
- Customer cookies are never sent to admin/store routes
- Prevents accidental cross-context authorization

### Dual Token Storage

The system uses dual token storage for flexibility:

1. **HTTP-Only Cookie** - For page navigation (automatic)
2. **localStorage** - For API calls (manual headers)

```javascript
// Login stores both
const response = await fetch('/api/v1/store/auth/login', {
    method: 'POST',
    body: JSON.stringify({ username, password })
});

const data = await response.json();
// Cookie set automatically by server
// Store token for API calls
localStorage.setItem('token', data.access_token);

// Page navigation - cookie sent automatically
window.location.href = '/store/ACME/dashboard';

// API call - use stored token
fetch('/api/v1/store/ACME/products', {
    headers: {
        'Authorization': `Bearer ${localStorage.getItem('token')}`
    }
});
```

---

## Authorization Implementation

### FastAPI Dependencies

The system uses FastAPI dependencies for consistent authorization checks.

#### Location

All authorization dependencies are in `app/api/deps.py`.

#### Basic Authentication Dependencies

```python
from fastapi import Depends, Request
from sqlalchemy.orm import Session
from app.core.database import get_db
from models.database.user import User

# Admin authentication (cookie OR header)
def get_current_admin_from_cookie_or_header(
    request: Request,
    db: Session = Depends(get_db)
) -> User:
    """
    Get current admin user from cookie OR Authorization header.

    Checks:
    1. admin_token cookie (path=/admin)
    2. Authorization: Bearer <token> header
    3. Validates is_admin (role in: super_admin, platform_admin)

    Use for: Admin HTML pages
    """
    # Implementation checks cookie first, then header
    # Returns User object if authenticated as admin
    # Raises AdminRequiredException if not admin

# Store authentication (cookie OR header)
def get_current_store_from_cookie_or_header(
    request: Request,
    db: Session = Depends(get_db)
) -> User:
    """
    Get current store user from cookie OR Authorization header.

    Checks:
    1. store_token cookie (path=/store)
    2. Authorization: Bearer <token> header
    3. Blocks admin users (super_admin, platform_admin)
    4. Validates store membership (merchant_owner or store_member)

    Use for: Store HTML pages
    """
    # Implementation checks cookie first, then header
    # Returns User object if authenticated as store user
    # Raises InsufficientPermissionsException if admin

# API-only authentication (header required)
def get_current_admin_api(
    request: Request,
    db: Session = Depends(get_db)
) -> User:
    """
    Get current admin from Authorization header only.

    Use for: Admin API endpoints
    """

def get_current_store_api(
    request: Request,
    db: Session = Depends(get_db)
) -> User:
    """
    Get current store from Authorization header only.

    Use for: Store API endpoints
    """
```

#### Permission-Based Dependencies

```python
from app.core.permissions import StorePermissions

def require_store_permission(permission: str):
    """
    Dependency factory for requiring specific permission.

    Usage:
        @router.post("/products")
        def create_product(
            user: User = Depends(require_store_permission("products.create"))
        ):
            # User verified to have products.create permission
            ...
    """
    def permission_checker(
        request: Request,
        db: Session = Depends(get_db),
        current_user: User = Depends(get_current_store_from_cookie_or_header)
    ) -> User:
        store = request.state.store  # Set by middleware

        if not current_user.has_store_permission(store.id, permission):
            raise InsufficientStorePermissionsException(
                required_permission=permission,
                store_code=store.store_code
            )

        return current_user

    return permission_checker

def require_store_owner(
    request: Request,
    db: Session = Depends(get_db),
    current_user: User = Depends(get_current_store_from_cookie_or_header)
) -> User:
    """
    Require store owner role.

    Usage:
        @router.post("/team/invite")
        def invite_member(
            user: User = Depends(require_store_owner)
        ):
            # User verified to be store owner
            ...
    """
    store = request.state.store

    if not current_user.is_owner_of(store.id):
        raise StoreOwnerOnlyException(
            operation="team management",
            store_code=store.store_code
        )

    return current_user

def require_any_store_permission(*permissions: str):
    """
    Require ANY of the specified permissions.

    Usage:
        @router.get("/dashboard")
        def dashboard(
            user: User = Depends(require_any_store_permission(
                "dashboard.view",
                "reports.view"
            ))
        ):
            # User has at least one permission
            ...
    """

def require_all_store_permissions(*permissions: str):
    """
    Require ALL of the specified permissions.

    Usage:
        @router.post("/products/bulk-delete")
        def bulk_delete(
            user: User = Depends(require_all_store_permissions(
                "products.view",
                "products.delete"
            ))
        ):
            # User has all permissions
            ...
    """

def get_user_permissions(
    request: Request,
    current_user: User = Depends(get_current_store_from_cookie_or_header)
) -> list:
    """
    Get all permissions for current user.

    Returns list of permission strings.

    Usage:
        @router.get("/me/permissions")
        def my_permissions(
            permissions: list = Depends(get_user_permissions)
        ):
            return {"permissions": permissions}
    """
```

### Model Helper Methods

#### User Model

```python
# In models/database/user.py

class User(Base):
    # ... fields ...

    @property
    def is_super_admin(self) -> bool:
        """Check if user is super admin (computed from role)."""
        return self.role == "super_admin"

    @property
    def is_admin(self) -> bool:
        """Check if user is any type of platform admin."""
        return self.role in ("super_admin", "platform_admin")

    @property
    def is_platform_admin(self) -> bool:
        """Check if user is a (non-super) platform admin."""
        return self.role == "platform_admin"

    @property
    def is_merchant_owner(self) -> bool:
        """Check if user is a merchant owner."""
        return self.role == "merchant_owner"

    @property
    def is_store_user(self) -> bool:
        """Check if user is a store-level user (owner or member)."""
        return self.role in ("merchant_owner", "store_member")

    def is_owner_of(self, store_id: int) -> bool:
        """Check if user owns a specific store."""
        return any(v.id == store_id for v in self.owned_stores)

    def is_member_of(self, store_id: int) -> bool:
        """Check if user is member of store (owner or team)."""
        if self.is_owner_of(store_id):
            return True
        return any(
            vm.store_id == store_id and vm.is_active
            for vm in self.store_memberships
        )

    def get_store_role(self, store_id: int) -> str:
        """Get role name within specific store."""
        if self.is_owner_of(store_id):
            return "owner"

        for vm in self.store_memberships:
            if vm.store_id == store_id and vm.is_active:
                return vm.role.name if vm.role else "member"

        return None

    def has_store_permission(self, store_id: int, permission: str) -> bool:
        """Check if user has specific permission in store."""
        # Owners have all permissions
        if self.is_owner_of(store_id):
            return True

        # Check team member permissions
        for vm in self.store_memberships:
            if vm.store_id == store_id and vm.is_active:
                if vm.role and permission in vm.role.permissions:
                    return True

        return False
```

#### StoreUser Model

```python
# In models/database/store.py

class StoreUser(Base):
    # ... fields ...

    @property
    def is_owner(self) -> bool:
        """Check if this user is the store owner.
        Determined by User.role == 'merchant_owner' and Merchant.owner_user_id,
        NOT by a column on StoreUser (user_type was removed).
        """
        return self.user.is_merchant_owner and self.user.is_owner_of(self.store_id)

    @property
    def is_team_member(self) -> bool:
        """Check if this is a team member (not owner)."""
        return not self.is_owner

    @property
    def is_invitation_pending(self) -> bool:
        """Check if invitation is pending acceptance."""
        return (
            self.invitation_token is not None and
            self.invitation_accepted_at is None
        )

    def has_permission(self, permission: str) -> bool:
        """Check if this membership has specific permission."""
        # Owners have all permissions
        if self.is_owner:
            return True

        # Inactive users have no permissions
        if not self.is_active:
            return False

        # Check role permissions
        if self.role and self.role.permissions:
            return permission in self.role.permissions

        return False

    def get_all_permissions(self) -> list:
        """Get all permissions for this membership."""
        if self.is_owner:
            from app.core.permissions import StorePermissions
            return [p.value for p in StorePermissions]

        if self.role and self.role.permissions:
            return self.role.permissions

        return []
```

---

## Team Management

### Invitation Flow

The system uses email-based invitations for team member onboarding.

#### Complete Flow Diagram

```
┌──────────────────────────────────────────────────────────────┐
│                    INVITATION WORKFLOW                        │
└──────────────────────────────────────────────────────────────┘

1. Owner initiates invitation
   └─> POST /api/v1/store/{code}/team/invite
       Body: { email, role }

2. System creates/updates records
   ├─> User record (if doesn't exist)
   │   - email: from invitation
   │   - username: auto-generated
   │   - role: "store_member"
   │   - is_active: FALSE
   │   - is_email_verified: FALSE
   │
   └─> StoreUser record
       - store_id: current store
       - user_id: from User
       - role_id: from role selection
       - invitation_token: secure random string
       - invitation_sent_at: now()
       - invited_by: current user
       - is_active: FALSE

3. Email sent to invitee
   └─> Contains: invitation link with token
       Link: /store/invitation/accept?token={invitation_token}

4. Invitee clicks link
   └─> GET /store/invitation/accept?token={token}
       Displays form: password, first_name, last_name

5. Invitee submits form
   └─> POST /api/v1/store/team/accept-invitation
       Body: { invitation_token, password, first_name, last_name }

6. System activates account
   ├─> User updates:
   │   - hashed_password: from form
   │   - first_name, last_name: from form
   │   - is_active: TRUE
   │   - is_email_verified: TRUE
   │
   └─> StoreUser updates:
       - is_active: TRUE
       - invitation_accepted_at: now()
       - invitation_token: NULL (cleared)

7. Member can now login
   └─> POST /api/v1/store/auth/login
       Redirect to store dashboard
```

### Service Layer Implementation

Team management is handled by `StoreTeamService` in `app/services/store_team_service.py`.

#### Key Methods

```python
class StoreTeamService:

    def invite_team_member(
        self,
        db: Session,
        store: Store,
        inviter: User,
        email: str,
        role_name: str,
        custom_permissions: Optional[List[str]] = None
    ) -> Dict[str, Any]:
        """
        Invite a new team member.

        Steps:
        1. Check team size limits
        2. Find or create User account
        3. Create or update StoreUser with invitation
        4. Generate secure invitation token
        5. Send invitation email

        Returns:
            {
                "invitation_token": str,
                "email": str,
                "role": str,
                "existing_user": bool
            }
        """

    def accept_invitation(
        self,
        db: Session,
        invitation_token: str,
        password: str,
        first_name: Optional[str] = None,
        last_name: Optional[str] = None
    ) -> Dict[str, Any]:
        """
        Accept team invitation and activate account.

        Steps:
        1. Validate invitation token
        2. Check token not expired (7 days)
        3. Update User (password, name, active status)
        4. Update StoreUser (active, accepted timestamp)
        5. Clear invitation token

        Returns:
            {
                "user": User,
                "store": Store,
                "role": str
            }
        """

    def remove_team_member(
        self,
        db: Session,
        store: Store,
        user_id: int
    ) -> bool:
        """
        Remove team member (soft delete).

        Cannot remove owner.
        Sets StoreUser.is_active = False
        """

    def update_member_role(
        self,
        db: Session,
        store: Store,
        user_id: int,
        new_role_name: str,
        custom_permissions: Optional[List[str]] = None
    ) -> StoreUser:
        """
        Update team member's role.

        Cannot change owner's role.
        Creates new role if custom permissions provided.
        """

    def get_team_members(
        self,
        db: Session,
        store: Store,
        include_inactive: bool = False
    ) -> List[Dict[str, Any]]:
        """
        Get all team members for a store.

        Returns list of member info including:
        - Basic user info
        - Role and permissions
        - Invitation status
        - Active status
        """
```

### API Routes

Complete team management routes in `app/api/v1/store/team.py`.

```python
router = APIRouter(prefix="/team")

# List team members
@router.get("/members")
def list_team_members(
    request: Request,
    user: User = Depends(require_store_permission("team.view"))
):
    """List all team members."""

# Invite team member (owner only)
@router.post("/invite")
def invite_team_member(
    invitation: InviteTeamMemberRequest,
    user: User = Depends(require_store_owner)
):
    """Send team invitation email."""

# Accept invitation (public, no auth)
@router.post("/accept-invitation")
def accept_invitation(
    acceptance: AcceptInvitationRequest
):
    """Accept invitation and activate account."""

# Remove team member (owner only)
@router.delete("/members/{user_id}")
def remove_team_member(
    user_id: int,
    user: User = Depends(require_store_owner)
):
    """Remove team member from store."""

# Update member role (owner only)
@router.put("/members/{user_id}/role")
def update_member_role(
    user_id: int,
    role_update: UpdateMemberRoleRequest,
    user: User = Depends(require_store_owner)
):
    """Change team member's role."""

# Get current user's permissions
@router.get("/me/permissions")
def get_my_permissions(
    permissions: list = Depends(get_user_permissions)
):
    """Get current user's permission list."""
```

### Security Considerations

#### Owner Protection

Owners cannot be removed or have their role changed:

```python
# In remove_team_member
if store_user.is_owner:
    raise CannotRemoveStoreOwnerException(store.store_code)

# In update_member_role
if store_user.is_owner:
    raise CannotRemoveStoreOwnerException(store.store_code)
```

#### Invitation Token Security

- Tokens are 32-byte cryptographically secure random strings
- Single-use (cleared after acceptance)
- Expire after 7 days
- Unique per invitation

```python
def _generate_invitation_token(self) -> str:
    """Generate secure invitation token."""
    import secrets
    return secrets.token_urlsafe(32)
```

#### Admin Blocking

Platform admins are blocked from store routes:

```python
# In store auth endpoint
if current_user.is_admin:  # role in ("super_admin", "platform_admin")
    raise InvalidCredentialsException(
        "Admins cannot access store portal"
    )

# In store dependencies
if current_user.is_admin:
    raise InsufficientPermissionsException(
        "Store access only"
    )
```

---

## Code Examples

### Example 1: Protected Route with Permission Check

```python
from fastapi import APIRouter, Depends
from app.api.deps import require_store_permission
from app.core.permissions import StorePermissions
from models.database.user import User

router = APIRouter()

@router.post("/products")
def create_product(
    product_data: ProductCreate,
    user: User = Depends(require_store_permission(
        StorePermissions.PRODUCTS_CREATE.value
    ))
):
    """
    Create a new product.

    Requires: products.create permission

    The dependency automatically:
    1. Authenticates the user
    2. Gets store from request.state
    3. Checks user has products.create permission
    4. Returns User if authorized
    5. Raises InsufficientStorePermissionsException if not
    """
    store = request.state.store

    # User is authenticated and authorized
    # Proceed with business logic
    product = product_service.create(
        db=db,
        store_id=store.id,
        user_id=user.id,
        data=product_data
    )

    return {"product": product}
```

### Example 2: Owner-Only Route

```python
from app.api.deps import require_store_owner

@router.delete("/team/members/{member_id}")
def remove_team_member(
    member_id: int,
    user: User = Depends(require_store_owner)
):
    """
    Remove a team member.

    Requires: Store owner role

    The dependency automatically:
    1. Authenticates the user
    2. Checks user is owner of current store
    3. Returns User if owner
    4. Raises StoreOwnerOnlyException if not owner
    """
    store = request.state.store

    # User is verified owner
    store_team_service.remove_team_member(
        db=db,
        store=store,
        user_id=member_id
    )

    return {"message": "Member removed"}
```

### Example 3: Multi-Permission Route

```python
from app.api.deps import require_all_store_permissions

@router.post("/store/{code}/products/bulk-import")
def bulk_import_products(
    file: UploadFile,
    user: User = Depends(require_all_store_permissions(
        StorePermissions.PRODUCTS_VIEW.value,
        StorePermissions.PRODUCTS_CREATE.value,
        StorePermissions.PRODUCTS_IMPORT.value
    ))
):
    """
    Bulk import products from CSV.

    Requires ALL of:
    - products.view
    - products.create
    - products.import

    The dependency checks user has ALL specified permissions.
    """
    store = request.state.store

    # User has all required permissions
    result = import_service.process_csv(
        db=db,
        store_id=store.id,
        file=file
    )

    return {"imported": result.success_count}
```

### Example 4: Frontend Permission Checking

```javascript
// On login, fetch user's permissions
async function login(username, password) {
    const response = await fetch('/api/v1/store/auth/login', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({ username, password })
    });

    const data = await response.json();

    // Store token
    localStorage.setItem('token', data.access_token);

    // Fetch permissions
    const permResponse = await fetch('/api/v1/store/team/me/permissions', {
        headers: {
            'Authorization': `Bearer ${data.access_token}`
        }
    });

    const { permissions } = await permResponse.json();

    // Store permissions
    localStorage.setItem('permissions', JSON.stringify(permissions));

    // Navigate
    window.location.href = '/store/dashboard';
}

// Check permission before showing UI element
function canCreateProducts() {
    const permissions = JSON.parse(localStorage.getItem('permissions') || '[]');
    return permissions.includes('products.create');
}

// In React/Alpine.js component
{canCreateProducts() && (
    <button onClick={handleCreateProduct}>
        Create Product
    </button>
)}

// Disable button if no permission
<button
    disabled={!canCreateProducts()}
    onClick={handleCreateProduct}
>
    Create Product
</button>
```

---

## Best Practices

### 1. Route-Level Authorization

**✅ DO: Check permissions at route level using dependencies**

```python
@router.post("/products")
def create_product(
    data: ProductCreate,
    user: User = Depends(require_store_permission("products.create"))
):
    # Permission already verified
    return product_service.create(data)
```

**❌ DON'T: Check permissions in service layer**

```python
# BAD
def create_product(db: Session, user: User, data: ProductCreate):
    if not user.has_permission("products.create"):
        raise Exception("No permission")
    # ...
```

### 2. Use Type-Safe Permission Constants

**✅ DO: Use StorePermissions enum**

```python
from app.core.permissions import StorePermissions

require_store_permission(StorePermissions.PRODUCTS_CREATE.value)
```

**❌ DON'T: Use magic strings**

```python
# BAD - typos won't be caught
require_store_permission("products.creat")  # Typo!
```

### 3. Owner Permission Bypass

**✅ DO: Let owners bypass permission checks automatically**

```python
def has_permission(self, permission: str) -> bool:
    if self.is_owner:
        return True  # Owners have all permissions
    # Check role permissions...
```

**❌ DON'T: Explicitly check owner in every route**

```python
# BAD - redundant
if not user.is_owner and not user.has_permission("products.create"):
    raise Exception()
```

### 4. Service Layer Design

**✅ DO: Keep services authorization-agnostic**

```python
# Service assumes caller is authorized
def create_product(db: Session, store_id: int, data: dict) -> Product:
    product = Product(store_id=store_id, **data)
    db.add(product)
    db.commit()
    return product
```

**❌ DON'T: Mix authorization into services**

```python
# BAD
def create_product(db: Session, user: User, data: dict):
    if not user.is_active:
        raise Exception()
    # ...
```

### 5. Frontend Permission Checks

**✅ DO: Hide/disable UI elements without permission**

```javascript
// Hide button if no permission
{hasPermission('products.delete') && (
    <DeleteButton />
)}

// Disable button if no permission
<button disabled={!hasPermission('products.edit')}>
    Edit
</button>
```

**❌ DON'T: Rely only on frontend checks**

Backend MUST always verify permissions. Frontend checks are for UX only.

---

## Testing Guidelines

### Unit Tests

Test permission logic in isolation.

```python
# tests/unit/test_permissions.py

def test_owner_has_all_permissions():
    """Merchant owners have all permissions automatically."""
    user = create_user(role="merchant_owner")
    store = create_store(owner=user)
    store_user = create_store_user(
        user=user,
        store=store
    )

    assert store_user.has_permission("products.create")
    assert store_user.has_permission("orders.delete")
    assert store_user.has_permission("team.invite")
    # All permissions should return True

def test_team_member_respects_role():
    """Store members have only their role's permissions."""
    user = create_user(role="store_member")
    store = create_store()
    role = create_role(
        store=store,
        name="Staff",
        permissions=["products.view", "products.create"]
    )
    store_user = create_store_user(
        user=user,
        store=store,
        role=role
    )

    assert store_user.has_permission("products.view")
    assert store_user.has_permission("products.create")
    assert not store_user.has_permission("products.delete")
    assert not store_user.has_permission("team.invite")

def test_inactive_user_has_no_permissions():
    """Inactive users have no permissions."""
    user = create_user()
    store = create_store()
    role = create_role(
        store=store,
        permissions=["products.view"]
    )
    store_user = create_store_user(
        user=user,
        store=store,
        role=role,
        is_active=False  # Inactive
    )

    assert not store_user.has_permission("products.view")
```

### Integration Tests

Test full request/response cycles with authentication.

```python
# tests/integration/test_product_routes.py

def test_create_product_with_permission(client, auth_headers):
    """Authenticated user with permission can create product."""
    # Setup: Create user with products.create permission
    user = create_store_team_member(
        permissions=["products.create"]
    )
    token = create_auth_token(user)

    # Request
    response = client.post(
        "/api/v1/store/ACME/products",
        json={"name": "Test Product", "price": 9.99},
        headers={"Authorization": f"Bearer {token}"}
    )

    # Assert
    assert response.status_code == 201
    assert response.json()["name"] == "Test Product"

def test_create_product_without_permission(client):
    """User without permission cannot create product."""
    # Setup: Create user WITHOUT products.create
    user = create_store_team_member(
        permissions=["products.view"]  # Can view but not create
    )
    token = create_auth_token(user)

    # Request
    response = client.post(
        "/api/v1/store/ACME/products",
        json={"name": "Test Product"},
        headers={"Authorization": f"Bearer {token}"}
    )

    # Assert
    assert response.status_code == 403
    assert "INSUFFICIENT_STORE_PERMISSIONS" in response.json()["error_code"]

def test_owner_bypasses_permission_check(client):
    """Store owner can create products without explicit permission."""
    # Setup: Create owner (no specific role)
    user, store = create_store_with_owner()
    token = create_auth_token(user)

    # Request
    response = client.post(
        f"/api/v1/store/{store.store_code}/products",
        json={"name": "Test Product"},
        headers={"Authorization": f"Bearer {token}"}
    )

    # Assert - Owner can create even without explicit permission
    assert response.status_code == 201

def test_admin_blocked_from_store_route(client):
    """Admins cannot access store routes."""
    # Setup: Create admin user
    admin = create_admin_user()
    token = create_auth_token(admin)

    # Request
    response = client.get(
        "/api/v1/store/ACME/products",
        headers={"Authorization": f"Bearer {token}"}
    )

    # Assert
    assert response.status_code == 403
    assert "INSUFFICIENT_PERMISSIONS" in response.json()["error_code"]
```

---

## Troubleshooting

### Common Issues

#### Issue: "INVALID_TOKEN" error

**Symptoms:**
- API calls return 401 Unauthorized
- Error message: "Invalid token"

**Causes:**
1. Token expired (default 30 minutes)
2. Token malformed
3. Token signature invalid

**Solutions:**
```python
# Check token expiry
import jwt
token = "eyJ0eXAi..."
decoded = jwt.decode(token, verify=False)
print(decoded['exp'])  # Unix timestamp

# Compare with current time
import time
if decoded['exp'] < time.time():
    print("Token expired - user needs to re-login")

# Verify token manually
from middleware.auth import AuthManager
auth = AuthManager()
try:
    user_data = auth.verify_token(token)
    print("Token valid")
except Exception as e:
    print(f"Token invalid: {e}")
```

#### Issue: User can't access route despite having permission

**Symptoms:**
- Route returns 403 Forbidden
- User believes they have required permission

**Debug Steps:**

```python
# 1. Check user's actual permissions
user = db.query(User).get(user_id)
store = db.query(Store).get(store_id)

print(f"Is owner? {user.is_owner_of(store.id)}")

if not user.is_owner_of(store.id):
    # Get team membership
    store_user = db.query(StoreUser).filter_by(
        user_id=user.id,
        store_id=store.id
    ).first()

    print(f"Has membership? {store_user is not None}")
    print(f"Is active? {store_user.is_active if store_user else 'N/A'}")
    print(f"Role: {store_user.role.name if store_user and store_user.role else 'N/A'}")
    print(f"Permissions: {store_user.role.permissions if store_user and store_user.role else []}")

# 2. Check specific permission
permission = "products.create"
has_perm = user.has_store_permission(store.id, permission)
print(f"Has {permission}? {has_perm}")
```

#### Issue: Admin can't access store routes

**Symptoms:**
- Admin user gets 403 on store routes

**This is intentional!** Admins are blocked from store routes for security.

**Solutions:**
1. Create separate store account for store management
2. Have admin create store, then use store owner account

```python
# Admin workflow
# 1. Admin creates store (from admin portal)
# 2. System creates store owner user automatically
# 3. Admin logs out of admin portal
# 4. Store owner logs into store portal
```

---

## Quick Reference

See [RBAC Quick Reference](../backend/rbac-quick-reference.md) for a condensed cheat sheet of common imports, route patterns, and permission constants.

---

## Related Documentation

- [Authentication System](authentication.md) - JWT authentication implementation
- [Architecture Overview](../architecture/auth-rbac.md) - System-wide authentication and RBAC
- [Backend Development](../backend/overview.md) - Backend development guide
- [API Reference](../backend/middleware-reference.md) - Auto-generated API documentation

---

**Document Version:** 2.0
**Last Updated:** February 2026
**Maintained By:** Backend Team
