# Store RBAC System - Complete Guide

## Overview

The store dashboard implements a **Role-Based Access Control (RBAC)** system that distinguishes between **Owners** and **Team Members**, with granular permissions for team members.

---

## User Types

### 1. Merchant Owner

**Who:** The user who created/owns the store account.

**Characteristics:**
- Has **ALL permissions** automatically (no role needed)
- Cannot be removed or have permissions restricted
- Can invite team members
- Can create and manage roles
- Identified by `User.role = "merchant_owner"` and `Merchant.owner_user_id`
- `User.is_merchant_owner` property returns `True`
- `User.is_store_user` property returns `True`
- Ownership checked via `User.is_owner_of(store_id)`

**Database:**
```python
# User record for merchant owner
{
    "id": 5,
    "role": "merchant_owner",    # Role on User model
}

# StoreUser record for owner
{
    "store_id": 1,
    "user_id": 5,
    "role_id": None,             # No role needed (owner has all perms)
    "is_active": True
}

# Merchant record links ownership
{
    "owner_user_id": 5           # Links to User.id
}
```

**Note:** The `user_type` column was removed from StoreUser. Ownership is determined by `User.role == "merchant_owner"` and `Merchant.owner_user_id`, not by a field on StoreUser.

**Permissions:**
- All 75 permissions (complete access)
- See full list below

---

### 2. Store Members (Team Members)

**Who:** Users invited by the merchant owner to help manage the store.

**Characteristics:**
- Have **limited permissions** based on assigned role
- Must be invited via email
- Invitation must be accepted before activation
- Can be assigned one of the pre-defined roles or custom role
- Identified by `User.role = "store_member"`
- `User.is_store_user` property returns `True`
- Permissions come from `StoreUser.role_id -> Role.permissions`

**Database:**
```python
# User record for store member
{
    "id": 7,
    "role": "store_member",      # Role on User model
}

# StoreUser record for team member
{
    "store_id": 1,
    "user_id": 7,
    "role_id": 3,                # Role required for permission lookup
    "is_active": True,
    "invitation_token": None,    # Accepted
    "invitation_accepted_at": "2024-11-15 10:30:00"
}

# Role record
{
    "id": 3,
    "store_id": 1,
    "name": "Manager",
    "permissions": [
        "dashboard.view",
        "products.view",
        "products.create",
        "products.edit",
        "orders.view",
        ...
    ]
}
```

**Permissions:**
- Limited based on assigned role
- Can have between 0 and 75 permissions
- Common roles: Manager, Staff, Support, Viewer, Marketing

---

## Permission System

### All Available Permissions (75 total)

```python
class StorePermissions(str, Enum):
    # Dashboard (1)
    DASHBOARD_VIEW = "dashboard.view"

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

    # Stock/Inventory (3)
    STOCK_VIEW = "stock.view"
    STOCK_EDIT = "stock.edit"
    STOCK_TRANSFER = "stock.transfer"

    # Orders (4)
    ORDERS_VIEW = "orders.view"
    ORDERS_EDIT = "orders.edit"
    ORDERS_CANCEL = "orders.cancel"
    ORDERS_REFUND = "orders.refund"

    # Customers (4)
    CUSTOMERS_VIEW = "customers.view"
    CUSTOMERS_EDIT = "customers.edit"
    CUSTOMERS_DELETE = "customers.delete"
    CUSTOMERS_EXPORT = "customers.export"

    # Marketing (3)
    MARKETING_VIEW = "marketing.view"
    MARKETING_CREATE = "marketing.create"
    MARKETING_SEND = "marketing.send"

    # Reports (3)
    REPORTS_VIEW = "reports.view"
    REPORTS_FINANCIAL = "reports.financial"
    REPORTS_EXPORT = "reports.export"

    # Settings (4)
    SETTINGS_VIEW = "settings.view"
    SETTINGS_EDIT = "settings.edit"
    SETTINGS_THEME = "settings.theme"
    SETTINGS_DOMAINS = "settings.domains"

    # Team Management (4)
    TEAM_VIEW = "team.view"
    TEAM_INVITE = "team.invite"
    TEAM_EDIT = "team.edit"
    TEAM_REMOVE = "team.remove"

    # Marketplace Imports (3)
    IMPORTS_VIEW = "imports.view"
    IMPORTS_CREATE = "imports.create"
    IMPORTS_CANCEL = "imports.cancel"
```

---

## Pre-Defined Roles

### 1. Owner (All 75 permissions)
**Use case:** Store owner (automatically assigned)
- Full access to everything
- Cannot be restricted
- No role record needed (permissions checked differently)

---

### 2. Manager (43 permissions)
**Use case:** Senior staff who manage most operations

**Has access to:**
- Dashboard, Products (all), Stock (all)
- Orders (all), Customers (view, edit, export)
- Marketing (all), Reports (all including financial)
- Settings (view, theme)
- Imports (all)

**Does NOT have:**
- `customers.delete` - Cannot delete customers
- `settings.edit` - Cannot change core settings
- `settings.domains` - Cannot manage domains
- `team.*` - Cannot manage team members

---

### 3. Staff (10 permissions)
**Use case:** Daily operations staff

**Has access to:**
- Dashboard view
- Products (view, create, edit)
- Stock (view, edit)
- Orders (view, edit)
- Customers (view, edit)

---

### 4. Support (6 permissions)
**Use case:** Customer support team

**Has access to:**
- Dashboard view
- Products (view only)
- Orders (view, edit)
- Customers (view, edit)

---

### 5. Viewer (6 permissions)
**Use case:** Read-only access for reporting/audit

**Has access to:**
- Dashboard (view), Products (view), Stock (view)
- Orders (view), Customers (view), Reports (view)

---

### 6. Marketing (7 permissions)
**Use case:** Marketing team focused on campaigns

**Has access to:**
- Dashboard (view), Customers (view, export)
- Marketing (all), Reports (view)

---

## Permission Checking Logic

### How Permissions Are Checked

```python
# In User model (models/database/user.py)

def has_store_permission(self, store_id: int, permission: str) -> bool:
    """Check if user has a specific permission in a store."""

    # Step 1: Check if user is owner
    if self.is_owner_of(store_id):
        return True  # Owners have ALL permissions

    # Step 2: 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  # Permission found in role

    # No permission found
    return False
```

### Permission Checking Flow

```
Request → Middleware → Extract store from URL
                    ↓
              Check user authentication
                    ↓
              Check if user is owner
                    ├── YES → Allow (all permissions)
                    └── NO ↓
              Check if user is team member
                    ├── NO → Deny
                    └── YES ↓
              Check if membership is active
                    ├── NO → Deny
                    └── YES ↓
              Check if role has required permission
                    ├── NO → Deny (403 Forbidden)
                    └── YES → Allow
```

---

## Using Permissions in Code

### 1. Require Specific Permission

**When to use:** Endpoint needs one specific permission

```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 product...
    pass
```

---

### 2. Require ANY Permission

**When to use:** Endpoint can be accessed with any of several permissions

```python
@router.get("/dashboard")
def view_dashboard(
    user: User = Depends(
        require_any_store_permission(
            StorePermissions.DASHBOARD_VIEW.value,
            StorePermissions.REPORTS_VIEW.value
        )
    )
):
    # Show dashboard...
    pass
```

---

### 3. Require ALL Permissions

**When to use:** Endpoint needs multiple permissions

```python
@router.post("/products/bulk-delete")
def bulk_delete_products(
    user: User = Depends(
        require_all_store_permissions(
            StorePermissions.PRODUCTS_VIEW.value,
            StorePermissions.PRODUCTS_DELETE.value
        )
    )
):
    # Delete products...
    pass
```

---

### 4. Require Owner Only

**When to use:** Endpoint is owner-only (team management, critical settings)

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

@router.post("/team/invite")
def invite_team_member(
    email: str,
    role_id: int,
    user: User = Depends(require_store_owner)
):
    # Invite team member...
    pass
```

---

### 5. Get User Permissions

**When to use:** Need to check permissions in business logic

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

@router.get("/my-permissions")
def list_my_permissions(
    permissions: list = Depends(get_user_permissions)
):
    return {"permissions": permissions}
```

---

## Database Schema

### StoreUser Table

```sql
CREATE TABLE store_users (
    id SERIAL PRIMARY KEY,
    store_id INTEGER NOT NULL REFERENCES stores(id),
    user_id INTEGER NOT NULL REFERENCES users(id),
    role_id INTEGER REFERENCES roles(id), -- NULL for merchant owners
    invited_by INTEGER REFERENCES users(id),
    invitation_token VARCHAR,
    invitation_sent_at TIMESTAMP,
    invitation_accepted_at TIMESTAMP,
    is_active BOOLEAN DEFAULT FALSE,
    created_at TIMESTAMP DEFAULT NOW(),
    updated_at TIMESTAMP DEFAULT NOW()
);
-- Note: user_type column removed. Ownership determined by
-- User.role = 'merchant_owner' and Merchant.owner_user_id.
```

### Role Table

```sql
CREATE TABLE roles (
    id SERIAL PRIMARY KEY,
    store_id INTEGER NOT NULL REFERENCES stores(id),
    name VARCHAR(100) NOT NULL,
    permissions JSON DEFAULT '[]',       -- Array of permission strings
    created_at TIMESTAMP DEFAULT NOW(),
    updated_at TIMESTAMP DEFAULT NOW()
);
```

---

## Team Member Lifecycle

### 1. Invitation

```
Merchant owner invites user:
  → User created with role="store_member"
  → StoreUser created:
{
    "role_id": 3,                        -- Assigned role
    "is_active": False,
    "invitation_token": "abc123...",
    "invitation_sent_at": "2024-11-29 10:00:00",
    "invitation_accepted_at": null
}
```

### 2. Acceptance

```
User accepts invitation → StoreUser updated:
{
    "is_active": True,
    "invitation_token": null,
    "invitation_accepted_at": "2024-11-29 10:30:00"
}
```

### 3. Deactivation

```
Owner deactivates member → StoreUser updated:
{
    "is_active": False
}
```

---

## Custom Role Management

### Overview

Store owners can create, edit, and delete custom roles with granular permission selection. Preset roles (manager, staff, support, viewer, marketing) cannot be deleted but can be edited.

### Store Role CRUD API

All endpoints require **store owner** authentication.

| Method | Endpoint | Description |
|--------|----------|-------------|
| `GET` | `/api/v1/store/team/roles` | List all roles (creates defaults if none exist) |
| `POST` | `/api/v1/store/team/roles` | Create a custom role |
| `PUT` | `/api/v1/store/team/roles/{id}` | Update a role's name/permissions |
| `DELETE` | `/api/v1/store/team/roles/{id}` | Delete a custom role |

**Validation rules:**
- Cannot create/rename a role to a preset name (manager, staff, etc.)
- Cannot delete preset roles
- Cannot delete a role with assigned team members
- All permission IDs are validated against the module-discovered permission catalog

### Permission Catalog API

Returns all available permissions grouped by category, with human-readable labels and descriptions.

```
GET /api/v1/store/team/permissions/catalog
```

**Required Permission:** `team.view`

**Response:**
```json
{
  "categories": [
    {
      "id": "team",
      "label": "tenancy.permissions.category.team",
      "permissions": [
        {
          "id": "team.view",
          "label": "tenancy.permissions.team_view",
          "description": "tenancy.permissions.team_view_desc",
          "is_owner_only": false
        }
      ]
    }
  ]
}
```

Permissions are discovered from all module `definition.py` files via `PermissionDiscoveryService.get_permissions_by_category()`.

### Role Editor UI

The role editor page at `/store/{store_code}/team/roles`:
- Lists all roles (preset + custom) with permission counts
- Modal for creating/editing roles with a permission matrix
- Permissions displayed with labels, IDs, and hover descriptions
- Category-level "Select All / Deselect All" toggles
- Owner-only permissions marked with an "Owner" badge

**Alpine.js component:** `storeRoles()` in `app/modules/tenancy/static/store/js/roles.js`

**Menu location:** Account section > Roles (requires `team.view` permission)

---

## Admin Store Roles Management

### Overview

Super admins and platform admins can manage roles for any store via the admin panel. Platform admins are scoped to stores within their assigned platforms.

### Admin Role CRUD API

All endpoints require **admin authentication** (`get_current_admin_api`).

| Method | Endpoint | Description |
|--------|----------|-------------|
| `GET` | `/api/v1/admin/store-roles?store_id=X` | List roles for a store |
| `GET` | `/api/v1/admin/store-roles/permissions/catalog` | Permission catalog |
| `POST` | `/api/v1/admin/store-roles?store_id=X` | Create a role |
| `PUT` | `/api/v1/admin/store-roles/{id}?store_id=X` | Update a role |
| `DELETE` | `/api/v1/admin/store-roles/{id}?store_id=X` | Delete a role |

### Platform Admin Scoping

Platform admins can only access stores that belong to one of their assigned platforms:

```python
# In StoreTeamService.validate_admin_store_access():
# 1. Super admin (accessible_platform_ids is None) → access all stores
# 2. Platform admin → store must exist in StorePlatform where
#    platform_id is in the admin's accessible_platform_ids
```

The scoping is enforced at the service layer via `validate_admin_store_access()`, called by every admin endpoint before performing operations.

### Admin UI

Page at `/admin/store-roles`:
- Tom Select store search/selector (shared `initStoreSelector()` component)
- Platform admins see only stores in their assigned platforms
- Same role CRUD and permission matrix as the store-side UI
- Located in the "User Management" admin menu section

**Alpine.js component:** `adminStoreRoles()` in `app/modules/tenancy/static/admin/js/store-roles.js`

### Audit Trail

All role operations are logged via `AuditAggregatorService`:

| Action | Description |
|--------|-------------|
| `role.create` | Custom role created |
| `role.update` | Role name or permissions modified |
| `role.delete` | Custom role deleted |
| `member.role_change` | Team member assigned a different role |
| `member.invite` | Team member invited |
| `member.remove` | Team member removed |

---

## Error Responses

### Missing Permission

```http
HTTP 403 Forbidden

{
    "error_code": "INSUFFICIENT_STORE_PERMISSIONS",
    "message": "You don't have permission to perform this action",
    "details": {
        "required_permission": "products.delete",
        "store_code": "orion"
    }
}
```

### Not Owner

```http
HTTP 403 Forbidden

{
    "error_code": "STORE_OWNER_ONLY",
    "message": "This operation requires store owner privileges",
    "details": {
        "operation": "team management",
        "store_code": "orion"
    }
}
```

### Inactive Membership

```http
HTTP 403 Forbidden

{
    "error_code": "INACTIVE_STORE_MEMBERSHIP",
    "message": "Your store membership is inactive"
}
```

---

## Summary

### Merchant Owner vs Store Member

| Feature | Merchant Owner (`role="merchant_owner"`) | Store Member (`role="store_member"`) |
|---------|-------|-------------|
| **Permissions** | All 75 (automatic) | Based on role (0-75) |
| **Role Required** | No | Yes |
| **Can Be Removed** | No | Yes |
| **Team Management** | Yes | No |
| **Critical Settings** | Yes | No (usually) |
| **Invitation Required** | No (creates store) | Yes |
| **Ownership Determined By** | `Merchant.owner_user_id` | N/A |

### Permission Hierarchy

```
Merchant Owner (75 permissions)
  └─ Manager (43 permissions)
      └─ Staff (10 permissions)
          └─ Support (6 permissions)
              └─ Viewer (6 permissions, read-only)

Marketing (7 permissions, specialized)
```

### Best Practices

1. **Use Constants:** Always use `StorePermissions.PERMISSION_NAME.value`
2. **Least Privilege:** Give team members minimum permissions needed
3. **Owner Only:** Keep sensitive operations owner-only
4. **Custom Roles:** Create custom roles for specific needs
5. **Regular Audit:** Review team member permissions regularly

---

## Key Files

| File | Purpose |
|------|---------|
| `app/modules/tenancy/services/store_team_service.py` | Role CRUD, platform scoping, audit trail |
| `app/modules/tenancy/services/permission_discovery_service.py` | Permission catalog, role presets |
| `app/modules/tenancy/routes/api/store_team.py` | Store team & role API endpoints |
| `app/modules/tenancy/routes/api/admin_store_roles.py` | Admin store role API endpoints |
| `app/modules/tenancy/schemas/team.py` | Request/response schemas |
| `app/modules/tenancy/static/store/js/roles.js` | Store role editor Alpine.js component |
| `app/modules/tenancy/static/admin/js/store-roles.js` | Admin role editor Alpine.js component |
| `app/modules/tenancy/templates/tenancy/store/roles.html` | Store role editor template |
| `app/modules/tenancy/templates/tenancy/admin/store-roles.html` | Admin role editor template |
