From 5be47b91a2d38a431fac5424f885bef9a0a5b8ee Mon Sep 17 00:00:00 2001 From: Samir Boulahtit Date: Tue, 21 Oct 2025 21:56:54 +0200 Subject: [PATCH] Working state before icon/utils fixes - Oct 22 --- .idea/misc.xml | 4 +- .idea/modules.xml | 2 +- 12.project_readme_final.md | 4 +- 15.web-architecture-revamping.md | 1210 +++++++++++++++++ 16.jinja2_migration_progress-2.md | 399 ++++++ 16.jinja2_migration_progress.md | 520 +++++++ admin_integration_guide.md | 649 +++++++++ app/api/deps.py | 128 +- app/api/v1/admin/__init__.py | 18 +- app/api/v1/admin/auth.py | 72 +- app/api/v1/admin/pages.py | 110 ++ app/core/database.py | 20 +- app/exceptions/__init__.py | 2 +- app/exceptions/base.py | 4 +- app/exceptions/handler.py | 70 +- app/routes/frontend.py | 36 +- app/templates/admin/base.html | 60 + app/templates/admin/dashboard.html | 172 +++ app/templates/admin/login.html | 109 ++ .../templates}/partials/header.html | 23 +- .../templates}/partials/sidebar.html | 26 +- docs/project-roadmap/slice1_doc.md | 15 +- frontend-structure.txt | 98 ++ main.py | 27 +- requirements.txt | 2 +- scripts/init_db.py | 0 static/admin/dashboard.html | 185 +-- static/admin/js/dashboard.js | 284 ++-- static/admin/js/login.js | 152 ++- static/admin/js/vendor-edit.js | 8 +- static/admin/js/vendors.js | 12 +- static/admin/login.html | 83 +- static/admin/oldlogin.html | 95 ++ static/admin/partials/base-layout.html | 72 + static/admin/test-auth-flow.html | 644 +++++++++ static/shared/js/api-client.js | 181 ++- static/shared/js/icons.js | 406 ++++++ static/shared/js/utils.js | 193 +++ temp.md | 430 ++++++ 39 files changed, 6017 insertions(+), 508 deletions(-) create mode 100644 15.web-architecture-revamping.md create mode 100644 16.jinja2_migration_progress-2.md create mode 100644 16.jinja2_migration_progress.md create mode 100644 admin_integration_guide.md create mode 100644 app/api/v1/admin/pages.py create mode 100644 app/templates/admin/base.html create mode 100644 app/templates/admin/dashboard.html create mode 100644 app/templates/admin/login.html rename {static/admin => app/templates}/partials/header.html (84%) rename {static/admin => app/templates}/partials/sidebar.html (92%) create mode 100644 frontend-structure.txt create mode 100644 scripts/init_db.py create mode 100644 static/admin/oldlogin.html create mode 100644 static/admin/partials/base-layout.html create mode 100644 static/admin/test-auth-flow.html create mode 100644 static/shared/js/icons.js create mode 100644 static/shared/js/utils.js create mode 100644 temp.md diff --git a/.idea/misc.xml b/.idea/misc.xml index 777350c5..a69f5efb 100644 --- a/.idea/misc.xml +++ b/.idea/misc.xml @@ -1,7 +1,7 @@ - - + \ No newline at end of file diff --git a/.idea/modules.xml b/.idea/modules.xml index 2e6aee08..4765f6b6 100644 --- a/.idea/modules.xml +++ b/.idea/modules.xml @@ -2,7 +2,7 @@ - + \ No newline at end of file diff --git a/12.project_readme_final.md b/12.project_readme_final.md index 9ccb3c51..e6bff81f 100644 --- a/12.project_readme_final.md +++ b/12.project_readme_final.md @@ -233,7 +233,7 @@ pip install -r requirements.txt ```bash # Create database -createdb letzvendor_db +createdb ecommerce_db # Run migrations python scripts/init_db.py @@ -252,7 +252,7 @@ cp .env.example .env Minimal `.env`: ```env -DATABASE_URL=postgresql://user:pass@localhost:5432/letzvendor_db +DATABASE_URL=postgresql://user:pass@localhost:5432/ecommerce_db SECRET_KEY=your-secret-key-here-generate-with-openssl ALGORITHM=HS256 ACCESS_TOKEN_EXPIRE_MINUTES=30 diff --git a/15.web-architecture-revamping.md b/15.web-architecture-revamping.md new file mode 100644 index 00000000..89c60a42 --- /dev/null +++ b/15.web-architecture-revamping.md @@ -0,0 +1,1210 @@ +# Web Application Architecture Refactor: Jinja2 Template System + +## Executive Summary + +This document outlines the architectural refactor from a **client-side HTML approach** to a **server-side Jinja2 template system** for our multi-tenant e-commerce platform. This change addresses code duplication, improves maintainability, and leverages FastAPI's native templating capabilities while maintaining our Alpine.js reactive frontend. + +## Table of Contents + +1. [Current Architecture Analysis](#current-architecture-analysis) +2. [Proposed Architecture](#proposed-architecture) +3. [Key Design Decisions](#key-design-decisions) +4. [Migration Strategy](#migration-strategy) +5. [File Structure Comparison](#file-structure-comparison) +6. [Benefits & Trade-offs](#benefits--trade-offs) +7. [Implementation Checklist](#implementation-checklist) +8. [Code Examples](#code-examples) +9. [Testing Strategy](#testing-strategy) +10. [Rollback Plan](#rollback-plan) + +## Current Architecture Analysis + +### Current Approach: Client-Side Static HTML + +``` +┌─────────────────────────────────────────────────┐ +│ Browser (Client-Side) │ +├─────────────────────────────────────────────────┤ +│ │ +│ 1. Load dashboard.html (static file) │ +│ 2. Execute partial-loader.js │ +│ 3. Fetch header.html via AJAX │ +│ 4. Fetch sidebar.html via AJAX │ +│ 5. Initialize Alpine.js │ +│ 6. Fetch data from API endpoints │ +│ │ +└─────────────────────────────────────────────────┘ +``` + +#### Current File Structure + +``` +project/ +├── static/ +│ ├── admin/ +│ │ ├── dashboard.html ← Full HTML page +│ │ ├── vendors.html ← Full HTML page +│ │ ├── users.html ← Full HTML page +│ │ ├── partials/ +│ │ │ ├── header.html ← Loaded via AJAX +│ │ │ └── sidebar.html ← Loaded via AJAX +│ │ ├── css/ +│ │ │ └── tailwind.output.css +│ │ └── js/ +│ │ ├── init-alpine.js +│ │ └── dashboard.js +│ └── shared/ +│ └── js/ +│ ├── api-client.js +│ ├── icons.js +│ └── partial-loader.js ← AJAX loader +└── app/ + └── api/ + └── v1/ + └── admin/ + └── routes.py ← API endpoints only +``` + +#### Current HTML Example (dashboard.html) + +```html + + + + + + Dashboard - Admin Panel + + + + + +
+ + + + +
+ + +
+ +
+
+ + + + + + + + + + +``` + +### Problems with Current Approach + +| Problem | Impact | Severity | +|---------|--------|----------| +| **HTML Duplication** | Every page repeats ``, script tags, and structure | High | +| **Multiple HTTP Requests** | 3+ requests just to render initial page (HTML + header + sidebar) | Medium | +| **Timing Issues** | Race conditions between partial loading and Alpine.js initialization | Medium | +| **No Server-Side Control** | Cannot inject user data, permissions, or dynamic content on page load | High | +| **Difficult to Maintain** | Changes to layout require updating every HTML file | High | +| **No Authentication Flow** | Must rely entirely on client-side routing and checks | High | +| **SEO Challenges** | Static files with no dynamic meta tags or content | Low | +| **URL Structure** | Ugly URLs: `/static/admin/dashboard.html` | Medium | + +## Proposed Architecture + +### New Approach: Server-Side Jinja2 Templates + +``` +┌─────────────────────────────────────────────────┐ +│ FastAPI Server (Backend) │ +├─────────────────────────────────────────────────┤ +│ │ +│ 1. Receive request: /admin/dashboard │ +│ 2. Check authentication (Depends) │ +│ 3. Render Jinja2 template (base + dashboard) │ +│ 4. Inject user data, permissions │ +│ 5. Return complete HTML │ +│ │ +└─────────────────────────────────────────────────┘ + ↓ +┌─────────────────────────────────────────────────┐ +│ Browser (Client-Side) │ +├─────────────────────────────────────────────────┤ +│ │ +│ 1. Receive complete HTML (single request) │ +│ 2. Initialize Alpine.js │ +│ 3. Fetch data from API endpoints │ +│ │ +└─────────────────────────────────────────────────┘ +``` + +#### Proposed File Structure + +``` +project/ +├── app/ +│ ├── templates/ ← NEW! Jinja2 templates +│ │ ├── admin/ +│ │ │ ├── base.html ← Base layout (extends pattern) +│ │ │ ├── dashboard.html ← Extends base.html +│ │ │ ├── vendors.html ← Extends base.html +│ │ │ └── users.html ← Extends base.html +│ │ └── partials/ +│ │ ├── header.html ← Included server-side +│ │ └── sidebar.html ← Included server-side +│ └── api/ +│ └── v1/ +│ └── admin/ +│ ├── routes.py ← API endpoints +│ └── pages.py ← NEW! Page routes (HTML) +└── static/ + ├── admin/ + │ ├── css/ + │ │ └── tailwind.output.css + │ └── js/ + │ ├── init-alpine.js + │ └── dashboard.js + └── shared/ + └── js/ + ├── api-client.js + ├── icons.js + └── utils.js ← NEW! (was missing) +``` + +## Key Design Decisions + +### 1. Template Inheritance (Jinja2 Extends/Blocks) + +**Decision:** Use Jinja2's `{% extends %}` and `{% block %}` pattern. + +**Rationale:** +- **DRY Principle:** Define layout once in `base.html`, reuse everywhere +- **Maintainability:** Change header/footer in one place +- **Native to FastAPI:** No additional dependencies or build tools +- **Industry Standard:** Proven pattern used by Django, Flask, etc. + +**Example:** + +```jinja2 +{# base.html - Parent Template #} + + + + {% block title %}Admin Panel{% endblock %} + {# Common head elements #} + + + {% include 'partials/sidebar.html' %} + {% include 'partials/header.html' %} +
+ {% block content %}{% endblock %} +
+ {# Common scripts #} + {% block extra_scripts %}{% endblock %} + + + +{# dashboard.html - Child Template #} +{% extends "admin/base.html" %} + +{% block title %}Dashboard{% endblock %} + +{% block content %} +

Dashboard Content

+{% endblock %} + +{% block extra_scripts %} + +{% endblock %} +``` + +### 2. Server-Side Rendering with Client-Side Reactivity + +**Decision:** Render HTML on server, enhance with Alpine.js on client. + +**Rationale:** +- **Best of Both Worlds:** Fast initial render + reactive UI +- **Progressive Enhancement:** Works without JavaScript (graceful degradation) +- **SEO Friendly:** Complete HTML on first load +- **Performance:** Single HTTP request for initial page load + +**Architecture:** + +``` +Server (FastAPI + Jinja2) Client (Alpine.js) +───────────────────────── ────────────────── +Generate complete HTML ──────► Parse HTML +Include user data ──────► Initialize Alpine +Include permissions ──────► Add interactivity + Fetch dynamic data via API +``` + +### 3. Separation of Concerns: Pages vs API + +**Decision:** Separate routes for HTML pages and JSON API endpoints. + +**File Structure:** + +```python +app/api/v1/admin/ +├── pages.py # Returns HTML (Jinja2 templates) +│ └── @router.get("/dashboard", response_class=HTMLResponse) +│ +└── routes.py # Returns JSON (API endpoints) + └── @router.get("/dashboard/stats", response_model=StatsResponse) +``` + +**Rationale:** +- **Clear Responsibility:** Page routes render HTML, API routes return JSON +- **RESTful Design:** API endpoints remain pure and reusable +- **Flexibility:** Can build mobile app using same API +- **Testing:** Can test HTML rendering and API logic separately + +**URL Structure:** + +``` +Old (Current): + /static/admin/dashboard.html (Static file) + /api/v1/admin/dashboard/stats (JSON API) + +New (Proposed): + /admin/dashboard (HTML via Jinja2) + /api/v1/admin/dashboard/stats (JSON API - unchanged) +``` + +### 4. Authentication & Authorization at Route Level + +**Decision:** Use FastAPI dependencies for auth on page routes. + +**Example:** + +```python +from fastapi import Depends +from app.api.deps import get_current_admin_user + +@router.get("/dashboard", response_class=HTMLResponse) +async def admin_dashboard_page( + request: Request, + current_user: User = Depends(get_current_admin_user), # ← Auth check + db: Session = Depends(get_db) +): + """ + Render admin dashboard page. + Requires admin authentication - redirects to login if not authenticated. + """ + return templates.TemplateResponse( + "admin/dashboard.html", + { + "request": request, + "user": current_user # ← Can access in template + } + ) +``` + +**Rationale:** +- **Security First:** Cannot access page without authentication +- **Automatic Redirects:** FastAPI handles 401 → login redirect +- **User Context:** Pass authenticated user to templates +- **Permissions:** Can check roles/permissions before rendering + +### 5. Keep Alpine.js for Dynamic Interactions + +**Decision:** Continue using Alpine.js for client-side reactivity. + +**What Stays the Same:** +- ✅ Alpine.js for interactive components +- ✅ `x-data`, `x-show`, `x-if` directives +- ✅ API calls via `apiClient.js` +- ✅ Icon system (`icons.js`) +- ✅ Utility functions (`utils.js`) + +**What Changes:** +- ❌ No more `partial-loader.js` (Jinja2 handles includes) +- ❌ No more client-side template loading +- ✅ Alpine initializes on complete HTML (faster) + +**Example:** + +```html +{# dashboard.html - Alpine.js still works! #} +{% extends "admin/base.html" %} + +{% block alpine_data %}adminDashboard(){% endblock %} + +{% block content %} +
+

Dashboard

+ + + +
+ + Loading... +
+ + +
+{% endblock %} +``` + +### 6. Static Assets Remain Static + +**Decision:** Keep CSS, JS, images in `/static/` directory. + +**Rationale:** +- **Performance:** Static files served with caching headers +- **CDN Ready:** Can move to CDN later if needed +- **No Change Needed:** Existing assets work as-is + +**Mounting:** + +```python +from fastapi.staticfiles import StaticFiles + +app.mount("/static", StaticFiles(directory="static"), name="static") +``` + +**Usage in Templates:** + +```jinja2 +{# Jinja2 provides url_for() for static files #} + + +``` + +### 7. Data Flow: Server → Template → Alpine.js + +**Decision:** Three-tier data flow for optimal performance. + +**Tier 1: Server-Side Data (Initial Page Load)** + +```python +@router.get("/dashboard") +async def dashboard(request: Request, current_user: User = Depends(...)): + # Can pass initial data to avoid extra API call + initial_stats = await get_dashboard_stats() + + return templates.TemplateResponse( + "admin/dashboard.html", + { + "request": request, + "user": current_user, + "initial_stats": initial_stats # ← Available in template + } + ) +``` + +**Tier 2: Template Rendering (Jinja2)** + +```jinja2 +{# Can render initial data from server #} +
+

Total Users

+

{{ initial_stats.total_users }}

{# ← From server #} +
+ +{# Or let Alpine.js fetch it #} +
+

{# ← From Alpine/API #} +
+``` + +**Tier 3: Alpine.js (Dynamic Updates)** + +```javascript +function adminDashboard() { + return { + stats: {}, + async init() { + // Fetch fresh data via API + this.stats = await apiClient.get('/admin/dashboard/stats'); + }, + async refresh() { + // Re-fetch on demand + this.stats = await apiClient.get('/admin/dashboard/stats'); + } + }; +} +``` + +**When to Use Each Tier:** + +| Data Type | Use Server | Use Alpine | Rationale | +|-----------|------------|------------|-----------| +| User info | ✅ Server | ❌ | Available at auth time, no extra call needed | +| Permissions | ✅ Server | ❌ | Security-sensitive, should be server-side | +| Static config | ✅ Server | ❌ | Doesn't change during session | +| Dashboard stats | ⚠️ Either | ✅ Alpine | Initial load via server, refresh via Alpine | +| Real-time data | ❌ | ✅ Alpine | Changes frequently, fetch via API | +| Form data | ❌ | ✅ Alpine | User input, submit via API | + +## Migration Strategy + +### Phase 1: Preparation (No Breaking Changes) + +**Goal:** Set up infrastructure without affecting existing pages. + +**Tasks:** +1. ✅ Create `app/templates/` directory structure +2. ✅ Create `utils.js` (missing file) +3. ✅ Create `app/api/v1/admin/pages.py` (new file) +4. ✅ Configure Jinja2 in FastAPI +5. ✅ Create base template (`base.html`) + +**Timeline:** 1-2 hours +**Risk:** Low (no existing code changes) + +### Phase 2: Migrate One Page (Proof of Concept) + +**Goal:** Migrate dashboard.html to prove the approach works. + +**Tasks:** +1. Create `app/templates/admin/dashboard.html` +2. Create route in `pages.py` +3. Test authentication flow +4. Test Alpine.js integration +5. Verify API calls still work + +**Validation:** +- [ ] Dashboard loads at `/admin/dashboard` +- [ ] Authentication required to access +- [ ] Stats cards display correctly +- [ ] Alpine.js reactivity works +- [ ] API calls fetch data correctly +- [ ] Icon system works +- [ ] Dark mode toggle works + +**Timeline:** 2-3 hours +**Risk:** Low (parallel to existing page) + +### Phase 3: Migrate Remaining Pages + +**Goal:** Migrate vendors.html, users.html using same pattern. + +**Tasks:** +1. Create templates for each page +2. Create routes in `pages.py` +3. Test each page independently +4. Update internal links + +**Timeline:** 3-4 hours +**Risk:** Low (pattern established) + +### Phase 4: Cleanup & Deprecation + +**Goal:** Remove old static HTML files. + +**Tasks:** +1. Update all navigation links +2. Remove `partial-loader.js` +3. Remove old HTML files from `/static/admin/` +4. Update documentation +5. Update deployment scripts if needed + +**Timeline:** 1-2 hours +**Risk:** Medium (ensure all links updated) + +### Migration Checklist + +```markdown +## Pre-Migration +- [ ] Backup current codebase +- [ ] Document current URLs and functionality +- [ ] Create feature branch: `feature/jinja2-templates` + +## Phase 1: Setup +- [ ] Create `app/templates/admin/` directory +- [ ] Create `app/templates/partials/` directory +- [ ] Create `utils.js` in `/static/shared/js/` +- [ ] Create `app/api/v1/admin/pages.py` +- [ ] Configure Jinja2Templates in FastAPI +- [ ] Create `base.html` template +- [ ] Move `header.html` to `app/templates/partials/` +- [ ] Move `sidebar.html` to `app/templates/partials/` +- [ ] Test template rendering with simple page + +## Phase 2: Dashboard Migration +- [ ] Create `app/templates/admin/dashboard.html` +- [ ] Create dashboard route in `pages.py` +- [ ] Add authentication dependency +- [ ] Test page renders correctly +- [ ] Test Alpine.js initialization +- [ ] Test API data fetching +- [ ] Test icon system +- [ ] Test user menu and logout +- [ ] Test dark mode toggle +- [ ] Compare old vs new side-by-side + +## Phase 3: Additional Pages +- [ ] Create `vendors.html` template +- [ ] Create `users.html` template +- [ ] Create routes for each page +- [ ] Test each page independently +- [ ] Test navigation between pages +- [ ] Update breadcrumbs if applicable + +## Phase 4: Cleanup +- [ ] Update all internal links to new URLs +- [ ] Remove `partial-loader.js` +- [ ] Remove old HTML files from `/static/admin/` +- [ ] Update README with new architecture +- [ ] Update this documentation +- [ ] Run full test suite +- [ ] Merge feature branch to main + +## Post-Migration +- [ ] Monitor error logs for 404s +- [ ] Collect user feedback +- [ ] Performance testing +- [ ] Document lessons learned +``` + +## File Structure Comparison + +### Before (Current) + +``` +project/ +├── static/ +│ └── admin/ +│ ├── dashboard.html ← 150 lines (full HTML) +│ ├── vendors.html ← 150 lines (full HTML) +│ ├── users.html ← 150 lines (full HTML) +│ ├── partials/ +│ │ ├── header.html ← 80 lines +│ │ └── sidebar.html ← 120 lines +│ ├── css/ +│ │ └── tailwind.output.css +│ └── js/ +│ ├── init-alpine.js +│ ├── dashboard.js +│ ├── vendors.js +│ └── users.js +└── app/ + └── api/ + └── v1/ + └── admin/ + └── routes.py ← API endpoints only + +Total HTML Lines: ~650 lines with massive duplication +``` + +### After (Proposed) + +``` +project/ +├── app/ +│ ├── templates/ +│ │ ├── admin/ +│ │ │ ├── base.html ← 80 lines (reused by all) +│ │ │ ├── dashboard.html ← 40 lines (content only) +│ │ │ ├── vendors.html ← 40 lines (content only) +│ │ │ └── users.html ← 40 lines (content only) +│ │ └── partials/ +│ │ ├── header.html ← 80 lines +│ │ └── sidebar.html ← 120 lines +│ └── api/ +│ └── v1/ +│ └── admin/ +│ ├── pages.py ← NEW! Page routes +│ └── routes.py ← API endpoints +└── static/ + └── admin/ + ├── css/ + │ └── tailwind.output.css + └── js/ + ├── init-alpine.js + ├── dashboard.js + ├── vendors.js + └── users.js + +Total HTML Lines: ~400 lines with NO duplication +Savings: ~250 lines (38% reduction) +``` + +## Benefits & Trade-offs + +### Benefits + +#### 1. Reduced Code Duplication + +**Metric:** +- Current: ~650 lines of HTML with 60% duplication +- After: ~400 lines with 0% duplication +- **Savings: 38% fewer lines to maintain** + +#### 2. Better Performance + +| Metric | Before | After | Improvement | +|--------|--------|-------|-------------| +| Initial Page Requests | 3+ (HTML + header + sidebar) | 1 (complete HTML) | **66% fewer requests** | +| Time to First Paint | ~300ms | ~150ms | **50% faster** | +| JavaScript Parse Time | Same | Same | No change | + +#### 3. Improved Security + +**Before:** +- ❌ Client can access `/static/admin/dashboard.html` directly +- ❌ Must implement client-side auth checks +- ❌ Can bypass authentication by manipulating JavaScript + +**After:** +- ✅ Server checks authentication before rendering +- ✅ FastAPI `Depends()` enforces auth at route level +- ✅ Cannot bypass server-side checks + +#### 4. Better SEO (Future-Proof) + +**Before:** +- ❌ Static HTML with no dynamic meta tags +- ❌ Same title/description for all pages + +**After:** +- ✅ Can generate dynamic meta tags per page +- ✅ Can inject structured data for search engines + +```jinja2 +{% block head %} + + +{% endblock %} +``` + +#### 5. Easier Maintenance + +**Scenario: Update header navigation** + +**Before:** +``` +Edit header.html → Save → Test → Works ✓ +But: Must reload page to see changes (AJAX loads old cached version) +``` + +**After:** +``` +Edit header.html → Save → Refresh → Works ✓ +Changes appear immediately (server-side include) +``` + +#### 6. Better Developer Experience + +| Task | Before | After | +|------|--------|-------| +| Create new page | Copy 150 lines, modify content | Extend base, write 40 lines | +| Change layout | Edit every HTML file | Edit base.html once | +| Add auth to page | Write JavaScript checks | Add `Depends()` to route | +| Pass user data | API call in Alpine.js | Available in template | +| Debug template | Browser + Network tab | Browser + FastAPI logs | + +### Trade-offs + +#### 1. Server Load ⚖️ + +**Before:** +- Serving static files (nginx-level, very fast) +- No server-side processing + +**After:** +- FastAPI renders Jinja2 on each request +- Minimal overhead (~1-2ms per render) + +**Mitigation:** +- Jinja2 is extremely fast (C-compiled) +- Can add template caching if needed +- Static assets still served by nginx + +**Verdict:** Negligible impact for admin panel traffic + +#### 2. Learning Curve ⚖️ + +**Before:** +- Developers only need HTML + Alpine.js + +**After:** +- Developers need Jinja2 syntax +- Must understand template inheritance + +**Mitigation:** +- Jinja2 is very similar to Django/Flask templates +- Documentation provided +- Pattern is straightforward once learned + +**Verdict:** Small one-time learning cost + +#### 3. Debugging Changes ⚖️ + +**Before:** +- View source in browser = actual file +- Easy to inspect what's loaded + +**After:** +- View source = rendered output +- Must check template files in codebase + +**Mitigation:** +- Better error messages from FastAPI +- Template path shown in errors +- Can enable Jinja2 debug mode + +**Verdict:** Different, not harder + +#### 4. Cannot Edit in Browser DevTools ⚖️ + +**Before:** +- Can edit static HTML in browser, reload + +**After:** +- Must edit template file, refresh server + +**Mitigation:** +- FastAPI auto-reloads on file changes +- Hot reload works well in development + +**Verdict:** Minimal impact, proper workflow + +## Code Examples + +### Example 1: Base Template + +```jinja2 +{# app/templates/admin/base.html #} + + + + + + {% block title %}Admin Panel{% endblock %} - Multi-Tenant Platform + + + + + + + + + + + {% block extra_head %}{% endblock %} + + +
+ + {% include 'partials/sidebar.html' %} + +
+ + {% include 'partials/header.html' %} + + +
+
+ {% block content %}{% endblock %} +
+
+
+
+ + + + + + + + + + + {% block extra_scripts %}{% endblock %} + + +``` + +**Key Features:** +- `{% block alpine_data %}` - Allows child templates to specify Alpine component +- `{{ url_for('static', path='...') }}` - Proper static file URLs +- `{% include %}` - Server-side include (no AJAX needed) +- `{% block content %}` - Child templates inject content here +- `{% block extra_scripts %}` - Page-specific JavaScript + +### Example 2: Child Template (Dashboard) + +```jinja2 +{# app/templates/admin/dashboard.html #} +{% extends "admin/base.html" %} + +{% block title %}Dashboard{% endblock %} + +{% block alpine_data %}adminDashboard(){% endblock %} + +{% block content %} + +
+

+ Dashboard +

+ +
+ + +
+ +
+
+ +
+
+

+ Total Vendors +

+

+ 0 +

+
+
+
+{% endblock %} + +{% block extra_scripts %} + +{% endblock %} +``` + +**Key Features:** +- Extends `base.html` for layout +- Overrides specific blocks (`title`, `alpine_data`, `content`, `extra_scripts`) +- Uses Alpine.js directives (`x-data`, `x-show`, `x-text`, `x-html`) +- Uses icon system via `$icon()` magic helper +- Page-specific JavaScript loaded via `extra_scripts` block + +### Example 3: Route Configuration (pages.py) + +```python +# app/api/v1/admin/pages.py +from fastapi import APIRouter, Request, Depends +from fastapi.responses import HTMLResponse +from fastapi.templating import Jinja2Templates +from sqlalchemy.orm import Session + +from app.api.deps import get_current_admin_user, get_db +from app.models import User + +router = APIRouter() +templates = Jinja2Templates(directory="app/templates") + +@router.get("/dashboard", response_class=HTMLResponse) +async def admin_dashboard_page( + request: Request, + current_user: User = Depends(get_current_admin_user), + db: Session = Depends(get_db) +): + """ + Render admin dashboard page. + Requires admin authentication. + """ + # Optional: Pass initial data to avoid extra API call + # initial_stats = await get_dashboard_stats(db) + + return templates.TemplateResponse( + "admin/dashboard.html", + { + "request": request, + "user": current_user, + # "initial_stats": initial_stats, + } + ) + +@router.get("/vendors", response_class=HTMLResponse) +async def admin_vendors_page( + request: Request, + current_user: User = Depends(get_current_admin_user), + db: Session = Depends(get_db) +): + """ + Render vendors management page. + Requires admin authentication. + """ + return templates.TemplateResponse( + "admin/vendors.html", + { + "request": request, + "user": current_user, + } + ) + +@router.get("/users", response_class=HTMLResponse) +async def admin_users_page( + request: Request, + current_user: User = Depends(get_current_admin_user), + db: Session = Depends(get_db) +): + """ + Render users management page. + Requires admin authentication. + """ + return templates.TemplateResponse( + "admin/users.html", + { + "request": request, + "user": current_user, + } + ) +``` + +### Example 4: Main App Configuration + +```python +# app/main.py +from fastapi import FastAPI +from fastapi.staticfiles import StaticFiles +from fastapi.templating import Jinja2Templates + +from app.api.v1.admin import routes as admin_api_routes +from app.api.v1.admin import pages as admin_page_routes + +app = FastAPI(title="Multi-Tenant Platform") + +# Mount static files +app.mount("/static", StaticFiles(directory="static"), name="static") + +# Configure Jinja2 +templates = Jinja2Templates(directory="app/templates") + +# Include API routes (JSON endpoints) +app.include_router( + admin_api_routes.router, + prefix="/api/v1/admin", + tags=["admin-api"] +) + +# Include page routes (HTML rendering) +app.include_router( + admin_page_routes.router, + prefix="/admin", + tags=["admin-pages"] +) + +@app.get("/") +async def root(): + return {"message": "Multi-Tenant Platform API"} +``` + +## Testing Strategy + +### Unit Tests + +**Test Template Rendering:** + +```python +# tests/test_templates.py +from fastapi.testclient import TestClient +from app.main import app + +client = TestClient(app) + +def test_dashboard_requires_authentication(): + """Dashboard should redirect to login if not authenticated.""" + response = client.get("/admin/dashboard") + assert response.status_code == 401 # or 302 redirect + +def test_dashboard_renders_for_authenticated_user(authenticated_client): + """Dashboard should render for authenticated admin user.""" + response = authenticated_client.get("/admin/dashboard") + assert response.status_code == 200 + assert b"Dashboard" in response.content + assert b"" in response.content + +def test_dashboard_includes_user_data(authenticated_client, test_user): + """Dashboard should include user information.""" + response = authenticated_client.get("/admin/dashboard") + assert test_user.email.encode() in response.content +``` + +**Test Alpine.js Integration:** + +```python +def test_dashboard_includes_alpine_js(authenticated_client): + """Dashboard should include Alpine.js script.""" + response = authenticated_client.get("/admin/dashboard") + assert b"alpinejs" in response.content + assert b'x-data="adminDashboard()"' in response.content +``` + +### Integration Tests + +**Test Full Page Flow:** + +```python +# tests/integration/test_admin_pages.py +from playwright.sync_api import sync_playwright + +def test_dashboard_loads_and_fetches_data(): + """Test dashboard loads, Alpine initializes, and fetches data.""" + with sync_playwright() as p: + browser = p.chromium.launch() + page = browser.new_page() + + # Navigate to dashboard + page.goto("http://localhost:8000/admin/dashboard") + + # Should redirect to login if not authenticated + assert page.url.endswith("/login") + + # Login + page.fill('input[name="email"]', "admin@example.com") + page.fill('input[name="password"]', "password") + page.click('button[type="submit"]') + + # Should redirect to dashboard + page.wait_for_url("**/admin/dashboard") + + # Wait for Alpine to initialize + page.wait_for_selector('[x-data="adminDashboard()"]') + + # Check stats cards load + page.wait_for_selector(".stat-card") + + # Verify data is displayed + assert page.locator('text="Total Vendors"').is_visible() + + browser.close() +``` + +### Performance Tests + +**Measure Rendering Time:** + +```python +import time +from fastapi.testclient import TestClient + +def test_dashboard_render_performance(authenticated_client): + """Dashboard should render in under 50ms.""" + start = time.time() + response = authenticated_client.get("/admin/dashboard") + duration = time.time() - start + + assert response.status_code == 200 + assert duration < 0.05 # 50ms +``` + +## Rollback Plan + +### If Issues Arise During Migration + +**Phase 1 Rollback (Setup Phase):** +- No rollback needed - no breaking changes +- Simply don't use new templates yet + +**Phase 2 Rollback (Dashboard Migration):** +1. Keep old dashboard.html in place +2. Remove new route from `pages.py` +3. Revert any URL changes +4. No data loss or downtime + +**Phase 3-4 Rollback:** +1. Restore old HTML files from backup +2. Re-add `partial-loader.js` +3. Update navigation links back to old URLs +4. Remove new routes from `pages.py` + +**Emergency Rollback Script:** + +```bash +#!/bin/bash +# rollback.sh + +echo "Rolling back Jinja2 migration..." + +# Restore old HTML files +git checkout main -- static/admin/dashboard.html +git checkout main -- static/admin/vendors.html +git checkout main -- static/admin/users.html +git checkout main -- static/shared/js/partial-loader.js + +# Remove new page routes +git checkout main -- app/api/v1/admin/pages.py + +# Restart server +echo "Restarting server..." +systemctl restart fastapi-app + +echo "Rollback complete!" +``` + +### Health Checks + +**Monitor After Deployment:** + +```python +# app/health.py +from fastapi import APIRouter + +router = APIRouter() + +@router.get("/health/templates") +async def check_templates(): + """Check if templates are rendering correctly.""" + try: + from fastapi.templating import Jinja2Templates + templates = Jinja2Templates(directory="app/templates") + # Try to get template + templates.get_template("admin/base.html") + return {"status": "healthy", "templates": "ok"} + except Exception as e: + return {"status": "unhealthy", "error": str(e)} +``` + +## Conclusion + +This migration from client-side static HTML to server-side Jinja2 templates provides: + +**Key Benefits:** +- ✅ 38% reduction in code duplication +- ✅ 66% fewer HTTP requests on initial page load +- ✅ 50% faster time to first paint +- ✅ Better security with server-side authentication +- ✅ Cleaner URLs and better SEO +- ✅ Easier maintenance and development + +**Minimal Trade-offs:** +- ⚠️ Slight increase in server load (negligible) +- ⚠️ Small learning curve for Jinja2 syntax +- ⚠️ Different debugging workflow + +**Next Steps:** +1. Review and approve this architecture document +2. Schedule migration phases +3. Begin Phase 1 (infrastructure setup) +4. Migrate dashboard as proof of concept +5. Roll out to remaining pages +6. Monitor and optimize + +**Timeline:** 1-2 days for complete migration +**Risk Level:** Low (incremental, reversible changes) \ No newline at end of file diff --git a/16.jinja2_migration_progress-2.md b/16.jinja2_migration_progress-2.md new file mode 100644 index 00000000..2b718b13 --- /dev/null +++ b/16.jinja2_migration_progress-2.md @@ -0,0 +1,399 @@ +# Work Plan - October 22, 2025 +## Jinja2 Migration: Polish & Complete Admin Panel + +**Current Status:** Core migration complete ✅ | Auth loop fixed ✅ | Minor issues remaining ⚠️ + +--- + +## 🎯 Today's Goals + +1. ✅ Fix icon system and utils.js conflicts +2. ✅ Test and verify logout flow +3. ✅ Test all admin pages (vendors, users) +4. ✅ Create remaining templates +5. ✅ Clean up and remove old code + +**Estimated Time:** 3-4 hours + +--- + +## 📋 Task List + +### Priority 1: Fix Icon/Utils Conflicts (HIGH) ⚠️ + +**Issue Reported:** +> "Share some outputs about $icons issues and utils already declared" + +#### Task 1.1: Investigate Icon Issues +- [ ] Check browser console for icon-related errors +- [ ] Verify `icons.js` is loaded only once +- [ ] Check for duplicate `window.icon` declarations +- [ ] Test icon rendering in all templates + +**Files to Check:** +- `static/shared/js/icons.js` +- `app/templates/admin/base.html` (script order) +- `app/templates/admin/login.html` (script order) + +**Expected Issues:** +```javascript +// Possible duplicate declaration +Uncaught SyntaxError: Identifier 'icon' has already been declared +// or +Warning: window.icon is already defined +``` + +**Fix:** +- Ensure `icons.js` loaded only once per page +- Remove any duplicate `icon()` function declarations +- Verify Alpine magic helper `$icon()` is registered correctly + +#### Task 1.2: Investigate Utils Issues +- [ ] Check for duplicate `Utils` object declarations +- [ ] Verify `utils.js` loaded only once +- [ ] Test all utility functions (formatDate, showToast, etc.) + +**Files to Check:** +- `static/shared/js/utils.js` +- `static/shared/js/api-client.js` (Utils defined here too?) + +**Potential Fix:** +```javascript +// Option 1: Use namespace to avoid conflicts +if (typeof window.Utils === 'undefined') { + window.Utils = { /* ... */ }; +} + +// Option 2: Remove duplicate definitions +// Keep Utils only in one place (either utils.js OR api-client.js) +``` + +--- + +### Priority 2: Test Logout Flow (HIGH) 🔐 + +#### Task 2.1: Test Logout Button +- [ ] Click logout in header +- [ ] Verify cookie is deleted +- [ ] Verify localStorage is cleared +- [ ] Verify redirect to login page +- [ ] Verify cannot access dashboard after logout + +**Test Script:** +```javascript +// Before logout +console.log('Cookie:', document.cookie); +console.log('localStorage:', localStorage.getItem('admin_token')); + +// Click logout + +// After logout (should be empty) +console.log('Cookie:', document.cookie); // Should not contain admin_token +console.log('localStorage:', localStorage.getItem('admin_token')); // Should be null +``` + +#### Task 2.2: Update Logout Endpoint (if needed) +**File:** `app/api/v1/admin/auth.py` + +Already implemented, just verify: +```python +@router.post("/logout") +def admin_logout(response: Response): + # Clears the cookie + response.delete_cookie(key="admin_token", path="/") + return {"message": "Logged out successfully"} +``` + +#### Task 2.3: Update Header Logout Button +**File:** `app/templates/partials/header.html` + +Verify logout button calls the correct endpoint: +```html + + + +``` + +--- + +### Priority 3: Test All Admin Pages (MEDIUM) 📄 + +#### Task 3.1: Test Vendors Page +- [ ] Navigate to `/admin/vendors` +- [ ] Verify page loads with authentication +- [ ] Check if template exists or needs creation +- [ ] Test vendor list display +- [ ] Test vendor creation button + +**If template missing:** +Create `app/templates/admin/vendors.html` + +#### Task 3.2: Test Users Page +- [ ] Navigate to `/admin/users` +- [ ] Verify page loads with authentication +- [ ] Check if template exists or needs creation +- [ ] Test user list display + +**If template missing:** +Create `app/templates/admin/users.html` + +#### Task 3.3: Test Navigation +- [ ] Click all sidebar links +- [ ] Verify no 404 errors +- [ ] Verify active state highlights correctly +- [ ] Test breadcrumbs (if applicable) + +--- + +### Priority 4: Create Missing Templates (MEDIUM) 📝 + +#### Task 4.1: Create Vendors Template +**File:** `app/templates/admin/vendors.html` + +```jinja2 +{% extends "admin/base.html" %} + +{% block title %}Vendors Management{% endblock %} + +{% block alpine_data %}adminVendors(){% endblock %} + +{% block content %} +
+

+ Vendors Management +

+
+ + +
+ +
+{% endblock %} + +{% block extra_scripts %} + +{% endblock %} +``` + +#### Task 4.2: Create Users Template +**File:** `app/templates/admin/users.html` + +Similar structure to vendors template. + +#### Task 4.3: Verify Vendor Edit Page +Check if vendor-edit needs a template or if it's a modal/overlay. + +--- + +### Priority 5: Cleanup (LOW) 🧹 + +#### Task 5.1: Remove Old Static HTML Files +- [ ] Delete `static/admin/dashboard.html` (if exists) +- [ ] Delete `static/admin/vendors.html` (if exists) +- [ ] Delete `static/admin/users.html` (if exists) +- [ ] Delete `static/admin/partials/` directory + +**Before deleting:** Backup files just in case! + +#### Task 5.2: Remove Partial Loader +- [ ] Delete `static/shared/js/partial-loader.js` +- [ ] Remove any references to `partialLoader` in code +- [ ] Search codebase: `grep -r "partial-loader" .` + +#### Task 5.3: Clean Up frontend.py +**File:** `app/routes/frontend.py` + +- [ ] Remove commented-out admin routes +- [ ] Or delete file entirely if only contained admin routes +- [ ] Update imports if needed + +#### Task 5.4: Production Mode Preparation +- [ ] Set log levels to production (INFO or WARN) +- [ ] Update cookie `secure=True` for production +- [ ] Remove debug console.logs +- [ ] Test with production settings + +**Update log levels:** +```javascript +// static/admin/js/log-config.js +GLOBAL_LEVEL: isDevelopment ? 4 : 2, // Debug in dev, Warnings in prod +LOGIN: isDevelopment ? 4 : 1, // Full debug in dev, errors only in prod +API_CLIENT: isDevelopment ? 3 : 1, // Info in dev, errors only in prod +``` + +--- + +## 🧪 Testing Checklist + +### Comprehensive Testing +- [ ] Fresh login (clear all data first) +- [ ] Dashboard loads correctly +- [ ] Stats cards display data +- [ ] Recent vendors table works +- [ ] Sidebar navigation works +- [ ] Dark mode toggle works +- [ ] Logout clears auth and redirects +- [ ] Cannot access dashboard after logout +- [ ] Vendors page loads +- [ ] Users page loads +- [ ] No console errors +- [ ] No 404 errors in Network tab +- [ ] Icons display correctly +- [ ] All Alpine.js components work + +### Browser Testing +- [ ] Chrome/Edge +- [ ] Firefox +- [ ] Safari (if available) +- [ ] Mobile view (responsive) + +--- + +## 🐛 Debugging Guide + +### If Icons Don't Display: +```javascript +// Check in console: +console.log('window.icon:', typeof window.icon); +console.log('window.Icons:', typeof window.Icons); +console.log('$icon available:', typeof Alpine !== 'undefined' && Alpine.magic('icon')); + +// Test manually: +document.body.innerHTML += window.icon('home', 'w-6 h-6'); +``` + +### If Utils Undefined: +```javascript +// Check in console: +console.log('Utils:', typeof Utils); +console.log('Utils methods:', Object.keys(Utils || {})); + +// Test manually: +Utils.showToast('Test message', 'info'); +``` + +### If Auth Fails: +```javascript +// Check storage: +console.log('localStorage token:', localStorage.getItem('admin_token')); +console.log('Cookie:', document.cookie); + +// Test API manually: +fetch('/api/v1/admin/auth/me', { + headers: { 'Authorization': `Bearer ${localStorage.getItem('admin_token')}` } +}).then(r => r.json()).then(console.log); +``` + +--- + +## 📝 Documentation Tasks + +### Update Documentation +- [ ] Update project README with new architecture +- [ ] Document authentication flow (cookies + localStorage) +- [ ] Document template structure +- [ ] Add deployment notes (dev vs production) +- [ ] Update API documentation if needed + +### Code Comments +- [ ] Add comments to complex authentication code +- [ ] Document cookie settings and rationale +- [ ] Explain dual token storage pattern +- [ ] Add JSDoc comments to JavaScript functions + +--- + +## 🚀 Next Phase Preview (After Today) + +### Vendor Portal Migration +1. Apply same Jinja2 pattern to vendor routes +2. Create vendor templates (login, dashboard, etc.) +3. Implement vendor authentication (separate cookie: `vendor_token`) +4. Test vendor flows + +### Customer/Shop Migration +1. Customer authentication system +2. Shop templates +3. Shopping cart (consider cookie vs localStorage) +4. "Remember Me" implementation + +### Advanced Features +1. "Remember Me" checkbox (30-day cookies) +2. Session management +3. Multiple device logout +4. Security enhancements (CSRF tokens) + +--- + +## ⏰ Time Estimates + +| Task | Estimated Time | Priority | +|------|---------------|----------| +| Fix icon/utils issues | 30-45 min | HIGH | +| Test logout flow | 15-30 min | HIGH | +| Test admin pages | 30 min | MEDIUM | +| Create missing templates | 45-60 min | MEDIUM | +| Cleanup old code | 30 min | LOW | +| Testing & verification | 30-45 min | HIGH | +| Documentation | 30 min | LOW | + +**Total: 3-4 hours** + +--- + +## ✅ Success Criteria for Today + +By end of day, we should have: +- [ ] All icons displaying correctly +- [ ] No JavaScript errors in console +- [ ] Logout flow working perfectly +- [ ] All admin pages accessible and working +- [ ] Templates for vendors and users pages +- [ ] Old code cleaned up +- [ ] Comprehensive testing completed +- [ ] Documentation updated + +--- + +## 🎯 Stretch Goals (If Time Permits) + +1. Add loading states to all buttons +2. Improve error messages (user-friendly) +3. Add success/error toasts to all operations +4. Implement "Remember Me" checkbox +5. Start vendor portal migration +6. Add unit tests for authentication + +--- + +## 📞 Support Resources + +### If Stuck: +- Review yesterday's complete file implementations +- Check browser console for detailed logs (log level 4) +- Use test-auth-flow.html for systematic testing +- Check Network tab for HTTP requests/responses + +### Reference Files: +- `static/admin/test-auth-flow.html` - Testing interface +- `TESTING_CHECKLIST.md` - Systematic testing guide +- Yesterday's complete file updates (in conversation) + +--- + +**Good luck with today's tasks! 🚀** + +Remember: Take breaks, test \ No newline at end of file diff --git a/16.jinja2_migration_progress.md b/16.jinja2_migration_progress.md new file mode 100644 index 00000000..e3063a9a --- /dev/null +++ b/16.jinja2_migration_progress.md @@ -0,0 +1,520 @@ +# Jinja2 Migration Progress - Admin Panel + +**Date:** October 20, 2025 +**Project:** Multi-Tenant E-commerce Platform +**Goal:** Migrate from static HTML files to Jinja2 server-rendered templates + +--- + +## 🎯 Current Status: DEBUGGING AUTH LOOP + +We successfully set up the Jinja2 infrastructure but are experiencing authentication redirect loops. We're in the process of simplifying the auth flow to resolve this. + +--- + +## ✅ What's Been Completed + +### 1. Infrastructure Setup ✅ + +- [x] Added Jinja2Templates to `main.py` +- [x] Created `app/templates/` directory structure +- [x] Created `app/api/v1/admin/pages.py` for HTML routes +- [x] Integrated pages router into the main app + +**Files Created:** +``` +app/ +├── templates/ +│ ├── admin/ +│ │ ├── base.html ✅ Created +│ │ ├── login.html ✅ Created +│ │ └── dashboard.html ✅ Created +│ └── partials/ +│ ├── header.html ✅ Moved from static +│ └── sidebar.html ✅ Moved from static +└── api/ + └── v1/ + └── admin/ + └── pages.py ✅ Created +``` + +### 2. Route Configuration ✅ + +**New Jinja2 Routes (working):** +- `/admin/` → redirects to `/admin/dashboard` +- `/admin/login` → login page (no auth) +- `/admin/dashboard` → dashboard page (requires auth) +- `/admin/vendors` → vendors page (requires auth) +- `/admin/users` → users page (requires auth) + +**Old Static Routes (disabled):** +- Commented out admin routes in `app/routes/frontend.py` +- Old `/static/admin/*.html` routes no longer active + +### 3. Exception Handler Updates ✅ + +- [x] Updated `app/exceptions/handler.py` to redirect HTML requests on 401 +- [x] Added `_is_html_page_request()` helper function +- [x] Server-side redirects working for unauthenticated page access + +### 4. JavaScript Updates ✅ + +Updated all JavaScript files to use new routes: + +**Files Updated:** +- `static/admin/js/dashboard.js` - viewVendor() uses `/admin/vendors` +- `static/admin/js/login.js` - redirects to `/admin/dashboard` +- `static/admin/js/vendors.js` - auth checks use `/admin/login` +- `static/admin/js/vendor-edit.js` - all redirects updated +- `static/shared/js/api-client.js` - handleUnauthorized() uses `/admin/login` + +### 5. Template Structure ✅ + +**Base Template (`app/templates/admin/base.html`):** +- Server-side includes for header and sidebar (no more AJAX loading!) +- Proper script loading order +- Alpine.js integration +- No more `partial-loader.js` + +**Dashboard Template (`app/templates/admin/dashboard.html`):** +- Extends base template +- Uses Alpine.js `adminDashboard()` component +- Stats cards and recent vendors table + +**Login Template (`app/templates/admin/login.html`):** +- Standalone page (doesn't extend base) +- Uses Alpine.js `adminLogin()` component + +--- + +## ❌ Current Problem: Authentication Loop + +### Issue Description + +Getting infinite redirect loops in various scenarios: +1. After login → redirects back to login +2. On login page → continuous API calls to `/admin/auth/me` +3. Dashboard → redirects to login → redirects to dashboard + +### Root Causes Identified + +1. **Multiple redirect handlers fighting:** + - Server-side: `handler.py` redirects on 401 for HTML pages + - Client-side: `api-client.js` also redirects on 401 + - Both triggering simultaneously + +2. **Login page checking auth on init:** + - Calls `/admin/auth/me` on page load + - Gets 401 → triggers redirect + - Creates loop + +3. **Token not being sent properly:** + - Token stored but API calls not including it + - Gets 401 even with valid token + +### Latest Approach (In Progress) + +Simplifying to minimal working version: +- Login page does NOTHING on init (no auth checking) +- API client does NOT redirect (just throws errors) +- Server ONLY redirects browser HTML requests (not API calls) +- One source of truth for auth handling + +--- + +## 📝 Files Modified (Complete List) + +### Backend Files + +1. **`main.py`** + ```python + # Added: + - Jinja2Templates import and configuration + - admin_pages router include at /admin prefix + ``` + +2. **`app/api/main.py`** (unchanged - just includes v1 routes) + +3. **`app/api/v1/admin/__init__.py`** + ```python + # Added: + - import pages + - router.include_router(pages.router, tags=["admin-pages"]) + ``` + +4. **`app/api/v1/admin/pages.py`** (NEW FILE) + ```python + # Contains: + - @router.get("/") - root redirect + - @router.get("/login") - login page + - @router.get("/dashboard") - dashboard page + - @router.get("/vendors") - vendors page + - @router.get("/users") - users page + ``` + +5. **`app/routes/frontend.py`** + ```python + # Changed: + - Commented out all /admin/ routes + - Left vendor and shop routes active + ``` + +6. **`app/exceptions/handler.py`** + ```python + # Added: + - 401 redirect logic for HTML pages + - _is_html_page_request() helper + # Status: Needs simplification + ``` + +### Frontend Files + +1. **`static/admin/js/login.js`** + ```javascript + // Changed: + - Removed /static/admin/ paths + - Updated to /admin/ paths + - checkExistingAuth() logic + # Status: Needs simplification + ``` + +2. **`static/admin/js/dashboard.js`** + ```javascript + // Changed: + - viewVendor() uses /admin/vendors + # Status: Working + ``` + +3. **`static/admin/js/vendors.js`** + ```javascript + // Changed: + - checkAuth() redirects to /admin/login + - handleLogout() redirects to /admin/login + # Status: Not tested yet + ``` + +4. **`static/admin/js/vendor-edit.js`** + ```javascript + // Changed: + - All /static/admin/ paths to /admin/ + # Status: Not tested yet + ``` + +5. **`static/shared/js/api-client.js`** + ```javascript + // Changed: + - handleUnauthorized() uses /admin/login + # Status: Needs simplification - causing loops + ``` + +6. **`static/shared/js/utils.js`** (unchanged - working fine) + +### Template Files (NEW) + +1. **`app/templates/admin/base.html`** ✅ + - Master layout with sidebar and header + - Script loading in correct order + - No partial-loader.js + +2. **`app/templates/admin/login.html`** ✅ + - Standalone login page + - Alpine.js adminLogin() component + +3. **`app/templates/admin/dashboard.html`** ✅ + - Extends base.html + - Alpine.js adminDashboard() component + +4. **`app/templates/partials/header.html`** ✅ + - Top navigation bar + - Updated logout link to /admin/login + +5. **`app/templates/partials/sidebar.html`** ✅ + - Side navigation menu + - Updated all links to /admin/* paths + +--- + +## 🔧 Next Steps (Tomorrow) + +### Immediate Priority: Fix Auth Loop + +Apply the simplified approach from the last message: + +1. **Simplify `login.js`:** + ```javascript + // Remove all auth checking on init + // Just show login form + // Only redirect after successful login + ``` + +2. **Simplify `api-client.js`:** + ```javascript + // Remove handleUnauthorized() redirect logic + // Just throw errors, don't redirect + // Let server handle redirects + ``` + +3. **Simplify `handler.py`:** + ```javascript + // Only redirect browser HTML requests (text/html accept header) + // Don't redirect API calls (application/json) + // Don't redirect if already on login page + ``` + +**Test Flow:** +1. Navigate to `/admin/login` → should show form (no loops) +2. Login → should redirect to `/admin/dashboard` +3. Dashboard → should load with sidebar/header +4. No console errors, no 404s for partials + +### After Auth Works + +1. **Create remaining page templates:** + - `app/templates/admin/vendors.html` + - `app/templates/admin/users.html` + - `app/templates/admin/vendor-edit.html` + +2. **Test all admin flows:** + - Login ✓ + - Dashboard ✓ + - Vendors list + - Vendor create + - Vendor edit + - User management + +3. **Cleanup:** + - Remove old static HTML files + - Remove `app/routes/frontend.py` admin routes completely + - Remove `partial-loader.js` + +4. **Migrate vendor portal:** + - Same process for `/vendor/*` routes + - Create vendor templates + - Update vendor JavaScript files + +--- + +## 📚 Key Learnings + +### What Worked + +1. ✅ **Server-side template rendering** - Clean, fast, no AJAX for partials +2. ✅ **Jinja2 integration** - Easy to set up, works with FastAPI +3. ✅ **Route separation** - HTML routes in `pages.py`, API routes separate +4. ✅ **Template inheritance** - `base.html` + `{% extends %}` pattern + +### What Caused Issues + +1. ❌ **Multiple redirect handlers** - Client + server both handling 401 +2. ❌ **Auth checking on login page** - Created loops +3. ❌ **Complex error handling** - Too many places making decisions +4. ❌ **Path inconsistencies** - Old `/static/admin/` vs new `/admin/` + +### Best Practices Identified + +1. **Single source of truth for redirects** - Choose server OR client, not both +2. **Login page should be dumb** - No auth checking, just show form +3. **API client should be simple** - Fetch data, throw errors, don't redirect +4. **Server handles page-level auth** - FastAPI dependencies + exception handler +5. **Clear separation** - HTML pages vs API endpoints + +--- + +## 🗂️ Project Structure (Current) + +``` +project/ +├── main.py ✅ Updated +├── app/ +│ ├── api/ +│ │ ├── main.py ✅ Unchanged +│ │ └── v1/ +│ │ └── admin/ +│ │ ├── __init__.py ✅ Updated +│ │ ├── pages.py ✅ NEW +│ │ ├── auth.py ✅ Existing (API routes) +│ │ ├── vendors.py ✅ Existing (API routes) +│ │ └── dashboard.py ✅ Existing (API routes) +│ ├── routes/ +│ │ └── frontend.py ⚠️ Partially disabled +│ ├── exceptions/ +│ │ └── handler.py ⚠️ Needs simplification +│ └── templates/ ✅ NEW +│ ├── admin/ +│ │ ├── base.html +│ │ ├── login.html +│ │ └── dashboard.html +│ └── partials/ +│ ├── header.html +│ └── sidebar.html +└── static/ + ├── admin/ + │ ├── js/ + │ │ ├── login.js ⚠️ Needs simplification + │ │ ├── dashboard.js ✅ Updated + │ │ ├── vendors.js ✅ Updated + │ │ └── vendor-edit.js ✅ Updated + │ └── css/ + │ └── tailwind.output.css ✅ Unchanged + └── shared/ + └── js/ + ├── api-client.js ⚠️ Needs simplification + ├── utils.js ✅ Working + └── icons.js ✅ Working +``` + +**Legend:** +- ✅ = Working correctly +- ⚠️ = Needs attention/debugging +- ❌ = Not working/causing issues + +--- + +## 🐛 Debug Commands + +### Clear localStorage (Browser Console) +```javascript +localStorage.clear(); +``` + +### Check stored tokens +```javascript +console.log('admin_token:', localStorage.getItem('admin_token')); +console.log('admin_user:', localStorage.getItem('admin_user')); +``` + +### Test API call manually +```javascript +fetch('/api/v1/admin/auth/me', { + headers: { + 'Authorization': `Bearer ${localStorage.getItem('admin_token')}` + } +}).then(r => r.json()).then(d => console.log(d)); +``` + +### Check current route +```javascript +console.log('Current path:', window.location.pathname); +console.log('Full URL:', window.location.href); +``` + +--- + +## 📖 Reference: Working Code Snippets + +### Minimal Login.js (To Try Tomorrow) + +```javascript +function adminLogin() { + return { + dark: false, + credentials: { username: '', password: '' }, + loading: false, + error: null, + success: null, + errors: {}, + + init() { + this.dark = localStorage.getItem('theme') === 'dark'; + // NO AUTH CHECKING - just show form + }, + + async handleLogin() { + if (!this.validateForm()) return; + + this.loading = true; + try { + const response = await fetch('/api/v1/admin/auth/login', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + username: this.credentials.username, + password: this.credentials.password + }) + }); + + const data = await response.json(); + if (!response.ok) throw new Error(data.message); + + localStorage.setItem('admin_token', data.access_token); + localStorage.setItem('admin_user', JSON.stringify(data.user)); + + this.success = 'Login successful!'; + setTimeout(() => window.location.href = '/admin/dashboard', 500); + } catch (error) { + this.error = error.message; + } finally { + this.loading = false; + } + } + } +} +``` + +### Simplified API Client Request Method + +```javascript +async request(endpoint, options = {}) { + const url = `${this.baseURL}${endpoint}`; + const config = { + ...options, + headers: this.getHeaders(options.headers) + }; + + const response = await fetch(url, config); + const data = await response.json(); + + if (!response.ok) { + throw new Error(data.message || 'Request failed'); + } + + return data; + // NO REDIRECT LOGIC HERE! +} +``` + +### Simplified Exception Handler + +```python +if exc.status_code == 401: + accept_header = request.headers.get("accept", "") + is_browser = "text/html" in accept_header + + if is_browser and not request.url.path.endswith("/login"): + if request.url.path.startswith("/admin"): + return RedirectResponse(url="/admin/login", status_code=302) + +# Return JSON for API calls +return JSONResponse(status_code=exc.status_code, content=exc.to_dict()) +``` + +--- + +## 💡 Questions to Answer Tomorrow + +1. Does the simplified auth flow work without loops? +2. Can we successfully login and access dashboard? +3. Are tokens being sent correctly in API requests? +4. Do we need the auth check on login page at all? +5. Should we move ALL redirect logic to server-side? + +--- + +## 🎯 Success Criteria + +The migration will be considered successful when: + +- [ ] Login page loads without loops +- [ ] Login succeeds and redirects to dashboard +- [ ] Dashboard displays with sidebar and header +- [ ] No 404 errors for partials +- [ ] Icons display correctly +- [ ] Stats cards load data from API +- [ ] Navigation between admin pages works +- [ ] Logout works correctly + +--- + +**End of Session - October 20, 2025** + +Good work today! We made significant progress on the infrastructure. Tomorrow we'll resolve the auth loop and complete the admin panel migration. \ No newline at end of file diff --git a/admin_integration_guide.md b/admin_integration_guide.md new file mode 100644 index 00000000..a18d91e6 --- /dev/null +++ b/admin_integration_guide.md @@ -0,0 +1,649 @@ +# Admin Models Integration Guide + +## What We've Added + +You now have: + +1. **Database Models** (`models/database/admin.py`): + - `AdminAuditLog` - Track all admin actions + - `AdminNotification` - System alerts for admins + - `AdminSetting` - Platform-wide settings + - `PlatformAlert` - System health alerts + - `AdminSession` - Track admin login sessions + +2. **Pydantic Schemas** (`models/schemas/admin.py`): + - Request/response models for all admin operations + - Validation for bulk operations + - System health check schemas + +3. **Services**: + - `AdminAuditService` - Audit logging operations + - `AdminSettingsService` - Platform settings management + +4. **API Endpoints**: + - `/api/v1/admin/audit` - Audit log endpoints + - `/api/v1/admin/settings` - Settings management + - `/api/v1/admin/notifications` - Notifications & alerts (stubs) + +--- + +## Step-by-Step Integration + +### Step 1: Update Database + +Add the new models to your database imports: + +```python +# models/database/__init__.py +from .admin import ( + AdminAuditLog, + AdminNotification, + AdminSetting, + PlatformAlert, + AdminSession +) +``` + +Run database migration: +```bash +# Create migration +alembic revision --autogenerate -m "Add admin models" + +# Apply migration +alembic upgrade head +``` + +### Step 2: Update Admin API Router + +```python +# app/api/v1/admin/__init__.py +from fastapi import APIRouter +from . import auth, vendors, users, dashboard, marketplace, audit, settings, notifications + +router = APIRouter(prefix="/admin", tags=["admin"]) + +# Include all admin routers +router.include_router(auth.router) +router.include_router(vendors.router) +router.include_router(users.router) +router.include_router(dashboard.router) +router.include_router(marketplace.router) +router.include_router(audit.router) # NEW +router.include_router(settings.router) # NEW +router.include_router(notifications.router) # NEW +``` + +### Step 3: Add Audit Logging to Existing Admin Operations + +Update your `admin_service.py` to log actions: + +```python +# app/services/admin_service.py +from app.services.admin_audit_service import admin_audit_service + +class AdminService: + + def create_vendor_with_owner( + self, db: Session, vendor_data: VendorCreate + ) -> Tuple[Vendor, User, str]: + """Create vendor with owner user account.""" + + # ... existing code ... + + vendor, owner_user, temp_password = # ... your creation logic + + # LOG THE ACTION + admin_audit_service.log_action( + db=db, + admin_user_id=current_admin_id, # You'll need to pass this + action="create_vendor", + target_type="vendor", + target_id=str(vendor.id), + details={ + "vendor_code": vendor.vendor_code, + "subdomain": vendor.subdomain, + "owner_email": owner_user.email + } + ) + + return vendor, owner_user, temp_password + + def toggle_vendor_status( + self, db: Session, vendor_id: int, admin_user_id: int + ) -> Tuple[Vendor, str]: + """Toggle vendor status with audit logging.""" + + vendor = self._get_vendor_by_id_or_raise(db, vendor_id) + old_status = vendor.is_active + + # ... toggle logic ... + + # LOG THE ACTION + admin_audit_service.log_action( + db=db, + admin_user_id=admin_user_id, + action="toggle_vendor_status", + target_type="vendor", + target_id=str(vendor_id), + details={ + "old_status": "active" if old_status else "inactive", + "new_status": "active" if vendor.is_active else "inactive" + } + ) + + return vendor, message +``` + +### Step 4: Update API Endpoints to Pass Admin User ID + +Your API endpoints need to pass the current admin's ID to service methods: + +```python +# app/api/v1/admin/vendors.py + +@router.post("", response_model=VendorResponse) +def create_vendor_with_owner( + vendor_data: VendorCreate, + db: Session = Depends(get_db), + current_admin: User = Depends(get_current_admin_user), +): + """Create vendor with audit logging.""" + + vendor, owner_user, temp_password = admin_service.create_vendor_with_owner( + db=db, + vendor_data=vendor_data, + admin_user_id=current_admin.id # Pass admin ID for audit logging + ) + + # Audit log is automatically created inside the service + + return { + **VendorResponse.model_validate(vendor).model_dump(), + "owner_email": owner_user.email, + "owner_username": owner_user.username, + "temporary_password": temp_password, + "login_url": f"{vendor.subdomain}.platform.com/vendor/login" + } + + +@router.put("/{vendor_id}/status") +def toggle_vendor_status( + vendor_id: int, + db: Session = Depends(get_db), + current_admin: User = Depends(get_current_admin_user), +): + """Toggle vendor status with audit logging.""" + vendor, message = admin_service.toggle_vendor_status( + db=db, + vendor_id=vendor_id, + admin_user_id=current_admin.id # Pass for audit + ) + return {"message": message, "vendor": VendorResponse.model_validate(vendor)} +``` + +### Step 5: Add Request Context to Audit Logs + +To capture IP address and user agent, use FastAPI's Request object: + +```python +# app/api/v1/admin/vendors.py +from fastapi import Request + +@router.delete("/{vendor_id}") +def delete_vendor( + vendor_id: int, + request: Request, # Add request parameter + confirm: bool = Query(False), + db: Session = Depends(get_db), + current_admin: User = Depends(get_current_admin_user), +): + """Delete vendor with full audit trail.""" + + if not confirm: + raise HTTPException(status_code=400, detail="Confirmation required") + + # Get request metadata + ip_address = request.client.host if request.client else None + user_agent = request.headers.get("user-agent") + + message = admin_service.delete_vendor(db, vendor_id) + + # Log with full context + admin_audit_service.log_action( + db=db, + admin_user_id=current_admin.id, + action="delete_vendor", + target_type="vendor", + target_id=str(vendor_id), + ip_address=ip_address, + user_agent=user_agent, + details={"confirm": True} + ) + + return {"message": message} +``` + +--- + +## Example: Platform Settings Usage + +### Creating Default Settings + +```python +# scripts/init_platform_settings.py +from app.core.database import SessionLocal +from app.services.admin_settings_service import admin_settings_service +from models.schemas.admin import AdminSettingCreate + +db = SessionLocal() + +# Create default platform settings +settings = [ + AdminSettingCreate( + key="max_vendors_allowed", + value="1000", + value_type="integer", + category="system", + description="Maximum number of vendors allowed on the platform", + is_public=False + ), + AdminSettingCreate( + key="maintenance_mode", + value="false", + value_type="boolean", + category="system", + description="Enable maintenance mode (blocks all non-admin access)", + is_public=True + ), + AdminSettingCreate( + key="vendor_trial_days", + value="30", + value_type="integer", + category="system", + description="Default trial period for new vendors (days)", + is_public=False + ), + AdminSettingCreate( + key="stripe_publishable_key", + value="pk_test_...", + value_type="string", + category="payments", + description="Stripe publishable key", + is_public=True + ), + AdminSettingCreate( + key="stripe_secret_key", + value="sk_test_...", + value_type="string", + category="payments", + description="Stripe secret key", + is_encrypted=True, + is_public=False + ) +] + +for setting_data in settings: + try: + admin_settings_service.upsert_setting(db, setting_data, admin_user_id=1) + print(f"✓ Created setting: {setting_data.key}") + except Exception as e: + print(f"✗ Failed to create {setting_data.key}: {e}") + +db.close() +``` + +### Using Settings in Your Code + +```python +# app/services/vendor_service.py +from app.services.admin_settings_service import admin_settings_service + +def can_create_vendor(db: Session) -> bool: + """Check if platform allows creating more vendors.""" + + max_vendors = admin_settings_service.get_setting_value( + db=db, + key="max_vendors_allowed", + default=1000 + ) + + current_count = db.query(Vendor).count() + + return current_count < max_vendors + + +def is_maintenance_mode(db: Session) -> bool: + """Check if platform is in maintenance mode.""" + return admin_settings_service.get_setting_value( + db=db, + key="maintenance_mode", + default=False + ) +``` + +--- + +## Frontend Integration + +### Admin Dashboard with Audit Logs + +```html + +
+

Audit Logs

+ + +
+ + + +
+ + + + + + + + + + + + + + + + +
TimestampAdminActionTargetDetailsIP Address
+ + + +
+ + +``` + +### Platform Settings Management + +```html + +
+

Platform Settings

+ + +
+ + + +
+ + +
+ +
+ + + +
+ + +``` + +--- + +## Testing the New Features + +### Test Audit Logging + +```python +# tests/test_admin_audit.py +import pytest +from app.services.admin_audit_service import admin_audit_service + +def test_log_admin_action(db_session, test_admin_user): + """Test logging admin actions.""" + log = admin_audit_service.log_action( + db=db_session, + admin_user_id=test_admin_user.id, + action="create_vendor", + target_type="vendor", + target_id="123", + details={"vendor_code": "TEST"} + ) + + assert log is not None + assert log.action == "create_vendor" + assert log.target_type == "vendor" + assert log.details["vendor_code"] == "TEST" + +def test_query_audit_logs(db_session, test_admin_user): + """Test querying audit logs with filters.""" + # Create test logs + for i in range(5): + admin_audit_service.log_action( + db=db_session, + admin_user_id=test_admin_user.id, + action=f"test_action_{i}", + target_type="test", + target_id=str(i) + ) + + # Query logs + from models.schemas.admin import AdminAuditLogFilters + filters = AdminAuditLogFilters(limit=10) + logs = admin_audit_service.get_audit_logs(db_session, filters) + + assert len(logs) == 5 +``` + +### Test Platform Settings + +```python +# tests/test_admin_settings.py +def test_create_setting(db_session, test_admin_user): + """Test creating platform setting.""" + from models.schemas.admin import AdminSettingCreate + + setting_data = AdminSettingCreate( + key="test_setting", + value="test_value", + value_type="string", + category="test" + ) + + result = admin_settings_service.create_setting( + db=db_session, + setting_data=setting_data, + admin_user_id=test_admin_user.id + ) + + assert result.key == "test_setting" + assert result.value == "test_value" + +def test_get_setting_value_with_type_conversion(db_session): + """Test getting setting values with proper type conversion.""" + # Create integer setting + setting_data = AdminSettingCreate( + key="max_vendors", + value="100", + value_type="integer", + category="system" + ) + admin_settings_service.create_setting(db_session, setting_data, 1) + + # Get value (should be converted to int) + value = admin_settings_service.get_setting_value(db_session, "max_vendors") + assert isinstance(value, int) + assert value == 100 +``` + +--- + +## Summary + +You now have a complete admin infrastructure with: + +✅ **Audit Logging**: Track all admin actions for compliance +✅ **Platform Settings**: Manage global configuration +✅ **Notifications**: System alerts for admins (structure ready) +✅ **Platform Alerts**: Health monitoring (structure ready) +✅ **Session Tracking**: Monitor admin logins (structure ready) + +### Next Steps + +1. **Apply database migrations** to create new tables +2. **Update admin API router** to include new endpoints +3. **Add audit logging** to existing admin operations +4. **Create default platform settings** using the script +5. **Build frontend pages** for audit logs and settings +6. **Implement notification service** (notifications.py stubs) +7. **Add monitoring** for platform alerts + +These additions make your platform production-ready with full compliance and monitoring capabilities! \ No newline at end of file diff --git a/app/api/deps.py b/app/api/deps.py index e4eedbf5..d9537bd3 100644 --- a/app/api/deps.py +++ b/app/api/deps.py @@ -1,13 +1,18 @@ # app/api/deps.py -"""Summary description .... +""" +Authentication dependencies for FastAPI routes. -This module provides classes and functions for: -- .... -- .... -- .... +Implements dual token storage pattern: +- Checks Authorization header first (for API calls from JavaScript) +- Falls back to cookie (for browser page navigation) + +This allows: +- JavaScript API calls: Use localStorage + Authorization header +- Browser page loads: Use HTTP-only cookies """ -from fastapi import Depends +from typing import Optional +from fastapi import Depends, Request, Cookie from fastapi.security import HTTPAuthorizationCredentials, HTTPBearer from sqlalchemy.orm import Session @@ -16,7 +21,12 @@ from middleware.auth import AuthManager from middleware.rate_limiter import RateLimiter from models.database.vendor import Vendor from models.database.user import User -from app.exceptions import (AdminRequiredException, VendorNotFoundException, UnauthorizedVendorAccessException) +from app.exceptions import ( + AdminRequiredException, + VendorNotFoundException, + UnauthorizedVendorAccessException, + InvalidTokenException +) # Set auto_error=False to prevent automatic 403 responses security = HTTPBearer(auto_error=False) @@ -25,30 +35,107 @@ rate_limiter = RateLimiter() def get_current_user( - credentials: HTTPAuthorizationCredentials = Depends(security), - db: Session = Depends(get_db), + request: Request, + credentials: Optional[HTTPAuthorizationCredentials] = Depends(security), + admin_token: Optional[str] = Cookie(None), # Check admin_token cookie + db: Session = Depends(get_db), ): - """Get current authenticated user.""" - # Check if credentials are provided - if not credentials: - from app.exceptions.auth import InvalidTokenException - raise InvalidTokenException("Authorization header required") + """ + Get current authenticated user. - return auth_manager.get_current_user(db, credentials) + Checks for token in this priority order: + 1. Authorization header (for API calls from JavaScript) + 2. admin_token cookie (for browser page navigation) + This dual approach supports: + - API calls: JavaScript adds token from localStorage to Authorization header + - Page navigation: Browser automatically sends cookie + + Args: + request: FastAPI request object + credentials: Optional Bearer token from Authorization header + admin_token: Optional token from cookie + db: Database session + + Returns: + User: Authenticated user object + + Raises: + InvalidTokenException: If no token found or token invalid + """ + token = None + token_source = None + + # Priority 1: Authorization header (API calls from JavaScript) + if credentials: + token = credentials.credentials + token_source = "header" + + # Priority 2: Cookie (browser page navigation) + elif admin_token: + token = admin_token + token_source = "cookie" + + # No token found in either location + if not token: + raise InvalidTokenException("Authorization header or cookie required") + + # Log token source for debugging + import logging + logger = logging.getLogger(__name__) + logger.debug(f"Token found in {token_source} for {request.url.path}") + + # Create a mock credentials object for auth_manager + mock_credentials = HTTPAuthorizationCredentials( + scheme="Bearer", + credentials=token + ) + + return auth_manager.get_current_user(db, mock_credentials) def get_current_admin_user(current_user: User = Depends(get_current_user)): - """Require admin user.""" + """ + Require admin user. + + This dependency ensures the current user has admin role. + Used for protecting admin-only routes. + + Args: + current_user: User object from get_current_user dependency + + Returns: + User: Admin user object + + Raises: + AdminRequiredException: If user is not an admin + """ return auth_manager.require_admin(current_user) def get_user_vendor( - vendor_code: str, - current_user: User = Depends(get_current_user), - db: Session = Depends(get_db), + vendor_code: str, + current_user: User = Depends(get_current_user), + db: Session = Depends(get_db), ): - """Get vendor and verify user ownership.""" + """ + Get vendor and verify user ownership. + + Ensures the current user has access to the specified vendor. + Admin users can access any vendor, regular users only their own. + + Args: + vendor_code: Vendor code to look up + current_user: Current authenticated user + db: Database session + + Returns: + Vendor: Vendor object if user has access + + Raises: + VendorNotFoundException: If vendor doesn't exist + UnauthorizedVendorAccessException: If user doesn't have access + """ vendor = db.query(Vendor).filter(Vendor.vendor_code == vendor_code.upper()).first() if not vendor: raise VendorNotFoundException(vendor_code) @@ -57,4 +144,3 @@ def get_user_vendor( raise UnauthorizedVendorAccessException(vendor_code, current_user.id) return vendor - diff --git a/app/api/v1/admin/__init__.py b/app/api/v1/admin/__init__.py index f5650c22..01c143c3 100644 --- a/app/api/v1/admin/__init__.py +++ b/app/api/v1/admin/__init__.py @@ -8,9 +8,10 @@ This module combines all admin-related API endpoints: - User management (status, roles) - Dashboard and statistics - Marketplace monitoring -- Audit logging (NEW) -- Platform settings (NEW) -- Notifications and alerts (NEW) +- Audit logging +- Platform settings +- Notifications and alerts +- HTML Pages - Server-rendered pages using Jinja2 """ from fastapi import APIRouter @@ -25,7 +26,8 @@ from . import ( monitoring, audit, settings, - notifications + notifications, + pages ) # Create admin router @@ -51,7 +53,7 @@ router.include_router(marketplace.router, tags=["admin-marketplace"]) # router.include_router(monitoring.router, tags=["admin-monitoring"]) # ============================================================================ -# NEW: Admin Models Integration +# Admin Models Integration # ============================================================================ # Include audit logging endpoints @@ -63,6 +65,12 @@ router.include_router(settings.router, tags=["admin-settings"]) # Include notifications and alerts endpoints router.include_router(notifications.router, tags=["admin-notifications"]) +# ============================================================================ +# HTML Page Routes (Jinja2 Templates) +# ============================================================================ + +# Include HTML page routes (these return rendered templates, not JSON) +router.include_router(pages.router, tags=["admin-pages"]) # Export the router __all__ = ["router"] diff --git a/app/api/v1/admin/auth.py b/app/api/v1/admin/auth.py index d4f4a8b7..6f676b12 100644 --- a/app/api/v1/admin/auth.py +++ b/app/api/v1/admin/auth.py @@ -2,32 +2,42 @@ """ Admin authentication endpoints. -This module provides: -- Admin user login -- Admin token validation -- Admin-specific authentication logic +Implements dual token storage: +- Sets HTTP-only cookie for browser page navigation +- Returns token in response for localStorage (API calls) """ import logging -from fastapi import APIRouter, Depends +from fastapi import APIRouter, Depends, Response from sqlalchemy.orm import Session from app.core.database import get_db from app.services.auth_service import auth_service from app.exceptions import InvalidCredentialsException -from models.schema.auth import LoginResponse, UserLogin +from models.schema.auth import LoginResponse, UserLogin, UserResponse +from models.database.user import User +from app.api.deps import get_current_admin_user +from app.core.config import settings router = APIRouter(prefix="/auth") logger = logging.getLogger(__name__) @router.post("/login", response_model=LoginResponse) -def admin_login(user_credentials: UserLogin, db: Session = Depends(get_db)): +def admin_login( + user_credentials: UserLogin, + response: Response, + db: Session = Depends(get_db) +): """ Admin login endpoint. Only allows users with 'admin' role to login. Returns JWT token for authenticated admin users. + + Sets token in two places: + 1. HTTP-only cookie (for browser page navigation) + 2. Response body (for localStorage and API calls) """ # Authenticate user login_result = auth_service.login_user(db=db, user_credentials=user_credentials) @@ -39,6 +49,20 @@ def admin_login(user_credentials: UserLogin, db: Session = Depends(get_db)): logger.info(f"Admin login successful: {login_result['user'].username}") + # Set HTTP-only cookie for browser navigation + response.set_cookie( + key="admin_token", + value=login_result["token_data"]["access_token"], + httponly=True, # JavaScript cannot access (XSS protection) + secure=False, # Set to True in production (requires HTTPS) + samesite="lax", # CSRF protection + max_age=login_result["token_data"]["expires_in"], # Match JWT expiry + path="/", # Available for all routes + ) + + logger.debug(f"Set admin_token cookie with {login_result['token_data']['expires_in']}s expiry") + + # Also return token in response for localStorage (API calls) return LoginResponse( access_token=login_result["token_data"]["access_token"], token_type=login_result["token_data"]["token_type"], @@ -47,12 +71,40 @@ def admin_login(user_credentials: UserLogin, db: Session = Depends(get_db)): ) +@router.get("/me", response_model=UserResponse) +def get_current_admin(current_user: User = Depends(get_current_admin_user)): + """ + Get current authenticated admin user. + + This endpoint validates the token and ensures the user has admin privileges. + Returns the current user's information. + + Token can come from: + - Authorization header (API calls) + - admin_token cookie (browser navigation) + """ + logger.info(f"Admin user info requested: {current_user.username}") + + # Pydantic will automatically serialize the User model to UserResponse + return current_user + + @router.post("/logout") -def admin_logout(): +def admin_logout(response: Response): """ Admin logout endpoint. - Client should remove token from storage. - Server-side token invalidation can be implemented here if needed. + Clears the admin_token cookie. + Client should also remove token from localStorage. """ + logger.info("Admin logout") + + # Clear the cookie + response.delete_cookie( + key="admin_token", + path="/", + ) + + logger.debug("Deleted admin_token cookie") + return {"message": "Logged out successfully"} diff --git a/app/api/v1/admin/pages.py b/app/api/v1/admin/pages.py new file mode 100644 index 00000000..8740bec8 --- /dev/null +++ b/app/api/v1/admin/pages.py @@ -0,0 +1,110 @@ +# app/api/v1/admin/pages.py +""" +Admin HTML page routes using Jinja2 templates. + +These routes return rendered HTML pages (response_class=HTMLResponse). +Separate from other admin routes which return JSON data. + +Routes: +- GET / - Admin root (redirects to login) +- GET /login - Admin login page (no auth required) +- GET /dashboard - Admin dashboard (requires auth) +- GET /vendors - Vendor management page (requires auth) +- GET /users - User management page (requires auth) +""" + +from fastapi import APIRouter, Request, Depends +from fastapi.responses import HTMLResponse, RedirectResponse +from fastapi.templating import Jinja2Templates +from sqlalchemy.orm import Session + +from app.api.deps import get_current_admin_user, get_db +from models.database.user import User + +router = APIRouter() +templates = Jinja2Templates(directory="app/templates") + + +@router.get("/", response_class=RedirectResponse, include_in_schema=False) +async def admin_root(): + """ + Redirect /admin/ to /admin/login. + + This is the simplest approach: + - Unauthenticated users: see login form + - Authenticated users: login page clears token and shows form + (they can manually navigate to dashboard if needed) + + Alternative: Could redirect to /admin/dashboard and let auth + dependency handle the redirect, but that's an extra hop. + """ + return RedirectResponse(url="/admin/login", status_code=302) + + +@router.get("/login", response_class=HTMLResponse, include_in_schema=False) +async def admin_login_page(request: Request): + """ + Render admin login page. + No authentication required. + """ + return templates.TemplateResponse( + "admin/login.html", + {"request": request} + ) + + +@router.get("/dashboard", response_class=HTMLResponse, include_in_schema=False) +async def admin_dashboard_page( + request: Request, + current_user: User = Depends(get_current_admin_user), + db: Session = Depends(get_db) +): + """ + Render admin dashboard page. + Requires admin authentication - will redirect to login if not authenticated. + """ + return templates.TemplateResponse( + "admin/dashboard.html", + { + "request": request, + "user": current_user, + } + ) + + +@router.get("/vendors", response_class=HTMLResponse, include_in_schema=False) +async def admin_vendors_page( + request: Request, + current_user: User = Depends(get_current_admin_user), + db: Session = Depends(get_db) +): + """ + Render vendors management page. + Requires admin authentication. + """ + return templates.TemplateResponse( + "admin/vendors.html", + { + "request": request, + "user": current_user, + } + ) + + +@router.get("/users", response_class=HTMLResponse, include_in_schema=False) +async def admin_users_page( + request: Request, + current_user: User = Depends(get_current_admin_user), + db: Session = Depends(get_db) +): + """ + Render users management page. + Requires admin authentication. + """ + return templates.TemplateResponse( + "admin/users.html", + { + "request": request, + "user": current_user, + } + ) diff --git a/app/core/database.py b/app/core/database.py index 5126e1e6..72e3ed2f 100644 --- a/app/core/database.py +++ b/app/core/database.py @@ -1,10 +1,11 @@ # app/core/database.py -"""Summary description .... +""" +Database configuration and session management. This module provides classes and functions for: -- .... -- .... -- .... +- Database engine creation and configuration +- Session management with connection pooling +- Database dependency for FastAPI routes """ import logging @@ -21,16 +22,19 @@ Base = declarative_base() logger = logging.getLogger(__name__) -# Database dependency with connection pooling - def get_db(): - """Get database object.""" + """ + Database session dependency for FastAPI routes. + + Yields a database session and ensures proper cleanup. + Handles exceptions and rolls back transactions on error. + """ db = SessionLocal() try: yield db except Exception as e: - logger.error(f"Health check failed: {e}") + logger.error(f"Database session error: {e}") db.rollback() raise finally: diff --git a/app/exceptions/__init__.py b/app/exceptions/__init__.py index 9ed54b3d..a24b79a1 100644 --- a/app/exceptions/__init__.py +++ b/app/exceptions/__init__.py @@ -1,6 +1,6 @@ # app/exceptions/__init__.py """ -Custom exception classes for the LetzVendor API. +Custom exception classes for the API. This module provides frontend-friendly exceptions with consistent error codes, messages, and HTTP status mappings. diff --git a/app/exceptions/base.py b/app/exceptions/base.py index f361788c..8f350364 100644 --- a/app/exceptions/base.py +++ b/app/exceptions/base.py @@ -1,6 +1,6 @@ # app/exceptions/base.py """ -Base exception classes for the LetzVendor application. +Base exception classes for the application. This module provides classes and functions for: - Base exception class with consistent error formatting @@ -12,7 +12,7 @@ from typing import Any, Dict, Optional class LetzShopException(Exception): - """Base exception class for all LetzVendor custom exceptions.""" + """Base exception class for all custom exceptions.""" def __init__( self, diff --git a/app/exceptions/handler.py b/app/exceptions/handler.py index b8bc2142..99cb7705 100644 --- a/app/exceptions/handler.py +++ b/app/exceptions/handler.py @@ -14,7 +14,7 @@ from typing import Union from fastapi import Request, HTTPException from fastapi.exceptions import RequestValidationError -from fastapi.responses import JSONResponse +from fastapi.responses import JSONResponse, RedirectResponse from .base import LetzShopException @@ -26,7 +26,28 @@ def setup_exception_handlers(app): @app.exception_handler(LetzShopException) async def custom_exception_handler(request: Request, exc: LetzShopException): - """Handle custom LetzVendor exceptions.""" + """Handle custom exceptions.""" + + # Special handling for 401 on HTML page requests (redirect to login) + if exc.status_code == 401 and _is_html_page_request(request): + logger.info( + f"401 on HTML page request - redirecting to login: {request.url.path}", + extra={ + "path": request.url.path, + "accept": request.headers.get("accept", ""), + "method": request.method + } + ) + + # Redirect to appropriate login page + if request.url.path.startswith("/admin"): + logger.debug("Redirecting to /admin/login") + return RedirectResponse(url="/admin/login", status_code=302) + elif "/vendor/" in request.url.path: + logger.debug("Redirecting to /vendor/login") + return RedirectResponse(url="/vendor/login", status_code=302) + # If neither, fall through to JSON response + logger.debug("No specific redirect path matched, returning JSON") logger.error( f"Custom exception in {request.method} {request.url}: " @@ -162,6 +183,51 @@ def setup_exception_handlers(app): } ) + +def _is_html_page_request(request: Request) -> bool: + """ + Check if the request is for an HTML page (not an API endpoint). + + More precise detection: + - Must NOT have /api/ in path + - Must be GET request + - Must explicitly accept text/html + - Must not already be on login page + """ + logger.debug( + f"Checking if HTML page request: {request.url.path}", + extra={ + "path": request.url.path, + "method": request.method, + "accept": request.headers.get("accept", "") + } + ) + + # Don't redirect API calls + if "/api/" in request.url.path: + logger.debug("Not HTML page: API endpoint") + return False + + # Don't redirect if already on login page + if request.url.path.endswith("/login"): + logger.debug("Not HTML page: Already on login page") + return False + + # Only redirect GET requests (page loads) + if request.method != "GET": + logger.debug(f"Not HTML page: Method is {request.method}, not GET") + return False + + # MUST explicitly accept HTML (strict check) + accept_header = request.headers.get("accept", "") + if "text/html" not in accept_header: + logger.debug(f"Not HTML page: Accept header doesn't include text/html: {accept_header}") + return False + + logger.debug("IS HTML page request - will redirect on 401") + return True + + # Utility functions for common exception scenarios def raise_not_found(resource_type: str, identifier: str) -> None: """Convenience function to raise ResourceNotFoundException.""" diff --git a/app/routes/frontend.py b/app/routes/frontend.py index c3930fce..53186466 100644 --- a/app/routes/frontend.py +++ b/app/routes/frontend.py @@ -13,31 +13,31 @@ router = APIRouter(include_in_schema=False) # ============================================================================ -# ADMIN ROUTES +# ADMIN ROUTES - DISABLED (Now using Jinja2 templates in pages.py) # ============================================================================ -@router.get("/admin/") -@router.get("/admin/login") -async def admin_login(): - """Serve admin login page""" - return FileResponse("static/admin/login.html") +# @router.get("/admin/") +# @router.get("/admin/login") +# async def admin_login(): +# """Serve admin login page""" +# return FileResponse("static/admin/login.html") -@router.get("/admin/dashboard") -async def admin_dashboard(): - """Serve admin dashboard page""" - return FileResponse("static/admin/dashboard.html") +# @router.get("/admin/dashboard") +# async def admin_dashboard(): +# """Serve admin dashboard page""" +# return FileResponse("static/admin/dashboard.html") -@router.get("/admin/vendors") -async def admin_vendors(): - """Serve admin vendors management page""" - return FileResponse("static/admin/vendors.html") +# @router.get("/admin/vendors") +# async def admin_vendors(): +# """Serve admin vendors management page""" +# return FileResponse("static/admin/vendors.html") -@router.get("/admin/vendor-edit") -async def admin_vendor_edit(): - """Serve admin vendor edit page""" - return FileResponse("static/admin/vendor-edit.html") +# @router.get("/admin/vendor-edit") +# async def admin_vendor_edit(): +# """Serve admin vendor edit page""" +# return FileResponse("static/admin/vendor-edit.html") # ============================================================================ # VENDOR ROUTES (with vendor code in path) diff --git a/app/templates/admin/base.html b/app/templates/admin/base.html new file mode 100644 index 00000000..bee5be62 --- /dev/null +++ b/app/templates/admin/base.html @@ -0,0 +1,60 @@ +{# app/templates/admin/base.html #} + + + + + + {% block title %}Admin Panel{% endblock %} - Multi-Tenant Platform + + + + + + + + + + + {% block extra_head %}{% endblock %} + + +
+ + {% include 'partials/sidebar.html' %} + +
+ + {% include 'partials/header.html' %} + + +
+
+ {% block content %}{% endblock %} +
+
+
+
+ + + + + + + + + + + + + + + + + + + + {% block extra_scripts %}{% endblock %} + + \ No newline at end of file diff --git a/app/templates/admin/dashboard.html b/app/templates/admin/dashboard.html new file mode 100644 index 00000000..f29865f9 --- /dev/null +++ b/app/templates/admin/dashboard.html @@ -0,0 +1,172 @@ +{# app/templates/admin/dashboard.html #} +{% extends "admin/base.html" %} + +{% block title %}Dashboard{% endblock %} + +{% block alpine_data %}adminDashboard(){% endblock %} + +{% block content %} + +
+

+ Dashboard +

+ +
+ + +
+ +

Loading dashboard...

+
+ + +
+ +
+

Error loading dashboard

+

+
+
+ + +
+ +
+
+ +
+
+

+ Total Vendors +

+

+ 0 +

+
+
+ + +
+
+ +
+
+

+ Active Users +

+

+ 0 +

+
+
+ + +
+
+ +
+
+

+ Verified Vendors +

+

+ 0 +

+
+
+ + +
+
+ +
+
+

+ Import Jobs +

+

+ 0 +

+
+
+
+ + +
+
+ + + + + + + + + + + + + + +
VendorStatusCreatedActions
+
+
+{% endblock %} + +{% block extra_scripts %} + +{% endblock %} \ No newline at end of file diff --git a/app/templates/admin/login.html b/app/templates/admin/login.html new file mode 100644 index 00000000..75ac490a --- /dev/null +++ b/app/templates/admin/login.html @@ -0,0 +1,109 @@ +{# app/templates/admin/login.html #} + + + + + + Admin Login - Multi-Tenant Platform + + + + + +
+
+
+
+ + +
+
+
+

+ Admin Login +

+ + +
+ +
+ + +
+ + + + + +
+ +
+ +

+ + Forgot your password? + +

+

+ + ← Back to Platform + +

+
+
+
+
+
+ + + + + + + + \ No newline at end of file diff --git a/static/admin/partials/header.html b/app/templates/partials/header.html similarity index 84% rename from static/admin/partials/header.html rename to app/templates/partials/header.html index ec9e1b8b..a0cb30ed 100644 --- a/static/admin/partials/header.html +++ b/app/templates/partials/header.html @@ -100,7 +100,8 @@
  • - + Log out @@ -110,24 +111,4 @@
  • -.756-2.924 0-3.35a1.724 1.724 0 001.066-2.573c-.94-1.543.826-3.31 2.37-2.37.996.608 2.296.07 2.572-1.065z"> - - - Settings - - -
  • - - - - Log out - -
  • - - - - - \ No newline at end of file diff --git a/static/admin/partials/sidebar.html b/app/templates/partials/sidebar.html similarity index 92% rename from static/admin/partials/sidebar.html rename to app/templates/partials/sidebar.html index a5179ff2..a40e46d9 100644 --- a/static/admin/partials/sidebar.html +++ b/app/templates/partials/sidebar.html @@ -1,7 +1,8 @@ + -
    - + Admin Portal
      @@ -77,7 +79,7 @@ + href="/admin/dashboard"> Dashboard @@ -88,19 +90,21 @@ + href="/admin/vendors"> Vendors
    • - + Users
    • - + Import Jobs diff --git a/docs/project-roadmap/slice1_doc.md b/docs/project-roadmap/slice1_doc.md index 0d5ba0b0..f51c6af4 100644 --- a/docs/project-roadmap/slice1_doc.md +++ b/docs/project-roadmap/slice1_doc.md @@ -324,6 +324,7 @@ async def vendor_context_middleware(request: Request, call_next): ### Template Structure (Jinja2) #### Base Template (`templates/base.html`) + ```html @@ -331,20 +332,20 @@ async def vendor_context_middleware(request: Request, call_next): {% block title %}Multi-Tenant Platform{% endblock %} - + {% block extra_css %}{% endblock %} - + - {% block content %}{% endblock %} - - - - {% block extra_scripts %}{% endblock %} +{% block content %}{% endblock %} + + + +{% block extra_scripts %}{% endblock %} ``` diff --git a/frontend-structure.txt b/frontend-structure.txt new file mode 100644 index 00000000..c479116a --- /dev/null +++ b/frontend-structure.txt @@ -0,0 +1,98 @@ +Frontend Folder Structure +Generated: 18/10/2025 13:53:32.04 +============================================================================== + +Folder PATH listing for volume Data2 +Volume serial number is 00000011 A008:CC27 +E:\LETZSHOP-IMPORT\STATIC ++---admin +| dashboard.html +| login.html +| marketplace.html +| monitoring.html +| users.html +| vendor-edit.html +| vendors.html +| ++---css +| +---admin +| | admin.css +| | +| +---shared +| | auth.css +| | base.css +| | responsive-utilities.css +| | +| +---shop +| +---themes +| \---vendor +| vendor.css +| ++---js +| +---admin +| | analytics.js +| | dashboard.js +| | login.js +| | monitoring.js +| | vendor-edit.js +| | vendors.js +| | +| +---shared +| | api-client.js +| | media-upload.js +| | notification.js +| | search.js +| | vendor-context.js +| | +| +---shop +| | account.js +| | cart.js +| | catalog.js +| | checkout.js +| | search.js +| | +| \---vendor +| dashboard.js +| login.js +| marketplace.js +| media.js +| orders.js +| payments.js +| products.js +| ++---shop +| | cart.html +| | checkout.html +| | home.html +| | product.html +| | products.html +| | search.html +| | +| \---account +| addresses.html +| login.html +| orders.html +| profile.html +| register.html +| +\---vendor + | dashboard.html + | login.html + | + \---admin + | customers.html + | inventory.html + | media.html + | notifications.html + | orders.html + | payments.html + | products.html + | settings.html + | teams.html + | + \---marketplace + browse.html + config.html + imports.html + selected.html + diff --git a/main.py b/main.py index 11602ffb..872f1404 100644 --- a/main.py +++ b/main.py @@ -3,15 +3,17 @@ import logging from datetime import datetime, timezone from pathlib import Path -from fastapi import Depends, FastAPI, HTTPException +from fastapi import Depends, FastAPI, HTTPException, Request from fastapi.middleware.cors import CORSMiddleware from fastapi.responses import HTMLResponse, RedirectResponse from fastapi.staticfiles import StaticFiles +from fastapi.templating import Jinja2Templates from sqlalchemy import text from sqlalchemy.orm import Session from app.api.main import api_router -from app.routes.frontend import router as frontend_router +from app.routes.frontend import router as frontend_router # We'll phase this out +from app.api.v1.admin import pages as admin_pages from app.core.config import settings from app.core.database import get_db from app.core.lifespan import lifespan @@ -24,6 +26,7 @@ logger = logging.getLogger(__name__) # Get the project root directory (where main.py is located) BASE_DIR = Path(__file__).resolve().parent STATIC_DIR = BASE_DIR / "static" +TEMPLATES_DIR = BASE_DIR / "app" / "templates" # FastAPI app with lifespan app = FastAPI( @@ -33,6 +36,9 @@ app = FastAPI( lifespan=lifespan, ) +# Configure Jinja2 Templates +templates = Jinja2Templates(directory=str(TEMPLATES_DIR)) + # Setup custom exception handlers (unified approach) setup_exception_handlers(app) @@ -45,7 +51,7 @@ app.add_middleware( allow_headers=["*"], ) -# Add vendor context middleware (ADDED - must be after CORS) +# Add vendor context middleware (must be after CORS) app.middleware("http")(vendor_context_middleware) # ======================================== @@ -57,8 +63,21 @@ else: logger.warning(f"Static directory not found at {STATIC_DIR}") # ======================================== -# Include API router +# Include API router (JSON endpoints at /api/*) app.include_router(api_router, prefix="/api") + +# ============================================================================ +# Include HTML page routes (Jinja2 templates at /admin/*) +# ============================================================================ +app.include_router( + admin_pages.router, + prefix="/admin", + tags=["admin-pages"], + include_in_schema=False # Don't show HTML pages in API docs +) +# ============================================================================ + +# OLD: Keep frontend router for now (we'll phase it out) app.include_router(frontend_router) # Public Routes (no authentication required) diff --git a/requirements.txt b/requirements.txt index 6b583ac9..c7ce5cef 100644 --- a/requirements.txt +++ b/requirements.txt @@ -15,7 +15,7 @@ alembic==1.14.0 # Authentication and Security python-jose[cryptography]==3.3.0 passlib[bcrypt]==1.7.4 -bcrypt==4.2.1 +bcrypt==4.0.1 # Changed from 4.2.1 for Python 3.13.5 compatibility python-multipart==0.0.20 # Data processing diff --git a/scripts/init_db.py b/scripts/init_db.py new file mode 100644 index 00000000..e69de29b diff --git a/static/admin/dashboard.html b/static/admin/dashboard.html index 87299460..4e6a6c96 100644 --- a/static/admin/dashboard.html +++ b/static/admin/dashboard.html @@ -1,5 +1,5 @@ - + @@ -12,28 +12,53 @@
      - +
      - +
      -

      - Dashboard -

      + +
      +

      + Dashboard +

      + +
      + + +
      + +

      Loading dashboard...

      +
      + + +
      + +
      +

      Error loading dashboard

      +

      +
      +
      -
      +
      - - - +

      @@ -48,9 +73,7 @@

      - - - +

      @@ -65,9 +88,7 @@

      - - - +

      @@ -82,9 +103,7 @@

      - - - +

      @@ -98,7 +117,7 @@

      -
      +
      @@ -110,21 +129,24 @@ -