feat: RBAC Phase 1 — consolidate user roles into 4-value enum
Some checks failed
Some checks failed
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 <noreply@anthropic.com>
This commit is contained in:
@@ -5,16 +5,32 @@
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────────┐
|
||||
│ PLATFORM LEVEL │
|
||||
│ User.role (4-value enum) │
|
||||
│ │
|
||||
│ ┌──────────────────┐ ┌──────────────────┐ │
|
||||
│ │ Admin Users │ │ Store Users │ │
|
||||
│ │ role="admin" │ │ role="store" │ │
|
||||
│ │ Super Admin │ │ Platform Admin │ │
|
||||
│ │ role= │ │ role= │ │
|
||||
│ │ "super_admin" │ │ "platform_admin"│ │
|
||||
│ │ │ │ │ │
|
||||
│ │ • Full platform │ │ • Can own/join │ │
|
||||
│ │ access │ │ stores │ │
|
||||
│ │ • Full platform │ │ • Scoped to │ │
|
||||
│ │ access │ │ assigned │ │
|
||||
│ │ • All platforms │ │ platforms │ │
|
||||
│ │ • Cannot access │ │ • Cannot access │ │
|
||||
│ │ store portal │ │ admin portal │ │
|
||||
│ │ store portal │ │ store portal │ │
|
||||
│ └──────────────────┘ └──────────────────┘ │
|
||||
│ │
|
||||
│ ┌──────────────────┐ ┌──────────────────┐ │
|
||||
│ │ Merchant Owner │ │ Store Member │ │
|
||||
│ │ role= │ │ role= │ │
|
||||
│ │ "merchant_owner"│ │ "store_member" │ │
|
||||
│ │ │ │ │ │
|
||||
│ │ • Owns stores │ │ • Invited to │ │
|
||||
│ │ • All perms in │ │ stores │ │
|
||||
│ │ own stores │ │ • Role-based │ │
|
||||
│ │ • Cannot access │ │ permissions │ │
|
||||
│ │ admin portal │ │ • Cannot access │ │
|
||||
│ └──────────────────┘ │ admin portal │ │
|
||||
│ └──────────────────┘ │
|
||||
│ │ │
|
||||
└──────────────────────────────────────────────┼──────────────────┘
|
||||
│
|
||||
@@ -27,17 +43,18 @@
|
||||
│ │ │ │
|
||||
│ │ ┌──────────────┐ ┌──────────────────────┐ │ │
|
||||
│ │ │ Owner │ │ Team Members │ │ │
|
||||
│ │ │ user_type= │ │ user_type= │ │ │
|
||||
│ │ │ "owner" │ │ "member" │ │ │
|
||||
│ │ │ │ │ │ │ │
|
||||
│ │ │ • All perms │ │ • Role-based perms │ │ │
|
||||
│ │ │ • Can invite │ │ • Manager/Staff/etc │ │ │
|
||||
│ │ │ • Can remove │ │ • Can be invited │ │ │
|
||||
│ │ │ • Cannot be │ │ • Can be removed │ │ │
|
||||
│ │ │ role= │ │ role= │ │ │
|
||||
│ │ │ "merchant_ │ │ "store_member" │ │ │
|
||||
│ │ │ owner" │ │ │ │ │
|
||||
│ │ │ │ │ • Role-based perms │ │ │
|
||||
│ │ │ • All perms │ │ • Manager/Staff/etc │ │ │
|
||||
│ │ │ • Can invite │ │ • Can be invited │ │ │
|
||||
│ │ │ • Can remove │ │ • Can be removed │ │ │
|
||||
│ │ │ • Cannot be │ │ │ │ │
|
||||
│ │ │ removed │ │ │ │ │
|
||||
│ │ └──────────────┘ └──────────────────────┘ │ │
|
||||
│ │ │ │ │
|
||||
│ │ ▼ │ │
|
||||
│ │ Ownership via Merchant.owner_user_id ▼ │ │
|
||||
│ │ ┌──────────────────────────────┐ │ │
|
||||
│ │ │ Roles │ │ │
|
||||
│ │ │ │ │ │
|
||||
@@ -180,8 +197,12 @@
|
||||
│ id (PK) │◄────┐
|
||||
│ email │ │
|
||||
│ role │ │
|
||||
│ ('admin' or │ │
|
||||
│ 'store') │ │
|
||||
│ ('super_admin', │ │
|
||||
│ 'platform_ │ │
|
||||
│ admin', │ │
|
||||
│ 'merchant_ │ │
|
||||
│ owner', │ │
|
||||
│ 'store_member') │ │
|
||||
└──────────────────┘ │
|
||||
│ │
|
||||
│ owner_user_id │
|
||||
@@ -204,12 +225,12 @@
|
||||
│ store_id (FK) │ │ store_id (FK) │
|
||||
│ user_id (FK) │ │ name │
|
||||
│ role_id (FK) ───┼────►│ permissions │
|
||||
│ user_type │ │ (JSON) │
|
||||
│ ('owner' or │ └──────────────────┘
|
||||
│ 'member') │
|
||||
│ invitation_* │
|
||||
│ is_active │
|
||||
│ invitation_* │ │ (JSON) │
|
||||
│ is_active │ └──────────────────┘
|
||||
└──────────────────┘
|
||||
(no user_type column;
|
||||
ownership via
|
||||
Merchant.owner_user_id)
|
||||
|
||||
Separate hierarchy:
|
||||
|
||||
@@ -302,12 +323,14 @@ Examples:
|
||||
## Security Boundaries
|
||||
|
||||
```
|
||||
❌ BLOCKED ✅ ALLOWED
|
||||
|
||||
Admin → Store Portal Admin → Admin Portal
|
||||
Store → Admin Portal Store → Store Portal
|
||||
Customer → Admin Portal Customer → Shop Catalog
|
||||
Customer → Store Portal Customer → Own Account
|
||||
❌ BLOCKED ✅ ALLOWED
|
||||
|
||||
super_admin → Store Portal super_admin → Admin Portal
|
||||
platform_admin → Store Portal platform_admin → Admin Portal
|
||||
merchant_owner → Admin Portal merchant_owner → Store Portal
|
||||
store_member → Admin Portal store_member → Store Portal
|
||||
Customer → Admin Portal Customer → Shop Catalog
|
||||
Customer → Store Portal Customer → Own Account
|
||||
|
||||
Cookie Isolation:
|
||||
admin_token (path=/admin) ← Only sent to /admin/*
|
||||
|
||||
225
docs/api/rbac.md
225
docs/api/rbac.md
@@ -1,7 +1,7 @@
|
||||
# Role-Based Access Control (RBAC) Developer Guide
|
||||
|
||||
**Version:** 1.0
|
||||
**Last Updated:** November 2025
|
||||
**Version:** 2.0
|
||||
**Last Updated:** February 2026
|
||||
**Audience:** Development Team
|
||||
|
||||
---
|
||||
@@ -48,47 +48,44 @@ The RBAC system ensures that:
|
||||
|
||||
## RBAC Overview
|
||||
|
||||
### Three-Tier Permission Model
|
||||
### Two-Tier Permission Model
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────┐
|
||||
│ PLATFORM LEVEL │
|
||||
│ User.role │
|
||||
│ │
|
||||
│ ┌──────────┐ ┌──────────┐ │
|
||||
│ │ Admin │ │ Store │ │
|
||||
│ │ (admin) │ │ (store) │ │
|
||||
│ └──────────┘ └──────────┘ │
|
||||
└─────────────────────────────────────────────────────────┘
|
||||
┌─────────────────────────────────────────────────────────────┐
|
||||
│ 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 LEVEL │
|
||||
│ StoreUser.user_type │
|
||||
│ │
|
||||
│ ┌──────────┐ ┌──────────────┐ │
|
||||
│ │ Owner │ │ Team Member │ │
|
||||
│ │ (owner) │ │ (member) │ │
|
||||
│ └──────────┘ └──────────────┘ │
|
||||
└─────────────────────────────────────────────────────────┘
|
||||
│
|
||||
▼
|
||||
┌─────────────────────────────────────────────────────────┐
|
||||
│ PERMISSION LEVEL │
|
||||
│ Role.permissions │
|
||||
│ │
|
||||
│ Manager, Staff, Support, Viewer, Marketing, Custom │
|
||||
└─────────────────────────────────────────────────────────┘
|
||||
┌─────────────────────────────────────────────────────────────┐
|
||||
│ 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 Type |
|
||||
|---------|--------|----------------|-----------|
|
||||
| **Admin** | `/admin/*` | `admin_token` cookie | Platform Admins |
|
||||
| **Store** | `/store/*` | `store_token` cookie | Store Owners & Teams |
|
||||
| Context | Routes | Authentication | User Roles |
|
||||
|---------|--------|----------------|------------|
|
||||
| **Admin** | `/admin/*` | `admin_token` cookie | `super_admin`, `platform_admin` |
|
||||
| **Store** | `/store/*` | `store_token` cookie | `merchant_owner`, `store_member` |
|
||||
| **Shop** | `/shop/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.
|
||||
@@ -165,18 +162,19 @@ The application operates in three isolated contexts:
|
||||
|
||||
## User Types & Contexts
|
||||
|
||||
### Platform Admins
|
||||
### Super Admins
|
||||
|
||||
**Characteristics:**
|
||||
- `User.role = "admin"`
|
||||
- `User.role = "super_admin"`
|
||||
- Full access to `/admin/*` routes
|
||||
- Manage all stores and users
|
||||
- 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:**
|
||||
- Platform configuration
|
||||
- Store approval/verification
|
||||
- User management
|
||||
- Full platform configuration
|
||||
- Multi-platform management
|
||||
- Super-level user management
|
||||
- System monitoring
|
||||
|
||||
**Authentication:**
|
||||
@@ -193,14 +191,31 @@ GET /admin/stores
|
||||
POST /admin/users/{user_id}/suspend
|
||||
```
|
||||
|
||||
### Store Owners
|
||||
### Platform Admins
|
||||
|
||||
**Characteristics:**
|
||||
- `User.role = "store"`
|
||||
- `StoreUser.user_type = "owner"`
|
||||
- Automatic full permissions within their store
|
||||
- `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
|
||||
@@ -210,7 +225,7 @@ POST /admin/users/{user_id}/suspend
|
||||
|
||||
**Special Privileges:**
|
||||
```python
|
||||
# Automatic permissions
|
||||
# Automatic permissions - owners bypass permission checks
|
||||
def has_permission(self, permission: str) -> bool:
|
||||
if self.is_owner:
|
||||
return True # Owners bypass permission checks
|
||||
@@ -219,11 +234,11 @@ def has_permission(self, permission: str) -> bool:
|
||||
### Store Team Members
|
||||
|
||||
**Characteristics:**
|
||||
- `User.role = "store"`
|
||||
- `StoreUser.user_type = "member"`
|
||||
- Permissions defined by `Role.permissions`
|
||||
- Invited by store owner via email
|
||||
- `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
|
||||
@@ -284,8 +299,12 @@ permissions = [
|
||||
│ email │ │
|
||||
│ username │ │
|
||||
│ role │ │ owner_user_id
|
||||
│ ('admin' | │ │
|
||||
│ 'store') │ │
|
||||
│ ('super_admin'| │ │
|
||||
│ 'platform_ │ │
|
||||
│ admin' | │ │
|
||||
│ 'merchant_ │ │
|
||||
│ owner' | │ │
|
||||
│ 'store_member') │ │
|
||||
│ is_active │ │
|
||||
│ is_email_ │ │
|
||||
│ verified │ │
|
||||
@@ -301,9 +320,6 @@ permissions = [
|
||||
│ store_id (FK) ─┼───┐ │
|
||||
│ user_id (FK) ───┼─┐ │ │
|
||||
│ role_id (FK) │ │ │ │
|
||||
│ user_type │ │ │ │
|
||||
│ ('owner' | │ │ │ │
|
||||
│ 'member') │ │ │ │
|
||||
│ invitation_ │ │ │ │
|
||||
│ token │ │ │ │
|
||||
│ invitation_ │ │ │ │
|
||||
@@ -355,11 +371,13 @@ permissions = [
|
||||
└──────────────────┘
|
||||
```
|
||||
|
||||
**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 admins and stores.
|
||||
Primary platform user table for all user types.
|
||||
|
||||
```python
|
||||
class User(Base):
|
||||
@@ -369,15 +387,25 @@ class User(Base):
|
||||
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) # 'admin' or 'store'
|
||||
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`: Only contains `"admin"` or `"store"` (platform-level role)
|
||||
- `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.
|
||||
@@ -411,7 +439,6 @@ class StoreUser(Base):
|
||||
id = Column(Integer, primary_key=True)
|
||||
store_id = Column(Integer, ForeignKey("stores.id"), nullable=False)
|
||||
user_id = Column(Integer, ForeignKey("users.id"), nullable=False)
|
||||
user_type = Column(String, nullable=False) # 'owner' or 'member'
|
||||
role_id = Column(Integer, ForeignKey("roles.id"), nullable=True)
|
||||
invited_by = Column(Integer, ForeignKey("users.id"))
|
||||
invitation_token = Column(String, nullable=True)
|
||||
@@ -421,11 +448,12 @@ class StoreUser(Base):
|
||||
```
|
||||
|
||||
**Important Fields:**
|
||||
- `user_type`: Distinguishes owners (`"owner"`) from team members (`"member"`)
|
||||
- `role_id`: NULL for owners (they have all permissions), set for team members
|
||||
- `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.
|
||||
@@ -685,7 +713,9 @@ role = Role(
|
||||
│ Admin Auth Endpoint │
|
||||
│ │
|
||||
│ 1. Validate credentials │
|
||||
│ 2. Check role == "admin" │
|
||||
│ 2. Check is_admin │
|
||||
│ (super_admin or │
|
||||
│ platform_admin) │
|
||||
│ 3. Generate JWT │
|
||||
└──────┬──────────────────────┘
|
||||
│
|
||||
@@ -719,9 +749,11 @@ role = Role(
|
||||
│ Store Auth Endpoint │
|
||||
│ │
|
||||
│ 1. Validate credentials │
|
||||
│ 2. Block if admin │
|
||||
│ 2. Block if is_admin │
|
||||
│ 3. Find store membership │
|
||||
│ 4. Get role (owner/member) │
|
||||
│ 4. Determine ownership │
|
||||
│ via Merchant. │
|
||||
│ owner_user_id │
|
||||
│ 5. Generate JWT │
|
||||
└──────┬──────────────────────┘
|
||||
│
|
||||
@@ -880,7 +912,7 @@ def get_current_admin_from_cookie_or_header(
|
||||
Checks:
|
||||
1. admin_token cookie (path=/admin)
|
||||
2. Authorization: Bearer <token> header
|
||||
3. Validates role == "admin"
|
||||
3. Validates is_admin (role in: super_admin, platform_admin)
|
||||
|
||||
Use for: Admin HTML pages
|
||||
"""
|
||||
@@ -899,13 +931,13 @@ def get_current_store_from_cookie_or_header(
|
||||
Checks:
|
||||
1. store_token cookie (path=/store)
|
||||
2. Authorization: Bearer <token> header
|
||||
3. Blocks admin users
|
||||
4. Validates store membership
|
||||
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
|
||||
# Returns User object if authenticated as store user
|
||||
# Raises InsufficientPermissionsException if admin
|
||||
|
||||
# API-only authentication (header required)
|
||||
@@ -1051,14 +1083,29 @@ class User(Base):
|
||||
# ... fields ...
|
||||
|
||||
@property
|
||||
def is_admin(self) -> bool:
|
||||
"""Check if user is platform admin."""
|
||||
return self.role == "admin"
|
||||
def is_super_admin(self) -> bool:
|
||||
"""Check if user is super admin (computed from role)."""
|
||||
return self.role == "super_admin"
|
||||
|
||||
@property
|
||||
def is_store(self) -> bool:
|
||||
"""Check if user is store."""
|
||||
return self.role == "store"
|
||||
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."""
|
||||
@@ -1109,13 +1156,16 @@ class StoreUser(Base):
|
||||
|
||||
@property
|
||||
def is_owner(self) -> bool:
|
||||
"""Check if this is an owner membership."""
|
||||
return self.user_type == "owner"
|
||||
"""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 self.user_type == "member"
|
||||
return not self.is_owner
|
||||
|
||||
@property
|
||||
def is_invitation_pending(self) -> bool:
|
||||
@@ -1176,14 +1226,13 @@ The system uses email-based invitations for team member onboarding.
|
||||
├─> User record (if doesn't exist)
|
||||
│ - email: from invitation
|
||||
│ - username: auto-generated
|
||||
│ - role: "store"
|
||||
│ - role: "store_member"
|
||||
│ - is_active: FALSE
|
||||
│ - is_email_verified: FALSE
|
||||
│
|
||||
└─> StoreUser record
|
||||
- store_id: current store
|
||||
- user_id: from User
|
||||
- user_type: "member"
|
||||
- role_id: from role selection
|
||||
- invitation_token: secure random string
|
||||
- invitation_sent_at: now()
|
||||
@@ -1414,17 +1463,17 @@ def _generate_invitation_token(self) -> str:
|
||||
|
||||
#### Admin Blocking
|
||||
|
||||
Admins are blocked from store routes:
|
||||
Platform admins are blocked from store routes:
|
||||
|
||||
```python
|
||||
# In store auth endpoint
|
||||
if user.role == "admin":
|
||||
if current_user.is_admin: # role in ("super_admin", "platform_admin")
|
||||
raise InvalidCredentialsException(
|
||||
"Admins cannot access store portal"
|
||||
)
|
||||
|
||||
# In store dependencies
|
||||
if current_user.role == "admin":
|
||||
if current_user.is_admin:
|
||||
raise InsufficientPermissionsException(
|
||||
"Store access only"
|
||||
)
|
||||
@@ -1719,13 +1768,12 @@ Test permission logic in isolation.
|
||||
# tests/unit/test_permissions.py
|
||||
|
||||
def test_owner_has_all_permissions():
|
||||
"""Owners have all permissions automatically."""
|
||||
user = create_user()
|
||||
"""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,
|
||||
user_type="owner"
|
||||
store=store
|
||||
)
|
||||
|
||||
assert store_user.has_permission("products.create")
|
||||
@@ -1734,8 +1782,8 @@ def test_owner_has_all_permissions():
|
||||
# All permissions should return True
|
||||
|
||||
def test_team_member_respects_role():
|
||||
"""Team members have only their role's permissions."""
|
||||
user = create_user()
|
||||
"""Store members have only their role's permissions."""
|
||||
user = create_user(role="store_member")
|
||||
store = create_store()
|
||||
role = create_role(
|
||||
store=store,
|
||||
@@ -1745,7 +1793,6 @@ def test_team_member_respects_role():
|
||||
store_user = create_store_user(
|
||||
user=user,
|
||||
store=store,
|
||||
user_type="member",
|
||||
role=role
|
||||
)
|
||||
|
||||
@@ -1959,6 +2006,6 @@ See [RBAC Quick Reference](../backend/rbac-quick-reference.md) for a condensed c
|
||||
|
||||
---
|
||||
|
||||
**Document Version:** 1.0
|
||||
**Last Updated:** November 2025
|
||||
**Document Version:** 2.0
|
||||
**Last Updated:** February 2026
|
||||
**Maintained By:** Backend Team
|
||||
|
||||
@@ -52,9 +52,13 @@ sequenceDiagram
|
||||
|
||||
## User Roles
|
||||
|
||||
The platform has three distinct user roles, each with specific permissions and access levels:
|
||||
The platform uses a **4-value role enum** on the `User` model to distinguish user types:
|
||||
|
||||
### Customer Role
|
||||
```
|
||||
User.role: "super_admin" | "platform_admin" | "merchant_owner" | "store_member"
|
||||
```
|
||||
|
||||
### Customer Role (Separate Model)
|
||||
|
||||
**Access**: Public shop and own account space
|
||||
|
||||
@@ -68,36 +72,56 @@ The platform has three distinct user roles, each with specific permissions and a
|
||||
|
||||
**Account Creation**: Self-registration via shop frontend (email verification required)
|
||||
|
||||
**Authentication**: Standard JWT authentication
|
||||
**Authentication**: Standard JWT authentication (separate `Customer` model, not `User`)
|
||||
|
||||
### Store Role
|
||||
### Merchant Owner (`role="merchant_owner"`)
|
||||
|
||||
**Access**: Store area based on permissions
|
||||
**Access**: Full access to owned store dashboards
|
||||
|
||||
**Types**:
|
||||
- **Store Owner**: Full access to store dashboard and settings
|
||||
- **Store Team Members**: Access based on assigned permissions
|
||||
**Characteristics**:
|
||||
- Has **ALL store permissions** automatically (no role record needed)
|
||||
- Ownership determined by `Merchant.owner_user_id`
|
||||
- `User.is_merchant_owner` property returns `True`
|
||||
- `User.is_store_user` property returns `True`
|
||||
- `User.is_owner_of(store_id)` checks ownership
|
||||
|
||||
**Capabilities**:
|
||||
- Manage products and inventory
|
||||
- Process orders
|
||||
- View analytics and reports
|
||||
- Configure shop settings (owners only)
|
||||
- Manage team members (owners only)
|
||||
- Configure shop settings
|
||||
- Manage team members (invite, remove, update roles)
|
||||
- Access store-specific APIs
|
||||
|
||||
**Account Creation**:
|
||||
- Owners: Created automatically when admin creates a store
|
||||
- Team members: Invited by store owner via email
|
||||
**Account Creation**: Created automatically when admin creates a store
|
||||
|
||||
**Permissions System**: Team members can have granular permissions for different areas
|
||||
### Store Member (`role="store_member"`)
|
||||
|
||||
### Admin Role
|
||||
**Access**: Store area based on assigned role permissions
|
||||
|
||||
**Access**: Full platform administration
|
||||
**Characteristics**:
|
||||
- Permissions come from `StoreUser.role_id -> Role.permissions`
|
||||
- `User.is_store_user` property returns `True`
|
||||
- Must be invited by merchant owner via email
|
||||
|
||||
**Capabilities**: Limited based on assigned role (Manager, Staff, Support, Viewer, Marketing, or custom)
|
||||
|
||||
**Account Creation**: Invited by merchant owner via email
|
||||
|
||||
**Permissions System**: Team members can have granular permissions for different areas (up to 75 permissions)
|
||||
|
||||
### Super Admin (`role="super_admin"`)
|
||||
|
||||
**Access**: Full platform administration across all platforms
|
||||
|
||||
**Characteristics**:
|
||||
- `User.is_super_admin` property returns `True` (computed: `role == "super_admin"`)
|
||||
- `User.is_admin` property returns `True`
|
||||
- Can access all platforms without restriction
|
||||
- Cannot access store portal (blocked by middleware)
|
||||
|
||||
**Capabilities**:
|
||||
- Manage all stores
|
||||
- Manage all stores across all platforms
|
||||
- Create/manage store accounts
|
||||
- Access system settings
|
||||
- View all data across the platform
|
||||
@@ -105,9 +129,24 @@ The platform has three distinct user roles, each with specific permissions and a
|
||||
- Access audit logs
|
||||
- Platform-wide analytics
|
||||
|
||||
**Account Creation**: Created by super admins on the backend
|
||||
**Account Creation**: Created by existing super admins on the backend
|
||||
|
||||
**Super Privileges**: Admins can access all areas including store and customer sections
|
||||
### Platform Admin (`role="platform_admin"`)
|
||||
|
||||
**Access**: Platform administration scoped to assigned platforms
|
||||
|
||||
**Characteristics**:
|
||||
- `User.is_platform_admin` property returns `True` (computed: `role == "platform_admin"`)
|
||||
- `User.is_admin` property returns `True`
|
||||
- Scoped to specific platforms via `AdminPlatform` association
|
||||
- Cannot access store portal (blocked by middleware)
|
||||
|
||||
**Capabilities**:
|
||||
- Manage stores within assigned platforms
|
||||
- Access platform-scoped settings and analytics
|
||||
- View data within assigned platforms
|
||||
|
||||
**Account Creation**: Created by super admins
|
||||
|
||||
## Application Areas & Access Control
|
||||
|
||||
@@ -115,22 +154,23 @@ The platform has three distinct areas with different access requirements:
|
||||
|
||||
| Area | URL Pattern | Access | Purpose |
|
||||
|------|-------------|--------|---------|
|
||||
| **Admin** | `/admin/*` or `admin.platform.com` | Admin users only | Platform administration and store management |
|
||||
| **Store** | `/store/*` | Store owners and team members | Store dashboard and shop management |
|
||||
| **Admin** | `/admin/*` or `admin.platform.com` | Super admins and platform admins (`is_admin`) | Platform administration and store management |
|
||||
| **Store** | `/store/*` | Merchant owners and store members (`is_store_user`) | Store dashboard and shop management |
|
||||
| **Shop** | `/shop/*`, custom domains, subdomains | Customers and public | Public-facing eCommerce storefront |
|
||||
| **API** | `/api/*` | All authenticated users (role-based) | REST API for all operations |
|
||||
|
||||
## Account Registration Flow
|
||||
|
||||
### Admin Accounts
|
||||
### Admin Accounts (Super Admin & Platform Admin)
|
||||
- ❌ Cannot register from frontend
|
||||
- ✅ Created by super admins on the backend
|
||||
- ✅ **Super Admins** (`role="super_admin"`): Created by existing super admins
|
||||
- ✅ **Platform Admins** (`role="platform_admin"`): Created by super admins
|
||||
- Used for: Platform administration
|
||||
|
||||
### Store Accounts
|
||||
### Store Accounts (Merchant Owner & Store Member)
|
||||
- ❌ Cannot register from frontend
|
||||
- ✅ **Store Owners**: Automatically created when admin creates a new store
|
||||
- ✅ **Team Members**: Invited by store owner via email invitation
|
||||
- ✅ **Merchant Owners** (`role="merchant_owner"`): Automatically created when admin creates a new store
|
||||
- ✅ **Store Members** (`role="store_member"`): Invited by merchant owner via email invitation
|
||||
- Activation: Upon clicking email verification link
|
||||
|
||||
### Customer Accounts
|
||||
@@ -201,7 +241,7 @@ def require_role(self, required_role: str) -> Callable
|
||||
```
|
||||
|
||||
**Parameters**:
|
||||
- `required_role` (str): The exact role name required (e.g., "admin", "store", "custom_role")
|
||||
- `required_role` (str): The exact role name required (e.g., `"super_admin"`, `"platform_admin"`, `"merchant_owner"`, `"store_member"`)
|
||||
|
||||
**Returns**: A decorator function that:
|
||||
1. Accepts a function as input
|
||||
@@ -216,31 +256,31 @@ from models.database.user import User
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
@router.get("/moderator-only")
|
||||
@auth_manager.require_role("moderator")
|
||||
async def moderator_endpoint(current_user: User):
|
||||
"""Only users with role='moderator' can access this."""
|
||||
return {"message": "Moderator access granted"}
|
||||
@router.get("/owner-only")
|
||||
@auth_manager.require_role("merchant_owner")
|
||||
async def owner_endpoint(current_user: User):
|
||||
"""Only users with role='merchant_owner' can access this."""
|
||||
return {"message": "Merchant owner access granted"}
|
||||
|
||||
# Can also be used with custom roles
|
||||
@router.get("/special-access")
|
||||
@auth_manager.require_role("special_user")
|
||||
async def special_endpoint(current_user: User):
|
||||
return {"data": "special content"}
|
||||
# Can also be used with other specific roles
|
||||
@router.get("/super-admin-only")
|
||||
@auth_manager.require_role("super_admin")
|
||||
async def super_admin_endpoint(current_user: User):
|
||||
return {"data": "super admin content"}
|
||||
```
|
||||
|
||||
**Error Response**:
|
||||
```json
|
||||
{
|
||||
"detail": "Required role 'moderator' not found. Current role: 'store'"
|
||||
"detail": "Required role 'merchant_owner' not found. Current role: 'store_member'"
|
||||
}
|
||||
```
|
||||
|
||||
**Note**: For standard roles (admin, store, customer), prefer using the dedicated methods (`require_admin()`, `require_store()`, `require_customer()`) as they provide better error handling and custom exceptions.
|
||||
**Note**: For standard access patterns, prefer using the dedicated methods (`require_admin()`, `require_store()`, `require_customer()`) or the computed properties (`is_admin`, `is_store_user`) as they provide better error handling and custom exceptions.
|
||||
|
||||
### create_default_admin_user()
|
||||
|
||||
Creates a default admin user if one doesn't already exist. This is typically used during initial application setup or database seeding.
|
||||
Creates a default super admin user if one doesn't already exist. This is typically used during initial application setup or database seeding.
|
||||
|
||||
**Method Signature**:
|
||||
```python
|
||||
@@ -254,11 +294,11 @@ def create_default_admin_user(self, db: Session) -> User
|
||||
|
||||
**Behavior**:
|
||||
1. Checks if a user with username "admin" already exists
|
||||
2. If not found, creates a new admin user with:
|
||||
2. If not found, creates a new super admin user with:
|
||||
- Username: `admin`
|
||||
- Email: `admin@example.com`
|
||||
- Password: `admin123` (hashed with bcrypt)
|
||||
- Role: `admin`
|
||||
- Role: `super_admin`
|
||||
- Status: Active
|
||||
3. If found, returns the existing user without modification
|
||||
|
||||
@@ -295,7 +335,7 @@ def create_admin_from_env(db: Session):
|
||||
username=admin_username,
|
||||
email=admin_email,
|
||||
hashed_password=auth_manager.hash_password(admin_password),
|
||||
role="admin",
|
||||
role="super_admin",
|
||||
is_active=True
|
||||
)
|
||||
db.add(admin)
|
||||
@@ -318,7 +358,7 @@ def create_admin_from_env(db: Session):
|
||||
"sub": "123", // User ID (JWT standard claim)
|
||||
"username": "testuser", // Username for display
|
||||
"email": "user@example.com", // User email
|
||||
"role": "store", // User role
|
||||
"role": "merchant_owner", // User role (4-value enum)
|
||||
"exp": 1700000000, // Expiration timestamp (JWT standard)
|
||||
"iat": 1699999000 // Issued at timestamp (JWT standard)
|
||||
}
|
||||
@@ -342,23 +382,28 @@ JWT_EXPIRE_MINUTES=30
|
||||
|
||||
```mermaid
|
||||
graph TD
|
||||
A[Admin] --> B[Full Platform Access]
|
||||
A --> C[Can Access All Areas]
|
||||
A[Super Admin<br/>role=super_admin] --> B[Full Platform Access]
|
||||
A --> C[All Platforms]
|
||||
|
||||
D[Store Owner] --> E[Store Dashboard]
|
||||
AA[Platform Admin<br/>role=platform_admin] --> D2[Scoped Platform Access]
|
||||
AA --> D3[Assigned Platforms Only]
|
||||
|
||||
D[Merchant Owner<br/>role=merchant_owner] --> E[Store Dashboard]
|
||||
D --> F[Team Management]
|
||||
D --> G[Shop Settings]
|
||||
D --> H[All Store Data]
|
||||
D --> H[All Store Permissions - 75]
|
||||
|
||||
I[Store Team Member] --> E
|
||||
I --> J[Limited Based on Permissions]
|
||||
I[Store Member<br/>role=store_member] --> E
|
||||
I --> J[Role-Based Permissions]
|
||||
|
||||
K[Customer] --> L[Shop Access]
|
||||
K[Customer<br/>separate model] --> L[Shop Access]
|
||||
K --> M[Own Orders]
|
||||
K --> N[Own Profile]
|
||||
```
|
||||
|
||||
**Admin Override**: Admin users have implicit access to all areas, including store and customer sections. This allows admins to provide support and manage the platform effectively.
|
||||
**Admin Override**: Admin users (`is_admin`: super admins and platform admins) have access to the admin portal. They cannot access the store portal directly -- these are separate security boundaries enforced by middleware.
|
||||
|
||||
**Note**: `is_super_admin` is no longer a database column. It is a computed property: `User.role == "super_admin"`. JWT tokens no longer include an `is_super_admin` claim; derive it from the `role` claim instead.
|
||||
|
||||
## Security Features
|
||||
|
||||
@@ -505,7 +550,7 @@ def test_password_hashing():
|
||||
|
||||
def test_create_token():
|
||||
auth_manager = AuthManager()
|
||||
user = create_test_user(role="store")
|
||||
user = create_test_user(role="merchant_owner")
|
||||
|
||||
token_data = auth_manager.create_access_token(user)
|
||||
|
||||
@@ -600,11 +645,11 @@ async def dashboard(
|
||||
db: Session = Depends(get_db)
|
||||
):
|
||||
"""Accessible by all authenticated users, but returns different data."""
|
||||
if current_user.role == "admin":
|
||||
# Admin sees everything
|
||||
if current_user.is_admin:
|
||||
# Admin (super_admin or platform_admin) sees platform data
|
||||
data = get_admin_dashboard(db)
|
||||
elif current_user.role == "store":
|
||||
# Store sees their data only
|
||||
elif current_user.is_store_user:
|
||||
# Store user (merchant_owner or store_member) sees their store data
|
||||
data = get_store_dashboard(db, current_user.id)
|
||||
else:
|
||||
# Customer sees their orders
|
||||
|
||||
@@ -35,18 +35,20 @@ from app.modules.tenancy.models import User # noqa: API-007 violation
|
||||
id: int # User ID
|
||||
email: str # Email address
|
||||
username: str # Username
|
||||
role: str # "admin" or "store"
|
||||
role: str # "super_admin", "platform_admin", "merchant_owner", or "store_member"
|
||||
is_active: bool # Account status
|
||||
```
|
||||
|
||||
### Admin-Specific Fields
|
||||
```python
|
||||
is_super_admin: bool # True for super admins
|
||||
is_super_admin: bool # Computed: role == "super_admin" (not stored in DB or JWT)
|
||||
accessible_platform_ids: list[int] | None # Platform IDs (None = all for super admin)
|
||||
token_platform_id: int | None # Selected platform from JWT
|
||||
token_platform_code: str | None # Selected platform code from JWT
|
||||
```
|
||||
|
||||
**Note**: `is_super_admin` is no longer a database column or JWT claim. It is derived from `role == "super_admin"`. On the `User` model it is a computed property; on `UserContext` it is populated from the role field during `from_user()` construction.
|
||||
|
||||
### Store-Specific Fields
|
||||
```python
|
||||
token_store_id: int | None # Store ID from JWT
|
||||
@@ -80,7 +82,9 @@ preferred_language: str | None
|
||||
```
|
||||
1. POST /api/v1/admin/auth/login
|
||||
- Returns LoginResponse with user data and token
|
||||
- Token includes: user_id, role, is_super_admin, accessible_platforms
|
||||
- Token includes: user_id, role (e.g. "super_admin" or "platform_admin"),
|
||||
accessible_platforms
|
||||
- Note: is_super_admin is NOT in the JWT; derive from role == "super_admin"
|
||||
|
||||
2. GET /api/v1/admin/auth/accessible-platforms
|
||||
- Returns list of platforms admin can access
|
||||
@@ -93,29 +97,30 @@ preferred_language: str | None
|
||||
4. Subsequent API calls
|
||||
- Token decoded → UserContext populated
|
||||
- current_user.token_platform_id available
|
||||
- current_user.is_super_admin derived from role
|
||||
```
|
||||
|
||||
### JWT Token → UserContext Mapping
|
||||
|
||||
When a JWT token is decoded, these fields are mapped:
|
||||
|
||||
| JWT Claim | UserContext Field |
|
||||
|-----------|-------------------|
|
||||
| `sub` | `id` |
|
||||
| `username` | `username` |
|
||||
| `email` | `email` |
|
||||
| `role` | `role` |
|
||||
| `is_super_admin` | `is_super_admin` |
|
||||
| `accessible_platforms` | `accessible_platform_ids` |
|
||||
| `platform_id` | `token_platform_id` |
|
||||
| `platform_code` | `token_platform_code` |
|
||||
| `store_id` | `token_store_id` |
|
||||
| `store_code` | `token_store_code` |
|
||||
| `store_role` | `token_store_role` |
|
||||
| JWT Claim | UserContext Field | Notes |
|
||||
|-----------|-------------------|-------|
|
||||
| `sub` | `id` | |
|
||||
| `username` | `username` | |
|
||||
| `email` | `email` | |
|
||||
| `role` | `role` | 4-value enum: `super_admin`, `platform_admin`, `merchant_owner`, `store_member` |
|
||||
| *(derived from role)* | `is_super_admin` | Computed: `role == "super_admin"` (no longer a JWT claim) |
|
||||
| `accessible_platforms` | `accessible_platform_ids` | |
|
||||
| `platform_id` | `token_platform_id` | |
|
||||
| `platform_code` | `token_platform_code` | |
|
||||
| `store_id` | `token_store_id` | |
|
||||
| `store_code` | `token_store_code` | |
|
||||
| `store_role` | `token_store_role` | |
|
||||
|
||||
## Helper Methods
|
||||
|
||||
`UserContext` provides helper methods:
|
||||
`UserContext` provides helper methods and computed properties:
|
||||
|
||||
```python
|
||||
# Check platform access
|
||||
@@ -127,10 +132,16 @@ platform_ids = current_user.get_accessible_platform_ids()
|
||||
# Returns None for super admins (all platforms)
|
||||
# Returns list[int] for platform admins
|
||||
|
||||
# Check role
|
||||
if current_user.is_admin:
|
||||
# Check role categories (computed from role field)
|
||||
if current_user.is_admin: # role in ("super_admin", "platform_admin")
|
||||
...
|
||||
if current_user.is_store:
|
||||
if current_user.is_super_admin: # role == "super_admin"
|
||||
...
|
||||
if current_user.is_platform_admin: # role == "platform_admin"
|
||||
...
|
||||
if current_user.is_merchant_owner: # role == "merchant_owner"
|
||||
...
|
||||
if current_user.is_store_user: # role in ("merchant_owner", "store_member")
|
||||
...
|
||||
|
||||
# Full name
|
||||
|
||||
@@ -155,23 +155,36 @@ StorePermissions.IMPORTS_CANCEL
|
||||
|
||||
---
|
||||
|
||||
## User Role Properties
|
||||
|
||||
```python
|
||||
# Check if super admin (computed: role == "super_admin")
|
||||
user.is_super_admin # bool
|
||||
|
||||
# Check if any admin (computed: role in ("super_admin", "platform_admin"))
|
||||
user.is_admin # bool
|
||||
|
||||
# Check if platform admin (computed: role == "platform_admin")
|
||||
user.is_platform_admin # bool
|
||||
|
||||
# Check if merchant owner (computed: role == "merchant_owner")
|
||||
user.is_merchant_owner # bool
|
||||
|
||||
# Check if store-level user (computed: role in ("merchant_owner", "store_member"))
|
||||
user.is_store_user # bool
|
||||
```
|
||||
|
||||
## User Helper Methods
|
||||
|
||||
```python
|
||||
# Check if admin
|
||||
user.is_admin # bool
|
||||
|
||||
# Check if store
|
||||
user.is_store # bool
|
||||
|
||||
# Check store ownership
|
||||
# Check store ownership (via Merchant.owner_user_id)
|
||||
user.is_owner_of(store_id) # bool
|
||||
|
||||
# Check store membership
|
||||
user.is_member_of(store_id) # bool
|
||||
|
||||
# Get role in store
|
||||
user.get_store_role(store_id) # str: "owner" | "member" | None
|
||||
user.get_store_role(store_id) # str: "owner" | role name | None
|
||||
|
||||
# Check specific permission
|
||||
user.has_store_permission(store_id, "products.create") # bool
|
||||
@@ -182,7 +195,7 @@ user.has_store_permission(store_id, "products.create") # bool
|
||||
## StoreUser Helper Methods
|
||||
|
||||
```python
|
||||
# Check if owner
|
||||
# Check if owner (derived from User.role and Merchant.owner_user_id)
|
||||
store_user.is_owner # bool
|
||||
|
||||
# Check if team member
|
||||
@@ -331,7 +344,8 @@ async function getPermissions() {
|
||||
### Unit Test
|
||||
```python
|
||||
def test_owner_has_all_permissions():
|
||||
store_user = create_store_user(user_type="owner")
|
||||
user = create_user(role="merchant_owner")
|
||||
store_user = create_store_user(user=user, store=store)
|
||||
assert store_user.has_permission("products.create")
|
||||
assert store_user.has_permission("team.invite")
|
||||
```
|
||||
@@ -423,8 +437,9 @@ token = "eyJ0eXAi..."
|
||||
decoded = jwt.decode(token, verify=False)
|
||||
print(f"User ID: {decoded['sub']}")
|
||||
print(f"Username: {decoded['username']}")
|
||||
print(f"Role: {decoded['role']}")
|
||||
print(f"Role: {decoded['role']}") # super_admin, platform_admin, merchant_owner, or store_member
|
||||
print(f"Expires: {decoded['exp']}")
|
||||
# Note: is_super_admin is no longer in JWT tokens; derive from role == "super_admin"
|
||||
```
|
||||
|
||||
### Check Cookie
|
||||
|
||||
@@ -8,56 +8,76 @@ The store dashboard implements a **Role-Based Access Control (RBAC)** system tha
|
||||
|
||||
## User Types
|
||||
|
||||
### 1. Store Owner
|
||||
### 1. Merchant Owner
|
||||
|
||||
**Who:** The user who created the store account.
|
||||
**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 `StoreUser.user_type = "owner"`
|
||||
- Linked via `Store.owner_user_id → User.id`
|
||||
- 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,
|
||||
"user_type": "owner", # ✓ Owner
|
||||
"role_id": None, # No role needed
|
||||
"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. Team Members
|
||||
### 2. Store Members (Team Members)
|
||||
|
||||
**Who:** Users invited by the store owner to help manage the store.
|
||||
**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 `StoreUser.user_type = "member"`
|
||||
- Permissions come from `StoreUser.role_id → Role.permissions`
|
||||
- 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,
|
||||
"user_type": "member", # ✓ Team member
|
||||
"role_id": 3, # ✓ Role required
|
||||
"role_id": 3, # Role required for permission lookup
|
||||
"is_active": True,
|
||||
"invitation_token": None, # Accepted
|
||||
"invitation_accepted_at": "2024-11-15 10:30:00"
|
||||
@@ -463,8 +483,7 @@ CREATE TABLE store_users (
|
||||
id SERIAL PRIMARY KEY,
|
||||
store_id INTEGER NOT NULL REFERENCES stores(id),
|
||||
user_id INTEGER NOT NULL REFERENCES users(id),
|
||||
user_type VARCHAR NOT NULL, -- 'owner' or 'member'
|
||||
role_id INTEGER REFERENCES roles(id), -- NULL for owners
|
||||
role_id INTEGER REFERENCES roles(id), -- NULL for merchant owners
|
||||
invited_by INTEGER REFERENCES users(id),
|
||||
invitation_token VARCHAR,
|
||||
invitation_sent_at TIMESTAMP,
|
||||
@@ -473,6 +492,8 @@ CREATE TABLE store_users (
|
||||
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
|
||||
@@ -495,9 +516,11 @@ CREATE TABLE roles (
|
||||
### 1. Invitation
|
||||
|
||||
```
|
||||
Owner invites user → StoreUser created:
|
||||
Merchant owner invites user:
|
||||
→ User created with role="store_member"
|
||||
→ StoreUser created:
|
||||
{
|
||||
"user_type": "member",
|
||||
"role_id": 3, -- Assigned role
|
||||
"is_active": False,
|
||||
"invitation_token": "abc123...",
|
||||
"invitation_sent_at": "2024-11-29 10:00:00",
|
||||
@@ -642,21 +665,22 @@ HTTP 403 Forbidden
|
||||
|
||||
## Summary
|
||||
|
||||
### Owner vs Team Member
|
||||
### Merchant Owner vs Store Member
|
||||
|
||||
| Feature | Owner | Team 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) |
|
||||
| **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
|
||||
|
||||
```
|
||||
Owner (75 permissions)
|
||||
Merchant Owner (75 permissions)
|
||||
└─ Manager (43 permissions)
|
||||
└─ Staff (10 permissions)
|
||||
└─ Support (6 permissions)
|
||||
|
||||
Reference in New Issue
Block a user