refactor: migrate vendor APIs to token-based context and consolidate architecture

## Vendor-in-Token Architecture (Complete Migration)
- Migrate all vendor API endpoints from require_vendor_context() to token_vendor_id
- Update permission dependencies to extract vendor from JWT token
- Add vendor exceptions: VendorAccessDeniedException, VendorOwnerOnlyException,
  InsufficientVendorPermissionsException
- Shop endpoints retain require_vendor_context() for URL-based detection
- Add AUTH-004 architecture rule enforcing vendor context patterns
- Fix marketplace router missing /marketplace prefix

## Exception Pattern Fixes (API-003/API-004)
- Services raise domain exceptions, endpoints let them bubble up
- Add code_quality and content_page exception modules
- Move business logic from endpoints to services (admin, auth, content_page)
- Fix exception handling in admin, shop, and vendor endpoints

## Tailwind CSS Consolidation
- Consolidate CSS to per-area files (admin, vendor, shop, platform)
- Remove shared/cdn-fallback.html and shared/css/tailwind.min.css
- Update all templates to use area-specific Tailwind output files
- Remove Node.js config (package.json, postcss.config.js, tailwind.config.js)

## Documentation & Cleanup
- Update vendor-in-token-architecture.md with completed migration status
- Update architecture-rules.md with new rules
- Move migration docs to docs/development/migration/
- Remove duplicate/obsolete documentation files
- Merge pytest.ini settings into pyproject.toml

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
2025-12-04 22:24:45 +01:00
parent 76f8a59954
commit 8a367077e1
85 changed files with 21787 additions and 134978 deletions

View File

@@ -1,6 +1,6 @@
{# app/templates/admin/base.html #}
<!DOCTYPE html>
<html :class="{ 'theme-dark': dark }" x-data="{% block alpine_data %}data(){% endblock %}" lang="en">
<html :class="{ 'dark': dark }" x-data="{% block alpine_data %}data(){% endblock %}" lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
@@ -10,11 +10,7 @@
<link href="/static/shared/fonts/inter.css" rel="stylesheet" />
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800&display=swap" rel="stylesheet" />
<!-- Tailwind CSS with CDN fallback -->
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/tailwindcss@2.2.19/dist/tailwind.min.css"
onerror="this.onerror=null; this.href='{{ url_for('static', path='shared/css/tailwind.min.css') }}';">
<!-- Admin-specific Tailwind customizations -->
<!-- Tailwind CSS v4 (built locally via standalone CLI) -->
<link rel="stylesheet" href="{{ url_for('static', path='admin/css/tailwind.output.css') }}" />
<!-- Alpine Cloak -->

View File

@@ -1,6 +1,6 @@
{# app/templates/admin/login.html #}
<!DOCTYPE html>
<html :class="{ 'theme-dark': dark }" x-data="adminLogin()" lang="en">
<html :class="{ 'dark': dark }" x-data="adminLogin()" lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />

View File

@@ -1,4 +1,5 @@
<DOCTYPE html>
{# standalone - Minimal monitoring page without admin chrome #}
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">

View File

@@ -1,519 +1,253 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Auth Flow Testing - Admin Panel</title>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
{# app/templates/admin/test-auth-flow.html #}
{% extends 'admin/base.html' %}
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, sans-serif;
padding: 20px;
background: #f5f5f5;
line-height: 1.6;
}
{% block title %}Auth Flow Testing{% endblock %}
.container {
max-width: 1200px;
margin: 0 auto;
background: white;
padding: 30px;
border-radius: 8px;
box-shadow: 0 2px 8px rgba(0,0,0,0.1);
}
h1 {
color: #333;
margin-bottom: 10px;
font-size: 28px;
}
.subtitle {
color: #666;
margin-bottom: 30px;
font-size: 14px;
}
.test-section {
margin-bottom: 30px;
padding: 20px;
background: #f9f9f9;
border-radius: 6px;
border-left: 4px solid #3b82f6;
}
.test-section h2 {
color: #333;
margin-bottom: 15px;
font-size: 20px;
}
.test-description {
color: #666;
margin-bottom: 15px;
font-size: 14px;
}
.test-steps {
background: white;
padding: 15px;
border-radius: 4px;
margin-bottom: 15px;
}
.test-steps ol {
margin-left: 20px;
}
.test-steps li {
margin-bottom: 8px;
color: #444;
}
.expected-result {
background: #e8f5e9;
padding: 12px;
border-radius: 4px;
border-left: 3px solid #4caf50;
margin-bottom: 15px;
}
.expected-result strong {
color: #2e7d32;
display: block;
margin-bottom: 5px;
}
.expected-result ul {
margin-left: 20px;
color: #555;
}
.button-group {
display: flex;
gap: 10px;
flex-wrap: wrap;
}
button {
padding: 12px 24px;
border: none;
border-radius: 6px;
font-size: 14px;
font-weight: 500;
cursor: pointer;
transition: all 0.2s;
}
button:hover {
transform: translateY(-1px);
box-shadow: 0 4px 8px rgba(0,0,0,0.15);
}
button:active {
transform: translateY(0);
}
.btn-primary {
background: #3b82f6;
color: white;
}
.btn-primary:hover {
background: #2563eb;
}
.btn-danger {
background: #ef4444;
color: white;
}
.btn-danger:hover {
background: #dc2626;
}
.btn-warning {
background: #f59e0b;
color: white;
}
.btn-warning:hover {
background: #d97706;
}
.btn-success {
background: #10b981;
color: white;
}
.btn-success:hover {
background: #059669;
}
.btn-secondary {
background: #6b7280;
color: white;
}
.btn-secondary:hover {
background: #4b5563;
}
.status-panel {
background: #1e293b;
color: #e2e8f0;
padding: 20px;
border-radius: 6px;
margin-top: 30px;
font-family: 'Courier New', monospace;
font-size: 13px;
}
.status-panel h3 {
color: #38bdf8;
margin-bottom: 15px;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
}
.status-item {
margin-bottom: 8px;
display: flex;
justify-content: space-between;
}
.status-label {
color: #94a3b8;
}
.status-value {
color: #34d399;
font-weight: 500;
}
.status-value.false {
color: #f87171;
}
.log-level-control {
background: #fef3c7;
padding: 15px;
border-radius: 6px;
margin-bottom: 30px;
border-left: 4px solid #f59e0b;
}
.log-level-control h3 {
color: #92400e;
margin-bottom: 10px;
font-size: 16px;
}
.log-level-buttons {
display: flex;
gap: 8px;
flex-wrap: wrap;
}
.log-level-buttons button {
padding: 8px 16px;
font-size: 12px;
}
.warning-box {
background: #fef2f2;
border: 1px solid #fecaca;
border-radius: 6px;
padding: 15px;
margin-top: 30px;
}
.warning-box h3 {
color: #991b1b;
margin-bottom: 10px;
font-size: 16px;
}
.warning-box ul {
margin-left: 20px;
color: #7f1d1d;
}
.warning-box li {
margin-bottom: 5px;
}
</style>
</head>
<body>
<div class="container">
<h1>🧪 Auth Flow Testing</h1>
<p class="subtitle">Comprehensive testing for the Jinja2 migration auth loop fix</p>
<!-- Log Level Control -->
<div class="log-level-control">
<h3>📊 Log Level Control</h3>
<p style="color: #78350f; font-size: 13px; margin-bottom: 10px;">
Change logging verbosity for login.js and api-client.js
{% block content %}
<div x-data="authFlowTest()" x-init="init()">
{# Page Header #}
<div class="flex flex-col md:flex-row md:items-center md:justify-between mb-8">
<div>
<h2 class="text-2xl font-semibold text-gray-700 dark:text-gray-200">
Auth Flow Testing
</h2>
<p class="text-gray-600 dark:text-gray-400 mt-1">
Comprehensive testing for Jinja2 migration auth loop fix
</p>
<div class="log-level-buttons">
<button onclick="setLogLevel(0)" class="btn-secondary">0 - None</button>
<button onclick="setLogLevel(1)" class="btn-danger">1 - Errors Only</button>
<button onclick="setLogLevel(2)" class="btn-warning">2 - Warnings</button>
<button onclick="setLogLevel(3)" class="btn-success">3 - Info (Production)</button>
<button onclick="setLogLevel(4)" class="btn-primary">4 - Debug (Full)</button>
</div>
<p style="color: #78350f; font-size: 12px; margin-top: 10px; font-style: italic;">
Current levels: LOGIN = <span id="currentLoginLevel">4</span>, API = <span id="currentApiLevel">3</span>
</p>
</div>
<!-- Test 1: Clean Slate -->
<div class="test-section">
<h2>Test 1: Clean Slate - Fresh Login Flow</h2>
<p class="test-description">
Tests the complete login flow from scratch with no existing tokens.
</p>
<div class="test-steps">
<strong>Steps:</strong>
<ol>
<li>Click "Clear All Data" below</li>
<li>Click "Navigate to /admin"</li>
<li>Observe browser behavior and console logs</li>
<li>You should land on login page</li>
</ol>
</div>
<div class="expected-result">
<strong>✅ Expected Result:</strong>
<ul>
<li>Single redirect: /admin → /admin/login</li>
<li>Login page loads with NO API calls to /admin/auth/me</li>
<li>No loops, no errors in console</li>
<li>Form is ready for input</li>
</ul>
</div>
<div class="button-group">
<button onclick="clearAllData()" class="btn-danger">Clear All Data</button>
<button onclick="navigateToAdmin()" class="btn-primary">Navigate to /admin</button>
<button onclick="navigateToLogin()" class="btn-secondary">Go to Login</button>
</div>
</div>
<!-- Test 2: Login Success -->
<div class="test-section">
<h2>Test 2: Successful Login</h2>
<p class="test-description">
Tests that login works correctly and redirects to dashboard.
</p>
<div class="test-steps">
<strong>Steps:</strong>
<ol>
<li>Ensure you're on /admin/login</li>
<li>Enter valid admin credentials</li>
<li>Click "Login"</li>
<li>Observe redirect and dashboard load</li>
</ol>
</div>
<div class="expected-result">
<strong>✅ Expected Result:</strong>
<ul>
<li>Login API call succeeds (check Network tab)</li>
<li>Token stored in localStorage</li>
<li>Success message shows briefly</li>
<li>Redirect to /admin/dashboard after 500ms</li>
<li>Dashboard loads with stats and recent vendors</li>
</ul>
</div>
<div class="button-group">
<button onclick="navigateToLogin()" class="btn-primary">Go to Login Page</button>
<button onclick="checkAuthStatus()" class="btn-secondary">Check Auth Status</button>
</div>
</div>
<!-- Test 3: Dashboard Refresh -->
<div class="test-section">
<h2>Test 3: Dashboard Refresh (Authenticated)</h2>
<p class="test-description">
Tests that refreshing the dashboard works without redirect loops.
</p>
<div class="test-steps">
<strong>Steps:</strong>
<ol>
<li>Complete Test 2 (login successfully)</li>
<li>On dashboard, press F5 or click "Refresh Page"</li>
<li>Observe page reload behavior</li>
</ol>
</div>
<div class="expected-result">
<strong>✅ Expected Result:</strong>
<ul>
<li>Dashboard reloads normally</li>
<li>No redirects to login</li>
<li>Stats and vendors load correctly</li>
<li>No console errors</li>
</ul>
</div>
<div class="button-group">
<button onclick="navigateToDashboard()" class="btn-primary">Go to Dashboard</button>
<button onclick="window.location.reload()" class="btn-secondary">Refresh Page</button>
</div>
</div>
<!-- Test 4: Expired Token -->
<div class="test-section">
<h2>Test 4: Expired Token Handling</h2>
<p class="test-description">
Tests that expired tokens are handled gracefully with redirect to login.
</p>
<div class="test-steps">
<strong>Steps:</strong>
<ol>
<li>Click "Set Expired Token"</li>
<li>Click "Navigate to Dashboard"</li>
<li>Observe authentication failure and redirect</li>
</ol>
</div>
<div class="expected-result">
<strong>✅ Expected Result:</strong>
<ul>
<li>Server detects expired token</li>
<li>Returns 401 Unauthorized</li>
<li>Browser redirects to /admin/login</li>
<li>Token is cleared from localStorage</li>
<li>No infinite loops</li>
</ul>
</div>
<div class="button-group">
<button onclick="setExpiredToken()" class="btn-warning">Set Expired Token</button>
<button onclick="navigateToDashboard()" class="btn-primary">Navigate to Dashboard</button>
</div>
</div>
<!-- Test 5: Direct Dashboard Access (No Token) -->
<div class="test-section">
<h2>Test 5: Direct Dashboard Access (Unauthenticated)</h2>
<p class="test-description">
Tests that accessing dashboard without token redirects to login.
</p>
<div class="test-steps">
<strong>Steps:</strong>
<ol>
<li>Click "Clear All Data"</li>
<li>Click "Navigate to Dashboard"</li>
<li>Observe immediate redirect to login</li>
</ol>
</div>
<div class="expected-result">
<strong>✅ Expected Result:</strong>
<ul>
<li>Redirect from /admin/dashboard to /admin/login</li>
<li>No API calls attempted</li>
<li>Login page loads correctly</li>
</ul>
</div>
<div class="button-group">
<button onclick="clearAllData()" class="btn-danger">Clear All Data</button>
<button onclick="navigateToDashboard()" class="btn-primary">Navigate to Dashboard</button>
</div>
</div>
<!-- Test 6: Login Page with Valid Token -->
<div class="test-section">
<h2>Test 6: Login Page with Valid Token</h2>
<p class="test-description">
Tests what happens when user visits login page while already authenticated.
</p>
<div class="test-steps">
<strong>Steps:</strong>
<ol>
<li>Login successfully (Test 2)</li>
<li>Click "Go to Login Page" below</li>
<li>Observe behavior</li>
</ol>
</div>
<div class="expected-result">
<strong>✅ Expected Result:</strong>
<ul>
<li>Login page loads</li>
<li>Existing token is cleared (init() clears it)</li>
<li>Form is displayed normally</li>
<li>NO redirect loops</li>
<li>NO API calls to validate token</li>
</ul>
</div>
<div class="button-group">
<button onclick="setValidToken()" class="btn-success">Set Valid Token (Mock)</button>
<button onclick="navigateToLogin()" class="btn-primary">Go to Login Page</button>
</div>
</div>
<!-- Status Panel -->
<div class="status-panel">
<h3>🔍 Current Auth Status</h3>
<div id="statusDisplay">
<div class="status-item">
<span class="status-label">Current URL:</span>
<span class="status-value" id="currentUrl">-</span>
</div>
<div class="status-item">
<span class="status-label">Has admin_token:</span>
<span class="status-value" id="hasToken">-</span>
</div>
<div class="status-item">
<span class="status-label">Has admin_user:</span>
<span class="status-value" id="hasUser">-</span>
</div>
<div class="status-item">
<span class="status-label">Token Preview:</span>
<span class="status-value" id="tokenPreview">-</span>
</div>
<div class="status-item">
<span class="status-label">Username:</span>
<span class="status-value" id="username">-</span>
</div>
</div>
<button onclick="updateStatus()" style="margin-top: 15px; background: #38bdf8; color: #0f172a; padding: 8px 16px; border-radius: 4px; font-size: 12px; cursor: pointer; border: none;">
🔄 Refresh Status
</button>
</div>
<!-- Warning Box -->
<div class="warning-box">
<h3>⚠️ Important Notes</h3>
<ul>
<li>Always check browser console for detailed logs</li>
<li>Use Network tab to see actual HTTP requests and redirects</li>
<li>Clear browser cache if you see unexpected behavior</li>
<li>Make sure FastAPI server is running on localhost:8000</li>
<li>Valid admin credentials required for login tests</li>
</ul>
</div>
</div>
<script>
// Update status display
function updateStatus() {
{# Log Level Control #}
<div class="px-4 py-3 mb-6 bg-yellow-50 dark:bg-yellow-900/20 rounded-lg shadow-md border-l-4 border-yellow-500">
<h4 class="mb-2 text-lg font-semibold text-yellow-800 dark:text-yellow-200">Log Level Control</h4>
<p class="text-sm text-yellow-700 dark:text-yellow-300 mb-3">
Change logging verbosity for login.js and api-client.js
</p>
<div class="flex flex-wrap gap-2">
<button @click="setLogLevel(0)" class="px-3 py-1 text-xs font-medium text-white bg-gray-600 rounded hover:bg-gray-700">0 - None</button>
<button @click="setLogLevel(1)" class="px-3 py-1 text-xs font-medium text-white bg-red-600 rounded hover:bg-red-700">1 - Errors</button>
<button @click="setLogLevel(2)" class="px-3 py-1 text-xs font-medium text-white bg-yellow-600 rounded hover:bg-yellow-700">2 - Warnings</button>
<button @click="setLogLevel(3)" class="px-3 py-1 text-xs font-medium text-white bg-green-600 rounded hover:bg-green-700">3 - Info</button>
<button @click="setLogLevel(4)" class="px-3 py-1 text-xs font-medium text-white bg-blue-600 rounded hover:bg-blue-700">4 - Debug</button>
</div>
<p class="text-xs text-yellow-600 dark:text-yellow-400 mt-2 italic">
Current: LOGIN = <span x-text="currentLoginLevel">4</span>, API = <span x-text="currentApiLevel">3</span>
</p>
</div>
{# Test Sections Grid #}
<div class="grid gap-6 mb-8 md:grid-cols-2">
{# Test 1: Clean Slate #}
<div class="px-4 py-3 bg-white rounded-lg shadow-md dark:bg-gray-800 border-l-4 border-blue-500">
<h4 class="mb-2 text-lg font-semibold text-gray-600 dark:text-gray-300">
Test 1: Clean Slate - Fresh Login
</h4>
<p class="text-sm text-gray-600 dark:text-gray-400 mb-3">
Tests complete login flow from scratch with no existing tokens.
</p>
<div class="p-3 mb-3 bg-gray-50 dark:bg-gray-700 rounded text-sm">
<ol class="list-decimal list-inside text-gray-700 dark:text-gray-300 space-y-1">
<li>Clear All Data</li>
<li>Navigate to /admin</li>
<li>Should land on login page</li>
</ol>
</div>
<div class="p-2 mb-3 bg-green-50 dark:bg-green-900/20 rounded border-l-3 border-green-500">
<p class="text-xs text-green-700 dark:text-green-400">Expected: Single redirect /admin -> /admin/login, no loops</p>
</div>
<div class="flex flex-wrap gap-2">
<button @click="clearAllData()" class="px-3 py-1 text-xs font-medium text-white bg-red-600 rounded hover:bg-red-700">Clear All Data</button>
<button @click="navigateTo('/admin')" class="px-3 py-1 text-xs font-medium text-white bg-blue-600 rounded hover:bg-blue-700">Go to /admin</button>
</div>
</div>
{# Test 2: Successful Login #}
<div class="px-4 py-3 bg-white rounded-lg shadow-md dark:bg-gray-800 border-l-4 border-green-500">
<h4 class="mb-2 text-lg font-semibold text-gray-600 dark:text-gray-300">
Test 2: Successful Login
</h4>
<p class="text-sm text-gray-600 dark:text-gray-400 mb-3">
Tests that login works correctly and redirects to dashboard.
</p>
<div class="p-3 mb-3 bg-gray-50 dark:bg-gray-700 rounded text-sm">
<ol class="list-decimal list-inside text-gray-700 dark:text-gray-300 space-y-1">
<li>Go to /admin/login</li>
<li>Enter valid admin credentials</li>
<li>Click Login</li>
</ol>
</div>
<div class="p-2 mb-3 bg-green-50 dark:bg-green-900/20 rounded border-l-3 border-green-500">
<p class="text-xs text-green-700 dark:text-green-400">Expected: Token stored, redirect to /admin/dashboard</p>
</div>
<div class="flex flex-wrap gap-2">
<button @click="navigateTo('/admin/login')" class="px-3 py-1 text-xs font-medium text-white bg-blue-600 rounded hover:bg-blue-700">Go to Login</button>
<button @click="checkAuthStatus()" class="px-3 py-1 text-xs font-medium text-white bg-gray-600 rounded hover:bg-gray-700">Check Status</button>
</div>
</div>
{# Test 3: Dashboard Refresh #}
<div class="px-4 py-3 bg-white rounded-lg shadow-md dark:bg-gray-800 border-l-4 border-purple-500">
<h4 class="mb-2 text-lg font-semibold text-gray-600 dark:text-gray-300">
Test 3: Dashboard Refresh
</h4>
<p class="text-sm text-gray-600 dark:text-gray-400 mb-3">
Tests that refreshing dashboard works without redirect loops.
</p>
<div class="p-3 mb-3 bg-gray-50 dark:bg-gray-700 rounded text-sm">
<ol class="list-decimal list-inside text-gray-700 dark:text-gray-300 space-y-1">
<li>Complete Test 2 (login)</li>
<li>Press F5 or click Refresh</li>
<li>Dashboard should reload normally</li>
</ol>
</div>
<div class="p-2 mb-3 bg-green-50 dark:bg-green-900/20 rounded border-l-3 border-green-500">
<p class="text-xs text-green-700 dark:text-green-400">Expected: No redirect to login, stats load correctly</p>
</div>
<div class="flex flex-wrap gap-2">
<button @click="navigateTo('/admin/dashboard')" class="px-3 py-1 text-xs font-medium text-white bg-blue-600 rounded hover:bg-blue-700">Go to Dashboard</button>
<button @click="window.location.reload()" class="px-3 py-1 text-xs font-medium text-white bg-gray-600 rounded hover:bg-gray-700">Refresh Page</button>
</div>
</div>
{# Test 4: Expired Token #}
<div class="px-4 py-3 bg-white rounded-lg shadow-md dark:bg-gray-800 border-l-4 border-orange-500">
<h4 class="mb-2 text-lg font-semibold text-gray-600 dark:text-gray-300">
Test 4: Expired Token Handling
</h4>
<p class="text-sm text-gray-600 dark:text-gray-400 mb-3">
Tests that expired tokens are handled gracefully.
</p>
<div class="p-3 mb-3 bg-gray-50 dark:bg-gray-700 rounded text-sm">
<ol class="list-decimal list-inside text-gray-700 dark:text-gray-300 space-y-1">
<li>Set Expired Token</li>
<li>Navigate to Dashboard</li>
<li>Should redirect to login</li>
</ol>
</div>
<div class="p-2 mb-3 bg-green-50 dark:bg-green-900/20 rounded border-l-3 border-green-500">
<p class="text-xs text-green-700 dark:text-green-400">Expected: 401 response, redirect to login, no loops</p>
</div>
<div class="flex flex-wrap gap-2">
<button @click="setExpiredToken()" class="px-3 py-1 text-xs font-medium text-white bg-orange-600 rounded hover:bg-orange-700">Set Expired Token</button>
<button @click="navigateTo('/admin/dashboard')" class="px-3 py-1 text-xs font-medium text-white bg-blue-600 rounded hover:bg-blue-700">Go to Dashboard</button>
</div>
</div>
{# Test 5: Direct Access (No Token) #}
<div class="px-4 py-3 bg-white rounded-lg shadow-md dark:bg-gray-800 border-l-4 border-red-500">
<h4 class="mb-2 text-lg font-semibold text-gray-600 dark:text-gray-300">
Test 5: Direct Access (Unauthenticated)
</h4>
<p class="text-sm text-gray-600 dark:text-gray-400 mb-3">
Tests accessing dashboard without token redirects to login.
</p>
<div class="p-3 mb-3 bg-gray-50 dark:bg-gray-700 rounded text-sm">
<ol class="list-decimal list-inside text-gray-700 dark:text-gray-300 space-y-1">
<li>Clear All Data</li>
<li>Navigate to Dashboard</li>
<li>Should redirect to login</li>
</ol>
</div>
<div class="p-2 mb-3 bg-green-50 dark:bg-green-900/20 rounded border-l-3 border-green-500">
<p class="text-xs text-green-700 dark:text-green-400">Expected: Redirect to /admin/login, no API calls</p>
</div>
<div class="flex flex-wrap gap-2">
<button @click="clearAllData()" class="px-3 py-1 text-xs font-medium text-white bg-red-600 rounded hover:bg-red-700">Clear All Data</button>
<button @click="navigateTo('/admin/dashboard')" class="px-3 py-1 text-xs font-medium text-white bg-blue-600 rounded hover:bg-blue-700">Go to Dashboard</button>
</div>
</div>
{# Test 6: Login with Valid Token #}
<div class="px-4 py-3 bg-white rounded-lg shadow-md dark:bg-gray-800 border-l-4 border-teal-500">
<h4 class="mb-2 text-lg font-semibold text-gray-600 dark:text-gray-300">
Test 6: Login Page with Valid Token
</h4>
<p class="text-sm text-gray-600 dark:text-gray-400 mb-3">
Tests visiting login page while already authenticated.
</p>
<div class="p-3 mb-3 bg-gray-50 dark:bg-gray-700 rounded text-sm">
<ol class="list-decimal list-inside text-gray-700 dark:text-gray-300 space-y-1">
<li>Login successfully (Test 2)</li>
<li>Click Go to Login Page</li>
<li>Token should be cleared</li>
</ol>
</div>
<div class="p-2 mb-3 bg-green-50 dark:bg-green-900/20 rounded border-l-3 border-green-500">
<p class="text-xs text-green-700 dark:text-green-400">Expected: Token cleared, form displayed, no loops</p>
</div>
<div class="flex flex-wrap gap-2">
<button @click="setMockToken()" class="px-3 py-1 text-xs font-medium text-white bg-green-600 rounded hover:bg-green-700">Set Mock Token</button>
<button @click="navigateTo('/admin/login')" class="px-3 py-1 text-xs font-medium text-white bg-blue-600 rounded hover:bg-blue-700">Go to Login</button>
</div>
</div>
</div>
{# Status Panel #}
<div class="px-4 py-3 bg-gray-800 rounded-lg shadow-md">
<div class="flex items-center justify-between mb-3">
<h4 class="text-lg font-semibold text-gray-200">Current Auth Status</h4>
<button @click="updateStatus()" class="px-3 py-1 text-xs text-gray-400 border border-gray-600 rounded hover:bg-gray-700">Refresh</button>
</div>
<div class="font-mono text-sm space-y-2">
<div class="flex justify-between">
<span class="text-gray-500">Current URL:</span>
<span class="text-blue-400" x-text="currentUrl">-</span>
</div>
<div class="flex justify-between">
<span class="text-gray-500">Has admin_token:</span>
<span :class="hasToken ? 'text-green-400' : 'text-red-400'" x-text="hasToken ? 'Yes' : 'No'">-</span>
</div>
<div class="flex justify-between">
<span class="text-gray-500">Has admin_user:</span>
<span :class="hasUser ? 'text-green-400' : 'text-red-400'" x-text="hasUser ? 'Yes' : 'No'">-</span>
</div>
<div class="flex justify-between">
<span class="text-gray-500">Token Preview:</span>
<span class="text-green-400 truncate max-w-xs" x-text="tokenPreview">-</span>
</div>
<div class="flex justify-between">
<span class="text-gray-500">Username:</span>
<span class="text-green-400" x-text="username">-</span>
</div>
</div>
</div>
{# Warning Box #}
<div class="mt-6 px-4 py-3 bg-red-50 dark:bg-red-900/20 rounded-lg border border-red-200 dark:border-red-800">
<h4 class="text-lg font-semibold text-red-700 dark:text-red-300 mb-2">Important Notes</h4>
<ul class="list-disc list-inside text-sm text-red-600 dark:text-red-400 space-y-1">
<li>Always check browser console for detailed logs</li>
<li>Use Network tab to see actual HTTP requests and redirects</li>
<li>Clear browser cache if you see unexpected behavior</li>
<li>Make sure FastAPI server is running on localhost:8000</li>
<li>Valid admin credentials required for login tests</li>
</ul>
</div>
</div>
{% endblock %}
{% block scripts %}
<script>
function authFlowTest() {
return {
...data(),
currentPage: 'auth-testing',
currentUrl: '-',
hasToken: false,
hasUser: false,
tokenPreview: '-',
username: '-',
currentLoginLevel: 4,
currentApiLevel: 3,
init() {
this.updateStatus();
setInterval(() => this.updateStatus(), 2000);
console.log('Auth Flow Testing Script Loaded');
},
updateStatus() {
const token = localStorage.getItem('admin_token');
const userStr = localStorage.getItem('admin_user');
let user = null;
@@ -524,121 +258,67 @@
console.error('Failed to parse user data:', e);
}
document.getElementById('currentUrl').textContent = window.location.href;
this.currentUrl = window.location.href;
this.hasToken = !!token;
this.hasUser = !!user;
this.tokenPreview = token ? token.substring(0, 30) + '...' : 'No token';
this.username = user?.username || 'Not logged in';
},
const hasTokenEl = document.getElementById('hasToken');
hasTokenEl.textContent = token ? 'Yes' : 'No';
hasTokenEl.className = token ? 'status-value' : 'status-value false';
const hasUserEl = document.getElementById('hasUser');
hasUserEl.textContent = user ? 'Yes' : 'No';
hasUserEl.className = user ? 'status-value' : 'status-value false';
document.getElementById('tokenPreview').textContent = token
? token.substring(0, 30) + '...'
: 'No token';
document.getElementById('username').textContent = user?.username || 'Not logged in';
console.log('📊 Status Updated:', {
hasToken: !!token,
hasUser: !!user,
user: user
});
}
// Test functions
function clearAllData() {
console.log('🗑️ Clearing all localStorage data...');
clearAllData() {
console.log('Clearing all localStorage data...');
localStorage.clear();
console.log('All data cleared');
alert('All localStorage data cleared!\n\nCheck console for details.');
updateStatus();
}
console.log('All data cleared');
alert('All localStorage data cleared!');
this.updateStatus();
},
function navigateToAdmin() {
console.log('🚀 Navigating to /admin...');
window.location.href = '/admin';
}
navigateTo(path) {
console.log(`Navigating to ${path}...`);
window.location.href = path;
},
function navigateToLogin() {
console.log('🚀 Navigating to /admin/login...');
window.location.href = '/admin/login';
}
function navigateToDashboard() {
console.log('🚀 Navigating to /admin/dashboard...');
window.location.href = '/admin/dashboard';
}
function checkAuthStatus() {
updateStatus();
checkAuthStatus() {
this.updateStatus();
alert('Check console and status panel for auth details.');
}
},
function setExpiredToken() {
const expiredToken = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyLCJleHAiOjE1MTYyMzkwMjJ9.invalidexpiredtoken';
console.log('⚠️ Setting expired/invalid token...');
setExpiredToken() {
const expiredToken = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxIiwiZXhwIjoxNTE2MjM5MDIyfQ.invalid';
localStorage.setItem('admin_token', expiredToken);
localStorage.setItem('admin_user', JSON.stringify({
id: 1,
username: 'test_expired',
role: 'admin'
}));
console.log('Expired token set');
alert('⚠️ Expired token set!\n\nNow try navigating to dashboard.');
updateStatus();
}
alert('Expired token set! Now try navigating to dashboard.');
this.updateStatus();
},
function setValidToken() {
// This is a mock token - won't actually work with backend
setMockToken() {
const mockToken = 'mock_valid_token_' + Date.now();
console.log('✅ Setting mock valid token...');
localStorage.setItem('admin_token', mockToken);
localStorage.setItem('admin_user', JSON.stringify({
id: 1,
username: 'test_user',
role: 'admin'
}));
console.log('Mock token set (will not work with real backend)');
alert('✅ Mock token set!\n\nNote: This is a fake token and won\'t work with the real backend.');
updateStatus();
}
alert('Mock token set! Note: This won\'t work with real backend.');
this.updateStatus();
},
// Log level control
function setLogLevel(level) {
console.log(`📊 Setting log level to ${level}...`);
// Note: This only works if login.js and api-client.js are loaded
// In production, you'd need to reload the page or use a more sophisticated approach
if (typeof LOG_LEVEL !== 'undefined') {
setLogLevel(level) {
if (typeof window.LOG_LEVEL !== 'undefined') {
window.LOG_LEVEL = level;
document.getElementById('currentLoginLevel').textContent = level;
console.log('✅ LOGIN log level set to', level);
} else {
console.warn('⚠️ LOG_LEVEL not found (login.js not loaded)');
this.currentLoginLevel = level;
}
if (typeof API_LOG_LEVEL !== 'undefined') {
if (typeof window.API_LOG_LEVEL !== 'undefined') {
window.API_LOG_LEVEL = level;
document.getElementById('currentApiLevel').textContent = level;
console.log('✅ API log level set to', level);
} else {
console.warn('⚠️ API_LOG_LEVEL not found (api-client.js not loaded)');
this.currentApiLevel = level;
}
alert(`Log level set to ${level}\n\n0 = None\n1 = Errors\n2 = Warnings\n3 = Info\n4 = Debug\n\nNote: Changes apply to current page. Reload to apply to all scripts.`);
alert(`Log level set to ${level}. Reload to apply to all scripts.`);
}
// Initialize status on load
updateStatus();
// Auto-refresh status every 2 seconds
setInterval(updateStatus, 2000);
console.log('🧪 Auth Flow Testing Script Loaded');
console.log('📊 Use the buttons above to run tests');
console.log('🔍 Watch browser console and Network tab for details');
</script>
</body>
</html>
};
}
</script>
{% endblock %}

File diff suppressed because it is too large Load Diff

View File

@@ -41,51 +41,8 @@
<link href="/static/shared/fonts/inter.css" rel="stylesheet" />
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800&display=swap" rel="stylesheet" />
{# Tailwind CSS with local fallback #}
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/tailwindcss@2.2.19/dist/tailwind.min.css"
onerror="this.onerror=null; this.href='{{ url_for('static', path='shared/css/tailwind.min.css') }}';">
{# Platform-specific styles #}
<style>
/* Smooth scrolling */
html {
scroll-behavior: smooth;
}
/* Custom gradients */
.gradient-primary {
background: linear-gradient(135deg, var(--color-primary) 0%, var(--color-secondary) 100%);
}
.gradient-accent {
background: linear-gradient(135deg, var(--color-secondary) 0%, var(--color-accent) 100%);
}
/* Button styles */
.btn-primary {
background-color: var(--color-primary);
color: white;
padding: 0.75rem 1.5rem;
border-radius: 0.5rem;
font-weight: 600;
transition: all 0.2s;
}
.btn-primary:hover {
opacity: 0.9;
transform: translateY(-1px);
}
/* Card hover effect */
.card-hover {
transition: all 0.3s ease;
}
.card-hover:hover {
transform: translateY(-4px);
box-shadow: 0 20px 25px -5px rgba(0, 0, 0, 0.1), 0 10px 10px -5px rgba(0, 0, 0, 0.04);
}
</style>
{# Tailwind CSS v4 (built locally via standalone CLI) #}
<link rel="stylesheet" href="{{ url_for('static', path='platform/css/tailwind.output.css') }}">
{% block extra_head %}{% endblock %}
</head>

View File

@@ -1,28 +0,0 @@
{# app/templates/shared/cdn-fallback.html #}
{# CDN with Local Fallback Pattern #}
{# This partial handles loading CDN resources with automatic fallback to local copies #}
{# Tailwind CSS with fallback #}
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/tailwindcss@2.2.19/dist/tailwind.min.css"
onerror="this.onerror=null; this.href='{{ url_for('static', path='shared/css/tailwind.min.css') }}';">
{# Alpine.js with fallback - must be loaded at the end of body #}
{# Usage: Include this partial at the bottom of your template, before page-specific scripts #}
<script>
// Alpine.js CDN with fallback
(function() {
var script = document.createElement('script');
script.defer = true;
script.src = 'https://cdn.jsdelivr.net/npm/alpinejs@3.13.3/dist/cdn.min.js';
script.onerror = function() {
console.warn('Alpine.js CDN failed, loading local copy...');
var fallbackScript = document.createElement('script');
fallbackScript.defer = true;
fallbackScript.src = '{{ url_for("static", path="shared/js/vendor/alpine.min.js") }}';
document.head.appendChild(fallbackScript);
};
document.head.appendChild(script);
})();
</script>

View File

@@ -1,11 +1,15 @@
<DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Address management</title>
</head>
<body>
<-- Address management -->
</body>
</html>
{# app/templates/shop/account/addresses.html #}
{% extends "shop/base.html" %}
{% block title %}My Addresses{% endblock %}
{% block content %}
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
<h1 class="text-2xl font-bold text-gray-900 dark:text-white mb-8">My Addresses</h1>
{# TODO: Implement address management #}
<div class="bg-white dark:bg-gray-800 rounded-lg shadow p-6">
<p class="text-gray-600 dark:text-gray-400">Address management coming soon...</p>
</div>
</div>
{% endblock %}

View File

@@ -1,6 +1,6 @@
{# app/templates/shop/account/forgot-password.html #}
<!DOCTYPE html>
<html :class="{ 'theme-dark': dark }" x-data="forgotPassword()" lang="en">
<html :class="{ 'dark': dark }" x-data="forgotPassword()" lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
@@ -37,9 +37,8 @@
[x-cloak] { display: none !important; }
</style>
{# Tailwind CSS with local fallback #}
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/tailwindcss@2.2.19/dist/tailwind.min.css"
onerror="this.onerror=null; this.href='{{ url_for('static', path='shared/css/tailwind.min.css') }}';">
{# Tailwind CSS v4 (built locally via standalone CLI) #}
<link rel="stylesheet" href="{{ url_for('static', path='shop/css/tailwind.output.css') }}">
</head>
<body>
<div class="flex items-center min-h-screen p-6 bg-gray-50 dark:bg-gray-900" x-cloak>

View File

@@ -1,6 +1,6 @@
{# app/templates/shop/account/login.html #}
<!DOCTYPE html>
<html :class="{ 'theme-dark': dark }" x-data="customerLogin()" lang="en">
<html :class="{ 'dark': dark }" x-data="customerLogin()" lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
@@ -37,9 +37,8 @@
[x-cloak] { display: none !important; }
</style>
{# Tailwind CSS with local fallback #}
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/tailwindcss@2.2.19/dist/tailwind.min.css"
onerror="this.onerror=null; this.href='{{ url_for('static', path='shared/css/tailwind.min.css') }}';">
{# Tailwind CSS v4 (built locally via standalone CLI) #}
<link rel="stylesheet" href="{{ url_for('static', path='shop/css/tailwind.output.css') }}">
</head>
<body>
<div class="flex items-center min-h-screen p-6 bg-gray-50 dark:bg-gray-900" x-cloak>

View File

@@ -1,11 +1,15 @@
<DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Order history</title>
</head>
<body>
<-- Order history -->
</body>
</html>
{# app/templates/shop/account/orders.html #}
{% extends "shop/base.html" %}
{% block title %}Order History{% endblock %}
{% block content %}
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
<h1 class="text-2xl font-bold text-gray-900 dark:text-white mb-8">Order History</h1>
{# TODO: Implement order history #}
<div class="bg-white dark:bg-gray-800 rounded-lg shadow p-6">
<p class="text-gray-600 dark:text-gray-400">Order history coming soon...</p>
</div>
</div>
{% endblock %}

View File

@@ -1,11 +1,15 @@
<DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Customer profile</title>
</head>
<body>
<-- Customer profile -->
</body>
</html>
{# app/templates/shop/account/profile.html #}
{% extends "shop/base.html" %}
{% block title %}My Profile{% endblock %}
{% block content %}
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
<h1 class="text-2xl font-bold text-gray-900 dark:text-white mb-8">My Profile</h1>
{# TODO: Implement profile management #}
<div class="bg-white dark:bg-gray-800 rounded-lg shadow p-6">
<p class="text-gray-600 dark:text-gray-400">Profile management coming soon...</p>
</div>
</div>
{% endblock %}

View File

@@ -1,6 +1,6 @@
{# app/templates/shop/account/register.html #}
<!DOCTYPE html>
<html :class="{ 'theme-dark': dark }" x-data="customerRegistration()" lang="en">
<html :class="{ 'dark': dark }" x-data="customerRegistration()" lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
@@ -37,9 +37,8 @@
[x-cloak] { display: none !important; }
</style>
{# Tailwind CSS with local fallback #}
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/tailwindcss@2.2.19/dist/tailwind.min.css"
onerror="this.onerror=null; this.href='{{ url_for('static', path='shared/css/tailwind.min.css') }}';">
{# Tailwind CSS v4 (built locally via standalone CLI) #}
<link rel="stylesheet" href="{{ url_for('static', path='shop/css/tailwind.output.css') }}">
</head>
<body>
<div class="flex items-center min-h-screen p-6 bg-gray-50 dark:bg-gray-900" x-cloak>

View File

@@ -37,9 +37,8 @@
{% endif %}
</style>
{# Tailwind CSS with local fallback #}
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/tailwindcss@2.2.19/dist/tailwind.min.css"
onerror="this.onerror=null; this.href='{{ url_for('static', path='shared/css/tailwind.min.css') }}';">
{# Tailwind CSS v4 (built locally via standalone CLI) #}
<link rel="stylesheet" href="{{ url_for('static', path='shop/css/tailwind.output.css') }}">
{# Base Shop Styles #}
<link rel="stylesheet" href="{{ url_for('static', path='shop/css/shop.css') }}">

View File

@@ -1,11 +1,15 @@
<DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Checkout process</title>
</head>
<body>
<-- Checkout process -->
</body>
</html>
{# app/templates/shop/checkout.html #}
{% extends "shop/base.html" %}
{% block title %}Checkout{% endblock %}
{% block content %}
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
<h1 class="text-2xl font-bold text-gray-900 dark:text-white mb-8">Checkout</h1>
{# TODO: Implement checkout process #}
<div class="bg-white dark:bg-gray-800 rounded-lg shadow p-6">
<p class="text-gray-600 dark:text-gray-400">Checkout process coming soon...</p>
</div>
</div>
{% endblock %}

View File

@@ -1,195 +1,108 @@
{# app/templates/shop/errors/base.html #}
{# Error page base template using Tailwind CSS with vendor theme support #}
<!DOCTYPE html>
<html lang="en">
<html lang="en" class="h-full">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>{% block title %}{{ status_code }} - {{ status_name }}{% endblock %}{% if vendor %} | {{ vendor.name }}{% endif %}</title>
{# Tailwind CSS #}
<link rel="stylesheet" href="{{ url_for('static', path='shop/css/tailwind.output.css') }}">
{# Vendor theme colors via CSS variables #}
<style>
:root {
/* Default theme colors (fallback) */
--color-primary: {{ theme.colors.primary if theme and theme.colors else '#6366f1' }};
--color-secondary: {{ theme.colors.secondary if theme and theme.colors else '#8b5cf6' }};
--color-accent: {{ theme.colors.accent if theme and theme.colors else '#ec4899' }};
--color-background: {{ theme.colors.background if theme and theme.colors else '#ffffff' }};
--color-text: {{ theme.colors.text if theme and theme.colors else '#1f2937' }};
--color-border: {{ theme.colors.border if theme and theme.colors else '#e5e7eb' }};
--font-heading: {{ theme.fonts.heading if theme and theme.fonts else "'Inter', sans-serif" }};
--font-body: {{ theme.fonts.body if theme and theme.fonts else "'Inter', sans-serif" }};
}
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: var(--font-body);
.bg-gradient-theme {
background: linear-gradient(135deg, var(--color-primary) 0%, var(--color-secondary) 100%);
min-height: 100vh;
display: flex;
align-items: center;
justify-content: center;
color: var(--color-text);
padding: 2rem;
}
.error-container {
background: var(--color-background);
border-radius: 1.5rem;
box-shadow: 0 25px 50px -12px rgba(0, 0, 0, 0.25);
max-width: 600px;
width: 100%;
padding: 3rem;
text-align: center;
}
{% if vendor and vendor.logo %}
.vendor-logo {
max-width: 150px;
max-height: 60px;
margin-bottom: 2rem;
}
{% endif %}
.error-icon {
font-size: 5rem;
margin-bottom: 1rem;
}
.status-code {
font-size: 6rem;
font-weight: 700;
.text-theme-primary {
color: var(--color-primary);
line-height: 1;
margin-bottom: 0.5rem;
font-family: var(--font-heading);
}
.status-name {
font-size: 1.75rem;
font-weight: 600;
color: var(--color-text);
margin-bottom: 1rem;
font-family: var(--font-heading);
.bg-theme-primary {
background-color: var(--color-primary);
}
.error-message {
font-size: 1.125rem;
color: #6b7280;
margin-bottom: 2.5rem;
line-height: 1.6;
.border-theme-primary {
border-color: var(--color-primary);
}
.action-buttons {
display: flex;
gap: 1rem;
justify-content: center;
flex-wrap: wrap;
margin-top: 2rem;
}
.btn {
display: inline-flex;
align-items: center;
padding: 1rem 2rem;
border-radius: 0.75rem;
font-weight: 600;
text-decoration: none;
transition: all 0.3s ease;
border: none;
cursor: pointer;
font-size: 1rem;
}
.btn-primary {
background: var(--color-primary);
color: white;
}
.btn-primary:hover {
opacity: 0.9;
transform: translateY(-2px);
box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.1);
}
.btn-secondary {
background: transparent;
color: var(--color-primary);
border: 2px solid var(--color-primary);
}
.btn-secondary:hover {
background: var(--color-primary);
color: white;
transform: translateY(-2px);
}
.support-link {
margin-top: 2.5rem;
padding-top: 2rem;
border-top: 1px solid var(--color-border);
font-size: 0.875rem;
color: #6b7280;
}
.support-link a {
color: var(--color-primary);
text-decoration: none;
font-weight: 600;
}
.support-link a:hover {
text-decoration: underline;
}
.vendor-info {
margin-top: 2rem;
font-size: 0.875rem;
color: #9ca3af;
.hover\:bg-theme-primary:hover {
background-color: var(--color-primary);
}
{% block extra_styles %}{% endblock %}
</style>
{% if theme and theme.custom_css %}
<style>
{{ theme.custom_css | safe }}
</style>
<style>{{ theme.custom_css | safe }}</style>
{% endif %}
</head>
<body>
<div class="error-container">
<body class="h-full bg-gradient-theme flex items-center justify-center p-8">
<div class="bg-white rounded-3xl shadow-2xl max-w-xl w-full p-12 text-center">
{# Vendor Logo #}
{% if vendor and theme and theme.branding and theme.branding.logo %}
<img src="{{ theme.branding.logo }}" alt="{{ vendor.name }}" class="vendor-logo">
<img src="{{ theme.branding.logo }}"
alt="{{ vendor.name }}"
class="max-w-[150px] max-h-[60px] mx-auto mb-8 object-contain">
{% endif %}
{% block content %}
<div class="error-icon">{% block icon %}⚠️{% endblock %}</div>
<div class="status-code">{{ status_code }}</div>
<div class="status-name">{{ status_name }}</div>
<div class="error-message">{{ message }}</div>
{# Error Icon #}
<div class="text-7xl mb-4">{% block icon %}⚠️{% endblock %}</div>
<div class="action-buttons">
{# Status Code #}
<div class="text-8xl font-bold text-theme-primary leading-none mb-2">
{{ status_code }}
</div>
{# Status Name #}
<h1 class="text-3xl font-semibold text-gray-900 mb-4">
{{ status_name }}
</h1>
{# Error Message #}
<p class="text-lg text-gray-500 mb-10 leading-relaxed">
{{ message }}
</p>
{# Action Buttons #}
<div class="flex gap-4 justify-center flex-wrap mt-8">
{% block action_buttons %}
<a href="{{ base_url }}shop/" class="btn btn-primary">Continue Shopping</a>
<a href="{{ base_url }}shop/contact" class="btn btn-secondary">Contact Us</a>
<a href="{{ base_url }}shop/"
class="inline-flex items-center px-8 py-4 rounded-xl font-semibold text-white bg-theme-primary hover:opacity-90 hover:-translate-y-0.5 transition-all shadow-lg">
Continue Shopping
</a>
<a href="{{ base_url }}shop/contact"
class="inline-flex items-center px-8 py-4 rounded-xl font-semibold text-theme-primary border-2 border-theme-primary hover:bg-theme-primary hover:text-white hover:-translate-y-0.5 transition-all">
Contact Us
</a>
{% endblock %}
</div>
{% block extra_content %}{% endblock %}
<div class="support-link">
{# Support Link #}
<div class="mt-10 pt-8 border-t border-gray-200 text-sm text-gray-500">
{% block support_link %}
Need help? <a href="{{ base_url }}shop/contact">Contact our support team</a>
Need help? <a href="{{ base_url }}shop/contact" class="text-theme-primary font-semibold hover:underline">Contact our support team</a>
{% endblock %}
</div>
{# Vendor Info #}
{% if vendor %}
<div class="vendor-info">
<div class="mt-8 text-sm text-gray-400">
{{ vendor.name }}
</div>
{% endif %}
{% endblock %}
</div>
</body>
</html>
</html>

View File

@@ -1,11 +1,15 @@
<DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Search results page</title>
</head>
<body>
<-- Search results page -->
</body>
</html>
{# app/templates/shop/search.html #}
{% extends "shop/base.html" %}
{% block title %}Search Results{% endblock %}
{% block content %}
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
<h1 class="text-2xl font-bold text-gray-900 dark:text-white mb-8">Search Results</h1>
{# TODO: Implement search results #}
<div class="bg-white dark:bg-gray-800 rounded-lg shadow p-6">
<p class="text-gray-600 dark:text-gray-400">Search results coming soon...</p>
</div>
</div>
{% endblock %}

View File

@@ -1,6 +1,6 @@
{# app/templates/vendor/base.html #}
<!DOCTYPE html>
<html :class="{ 'theme-dark': dark }" x-data="{% block alpine_data %}data(){% endblock %}" lang="en">
<html :class="{ 'dark': dark }" x-data="{% block alpine_data %}data(){% endblock %}" lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
@@ -10,11 +10,7 @@
<link href="/static/shared/fonts/inter.css" rel="stylesheet" />
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800&display=swap" rel="stylesheet" />
<!-- Tailwind CSS with CDN fallback -->
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/tailwindcss@2.2.19/dist/tailwind.min.css"
onerror="this.onerror=null; this.href='{{ url_for('static', path='shared/css/tailwind.min.css') }}';">
<!-- Vendor-specific Tailwind customizations -->
<!-- Tailwind CSS v4 (built locally via standalone CLI) -->
<link rel="stylesheet" href="{{ url_for('static', path='vendor/css/tailwind.output.css') }}" />
<!-- Alpine Cloak -->

View File

@@ -1,6 +1,6 @@
{# app/templates/vendor/login.html #}
<!DOCTYPE html>
<html :class="{ 'theme-dark': dark }" x-data="vendorLogin()" lang="en">
<html :class="{ 'dark': dark }" x-data="vendorLogin()" lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />