Working state before icon/utils fixes - Oct 22
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
<!DOCTYPE html>
|
||||
<html :class="{ 'theme-dark': dark }" x-data="dashboardData()" lang="en">
|
||||
<html :class="{ 'theme-dark': dark }" x-data="adminDashboard()" lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
@@ -12,28 +12,53 @@
|
||||
</head>
|
||||
<body x-cloak>
|
||||
<div class="flex h-screen bg-gray-50 dark:bg-gray-900" :class="{ 'overflow-hidden': isSideMenuOpen }">
|
||||
<!-- Sidebar Container -->
|
||||
<!-- Sidebar Container (loaded via partial-loader) -->
|
||||
<div id="sidebar-container"></div>
|
||||
|
||||
<div class="flex flex-col flex-1 w-full">
|
||||
<!-- Header Container -->
|
||||
<!-- Header Container (loaded via partial-loader) -->
|
||||
<div id="header-container"></div>
|
||||
|
||||
<!-- Main Content -->
|
||||
<main class="h-full overflow-y-auto">
|
||||
<div class="container px-6 mx-auto grid">
|
||||
<h2 class="my-6 text-2xl font-semibold text-gray-700 dark:text-gray-200">
|
||||
Dashboard
|
||||
</h2>
|
||||
<!-- Page Header with Refresh Button -->
|
||||
<div class="flex items-center justify-between my-6">
|
||||
<h2 class="text-2xl font-semibold text-gray-700 dark:text-gray-200">
|
||||
Dashboard
|
||||
</h2>
|
||||
<button
|
||||
@click="refresh()"
|
||||
:disabled="loading"
|
||||
class="flex items-center px-4 py-2 text-sm font-medium leading-5 text-white transition-colors duration-150 bg-purple-600 border border-transparent rounded-lg hover:bg-purple-700 focus:outline-none focus:shadow-outline-purple disabled:opacity-50 disabled:cursor-not-allowed"
|
||||
>
|
||||
<span x-show="!loading" x-html="$icon('refresh', 'w-4 h-4 mr-2')"></span>
|
||||
<span x-show="loading" x-html="$icon('spinner', 'w-4 h-4 mr-2')"></span>
|
||||
<span x-text="loading ? 'Loading...' : 'Refresh'"></span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Loading State -->
|
||||
<div x-show="loading" class="text-center py-12">
|
||||
<span x-html="$icon('spinner', 'inline w-8 h-8 text-purple-600')"></span>
|
||||
<p class="mt-2 text-gray-600 dark:text-gray-400">Loading dashboard...</p>
|
||||
</div>
|
||||
|
||||
<!-- Error State -->
|
||||
<div x-show="error && !loading" class="mb-6 p-4 bg-red-100 border border-red-400 text-red-700 rounded-lg flex items-start">
|
||||
<span x-html="$icon('exclamation', 'w-5 h-5 mr-3 mt-0.5 flex-shrink-0')"></span>
|
||||
<div>
|
||||
<p class="font-semibold">Error loading dashboard</p>
|
||||
<p class="text-sm" x-text="error"></p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Stats Cards -->
|
||||
<div class="grid gap-6 mb-8 md:grid-cols-2 xl:grid-cols-4">
|
||||
<div x-show="!loading" class="grid gap-6 mb-8 md:grid-cols-2 xl:grid-cols-4">
|
||||
<!-- Card: Total Vendors -->
|
||||
<div class="flex items-center p-4 bg-white rounded-lg shadow-xs dark:bg-gray-800">
|
||||
<div class="p-3 mr-4 text-orange-500 bg-orange-100 rounded-full dark:text-orange-100 dark:bg-orange-500">
|
||||
<svg class="w-5 h-5" fill="currentColor" viewBox="0 0 20 20">
|
||||
<path d="M13 6a3 3 0 11-6 0 3 3 0 016 0zM18 8a2 2 0 11-4 0 2 2 0 014 0zM14 15a4 4 0 00-8 0v3h8v-3zM6 8a2 2 0 11-4 0 2 2 0 014 0zM16 18v-3a5.972 5.972 0 00-.75-2.906A3.005 3.005 0 0119 15v3h-3zM4.75 12.094A5.973 5.973 0 004 15v3H1v-3a3 3 0 013.75-2.906z"></path>
|
||||
</svg>
|
||||
<span x-html="$icon('user-group', 'w-5 h-5')"></span>
|
||||
</div>
|
||||
<div>
|
||||
<p class="mb-2 text-sm font-medium text-gray-600 dark:text-gray-400">
|
||||
@@ -48,9 +73,7 @@
|
||||
<!-- Card: Active Users -->
|
||||
<div class="flex items-center p-4 bg-white rounded-lg shadow-xs dark:bg-gray-800">
|
||||
<div class="p-3 mr-4 text-green-500 bg-green-100 rounded-full dark:text-green-100 dark:bg-green-500">
|
||||
<svg class="w-5 h-5" fill="currentColor" viewBox="0 0 20 20">
|
||||
<path fill-rule="evenodd" d="M4 4a2 2 0 00-2 2v4a2 2 0 002 2V6h10a2 2 0 00-2-2H4zm2 6a2 2 0 012-2h8a2 2 0 012 2v4a2 2 0 01-2 2H8a2 2 0 01-2-2v-4zm6 4a2 2 0 100-4 2 2 0 000 4z" clip-rule="evenodd"></path>
|
||||
</svg>
|
||||
<span x-html="$icon('users', 'w-5 h-5')"></span>
|
||||
</div>
|
||||
<div>
|
||||
<p class="mb-2 text-sm font-medium text-gray-600 dark:text-gray-400">
|
||||
@@ -65,9 +88,7 @@
|
||||
<!-- Card: Verified Vendors -->
|
||||
<div class="flex items-center p-4 bg-white rounded-lg shadow-xs dark:bg-gray-800">
|
||||
<div class="p-3 mr-4 text-blue-500 bg-blue-100 rounded-full dark:text-blue-100 dark:bg-blue-500">
|
||||
<svg class="w-5 h-5" fill="currentColor" viewBox="0 0 20 20">
|
||||
<path d="M3 1a1 1 0 000 2h1.22l.305 1.222a.997.997 0 00.01.042l1.358 5.43-.893.892C3.74 11.846 4.632 14 6.414 14H15a1 1 0 000-2H6.414l1-1H14a1 1 0 00.894-.553l3-6A1 1 0 0017 3H6.28l-.31-1.243A1 1 0 005 1H3zM16 16.5a1.5 1.5 0 11-3 0 1.5 1.5 0 013 0zM6.5 18a1.5 1.5 0 100-3 1.5 1.5 0 000 3z"></path>
|
||||
</svg>
|
||||
<span x-html="$icon('badge-check', 'w-5 h-5')"></span>
|
||||
</div>
|
||||
<div>
|
||||
<p class="mb-2 text-sm font-medium text-gray-600 dark:text-gray-400">
|
||||
@@ -82,9 +103,7 @@
|
||||
<!-- Card: Import Jobs -->
|
||||
<div class="flex items-center p-4 bg-white rounded-lg shadow-xs dark:bg-gray-800">
|
||||
<div class="p-3 mr-4 text-teal-500 bg-teal-100 rounded-full dark:text-teal-100 dark:bg-teal-500">
|
||||
<svg class="w-5 h-5" fill="currentColor" viewBox="0 0 20 20">
|
||||
<path fill-rule="evenodd" d="M18 5v8a2 2 0 01-2 2h-5l-5 4v-4H4a2 2 0 01-2-2V5a2 2 0 012-2h12a2 2 0 012 2zM7 8H5v2h2V8zm2 0h2v2H9V8zm6 0h-2v2h2V8z" clip-rule="evenodd"></path>
|
||||
</svg>
|
||||
<span x-html="$icon('download', 'w-5 h-5')"></span>
|
||||
</div>
|
||||
<div>
|
||||
<p class="mb-2 text-sm font-medium text-gray-600 dark:text-gray-400">
|
||||
@@ -98,7 +117,7 @@
|
||||
</div>
|
||||
|
||||
<!-- Recent Vendors Table -->
|
||||
<div class="w-full overflow-hidden rounded-lg shadow-xs">
|
||||
<div x-show="!loading" class="w-full overflow-hidden rounded-lg shadow-xs">
|
||||
<div class="w-full overflow-x-auto">
|
||||
<table class="w-full whitespace-no-wrap">
|
||||
<thead>
|
||||
@@ -110,21 +129,24 @@
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="bg-white divide-y dark:divide-gray-700 dark:bg-gray-800">
|
||||
<template x-if="vendors.length === 0">
|
||||
<template x-if="recentVendors.length === 0">
|
||||
<tr>
|
||||
<td colspan="4" class="px-4 py-3 text-sm text-center text-gray-600 dark:text-gray-400">
|
||||
No vendors yet. <a href="/static/admin/vendors.html" class="text-purple-600 hover:underline">Create your first vendor</a>
|
||||
<td colspan="4" class="px-4 py-8 text-center text-gray-600 dark:text-gray-400">
|
||||
<div class="flex flex-col items-center">
|
||||
<span x-html="$icon('user-group', 'w-12 h-12 mb-2 text-gray-300')"></span>
|
||||
<p>No vendors yet.</p>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
</template>
|
||||
|
||||
<template x-for="vendor in vendors" :key="vendor.id">
|
||||
<tr class="text-gray-700 dark:text-gray-400">
|
||||
<template x-for="vendor in recentVendors" :key="vendor.vendor_code">
|
||||
<tr class="text-gray-700 dark:text-gray-400 hover:bg-gray-50 dark:hover:bg-gray-700 transition-colors">
|
||||
<td class="px-4 py-3">
|
||||
<div class="flex items-center text-sm">
|
||||
<div class="relative hidden w-8 h-8 mr-3 rounded-full md:block">
|
||||
<div class="absolute inset-0 rounded-full bg-purple-100 dark:bg-purple-600 flex items-center justify-center">
|
||||
<span class="text-xs font-semibold text-purple-600 dark:text-purple-100" x-text="vendor.name.charAt(0)"></span>
|
||||
<span class="text-xs font-semibold text-purple-600 dark:text-purple-100" x-text="vendor.name?.charAt(0).toUpperCase() || '?'"></span>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
@@ -134,20 +156,22 @@
|
||||
</div>
|
||||
</td>
|
||||
<td class="px-4 py-3 text-xs">
|
||||
<span class="px-2 py-1 font-semibold leading-tight rounded-full"
|
||||
:class="vendor.is_verified ? 'text-green-700 bg-green-100 dark:bg-green-700 dark:text-green-100' : 'text-orange-700 bg-orange-100 dark:text-white dark:bg-orange-600'"
|
||||
x-text="vendor.is_verified ? 'Verified' : 'Pending'">
|
||||
<span class="inline-flex items-center px-2 py-1 font-semibold leading-tight rounded-full"
|
||||
:class="vendor.is_verified ? 'text-green-700 bg-green-100 dark:bg-green-700 dark:text-green-100' : 'text-orange-700 bg-orange-100 dark:text-white dark:bg-orange-600'">
|
||||
<span x-show="vendor.is_verified" x-html="$icon('badge-check', 'w-3 h-3 mr-1')"></span>
|
||||
<span x-text="vendor.is_verified ? 'Verified' : 'Pending'"></span>
|
||||
</span>
|
||||
</td>
|
||||
<td class="px-4 py-3 text-sm" x-text="formatDate(vendor.created_at)">
|
||||
</td>
|
||||
<td class="px-4 py-3">
|
||||
<div class="flex items-center space-x-4 text-sm">
|
||||
<button class="flex items-center justify-between px-2 py-2 text-sm font-medium leading-5 text-purple-600 rounded-lg dark:text-gray-400 focus:outline-none focus:shadow-outline-gray"
|
||||
aria-label="Edit">
|
||||
<svg class="w-5 h-5" aria-hidden="true" fill="currentColor" viewBox="0 0 20 20">
|
||||
<path d="M13.586 3.586a2 2 0 112.828 2.828l-.793.793-2.828-2.828.793-.793zM11.379 5.793L3 14.172V17h2.828l8.38-8.379-2.83-2.828z"></path>
|
||||
</svg>
|
||||
<div class="flex items-center space-x-2 text-sm">
|
||||
<button
|
||||
@click="viewVendor(vendor.vendor_code)"
|
||||
class="flex items-center justify-center p-2 text-purple-600 rounded-lg hover:bg-purple-50 dark:text-gray-400 dark:hover:bg-gray-700 focus:outline-none transition-colors"
|
||||
title="View vendor"
|
||||
>
|
||||
<span x-html="$icon('eye', 'w-5 h-5')"></span>
|
||||
</button>
|
||||
</div>
|
||||
</td>
|
||||
@@ -162,11 +186,15 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Load partials BEFORE Alpine -->
|
||||
<!-- Scripts in CORRECT ORDER -->
|
||||
<!-- 1. Partial Loader (auto-detects admin area) -->
|
||||
<script src="/static/shared/js/partial-loader.js"></script>
|
||||
|
||||
<!-- 2. Icons Helper -->
|
||||
<script src="/static/shared/js/icons.js"></script>
|
||||
|
||||
<!-- 3. Load Partials (before Alpine initializes) -->
|
||||
<script>
|
||||
// Load partials synchronously before Alpine starts
|
||||
(async () => {
|
||||
await window.partialLoader.loadAll({
|
||||
'header-container': 'header.html',
|
||||
@@ -175,84 +203,17 @@
|
||||
})();
|
||||
</script>
|
||||
|
||||
<!-- Alpine.js v3 -->
|
||||
<script defer src="https://cdn.jsdelivr.net/npm/alpinejs@3.14.0/dist/cdn.min.js"></script>
|
||||
|
||||
<!-- Initialize Alpine data -->
|
||||
<!-- 4. Base Alpine Data -->
|
||||
<script src="/static/admin/js/init-alpine.js"></script>
|
||||
|
||||
<!-- Dashboard-specific logic -->
|
||||
<script>
|
||||
function dashboardData() {
|
||||
return {
|
||||
...data(), // Spread base data from init-alpine.js
|
||||
currentPage: 'dashboard',
|
||||
stats: {
|
||||
totalVendors: 0,
|
||||
activeUsers: 0,
|
||||
verifiedVendors: 0,
|
||||
importJobs: 0
|
||||
},
|
||||
vendors: [],
|
||||
loading: false,
|
||||
<!-- 5. API Client & Utils -->
|
||||
<script src="/static/shared/js/api-client.js"></script>
|
||||
<script src="/static/shared/js/utils.js"></script>
|
||||
|
||||
async init() {
|
||||
await this.loadStats();
|
||||
await this.loadVendors();
|
||||
},
|
||||
<!-- 6. Alpine.js v3 (deferred) -->
|
||||
<script defer src="https://cdn.jsdelivr.net/npm/alpinejs@3.14.0/dist/cdn.min.js"></script>
|
||||
|
||||
async loadStats() {
|
||||
try {
|
||||
// Replace with your actual API endpoint
|
||||
const response = await fetch('/api/v1/admin/dashboard/stats', {
|
||||
headers: {
|
||||
'Authorization': `Bearer ${localStorage.getItem('token')}`
|
||||
}
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
const data = await response.json();
|
||||
this.stats = {
|
||||
totalVendors: data.vendors?.total_vendors || 0,
|
||||
activeUsers: data.users?.active_users || 0,
|
||||
verifiedVendors: data.vendors?.verified_vendors || 0,
|
||||
importJobs: data.imports?.total_imports || 0
|
||||
};
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error loading stats:', error);
|
||||
}
|
||||
},
|
||||
|
||||
async loadVendors() {
|
||||
try {
|
||||
// Replace with your actual API endpoint
|
||||
const response = await fetch('/api/v1/admin/vendors?limit=5', {
|
||||
headers: {
|
||||
'Authorization': `Bearer ${localStorage.getItem('token')}`
|
||||
}
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
const data = await response.json();
|
||||
this.vendors = data.vendors || [];
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error loading vendors:', error);
|
||||
}
|
||||
},
|
||||
|
||||
formatDate(dateString) {
|
||||
if (!dateString) return '-';
|
||||
const date = new Date(dateString);
|
||||
return date.toLocaleDateString('en-US', {
|
||||
year: 'numeric',
|
||||
month: 'short',
|
||||
day: 'numeric'
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
<!-- 7. Dashboard-specific script (AFTER Alpine loads) -->
|
||||
<script src="/static/admin/js/dashboard.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,72 +1,126 @@
|
||||
/**
|
||||
* Admin Dashboard Component
|
||||
* Extends adminLayout with dashboard-specific functionality
|
||||
*/
|
||||
// static/admin/js/dashboard.js
|
||||
|
||||
// Log levels: 0 = None, 1 = Error, 2 = Warning, 3 = Info, 4 = Debug
|
||||
const DASHBOARD_LOG_LEVEL = 3; // Set to 3 for production, 4 for full debugging
|
||||
|
||||
const dashLog = {
|
||||
error: (...args) => DASHBOARD_LOG_LEVEL >= 1 && console.error('❌ [DASHBOARD ERROR]', ...args),
|
||||
warn: (...args) => DASHBOARD_LOG_LEVEL >= 2 && console.warn('⚠️ [DASHBOARD WARN]', ...args),
|
||||
info: (...args) => DASHBOARD_LOG_LEVEL >= 3 && console.info('ℹ️ [DASHBOARD INFO]', ...args),
|
||||
debug: (...args) => DASHBOARD_LOG_LEVEL >= 4 && console.log('🔍 [DASHBOARD DEBUG]', ...args)
|
||||
};
|
||||
|
||||
function adminDashboard() {
|
||||
return {
|
||||
// Inherit all adminLayout functionality
|
||||
...window.adminLayout(),
|
||||
// Inherit base layout functionality from init-alpine.js
|
||||
...data(),
|
||||
|
||||
// Dashboard-specific state
|
||||
currentSection: 'dashboard',
|
||||
currentPage: 'dashboard',
|
||||
stats: {
|
||||
vendors: {},
|
||||
users: {},
|
||||
imports: {}
|
||||
totalVendors: 0,
|
||||
activeUsers: 0,
|
||||
verifiedVendors: 0,
|
||||
importJobs: 0
|
||||
},
|
||||
vendors: [],
|
||||
users: [],
|
||||
imports: [],
|
||||
recentVendors: [],
|
||||
recentImports: [],
|
||||
loading: false,
|
||||
loading: true,
|
||||
error: null,
|
||||
|
||||
/**
|
||||
* Initialize dashboard
|
||||
*/
|
||||
async init() {
|
||||
// Call parent init from adminLayout
|
||||
this.currentPage = this.getCurrentPage();
|
||||
await this.loadUserData();
|
||||
dashLog.info('=== DASHBOARD INITIALIZING ===');
|
||||
dashLog.debug('Current URL:', window.location.href);
|
||||
dashLog.debug('Current pathname:', window.location.pathname);
|
||||
|
||||
// Load dashboard data
|
||||
await this.loadDashboardData();
|
||||
const token = localStorage.getItem('admin_token');
|
||||
dashLog.debug('Has admin_token?', !!token);
|
||||
if (token) {
|
||||
dashLog.debug('Token preview:', token.substring(0, 20) + '...');
|
||||
}
|
||||
|
||||
// Prevent multiple initializations
|
||||
if (window._dashboardInitialized) {
|
||||
dashLog.warn('Dashboard already initialized, skipping...');
|
||||
return;
|
||||
}
|
||||
window._dashboardInitialized = true;
|
||||
dashLog.debug('Dashboard initialization flag set');
|
||||
|
||||
await this.loadDashboard();
|
||||
dashLog.info('=== DASHBOARD INITIALIZATION COMPLETE ===');
|
||||
},
|
||||
|
||||
/**
|
||||
* Load all dashboard data
|
||||
*/
|
||||
async loadDashboardData() {
|
||||
async loadDashboard() {
|
||||
dashLog.info('Loading dashboard data...');
|
||||
this.loading = true;
|
||||
this.error = null;
|
||||
dashLog.debug('Dashboard state: loading=true, error=null');
|
||||
|
||||
try {
|
||||
dashLog.info('Starting parallel data fetch...');
|
||||
const startTime = Date.now();
|
||||
|
||||
// Load stats and vendors in parallel
|
||||
await Promise.all([
|
||||
this.loadStats(),
|
||||
this.loadRecentVendors(),
|
||||
this.loadRecentImports()
|
||||
this.loadRecentVendors()
|
||||
]);
|
||||
|
||||
const duration = Date.now() - startTime;
|
||||
dashLog.info(`Dashboard data loaded successfully in ${duration}ms`);
|
||||
|
||||
} catch (error) {
|
||||
console.error('Error loading dashboard data:', error);
|
||||
this.showErrorModal({
|
||||
message: 'Failed to load dashboard data',
|
||||
details: error.message
|
||||
dashLog.error('Dashboard load error:', error);
|
||||
dashLog.error('Error details:', {
|
||||
message: error.message,
|
||||
name: error.name,
|
||||
stack: error.stack
|
||||
});
|
||||
|
||||
this.error = error.message;
|
||||
Utils.showToast('Failed to load dashboard data', 'error');
|
||||
|
||||
} finally {
|
||||
this.loading = false;
|
||||
dashLog.debug('Dashboard state: loading=false');
|
||||
dashLog.info('Dashboard load attempt finished');
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Load statistics
|
||||
* Load platform statistics
|
||||
*/
|
||||
async loadStats() {
|
||||
dashLog.info('Loading platform statistics...');
|
||||
dashLog.debug('API endpoint: /admin/dashboard/stats/platform');
|
||||
|
||||
try {
|
||||
const response = await apiClient.get('/admin/stats');
|
||||
this.stats = response;
|
||||
const startTime = Date.now();
|
||||
const data = await apiClient.get('/admin/dashboard/stats/platform');
|
||||
const duration = Date.now() - startTime;
|
||||
|
||||
dashLog.info(`Stats loaded in ${duration}ms`);
|
||||
dashLog.debug('Raw stats data:', data);
|
||||
|
||||
// Map API response to stats cards
|
||||
this.stats = {
|
||||
totalVendors: data.vendors?.total_vendors || 0,
|
||||
activeUsers: data.users?.active_users || 0,
|
||||
verifiedVendors: data.vendors?.verified_vendors || 0,
|
||||
importJobs: data.imports?.total_imports || 0
|
||||
};
|
||||
|
||||
dashLog.info('Stats mapped:', this.stats);
|
||||
|
||||
} catch (error) {
|
||||
console.error('Failed to load stats:', error);
|
||||
// Don't show error modal for stats, just log it
|
||||
dashLog.error('Failed to load stats:', error);
|
||||
throw error;
|
||||
}
|
||||
},
|
||||
|
||||
@@ -74,111 +128,32 @@ function adminDashboard() {
|
||||
* Load recent vendors
|
||||
*/
|
||||
async loadRecentVendors() {
|
||||
dashLog.info('Loading recent vendors...');
|
||||
dashLog.debug('API endpoint: /admin/dashboard');
|
||||
|
||||
try {
|
||||
const response = await apiClient.get('/admin/vendors', {
|
||||
skip: 0,
|
||||
limit: 5
|
||||
const startTime = Date.now();
|
||||
const data = await apiClient.get('/admin/dashboard');
|
||||
const duration = Date.now() - startTime;
|
||||
|
||||
dashLog.info(`Recent vendors loaded in ${duration}ms`);
|
||||
dashLog.debug('Vendors data:', {
|
||||
count: data.recent_vendors?.length || 0,
|
||||
hasData: !!data.recent_vendors
|
||||
});
|
||||
this.recentVendors = response.vendors || response;
|
||||
|
||||
this.recentVendors = data.recent_vendors || [];
|
||||
|
||||
if (this.recentVendors.length > 0) {
|
||||
dashLog.info(`Loaded ${this.recentVendors.length} recent vendors`);
|
||||
dashLog.debug('First vendor:', this.recentVendors[0]);
|
||||
} else {
|
||||
dashLog.warn('No recent vendors found');
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
console.error('Failed to load recent vendors:', error);
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Load recent import jobs
|
||||
*/
|
||||
async loadRecentImports() {
|
||||
try {
|
||||
const response = await apiClient.get('/admin/imports', {
|
||||
skip: 0,
|
||||
limit: 5
|
||||
});
|
||||
this.recentImports = response.imports || response;
|
||||
} catch (error) {
|
||||
console.error('Failed to load recent imports:', error);
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Show different sections
|
||||
*/
|
||||
async showSection(section) {
|
||||
this.currentSection = section;
|
||||
|
||||
// Load data based on section
|
||||
if (section === 'vendors' && this.vendors.length === 0) {
|
||||
await this.loadAllVendors();
|
||||
} else if (section === 'users' && this.users.length === 0) {
|
||||
await this.loadAllUsers();
|
||||
} else if (section === 'imports' && this.imports.length === 0) {
|
||||
await this.loadAllImports();
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Load all vendors
|
||||
*/
|
||||
async loadAllVendors() {
|
||||
this.loading = true;
|
||||
try {
|
||||
const response = await apiClient.get('/admin/vendors', {
|
||||
skip: 0,
|
||||
limit: 100
|
||||
});
|
||||
this.vendors = response.vendors || response;
|
||||
} catch (error) {
|
||||
console.error('Failed to load vendors:', error);
|
||||
this.showErrorModal({
|
||||
message: 'Failed to load vendors',
|
||||
details: error.message
|
||||
});
|
||||
} finally {
|
||||
this.loading = false;
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Load all users
|
||||
*/
|
||||
async loadAllUsers() {
|
||||
this.loading = true;
|
||||
try {
|
||||
const response = await apiClient.get('/admin/users', {
|
||||
skip: 0,
|
||||
limit: 100
|
||||
});
|
||||
this.users = response.users || response;
|
||||
} catch (error) {
|
||||
console.error('Failed to load users:', error);
|
||||
this.showErrorModal({
|
||||
message: 'Failed to load users',
|
||||
details: error.message
|
||||
});
|
||||
} finally {
|
||||
this.loading = false;
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Load all import jobs
|
||||
*/
|
||||
async loadAllImports() {
|
||||
this.loading = true;
|
||||
try {
|
||||
const response = await apiClient.get('/admin/imports', {
|
||||
skip: 0,
|
||||
limit: 100
|
||||
});
|
||||
this.imports = response.imports || response;
|
||||
} catch (error) {
|
||||
console.error('Failed to load import jobs:', error);
|
||||
this.showErrorModal({
|
||||
message: 'Failed to load import jobs',
|
||||
details: error.message
|
||||
});
|
||||
} finally {
|
||||
this.loading = false;
|
||||
dashLog.error('Failed to load recent vendors:', error);
|
||||
throw error;
|
||||
}
|
||||
},
|
||||
|
||||
@@ -186,18 +161,35 @@ function adminDashboard() {
|
||||
* Format date for display
|
||||
*/
|
||||
formatDate(dateString) {
|
||||
if (!dateString) return '-';
|
||||
|
||||
try {
|
||||
const date = new Date(dateString);
|
||||
return date.toLocaleDateString('en-US', {
|
||||
year: 'numeric',
|
||||
month: 'short',
|
||||
day: 'numeric'
|
||||
});
|
||||
} catch (error) {
|
||||
return dateString;
|
||||
if (!dateString) {
|
||||
dashLog.debug('formatDate called with empty dateString');
|
||||
return '-';
|
||||
}
|
||||
const formatted = Utils.formatDate(dateString);
|
||||
dashLog.debug(`Date formatted: ${dateString} -> ${formatted}`);
|
||||
return formatted;
|
||||
},
|
||||
|
||||
/**
|
||||
* Navigate to vendor detail page
|
||||
*/
|
||||
viewVendor(vendorCode) {
|
||||
dashLog.info('Navigating to vendor:', vendorCode);
|
||||
const url = `/admin/vendors?code=${vendorCode}`;
|
||||
dashLog.debug('Navigation URL:', url);
|
||||
window.location.href = url;
|
||||
},
|
||||
|
||||
/**
|
||||
* Refresh dashboard data
|
||||
*/
|
||||
async refresh() {
|
||||
dashLog.info('=== DASHBOARD REFRESH TRIGGERED ===');
|
||||
await this.loadDashboard();
|
||||
Utils.showToast('Dashboard refreshed', 'success');
|
||||
dashLog.info('=== DASHBOARD REFRESH COMPLETE ===');
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
dashLog.info('Dashboard module loaded');
|
||||
@@ -1,7 +1,18 @@
|
||||
// Admin Login Component
|
||||
// static/admin/js/login.js
|
||||
|
||||
// Log levels: 0 = None, 1 = Error, 2 = Warning, 3 = Info, 4 = Debug
|
||||
const LOG_LEVEL = 4; // Set to 4 for full debugging, 1 for errors only
|
||||
|
||||
const log = {
|
||||
error: (...args) => LOG_LEVEL >= 1 && console.error('❌ [ERROR]', ...args),
|
||||
warn: (...args) => LOG_LEVEL >= 2 && console.warn('⚠️ [WARN]', ...args),
|
||||
info: (...args) => LOG_LEVEL >= 3 && console.info('ℹ️ [INFO]', ...args),
|
||||
debug: (...args) => LOG_LEVEL >= 4 && console.log('🔍 [DEBUG]', ...args)
|
||||
};
|
||||
|
||||
function adminLogin() {
|
||||
return {
|
||||
dark: false, // For dark mode toggle
|
||||
dark: false,
|
||||
credentials: {
|
||||
username: '',
|
||||
password: ''
|
||||
@@ -12,119 +23,192 @@ function adminLogin() {
|
||||
errors: {},
|
||||
|
||||
init() {
|
||||
// Check if already logged in
|
||||
this.checkExistingAuth();
|
||||
log.info('Login page initializing...');
|
||||
log.debug('Current pathname:', window.location.pathname);
|
||||
log.debug('Current URL:', window.location.href);
|
||||
|
||||
// Check for dark mode preference
|
||||
// Just set theme - NO auth checking, NO token clearing!
|
||||
this.dark = localStorage.getItem('theme') === 'dark';
|
||||
},
|
||||
log.debug('Dark mode:', this.dark);
|
||||
|
||||
checkExistingAuth() {
|
||||
const token = localStorage.getItem('admin_token') || localStorage.getItem('token');
|
||||
// DON'T clear tokens on init!
|
||||
// If user lands here with a valid token, they might be navigating manually
|
||||
// or got redirected. Let them try to login or navigate away.
|
||||
const token = localStorage.getItem('admin_token');
|
||||
if (token) {
|
||||
// Verify token is still valid
|
||||
const userData = localStorage.getItem('admin_user');
|
||||
if (userData) {
|
||||
try {
|
||||
const user = JSON.parse(userData);
|
||||
if (user.role === 'admin') {
|
||||
window.location.href = '/static/admin/dashboard.html';
|
||||
}
|
||||
} catch (e) {
|
||||
// Invalid user data, clear storage
|
||||
this.clearAuthData();
|
||||
}
|
||||
}
|
||||
log.warn('Found existing token on login page');
|
||||
log.debug('Token preview:', token.substring(0, 20) + '...');
|
||||
log.info('Not clearing token - user may have navigated here manually');
|
||||
} else {
|
||||
log.debug('No existing token found');
|
||||
}
|
||||
|
||||
log.info('Login page initialization complete');
|
||||
},
|
||||
|
||||
clearAuthData() {
|
||||
clearTokens() {
|
||||
log.debug('Clearing all auth tokens...');
|
||||
const tokensBefore = {
|
||||
admin_token: !!localStorage.getItem('admin_token'),
|
||||
admin_user: !!localStorage.getItem('admin_user'),
|
||||
token: !!localStorage.getItem('token')
|
||||
};
|
||||
log.debug('Tokens before clear:', tokensBefore);
|
||||
|
||||
localStorage.removeItem('admin_token');
|
||||
localStorage.removeItem('admin_user');
|
||||
localStorage.removeItem('token');
|
||||
|
||||
const tokensAfter = {
|
||||
admin_token: !!localStorage.getItem('admin_token'),
|
||||
admin_user: !!localStorage.getItem('admin_user'),
|
||||
token: !!localStorage.getItem('token')
|
||||
};
|
||||
log.debug('Tokens after clear:', tokensAfter);
|
||||
},
|
||||
|
||||
clearErrors() {
|
||||
log.debug('Clearing form errors');
|
||||
this.error = null;
|
||||
this.success = null;
|
||||
this.errors = {};
|
||||
},
|
||||
|
||||
validateForm() {
|
||||
log.debug('Validating login form...');
|
||||
this.clearErrors();
|
||||
let isValid = true;
|
||||
|
||||
if (!this.credentials.username.trim()) {
|
||||
this.errors.username = 'Username is required';
|
||||
log.warn('Validation failed: Username is required');
|
||||
isValid = false;
|
||||
}
|
||||
|
||||
if (!this.credentials.password) {
|
||||
this.errors.password = 'Password is required';
|
||||
log.warn('Validation failed: Password is required');
|
||||
isValid = false;
|
||||
} else if (this.credentials.password.length < 6) {
|
||||
this.errors.password = 'Password must be at least 6 characters';
|
||||
log.warn('Validation failed: Password too short');
|
||||
isValid = false;
|
||||
}
|
||||
|
||||
log.info('Form validation result:', isValid ? 'VALID' : 'INVALID');
|
||||
return isValid;
|
||||
},
|
||||
|
||||
async handleLogin() {
|
||||
log.info('=== LOGIN ATTEMPT STARTED ===');
|
||||
|
||||
if (!this.validateForm()) {
|
||||
log.warn('Form validation failed, aborting login');
|
||||
return;
|
||||
}
|
||||
|
||||
this.loading = true;
|
||||
this.clearErrors();
|
||||
log.debug('Login state set to loading');
|
||||
|
||||
try {
|
||||
// Use apiClient from api-client.js
|
||||
log.info('Calling login API endpoint...');
|
||||
log.debug('Username:', this.credentials.username);
|
||||
log.debug('API endpoint: /api/v1/admin/auth/login');
|
||||
|
||||
const startTime = Date.now();
|
||||
const response = await apiClient.post('/admin/auth/login', {
|
||||
username: this.credentials.username.trim(),
|
||||
password: this.credentials.password
|
||||
});
|
||||
const duration = Date.now() - startTime;
|
||||
|
||||
log.info(`Login API response received in ${duration}ms`);
|
||||
log.debug('Response structure:', {
|
||||
hasToken: !!response.access_token,
|
||||
hasUser: !!response.user,
|
||||
userRole: response.user?.role,
|
||||
userName: response.user?.username
|
||||
});
|
||||
|
||||
// Validate response
|
||||
if (!response.access_token) {
|
||||
throw new Error('Invalid response from server');
|
||||
log.error('Invalid response: No access token');
|
||||
throw new Error('Invalid response from server - no token');
|
||||
}
|
||||
|
||||
// Check if user is admin (if user data is provided)
|
||||
if (response.user && response.user.role !== 'admin') {
|
||||
log.error('Authorization failed: User is not admin', {
|
||||
actualRole: response.user.role
|
||||
});
|
||||
throw new Error('Access denied. Admin privileges required.');
|
||||
}
|
||||
|
||||
log.info('Login successful, storing authentication data...');
|
||||
|
||||
// Store authentication data
|
||||
localStorage.setItem('admin_token', response.access_token);
|
||||
localStorage.setItem('token', response.access_token); // Backup
|
||||
localStorage.setItem('token', response.access_token);
|
||||
log.debug('Token stored, length:', response.access_token.length);
|
||||
|
||||
if (response.user) {
|
||||
localStorage.setItem('admin_user', JSON.stringify(response.user));
|
||||
log.debug('User data stored:', {
|
||||
username: response.user.username,
|
||||
role: response.user.role,
|
||||
id: response.user.id
|
||||
});
|
||||
}
|
||||
|
||||
// Verify storage
|
||||
const storedToken = localStorage.getItem('admin_token');
|
||||
const storedUser = localStorage.getItem('admin_user');
|
||||
log.info('Storage verification:', {
|
||||
tokenStored: !!storedToken,
|
||||
userStored: !!storedUser,
|
||||
tokenLength: storedToken?.length
|
||||
});
|
||||
|
||||
// Show success message
|
||||
this.success = 'Login successful! Redirecting...';
|
||||
log.info('Success message displayed to user');
|
||||
|
||||
// Redirect after short delay
|
||||
setTimeout(() => {
|
||||
window.location.href = '/static/admin/dashboard.html';
|
||||
}, 1000);
|
||||
log.info('Redirecting to dashboard immediately...');
|
||||
log.info('=== EXECUTING REDIRECT ===');
|
||||
log.debug('Target URL: /admin/dashboard');
|
||||
log.debug('Redirect method: window.location.href');
|
||||
|
||||
// Use href instead of replace to allow back button
|
||||
// But redirect IMMEDIATELY - don't wait!
|
||||
window.location.href = '/admin/dashboard';
|
||||
|
||||
} catch (error) {
|
||||
console.error('Login error:', error);
|
||||
this.error = error.message || 'Invalid username or password. Please try again.';
|
||||
log.error('Login failed:', error);
|
||||
log.error('Error details:', {
|
||||
message: error.message,
|
||||
name: error.name,
|
||||
stack: error.stack
|
||||
});
|
||||
|
||||
this.error = error.message || 'Invalid username or password. Please try again.';
|
||||
log.info('Error message displayed to user:', this.error);
|
||||
|
||||
// Only clear tokens on login FAILURE
|
||||
this.clearTokens();
|
||||
log.info('Tokens cleared after error');
|
||||
|
||||
// Clear any partial auth data
|
||||
this.clearAuthData();
|
||||
} finally {
|
||||
this.loading = false;
|
||||
log.debug('Login state set to not loading');
|
||||
log.info('=== LOGIN ATTEMPT FINISHED ===');
|
||||
}
|
||||
},
|
||||
|
||||
toggleDarkMode() {
|
||||
log.debug('Toggling dark mode...');
|
||||
this.dark = !this.dark;
|
||||
localStorage.setItem('theme', this.dark ? 'dark' : 'light');
|
||||
log.info('Dark mode:', this.dark ? 'ON' : 'OFF');
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -46,7 +46,7 @@ function vendorEdit() {
|
||||
// Check authentication
|
||||
if (!Auth.isAuthenticated() || !Auth.isAdmin()) {
|
||||
console.log('Not authenticated as admin, redirecting to login');
|
||||
window.location.href = '/static/admin/login.html';
|
||||
window.location.href = '/admin/login';
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -60,7 +60,7 @@ function vendorEdit() {
|
||||
if (!this.vendorId) {
|
||||
console.error('No vendor ID in URL');
|
||||
alert('No vendor ID provided');
|
||||
window.location.href = '/static/admin/dashboard.html#vendors';
|
||||
window.location.href = '/admin/dashboard.html#vendors';
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -95,7 +95,7 @@ function vendorEdit() {
|
||||
} catch (error) {
|
||||
console.error('❌ Failed to load vendor:', error);
|
||||
Utils.showToast('Failed to load vendor details: ' + (error.message || 'Unknown error'), 'error');
|
||||
window.location.href = '/static/admin/dashboard.html#vendors';
|
||||
window.location.href = '/admin/dashboard';
|
||||
} finally {
|
||||
this.loadingVendor = false;
|
||||
}
|
||||
@@ -331,7 +331,7 @@ function vendorEdit() {
|
||||
|
||||
// Redirect to login after brief delay
|
||||
setTimeout(() => {
|
||||
window.location.href = '/static/admin/login.html';
|
||||
window.location.href = '/admin/login';
|
||||
}, 500);
|
||||
},
|
||||
};
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
// static/admin/js/vendors.js
|
||||
// Admin Vendor Creation Component
|
||||
function vendorCreation() {
|
||||
return {
|
||||
@@ -26,7 +27,8 @@ function vendorCreation() {
|
||||
|
||||
checkAuth() {
|
||||
if (!Auth.isAuthenticated()) {
|
||||
window.location.href = '/static/admin/login.html';
|
||||
// ← CHANGED: Use new Jinja2 route
|
||||
window.location.href = '/admin/login';
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -34,7 +36,8 @@ function vendorCreation() {
|
||||
if (!user || user.role !== 'admin') {
|
||||
Utils.showToast('Access denied. Admin privileges required.', 'error');
|
||||
Auth.logout();
|
||||
window.location.href = '/static/admin/login.html';
|
||||
// ← CHANGED: Use new Jinja2 route
|
||||
window.location.href = '/admin/login';
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -52,11 +55,14 @@ function vendorCreation() {
|
||||
Auth.logout();
|
||||
Utils.showToast('Logged out successfully', 'success', 2000);
|
||||
setTimeout(() => {
|
||||
window.location.href = '/static/admin/login.html';
|
||||
// ← CHANGED: Use new Jinja2 route
|
||||
window.location.href = '/admin/login';
|
||||
}, 500);
|
||||
}
|
||||
},
|
||||
|
||||
// ... rest of the methods stay the same ...
|
||||
|
||||
// Auto-format vendor code (uppercase)
|
||||
formatVendorCode() {
|
||||
this.formData.vendor_code = this.formData.vendor_code
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<!DOCTYPE html>
|
||||
<html :class="{ 'theme-dark': dark }" x-data="loginData()" lang="en">
|
||||
<html :class="{ 'theme-dark': dark }" x-data="adminLogin()" lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
@@ -45,6 +45,7 @@
|
||||
class="block w-full mt-1 text-sm dark:border-gray-600 dark:bg-gray-700 focus:border-purple-400 focus:outline-none focus:shadow-outline-purple dark:text-gray-300 dark:focus:shadow-outline-gray form-input"
|
||||
:class="{ 'border-red-600': errors.username }"
|
||||
placeholder="Enter your username"
|
||||
autocomplete="username"
|
||||
required />
|
||||
<span x-show="errors.username" x-text="errors.username"
|
||||
class="text-xs text-red-600 dark:text-red-400"></span>
|
||||
@@ -59,6 +60,7 @@
|
||||
:class="{ 'border-red-600': errors.password }"
|
||||
placeholder="***************"
|
||||
type="password"
|
||||
autocomplete="current-password"
|
||||
required />
|
||||
<span x-show="errors.password" x-text="errors.password"
|
||||
class="text-xs text-red-600 dark:text-red-400"></span>
|
||||
@@ -85,80 +87,29 @@
|
||||
Forgot your password?
|
||||
</a>
|
||||
</p>
|
||||
<p class="mt-2">
|
||||
<a class="text-sm font-medium text-gray-600 dark:text-gray-400 hover:underline"
|
||||
href="/">
|
||||
← Back to Platform
|
||||
</a>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Alpine.js v3 -->
|
||||
<script defer src="https://cdn.jsdelivr.net/npm/alpinejs@3.14.0/dist/cdn.min.js"></script>
|
||||
<!-- Scripts in CORRECT ORDER -->
|
||||
<!-- 1. Icons FIRST (defines $icon magic) -->
|
||||
<script src="/static/shared/js/icons.js"></script>
|
||||
|
||||
<!-- API Client -->
|
||||
<!-- 2. API Client -->
|
||||
<script src="/static/shared/js/api-client.js"></script>
|
||||
|
||||
<!-- Login Logic -->
|
||||
<script>
|
||||
function loginData() {
|
||||
return {
|
||||
dark: false,
|
||||
loading: false,
|
||||
error: '',
|
||||
success: '',
|
||||
credentials: {
|
||||
username: '',
|
||||
password: ''
|
||||
},
|
||||
errors: {
|
||||
username: '',
|
||||
password: ''
|
||||
},
|
||||
<!-- 3. Login Logic -->
|
||||
<script src="/static/admin/js/login.js"></script>
|
||||
|
||||
clearErrors() {
|
||||
this.error = '';
|
||||
this.errors = { username: '', password: '' };
|
||||
},
|
||||
|
||||
async handleLogin() {
|
||||
this.clearErrors();
|
||||
this.loading = true;
|
||||
|
||||
try {
|
||||
// Your existing API call
|
||||
const response = await fetch('/api/v1/auth/login', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify(this.credentials)
|
||||
});
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(data.message || 'Login failed');
|
||||
}
|
||||
|
||||
// Store token
|
||||
localStorage.setItem('token', data.access_token);
|
||||
|
||||
// Show success
|
||||
this.success = 'Login successful! Redirecting...';
|
||||
|
||||
// Redirect to dashboard
|
||||
setTimeout(() => {
|
||||
window.location.href = '/static/admin/dashboard.html';
|
||||
}, 1000);
|
||||
|
||||
} catch (error) {
|
||||
this.error = error.message || 'Invalid username or password';
|
||||
console.error('Login error:', error);
|
||||
} finally {
|
||||
this.loading = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
<!-- 4. Alpine.js LAST with defer -->
|
||||
<script defer src="https://cdn.jsdelivr.net/npm/alpinejs@3.14.0/dist/cdn.min.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
95
static/admin/oldlogin.html
Normal file
95
static/admin/oldlogin.html
Normal file
@@ -0,0 +1,95 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Admin Login - Multi-Tenant Ecommerce Platform</title>
|
||||
<link rel="stylesheet" href="/static/css/shared/base.css">
|
||||
<link rel="stylesheet" href="/static/css/shared/auth.css">
|
||||
<script defer src="https://cdn.jsdelivr.net/npm/alpinejs@3.x.x/dist/cdn.min.js"></script>
|
||||
</head>
|
||||
<body>
|
||||
<div class="auth-page" x-data="adminLogin()" x-cloak>
|
||||
<div class="login-container">
|
||||
<div class="login-header">
|
||||
<div class="auth-logo">🔐</div>
|
||||
<h1>Admin Portal</h1>
|
||||
<p>Multi-Tenant Ecommerce Platform</p>
|
||||
</div>
|
||||
|
||||
<!-- Alert Messages -->
|
||||
<div x-show="error"
|
||||
x-text="error"
|
||||
class="alert alert-error"
|
||||
x-transition></div>
|
||||
|
||||
<div x-show="success"
|
||||
x-text="success"
|
||||
class="alert alert-success"
|
||||
x-transition></div>
|
||||
|
||||
<!-- Login Form -->
|
||||
<form @submit.prevent="handleLogin">
|
||||
<div class="form-group">
|
||||
<label for="username">Username</label>
|
||||
<input
|
||||
type="text"
|
||||
id="username"
|
||||
x-model="credentials.username"
|
||||
:class="{ 'error': errors.username }"
|
||||
required
|
||||
autocomplete="username"
|
||||
placeholder="Enter your username"
|
||||
:disabled="loading"
|
||||
@input="clearErrors"
|
||||
>
|
||||
<div x-show="errors.username"
|
||||
x-text="errors.username"
|
||||
class="error-message show"
|
||||
x-transition></div>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="password">Password</label>
|
||||
<input
|
||||
type="password"
|
||||
id="password"
|
||||
x-model="credentials.password"
|
||||
:class="{ 'error': errors.password }"
|
||||
required
|
||||
autocomplete="current-password"
|
||||
placeholder="Enter your password"
|
||||
:disabled="loading"
|
||||
@input="clearErrors"
|
||||
>
|
||||
<div x-show="errors.password"
|
||||
x-text="errors.password"
|
||||
class="error-message show"
|
||||
x-transition></div>
|
||||
</div>
|
||||
|
||||
<button type="submit"
|
||||
class="btn-login"
|
||||
:disabled="loading">
|
||||
<template x-if="!loading">
|
||||
<span>Sign In</span>
|
||||
</template>
|
||||
<template x-if="loading">
|
||||
<span>
|
||||
<span class="loading-spinner"></span>
|
||||
Signing in...
|
||||
</span>
|
||||
</template>
|
||||
</button>
|
||||
</form>
|
||||
|
||||
<div class="login-footer">
|
||||
<a href="/">← Back to Platform</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script src="/static/js/shared/api-client.js"></script>
|
||||
<script src="/static/admin/js/login.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
72
static/admin/partials/base-layout.html
Normal file
72
static/admin/partials/base-layout.html
Normal file
@@ -0,0 +1,72 @@
|
||||
<!-- static/admin/partials/base-layout.html -->
|
||||
<!DOCTYPE html>
|
||||
<html :class="{ 'theme-dark': dark }" lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title data-page-title>Admin Panel - Multi-Tenant Platform</title>
|
||||
|
||||
<!-- Fonts -->
|
||||
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800&display=swap" rel="stylesheet" />
|
||||
|
||||
<!-- Tailwind CSS -->
|
||||
<link rel="stylesheet" href="/static/admin/css/tailwind.output.css" />
|
||||
|
||||
<!-- Alpine Cloak -->
|
||||
<style>
|
||||
[x-cloak] { display: none !important; }
|
||||
</style>
|
||||
|
||||
<!-- Page-specific styles slot -->
|
||||
<slot name="head"></slot>
|
||||
</head>
|
||||
<body x-cloak>
|
||||
<div class="flex h-screen bg-gray-50 dark:bg-gray-900" :class="{ 'overflow-hidden': isSideMenuOpen }">
|
||||
<!-- Sidebar Container -->
|
||||
<div id="sidebar-container"></div>
|
||||
|
||||
<div class="flex flex-col flex-1 w-full">
|
||||
<!-- Header Container -->
|
||||
<div id="header-container"></div>
|
||||
|
||||
<!-- Main Content Area (Child pages inject content here) -->
|
||||
<main class="h-full overflow-y-auto">
|
||||
<div class="container px-6 mx-auto grid">
|
||||
<!-- Page content slot -->
|
||||
<slot name="content"></slot>
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Core Scripts (loaded for all pages) -->
|
||||
<!-- 1. Partial Loader -->
|
||||
<script src="/static/shared/js/partial-loader.js"></script>
|
||||
|
||||
<!-- 2. Icons Helper -->
|
||||
<script src="/static/shared/js/icons.js"></script>
|
||||
|
||||
<!-- 3. Load Header & Sidebar -->
|
||||
<script>
|
||||
(async () => {
|
||||
await window.partialLoader.loadAll({
|
||||
'header-container': 'header.html',
|
||||
'sidebar-container': 'sidebar.html'
|
||||
});
|
||||
})();
|
||||
</script>
|
||||
|
||||
<!-- 4. Base Alpine Data -->
|
||||
<script src="/static/admin/js/init-alpine.js"></script>
|
||||
|
||||
<!-- 5. API Client & Utils -->
|
||||
<script src="/static/shared/js/api-client.js"></script>
|
||||
<script src="/static/shared/js/utils.js"></script>
|
||||
|
||||
<!-- 6. Alpine.js v3 -->
|
||||
<script defer src="https://cdn.jsdelivr.net/npm/alpinejs@3.14.0/dist/cdn.min.js"></script>
|
||||
|
||||
<!-- Page-specific scripts slot -->
|
||||
<slot name="scripts"></slot>
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,133 +0,0 @@
|
||||
<!-- Top header bar with search, theme toggle, notifications, profile -->
|
||||
<header class="z-10 py-4 bg-white shadow-md dark:bg-gray-800">
|
||||
<div class="container flex items-center justify-between h-full px-6 mx-auto text-purple-600 dark:text-purple-300">
|
||||
<!-- Mobile hamburger -->
|
||||
<button class="p-1 mr-5 -ml-1 rounded-md md:hidden focus:outline-none focus:shadow-outline-purple"
|
||||
@click="toggleSideMenu" aria-label="Menu">
|
||||
<span x-html="$icon('menu', 'w-6 h-6')"></span>
|
||||
</button>
|
||||
|
||||
<!-- Search input -->
|
||||
<div class="flex justify-center flex-1 lg:mr-32">
|
||||
<div class="relative w-full max-w-xl mr-6 focus-within:text-purple-500">
|
||||
<div class="absolute inset-y-0 flex items-center pl-2">
|
||||
<span x-html="$icon('search', 'w-4 h-4')"></span>
|
||||
</div>
|
||||
<input class="w-full pl-8 pr-2 text-sm text-gray-700 placeholder-gray-600 bg-gray-100 border-0 rounded-md dark:placeholder-gray-500 dark:focus:shadow-outline-gray dark:focus:placeholder-gray-600 dark:bg-gray-700 dark:text-gray-200 focus:placeholder-gray-500 focus:bg-white focus:border-purple-300 focus:outline-none focus:shadow-outline-purple form-input"
|
||||
type="text" placeholder="Search for projects" aria-label="Search"/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<ul class="flex items-center flex-shrink-0 space-x-6">
|
||||
<!-- Theme toggler -->
|
||||
<li class="flex">
|
||||
<button class="rounded-md focus:outline-none focus:shadow-outline-purple"
|
||||
@click="toggleTheme" aria-label="Toggle color mode">
|
||||
<template x-if="!dark">
|
||||
<span x-html="$icon('moon', 'w-5 h-5')"></span>
|
||||
</template>
|
||||
<template x-if="dark">
|
||||
<span x-html="$icon('sun', 'w-5 h-5')"></span>
|
||||
</template>
|
||||
</button>
|
||||
</li>
|
||||
|
||||
<!-- Notifications menu -->
|
||||
<li class="relative">
|
||||
<button class="relative align-middle rounded-md focus:outline-none focus:shadow-outline-purple"
|
||||
@click="toggleNotificationsMenu" @keydown.escape="closeNotificationsMenu"
|
||||
aria-label="Notifications" aria-haspopup="true">
|
||||
<span x-html="$icon('bell', 'w-5 h-5')"></span>
|
||||
<span aria-hidden="true" class="absolute top-0 right-0 inline-block w-3 h-3 transform translate-x-1 -translate-y-1 bg-red-600 border-2 border-white rounded-full dark:border-gray-800"></span>
|
||||
</button>
|
||||
|
||||
<template x-if="isNotificationsMenuOpen">
|
||||
<ul x-transition:leave="transition ease-in duration-150"
|
||||
x-transition:leave-start="opacity-100"
|
||||
x-transition:leave-end="opacity-0"
|
||||
@click.away="closeNotificationsMenu"
|
||||
@keydown.escape="closeNotificationsMenu"
|
||||
class="absolute right-0 w-56 p-2 mt-2 space-y-2 text-gray-600 bg-white border border-gray-100 rounded-md shadow-md dark:text-gray-300 dark:border-gray-700 dark:bg-gray-700">
|
||||
<li class="flex">
|
||||
<a class="inline-flex items-center justify-between w-full px-2 py-1 text-sm font-semibold transition-colors duration-150 rounded-md hover:bg-gray-100 hover:text-gray-800 dark:hover:bg-gray-800 dark:hover:text-gray-200" href="#">
|
||||
<span>Messages</span>
|
||||
<span class="inline-flex items-center justify-center px-2 py-1 text-xs font-bold leading-none text-red-600 bg-red-100 rounded-full dark:text-red-100 dark:bg-red-600">13</span>
|
||||
</a>
|
||||
</li>
|
||||
<li class="flex">
|
||||
<a class="inline-flex items-center justify-between w-full px-2 py-1 text-sm font-semibold transition-colors duration-150 rounded-md hover:bg-gray-100 hover:text-gray-800 dark:hover:bg-gray-800 dark:hover:text-gray-200" href="#">
|
||||
<span>Sales</span>
|
||||
<span class="inline-flex items-center justify-center px-2 py-1 text-xs font-bold leading-none text-red-600 bg-red-100 rounded-full dark:text-red-100 dark:bg-red-600">2</span>
|
||||
</a>
|
||||
</li>
|
||||
<li class="flex">
|
||||
<a class="inline-flex items-center justify-between w-full px-2 py-1 text-sm font-semibold transition-colors duration-150 rounded-md hover:bg-gray-100 hover:text-gray-800 dark:hover:bg-gray-800 dark:hover:text-gray-200" href="#">
|
||||
<span>Alerts</span>
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</template>
|
||||
</li>
|
||||
|
||||
<!-- Profile menu -->
|
||||
<li class="relative">
|
||||
<button class="align-middle rounded-full focus:shadow-outline-purple focus:outline-none"
|
||||
@click="toggleProfileMenu" @keydown.escape="closeProfileMenu"
|
||||
aria-label="Account" aria-haspopup="true">
|
||||
<img class="object-cover w-8 h-8 rounded-full"
|
||||
src="https://images.unsplash.com/photo-1502378735452-bc7d86632805?ixlib=rb-0.3.5&q=80&fm=jpg&crop=entropy&cs=tinysrgb&w=200&fit=max&s=aa3a807e1bbdfd4364d1f449eaa96d82"
|
||||
alt="" aria-hidden="true"/>
|
||||
</button>
|
||||
|
||||
<template x-if="isProfileMenuOpen">
|
||||
<ul x-transition:leave="transition ease-in duration-150"
|
||||
x-transition:leave-start="opacity-100"
|
||||
x-transition:leave-end="opacity-0"
|
||||
@click.away="closeProfileMenu"
|
||||
@keydown.escape="closeProfileMenu"
|
||||
class="absolute right-0 w-56 p-2 mt-2 space-y-2 text-gray-600 bg-white border border-gray-100 rounded-md shadow-md dark:border-gray-700 dark:text-gray-300 dark:bg-gray-700"
|
||||
aria-label="submenu">
|
||||
<li class="flex">
|
||||
<a class="inline-flex items-center w-full px-2 py-1 text-sm font-semibold transition-colors duration-150 rounded-md hover:bg-gray-100 hover:text-gray-800 dark:hover:bg-gray-800 dark:hover:text-gray-200" href="#">
|
||||
<span x-html="$icon('user', 'w-4 h-4 mr-3')"></span>
|
||||
<span>Profile</span>
|
||||
</a>
|
||||
</li>
|
||||
<li class="flex">
|
||||
<a class="inline-flex items-center w-full px-2 py-1 text-sm font-semibold transition-colors duration-150 rounded-md hover:bg-gray-100 hover:text-gray-800 dark:hover:bg-gray-800 dark:hover:text-gray-200" href="#">
|
||||
<span x-html="$icon('cog', 'w-4 h-4 mr-3')"></span>
|
||||
<span>Settings</span>
|
||||
</a>
|
||||
</li>
|
||||
<li class="flex">
|
||||
<a class="inline-flex items-center w-full px-2 py-1 text-sm font-semibold transition-colors duration-150 rounded-md hover:bg-gray-100 hover:text-gray-800 dark:hover:bg-gray-800 dark:hover:text-gray-200" href="/static/admin/login.html">
|
||||
<span x-html="$icon('logout', 'w-4 h-4 mr-3')"></span>
|
||||
<span>Log out</span>
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</template>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</header>.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"></path>
|
||||
<path d="M15 12a3 3 0 11-6 0 3 3 0 016 0z"></path>
|
||||
</svg>
|
||||
<span>Settings</span>
|
||||
</a>
|
||||
</li>
|
||||
<li class="flex">
|
||||
<a class="inline-flex items-center w-full px-2 py-1 text-sm font-semibold transition-colors duration-150 rounded-md hover:bg-gray-100 hover:text-gray-800 dark:hover:bg-gray-800 dark:hover:text-gray-200" href="/static/admin/login.html">
|
||||
<!-- Heroicon: logout -->
|
||||
<svg class="w-4 h-4 mr-3" aria-hidden="true" fill="none" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path d="M11 16l-4-4m0 0l4-4m-4 4h14m-5 4v1a3 3 0 01-3 3H6a3 3 0 01-3-3V7a3 3 0 013-3h7a3 3 0 013 3v1"></path>
|
||||
</svg>
|
||||
<span>Log out</span>
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</template>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</header>
|
||||
@@ -1,116 +0,0 @@
|
||||
<!-- Desktop sidebar -->
|
||||
<aside class="z-20 hidden w-64 overflow-y-auto bg-white dark:bg-gray-800 md:block flex-shrink-0">
|
||||
<div class="py-4 text-gray-500 dark:text-gray-400">
|
||||
<a class="ml-6 text-lg font-bold text-gray-800 dark:text-gray-200" href="/static/admin/dashboard.html">
|
||||
Admin Portal
|
||||
</a>
|
||||
<ul class="mt-6">
|
||||
<li class="relative px-6 py-3">
|
||||
<span x-show="currentPage === 'dashboard'" class="absolute inset-y-0 left-0 w-1 bg-purple-600 rounded-tr-lg rounded-br-lg" aria-hidden="true"></span>
|
||||
<a class="inline-flex items-center w-full text-sm font-semibold transition-colors duration-150 hover:text-gray-800 dark:hover:text-gray-200"
|
||||
:class="currentPage === 'dashboard' ? 'text-gray-800 dark:text-gray-100' : ''"
|
||||
href="/static/admin/dashboard.html">
|
||||
<span x-html="$icon('home')"></span>
|
||||
<span class="ml-4">Dashboard</span>
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
<ul>
|
||||
<li class="relative px-6 py-3">
|
||||
<span x-show="currentPage === 'vendors'" class="absolute inset-y-0 left-0 w-1 bg-purple-600 rounded-tr-lg rounded-br-lg" aria-hidden="true"></span>
|
||||
<a class="inline-flex items-center w-full text-sm font-semibold transition-colors duration-150 hover:text-gray-800 dark:hover:text-gray-200"
|
||||
:class="currentPage === 'vendors' ? 'text-gray-800 dark:text-gray-100' : ''"
|
||||
href="/static/admin/vendors.html">
|
||||
<span x-html="$icon('shopping-bag')"></span>
|
||||
<span class="ml-4">Vendors</span>
|
||||
</a>
|
||||
</li>
|
||||
<li class="relative px-6 py-3">
|
||||
<a class="inline-flex items-center w-full text-sm font-semibold transition-colors duration-150 hover:text-gray-800 dark:hover:text-gray-200" href="#">
|
||||
<span x-html="$icon('users')"></span>
|
||||
<span class="ml-4">Users</span>
|
||||
</a>
|
||||
</li>
|
||||
<li class="relative px-6 py-3">
|
||||
<a class="inline-flex items-center w-full text-sm font-semibold transition-colors duration-150 hover:text-gray-800 dark:hover:text-gray-200" href="#">
|
||||
<span x-html="$icon('cube')"></span>
|
||||
<span class="ml-4">Import Jobs</span>
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
<div class="px-6 my-6">
|
||||
<button class="flex items-center justify-between w-full px-4 py-2 text-sm font-medium leading-5 text-white transition-colors duration-150 bg-purple-600 border border-transparent rounded-lg active:bg-purple-600 hover:bg-purple-700 focus:outline-none focus:shadow-outline-purple">
|
||||
Create vendor
|
||||
<span class="ml-2" aria-hidden="true">+</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</aside>
|
||||
|
||||
<!-- Mobile sidebar -->
|
||||
<!-- Backdrop -->
|
||||
<div x-show="isSideMenuOpen"
|
||||
x-transition:enter="transition ease-in-out duration-150"
|
||||
x-transition:enter-start="opacity-0"
|
||||
x-transition:enter-end="opacity-100"
|
||||
x-transition:leave="transition ease-in-out duration-150"
|
||||
x-transition:leave-start="opacity-100"
|
||||
x-transition:leave-end="opacity-0"
|
||||
class="fixed inset-0 z-10 flex items-end bg-black bg-opacity-50 sm:items-center sm:justify-center"></div>
|
||||
|
||||
<aside class="fixed inset-y-0 z-20 flex-shrink-0 w-64 mt-16 overflow-y-auto bg-white dark:bg-gray-800 md:hidden"
|
||||
x-show="isSideMenuOpen"
|
||||
x-transition:enter="transition ease-in-out duration-150"
|
||||
x-transition:enter-start="opacity-0 transform -translate-x-20"
|
||||
x-transition:enter-end="opacity-100"
|
||||
x-transition:leave="transition ease-in-out duration-150"
|
||||
x-transition:leave-start="opacity-100"
|
||||
x-transition:leave-end="opacity-0 transform -translate-x-20"
|
||||
@click.away="closeSideMenu"
|
||||
@keydown.escape="closeSideMenu">
|
||||
<div class="py-4 text-gray-500 dark:text-gray-400">
|
||||
<a class="ml-6 text-lg font-bold text-gray-800 dark:text-gray-200" href="/static/admin/dashboard.html">
|
||||
Admin Portal
|
||||
</a>
|
||||
<ul class="mt-6">
|
||||
<li class="relative px-6 py-3">
|
||||
<span x-show="currentPage === 'dashboard'" class="absolute inset-y-0 left-0 w-1 bg-purple-600 rounded-tr-lg rounded-br-lg" aria-hidden="true"></span>
|
||||
<a class="inline-flex items-center w-full text-sm font-semibold transition-colors duration-150 hover:text-gray-800 dark:hover:text-gray-200"
|
||||
:class="currentPage === 'dashboard' ? 'text-gray-800 dark:text-gray-100' : ''"
|
||||
href="/static/admin/dashboard.html">
|
||||
<span x-html="$icon('home')"></span>
|
||||
<span class="ml-4">Dashboard</span>
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
<ul>
|
||||
<li class="relative px-6 py-3">
|
||||
<span x-show="currentPage === 'vendors'" class="absolute inset-y-0 left-0 w-1 bg-purple-600 rounded-tr-lg rounded-br-lg" aria-hidden="true"></span>
|
||||
<a class="inline-flex items-center w-full text-sm font-semibold transition-colors duration-150 hover:text-gray-800 dark:hover:text-gray-200"
|
||||
:class="currentPage === 'vendors' ? 'text-gray-800 dark:text-gray-100' : ''"
|
||||
href="/static/admin/vendors.html">
|
||||
<span x-html="$icon('shopping-bag')"></span>
|
||||
<span class="ml-4">Vendors</span>
|
||||
</a>
|
||||
</li>
|
||||
<li class="relative px-6 py-3">
|
||||
<a class="inline-flex items-center w-full text-sm font-semibold transition-colors duration-150 hover:text-gray-800 dark:hover:text-gray-200" href="#">
|
||||
<span x-html="$icon('users')"></span>
|
||||
<span class="ml-4">Users</span>
|
||||
</a>
|
||||
</li>
|
||||
<li class="relative px-6 py-3">
|
||||
<a class="inline-flex items-center w-full text-sm font-semibold transition-colors duration-150 hover:text-gray-800 dark:hover:text-gray-200" href="#">
|
||||
<span x-html="$icon('cube')"></span>
|
||||
<span class="ml-4">Import Jobs</span>
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
<div class="px-6 my-6">
|
||||
<button class="flex items-center justify-between px-4 py-2 text-sm font-medium leading-5 text-white transition-colors duration-150 bg-purple-600 border border-transparent rounded-lg active:bg-purple-600 hover:bg-purple-700 focus:outline-none focus:shadow-outline-purple">
|
||||
Create vendor
|
||||
<span class="ml-2" aria-hidden="true">+</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</aside>
|
||||
644
static/admin/test-auth-flow.html
Normal file
644
static/admin/test-auth-flow.html
Normal file
@@ -0,0 +1,644 @@
|
||||
<!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;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, sans-serif;
|
||||
padding: 20px;
|
||||
background: #f5f5f5;
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
.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
|
||||
</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() {
|
||||
const token = localStorage.getItem('admin_token');
|
||||
const userStr = localStorage.getItem('admin_user');
|
||||
let user = null;
|
||||
|
||||
try {
|
||||
user = userStr ? JSON.parse(userStr) : null;
|
||||
} catch (e) {
|
||||
console.error('Failed to parse user data:', e);
|
||||
}
|
||||
|
||||
document.getElementById('currentUrl').textContent = window.location.href;
|
||||
|
||||
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...');
|
||||
localStorage.clear();
|
||||
console.log('✅ All data cleared');
|
||||
alert('✅ All localStorage data cleared!\n\nCheck console for details.');
|
||||
updateStatus();
|
||||
}
|
||||
|
||||
function navigateToAdmin() {
|
||||
console.log('🚀 Navigating to /admin...');
|
||||
window.location.href = '/admin';
|
||||
}
|
||||
|
||||
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();
|
||||
alert('Check console and status panel for auth details.');
|
||||
}
|
||||
|
||||
function setExpiredToken() {
|
||||
const expiredToken = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyLCJleHAiOjE1MTYyMzkwMjJ9.invalidexpiredtoken';
|
||||
console.log('⚠️ Setting expired/invalid token...');
|
||||
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();
|
||||
}
|
||||
|
||||
function setValidToken() {
|
||||
// This is a mock token - won't actually work with backend
|
||||
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();
|
||||
}
|
||||
|
||||
// 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') {
|
||||
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)');
|
||||
}
|
||||
|
||||
if (typeof 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)');
|
||||
}
|
||||
|
||||
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.`);
|
||||
}
|
||||
|
||||
// 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>
|
||||
Reference in New Issue
Block a user