Files
orion/docs/development/customer-authentication-implementation.md
Samir Boulahtit e9253fbd84 refactor: rename Wizamart to Orion across entire codebase
Replace all ~1,086 occurrences of Wizamart/wizamart/WIZAMART/WizaMart
with Orion/orion/ORION across 184 files. This includes database
identifiers, email addresses, domain references, R2 bucket names,
DNS prefixes, encryption salt, Celery app name, config defaults,
Docker configs, CI configs, documentation, seed data, and templates.

Renames homepage-wizamart.html template to homepage-orion.html.
Fixes duplicate file_pattern key in api.yaml architecture rule.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-14 16:46:56 +01:00

20 KiB

Customer Authentication Implementation

Date: 2025-11-24 Status: Completed

Overview

This document describes the implementation of customer authentication for the shop frontend, including login, registration, and account management pages. This work creates a complete separation between customer authentication and admin/store authentication systems.

Problem Statement

The shop frontend needed proper authentication pages (login, registration, forgot password) and a working customer authentication system. The initial implementation had several issues:

  1. No styled authentication pages for customers
  2. Customer authentication was incorrectly trying to use the User model (admins/stores)
  3. Cookie paths were hardcoded and didn't work with multi-access routing (domain, subdomain, path-based)
  4. Store detection method was inconsistent between direct path access and API calls via referer

Solution Architecture

1. Customer vs User Separation

Key Insight: Customers are NOT users. They are a separate entity in the system.

  • Users (models/database/user.py): Admin and store accounts

    • Have role field (admin/store)
    • Have username field
    • Managed via app/services/auth_service.py
  • Customers (models/database/customer.py): Shop customers

    • Store-scoped (each store has independent customers)
    • No role or username fields
    • Have customer_number, total_orders, store relationship
    • Managed via app/services/customer_service.py

2. JWT Token Structure

Customer tokens have a distinct structure:

{
    "sub": str(customer.id),           # Customer ID
    "email": customer.email,
    "store_id": store_id,            # Important: Store isolation
    "type": "customer",                # CRITICAL: Distinguishes from User tokens
    "exp": expire_timestamp,
    "iat": issued_at_timestamp,
}

User tokens have type implicitly set to user role (admin/store) and different payload structure.

Cookies must be set with paths that match how the store is accessed:

Access Method Example URL Cookie Path
Domain orion.lu/shop/account/login /shop
Subdomain orion.localhost/shop/account/login /shop
Path-based localhost/stores/orion/shop/account/login /stores/orion/shop

This ensures cookies are only sent to the correct store's routes.

Implementation Details

Files Created

  1. app/templates/shop/account/login.html

    • Customer login page
    • Extends shop/base.html (follows design pattern)
    • Uses Tailwind CSS and Alpine.js
    • Theme-aware styling with CSS variables
    • Two-column layout (branding + form)
    • Form validation and error handling
  2. app/templates/shop/account/register.html

    • Customer registration page
    • Fields: first_name, last_name, email, phone (optional), password
    • Client-side validation
    • Marketing consent checkbox
    • Theme integration
  3. app/templates/shop/account/forgot-password.html

    • Password reset request page
    • Two-state UI (form → success)
    • Email validation
  4. app/templates/shop/account/dashboard.html

    • Customer account dashboard
    • Displays account summary, order statistics
    • Quick links to orders, profile, addresses
    • Logout functionality
    • Follows shop design pattern (extends base.html)

Important Schema Change

models/schema/auth.py - Unified Login Schema

Changed the UserLogin schema to use email_or_username instead of username to support both username and email login across all contexts (admin, store, and customer).

Before:

class UserLogin(BaseModel):
    username: str
    password: str

After:

class UserLogin(BaseModel):
    email_or_username: str = Field(..., description="Username or email address")
    password: str
    store_code: Optional[str] = Field(None, description="Optional store code for context")

Impact: This change affects all login endpoints:

  • Admin login: /api/v1/admin/auth/login
  • Store login: /api/v1/store/auth/login
  • Customer login: /api/v1/shop/auth/login

Updated Files:

  • app/services/auth_service.py - Changed user_credentials.username to user_credentials.email_or_username
  • app/api/v1/admin/auth.py - Updated logging to use email_or_username
  • static/admin/js/login.js - Send email_or_username in payload
  • static/store/js/login.js - Send email_or_username in payload

Files Modified

1. app/api/v1/shop/auth.py

Changes:

  • Added CustomerLoginResponse model (uses CustomerResponse instead of UserResponse)
  • Updated customer_login endpoint to:
    • Calculate cookie path dynamically based on store access method
    • Set cookie with correct path for multi-access support
    • Return CustomerLoginResponse with proper customer data
  • Updated customer_logout endpoint to calculate cookie path dynamically

Key Code:

# Calculate cookie path based on store access method
store_context = getattr(request.state, 'store_context', None)
access_method = store_context.get('detection_method', 'unknown') if store_context else 'unknown'

cookie_path = "/shop"  # Default for domain/subdomain access
if access_method == "path":
    # For path-based access like /stores/orion/shop
    full_prefix = store_context.get('full_prefix', '/store/') if store_context else '/store/'
    cookie_path = f"{full_prefix}{store.subdomain}/shop"

response.set_cookie(
    key="customer_token",
    value=token,
    httponly=True,
    secure=should_use_secure_cookies(),
    samesite="lax",
    max_age=expires_in,
    path=cookie_path,  # Dynamic path
)

2. app/services/customer_service.py

Changes:

  • Updated login_customer to create JWT tokens directly using auth_manager
  • No longer tries to use auth_service.create_access_token() (that's for Users only)
  • Directly uses jose.jwt.encode() with custom customer payload

Key Code:

from jose import jwt
from datetime import datetime, timedelta, timezone

auth_manager = self.auth_service.auth_manager
expires_delta = timedelta(minutes=auth_manager.token_expire_minutes)
expire = datetime.now(timezone.utc) + expires_delta

payload = {
    "sub": str(customer.id),
    "email": customer.email,
    "store_id": store_id,
    "type": "customer",  # Critical distinction
    "exp": expire,
    "iat": datetime.now(timezone.utc),
}

token = jwt.encode(payload, auth_manager.secret_key, algorithm=auth_manager.algorithm)

3. app/api/deps.py

Major Rewrite: get_current_customer_from_cookie_or_header

Before: Tried to validate customer tokens as User tokens, expected role field

After:

  • Decodes JWT manually
  • Validates type == "customer" in payload
  • Loads Customer from database (not User)
  • Returns Customer object

Key Code:

def get_current_customer_from_cookie_or_header(
        request: Request,
        credentials: Optional[HTTPAuthorizationCredentials] = Depends(security),
        customer_token: Optional[str] = Cookie(None),
        db: Session = Depends(get_db),
):
    from models.database.customer import Customer
    from jose import jwt, JWTError

    token, source = _get_token_from_request(...)

    if not token:
        raise InvalidTokenException("Customer authentication required")

    # Decode and validate customer JWT token
    payload = jwt.decode(token, auth_manager.secret_key, algorithms=[auth_manager.algorithm])

    # Verify this is a customer token
    if payload.get("type") != "customer":
        raise InvalidTokenException("Customer authentication required")

    customer_id = payload.get("sub")
    customer = db.query(Customer).filter(Customer.id == int(customer_id)).first()

    if not customer or not customer.is_active:
        raise InvalidTokenException("Customer not found or inactive")

    return customer  # Returns Customer, not User

4. app/routes/shop_pages.py

Changes:

  • Changed import from User to Customer
  • Updated all protected route handlers:
    • Changed parameter type from current_user: User to current_customer: Customer
    • Updated function calls from user=current_user to user=current_customer

Affected Routes:

  • /account/dashboard
  • /account/orders
  • /account/orders/{order_id}
  • /account/profile
  • /account/addresses
  • /account/wishlist
  • /account/reviews

5. middleware/store_context.py

Critical Fix: Harmonized store detection methods

Problem:

  • Direct page access: detection_method = "path"
  • API call via referer: detection_method = "referer_path"
  • This inconsistency broke cookie path calculation

Solution: When detecting store from referer path, use the same detection_method = "path" and include the same fields (full_prefix, path_prefix) as direct path detection.

Key Code:

# Method 1: Path-based detection from referer path
if referer_path.startswith("/stores/") or referer_path.startswith("/store/"):
    prefix = "/stores/" if referer_path.startswith("/stores/") else "/store/"
    path_parts = referer_path[len(prefix):].split("/")
    if len(path_parts) >= 1 and path_parts[0]:
        store_code = path_parts[0]
        prefix_len = len(prefix)

        # Use "path" as detection_method to be consistent with direct path detection
        return {
            "subdomain": store_code,
            "detection_method": "path",  # Consistent!
            "path_prefix": referer_path[:prefix_len + len(store_code)],
            "full_prefix": prefix,
            "host": referer_host,
            "referer": referer,
        }

6. app/exceptions/handler.py

Changes:

  • Password sanitization in validation error logging
  • Already had proper redirect logic for customer login (no changes needed)

7. models/schema/auth.py

Changes:

  • Updated UserLogin schema to accept email_or_username instead of username
  • This allows customers to login with email (they don't have usernames)

Multi-Access Routing Support

The implementation properly supports all three store access methods:

Domain-based Access

URL: https://orion.lu/shop/account/login
Cookie Path: /shop
Cookie Sent To: https://orion.lu/shop/*

Subdomain-based Access

URL: https://orion.myplatform.com/shop/account/login
Cookie Path: /shop
Cookie Sent To: https://orion.myplatform.com/shop/*

Path-based Access

URL: https://myplatform.com/stores/orion/shop/account/login
Cookie Path: /stores/orion/shop
Cookie Sent To: https://myplatform.com/stores/orion/shop/*

Authentication Flow

Login Flow

  1. User loads login pageGET /stores/orion/shop/account/login

    • Middleware detects store from path
    • Sets detection_method = "path" in store_context
    • Renders login template
  2. User submits credentialsPOST /api/v1/shop/auth/login

    • Middleware detects store from Referer header
    • Sets detection_method = "path" (harmonized!)
    • Validates credentials via customer_service.login_customer()
    • Creates JWT token with type: "customer"
    • Calculates cookie path based on access method
    • Sets customer_token cookie with correct path
    • Returns token + customer data
  3. Browser redirects to dashboardGET /stores/orion/shop/account/dashboard

    • Browser sends customer_token cookie (path matches!)
    • Dependency get_current_customer_from_cookie_or_header extracts token
    • Decodes JWT, validates type == "customer"
    • Loads Customer from database
    • Renders dashboard with customer data

Logout Flow

  1. User clicks logout button → Shows Tailwind modal confirmation

    • Custom modal (not browser confirm dialog)
    • Alpine.js state management
    • Smooth animations with transitions
    • Dark mode support
  2. User confirms logoutPOST /api/v1/shop/auth/logout

    • Calculates cookie path (same logic as login)
    • Deletes cookie with matching path
    • Returns success message
  3. Frontend redirects to login page

    • Shows success toast notification
    • Clears localStorage token
    • Redirects after 500ms delay

Security Features

response.set_cookie(
    key="customer_token",
    value=token,
    httponly=True,        # JavaScript cannot access (XSS protection)
    secure=True,          # HTTPS only (production/staging)
    samesite="lax",       # CSRF protection
    max_age=1800,         # 30 minutes (matches JWT expiry)
    path=cookie_path,     # Restricted to store's shop routes
)

Token Validation

  • JWT expiration checked
  • Customer active status verified
  • Token type validated (type == "customer")
  • Store isolation enforced (customer must belong to store)

Password Security

  • Bcrypt hashing via auth_manager.hash_password()
  • Validation errors sanitized (passwords never logged)
  • Minimum password length enforced

Design Patterns Followed

Frontend Templates

All authentication pages follow the shop template pattern:

{% extends "shop/base.html" %}

{% block title %}Page Title{% endblock %}

{% block alpine_data %}componentName(){% endblock %}

{% block content %}
<!-- Page content -->
{% endblock %}

{% block extra_scripts %}
<script>
function componentName() {
    return {
        ...shopLayoutData(),
        // Component-specific data/methods
    }
}
</script>
{% endblock %}

Benefits:

  • Consistent header/footer/navigation
  • Theme CSS variables automatically injected
  • Dark mode support
  • Mobile responsive
  • Alpine.js component pattern

Service Layer

  • Customer operations in customer_service.py
  • Auth operations in auth_service.py
  • Clear separation of concerns
  • Database operations via SQLAlchemy ORM

Exception Handling

  • Custom exceptions for customer-specific errors
  • Consistent error responses (JSON for API, HTML for pages)
  • Automatic redirect to login on 401 for HTML page requests

UI Components

Logout Confirmation Modal

Custom Tailwind CSS modal for logout confirmation instead of browser's native confirm() dialog.

Features:

  • Beautiful animated modal with backdrop overlay
  • Warning icon (red triangle with exclamation mark)
  • Clear confirmation message
  • Two action buttons: "Logout" (red) and "Cancel" (gray)
  • Dark mode support
  • Mobile responsive
  • Keyboard accessible (ARIA attributes)
  • Click backdrop to dismiss

Implementation:

<!-- Modal trigger -->
<button @click="showLogoutModal = true">Logout</button>

<!-- Modal component -->
<div x-show="showLogoutModal" x-cloak class="fixed inset-0 z-50">
    <!-- Backdrop with fade animation -->
    <div x-show="showLogoutModal"
         x-transition:enter="ease-out duration-300"
         x-transition:enter-start="opacity-0"
         x-transition:enter-end="opacity-100"
         @click="showLogoutModal = false"
         class="fixed inset-0 bg-gray-500 bg-opacity-75">
    </div>

    <!-- Modal panel with slide+scale animation -->
    <div x-show="showLogoutModal"
         x-transition:enter="ease-out duration-300"
         x-transition:enter-start="opacity-0 scale-95"
         x-transition:enter-end="opacity-100 scale-100"
         class="bg-white dark:bg-gray-800 rounded-lg">
        <!-- Modal content -->
    </div>
</div>

Alpine.js Component:

function accountDashboard() {
    return {
        ...shopLayoutData(),
        showLogoutModal: false,  // Modal state

        confirmLogout() {
            this.showLogoutModal = false;
            // Perform logout API call
            // Show toast notification
            // Redirect to login
        }
    }
}

Why Custom Modal vs Browser Confirm:

  • Consistent with design system
  • Customizable styling and animations
  • Dark mode support
  • Better mobile experience
  • More professional appearance
  • Accessible (ARIA labels, keyboard navigation)
  • Browser confirm: Cannot be styled, looks dated, poor mobile UX

Testing Checklist

  • Customer can register new account
  • Customer can login with email/password
  • Admin can login with username (using unified schema)
  • Store can login with username (using unified schema)
  • Cookie is set with correct path for path-based access
  • Cookie is sent on subsequent requests to dashboard
  • Customer authentication dependency validates token correctly
  • Dashboard loads with customer data
  • Customer can logout
  • Logout confirmation modal displays correctly
  • Modal has smooth animations and transitions
  • Modal supports dark mode
  • Toast notification shows on logout
  • Cookie is properly deleted on logout
  • Unauthorized access redirects to login
  • Theme styling is applied correctly
  • Dark mode works
  • Mobile responsive layout
  • Admin/store login button spinner aligns correctly

Known Limitations

  1. Password Reset Not Implemented: forgot-password and reset-password endpoints are placeholders (TODO comments in code)

  2. Email Verification Not Implemented: Customers are immediately active after registration

  3. Session Management: No refresh tokens, single JWT with 30-minute expiry

  4. Account Pages Are Placeholders:

    • /account/orders - needs order history implementation
    • /account/profile - needs profile editing implementation
    • /account/addresses - needs address management implementation

Future Enhancements

  1. Password Reset Flow:

    • Generate secure reset tokens
    • Send password reset emails
    • Token expiry and validation
    • Password reset form
  2. Email Verification:

    • Send verification email on registration
    • Verification token validation
    • Resend verification email
  3. Account Management:

    • Edit profile (name, email, phone)
    • Change password
    • Manage addresses (CRUD)
    • View order history with filtering/search
    • Order tracking
  4. Security Enhancements:

    • Refresh tokens for longer sessions
    • Rate limiting on login/registration
    • Account lockout after failed attempts
    • 2FA/MFA support
  5. User Experience:

    • Remember me functionality
    • Social login (OAuth)
    • Progressive disclosure of forms
    • Better error messages

References

  • Customer Model: models/database/customer.py
  • Customer Service: app/services/customer_service.py
  • Auth Endpoints: app/api/v1/shop/auth.py
  • Auth Dependencies: app/api/deps.py
  • Shop Routes: app/routes/shop_pages.py
  • Store Context: middleware/store_context.py
  • Templates: app/templates/shop/account/

Deployment Notes

Environment Variables

No new environment variables required. Uses existing:

  • JWT_SECRET_KEY - for token signing
  • JWT_EXPIRE_MINUTES - token expiry (default: 30)
  • ENVIRONMENT - for secure cookie setting

Database

No migrations required. Uses existing customer table.

Static Files

Ensure these files exist:

  • static/shared/js/log-config.js
  • static/shared/js/icons.js
  • static/shop/js/shop-layout.js
  • static/shared/js/utils.js
  • static/shared/js/api-client.js
  • static/shop/css/shop.css

Troubleshooting

Issue: "No token for path"

Cause: Cookie path doesn't match request path

Solution:

  • Check store context middleware is running
  • Verify detection_method is set correctly
  • Confirm cookie path calculation includes store subdomain for path-based access

Issue: "Invalid token type"

Cause: Trying to use User token for customer route or vice versa

Solution:

  • Ensure customer login creates token with type: "customer"
  • Verify dependency checks type == "customer"

Cause: Cookie path doesn't match or cookie expired

Solution:

  • Check browser DevTools → Application → Cookies
  • Verify cookie path matches request URL
  • Check cookie expiry timestamp

Summary

This implementation establishes a complete customer authentication system that is:

Secure: HTTP-only cookies, CSRF protection, password hashing Scalable: Multi-tenant with store isolation Flexible: Supports domain, subdomain, and path-based access Maintainable: Clear separation of concerns, follows established patterns User-Friendly: Responsive design, theme integration, proper UX flows

The key architectural decision was recognizing that customers and users are fundamentally different entities requiring separate authentication flows, token structures, and database models.