# Password Reset Implementation

## Overview

This document describes the password reset feature for shop customers, allowing them to securely reset their password via email.

---

## Architecture

### Flow Diagram

```
┌──────────────┐    ┌──────────────┐    ┌──────────────┐    ┌──────────────┐
│   Customer   │───▶│ Forgot Page  │───▶│  API: POST   │───▶│ Email Sent   │
│   Clicks     │    │ Enter Email  │    │ /forgot-pwd  │    │ with Link    │
└──────────────┘    └──────────────┘    └──────────────┘    └──────────────┘
                                                                    │
                                                                    ▼
┌──────────────┐    ┌──────────────┐    ┌──────────────┐    ┌──────────────┐
│   Success    │◀───│  API: POST   │◀───│ Reset Page   │◀───│ Customer     │
│   Redirect   │    │ /reset-pwd   │    │ New Password │    │ Clicks Link  │
└──────────────┘    └──────────────┘    └──────────────┘    └──────────────┘
```

---

## Components

### Database Model

**File:** `models/database/password_reset_token.py`

```python
class PasswordResetToken(Base):
    __tablename__ = "password_reset_tokens"

    id: int                     # Primary key
    customer_id: int            # FK to customers.id
    token_hash: str             # SHA256 hash of token
    expires_at: datetime        # Expiration timestamp
    used_at: datetime | None    # When token was used
    created_at: datetime        # Creation timestamp

    TOKEN_EXPIRY_HOURS = 1      # Token valid for 1 hour
```

**Key Methods:**

| Method | Description |
|--------|-------------|
| `create_for_customer(db, customer_id)` | Creates new token, returns plaintext |
| `find_valid_token(db, plaintext_token)` | Finds unexpired, unused token |
| `mark_used(db)` | Marks token as used |

**Security:**
- Tokens are hashed with SHA256 before storage
- Only one active token per customer (old tokens invalidated)
- Tokens expire after 1 hour
- Tokens can only be used once

---

### API Endpoints

**File:** `app/api/v1/shop/auth.py`

#### POST /api/v1/shop/auth/forgot-password

Request a password reset link.

**Request:**
```json
{
  "email": "customer@example.com"
}
```

**Response:** (Always returns success to prevent email enumeration)
```json
{
  "message": "If an account exists with this email, a password reset link has been sent."
}
```

**Implementation:**
```python
@router.post("/auth/forgot-password", response_model=PasswordResetRequestResponse)
def forgot_password(request: Request, email: str, db: Session = Depends(get_db)):
    vendor = getattr(request.state, "vendor", None)
    if not vendor:
        raise VendorNotFoundException("context", identifier_type="subdomain")

    # Look up customer (vendor-scoped)
    customer = customer_service.get_customer_for_password_reset(db, vendor.id, email)

    if customer:
        # Generate token and send email
        plaintext_token = PasswordResetToken.create_for_customer(db, customer.id)
        reset_link = f"{scheme}://{host}/shop/account/reset-password?token={plaintext_token}"

        email_service.send_template(
            template_code="password_reset",
            to_email=customer.email,
            variables={
                "customer_name": customer.first_name,
                "reset_link": reset_link,
                "expiry_hours": str(PasswordResetToken.TOKEN_EXPIRY_HOURS),
            },
            vendor_id=vendor.id,
        )
        db.commit()

    # Always return success (security: don't reveal if email exists)
    return PasswordResetRequestResponse(
        message="If an account exists with this email, a password reset link has been sent."
    )
```

---

#### POST /api/v1/shop/auth/reset-password

Reset password using token from email.

**Request:**
```json
{
  "reset_token": "abc123...",
  "new_password": "newSecurePassword123"
}
```

**Response:**
```json
{
  "message": "Password reset successfully. You can now log in with your new password."
}
```

**Implementation:**
```python
@router.post("/auth/reset-password", response_model=PasswordResetResponse)
def reset_password(
    request: Request, reset_token: str, new_password: str, db: Session = Depends(get_db)
):
    vendor = getattr(request.state, "vendor", None)
    if not vendor:
        raise VendorNotFoundException("context", identifier_type="subdomain")

    # Service handles all validation and password update
    customer = customer_service.validate_and_reset_password(
        db=db,
        vendor_id=vendor.id,
        reset_token=reset_token,
        new_password=new_password,
    )
    db.commit()

    return PasswordResetResponse(
        message="Password reset successfully. You can now log in with your new password."
    )
```

---

### Service Layer

**File:** `app/services/customer_service.py`

#### get_customer_for_password_reset

```python
def get_customer_for_password_reset(
    self, db: Session, vendor_id: int, email: str
) -> Customer | None:
    """Get active customer by email for password reset.

    Returns None if customer doesn't exist or is inactive.
    This method is designed to not reveal whether an email exists.
    """
    return (
        db.query(Customer)
        .filter(
            Customer.vendor_id == vendor_id,
            Customer.email == email.lower(),
            Customer.is_active == True,
        )
        .first()
    )
```

#### validate_and_reset_password

```python
def validate_and_reset_password(
    self,
    db: Session,
    vendor_id: int,
    reset_token: str,
    new_password: str,
) -> Customer:
    """Validate reset token and update customer password.

    Args:
        db: Database session
        vendor_id: Vendor ID (for security verification)
        reset_token: Password reset token from email
        new_password: New password

    Returns:
        Customer: Updated customer

    Raises:
        PasswordTooShortException: If password < 8 characters
        InvalidPasswordResetTokenException: If token invalid/expired
        CustomerNotActiveException: If customer account is inactive
    """
    # Validate password length
    if len(new_password) < 8:
        raise PasswordTooShortException(min_length=8)

    # Find valid token
    token_record = PasswordResetToken.find_valid_token(db, reset_token)
    if not token_record:
        raise InvalidPasswordResetTokenException()

    # Get customer and verify vendor ownership
    customer = db.query(Customer).filter(Customer.id == token_record.customer_id).first()
    if not customer or customer.vendor_id != vendor_id:
        raise InvalidPasswordResetTokenException()

    if not customer.is_active:
        raise CustomerNotActiveException(customer.email)

    # Update password
    hashed_password = self.auth_service.hash_password(new_password)
    customer.hashed_password = hashed_password

    # Mark token as used
    token_record.mark_used(db)

    return customer
```

---

### Exceptions

**File:** `app/exceptions/customer.py`

```python
class InvalidPasswordResetTokenException(ValidationException):
    """Raised when password reset token is invalid or expired."""

    def __init__(self):
        super().__init__(
            message="Invalid or expired password reset link. Please request a new one.",
            field="reset_token",
        )
        self.error_code = "INVALID_RESET_TOKEN"


class PasswordTooShortException(ValidationException):
    """Raised when password doesn't meet minimum length requirement."""

    def __init__(self, min_length: int = 8):
        super().__init__(
            message=f"Password must be at least {min_length} characters long",
            field="password",
            details={"min_length": min_length},
        )
        self.error_code = "PASSWORD_TOO_SHORT"
```

---

### Frontend Pages

#### Forgot Password Page

**Template:** `app/templates/shop/account/forgot-password.html`
**Route:** `/shop/account/forgot-password`

Features:
- Email input form
- Form validation
- Success message display
- Link back to login

#### Reset Password Page

**Template:** `app/templates/shop/account/reset-password.html`
**Route:** `/shop/account/reset-password?token=...`

Features:
- New password input
- Confirm password input
- Password strength indicator
- Password match validation
- Success redirect to login

---

### Email Template

**Code:** `password_reset`
**Category:** AUTH
**Languages:** en, fr, de, lb

**Variables:**
| Variable | Description |
|----------|-------------|
| `customer_name` | Customer's first name |
| `reset_link` | Full URL to reset password page |
| `expiry_hours` | Hours until link expires |

**Example Email:**
```
Subject: Reset Your Password

Hi {{ customer_name }},

We received a request to reset your password.

Click here to reset your password:
{{ reset_link }}

This link will expire in {{ expiry_hours }} hour(s).

If you didn't request this, you can safely ignore this email.
```

---

## Security Considerations

### Token Security

1. **Hashed Storage**: Tokens stored as SHA256 hashes
2. **Short Expiry**: 1 hour expiration
3. **Single Use**: Tokens invalidated after use
4. **One Active Token**: Creating new token invalidates previous

### Email Enumeration Prevention

- Same response whether email exists or not
- Same response timing regardless of email existence

### Vendor Isolation

- Tokens are verified against the requesting vendor
- Prevents cross-vendor token reuse

### Password Requirements

- Minimum 8 characters
- Enforced by service layer exception

---

## Testing Checklist

- [ ] Request reset for existing email (email received)
- [ ] Request reset for non-existent email (same response)
- [ ] Click reset link within expiry (success)
- [ ] Click reset link after expiry (error)
- [ ] Click reset link twice (second fails)
- [ ] Password too short (validation error)
- [ ] Password mismatch on form (client validation)
- [ ] Multi-language email templates
- [ ] Token works only for correct vendor

---

## File Structure

```
├── alembic/versions/
│   └── t8b9c0d1e2f3_add_password_reset_tokens.py
├── app/
│   ├── api/v1/shop/
│   │   └── auth.py
│   ├── exceptions/
│   │   └── customer.py
│   ├── routes/
│   │   └── shop_pages.py
│   ├── services/
│   │   └── customer_service.py
│   └── templates/shop/account/
│       ├── forgot-password.html
│       └── reset-password.html
├── models/
│   ├── database/
│   │   └── password_reset_token.py
│   └── schema/
│       └── auth.py
└── scripts/
    └── seed_email_templates.py
```
