feat: add logging, marketplace, and admin enhancements
Database & Migrations: - Add application_logs table migration for hybrid cloud logging - Add companies table migration and restructure vendor relationships Logging System: - Implement hybrid logging system (database + file) - Add log_service for centralized log management - Create admin logs page with filtering and viewing capabilities - Add init_log_settings.py script for log configuration - Enhance core logging with database integration Marketplace Integration: - Add marketplace admin page with product management - Create marketplace vendor page with product listings - Implement marketplace.js for both admin and vendor interfaces - Add marketplace integration documentation Admin Enhancements: - Add imports management page and functionality - Create settings page for admin configuration - Add vendor themes management page - Enhance vendor detail and edit pages - Improve code quality dashboard and violation details - Add logs viewing and management - Update icons guide and shared icon system Architecture & Documentation: - Document frontend structure and component architecture - Document models structure and relationships - Add vendor-in-token architecture documentation - Add vendor RBAC (role-based access control) documentation - Document marketplace integration patterns - Update architecture patterns documentation Infrastructure: - Add platform static files structure (css, img, js) - Move architecture_scan.py to proper models location - Update model imports and registrations - Enhance exception handling - Update dependency injection patterns UI/UX: - Improve vendor edit interface - Update admin user interface - Enhance page templates documentation - Add vendor marketplace interface
This commit is contained in:
@@ -221,6 +221,101 @@ async def create_vendor(
|
||||
return result
|
||||
```
|
||||
|
||||
### Rule API-005: Vendor Context from Token (Not URL)
|
||||
|
||||
**Vendor API endpoints MUST extract vendor context from JWT token, NOT from URL.**
|
||||
|
||||
> **Rationale:** Embedding vendor context in JWT tokens enables clean RESTful API endpoints, eliminates URL-based vendor detection issues, and improves security by cryptographically signing vendor access.
|
||||
|
||||
**❌ BAD: URL-based vendor detection**
|
||||
|
||||
```python
|
||||
from middleware.vendor_context import require_vendor_context
|
||||
|
||||
@router.get("/products")
|
||||
def get_products(
|
||||
vendor: Vendor = Depends(require_vendor_context()), # ❌ Requires vendor in URL
|
||||
current_user: User = Depends(get_current_vendor_api),
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
# This fails on /api/v1/vendor/products (no vendor in URL)
|
||||
products = product_service.get_vendor_products(db, vendor.id)
|
||||
return products
|
||||
```
|
||||
|
||||
**Issues with URL-based approach:**
|
||||
- ❌ Only works with routes like `/vendor/{vendor_code}/dashboard`
|
||||
- ❌ Fails on API routes like `/api/v1/vendor/products` (no vendor in URL)
|
||||
- ❌ Inconsistent between page routes and API routes
|
||||
- ❌ Violates RESTful API design
|
||||
- ❌ Requires database lookup on every request
|
||||
|
||||
**✅ GOOD: Token-based vendor context**
|
||||
|
||||
```python
|
||||
@router.get("/products")
|
||||
def get_products(
|
||||
current_user: User = Depends(get_current_vendor_api), # ✅ Vendor in token
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
# Extract vendor from JWT token
|
||||
if not hasattr(current_user, "token_vendor_id"):
|
||||
raise HTTPException(
|
||||
status_code=400,
|
||||
detail="Token missing vendor information. Please login again.",
|
||||
)
|
||||
|
||||
vendor_id = current_user.token_vendor_id
|
||||
|
||||
# Use vendor_id from token
|
||||
products = product_service.get_vendor_products(db, vendor_id)
|
||||
return products
|
||||
```
|
||||
|
||||
**Benefits of token-based approach:**
|
||||
- ✅ Works on all routes (page and API)
|
||||
- ✅ Clean RESTful API endpoints
|
||||
- ✅ Vendor context cryptographically signed in JWT
|
||||
- ✅ No database lookup needed for vendor detection
|
||||
- ✅ Consistent authentication mechanism
|
||||
- ✅ Security: Cannot be tampered with by client
|
||||
|
||||
**Token structure:**
|
||||
```json
|
||||
{
|
||||
"sub": "user_id",
|
||||
"username": "john.doe",
|
||||
"vendor_id": 123, ← Vendor context
|
||||
"vendor_code": "WIZAMART", ← Vendor code
|
||||
"vendor_role": "Owner" ← Vendor role
|
||||
}
|
||||
```
|
||||
|
||||
**Available token attributes:**
|
||||
- `current_user.token_vendor_id` - Vendor ID (use for database queries)
|
||||
- `current_user.token_vendor_code` - Vendor code (use for logging)
|
||||
- `current_user.token_vendor_role` - Vendor role (Owner, Manager, etc.)
|
||||
|
||||
**Migration checklist:**
|
||||
1. Remove `vendor: Vendor = Depends(require_vendor_context())`
|
||||
2. Remove unused imports: `from middleware.vendor_context import require_vendor_context`
|
||||
3. Extract vendor from token: `vendor_id = current_user.token_vendor_id`
|
||||
4. Add token validation check (see example above)
|
||||
5. Update logging to use `current_user.token_vendor_code`
|
||||
|
||||
**See also:** `docs/backend/vendor-in-token-architecture.md` for complete migration guide
|
||||
|
||||
**Files requiring migration:**
|
||||
- `app/api/v1/vendor/customers.py`
|
||||
- `app/api/v1/vendor/notifications.py`
|
||||
- `app/api/v1/vendor/media.py`
|
||||
- `app/api/v1/vendor/marketplace.py`
|
||||
- `app/api/v1/vendor/inventory.py`
|
||||
- `app/api/v1/vendor/settings.py`
|
||||
- `app/api/v1/vendor/analytics.py`
|
||||
- `app/api/v1/vendor/payments.py`
|
||||
- `app/api/v1/vendor/profile.py`
|
||||
|
||||
---
|
||||
|
||||
## Service Layer Patterns
|
||||
|
||||
545
docs/architecture/frontend-structure.md
Normal file
545
docs/architecture/frontend-structure.md
Normal file
@@ -0,0 +1,545 @@
|
||||
# Frontend Architecture
|
||||
|
||||
## Overview
|
||||
|
||||
This application has **4 distinct frontends**, each with its own templates and static assets:
|
||||
|
||||
1. **Platform** - Public platform pages (homepage, about, contact)
|
||||
2. **Admin** - Administrative control panel
|
||||
3. **Vendor** - Vendor management portal
|
||||
4. **Shop** - Customer-facing e-commerce store
|
||||
|
||||
## Directory Structure
|
||||
|
||||
```
|
||||
app/
|
||||
├── templates/
|
||||
│ ├── platform/ # Platform public pages
|
||||
│ ├── admin/ # Admin portal pages
|
||||
│ ├── vendor/ # Vendor portal pages
|
||||
│ ├── shop/ # Shop customer pages
|
||||
│ └── shared/ # Shared components (emails, errors)
|
||||
│
|
||||
└── static/
|
||||
├── platform/ # Platform static assets
|
||||
│ ├── js/
|
||||
│ ├── css/
|
||||
│ └── img/
|
||||
├── admin/ # Admin static assets
|
||||
│ ├── js/
|
||||
│ ├── css/
|
||||
│ └── img/
|
||||
├── vendor/ # Vendor static assets
|
||||
│ ├── js/
|
||||
│ ├── css/
|
||||
│ └── img/
|
||||
├── shop/ # Shop static assets
|
||||
│ ├── js/
|
||||
│ ├── css/
|
||||
│ └── img/
|
||||
└── shared/ # Shared assets (icons, utilities)
|
||||
├── js/
|
||||
├── css/
|
||||
└── img/
|
||||
```
|
||||
|
||||
## Frontend Details
|
||||
|
||||
### 1. Platform Frontend
|
||||
|
||||
**Purpose:** Public-facing platform pages (marketing, info pages)
|
||||
|
||||
**Location:**
|
||||
- Templates: `app/templates/platform/`
|
||||
- Static: `static/platform/`
|
||||
|
||||
**Pages:**
|
||||
- Homepage (multiple layouts: default, minimal, modern)
|
||||
- Content pages (about, privacy, terms)
|
||||
- Landing pages
|
||||
|
||||
**Features:**
|
||||
- SEO-optimized
|
||||
- Multi-layout homepage support
|
||||
- Content management system integration
|
||||
- Responsive design
|
||||
|
||||
**Routes:** `/`, `/about`, `/contact`, etc.
|
||||
|
||||
**Authentication:** Not required (public access)
|
||||
|
||||
---
|
||||
|
||||
### 2. Admin Frontend
|
||||
|
||||
**Purpose:** Platform administration and management
|
||||
|
||||
**Location:**
|
||||
- Templates: `app/templates/admin/`
|
||||
- Static: `static/admin/`
|
||||
|
||||
**Pages:**
|
||||
- Dashboard
|
||||
- Vendor management
|
||||
- User management
|
||||
- Content management
|
||||
- Theme customization
|
||||
- System settings
|
||||
- Logs and monitoring
|
||||
- Code quality dashboard
|
||||
|
||||
**Technology Stack:**
|
||||
- Alpine.js for reactive components
|
||||
- Tailwind CSS for styling
|
||||
- Heroicons for icons
|
||||
- Centralized logging system
|
||||
- API-driven architecture
|
||||
|
||||
**Routes:** `/admin/*`
|
||||
|
||||
**Authentication:** Admin role required
|
||||
|
||||
---
|
||||
|
||||
### 3. Vendor Frontend
|
||||
|
||||
**Purpose:** Vendor portal for product and order management
|
||||
|
||||
**Location:**
|
||||
- Templates: `app/templates/vendor/`
|
||||
- Static: `static/vendor/`
|
||||
|
||||
**Pages:**
|
||||
- Vendor dashboard
|
||||
- Product management
|
||||
- Inventory management
|
||||
- Order management
|
||||
- Analytics
|
||||
- Profile settings
|
||||
|
||||
**Technology Stack:**
|
||||
- Alpine.js for reactive components
|
||||
- Tailwind CSS for styling
|
||||
- Heroicons for icons
|
||||
- API-driven architecture
|
||||
- Vendor context middleware
|
||||
|
||||
**Routes:** `/vendor/{vendor_code}/*`
|
||||
|
||||
**Authentication:** Vendor role required
|
||||
|
||||
---
|
||||
|
||||
### 4. Shop Frontend
|
||||
|
||||
**Purpose:** Customer-facing e-commerce store
|
||||
|
||||
**Location:**
|
||||
- Templates: `app/templates/shop/`
|
||||
- Static: `static/shop/`
|
||||
|
||||
**Pages:**
|
||||
- Product catalog
|
||||
- Product details
|
||||
- Shopping cart
|
||||
- Checkout
|
||||
- Order tracking
|
||||
- Customer account
|
||||
|
||||
**Technology Stack:**
|
||||
- Alpine.js for interactive features
|
||||
- Tailwind CSS for styling
|
||||
- E-commerce specific components
|
||||
- Payment integration
|
||||
- Shopping cart management
|
||||
|
||||
**Routes:** `/shop/*`
|
||||
|
||||
**Authentication:** Optional (required for checkout)
|
||||
|
||||
---
|
||||
|
||||
## Using Static Assets
|
||||
|
||||
Each frontend has its own static directory for frontend-specific assets. Use the appropriate directory based on which frontend the asset belongs to.
|
||||
|
||||
### Platform Static Assets (`static/platform/`)
|
||||
|
||||
**JavaScript Files:**
|
||||
```html
|
||||
<!-- In platform templates -->
|
||||
<script src="{{ url_for('static', path='platform/js/homepage.js') }}"></script>
|
||||
<script src="{{ url_for('static', path='platform/js/animations.js') }}"></script>
|
||||
```
|
||||
|
||||
**CSS Files:**
|
||||
```html
|
||||
<link href="{{ url_for('static', path='platform/css/styles.css') }}" rel="stylesheet">
|
||||
<link href="{{ url_for('static', path='platform/css/landing.css') }}" rel="stylesheet">
|
||||
```
|
||||
|
||||
**Images:**
|
||||
```html
|
||||
<img src="{{ url_for('static', path='platform/img/hero-banner.jpg') }}" alt="Hero">
|
||||
<img src="{{ url_for('static', path='platform/img/features/feature-1.svg') }}" alt="Feature">
|
||||
```
|
||||
|
||||
**Current Usage:** Platform currently uses only shared assets (fonts, Tailwind CSS). Platform-specific directories are ready for future platform-specific assets.
|
||||
|
||||
---
|
||||
|
||||
### Admin Static Assets (`static/admin/`)
|
||||
|
||||
**JavaScript Files:**
|
||||
```html
|
||||
<!-- In admin templates -->
|
||||
<script src="{{ url_for('static', path='admin/js/dashboard.js') }}"></script>
|
||||
<script src="{{ url_for('static', path='admin/js/vendors.js') }}"></script>
|
||||
```
|
||||
|
||||
**CSS Files:**
|
||||
```html
|
||||
<link href="{{ url_for('static', path='admin/css/custom.css') }}" rel="stylesheet">
|
||||
```
|
||||
|
||||
**Images:**
|
||||
```html
|
||||
<img src="{{ url_for('static', path='admin/img/placeholder.png') }}" alt="Placeholder">
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Vendor Static Assets (`static/vendor/`)
|
||||
|
||||
**JavaScript Files:**
|
||||
```html
|
||||
<!-- In vendor templates -->
|
||||
<script src="{{ url_for('static', path='vendor/js/dashboard.js') }}"></script>
|
||||
<script src="{{ url_for('static', path='vendor/js/products.js') }}"></script>
|
||||
```
|
||||
|
||||
**CSS Files:**
|
||||
```html
|
||||
<link href="{{ url_for('static', path='vendor/css/custom.css') }}" rel="stylesheet">
|
||||
```
|
||||
|
||||
**Images:**
|
||||
```html
|
||||
<img src="{{ url_for('static', path='vendor/img/no-products.svg') }}" alt="No Products">
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Shop Static Assets (`static/shop/`)
|
||||
|
||||
**JavaScript Files:**
|
||||
```html
|
||||
<!-- In shop templates -->
|
||||
<script src="{{ url_for('static', path='shop/js/cart.js') }}"></script>
|
||||
<script src="{{ url_for('static', path='shop/js/checkout.js') }}"></script>
|
||||
```
|
||||
|
||||
**CSS Files:**
|
||||
```html
|
||||
<link href="{{ url_for('static', path='shop/css/product-gallery.css') }}" rel="stylesheet">
|
||||
```
|
||||
|
||||
**Images:**
|
||||
```html
|
||||
<img src="{{ url_for('static', path='shop/img/placeholder-product.jpg') }}" alt="Product">
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### When to Use Shared vs. Frontend-Specific
|
||||
|
||||
**Use `static/shared/` when:**
|
||||
- Asset is used by 2 or more frontends
|
||||
- Common utilities (icons, API client, utilities)
|
||||
- Brand assets (logos, favicons)
|
||||
- Core libraries (Alpine.js, Tailwind CSS fallbacks)
|
||||
|
||||
**Use `static/{frontend}/` when:**
|
||||
- Asset is only used by one specific frontend
|
||||
- Frontend-specific styling
|
||||
- Frontend-specific JavaScript components
|
||||
- Frontend-specific images/graphics
|
||||
|
||||
**Example Decision Tree:**
|
||||
```
|
||||
Icon system (used by all 4 frontends) → static/shared/js/icons.js
|
||||
Admin dashboard chart → static/admin/js/charts.js
|
||||
Vendor product form → static/vendor/js/product-form.js
|
||||
Platform hero image → static/platform/img/hero.jpg
|
||||
Shop product carousel → static/shop/js/carousel.js
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Shared Resources
|
||||
|
||||
### Templates (`app/templates/shared/`)
|
||||
|
||||
**Shared components used across multiple frontends:**
|
||||
- Email templates
|
||||
- Error pages (404, 500)
|
||||
- Common partials
|
||||
|
||||
### Static Assets (`static/shared/`)
|
||||
|
||||
**Shared JavaScript:**
|
||||
- `js/icons.js` - Heroicons system (used by all frontends)
|
||||
- `js/utils.js` - Common utilities
|
||||
- `js/api-client.js` - API communication
|
||||
- `js/log-config.js` - Centralized logging
|
||||
|
||||
**Shared CSS:**
|
||||
- Common utility classes
|
||||
- Shared theme variables
|
||||
|
||||
**Shared Images:**
|
||||
- Logos
|
||||
- Brand assets
|
||||
- Icons
|
||||
|
||||
---
|
||||
|
||||
## Architecture Principles
|
||||
|
||||
### 1. Separation of Concerns
|
||||
|
||||
Each frontend is completely isolated:
|
||||
- Own templates directory
|
||||
- Own static assets directory
|
||||
- Own JavaScript components
|
||||
- Own CSS styles
|
||||
|
||||
**Benefits:**
|
||||
- Clear boundaries
|
||||
- Independent development
|
||||
- No cross-contamination
|
||||
- Easy to maintain
|
||||
|
||||
### 2. Shared Core
|
||||
|
||||
Common functionality is shared via `static/shared/`:
|
||||
- Icon system
|
||||
- API client
|
||||
- Utilities
|
||||
- Logging
|
||||
|
||||
**Benefits:**
|
||||
- DRY principle
|
||||
- Consistent behavior
|
||||
- Single source of truth
|
||||
- Easy updates
|
||||
|
||||
### 3. Template Inheritance
|
||||
|
||||
Each frontend has a base template:
|
||||
- `platform/base.html`
|
||||
- `admin/base.html`
|
||||
- `vendor/base.html`
|
||||
- `shop/base.html`
|
||||
|
||||
**Benefits:**
|
||||
- Consistent layout within frontend
|
||||
- Easy to customize per frontend
|
||||
- Different design systems possible
|
||||
|
||||
### 4. API-Driven
|
||||
|
||||
All frontends communicate with backend via APIs:
|
||||
- `/api/v1/admin/*` - Admin APIs
|
||||
- `/api/v1/vendor/*` - Vendor APIs
|
||||
- `/api/v1/shop/*` - Shop APIs
|
||||
- `/api/v1/platform/*` - Platform APIs
|
||||
|
||||
**Benefits:**
|
||||
- Clear backend contracts
|
||||
- Testable independently
|
||||
- Can be replaced with SPA if needed
|
||||
- Mobile app ready
|
||||
|
||||
---
|
||||
|
||||
## Frontend Technology Matrix
|
||||
|
||||
| Frontend | Framework | CSS | Icons | Auth Required | Base URL |
|
||||
|----------|-----------|-----------|------------|---------------|-------------------|
|
||||
| Platform | Alpine.js | Tailwind | Heroicons | No | `/` |
|
||||
| Admin | Alpine.js | Tailwind | Heroicons | Yes (Admin) | `/admin` |
|
||||
| Vendor | Alpine.js | Tailwind | Heroicons | Yes (Vendor) | `/vendor/{code}` |
|
||||
| Shop | Alpine.js | Tailwind | Heroicons | Optional | `/shop` |
|
||||
|
||||
---
|
||||
|
||||
## Development Guidelines
|
||||
|
||||
### Adding a New Page
|
||||
|
||||
1. **Determine which frontend** the page belongs to
|
||||
2. **Create template** in appropriate `app/templates/{frontend}/` directory
|
||||
3. **Create JavaScript** (if needed) in `static/{frontend}/js/`
|
||||
4. **Create CSS** (if needed) in `static/{frontend}/css/`
|
||||
5. **Add route** in appropriate route handler
|
||||
6. **Update navigation** in frontend's base template
|
||||
|
||||
### Using Shared Resources
|
||||
|
||||
**Icons:**
|
||||
```html
|
||||
<span x-html="$icon('icon-name', 'w-5 h-5')"></span>
|
||||
```
|
||||
|
||||
**API Client:**
|
||||
```javascript
|
||||
const data = await apiClient.get('/api/v1/admin/users');
|
||||
```
|
||||
|
||||
**Utilities:**
|
||||
```javascript
|
||||
Utils.showToast('Success!', 'success');
|
||||
Utils.formatDate(dateString);
|
||||
```
|
||||
|
||||
**Logging:**
|
||||
```javascript
|
||||
const log = window.LogConfig.loggers.myPage;
|
||||
log.info('Page loaded');
|
||||
```
|
||||
|
||||
### Frontend-Specific Resources
|
||||
|
||||
**Platform-specific JavaScript:**
|
||||
```html
|
||||
<script src="{{ url_for('static', path='platform/js/homepage.js') }}"></script>
|
||||
```
|
||||
|
||||
**Admin-specific CSS:**
|
||||
```html
|
||||
<link href="{{ url_for('static', path='admin/css/dashboard.css') }}" rel="stylesheet">
|
||||
```
|
||||
|
||||
**Vendor-specific images:**
|
||||
```html
|
||||
<img src="{{ url_for('static', path='vendor/img/logo.png') }}">
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Migration Notes
|
||||
|
||||
### Moving Assets Between Frontends
|
||||
|
||||
If an asset is used by multiple frontends:
|
||||
1. **Move to `static/shared/`**
|
||||
2. **Update all references**
|
||||
3. **Test all affected frontends**
|
||||
|
||||
If an asset is only used by one frontend:
|
||||
1. **Move to `static/{frontend}/`**
|
||||
2. **Update references in that frontend only**
|
||||
|
||||
### Deprecation Path
|
||||
|
||||
When removing a frontend:
|
||||
1. Remove `app/templates/{frontend}/`
|
||||
2. Remove `static/{frontend}/`
|
||||
3. Remove routes
|
||||
4. Update documentation
|
||||
|
||||
---
|
||||
|
||||
## Future Considerations
|
||||
|
||||
### Potential Additional Frontends
|
||||
|
||||
- **Partner Portal** - For business partners/affiliates
|
||||
- **API Documentation** - Interactive API docs (Swagger UI)
|
||||
- **Mobile App** - Native mobile using existing APIs
|
||||
|
||||
### Frontend Modernization
|
||||
|
||||
Each frontend can be independently modernized:
|
||||
- Replace Alpine.js with React/Vue/Svelte
|
||||
- Add TypeScript
|
||||
- Implement SSR/SSG
|
||||
- Convert to PWA
|
||||
|
||||
The API-driven architecture allows this flexibility.
|
||||
|
||||
---
|
||||
|
||||
## Testing Strategy
|
||||
|
||||
### Per-Frontend Testing
|
||||
|
||||
Each frontend should have:
|
||||
- **Unit tests** for JavaScript components
|
||||
- **Integration tests** for API interactions
|
||||
- **E2E tests** for critical user flows
|
||||
- **Accessibility tests**
|
||||
- **Responsive design tests**
|
||||
|
||||
### Shared Resource Testing
|
||||
|
||||
Shared resources need:
|
||||
- **Unit tests** for utilities
|
||||
- **Integration tests** with all frontends
|
||||
- **Visual regression tests** for icons
|
||||
|
||||
---
|
||||
|
||||
## Performance Optimization
|
||||
|
||||
### Per-Frontend Optimization
|
||||
|
||||
Each frontend can optimize independently:
|
||||
- Code splitting
|
||||
- Lazy loading
|
||||
- Asset minification
|
||||
- CDN deployment
|
||||
- Browser caching
|
||||
|
||||
### Shared Resource Optimization
|
||||
|
||||
Shared resources are cached globally:
|
||||
- Long cache headers
|
||||
- Versioning via query params
|
||||
- CDN distribution
|
||||
- Compression
|
||||
|
||||
---
|
||||
|
||||
## Security Considerations
|
||||
|
||||
### Frontend-Specific Security
|
||||
|
||||
Each frontend has different security needs:
|
||||
- **Platform:** XSS protection, CSP
|
||||
- **Admin:** CSRF tokens, admin-only routes
|
||||
- **Vendor:** Vendor isolation, rate limiting
|
||||
- **Shop:** PCI compliance, secure checkout
|
||||
|
||||
### Shared Security
|
||||
|
||||
All frontends use:
|
||||
- JWT authentication
|
||||
- HTTPS only
|
||||
- Secure headers
|
||||
- Input sanitization
|
||||
|
||||
---
|
||||
|
||||
## Conclusion
|
||||
|
||||
The 4-frontend architecture provides:
|
||||
- ✅ Clear separation of concerns
|
||||
- ✅ Independent development and deployment
|
||||
- ✅ Shared core functionality
|
||||
- ✅ Flexibility for future changes
|
||||
- ✅ Optimized for each user type
|
||||
- ✅ Maintainable and scalable
|
||||
|
||||
Each frontend serves a specific purpose and audience, with shared infrastructure for common needs.
|
||||
469
docs/architecture/models-structure.md
Normal file
469
docs/architecture/models-structure.md
Normal file
@@ -0,0 +1,469 @@
|
||||
# Models Structure
|
||||
|
||||
## Overview
|
||||
|
||||
This project follows a **standardized models structure** at the root level, separating database models from Pydantic schemas.
|
||||
|
||||
## Directory Structure
|
||||
|
||||
```
|
||||
models/
|
||||
├── database/ # SQLAlchemy database models (ORM)
|
||||
│ ├── __init__.py
|
||||
│ ├── user.py
|
||||
│ ├── vendor.py
|
||||
│ ├── product.py
|
||||
│ ├── order.py
|
||||
│ ├── admin.py
|
||||
│ ├── architecture_scan.py
|
||||
│ └── ...
|
||||
│
|
||||
└── schema/ # Pydantic schemas (API validation)
|
||||
├── __init__.py
|
||||
├── auth.py
|
||||
├── admin.py
|
||||
├── product.py
|
||||
├── order.py
|
||||
└── ...
|
||||
```
|
||||
|
||||
## Important Rules
|
||||
|
||||
### ✅ DO: Use Root-Level Models
|
||||
|
||||
**ALL models must be in the root `models/` directory:**
|
||||
- Database models → `models/database/`
|
||||
- Pydantic schemas → `models/schema/`
|
||||
|
||||
### ❌ DON'T: Create `app/models/`
|
||||
|
||||
**NEVER create or use `app/models/` directory.**
|
||||
|
||||
The application structure is:
|
||||
```
|
||||
app/ # Application code (routes, services, core)
|
||||
models/ # Models (database & schemas)
|
||||
```
|
||||
|
||||
NOT:
|
||||
```
|
||||
app/
|
||||
models/ # ❌ WRONG - Don't create this!
|
||||
models/ # ✓ Correct location
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Database Models (`models/database/`)
|
||||
|
||||
### Purpose
|
||||
SQLAlchemy ORM models that represent database tables.
|
||||
|
||||
### Naming Convention
|
||||
- Singular class names: `User`, `Product`, `Order`
|
||||
- File names match class: `user.py`, `product.py`, `order.py`
|
||||
|
||||
### Example Structure
|
||||
|
||||
**File:** `models/database/product.py`
|
||||
```python
|
||||
"""Product database model"""
|
||||
|
||||
from sqlalchemy import Column, Integer, String, Float, ForeignKey
|
||||
from sqlalchemy.orm import relationship
|
||||
|
||||
from .base import Base
|
||||
|
||||
|
||||
class Product(Base):
|
||||
"""Product database model"""
|
||||
|
||||
__tablename__ = "products"
|
||||
|
||||
id = Column(Integer, primary_key=True, index=True)
|
||||
name = Column(String(255), nullable=False)
|
||||
price = Column(Float, nullable=False)
|
||||
vendor_id = Column(Integer, ForeignKey("vendors.id"))
|
||||
|
||||
# Relationships
|
||||
vendor = relationship("Vendor", back_populates="products")
|
||||
```
|
||||
|
||||
### Exporting Models
|
||||
|
||||
All database models must be exported in `models/database/__init__.py`:
|
||||
|
||||
```python
|
||||
# models/database/__init__.py
|
||||
from .user import User
|
||||
from .vendor import Vendor
|
||||
from .product import Product
|
||||
from .order import Order, OrderItem
|
||||
|
||||
__all__ = [
|
||||
"User",
|
||||
"Vendor",
|
||||
"Product",
|
||||
"Order",
|
||||
"OrderItem",
|
||||
]
|
||||
```
|
||||
|
||||
### Importing Database Models
|
||||
|
||||
```python
|
||||
# ✅ CORRECT - Import from models.database
|
||||
from models.database import User, Product
|
||||
from models.database.vendor import Vendor
|
||||
|
||||
# ❌ WRONG - Don't import from app.models
|
||||
from app.models.user import User # This path doesn't exist!
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Pydantic Schemas (`models/schema/`)
|
||||
|
||||
### Purpose
|
||||
Pydantic models for API request/response validation and serialization.
|
||||
|
||||
### Naming Convention
|
||||
- Use descriptive suffixes: `Create`, `Update`, `Response`, `InDB`
|
||||
- Group related schemas in same file
|
||||
- File names match domain: `auth.py`, `product.py`, `order.py`
|
||||
|
||||
### Example Structure
|
||||
|
||||
**File:** `models/schema/product.py`
|
||||
```python
|
||||
"""Product Pydantic schemas"""
|
||||
|
||||
from typing import Optional
|
||||
from pydantic import BaseModel, Field
|
||||
|
||||
|
||||
class ProductBase(BaseModel):
|
||||
"""Base product schema"""
|
||||
name: str = Field(..., min_length=1, max_length=255)
|
||||
description: Optional[str] = None
|
||||
price: float = Field(..., gt=0)
|
||||
|
||||
|
||||
class ProductCreate(ProductBase):
|
||||
"""Schema for creating a product"""
|
||||
vendor_id: int
|
||||
|
||||
|
||||
class ProductUpdate(BaseModel):
|
||||
"""Schema for updating a product"""
|
||||
name: Optional[str] = Field(None, min_length=1, max_length=255)
|
||||
description: Optional[str] = None
|
||||
price: Optional[float] = Field(None, gt=0)
|
||||
|
||||
|
||||
class ProductResponse(ProductBase):
|
||||
"""Schema for product API response"""
|
||||
id: int
|
||||
vendor_id: int
|
||||
|
||||
class Config:
|
||||
from_attributes = True # Pydantic v2
|
||||
# orm_mode = True # Pydantic v1
|
||||
```
|
||||
|
||||
### Exporting Schemas
|
||||
|
||||
Export schemas in `models/schema/__init__.py`:
|
||||
|
||||
```python
|
||||
# models/schema/__init__.py
|
||||
from .auth import LoginRequest, TokenResponse
|
||||
from .product import ProductCreate, ProductUpdate, ProductResponse
|
||||
|
||||
__all__ = [
|
||||
"LoginRequest",
|
||||
"TokenResponse",
|
||||
"ProductCreate",
|
||||
"ProductUpdate",
|
||||
"ProductResponse",
|
||||
]
|
||||
```
|
||||
|
||||
### Importing Schemas
|
||||
|
||||
```python
|
||||
# ✅ CORRECT
|
||||
from models.schema import ProductCreate, ProductResponse
|
||||
from models.schema.auth import LoginRequest
|
||||
|
||||
# ❌ WRONG
|
||||
from app.models.schema.product import ProductCreate
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Common Patterns
|
||||
|
||||
### Pattern 1: Database Model with Schema
|
||||
|
||||
**Database Model:** `models/database/vendor.py`
|
||||
```python
|
||||
from sqlalchemy import Column, Integer, String, Boolean
|
||||
from .base import Base
|
||||
|
||||
class Vendor(Base):
|
||||
__tablename__ = "vendors"
|
||||
|
||||
id = Column(Integer, primary_key=True)
|
||||
name = Column(String(255), nullable=False)
|
||||
code = Column(String(50), unique=True, nullable=False)
|
||||
is_active = Column(Boolean, default=True)
|
||||
```
|
||||
|
||||
**Pydantic Schema:** `models/schema/vendor.py`
|
||||
```python
|
||||
from pydantic import BaseModel
|
||||
|
||||
class VendorBase(BaseModel):
|
||||
name: str
|
||||
code: str
|
||||
|
||||
class VendorCreate(VendorBase):
|
||||
pass
|
||||
|
||||
class VendorResponse(VendorBase):
|
||||
id: int
|
||||
is_active: bool
|
||||
|
||||
class Config:
|
||||
from_attributes = True
|
||||
```
|
||||
|
||||
**Usage in API:**
|
||||
```python
|
||||
from fastapi import APIRouter
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from models.database import Vendor
|
||||
from models.schema import VendorCreate, VendorResponse
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
@router.post("/vendors", response_model=VendorResponse)
|
||||
def create_vendor(vendor_data: VendorCreate, db: Session):
|
||||
# VendorCreate validates input
|
||||
db_vendor = Vendor(**vendor_data.dict())
|
||||
db.add(db_vendor)
|
||||
db.commit()
|
||||
db.refresh(db_vendor)
|
||||
# VendorResponse serializes output
|
||||
return db_vendor
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Pattern 2: Complex Schemas
|
||||
|
||||
For complex domains, organize schemas by purpose:
|
||||
|
||||
```python
|
||||
# models/schema/order.py
|
||||
class OrderBase(BaseModel):
|
||||
"""Base order fields"""
|
||||
pass
|
||||
|
||||
class OrderCreate(OrderBase):
|
||||
"""Create order from customer"""
|
||||
items: List[OrderItemCreate]
|
||||
|
||||
class OrderUpdate(BaseModel):
|
||||
"""Admin order update"""
|
||||
status: Optional[OrderStatus]
|
||||
|
||||
class OrderResponse(OrderBase):
|
||||
"""Order API response"""
|
||||
id: int
|
||||
items: List[OrderItemResponse]
|
||||
|
||||
class OrderAdminResponse(OrderResponse):
|
||||
"""Extended response for admin"""
|
||||
internal_notes: Optional[str]
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Migration Guide
|
||||
|
||||
If you accidentally created models in the wrong location:
|
||||
|
||||
### Moving Database Models
|
||||
|
||||
```bash
|
||||
# If you created app/models/my_model.py (WRONG)
|
||||
# Move to correct location:
|
||||
mv app/models/my_model.py models/database/my_model.py
|
||||
|
||||
# Update imports in all files
|
||||
# FROM: from app.models.my_model import MyModel
|
||||
# TO: from models.database.my_model import MyModel
|
||||
|
||||
# Add to models/database/__init__.py
|
||||
# Remove app/models/ directory
|
||||
rm -rf app/models/
|
||||
```
|
||||
|
||||
### Moving Pydantic Schemas
|
||||
|
||||
```bash
|
||||
# If you created app/schemas/my_schema.py (WRONG)
|
||||
# Move to correct location:
|
||||
mv app/schemas/my_schema.py models/schema/my_schema.py
|
||||
|
||||
# Update imports
|
||||
# FROM: from app.schemas.my_schema import MySchema
|
||||
# TO: from models.schema.my_schema import MySchema
|
||||
|
||||
# Add to models/schema/__init__.py
|
||||
# Remove app/schemas/ directory
|
||||
rm -rf app/schemas/
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Why This Structure?
|
||||
|
||||
### ✅ Benefits
|
||||
|
||||
1. **Clear Separation**
|
||||
- Database layer separate from application layer
|
||||
- Easy to understand where models live
|
||||
|
||||
2. **Import Consistency**
|
||||
- `from models.database import ...`
|
||||
- `from models.schema import ...`
|
||||
- No confusion about import paths
|
||||
|
||||
3. **Testing**
|
||||
- Easy to mock database models
|
||||
- Easy to test schema validation
|
||||
|
||||
4. **Scalability**
|
||||
- Models can be used by multiple apps
|
||||
- Clean separation of concerns
|
||||
|
||||
5. **Tool Compatibility**
|
||||
- Alembic migrations find models easily
|
||||
- IDE autocomplete works better
|
||||
- Linters understand structure
|
||||
|
||||
### ❌ Problems with `app/models/`
|
||||
|
||||
1. **Confusion**: Is it database or schema?
|
||||
2. **Import Issues**: Circular dependencies
|
||||
3. **Migration Problems**: Alembic can't find models
|
||||
4. **Inconsistency**: Different parts of codebase use different paths
|
||||
|
||||
---
|
||||
|
||||
## Verification Checklist
|
||||
|
||||
Use this checklist when adding new models:
|
||||
|
||||
### Database Model Checklist
|
||||
- [ ] File in `models/database/{name}.py`
|
||||
- [ ] Inherits from `Base`
|
||||
- [ ] Has `__tablename__` defined
|
||||
- [ ] Exported in `models/database/__init__.py`
|
||||
- [ ] Imported using `from models.database import ...`
|
||||
- [ ] NO file in `app/models/`
|
||||
|
||||
### Pydantic Schema Checklist
|
||||
- [ ] File in `models/schema/{name}.py`
|
||||
- [ ] Inherits from `BaseModel`
|
||||
- [ ] Has descriptive suffix (`Create`, `Update`, `Response`)
|
||||
- [ ] Exported in `models/schema/__init__.py`
|
||||
- [ ] Imported using `from models.schema import ...`
|
||||
- [ ] NO file in `app/schemas/`
|
||||
|
||||
---
|
||||
|
||||
## Project Structure
|
||||
|
||||
```
|
||||
project/
|
||||
├── app/
|
||||
│ ├── api/ # API routes
|
||||
│ ├── core/ # Core functionality (config, database, auth)
|
||||
│ ├── services/ # Business logic
|
||||
│ ├── templates/ # Jinja2 templates
|
||||
│ └── routes/ # Page routes
|
||||
│
|
||||
├── models/ # ✓ Models live here!
|
||||
│ ├── database/ # ✓ SQLAlchemy models
|
||||
│ └── schema/ # ✓ Pydantic schemas
|
||||
│
|
||||
├── static/ # Frontend assets
|
||||
├── docs/ # Documentation
|
||||
├── tests/ # Tests
|
||||
└── scripts/ # Utility scripts
|
||||
```
|
||||
|
||||
**NOT:**
|
||||
```
|
||||
app/
|
||||
models/ # ❌ Don't create this
|
||||
schemas/ # ❌ Don't create this
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Examples from the Codebase
|
||||
|
||||
### ✅ Correct Examples
|
||||
|
||||
**Database Model:**
|
||||
```python
|
||||
# models/database/architecture_scan.py
|
||||
from sqlalchemy import Column, Integer, String
|
||||
from .base import Base
|
||||
|
||||
class ArchitectureScan(Base):
|
||||
__tablename__ = "architecture_scans"
|
||||
id = Column(Integer, primary_key=True)
|
||||
```
|
||||
|
||||
**Import in Service:**
|
||||
```python
|
||||
# app/services/code_quality_service.py
|
||||
from models.database.architecture_scan import ArchitectureScan
|
||||
```
|
||||
|
||||
**Pydantic Schema:**
|
||||
```python
|
||||
# models/schema/admin.py
|
||||
from pydantic import BaseModel
|
||||
|
||||
class AdminDashboardStats(BaseModel):
|
||||
total_vendors: int
|
||||
total_users: int
|
||||
```
|
||||
|
||||
**Import in API:**
|
||||
```python
|
||||
# app/api/v1/admin/dashboard.py
|
||||
from models.schema.admin import AdminDashboardStats
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Summary
|
||||
|
||||
**Golden Rule:** All models in `models/`, never in `app/models/` or `app/schemas/`.
|
||||
|
||||
**Quick Reference:**
|
||||
- Database models → `models/database/`
|
||||
- Pydantic schemas → `models/schema/`
|
||||
- Import pattern → `from models.{type} import ...`
|
||||
- No models in `app/` directory
|
||||
|
||||
This standard ensures consistency, clarity, and maintainability across the entire project.
|
||||
500
docs/backend/vendor-in-token-architecture.md
Normal file
500
docs/backend/vendor-in-token-architecture.md
Normal file
@@ -0,0 +1,500 @@
|
||||
# Vendor-in-Token Architecture
|
||||
|
||||
## Overview
|
||||
|
||||
This document describes the vendor-in-token authentication architecture used for vendor API endpoints. This architecture embeds vendor context directly into JWT tokens, eliminating the need for URL-based vendor detection and enabling clean, RESTful API endpoints.
|
||||
|
||||
## The Problem: URL-Based Vendor Detection
|
||||
|
||||
### Old Pattern (Deprecated)
|
||||
```python
|
||||
# ❌ DEPRECATED: URL-based vendor detection
|
||||
@router.get("/{product_id}")
|
||||
def get_product(
|
||||
product_id: int,
|
||||
vendor: Vendor = Depends(require_vendor_context()), # ❌ Don't use
|
||||
current_user: User = Depends(get_current_vendor_api),
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
product = product_service.get_product(db, vendor.id, product_id)
|
||||
return product
|
||||
```
|
||||
|
||||
### Issues with URL-Based Detection
|
||||
|
||||
1. **Inconsistent API Routes**
|
||||
- Page routes: `/vendor/{vendor_code}/dashboard` (has vendor in URL)
|
||||
- API routes: `/api/v1/vendor/products` (no vendor in URL)
|
||||
- `require_vendor_context()` only works when vendor is in the URL path
|
||||
|
||||
2. **404 Errors on API Endpoints**
|
||||
- API calls to `/api/v1/vendor/products` would return 404
|
||||
- The dependency expected vendor code in URL but API routes don't have it
|
||||
- Breaking RESTful API design principles
|
||||
|
||||
3. **Architecture Violation**
|
||||
- Mixed concerns: URL structure determining business logic
|
||||
- Tight coupling between routing and vendor context
|
||||
- Harder to test and maintain
|
||||
|
||||
## The Solution: Vendor-in-Token
|
||||
|
||||
### Architecture Overview
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────────┐
|
||||
│ Vendor Login Flow │
|
||||
└─────────────────────────────────────────────────────────────────┘
|
||||
│
|
||||
↓
|
||||
┌─────────────────────────────────────────────────────────────────┐
|
||||
│ 1. Authenticate user credentials │
|
||||
│ 2. Validate vendor membership │
|
||||
│ 3. Create JWT with vendor context: │
|
||||
│ { │
|
||||
│ "sub": "user_id", │
|
||||
│ "username": "john.doe", │
|
||||
│ "vendor_id": 123, ← Vendor context in token │
|
||||
│ "vendor_code": "WIZAMART", ← Vendor code in token │
|
||||
│ "vendor_role": "Owner" ← Vendor role in token │
|
||||
│ } │
|
||||
└─────────────────────────────────────────────────────────────────┘
|
||||
│
|
||||
↓
|
||||
┌─────────────────────────────────────────────────────────────────┐
|
||||
│ 4. Set dual token storage: │
|
||||
│ - HTTP-only cookie (path=/vendor) for page navigation │
|
||||
│ - Response body for localStorage (API calls) │
|
||||
└─────────────────────────────────────────────────────────────────┘
|
||||
│
|
||||
↓
|
||||
┌─────────────────────────────────────────────────────────────────┐
|
||||
│ 5. Subsequent API requests include vendor context │
|
||||
│ Authorization: Bearer <token-with-vendor-context> │
|
||||
└─────────────────────────────────────────────────────────────────┘
|
||||
│
|
||||
↓
|
||||
┌─────────────────────────────────────────────────────────────────┐
|
||||
│ 6. get_current_vendor_api() extracts vendor from token: │
|
||||
│ - current_user.token_vendor_id │
|
||||
│ - current_user.token_vendor_code │
|
||||
│ - current_user.token_vendor_role │
|
||||
│ 7. Validates user still has access to vendor │
|
||||
└─────────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
### Implementation Components
|
||||
|
||||
#### 1. Token Creation (middleware/auth.py)
|
||||
```python
|
||||
def create_access_token(
|
||||
self,
|
||||
user: User,
|
||||
vendor_id: int | None = None,
|
||||
vendor_code: str | None = None,
|
||||
vendor_role: str | None = None,
|
||||
) -> dict[str, Any]:
|
||||
"""Create JWT with optional vendor context."""
|
||||
payload = {
|
||||
"sub": str(user.id),
|
||||
"username": user.username,
|
||||
"email": user.email,
|
||||
"role": user.role,
|
||||
"exp": expire,
|
||||
"iat": datetime.now(UTC),
|
||||
}
|
||||
|
||||
# Include vendor information in token if provided
|
||||
if vendor_id is not None:
|
||||
payload["vendor_id"] = vendor_id
|
||||
if vendor_code is not None:
|
||||
payload["vendor_code"] = vendor_code
|
||||
if vendor_role is not None:
|
||||
payload["vendor_role"] = vendor_role
|
||||
|
||||
return {
|
||||
"access_token": jwt.encode(payload, self.secret_key, algorithm=self.algorithm),
|
||||
"token_type": "bearer",
|
||||
"expires_in": self.access_token_expire_minutes * 60,
|
||||
}
|
||||
```
|
||||
|
||||
#### 2. Vendor Login (app/api/v1/vendor/auth.py)
|
||||
```python
|
||||
@router.post("/login", response_model=VendorLoginResponse)
|
||||
def vendor_login(
|
||||
user_credentials: UserLogin,
|
||||
response: Response,
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
"""
|
||||
Vendor team member login.
|
||||
|
||||
Creates vendor-scoped JWT token with vendor context embedded.
|
||||
"""
|
||||
# Authenticate user and determine vendor
|
||||
login_result = auth_service.login_user(db=db, user_credentials=user_credentials)
|
||||
user = login_result["user"]
|
||||
|
||||
# Determine vendor and role
|
||||
vendor = determine_vendor(db, user) # Your vendor detection logic
|
||||
vendor_role = determine_role(db, user, vendor) # Your role detection logic
|
||||
|
||||
# Create vendor-scoped access token
|
||||
token_data = auth_service.auth_manager.create_access_token(
|
||||
user=user,
|
||||
vendor_id=vendor.id,
|
||||
vendor_code=vendor.vendor_code,
|
||||
vendor_role=vendor_role,
|
||||
)
|
||||
|
||||
# Set cookie and return token
|
||||
response.set_cookie(
|
||||
key="vendor_token",
|
||||
value=token_data["access_token"],
|
||||
httponly=True,
|
||||
path="/vendor", # Restricted to vendor routes
|
||||
)
|
||||
|
||||
return VendorLoginResponse(**token_data, user=user, vendor=vendor)
|
||||
```
|
||||
|
||||
#### 3. Token Verification (app/api/deps.py)
|
||||
```python
|
||||
def get_current_vendor_api(
|
||||
authorization: str | None = Header(None, alias="Authorization"),
|
||||
db: Session = Depends(get_db),
|
||||
) -> User:
|
||||
"""
|
||||
Get current vendor API user from Authorization header.
|
||||
|
||||
Extracts vendor context from JWT token and validates access.
|
||||
"""
|
||||
if not authorization or not authorization.startswith("Bearer "):
|
||||
raise AuthenticationException("Authorization header required for API calls")
|
||||
|
||||
token = authorization.replace("Bearer ", "")
|
||||
user = auth_service.auth_manager.get_current_user(token, db)
|
||||
|
||||
# Validate vendor access if token is vendor-scoped
|
||||
if hasattr(user, "token_vendor_id"):
|
||||
vendor_id = user.token_vendor_id
|
||||
|
||||
# Verify user still has access to this vendor
|
||||
if not user.is_member_of(vendor_id):
|
||||
raise InsufficientPermissionsException(
|
||||
"Access to vendor has been revoked. Please login again."
|
||||
)
|
||||
|
||||
return user
|
||||
```
|
||||
|
||||
#### 4. Endpoint Usage (app/api/v1/vendor/products.py)
|
||||
```python
|
||||
@router.get("", response_model=ProductListResponse)
|
||||
def get_vendor_products(
|
||||
skip: int = Query(0, ge=0),
|
||||
limit: int = Query(100, ge=1, le=1000),
|
||||
current_user: User = Depends(get_current_vendor_api), # ✅ Only need this
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
"""
|
||||
Get all products in vendor catalog.
|
||||
|
||||
Vendor is determined from JWT token (vendor_id claim).
|
||||
"""
|
||||
# Extract vendor ID from token
|
||||
if not hasattr(current_user, "token_vendor_id"):
|
||||
raise HTTPException(
|
||||
status_code=400,
|
||||
detail="Token missing vendor information. Please login again.",
|
||||
)
|
||||
|
||||
vendor_id = current_user.token_vendor_id
|
||||
|
||||
# Use vendor_id from token for business logic
|
||||
products, total = product_service.get_vendor_products(
|
||||
db=db,
|
||||
vendor_id=vendor_id,
|
||||
skip=skip,
|
||||
limit=limit,
|
||||
)
|
||||
|
||||
return ProductListResponse(products=products, total=total)
|
||||
```
|
||||
|
||||
## Migration Guide
|
||||
|
||||
### Step 1: Identify Endpoints Using require_vendor_context()
|
||||
|
||||
Search for all occurrences:
|
||||
```bash
|
||||
grep -r "require_vendor_context" app/api/v1/vendor/
|
||||
```
|
||||
|
||||
### Step 2: Update Endpoint Signature
|
||||
|
||||
**Before:**
|
||||
```python
|
||||
@router.get("/{product_id}")
|
||||
def get_product(
|
||||
product_id: int,
|
||||
vendor: Vendor = Depends(require_vendor_context()), # ❌ Remove this
|
||||
current_user: User = Depends(get_current_vendor_api),
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
```
|
||||
|
||||
**After:**
|
||||
```python
|
||||
@router.get("/{product_id}")
|
||||
def get_product(
|
||||
product_id: int,
|
||||
current_user: User = Depends(get_current_vendor_api), # ✅ Only need this
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
```
|
||||
|
||||
### Step 3: Extract Vendor from Token
|
||||
|
||||
**Before:**
|
||||
```python
|
||||
product = product_service.get_product(db, vendor.id, product_id)
|
||||
```
|
||||
|
||||
**After:**
|
||||
```python
|
||||
from fastapi import HTTPException
|
||||
|
||||
# Extract vendor ID from token
|
||||
if not hasattr(current_user, "token_vendor_id"):
|
||||
raise HTTPException(
|
||||
status_code=400,
|
||||
detail="Token missing vendor information. Please login again.",
|
||||
)
|
||||
|
||||
vendor_id = current_user.token_vendor_id
|
||||
|
||||
# Use vendor_id from token
|
||||
product = product_service.get_product(db, vendor_id, product_id)
|
||||
```
|
||||
|
||||
### Step 4: Update Logging References
|
||||
|
||||
**Before:**
|
||||
```python
|
||||
logger.info(f"Product updated for vendor {vendor.vendor_code}")
|
||||
```
|
||||
|
||||
**After:**
|
||||
```python
|
||||
logger.info(f"Product updated for vendor {current_user.token_vendor_code}")
|
||||
```
|
||||
|
||||
### Complete Migration Example
|
||||
|
||||
**Before (URL-based vendor detection):**
|
||||
```python
|
||||
@router.put("/{product_id}", response_model=ProductResponse)
|
||||
def update_product(
|
||||
product_id: int,
|
||||
product_data: ProductUpdate,
|
||||
vendor: Vendor = Depends(require_vendor_context()), # ❌
|
||||
current_user: User = Depends(get_current_vendor_api),
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
"""Update product in vendor catalog."""
|
||||
product = product_service.update_product(
|
||||
db=db,
|
||||
vendor_id=vendor.id, # ❌ From URL
|
||||
product_id=product_id,
|
||||
product_update=product_data
|
||||
)
|
||||
|
||||
logger.info(
|
||||
f"Product {product_id} updated by {current_user.username} "
|
||||
f"for vendor {vendor.vendor_code}" # ❌ From URL
|
||||
)
|
||||
|
||||
return ProductResponse.model_validate(product)
|
||||
```
|
||||
|
||||
**After (Token-based vendor context):**
|
||||
```python
|
||||
@router.put("/{product_id}", response_model=ProductResponse)
|
||||
def update_product(
|
||||
product_id: int,
|
||||
product_data: ProductUpdate,
|
||||
current_user: User = Depends(get_current_vendor_api), # ✅ Only dependency
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
"""Update product in vendor catalog."""
|
||||
from fastapi import HTTPException
|
||||
|
||||
# Extract vendor ID from token
|
||||
if not hasattr(current_user, "token_vendor_id"):
|
||||
raise HTTPException(
|
||||
status_code=400,
|
||||
detail="Token missing vendor information. Please login again.",
|
||||
)
|
||||
|
||||
vendor_id = current_user.token_vendor_id # ✅ From token
|
||||
|
||||
product = product_service.update_product(
|
||||
db=db,
|
||||
vendor_id=vendor_id, # ✅ From token
|
||||
product_id=product_id,
|
||||
product_update=product_data
|
||||
)
|
||||
|
||||
logger.info(
|
||||
f"Product {product_id} updated by {current_user.username} "
|
||||
f"for vendor {current_user.token_vendor_code}" # ✅ From token
|
||||
)
|
||||
|
||||
return ProductResponse.model_validate(product)
|
||||
```
|
||||
|
||||
## Files to Migrate
|
||||
|
||||
Current files still using `require_vendor_context()`:
|
||||
- `app/api/v1/vendor/customers.py`
|
||||
- `app/api/v1/vendor/notifications.py`
|
||||
- `app/api/v1/vendor/media.py`
|
||||
- `app/api/v1/vendor/marketplace.py`
|
||||
- `app/api/v1/vendor/inventory.py`
|
||||
- `app/api/v1/vendor/settings.py`
|
||||
- `app/api/v1/vendor/analytics.py`
|
||||
- `app/api/v1/vendor/payments.py`
|
||||
- `app/api/v1/vendor/profile.py`
|
||||
|
||||
## Benefits of Vendor-in-Token
|
||||
|
||||
### 1. Clean RESTful APIs
|
||||
```
|
||||
✅ /api/v1/vendor/products
|
||||
✅ /api/v1/vendor/orders
|
||||
✅ /api/v1/vendor/customers
|
||||
|
||||
❌ /api/v1/vendor/{vendor_code}/products (unnecessary vendor in URL)
|
||||
```
|
||||
|
||||
### 2. Security
|
||||
- Vendor context cryptographically signed in JWT
|
||||
- Cannot be tampered with by client
|
||||
- Automatic validation on every request
|
||||
- Token revocation possible via database checks
|
||||
|
||||
### 3. Consistency
|
||||
- Same authentication mechanism for all vendor API endpoints
|
||||
- No confusion between page routes and API routes
|
||||
- Single source of truth (the token)
|
||||
|
||||
### 4. Performance
|
||||
- No database lookup for vendor context on every request
|
||||
- Vendor information already in token payload
|
||||
- Optional validation for revoked access
|
||||
|
||||
### 5. Maintainability
|
||||
- Simpler endpoint signatures
|
||||
- Less boilerplate code
|
||||
- Easier to test
|
||||
- Follows architecture rule API-002 (no DB queries in endpoints)
|
||||
|
||||
## Security Considerations
|
||||
|
||||
### Token Validation
|
||||
The token vendor context is validated on every request:
|
||||
1. JWT signature verification (ensures token not tampered with)
|
||||
2. Token expiration check (typically 30 minutes)
|
||||
3. Optional: Verify user still member of vendor (database check)
|
||||
|
||||
### Access Revocation
|
||||
If a user's vendor access is revoked:
|
||||
1. Existing tokens remain valid until expiration
|
||||
2. `get_current_vendor_api()` performs optional database check
|
||||
3. User forced to re-login after token expires
|
||||
4. New login will fail if access revoked
|
||||
|
||||
### Token Refresh
|
||||
Tokens should be refreshed periodically:
|
||||
- Default: 30 minutes expiration
|
||||
- Refresh before expiration for seamless UX
|
||||
- New login creates new token with current vendor membership
|
||||
|
||||
## Testing
|
||||
|
||||
### Unit Tests
|
||||
```python
|
||||
def test_vendor_in_token():
|
||||
"""Test vendor context in JWT token."""
|
||||
# Create token with vendor context
|
||||
token_data = auth_manager.create_access_token(
|
||||
user=user,
|
||||
vendor_id=123,
|
||||
vendor_code="WIZAMART",
|
||||
vendor_role="Owner",
|
||||
)
|
||||
|
||||
# Verify token contains vendor data
|
||||
payload = jwt.decode(token_data["access_token"], secret_key)
|
||||
assert payload["vendor_id"] == 123
|
||||
assert payload["vendor_code"] == "WIZAMART"
|
||||
assert payload["vendor_role"] == "Owner"
|
||||
|
||||
def test_api_endpoint_uses_token_vendor():
|
||||
"""Test API endpoint extracts vendor from token."""
|
||||
response = client.get(
|
||||
"/api/v1/vendor/products",
|
||||
headers={"Authorization": f"Bearer {token}"}
|
||||
)
|
||||
assert response.status_code == 200
|
||||
# Verify products are filtered by token vendor_id
|
||||
```
|
||||
|
||||
### Integration Tests
|
||||
```python
|
||||
def test_vendor_login_and_api_access():
|
||||
"""Test full vendor login and API access flow."""
|
||||
# Login as vendor user
|
||||
response = client.post("/api/v1/vendor/auth/login", json={
|
||||
"username": "john.doe",
|
||||
"password": "password123"
|
||||
})
|
||||
assert response.status_code == 200
|
||||
token = response.json()["access_token"]
|
||||
|
||||
# Access vendor API with token
|
||||
response = client.get(
|
||||
"/api/v1/vendor/products",
|
||||
headers={"Authorization": f"Bearer {token}"}
|
||||
)
|
||||
assert response.status_code == 200
|
||||
|
||||
# Verify vendor context from token
|
||||
products = response.json()["products"]
|
||||
# All products should belong to token vendor
|
||||
```
|
||||
|
||||
## Architecture Rules
|
||||
|
||||
See `docs/architecture/rules/API-VND-001.md` for the formal architecture rule enforcing this pattern.
|
||||
|
||||
## Related Documentation
|
||||
|
||||
- [Vendor RBAC System](./vendor-rbac.md) - Role-based access control for vendors
|
||||
- [Vendor Authentication](./vendor-authentication.md) - Complete authentication guide
|
||||
- [Architecture Rules](../architecture/rules/) - All architecture rules
|
||||
- [API Design Guidelines](../architecture/api-design.md) - RESTful API patterns
|
||||
|
||||
## Summary
|
||||
|
||||
The vendor-in-token architecture:
|
||||
- ✅ Embeds vendor context in JWT tokens
|
||||
- ✅ Eliminates URL-based vendor detection
|
||||
- ✅ Enables clean RESTful API endpoints
|
||||
- ✅ Improves security and performance
|
||||
- ✅ Simplifies endpoint implementation
|
||||
- ✅ Follows architecture best practices
|
||||
|
||||
**Migration Status:** In progress - 9 endpoint files remaining to migrate
|
||||
678
docs/backend/vendor-rbac.md
Normal file
678
docs/backend/vendor-rbac.md
Normal file
@@ -0,0 +1,678 @@
|
||||
# Vendor RBAC System - Complete Guide
|
||||
|
||||
## Overview
|
||||
|
||||
The vendor 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. Vendor Owner
|
||||
|
||||
**Who:** The user who created the vendor 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 `VendorUser.user_type = "owner"`
|
||||
- Linked via `Vendor.owner_user_id → User.id`
|
||||
|
||||
**Database:**
|
||||
```python
|
||||
# VendorUser record for owner
|
||||
{
|
||||
"vendor_id": 1,
|
||||
"user_id": 5,
|
||||
"user_type": "owner", # ✓ Owner
|
||||
"role_id": None, # No role needed
|
||||
"is_active": True
|
||||
}
|
||||
```
|
||||
|
||||
**Permissions:**
|
||||
- ✅ **All 75 permissions** (complete access)
|
||||
- See full list below
|
||||
|
||||
---
|
||||
|
||||
### 2. Team Members
|
||||
|
||||
**Who:** Users invited by the vendor owner to help manage the vendor.
|
||||
|
||||
**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 `VendorUser.user_type = "member"`
|
||||
- Permissions come from `VendorUser.role_id → Role.permissions`
|
||||
|
||||
**Database:**
|
||||
```python
|
||||
# VendorUser record for team member
|
||||
{
|
||||
"vendor_id": 1,
|
||||
"user_id": 7,
|
||||
"user_type": "member", # ✓ Team member
|
||||
"role_id": 3, # ✓ Role required
|
||||
"is_active": True,
|
||||
"invitation_token": None, # Accepted
|
||||
"invitation_accepted_at": "2024-11-15 10:30:00"
|
||||
}
|
||||
|
||||
# Role record
|
||||
{
|
||||
"id": 3,
|
||||
"vendor_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 VendorPermissions(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:** Vendor 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)
|
||||
|
||||
**Does NOT have:**
|
||||
- ❌ Delete anything
|
||||
- ❌ Import/export
|
||||
- ❌ Marketing
|
||||
- ❌ Financial reports
|
||||
- ❌ Settings
|
||||
- ❌ Team management
|
||||
|
||||
---
|
||||
|
||||
### 4. Support (6 permissions)
|
||||
**Use case:** Customer support team
|
||||
|
||||
**Has access to:**
|
||||
- ✅ Dashboard view
|
||||
- ✅ Products (view only)
|
||||
- ✅ Orders (view, edit)
|
||||
- ✅ Customers (view, edit)
|
||||
|
||||
**Does NOT have:**
|
||||
- ❌ Create/delete products
|
||||
- ❌ Stock management
|
||||
- ❌ Marketing
|
||||
- ❌ Reports
|
||||
- ❌ Settings
|
||||
- ❌ Team management
|
||||
|
||||
---
|
||||
|
||||
### 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)
|
||||
|
||||
**Does NOT have:**
|
||||
- ❌ Edit anything
|
||||
- ❌ Create/delete anything
|
||||
- ❌ Marketing
|
||||
- ❌ Financial reports
|
||||
- ❌ Settings
|
||||
- ❌ Team management
|
||||
|
||||
---
|
||||
|
||||
### 6. Marketing (7 permissions)
|
||||
**Use case:** Marketing team focused on campaigns
|
||||
|
||||
**Has access to:**
|
||||
- ✅ Dashboard (view)
|
||||
- ✅ Customers (view, export)
|
||||
- ✅ Marketing (all)
|
||||
- ✅ Reports (view)
|
||||
|
||||
**Does NOT have:**
|
||||
- ❌ Products management
|
||||
- ❌ Orders management
|
||||
- ❌ Stock management
|
||||
- ❌ Financial reports
|
||||
- ❌ Settings
|
||||
- ❌ Team management
|
||||
|
||||
---
|
||||
|
||||
## Permission Checking Logic
|
||||
|
||||
### How Permissions Are Checked
|
||||
|
||||
```python
|
||||
# In User model (models/database/user.py)
|
||||
|
||||
def has_vendor_permission(self, vendor_id: int, permission: str) -> bool:
|
||||
"""Check if user has a specific permission in a vendor."""
|
||||
|
||||
# Step 1: Check if user is owner
|
||||
if self.is_owner_of(vendor_id):
|
||||
return True # ✅ Owners have ALL permissions
|
||||
|
||||
# Step 2: Check team member permissions
|
||||
for vm in self.vendor_memberships:
|
||||
if vm.vendor_id == vendor_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 vendor 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_vendor_permission
|
||||
from app.core.permissions import VendorPermissions
|
||||
from models.database.user import User
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
@router.post("/products")
|
||||
def create_product(
|
||||
product_data: ProductCreate,
|
||||
user: User = Depends(
|
||||
require_vendor_permission(VendorPermissions.PRODUCTS_CREATE.value)
|
||||
)
|
||||
):
|
||||
"""
|
||||
Create a product.
|
||||
|
||||
Required permission: products.create
|
||||
✅ Owner: Always allowed
|
||||
✅ Manager: Allowed (has products.create)
|
||||
✅ Staff: Allowed (has products.create)
|
||||
❌ Support: Denied (no products.create)
|
||||
❌ Viewer: Denied (no products.create)
|
||||
❌ Marketing: Denied (no products.create)
|
||||
"""
|
||||
# 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_vendor_permission(
|
||||
VendorPermissions.DASHBOARD_VIEW.value,
|
||||
VendorPermissions.REPORTS_VIEW.value
|
||||
)
|
||||
)
|
||||
):
|
||||
"""
|
||||
View dashboard.
|
||||
|
||||
Required: dashboard.view OR reports.view
|
||||
✅ Owner: Always allowed
|
||||
✅ Manager: Allowed (has both)
|
||||
✅ Staff: Allowed (has dashboard.view)
|
||||
✅ Support: Allowed (has dashboard.view)
|
||||
✅ Viewer: Allowed (has both)
|
||||
✅ Marketing: Allowed (has both)
|
||||
"""
|
||||
# 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_vendor_permissions(
|
||||
VendorPermissions.PRODUCTS_VIEW.value,
|
||||
VendorPermissions.PRODUCTS_DELETE.value
|
||||
)
|
||||
)
|
||||
):
|
||||
"""
|
||||
Bulk delete products.
|
||||
|
||||
Required: products.view AND products.delete
|
||||
✅ Owner: Always allowed
|
||||
✅ Manager: Allowed (has both)
|
||||
❌ Staff: Denied (no products.delete)
|
||||
❌ Support: Denied (no products.delete)
|
||||
❌ Viewer: Denied (no products.delete)
|
||||
❌ Marketing: Denied (no products.delete)
|
||||
"""
|
||||
# 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_vendor_owner
|
||||
|
||||
@router.post("/team/invite")
|
||||
def invite_team_member(
|
||||
email: str,
|
||||
role_id: int,
|
||||
user: User = Depends(require_vendor_owner)
|
||||
):
|
||||
"""
|
||||
Invite a team member.
|
||||
|
||||
Required: Must be vendor owner
|
||||
✅ Owner: Allowed
|
||||
❌ Manager: Denied (not owner)
|
||||
❌ All team members: Denied (not 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)
|
||||
):
|
||||
"""
|
||||
Get all permissions for current user.
|
||||
|
||||
Returns:
|
||||
- Owner: All 75 permissions
|
||||
- Team Member: Permissions from their role
|
||||
"""
|
||||
return {"permissions": permissions}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Database Schema
|
||||
|
||||
### VendorUser Table
|
||||
|
||||
```sql
|
||||
CREATE TABLE vendor_users (
|
||||
id SERIAL PRIMARY KEY,
|
||||
vendor_id INTEGER NOT NULL REFERENCES vendors(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
|
||||
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()
|
||||
);
|
||||
```
|
||||
|
||||
### Role Table
|
||||
|
||||
```sql
|
||||
CREATE TABLE roles (
|
||||
id SERIAL PRIMARY KEY,
|
||||
vendor_id INTEGER NOT NULL REFERENCES vendors(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
|
||||
|
||||
```
|
||||
Owner invites user → VendorUser created:
|
||||
{
|
||||
"user_type": "member",
|
||||
"is_active": False,
|
||||
"invitation_token": "abc123...",
|
||||
"invitation_sent_at": "2024-11-29 10:00:00",
|
||||
"invitation_accepted_at": null
|
||||
}
|
||||
```
|
||||
|
||||
### 2. Acceptance
|
||||
|
||||
```
|
||||
User accepts invitation → VendorUser updated:
|
||||
{
|
||||
"is_active": True,
|
||||
"invitation_token": null,
|
||||
"invitation_accepted_at": "2024-11-29 10:30:00"
|
||||
}
|
||||
```
|
||||
|
||||
### 3. Active Member
|
||||
|
||||
```
|
||||
Member can now access vendor dashboard with role permissions
|
||||
```
|
||||
|
||||
### 4. Deactivation
|
||||
|
||||
```
|
||||
Owner deactivates member → VendorUser updated:
|
||||
{
|
||||
"is_active": False
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Common Use Cases
|
||||
|
||||
### Use Case 1: Dashboard Access
|
||||
|
||||
**Q:** Can all users access the dashboard?
|
||||
|
||||
**A:** Yes, if they have `dashboard.view` permission.
|
||||
|
||||
- ✅ Owner: Always
|
||||
- ✅ Manager, Staff, Support, Viewer, Marketing: All have it
|
||||
- ❌ Custom role without `dashboard.view`: No
|
||||
|
||||
---
|
||||
|
||||
### Use Case 2: Product Management
|
||||
|
||||
**Q:** Who can create products?
|
||||
|
||||
**A:** Users with `products.create` permission.
|
||||
|
||||
- ✅ Owner: Always
|
||||
- ✅ Manager: Yes (has permission)
|
||||
- ✅ Staff: Yes (has permission)
|
||||
- ❌ Support, Viewer, Marketing: No
|
||||
|
||||
---
|
||||
|
||||
### Use Case 3: Financial Reports
|
||||
|
||||
**Q:** Who can view financial reports?
|
||||
|
||||
**A:** Users with `reports.financial` permission.
|
||||
|
||||
- ✅ Owner: Always
|
||||
- ✅ Manager: Yes (has permission)
|
||||
- ❌ Staff, Support, Viewer, Marketing: No
|
||||
|
||||
---
|
||||
|
||||
### Use Case 4: Team Management
|
||||
|
||||
**Q:** Who can invite team members?
|
||||
|
||||
**A:** Only the vendor owner.
|
||||
|
||||
- ✅ Owner: Yes (owner-only operation)
|
||||
- ❌ All team members (including Manager): No
|
||||
|
||||
---
|
||||
|
||||
### Use Case 5: Settings Changes
|
||||
|
||||
**Q:** Who can change vendor settings?
|
||||
|
||||
**A:** Users with `settings.edit` permission.
|
||||
|
||||
- ✅ Owner: Always
|
||||
- ❌ Manager: No (doesn't have permission)
|
||||
- ❌ All other roles: No
|
||||
|
||||
---
|
||||
|
||||
## Error Responses
|
||||
|
||||
### Missing Permission
|
||||
|
||||
```http
|
||||
HTTP 403 Forbidden
|
||||
|
||||
{
|
||||
"error_code": "INSUFFICIENT_VENDOR_PERMISSIONS",
|
||||
"message": "You don't have permission to perform this action",
|
||||
"details": {
|
||||
"required_permission": "products.delete",
|
||||
"vendor_code": "wizamart"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Not Owner
|
||||
|
||||
```http
|
||||
HTTP 403 Forbidden
|
||||
|
||||
{
|
||||
"error_code": "VENDOR_OWNER_ONLY",
|
||||
"message": "This operation requires vendor owner privileges",
|
||||
"details": {
|
||||
"operation": "team management",
|
||||
"vendor_code": "wizamart"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Inactive Membership
|
||||
|
||||
```http
|
||||
HTTP 403 Forbidden
|
||||
|
||||
{
|
||||
"error_code": "INACTIVE_VENDOR_MEMBERSHIP",
|
||||
"message": "Your vendor membership is inactive"
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Summary
|
||||
|
||||
### Owner vs Team Member
|
||||
|
||||
| Feature | Owner | Team 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 vendor) | Yes |
|
||||
|
||||
### Permission Hierarchy
|
||||
|
||||
```
|
||||
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 `VendorPermissions.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
|
||||
|
||||
---
|
||||
|
||||
This RBAC system provides flexible, secure access control for vendor dashboards with clear separation between owners and team members.
|
||||
@@ -2,7 +2,11 @@
|
||||
|
||||
## Overview
|
||||
|
||||
This project uses **Heroicons** (inline SVG) with a custom helper system for clean, maintainable icon usage across the multi-tenant ecommerce platform.
|
||||
This project uses **Heroicons** (inline SVG) with a custom helper system for clean, maintainable icon usage across all **4 frontends**:
|
||||
- **Platform** - Public platform pages
|
||||
- **Admin** - Administrative portal
|
||||
- **Vendor** - Vendor management portal
|
||||
- **Shop** - Customer-facing store
|
||||
|
||||
### Why This Approach?
|
||||
|
||||
|
||||
@@ -225,7 +225,7 @@ app/
|
||||
class="flex items-center justify-center p-2 text-red-600 rounded-lg hover:bg-red-50 dark:text-gray-400 dark:hover:bg-gray-700"
|
||||
title="Delete"
|
||||
>
|
||||
<span x-html="$icon('trash', 'w-5 h-5')"></span>
|
||||
<span x-html="$icon('delete', 'w-5 h-5')"></span>
|
||||
</button>
|
||||
</div>
|
||||
</td>
|
||||
@@ -1308,3 +1308,361 @@ return {
|
||||
---
|
||||
|
||||
This template provides a complete, production-ready pattern for building admin pages with consistent structure, proper initialization, comprehensive logging, and excellent maintainability.
|
||||
|
||||
---
|
||||
|
||||
## 🎯 Real-World Examples: Marketplace Import Pages
|
||||
|
||||
The marketplace import system provides two comprehensive real-world implementations demonstrating all best practices.
|
||||
|
||||
### 1. Self-Service Import (`/admin/marketplace`)
|
||||
|
||||
**Purpose**: Admin tool for triggering imports for any vendor
|
||||
|
||||
**Files**:
|
||||
- **Template**: `app/templates/admin/marketplace.html`
|
||||
- **JavaScript**: `static/admin/js/marketplace.js`
|
||||
- **Route**: `app/routes/admin_pages.py` - `admin_marketplace_page()`
|
||||
|
||||
#### Key Features
|
||||
|
||||
##### Vendor Selection with Auto-Load
|
||||
```javascript
|
||||
// Load all vendors
|
||||
async loadVendors() {
|
||||
const response = await apiClient.get('/admin/vendors?limit=1000');
|
||||
this.vendors = response.items || [];
|
||||
}
|
||||
|
||||
// Handle vendor selection change
|
||||
onVendorChange() {
|
||||
const vendorId = parseInt(this.importForm.vendor_id);
|
||||
this.selectedVendor = this.vendors.find(v => v.id === vendorId) || null;
|
||||
}
|
||||
|
||||
// Quick fill from selected vendor's settings
|
||||
quickFill(language) {
|
||||
if (!this.selectedVendor) return;
|
||||
|
||||
const urlMap = {
|
||||
'fr': this.selectedVendor.letzshop_csv_url_fr,
|
||||
'en': this.selectedVendor.letzshop_csv_url_en,
|
||||
'de': this.selectedVendor.letzshop_csv_url_de
|
||||
};
|
||||
|
||||
if (urlMap[language]) {
|
||||
this.importForm.csv_url = urlMap[language];
|
||||
this.importForm.language = language;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
##### Filter by Current User
|
||||
```javascript
|
||||
async loadJobs() {
|
||||
const params = new URLSearchParams({
|
||||
page: this.page,
|
||||
limit: this.limit,
|
||||
created_by_me: 'true' // Only show jobs I triggered
|
||||
});
|
||||
|
||||
const response = await apiClient.get(
|
||||
`/admin/marketplace-import-jobs?${params.toString()}`
|
||||
);
|
||||
|
||||
this.jobs = response.items || [];
|
||||
}
|
||||
```
|
||||
|
||||
##### Vendor Name Helper
|
||||
```javascript
|
||||
getVendorName(vendorId) {
|
||||
const vendor = this.vendors.find(v => v.id === vendorId);
|
||||
return vendor ? `${vendor.name} (${vendor.vendor_code})` : `Vendor #${vendorId}`;
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 2. Platform Monitoring (`/admin/imports`)
|
||||
|
||||
**Purpose**: System-wide oversight of all import jobs
|
||||
|
||||
**Files**:
|
||||
- **Template**: `app/templates/admin/imports.html`
|
||||
- **JavaScript**: `static/admin/js/imports.js`
|
||||
- **Route**: `app/routes/admin_pages.py` - `admin_imports_page()`
|
||||
|
||||
#### Key Features
|
||||
|
||||
##### Statistics Dashboard
|
||||
```javascript
|
||||
async loadStats() {
|
||||
const response = await apiClient.get('/admin/marketplace-import-jobs/stats');
|
||||
this.stats = {
|
||||
total: response.total || 0,
|
||||
active: (response.pending || 0) + (response.processing || 0),
|
||||
completed: response.completed || 0,
|
||||
failed: response.failed || 0
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
**Template**:
|
||||
```html
|
||||
<!-- Stats Cards -->
|
||||
<div class="grid gap-6 mb-8 md:grid-cols-2 xl:grid-cols-4">
|
||||
<!-- Total Jobs -->
|
||||
<div class="flex items-center p-4 bg-white rounded-lg shadow-xs dark:bg-gray-800">
|
||||
<div class="p-3 mr-4 text-blue-500 bg-blue-100 rounded-full">
|
||||
<span x-html="$icon('cube', 'w-5 h-5')"></span>
|
||||
</div>
|
||||
<div>
|
||||
<p class="mb-2 text-sm font-medium text-gray-600 dark:text-gray-400">
|
||||
Total Jobs
|
||||
</p>
|
||||
<p class="text-lg font-semibold text-gray-700 dark:text-gray-200" x-text="stats.total">
|
||||
0
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Repeat for active, completed, failed -->
|
||||
</div>
|
||||
```
|
||||
|
||||
##### Advanced Filtering
|
||||
```javascript
|
||||
filters: {
|
||||
vendor_id: '',
|
||||
status: '',
|
||||
marketplace: '',
|
||||
created_by: '' // 'me' or empty for all
|
||||
},
|
||||
|
||||
async applyFilters() {
|
||||
this.page = 1; // Reset to first page
|
||||
|
||||
const params = new URLSearchParams({
|
||||
page: this.page,
|
||||
limit: this.limit
|
||||
});
|
||||
|
||||
// Add filters
|
||||
if (this.filters.vendor_id) {
|
||||
params.append('vendor_id', this.filters.vendor_id);
|
||||
}
|
||||
if (this.filters.status) {
|
||||
params.append('status', this.filters.status);
|
||||
}
|
||||
if (this.filters.created_by === 'me') {
|
||||
params.append('created_by_me', 'true');
|
||||
}
|
||||
|
||||
await this.loadJobs();
|
||||
await this.loadStats(); // Update stats based on filters
|
||||
}
|
||||
```
|
||||
|
||||
**Template**:
|
||||
```html
|
||||
<div class="grid gap-4 md:grid-cols-5">
|
||||
<!-- Vendor Filter -->
|
||||
<select x-model="filters.vendor_id" @change="applyFilters()">
|
||||
<option value="">All Vendors</option>
|
||||
<template x-for="vendor in vendors" :key="vendor.id">
|
||||
<option :value="vendor.id"
|
||||
x-text="`${vendor.name} (${vendor.vendor_code})`">
|
||||
</option>
|
||||
</template>
|
||||
</select>
|
||||
|
||||
<!-- Status Filter -->
|
||||
<select x-model="filters.status" @change="applyFilters()">
|
||||
<option value="">All Statuses</option>
|
||||
<option value="pending">Pending</option>
|
||||
<option value="processing">Processing</option>
|
||||
<option value="completed">Completed</option>
|
||||
<option value="failed">Failed</option>
|
||||
</select>
|
||||
|
||||
<!-- Creator Filter -->
|
||||
<select x-model="filters.created_by" @change="applyFilters()">
|
||||
<option value="">All Users</option>
|
||||
<option value="me">My Jobs Only</option>
|
||||
</select>
|
||||
</div>
|
||||
```
|
||||
|
||||
##### Enhanced Job Table
|
||||
```html
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Job ID</th>
|
||||
<th>Vendor</th>
|
||||
<th>Status</th>
|
||||
<th>Progress</th>
|
||||
<th>Created By</th> <!-- Extra column for platform monitoring -->
|
||||
<th>Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<template x-for="job in jobs" :key="job.id">
|
||||
<tr>
|
||||
<td>#<span x-text="job.id"></span></td>
|
||||
<td><span x-text="getVendorName(job.vendor_id)"></span></td>
|
||||
<td><!-- Status badge --></td>
|
||||
<td><!-- Progress metrics --></td>
|
||||
<td><span x-text="job.created_by_name || 'System'"></span></td>
|
||||
<td><!-- Action buttons --></td>
|
||||
</tr>
|
||||
</template>
|
||||
</tbody>
|
||||
</table>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🔄 Comparison: Two Admin Interfaces
|
||||
|
||||
| Feature | Self-Service (`/marketplace`) | Platform Monitoring (`/imports`) |
|
||||
|---------|-------------------------------|----------------------------------|
|
||||
| **Purpose** | Import products for vendors | Monitor all system imports |
|
||||
| **Scope** | Personal (my jobs) | System-wide (all jobs) |
|
||||
| **Primary Action** | Trigger new imports | View and analyze |
|
||||
| **Jobs Shown** | Only jobs I triggered | All jobs (with filtering) |
|
||||
| **Vendor Selection** | Required (select vendor to import for) | Optional (filter view) |
|
||||
| **Statistics** | No | Yes (dashboard cards) |
|
||||
| **Auto-Refresh** | 10 seconds | 15 seconds |
|
||||
| **Filter Options** | Vendor, Status, Marketplace | Vendor, Status, Marketplace, Creator |
|
||||
| **Use Case** | "I need to import for Vendor X" | "What's happening system-wide?" |
|
||||
|
||||
---
|
||||
|
||||
## 📋 Navigation Structure
|
||||
|
||||
### Sidebar Organization
|
||||
|
||||
```javascript
|
||||
// Admin sidebar sections
|
||||
{
|
||||
"Main Navigation": [
|
||||
"Dashboard",
|
||||
"Users",
|
||||
"Vendors",
|
||||
"Marketplace Import" // ← Self-service import
|
||||
],
|
||||
"Platform Monitoring": [
|
||||
"Import Jobs", // ← System-wide monitoring
|
||||
"Application Logs"
|
||||
],
|
||||
"Settings": [
|
||||
"Settings"
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
### Setting currentPage
|
||||
|
||||
```javascript
|
||||
// marketplace.js
|
||||
return {
|
||||
...data(),
|
||||
currentPage: 'marketplace', // Highlights "Marketplace Import" in sidebar
|
||||
// ...
|
||||
};
|
||||
|
||||
// imports.js
|
||||
return {
|
||||
...data(),
|
||||
currentPage: 'imports', // Highlights "Import Jobs" in sidebar
|
||||
// ...
|
||||
};
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🎨 UI Patterns
|
||||
|
||||
### Success/Error Messages
|
||||
|
||||
```html
|
||||
<!-- Success -->
|
||||
<div x-show="successMessage" x-transition
|
||||
class="mb-6 p-4 bg-green-100 border border-green-400 text-green-700 rounded-lg">
|
||||
<span x-html="$icon('check-circle', 'w-5 h-5 mr-3')"></span>
|
||||
<p class="font-semibold" x-text="successMessage"></p>
|
||||
</div>
|
||||
|
||||
<!-- Error -->
|
||||
<div x-show="error" x-transition
|
||||
class="mb-6 p-4 bg-red-100 border border-red-400 text-red-700 rounded-lg dark:bg-red-900/20">
|
||||
<span x-html="$icon('exclamation', 'w-5 h-5 mr-3')"></span>
|
||||
<div>
|
||||
<p class="font-semibold">Error</p>
|
||||
<p class="text-sm" x-text="error"></p>
|
||||
</div>
|
||||
</div>
|
||||
```
|
||||
|
||||
### Empty States
|
||||
|
||||
```html
|
||||
<!-- Personalized empty state -->
|
||||
<div x-show="!loading && jobs.length === 0" class="text-center py-12">
|
||||
<span x-html="$icon('inbox', 'inline w-12 h-12 text-gray-400 mb-4')"></span>
|
||||
<p class="text-gray-600 dark:text-gray-400">
|
||||
You haven't triggered any imports yet
|
||||
</p>
|
||||
<p class="text-sm text-gray-500">
|
||||
Start a new import using the form above
|
||||
</p>
|
||||
</div>
|
||||
```
|
||||
|
||||
### Loading States with Spinners
|
||||
|
||||
```html
|
||||
<div x-show="loading" class="text-center py-12">
|
||||
<span x-html="$icon('spinner', 'inline w-8 h-8 text-purple-600')"></span>
|
||||
<p class="mt-2 text-gray-600 dark:text-gray-400">Loading import jobs...</p>
|
||||
</div>
|
||||
```
|
||||
|
||||
### Modal Dialogs
|
||||
|
||||
```html
|
||||
<div x-show="showJobModal" x-cloak @click.away="closeJobModal()"
|
||||
class="fixed inset-0 z-30 flex items-end bg-black bg-opacity-50 sm:items-center sm:justify-center"
|
||||
x-transition>
|
||||
<div class="w-full px-6 py-4 overflow-hidden bg-white rounded-t-lg dark:bg-gray-800 sm:rounded-lg sm:m-4 sm:max-w-2xl">
|
||||
<!-- Modal Header -->
|
||||
<div class="flex items-center justify-between mb-4">
|
||||
<h3 class="text-lg font-semibold">Import Job Details</h3>
|
||||
<button @click="closeJobModal()">
|
||||
<span x-html="$icon('close', 'w-5 h-5')"></span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Modal Content -->
|
||||
<div x-show="selectedJob">
|
||||
<!-- Job details grid -->
|
||||
</div>
|
||||
|
||||
<!-- Modal Footer -->
|
||||
<div class="flex justify-end mt-6">
|
||||
<button @click="closeJobModal()" class="...">Close</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 📚 Related Documentation
|
||||
|
||||
- [Marketplace Integration Guide](../../guides/marketplace-integration.md) - Complete marketplace system documentation
|
||||
- [Vendor Page Templates](../vendor/page-templates.md) - Vendor page patterns
|
||||
- [Icons Guide](../../development/icons-guide.md) - Available icons
|
||||
- [Admin Integration Guide](../../backend/admin-integration-guide.md) - Backend integration
|
||||
|
||||
|
||||
199
docs/frontend/vendor/page-templates.md
vendored
199
docs/frontend/vendor/page-templates.md
vendored
@@ -220,7 +220,7 @@ app/
|
||||
class="flex items-center justify-center p-2 text-red-600 rounded-lg hover:bg-red-50 dark:text-gray-400 dark:hover:bg-gray-700"
|
||||
title="Delete"
|
||||
>
|
||||
<span x-html="$icon('trash', 'w-5 h-5')"></span>
|
||||
<span x-html="$icon('delete', 'w-5 h-5')"></span>
|
||||
</button>
|
||||
</div>
|
||||
</td>
|
||||
@@ -994,3 +994,200 @@ The base template loads scripts in this specific order:
|
||||
---
|
||||
|
||||
This template provides a complete, production-ready pattern for building vendor admin pages with consistent structure, error handling, and user experience.
|
||||
|
||||
---
|
||||
|
||||
## 🎯 Real-World Example: Marketplace Import Page
|
||||
|
||||
The marketplace import page is a comprehensive real-world implementation demonstrating all best practices.
|
||||
|
||||
### Implementation Files
|
||||
|
||||
**Template**: `app/templates/vendor/marketplace.html`
|
||||
**JavaScript**: `static/vendor/js/marketplace.js`
|
||||
**Route**: `app/routes/vendor_pages.py` - `vendor_marketplace_page()`
|
||||
|
||||
### Key Features Demonstrated
|
||||
|
||||
#### 1. Complete Form Handling
|
||||
```javascript
|
||||
// Import form with validation
|
||||
importForm: {
|
||||
csv_url: '',
|
||||
marketplace: 'Letzshop',
|
||||
language: 'fr',
|
||||
batch_size: 1000
|
||||
},
|
||||
|
||||
async startImport() {
|
||||
if (!this.importForm.csv_url) {
|
||||
this.error = 'Please enter a CSV URL';
|
||||
return;
|
||||
}
|
||||
|
||||
this.importing = true;
|
||||
try {
|
||||
const response = await apiClient.post('/vendor/marketplace/import', {
|
||||
source_url: this.importForm.csv_url,
|
||||
marketplace: this.importForm.marketplace,
|
||||
batch_size: this.importForm.batch_size
|
||||
});
|
||||
|
||||
this.successMessage = `Import job #${response.job_id} started!`;
|
||||
await this.loadJobs(); // Refresh list
|
||||
} catch (error) {
|
||||
this.error = error.message;
|
||||
} finally {
|
||||
this.importing = false;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### 2. Auto-Refresh for Active Jobs
|
||||
```javascript
|
||||
startAutoRefresh() {
|
||||
this.autoRefreshInterval = setInterval(async () => {
|
||||
const hasActiveJobs = this.jobs.some(job =>
|
||||
job.status === 'pending' || job.status === 'processing'
|
||||
);
|
||||
|
||||
if (hasActiveJobs) {
|
||||
await this.loadJobs();
|
||||
}
|
||||
}, 10000); // Every 10 seconds
|
||||
}
|
||||
```
|
||||
|
||||
#### 3. Quick Fill from Settings
|
||||
```javascript
|
||||
// Load vendor settings
|
||||
async loadVendorSettings() {
|
||||
const response = await apiClient.get('/vendor/settings');
|
||||
this.vendorSettings = {
|
||||
letzshop_csv_url_fr: response.letzshop_csv_url_fr || '',
|
||||
letzshop_csv_url_en: response.letzshop_csv_url_en || '',
|
||||
letzshop_csv_url_de: response.letzshop_csv_url_de || ''
|
||||
};
|
||||
}
|
||||
|
||||
// Quick fill function
|
||||
quickFill(language) {
|
||||
const urlMap = {
|
||||
'fr': this.vendorSettings.letzshop_csv_url_fr,
|
||||
'en': this.vendorSettings.letzshop_csv_url_en,
|
||||
'de': this.vendorSettings.letzshop_csv_url_de
|
||||
};
|
||||
|
||||
if (urlMap[language]) {
|
||||
this.importForm.csv_url = urlMap[language];
|
||||
this.importForm.language = language;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### 4. Job Details Modal
|
||||
```javascript
|
||||
async viewJobDetails(jobId) {
|
||||
try {
|
||||
const response = await apiClient.get(`/vendor/marketplace/imports/${jobId}`);
|
||||
this.selectedJob = response;
|
||||
this.showJobModal = true;
|
||||
} catch (error) {
|
||||
this.error = error.message;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### 5. Pagination
|
||||
```javascript
|
||||
async nextPage() {
|
||||
if (this.page * this.limit < this.totalJobs) {
|
||||
this.page++;
|
||||
await this.loadJobs();
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### 6. Utility Functions
|
||||
```javascript
|
||||
formatDate(dateString) {
|
||||
if (!dateString) return 'N/A';
|
||||
const date = new Date(dateString);
|
||||
return date.toLocaleString('en-US', {
|
||||
year: 'numeric',
|
||||
month: 'short',
|
||||
day: 'numeric',
|
||||
hour: '2-digit',
|
||||
minute: '2-digit'
|
||||
});
|
||||
}
|
||||
|
||||
calculateDuration(job) {
|
||||
if (!job.started_at) return 'Not started';
|
||||
|
||||
const start = new Date(job.started_at);
|
||||
const end = job.completed_at ? new Date(job.completed_at) : new Date();
|
||||
const durationMs = end - start;
|
||||
|
||||
const seconds = Math.floor(durationMs / 1000);
|
||||
const minutes = Math.floor(seconds / 60);
|
||||
const hours = Math.floor(minutes / 60);
|
||||
|
||||
if (hours > 0) {
|
||||
return `${hours}h ${minutes % 60}m`;
|
||||
} else if (minutes > 0) {
|
||||
return `${minutes}m ${seconds % 60}s`;
|
||||
}
|
||||
return `${seconds}s`;
|
||||
}
|
||||
```
|
||||
|
||||
### Template Features
|
||||
|
||||
#### Dynamic Status Badges
|
||||
```html
|
||||
<span class="px-2 py-1 font-semibold leading-tight rounded-full text-xs"
|
||||
:class="{
|
||||
'text-green-700 bg-green-100 dark:bg-green-700 dark:text-green-100': job.status === 'completed',
|
||||
'text-blue-700 bg-blue-100 dark:bg-blue-700 dark:text-blue-100': job.status === 'processing',
|
||||
'text-yellow-700 bg-yellow-100 dark:bg-yellow-700 dark:text-yellow-100': job.status === 'pending',
|
||||
'text-red-700 bg-red-100 dark:bg-red-700 dark:text-red-100': job.status === 'failed'
|
||||
}"
|
||||
x-text="job.status.toUpperCase()">
|
||||
</span>
|
||||
```
|
||||
|
||||
#### Conditional Display
|
||||
```html
|
||||
<!-- Quick fill buttons -->
|
||||
<button
|
||||
type="button"
|
||||
@click="quickFill('fr')"
|
||||
x-show="vendorSettings.letzshop_csv_url_fr"
|
||||
class="...">
|
||||
<span x-html="$icon('lightning-bolt', 'w-3 h-3 mr-1')"></span>
|
||||
French CSV
|
||||
</button>
|
||||
```
|
||||
|
||||
#### Progress Metrics
|
||||
```html
|
||||
<div class="space-y-1">
|
||||
<div class="text-xs text-gray-600 dark:text-gray-400">
|
||||
<span class="text-green-600" x-text="job.imported_count"></span> imported,
|
||||
<span class="text-blue-600" x-text="job.updated_count"></span> updated
|
||||
</div>
|
||||
<div x-show="job.error_count > 0" class="text-xs text-red-600">
|
||||
<span x-text="job.error_count"></span> errors
|
||||
</div>
|
||||
</div>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 📚 Related Documentation
|
||||
|
||||
- [Marketplace Integration Guide](../../guides/marketplace-integration.md) - Complete marketplace system documentation
|
||||
- [Admin Page Templates](../admin/page-templates.md) - Admin page patterns
|
||||
- [Icons Guide](../../development/icons-guide.md) - Available icons
|
||||
|
||||
|
||||
Binary file not shown.
Reference in New Issue
Block a user