feat: add customer authentication pages and documentation
Add complete customer authentication UI with login, registration, forgot password, and dashboard pages. Templates Added: - app/templates/shop/account/login.html - Two-column layout with vendor branding - Email/password login with validation - Password visibility toggle - "Remember me" functionality - Error/success alerts - Loading states with spinner - app/templates/shop/account/register.html - Customer registration form - Client-side validation (password strength, email format) - Marketing consent checkbox - Confirm password matching - app/templates/shop/account/forgot-password.html - Password reset request page - Email validation - Success confirmation - app/templates/shop/account/dashboard.html - Customer account dashboard - Overview of orders, profile, addresses Styles Added: - static/shared/css/auth.css - Authentication page styling - Two-column layout system - Form components and validation states - Theme-aware with CSS variables - Dark mode support - Mobile responsive - static/shared/css/base.css updates - Enhanced utility classes - Additional form styles - Improved button states Documentation Added: - docs/frontend/shop/authentication-pages.md - Comprehensive guide to auth page implementation - Component architecture - API integration patterns - Theme customization - docs/development/CUSTOMER_AUTHENTICATION_IMPLEMENTATION.md - Implementation details and technical decisions - Security considerations - Testing procedures - docs/development/CUSTOMER_AUTH_SUMMARY.md - Quick reference guide - Endpoints and flows - Updated docs/frontend/shop/architecture.md - Added authentication section - Documented all auth pages - Updated docs/frontend/shop/page-templates.md - Added auth template documentation - Updated mkdocs.yml - Added new documentation pages to navigation Features: - Full theme integration with vendor branding - Alpine.js reactive components - Tailwind CSS utility-first styling - Client and server-side validation - JWT token management - Multi-access routing support (domain/subdomain/path) - Error handling with user-friendly messages - Loading states and animations - Mobile responsive design - Dark mode support 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -1,127 +1,177 @@
|
||||
{# app/templates/shop/account/login.html #}
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<html :class="{ 'theme-dark': dark }" x-data="customerLogin()" lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title><!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Login - {{ vendor.name }}</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>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Customer Login - {{ vendor.name }}</title>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800&display=swap" rel="stylesheet" />
|
||||
|
||||
{# CRITICAL: Inject theme CSS variables #}
|
||||
<style id="vendor-theme-variables">
|
||||
:root {
|
||||
{% for key, value in theme.css_variables.items() %}
|
||||
{{ key }}: {{ value }};
|
||||
{% endfor %}
|
||||
}
|
||||
|
||||
{# Custom CSS from vendor theme #}
|
||||
{% if theme.custom_css %}
|
||||
{{ theme.custom_css | safe }}
|
||||
{% endif %}
|
||||
|
||||
/* Theme-aware button and focus colors */
|
||||
.btn-primary-theme {
|
||||
background-color: var(--color-primary);
|
||||
}
|
||||
.btn-primary-theme:hover:not(:disabled) {
|
||||
background-color: var(--color-primary-dark, var(--color-primary));
|
||||
filter: brightness(0.9);
|
||||
}
|
||||
.focus-primary:focus {
|
||||
border-color: var(--color-primary);
|
||||
box-shadow: 0 0 0 3px rgba(var(--color-primary-rgb, 124, 58, 237), 0.1);
|
||||
}
|
||||
[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') }}';">
|
||||
</head>
|
||||
<body class="auth-page">
|
||||
<div class="login-container"
|
||||
x-data="customerLogin()"
|
||||
x-init="checkRegistrationSuccess()"
|
||||
data-vendor-id="{{ vendor.id }}"
|
||||
data-vendor-name="{{ vendor.name }}"
|
||||
>
|
||||
<!-- Header -->
|
||||
<div class="login-header">
|
||||
{% if vendor.logo_url %}
|
||||
<img src="{{ vendor.logo_url }}" alt="{{ vendor.name }}" class="auth-logo">
|
||||
{% else %}
|
||||
<div class="auth-logo">🛒</div>
|
||||
{% endif %}
|
||||
<h1>Welcome Back</h1>
|
||||
<p>Sign in to {{ vendor.name }}</p>
|
||||
</div>
|
||||
|
||||
<!-- Alert Box -->
|
||||
<div x-show="alert.show"
|
||||
x-transition
|
||||
:class="'alert alert-' + alert.type"
|
||||
x-text="alert.message"
|
||||
></div>
|
||||
|
||||
<!-- Login Form -->
|
||||
<form @submit.prevent="handleLogin">
|
||||
<!-- Email -->
|
||||
<div class="form-group">
|
||||
<label for="email">Email Address</label>
|
||||
<input
|
||||
type="email"
|
||||
id="email"
|
||||
x-model="credentials.email"
|
||||
required
|
||||
placeholder="your@email.com"
|
||||
:class="{ 'error': errors.email }"
|
||||
@input="clearAllErrors()"
|
||||
>
|
||||
<div x-show="errors.email"
|
||||
x-text="errors.email"
|
||||
class="error-message show"
|
||||
></div>
|
||||
</div>
|
||||
|
||||
<!-- Password -->
|
||||
<div class="form-group">
|
||||
<label for="password">Password</label>
|
||||
<div class="password-group">
|
||||
<input
|
||||
:type="showPassword ? 'text' : 'password'"
|
||||
id="password"
|
||||
x-model="credentials.password"
|
||||
required
|
||||
placeholder="Enter your password"
|
||||
:class="{ 'error': errors.password }"
|
||||
@input="clearAllErrors()"
|
||||
>
|
||||
<button
|
||||
type="button"
|
||||
class="password-toggle"
|
||||
@click="showPassword = !showPassword"
|
||||
>
|
||||
<span x-text="showPassword ? '👁️' : '👁️🗨️'"></span>
|
||||
</button>
|
||||
<body>
|
||||
<div class="flex items-center min-h-screen p-6 bg-gray-50 dark:bg-gray-900" x-cloak>
|
||||
<div class="flex-1 h-full max-w-4xl mx-auto overflow-hidden bg-white rounded-lg shadow-xl dark:bg-gray-800">
|
||||
<div class="flex flex-col overflow-y-auto md:flex-row">
|
||||
<!-- Left side - Image/Branding with Theme Colors -->
|
||||
<div class="h-32 md:h-auto md:w-1/2 flex items-center justify-center"
|
||||
style="background-color: var(--color-primary);">
|
||||
<div class="text-center p-8">
|
||||
{% if theme.branding.logo %}
|
||||
<img src="{{ theme.branding.logo }}"
|
||||
alt="{{ vendor.name }}"
|
||||
class="mx-auto mb-4 max-w-xs max-h-32 object-contain" />
|
||||
{% else %}
|
||||
<div class="text-6xl mb-4">🛒</div>
|
||||
{% endif %}
|
||||
<h2 class="text-2xl font-bold text-white mb-2">{{ vendor.name }}</h2>
|
||||
<p class="text-white opacity-90">Welcome back to your shopping experience</p>
|
||||
</div>
|
||||
</div>
|
||||
<div x-show="errors.password"
|
||||
x-text="errors.password"
|
||||
class="error-message show"
|
||||
></div>
|
||||
</div>
|
||||
|
||||
<!-- Remember Me & Forgot Password -->
|
||||
<div class="form-options">
|
||||
<div class="remember-me">
|
||||
<input
|
||||
type="checkbox"
|
||||
id="rememberMe"
|
||||
x-model="rememberMe"
|
||||
>
|
||||
<label for="rememberMe">Remember me</label>
|
||||
<!-- Right side - Login Form -->
|
||||
<div class="flex items-center justify-center p-6 sm:p-12 md:w-1/2">
|
||||
<div class="w-full">
|
||||
<h1 class="mb-4 text-xl font-semibold text-gray-700 dark:text-gray-200">
|
||||
Customer Login
|
||||
</h1>
|
||||
|
||||
<!-- Success Message (after registration) -->
|
||||
<div x-show="alert.show && alert.type === 'success'"
|
||||
x-text="alert.message"
|
||||
class="px-4 py-3 mb-4 text-sm text-green-700 bg-green-100 rounded-lg dark:bg-green-200 dark:text-green-800"
|
||||
x-transition></div>
|
||||
|
||||
<!-- Error Message -->
|
||||
<div x-show="alert.show && alert.type === 'error'"
|
||||
x-text="alert.message"
|
||||
class="px-4 py-3 mb-4 text-sm text-red-700 bg-red-100 rounded-lg dark:bg-red-200 dark:text-red-800"
|
||||
x-transition></div>
|
||||
|
||||
<!-- Login Form -->
|
||||
<form @submit.prevent="handleLogin">
|
||||
<label class="block text-sm">
|
||||
<span class="text-gray-700 dark:text-gray-400">Email Address</span>
|
||||
<input x-model="credentials.email"
|
||||
:disabled="loading"
|
||||
@input="clearAllErrors"
|
||||
type="email"
|
||||
class="block w-full mt-1 text-sm dark:border-gray-600 dark:bg-gray-700 focus-primary focus:outline-none dark:text-gray-300 form-input rounded-md border-gray-300"
|
||||
:class="{ 'border-red-600': errors.email }"
|
||||
placeholder="your@email.com"
|
||||
autocomplete="email"
|
||||
required />
|
||||
<span x-show="errors.email" x-text="errors.email"
|
||||
class="text-xs text-red-600 dark:text-red-400 mt-1"></span>
|
||||
</label>
|
||||
|
||||
<label class="block mt-4 text-sm">
|
||||
<span class="text-gray-700 dark:text-gray-400">Password</span>
|
||||
<div class="relative">
|
||||
<input x-model="credentials.password"
|
||||
:disabled="loading"
|
||||
@input="clearAllErrors"
|
||||
:type="showPassword ? 'text' : 'password'"
|
||||
class="block w-full mt-1 text-sm dark:border-gray-600 dark:bg-gray-700 focus-primary focus:outline-none dark:text-gray-300 form-input rounded-md border-gray-300"
|
||||
:class="{ 'border-red-600': errors.password }"
|
||||
placeholder="Enter your password"
|
||||
autocomplete="current-password"
|
||||
required />
|
||||
<button type="button"
|
||||
@click="showPassword = !showPassword"
|
||||
class="absolute right-3 top-1/2 transform -translate-y-1/2 text-gray-400 hover:text-gray-600 dark:hover:text-gray-300">
|
||||
<span x-text="showPassword ? '👁️' : '👁️🗨️'"></span>
|
||||
</button>
|
||||
</div>
|
||||
<span x-show="errors.password" x-text="errors.password"
|
||||
class="text-xs text-red-600 dark:text-red-400 mt-1"></span>
|
||||
</label>
|
||||
|
||||
<!-- Remember Me & Forgot Password -->
|
||||
<div class="flex items-center justify-between mt-4">
|
||||
<label class="flex items-center text-sm">
|
||||
<input type="checkbox"
|
||||
x-model="rememberMe"
|
||||
class="form-checkbox focus-primary focus:outline-none"
|
||||
style="color: var(--color-primary);">
|
||||
<span class="ml-2 text-gray-700 dark:text-gray-400">Remember me</span>
|
||||
</label>
|
||||
<a href="{{ base_url }}shop/account/forgot-password"
|
||||
class="text-sm font-medium hover:underline"
|
||||
style="color: var(--color-primary);">
|
||||
Forgot password?
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<button type="submit" :disabled="loading"
|
||||
class="btn-primary-theme block w-full px-4 py-2 mt-4 text-sm font-medium leading-5 text-center text-white transition-colors duration-150 border border-transparent rounded-lg focus:outline-none focus:shadow-outline-purple disabled:opacity-50 disabled:cursor-not-allowed">
|
||||
<span x-show="!loading">Sign in</span>
|
||||
<span x-show="loading" class="flex items-center justify-center">
|
||||
<svg class="inline w-4 h-4 mr-2 animate-spin" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
|
||||
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
|
||||
<path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
|
||||
</svg>
|
||||
Signing in...
|
||||
</span>
|
||||
</button>
|
||||
</form>
|
||||
|
||||
<hr class="my-8" />
|
||||
|
||||
<p class="mt-4 text-center">
|
||||
<span class="text-sm text-gray-600 dark:text-gray-400">Don't have an account?</span>
|
||||
<a class="text-sm font-medium hover:underline ml-1"
|
||||
style="color: var(--color-primary);"
|
||||
href="{{ base_url }}shop/account/register">
|
||||
Create an account
|
||||
</a>
|
||||
</p>
|
||||
<p class="mt-2 text-center">
|
||||
<a class="text-sm font-medium text-gray-600 dark:text-gray-400 hover:underline"
|
||||
href="{{ base_url }}shop/">
|
||||
← Continue shopping
|
||||
</a>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<a href="{{ base_url }}shop/account/forgot-password" class="forgot-password">
|
||||
Forgot password?
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<!-- Submit Button -->
|
||||
<button
|
||||
type="submit"
|
||||
class="btn-login"
|
||||
:disabled="loading"
|
||||
>
|
||||
<span x-show="loading" class="loading-spinner"></span>
|
||||
<span x-text="loading ? 'Signing in...' : 'Sign In'"></span>
|
||||
</button>
|
||||
</form>
|
||||
|
||||
<!-- Register Link -->
|
||||
<div class="login-footer">
|
||||
<div class="auth-footer-text">Don't have an account?</div>
|
||||
<a href="{{ base_url }}shop/account/register">Create an account</a>
|
||||
</div>
|
||||
|
||||
<!-- Back to Shop -->
|
||||
<div class="login-footer" style="border-top: none; padding-top: 0;">
|
||||
<a href="{{ base_url }}shop/">← Continue shopping</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Alpine.js v3 -->
|
||||
<script defer src="https://cdn.jsdelivr.net/npm/alpinejs@3.14.0/dist/cdn.min.js"></script>
|
||||
|
||||
<!-- Login Logic -->
|
||||
<script>
|
||||
function customerLogin() {
|
||||
return {
|
||||
@@ -139,33 +189,29 @@
|
||||
type: 'error',
|
||||
message: ''
|
||||
},
|
||||
|
||||
// Get vendor data
|
||||
get vendorId() {
|
||||
return this.$el.dataset.vendorId;
|
||||
dark: false,
|
||||
|
||||
// Initialize
|
||||
init() {
|
||||
this.checkRegistrationSuccess();
|
||||
// Check for dark mode preference
|
||||
this.dark = localStorage.getItem('darkMode') === 'true';
|
||||
},
|
||||
|
||||
get vendorName() {
|
||||
return this.$el.dataset.vendorName;
|
||||
},
|
||||
|
||||
|
||||
// Check if redirected after registration
|
||||
checkRegistrationSuccess() {
|
||||
const urlParams = new URLSearchParams(window.location.search);
|
||||
if (urlParams.get('registered') === 'true') {
|
||||
this.showAlert(
|
||||
'Account created successfully! Please sign in.',
|
||||
'success'
|
||||
);
|
||||
this.showAlert('Account created successfully! Please sign in.', 'success');
|
||||
}
|
||||
},
|
||||
|
||||
|
||||
// Clear errors
|
||||
clearAllErrors() {
|
||||
this.errors = {};
|
||||
this.alert.show = false;
|
||||
},
|
||||
|
||||
|
||||
// Show alert
|
||||
showAlert(message, type = 'error') {
|
||||
this.alert = {
|
||||
@@ -173,63 +219,56 @@
|
||||
type: type,
|
||||
message: message
|
||||
};
|
||||
|
||||
window.scrollTo({ top: 0, behavior: 'smooth' });
|
||||
},
|
||||
|
||||
|
||||
// Handle login
|
||||
async handleLogin() {
|
||||
this.clearAllErrors();
|
||||
|
||||
|
||||
// Basic validation
|
||||
if (!this.credentials.email) {
|
||||
this.errors.email = 'Email is required';
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
if (!this.credentials.password) {
|
||||
this.errors.password = 'Password is required';
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
this.loading = true;
|
||||
|
||||
|
||||
try {
|
||||
const response = await fetch(
|
||||
`/api/v1/shop/auth/login`,
|
||||
{
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify({
|
||||
email_or_username: this.credentials.email,
|
||||
password: this.credentials.password
|
||||
})
|
||||
}
|
||||
);
|
||||
|
||||
const response = await fetch('/api/v1/shop/auth/login', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify({
|
||||
email_or_username: this.credentials.email,
|
||||
password: this.credentials.password
|
||||
})
|
||||
});
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(data.detail || 'Login failed');
|
||||
}
|
||||
|
||||
|
||||
// Store token and user data
|
||||
localStorage.setItem('customer_token', data.access_token);
|
||||
localStorage.setItem('customer_user', JSON.stringify(data.user));
|
||||
|
||||
// Store vendor context
|
||||
localStorage.setItem('customer_vendor_id', this.vendorId);
|
||||
|
||||
|
||||
this.showAlert('Login successful! Redirecting...', 'success');
|
||||
|
||||
// Redirect to account page or cart
|
||||
|
||||
// Redirect to account page or return URL
|
||||
setTimeout(() => {
|
||||
const returnUrl = new URLSearchParams(window.location.search).get('return') || '/shop/account';
|
||||
const returnUrl = new URLSearchParams(window.location.search).get('return') || '{{ base_url }}shop/account';
|
||||
window.location.href = returnUrl;
|
||||
}, 1000);
|
||||
|
||||
|
||||
} catch (error) {
|
||||
console.error('Login error:', error);
|
||||
this.showAlert(error.message || 'Invalid email or password');
|
||||
@@ -241,9 +280,4 @@
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
</html></title>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
</html>
|
||||
|
||||
Reference in New Issue
Block a user