1069 lines
32 KiB
Markdown
1069 lines
32 KiB
Markdown
# Slice 1: Multi-Tenant Foundation
|
|
## Admin Creates Vendor → Vendor Owner Logs In
|
|
|
|
**Status**: 🔄 IN PROGRESS
|
|
**Timeline**: Week 1 (5 days)
|
|
**Current Progress**: Backend ~90%, Frontend ~60%
|
|
|
|
## 🎯 Slice Objectives
|
|
|
|
Establish the multi-tenant foundation with complete vendor isolation and admin capabilities.
|
|
|
|
### User Stories
|
|
- ✅ As a Super Admin, I can create vendors through the admin interface
|
|
- ✅ As a Super Admin, I can manage vendor accounts (verify, activate, deactivate)
|
|
- ⏳ As a Vendor Owner, I can log into my vendor-specific admin interface
|
|
- ✅ The system correctly isolates vendor contexts (subdomain + path-based)
|
|
|
|
### Success Criteria
|
|
- [ ] Admin can log into admin interface
|
|
- [ ] Admin can create new vendors with auto-generated owner accounts
|
|
- [ ] System generates secure temporary passwords
|
|
- [ ] Vendor owner can log into vendor-specific interface
|
|
- [ ] Vendor context detection works in dev (path) and prod (subdomain) modes
|
|
- [ ] Database properly isolates vendor data
|
|
- [ ] All API endpoints protected with JWT authentication
|
|
- [ ] Frontend integrates seamlessly with backend
|
|
|
|
## 📋 Backend Implementation
|
|
|
|
### Database Models (✅ Complete)
|
|
|
|
#### User Model (`models/database/user.py`)
|
|
```python
|
|
class User(Base, TimestampMixin):
|
|
__tablename__ = "users"
|
|
|
|
id = Column(Integer, primary_key=True, index=True)
|
|
email = Column(String, unique=True, nullable=False, index=True)
|
|
username = Column(String, unique=True, nullable=False, index=True)
|
|
hashed_password = Column(String, nullable=False)
|
|
role = Column(String, nullable=False) # 'admin' or 'user'
|
|
is_active = Column(Boolean, default=True)
|
|
|
|
# Relationships
|
|
owned_vendors = relationship("Vendor", back_populates="owner")
|
|
vendor_memberships = relationship("VendorUser", back_populates="user")
|
|
```
|
|
|
|
#### Vendor Model (`models/database/vendor.py`)
|
|
```python
|
|
class Vendor(Base, TimestampMixin):
|
|
__tablename__ = "vendors"
|
|
|
|
id = Column(Integer, primary_key=True, index=True)
|
|
vendor_code = Column(String, unique=True, nullable=False, index=True)
|
|
subdomain = Column(String(100), unique=True, nullable=False, index=True)
|
|
name = Column(String, nullable=False)
|
|
description = Column(Text)
|
|
owner_user_id = Column(Integer, ForeignKey("users.id"), nullable=False)
|
|
|
|
# Business info
|
|
business_email = Column(String)
|
|
business_phone = Column(String)
|
|
website = Column(String)
|
|
business_address = Column(Text)
|
|
tax_number = Column(String)
|
|
|
|
# Status
|
|
is_active = Column(Boolean, default=True)
|
|
is_verified = Column(Boolean, default=False)
|
|
verified_at = Column(DateTime, nullable=True)
|
|
|
|
# Configuration
|
|
theme_config = Column(JSON, default=dict)
|
|
letzshop_csv_url_fr = Column(String)
|
|
letzshop_csv_url_en = Column(String)
|
|
letzshop_csv_url_de = Column(String)
|
|
|
|
# Relationships
|
|
owner = relationship("User", back_populates="owned_vendors")
|
|
roles = relationship("Role", back_populates="vendor", cascade="all, delete-orphan")
|
|
team_members = relationship("VendorUser", back_populates="vendor")
|
|
```
|
|
|
|
#### Role Model (`models/database/vendor.py`)
|
|
```python
|
|
class Role(Base, TimestampMixin):
|
|
__tablename__ = "roles"
|
|
|
|
id = Column(Integer, primary_key=True, index=True)
|
|
vendor_id = Column(Integer, ForeignKey("vendors.id"), nullable=False)
|
|
name = Column(String, nullable=False) # Owner, Manager, Editor, Viewer
|
|
permissions = Column(JSON, default=list)
|
|
|
|
vendor = relationship("Vendor", back_populates="roles")
|
|
vendor_users = relationship("VendorUser", back_populates="role")
|
|
```
|
|
|
|
### Pydantic Schemas (✅ Complete)
|
|
|
|
#### Vendor Schemas (`models/schema/vendor.py`)
|
|
```python
|
|
class VendorCreate(BaseModel):
|
|
vendor_code: str = Field(..., min_length=2, max_length=50)
|
|
name: str = Field(..., min_length=2, max_length=200)
|
|
subdomain: str = Field(..., min_length=2, max_length=100)
|
|
owner_email: EmailStr # NEW for Slice 1
|
|
description: Optional[str] = None
|
|
business_email: Optional[EmailStr] = None
|
|
business_phone: Optional[str] = None
|
|
website: Optional[str] = None
|
|
|
|
@validator('vendor_code')
|
|
def vendor_code_uppercase(cls, v):
|
|
return v.upper()
|
|
|
|
@validator('subdomain')
|
|
def subdomain_lowercase(cls, v):
|
|
return v.lower().strip()
|
|
|
|
class VendorCreateResponse(BaseModel):
|
|
"""Response after creating vendor - includes generated credentials"""
|
|
id: int
|
|
vendor_code: str
|
|
subdomain: str
|
|
name: str
|
|
owner_user_id: int
|
|
owner_email: str
|
|
owner_username: str
|
|
temporary_password: str # Shown only once!
|
|
is_active: bool
|
|
is_verified: bool
|
|
created_at: datetime
|
|
```
|
|
|
|
### Service Layer (✅ Complete)
|
|
|
|
#### Admin Service (`app/services/admin_service.py`)
|
|
|
|
**Key Method**: `create_vendor_with_owner()`
|
|
```python
|
|
async def create_vendor_with_owner(
|
|
self,
|
|
vendor_data: VendorCreate,
|
|
db: Session
|
|
) -> Dict[str, Any]:
|
|
"""
|
|
Creates vendor + owner user + default roles
|
|
Returns vendor details with temporary password
|
|
"""
|
|
# 1. Generate owner username
|
|
owner_username = f"{vendor_data.subdomain}_owner"
|
|
|
|
# 2. Generate secure temporary password
|
|
temp_password = self._generate_temp_password()
|
|
|
|
# 3. Create owner user
|
|
owner_user = User(
|
|
email=vendor_data.owner_email,
|
|
username=owner_username,
|
|
hashed_password=self.auth_manager.hash_password(temp_password),
|
|
role="user",
|
|
is_active=True
|
|
)
|
|
db.add(owner_user)
|
|
db.flush() # Get owner_user.id
|
|
|
|
# 4. Create vendor
|
|
vendor = Vendor(
|
|
vendor_code=vendor_data.vendor_code,
|
|
name=vendor_data.name,
|
|
subdomain=vendor_data.subdomain,
|
|
owner_user_id=owner_user.id,
|
|
is_verified=True, # Auto-verify admin-created vendors
|
|
verified_at=datetime.utcnow(),
|
|
# ... other fields
|
|
)
|
|
db.add(vendor)
|
|
db.flush() # Get vendor.id
|
|
|
|
# 5. Create default roles
|
|
default_roles = ["Owner", "Manager", "Editor", "Viewer"]
|
|
for role_name in default_roles:
|
|
role = Role(
|
|
vendor_id=vendor.id,
|
|
name=role_name,
|
|
permissions=self._get_default_permissions(role_name)
|
|
)
|
|
db.add(role)
|
|
|
|
# 6. Link owner to Owner role
|
|
owner_role = db.query(Role).filter(
|
|
Role.vendor_id == vendor.id,
|
|
Role.name == "Owner"
|
|
).first()
|
|
|
|
vendor_user = VendorUser(
|
|
vendor_id=vendor.id,
|
|
user_id=owner_user.id,
|
|
role_id=owner_role.id,
|
|
is_active=True
|
|
)
|
|
db.add(vendor_user)
|
|
|
|
db.commit()
|
|
|
|
return {
|
|
"vendor": vendor,
|
|
"owner_username": owner_username,
|
|
"temporary_password": temp_password
|
|
}
|
|
```
|
|
|
|
### API Endpoints (✅ Complete)
|
|
|
|
#### Admin Endpoints (`app/api/v1/admin.py`)
|
|
|
|
```python
|
|
@router.post("/vendors", response_model=VendorCreateResponse)
|
|
async def create_vendor(
|
|
vendor_data: VendorCreate,
|
|
current_user: User = Depends(get_current_admin_user),
|
|
db: Session = Depends(get_db)
|
|
):
|
|
"""Create new vendor with owner account"""
|
|
result = await admin_service.create_vendor_with_owner(vendor_data, db)
|
|
|
|
return VendorCreateResponse(
|
|
id=result["vendor"].id,
|
|
vendor_code=result["vendor"].vendor_code,
|
|
subdomain=result["vendor"].subdomain,
|
|
name=result["vendor"].name,
|
|
owner_user_id=result["vendor"].owner_user_id,
|
|
owner_email=vendor_data.owner_email,
|
|
owner_username=result["owner_username"],
|
|
temporary_password=result["temporary_password"],
|
|
is_active=result["vendor"].is_active,
|
|
is_verified=result["vendor"].is_verified,
|
|
created_at=result["vendor"].created_at
|
|
)
|
|
|
|
@router.get("/vendors", response_model=VendorListResponse)
|
|
async def list_vendors(
|
|
skip: int = 0,
|
|
limit: int = 100,
|
|
is_active: Optional[bool] = None,
|
|
is_verified: Optional[bool] = None,
|
|
current_user: User = Depends(get_current_admin_user),
|
|
db: Session = Depends(get_db)
|
|
):
|
|
"""List all vendors with filtering"""
|
|
return await admin_service.get_vendors(db, skip, limit, is_active, is_verified)
|
|
|
|
@router.get("/dashboard", response_model=AdminDashboardResponse)
|
|
async def get_admin_dashboard(
|
|
current_user: User = Depends(get_current_admin_user),
|
|
db: Session = Depends(get_db)
|
|
):
|
|
"""Get admin dashboard statistics"""
|
|
return await admin_service.get_dashboard_stats(db)
|
|
|
|
@router.put("/vendors/{vendor_id}/verify")
|
|
async def verify_vendor(
|
|
vendor_id: int,
|
|
current_user: User = Depends(get_current_admin_user),
|
|
db: Session = Depends(get_db)
|
|
):
|
|
"""Verify/unverify vendor"""
|
|
return await admin_service.toggle_vendor_verification(vendor_id, db)
|
|
|
|
@router.put("/vendors/{vendor_id}/status")
|
|
async def toggle_vendor_status(
|
|
vendor_id: int,
|
|
current_user: User = Depends(get_current_admin_user),
|
|
db: Session = Depends(get_db)
|
|
):
|
|
"""Activate/deactivate vendor"""
|
|
return await admin_service.toggle_vendor_status(vendor_id, db)
|
|
```
|
|
|
|
### Middleware (✅ Complete)
|
|
|
|
#### Vendor Context Detection (`middleware/vendor_context.py`)
|
|
|
|
```python
|
|
async def vendor_context_middleware(request: Request, call_next):
|
|
"""
|
|
Detects vendor context from:
|
|
1. Subdomain: vendor.platform.com (production)
|
|
2. Path: /vendor/VENDOR_CODE/ (development)
|
|
"""
|
|
vendor_context = None
|
|
|
|
# Skip for admin/API routes
|
|
if request.url.path.startswith(("/api/", "/admin/", "/static/")):
|
|
response = await call_next(request)
|
|
return response
|
|
|
|
# 1. Try subdomain detection (production)
|
|
host = request.headers.get("host", "").split(":")[0]
|
|
parts = host.split(".")
|
|
if len(parts) > 2:
|
|
subdomain = parts[0]
|
|
vendor = get_vendor_by_subdomain(subdomain)
|
|
if vendor:
|
|
vendor_context = vendor
|
|
|
|
# 2. Try path detection (development)
|
|
if not vendor_context:
|
|
path_parts = request.url.path.split("/")
|
|
if len(path_parts) > 2 and path_parts[1] == "vendor":
|
|
vendor_code = path_parts[2].upper()
|
|
vendor = get_vendor_by_code(vendor_code)
|
|
if vendor:
|
|
vendor_context = vendor
|
|
|
|
request.state.vendor = vendor_context
|
|
response = await call_next(request)
|
|
return response
|
|
```
|
|
|
|
## 🎨 Frontend Implementation
|
|
|
|
### Template Structure (Jinja2)
|
|
|
|
#### Base Template (`templates/base.html`)
|
|
```html
|
|
<!DOCTYPE html>
|
|
<html lang="en">
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
<title>{% block title %}Multi-Tenant Platform{% endblock %}</title>
|
|
|
|
<!-- CSS -->
|
|
<link rel="stylesheet" href="/static/css/shared/base.css">
|
|
{% block extra_css %}{% endblock %}
|
|
|
|
<!-- Alpine.js from CDN -->
|
|
<script defer src="https://cdn.jsdelivr.net/npm/alpinejs@3.x.x/dist/cdn.min.js"></script>
|
|
</head>
|
|
<body>
|
|
{% block content %}{% endblock %}
|
|
|
|
<!-- Shared JavaScript -->
|
|
<script src="/static/js/shared/api-client.js"></script>
|
|
{% block extra_scripts %}{% endblock %}
|
|
</body>
|
|
</html>
|
|
```
|
|
|
|
### Admin Pages (🔄 In Progress)
|
|
|
|
#### 1. Admin Login (`templates/admin/login.html`)
|
|
|
|
**Status**: ✅ Complete
|
|
|
|
```html
|
|
{% extends "base.html" %}
|
|
|
|
{% block title %}Admin Login{% endblock %}
|
|
|
|
{% block extra_css %}
|
|
<link rel="stylesheet" href="/static/css/shared/auth.css">
|
|
{% endblock %}
|
|
|
|
{% block content %}
|
|
<div class="auth-page" x-data="adminLogin()">
|
|
<div class="login-container">
|
|
<div class="login-header">
|
|
<div class="auth-logo">🔐</div>
|
|
<h1>Admin Portal</h1>
|
|
<p>Sign in to continue</p>
|
|
</div>
|
|
|
|
<form @submit.prevent="handleLogin">
|
|
<!-- Alert -->
|
|
<div x-show="error" x-text="error" class="alert alert-error"></div>
|
|
|
|
<!-- Username -->
|
|
<div class="form-group">
|
|
<label>Username</label>
|
|
<input
|
|
type="text"
|
|
x-model="username"
|
|
class="form-control"
|
|
required
|
|
:disabled="loading"
|
|
>
|
|
</div>
|
|
|
|
<!-- Password -->
|
|
<div class="form-group">
|
|
<label>Password</label>
|
|
<input
|
|
type="password"
|
|
x-model="password"
|
|
class="form-control"
|
|
required
|
|
:disabled="loading"
|
|
>
|
|
</div>
|
|
|
|
<!-- Submit -->
|
|
<button
|
|
type="submit"
|
|
class="btn-login"
|
|
:disabled="loading"
|
|
>
|
|
<span x-show="!loading">Sign In</span>
|
|
<span x-show="loading" class="loading-spinner"></span>
|
|
</button>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
{% endblock %}
|
|
|
|
{% block extra_scripts %}
|
|
<script>
|
|
function adminLogin() {
|
|
return {
|
|
username: '',
|
|
password: '',
|
|
loading: false,
|
|
error: null,
|
|
|
|
async handleLogin() {
|
|
this.loading = true;
|
|
this.error = null;
|
|
|
|
try {
|
|
const response = await apiClient.post('/api/v1/auth/login', {
|
|
username: this.username,
|
|
password: this.password
|
|
});
|
|
|
|
if (response.user.role !== 'admin') {
|
|
this.error = 'Admin access required';
|
|
return;
|
|
}
|
|
|
|
localStorage.setItem('admin_token', response.access_token);
|
|
window.location.href = '/admin/dashboard';
|
|
} catch (error) {
|
|
this.error = error.message || 'Login failed';
|
|
} finally {
|
|
this.loading = false;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
</script>
|
|
{% endblock %}
|
|
```
|
|
|
|
#### 2. Admin Dashboard (`templates/admin/dashboard.html`)
|
|
|
|
**Status**: ⏳ In Progress
|
|
|
|
```html
|
|
{% extends "admin/base_admin.html" %}
|
|
|
|
{% block title %}Admin Dashboard{% endblock %}
|
|
|
|
{% block content %}
|
|
<div x-data="adminDashboard()" x-init="loadStats()">
|
|
<!-- Header -->
|
|
<div class="page-header">
|
|
<h1>Dashboard</h1>
|
|
<div class="header-actions">
|
|
<button @click="refreshStats()" class="btn btn-secondary">
|
|
<span x-show="!loading">Refresh</span>
|
|
<span x-show="loading" class="loading-spinner"></span>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Stats Grid -->
|
|
<div class="stats-grid">
|
|
<!-- Total Vendors -->
|
|
<div class="stat-card">
|
|
<div class="stat-header">
|
|
<div class="stat-title">Total Vendors</div>
|
|
<div class="stat-icon">🏪</div>
|
|
</div>
|
|
<div class="stat-value" x-text="stats.vendors.total_vendors || '-'"></div>
|
|
<div class="stat-subtitle">
|
|
<span x-text="stats.vendors.active_vendors || 0"></span> active
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Total Users -->
|
|
<div class="stat-card">
|
|
<div class="stat-header">
|
|
<div class="stat-title">Total Users</div>
|
|
<div class="stat-icon">👥</div>
|
|
</div>
|
|
<div class="stat-value" x-text="stats.users.total_users || '-'"></div>
|
|
<div class="stat-subtitle">
|
|
<span x-text="stats.users.active_users || 0"></span> active
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Verified Vendors -->
|
|
<div class="stat-card">
|
|
<div class="stat-header">
|
|
<div class="stat-title">Verified</div>
|
|
<div class="stat-icon">✓</div>
|
|
</div>
|
|
<div class="stat-value" x-text="stats.vendors.verified_vendors || '-'"></div>
|
|
<div class="stat-subtitle">vendors verified</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Recent Vendors -->
|
|
<div class="content-section">
|
|
<div class="section-header">
|
|
<h2>Recent Vendors</h2>
|
|
<a href="/admin/vendors" class="btn btn-sm">View All</a>
|
|
</div>
|
|
|
|
<template x-if="stats.recent_vendors && stats.recent_vendors.length > 0">
|
|
<table class="data-table">
|
|
<thead>
|
|
<tr>
|
|
<th>Vendor Code</th>
|
|
<th>Name</th>
|
|
<th>Subdomain</th>
|
|
<th>Status</th>
|
|
<th>Created</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
<template x-for="vendor in stats.recent_vendors" :key="vendor.id">
|
|
<tr>
|
|
<td><strong x-text="vendor.vendor_code"></strong></td>
|
|
<td x-text="vendor.name"></td>
|
|
<td x-text="vendor.subdomain"></td>
|
|
<td>
|
|
<span
|
|
class="badge"
|
|
:class="vendor.is_active ? 'badge-success' : 'badge-danger'"
|
|
x-text="vendor.is_active ? 'Active' : 'Inactive'"
|
|
></span>
|
|
</td>
|
|
<td x-text="formatDate(vendor.created_at)"></td>
|
|
</tr>
|
|
</template>
|
|
</tbody>
|
|
</table>
|
|
</template>
|
|
|
|
<template x-if="!stats.recent_vendors || stats.recent_vendors.length === 0">
|
|
<p class="text-muted">No vendors yet. Create your first vendor!</p>
|
|
</template>
|
|
</div>
|
|
</div>
|
|
{% endblock %}
|
|
|
|
{% block extra_scripts %}
|
|
<script>
|
|
function adminDashboard() {
|
|
return {
|
|
stats: {},
|
|
loading: false,
|
|
error: null,
|
|
|
|
async loadStats() {
|
|
this.loading = true;
|
|
try {
|
|
this.stats = await apiClient.get('/api/v1/admin/dashboard');
|
|
} catch (error) {
|
|
this.error = error.message;
|
|
showNotification('Failed to load dashboard', 'error');
|
|
} finally {
|
|
this.loading = false;
|
|
}
|
|
},
|
|
|
|
async refreshStats() {
|
|
await this.loadStats();
|
|
showNotification('Dashboard refreshed', 'success');
|
|
},
|
|
|
|
formatDate(dateString) {
|
|
return new Date(dateString).toLocaleDateString();
|
|
}
|
|
}
|
|
}
|
|
</script>
|
|
{% endblock %}
|
|
```
|
|
|
|
#### 3. Vendor Creation (`templates/admin/vendors.html`)
|
|
|
|
**Status**: ⏳ In Progress
|
|
|
|
Alpine.js component for creating vendors with real-time validation and credential display.
|
|
|
|
### Vendor Pages (📋 To Do)
|
|
|
|
#### 1. Vendor Login (`templates/vendor/login.html`)
|
|
|
|
**Status**: 📋 To Do
|
|
|
|
```html
|
|
{% extends "base.html" %}
|
|
|
|
{% block title %}Vendor Login{% endblock %}
|
|
|
|
{% block extra_css %}
|
|
<link rel="stylesheet" href="/static/css/shared/auth.css">
|
|
{% endblock %}
|
|
|
|
{% block content %}
|
|
<div class="auth-page" x-data="vendorLogin()">
|
|
<div class="login-container">
|
|
<!-- Vendor Info -->
|
|
<template x-if="vendor">
|
|
<div class="vendor-info">
|
|
<h2 x-text="vendor.name"></h2>
|
|
<p class="text-muted" x-text="vendor.vendor_code"></p>
|
|
</div>
|
|
</template>
|
|
|
|
<!-- Login Form -->
|
|
<template x-if="vendor">
|
|
<form @submit.prevent="handleLogin">
|
|
<div x-show="error" x-text="error" class="alert alert-error"></div>
|
|
|
|
<div class="form-group">
|
|
<label>Username</label>
|
|
<input
|
|
type="text"
|
|
x-model="username"
|
|
class="form-control"
|
|
required
|
|
>
|
|
</div>
|
|
|
|
<div class="form-group">
|
|
<label>Password</label>
|
|
<input
|
|
type="password"
|
|
x-model="password"
|
|
class="form-control"
|
|
required
|
|
>
|
|
</div>
|
|
|
|
<button type="submit" class="btn-login" :disabled="loading">
|
|
<span x-show="!loading">Sign In</span>
|
|
<span x-show="loading" class="loading-spinner"></span>
|
|
</button>
|
|
</form>
|
|
</template>
|
|
|
|
<!-- Vendor Not Found -->
|
|
<template x-if="!vendor && !loading">
|
|
<div class="error-state">
|
|
<h2>Vendor Not Found</h2>
|
|
<p>The vendor you're trying to access doesn't exist.</p>
|
|
</div>
|
|
</template>
|
|
</div>
|
|
</div>
|
|
|
|
<script>
|
|
// Pass vendor data from backend
|
|
window.vendorData = {{ vendor|tojson if vendor else 'null' }};
|
|
</script>
|
|
{% endblock %}
|
|
|
|
{% block extra_scripts %}
|
|
<script>
|
|
function vendorLogin() {
|
|
return {
|
|
vendor: window.vendorData,
|
|
username: '',
|
|
password: '',
|
|
loading: false,
|
|
error: null,
|
|
|
|
async handleLogin() {
|
|
this.loading = true;
|
|
this.error = null;
|
|
|
|
try {
|
|
const response = await apiClient.post('/api/v1/auth/login', {
|
|
username: this.username,
|
|
password: this.password,
|
|
vendor_id: this.vendor.id
|
|
});
|
|
|
|
localStorage.setItem('vendor_token', response.access_token);
|
|
localStorage.setItem('vendor_id', this.vendor.id);
|
|
window.location.href = `/vendor/${this.vendor.subdomain}/dashboard`;
|
|
} catch (error) {
|
|
this.error = error.message || 'Login failed';
|
|
} finally {
|
|
this.loading = false;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
</script>
|
|
{% endblock %}
|
|
```
|
|
|
|
#### 2. Vendor Dashboard (`templates/vendor/dashboard.html`)
|
|
|
|
**Status**: 📋 To Do
|
|
|
|
```html
|
|
{% extends "vendor/base_vendor.html" %}
|
|
|
|
{% block title %}{{ vendor.name }} Dashboard{% endblock %}
|
|
|
|
{% block content %}
|
|
<div x-data="vendorDashboard()" x-init="loadDashboard()">
|
|
<!-- Welcome Card -->
|
|
<div class="card welcome-card mb-3">
|
|
<h1>Welcome to {{ vendor.name }}</h1>
|
|
<p class="text-muted">Vendor Code: {{ vendor.vendor_code }}</p>
|
|
<div class="mt-2">
|
|
<span
|
|
class="badge"
|
|
:class="vendor.is_verified ? 'badge-success' : 'badge-warning'"
|
|
>
|
|
${vendor.is_verified ? 'Verified' : 'Pending Verification'}
|
|
</span>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Quick Stats -->
|
|
<div class="stats-grid">
|
|
<div class="stat-card">
|
|
<div class="stat-header">
|
|
<div class="stat-title">Products</div>
|
|
<div class="stat-icon">📦</div>
|
|
</div>
|
|
<div class="stat-value" x-text="stats.products_count || '0'"></div>
|
|
<div class="stat-subtitle">in catalog</div>
|
|
</div>
|
|
|
|
<div class="stat-card">
|
|
<div class="stat-header">
|
|
<div class="stat-title">Orders</div>
|
|
<div class="stat-icon">🛒</div>
|
|
</div>
|
|
<div class="stat-value" x-text="stats.orders_count || '0'"></div>
|
|
<div class="stat-subtitle">total orders</div>
|
|
</div>
|
|
|
|
<div class="stat-card">
|
|
<div class="stat-header">
|
|
<div class="stat-title">Customers</div>
|
|
<div class="stat-icon">👥</div>
|
|
</div>
|
|
<div class="stat-value" x-text="stats.customers_count || '0'"></div>
|
|
<div class="stat-subtitle">registered</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Coming Soon Notice -->
|
|
<div class="card mt-3 p-3 text-center">
|
|
<h3>🚀 Coming in Slice 2</h3>
|
|
<p class="text-muted">
|
|
Product import from Letzshop marketplace will be available soon!
|
|
</p>
|
|
</div>
|
|
</div>
|
|
|
|
<script>
|
|
window.vendorData = {{ vendor|tojson }};
|
|
</script>
|
|
{% endblock %}
|
|
|
|
{% block extra_scripts %}
|
|
<script>
|
|
function vendorDashboard() {
|
|
return {
|
|
vendor: window.vendorData,
|
|
stats: {},
|
|
loading: false,
|
|
|
|
async loadDashboard() {
|
|
this.loading = true;
|
|
try {
|
|
this.stats = await apiClient.get('/api/v1/vendor/dashboard/stats');
|
|
} catch (error) {
|
|
console.error('Failed to load dashboard:', error);
|
|
} finally {
|
|
this.loading = false;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
</script>
|
|
{% endblock %}
|
|
```
|
|
|
|
## ✅ Testing Checklist
|
|
|
|
### Backend Tests
|
|
|
|
#### Authentication
|
|
- [ ] Admin can login with valid credentials
|
|
- [ ] Admin login rejects invalid credentials
|
|
- [ ] Non-admin users cannot access admin endpoints
|
|
- [ ] JWT tokens expire after configured time
|
|
- [ ] Token refresh works correctly
|
|
|
|
#### Vendor Management
|
|
- [ ] Admin can create vendor with all required fields
|
|
- [ ] System generates unique vendor code
|
|
- [ ] System generates unique subdomain
|
|
- [ ] Owner user account is created automatically
|
|
- [ ] Temporary password is generated (12+ characters)
|
|
- [ ] Default roles are created (Owner, Manager, Editor, Viewer)
|
|
- [ ] Owner is linked to Owner role
|
|
- [ ] Vendor is auto-verified when created by admin
|
|
- [ ] Duplicate vendor code is rejected
|
|
- [ ] Duplicate subdomain is rejected
|
|
- [ ] Duplicate owner email is rejected
|
|
|
|
#### Vendor Context Detection
|
|
- [ ] Subdomain detection works: `vendor.platform.com`
|
|
- [ ] Path detection works: `/vendor/VENDORCODE/`
|
|
- [ ] Admin routes bypass vendor context
|
|
- [ ] API routes bypass vendor context
|
|
- [ ] Static routes bypass vendor context
|
|
- [ ] Invalid vendor returns appropriate error
|
|
|
|
#### API Endpoints
|
|
- [ ] `POST /api/v1/admin/vendors` creates vendor
|
|
- [ ] `GET /api/v1/admin/vendors` lists vendors with pagination
|
|
- [ ] `GET /api/v1/admin/dashboard` returns statistics
|
|
- [ ] `PUT /api/v1/admin/vendors/{id}/verify` toggles verification
|
|
- [ ] `PUT /api/v1/admin/vendors/{id}/status` toggles active status
|
|
- [ ] All endpoints require admin authentication
|
|
- [ ] All endpoints return proper error messages
|
|
|
|
### Frontend Tests
|
|
|
|
#### Admin Login Page
|
|
- [ ] Page loads without errors
|
|
- [ ] Form validation works
|
|
- [ ] Loading state displays during login
|
|
- [ ] Error messages display correctly
|
|
- [ ] Successful login redirects to dashboard
|
|
- [ ] Token is stored in localStorage
|
|
- [ ] Page is responsive on mobile
|
|
|
|
#### Admin Dashboard
|
|
- [ ] Dashboard loads with statistics
|
|
- [ ] Vendor count is accurate
|
|
- [ ] User count is accurate
|
|
- [ ] Recent vendors list displays
|
|
- [ ] Navigation works correctly
|
|
- [ ] Refresh button updates data
|
|
- [ ] Loading states work correctly
|
|
- [ ] No console errors
|
|
|
|
#### Vendor Creation Page
|
|
- [ ] Form loads correctly
|
|
- [ ] All fields are validated
|
|
- [ ] Vendor code auto-uppercases
|
|
- [ ] Subdomain auto-lowercases
|
|
- [ ] Email validation works
|
|
- [ ] Submit creates vendor successfully
|
|
- [ ] Credentials are displayed once
|
|
- [ ] Can copy credentials
|
|
- [ ] Form resets after creation
|
|
- [ ] Error messages display correctly
|
|
|
|
#### Vendor Login Page
|
|
- [ ] Page loads with vendor context
|
|
- [ ] Vendor name displays correctly
|
|
- [ ] Form validation works
|
|
- [ ] Login succeeds with correct credentials
|
|
- [ ] Login fails with wrong credentials
|
|
- [ ] Redirects to vendor dashboard
|
|
- [ ] Token stored in localStorage
|
|
- [ ] "Vendor Not Found" shows for invalid vendor
|
|
|
|
#### Vendor Dashboard
|
|
- [ ] Dashboard loads successfully
|
|
- [ ] Vendor information displays
|
|
- [ ] Statistics load correctly
|
|
- [ ] Welcome message shows
|
|
- [ ] Verification badge shows correct status
|
|
- [ ] No console errors
|
|
|
|
### Database Tests
|
|
|
|
#### Schema Verification
|
|
```sql
|
|
-- Check tables exist
|
|
SELECT table_name FROM information_schema.tables
|
|
WHERE table_schema = 'public';
|
|
-- Expected: users, vendors, roles, vendor_users
|
|
|
|
-- Check admin user
|
|
SELECT * FROM users WHERE role = 'admin';
|
|
|
|
-- Check vendor creation
|
|
SELECT * FROM vendors WHERE vendor_code = 'TESTVENDOR';
|
|
|
|
-- Check owner user
|
|
SELECT * FROM users WHERE email = 'owner@testvendor.com';
|
|
|
|
-- Check default roles
|
|
SELECT * FROM roles WHERE vendor_id = (
|
|
SELECT id FROM vendors WHERE vendor_code = 'TESTVENDOR'
|
|
);
|
|
-- Expected: 4 roles (Owner, Manager, Editor, Viewer)
|
|
```
|
|
|
|
### Security Tests
|
|
|
|
- [ ] Passwords are hashed with bcrypt
|
|
- [ ] JWT tokens are properly signed
|
|
- [ ] Admin endpoints reject non-admin users
|
|
- [ ] Vendor endpoints require authentication
|
|
- [ ] Cross-vendor access is prevented
|
|
- [ ] SQL injection is prevented
|
|
- [ ] XSS is prevented in forms
|
|
- [ ] CSRF protection is enabled
|
|
|
|
### Performance Tests
|
|
|
|
- [ ] Admin login responds < 1 second
|
|
- [ ] Dashboard loads < 2 seconds
|
|
- [ ] Vendor creation completes < 3 seconds
|
|
- [ ] Vendor list loads < 1 second
|
|
- [ ] No N+1 query problems
|
|
|
|
## 📝 Documentation Tasks
|
|
|
|
- [ ] Update API documentation with new endpoints
|
|
- [ ] Document Alpine.js component patterns
|
|
- [ ] Document Jinja2 template structure
|
|
- [ ] Create deployment guide
|
|
- [ ] Update environment variables documentation
|
|
- [ ] Document vendor context detection logic
|
|
|
|
## 🚀 Deployment Checklist
|
|
|
|
### Environment Setup
|
|
- [ ] PostgreSQL database created
|
|
- [ ] Environment variables configured
|
|
- [ ] Static files directory created
|
|
- [ ] Template directory created
|
|
- [ ] Database migrations applied
|
|
|
|
### Configuration
|
|
- [ ] `JWT_SECRET_KEY` set to strong random value
|
|
- [ ] `DATABASE_URL` configured correctly
|
|
- [ ] `DEBUG` set appropriately (False for production)
|
|
- [ ] `ALLOWED_HOSTS` configured
|
|
- [ ] CORS settings configured
|
|
|
|
### Security
|
|
- [ ] Change default admin password
|
|
- [ ] Enable HTTPS in production
|
|
- [ ] Configure secure cookie settings
|
|
- [ ] Set up rate limiting
|
|
- [ ] Enable request logging
|
|
|
|
### DNS (Production Only)
|
|
- [ ] Wildcard subdomain configured: `*.platform.com`
|
|
- [ ] Admin subdomain configured: `admin.platform.com`
|
|
- [ ] SSL certificates installed
|
|
|
|
## 🎯 Acceptance Criteria
|
|
|
|
Slice 1 is complete when:
|
|
|
|
1. **Admin Workflow Works**
|
|
- [ ] Admin can log in
|
|
- [ ] Admin can view dashboard
|
|
- [ ] Admin can create vendors
|
|
- [ ] Admin can manage vendors
|
|
|
|
2. **Vendor Workflow Works**
|
|
- [ ] Vendor owner receives credentials
|
|
- [ ] Vendor owner can log in
|
|
- [ ] Vendor dashboard displays correctly
|
|
- [ ] Vendor context is properly isolated
|
|
|
|
3. **Technical Requirements Met**
|
|
- [ ] All API endpoints implemented
|
|
- [ ] All frontend pages created
|
|
- [ ] Database schema complete
|
|
- [ ] Tests pass
|
|
- [ ] Documentation complete
|
|
|
|
4. **Quality Standards Met**
|
|
- [ ] Code follows conventions
|
|
- [ ] No security vulnerabilities
|
|
- [ ] Performance acceptable
|
|
- [ ] Mobile responsive
|
|
- [ ] Browser compatible
|
|
|
|
## 🔄 Known Issues / To Do
|
|
|
|
### High Priority
|
|
- [ ] Complete vendor login page
|
|
- [ ] Complete vendor dashboard page
|
|
- [ ] Test vendor context detection thoroughly
|
|
- [ ] Add password reset functionality
|
|
|
|
### Medium Priority
|
|
- [ ] Add vendor list page for admin
|
|
- [ ] Add vendor detail page for admin
|
|
- [ ] Improve error messages
|
|
- [ ] Add loading skeletons
|
|
|
|
### Low Priority
|
|
- [ ] Add dark mode support
|
|
- [ ] Add keyboard shortcuts
|
|
- [ ] Add export functionality
|
|
- [ ] Add audit logging
|
|
|
|
## 📚 Related Documentation
|
|
|
|
- `00_slices_overview.md` - Overview of all slices
|
|
- `../quick_start_guide.md` - Quick setup guide
|
|
- `../css_structure_guide.txt` - CSS organization
|
|
- `../12.project_readme_final.md` - Complete README
|
|
|
|
## ➡️ Next Steps
|
|
|
|
After completing Slice 1:
|
|
|
|
1. **Test Thoroughly**: Run through entire testing checklist
|
|
2. **Deploy to Staging**: Test in production-like environment
|
|
3. **Demo to Stakeholders**: Show admin → vendor creation flow
|
|
4. **Document Learnings**: Update this document with lessons learned
|
|
5. **Move to Slice 2**: Begin marketplace import implementation
|
|
|
|
## 💡 Tips & Best Practices
|
|
|
|
### Working with Alpine.js
|
|
- Keep components focused and small
|
|
- Use `x-data` for reactive state
|
|
- Use `x-init` for loading data
|
|
- Prefer `x-show` over `x-if` for toggles
|
|
- Use `@click`, `@submit` for event handling
|
|
|
|
### Working with Jinja2
|
|
- Pass initial data from backend to avoid extra API calls
|
|
- Use template inheritance (extends, blocks)
|
|
- Keep logic in backend, not templates
|
|
- Use filters for formatting
|
|
|
|
### API Integration
|
|
- Always handle loading states
|
|
- Always handle errors gracefully
|
|
- Use the shared `apiClient` utility
|
|
- Store tokens in localStorage
|
|
- Clear tokens on logout
|
|
|
|
---
|
|
|
|
**Slice 1 Status**: 🔄 In Progress
|
|
**Next Milestone**: Complete vendor login and dashboard pages
|
|
**Estimated Completion**: End of Week 1 |