Fixed login redirecting issues

This commit is contained in:
2025-11-21 23:38:03 +01:00
parent 2532a977c1
commit 608fa8b95c
5 changed files with 402 additions and 23 deletions

View File

@@ -674,3 +674,157 @@ def get_user_permissions(
return [] return []
# ============================================================================
# OPTIONAL AUTHENTICATION (For Login Page Redirects)
# ============================================================================
def get_current_admin_optional(
request: Request,
credentials: Optional[HTTPAuthorizationCredentials] = Depends(security),
admin_token: Optional[str] = Cookie(None),
db: Session = Depends(get_db),
) -> Optional[User]:
"""
Get current admin user from admin_token cookie or Authorization header.
Returns None instead of raising exceptions if not authenticated.
Used for login pages to check if user is already authenticated.
Priority:
1. Authorization header (API calls)
2. admin_token cookie (page navigation)
Args:
request: FastAPI request
credentials: Optional Bearer token from header
admin_token: Optional token from admin_token cookie
db: Database session
Returns:
User: Authenticated admin user if valid token exists
None: If no token, invalid token, or user is not admin
"""
token, source = _get_token_from_request(
credentials,
admin_token,
"admin_token",
str(request.url.path)
)
if not token:
return None
try:
# Validate token and get user
user = _validate_user_token(token, db)
# Verify user is admin
if user.role == "admin":
return user
except Exception:
# Invalid token or other error
pass
return None
def get_current_vendor_optional(
request: Request,
credentials: Optional[HTTPAuthorizationCredentials] = Depends(security),
vendor_token: Optional[str] = Cookie(None),
db: Session = Depends(get_db),
) -> Optional[User]:
"""
Get current vendor user from vendor_token cookie or Authorization header.
Returns None instead of raising exceptions if not authenticated.
Used for login pages to check if user is already authenticated.
Priority:
1. Authorization header (API calls)
2. vendor_token cookie (page navigation)
Args:
request: FastAPI request
credentials: Optional Bearer token from header
vendor_token: Optional token from vendor_token cookie
db: Database session
Returns:
User: Authenticated vendor user if valid token exists
None: If no token, invalid token, or user is not vendor
"""
token, source = _get_token_from_request(
credentials,
vendor_token,
"vendor_token",
str(request.url.path)
)
if not token:
return None
try:
# Validate token and get user
user = _validate_user_token(token, db)
# Verify user is vendor
if user.role == "vendor":
return user
except Exception:
# Invalid token or other error
pass
return None
def get_current_customer_optional(
request: Request,
credentials: Optional[HTTPAuthorizationCredentials] = Depends(security),
customer_token: Optional[str] = Cookie(None),
db: Session = Depends(get_db),
) -> Optional[User]:
"""
Get current customer user from customer_token cookie or Authorization header.
Returns None instead of raising exceptions if not authenticated.
Used for login pages to check if user is already authenticated.
Priority:
1. Authorization header (API calls)
2. customer_token cookie (page navigation)
Args:
request: FastAPI request
credentials: Optional Bearer token from header
customer_token: Optional token from customer_token cookie
db: Database session
Returns:
User: Authenticated customer user if valid token exists
None: If no token, invalid token, or user is not customer
"""
token, source = _get_token_from_request(
credentials,
customer_token,
"customer_token",
str(request.url.path)
)
if not token:
return None
try:
# Validate token and get user
user = _validate_user_token(token, db)
# Verify user is customer
if user.role == "customer":
return user
except Exception:
# Invalid token or other error
pass
return None

View File

@@ -27,8 +27,13 @@ from fastapi import APIRouter, Request, Depends, Path
from fastapi.responses import HTMLResponse, RedirectResponse from fastapi.responses import HTMLResponse, RedirectResponse
from fastapi.templating import Jinja2Templates from fastapi.templating import Jinja2Templates
from sqlalchemy.orm import Session from sqlalchemy.orm import Session
from typing import Optional
from app.api.deps import get_current_admin_from_cookie_or_header, get_db from app.api.deps import (
get_current_admin_from_cookie_or_header,
get_current_admin_optional,
get_db
)
from models.database.user import User from models.database.user import User
router = APIRouter() router = APIRouter()
@@ -40,23 +45,37 @@ templates = Jinja2Templates(directory="app/templates")
# ============================================================================ # ============================================================================
@router.get("/", response_class=RedirectResponse, include_in_schema=False) @router.get("/", response_class=RedirectResponse, include_in_schema=False)
async def admin_root(): async def admin_root(
current_user: Optional[User] = Depends(get_current_admin_optional)
):
""" """
Redirect /admin/ to /admin/login. Redirect /admin/ based on authentication status.
Simple approach: - Authenticated admin users → /admin/dashboard
- Unauthenticated users → see login form - Unauthenticated users → /admin/login
- Authenticated users → login page shows form (they can navigate to dashboard)
""" """
if current_user:
# User is already logged in as admin, redirect to dashboard
return RedirectResponse(url="/admin/dashboard", status_code=302)
return RedirectResponse(url="/admin/login", status_code=302) return RedirectResponse(url="/admin/login", status_code=302)
@router.get("/login", response_class=HTMLResponse, include_in_schema=False) @router.get("/login", response_class=HTMLResponse, include_in_schema=False)
async def admin_login_page(request: Request): async def admin_login_page(
request: Request,
current_user: Optional[User] = Depends(get_current_admin_optional)
):
""" """
Render admin login page. Render admin login page.
No authentication required.
If user is already authenticated as admin, redirect to dashboard.
Otherwise, show login form.
""" """
if current_user:
# User is already logged in as admin, redirect to dashboard
return RedirectResponse(url="/admin/dashboard", status_code=302)
return templates.TemplateResponse( return templates.TemplateResponse(
"admin/login.html", "admin/login.html",
{"request": request} {"request": request}

View File

@@ -24,8 +24,12 @@ Routes:
from fastapi import APIRouter, Request, Depends, Path from fastapi import APIRouter, Request, Depends, Path
from fastapi.responses import HTMLResponse, RedirectResponse from fastapi.responses import HTMLResponse, RedirectResponse
from fastapi.templating import Jinja2Templates from fastapi.templating import Jinja2Templates
from typing import Optional
from app.api.deps import get_current_vendor_from_cookie_or_header from app.api.deps import (
get_current_vendor_from_cookie_or_header,
get_current_vendor_optional
)
from models.database.user import User from models.database.user import User
router = APIRouter() router = APIRouter()
@@ -46,28 +50,44 @@ async def vendor_root_no_slash(vendor_code: str = Path(..., description="Vendor
@router.get("/{vendor_code}/", response_class=RedirectResponse, include_in_schema=False) @router.get("/{vendor_code}/", response_class=RedirectResponse, include_in_schema=False)
async def vendor_root(vendor_code: str = Path(..., description="Vendor code")): async def vendor_root(
vendor_code: str = Path(..., description="Vendor code"),
current_user: Optional[User] = Depends(get_current_vendor_optional)
):
""" """
Redirect /vendor/{code}/ to login page. Redirect /vendor/{code}/ based on authentication status.
Simple approach - let login page handle authenticated redirects.
- Authenticated vendor users → /vendor/{code}/dashboard
- Unauthenticated users → /vendor/{code}/login
""" """
if current_user:
# User is already logged in as vendor, redirect to dashboard
return RedirectResponse(url=f"/vendor/{vendor_code}/dashboard", status_code=302)
return RedirectResponse(url=f"/vendor/{vendor_code}/login", status_code=302) return RedirectResponse(url=f"/vendor/{vendor_code}/login", status_code=302)
@router.get("/{vendor_code}/login", response_class=HTMLResponse, include_in_schema=False) @router.get("/{vendor_code}/login", response_class=HTMLResponse, include_in_schema=False)
async def vendor_login_page( async def vendor_login_page(
request: Request, request: Request,
vendor_code: str = Path(..., description="Vendor code") vendor_code: str = Path(..., description="Vendor code"),
current_user: Optional[User] = Depends(get_current_vendor_optional)
): ):
""" """
Render vendor login page. Render vendor login page.
No authentication required.
If user is already authenticated as vendor, redirect to dashboard.
Otherwise, show login form.
JavaScript will: JavaScript will:
- Load vendor info via API - Load vendor info via API
- Handle login form submission - Handle login form submission
- Redirect to dashboard on success - Redirect to dashboard on success
""" """
if current_user:
# User is already logged in as vendor, redirect to dashboard
return RedirectResponse(url=f"/vendor/{vendor_code}/dashboard", status_code=302)
return templates.TemplateResponse( return templates.TemplateResponse(
"vendor/login.html", "vendor/login.html",
{ {
@@ -97,7 +117,7 @@ async def vendor_dashboard_page(
- Handle all interactivity - Handle all interactivity
""" """
return templates.TemplateResponse( return templates.TemplateResponse(
"vendor/admin/dashboard.html", "vendor/dashboard.html",
{ {
"request": request, "request": request,
"user": current_user, "user": current_user,
@@ -121,7 +141,7 @@ async def vendor_products_page(
JavaScript loads product list via API. JavaScript loads product list via API.
""" """
return templates.TemplateResponse( return templates.TemplateResponse(
"vendor/admin/products.html", "vendor/products.html",
{ {
"request": request, "request": request,
"user": current_user, "user": current_user,
@@ -145,7 +165,7 @@ async def vendor_orders_page(
JavaScript loads order list via API. JavaScript loads order list via API.
""" """
return templates.TemplateResponse( return templates.TemplateResponse(
"vendor/admin/orders.html", "vendor/orders.html",
{ {
"request": request, "request": request,
"user": current_user, "user": current_user,
@@ -169,7 +189,7 @@ async def vendor_customers_page(
JavaScript loads customer list via API. JavaScript loads customer list via API.
""" """
return templates.TemplateResponse( return templates.TemplateResponse(
"vendor/admin/customers.html", "vendor/customers.html",
{ {
"request": request, "request": request,
"user": current_user, "user": current_user,
@@ -193,7 +213,7 @@ async def vendor_inventory_page(
JavaScript loads inventory data via API. JavaScript loads inventory data via API.
""" """
return templates.TemplateResponse( return templates.TemplateResponse(
"vendor/admin/inventory.html", "vendor/inventory.html",
{ {
"request": request, "request": request,
"user": current_user, "user": current_user,
@@ -217,7 +237,7 @@ async def vendor_marketplace_page(
JavaScript loads import jobs and products via API. JavaScript loads import jobs and products via API.
""" """
return templates.TemplateResponse( return templates.TemplateResponse(
"vendor/admin/marketplace.html", "vendor/marketplace.html",
{ {
"request": request, "request": request,
"user": current_user, "user": current_user,
@@ -241,7 +261,7 @@ async def vendor_team_page(
JavaScript loads team members via API. JavaScript loads team members via API.
""" """
return templates.TemplateResponse( return templates.TemplateResponse(
"vendor/admin/team.html", "vendor/team.html",
{ {
"request": request, "request": request,
"user": current_user, "user": current_user,
@@ -251,9 +271,29 @@ async def vendor_team_page(
# ============================================================================ # ============================================================================
# SETTINGS # PROFILE & SETTINGS
# ============================================================================ # ============================================================================
@router.get("/{vendor_code}/profile", response_class=HTMLResponse, include_in_schema=False)
async def vendor_profile_page(
request: Request,
vendor_code: str = Path(..., description="Vendor code"),
current_user: User = Depends(get_current_vendor_from_cookie_or_header)
):
"""
Render vendor profile page.
User can manage their personal profile information.
"""
return templates.TemplateResponse(
"vendor/profile.html",
{
"request": request,
"user": current_user,
"vendor_code": vendor_code,
}
)
@router.get("/{vendor_code}/settings", response_class=HTMLResponse, include_in_schema=False) @router.get("/{vendor_code}/settings", response_class=HTMLResponse, include_in_schema=False)
async def vendor_settings_page( async def vendor_settings_page(
request: Request, request: Request,

View File

@@ -506,6 +506,172 @@ current_customer: Customer = Depends(get_current_customer_api)
current_user: User = Depends(get_current_user) current_user: User = Depends(get_current_user)
``` ```
---
### Optional Authentication Dependencies
These dependencies return `None` instead of raising exceptions when authentication fails. Used for login pages and public routes that need to conditionally check if a user is authenticated.
#### `get_current_admin_optional()`
**Purpose:** Check if admin user is authenticated (without enforcing)
**Accepts:** Cookie (`admin_token`) OR Authorization header
**Returns:**
- `User` object with `role="admin"` if authenticated
- `None` if no token, invalid token, or user is not admin
**Raises:** Never raises exceptions
**Usage:**
```python
# Login page redirect
@router.get("/admin/login")
async def admin_login_page(
request: Request,
current_user: Optional[User] = Depends(get_current_admin_optional)
):
if current_user:
# User already logged in, redirect to dashboard
return RedirectResponse(url="/admin/dashboard", status_code=302)
# Not logged in, show login form
return templates.TemplateResponse("admin/login.html", {"request": request})
```
**Use Cases:**
- Login pages (redirect if already authenticated)
- Public pages with conditional admin content
- Root redirects based on authentication status
#### `get_current_vendor_optional()`
**Purpose:** Check if vendor user is authenticated (without enforcing)
**Accepts:** Cookie (`vendor_token`) OR Authorization header
**Returns:**
- `User` object with `role="vendor"` if authenticated
- `None` if no token, invalid token, or user is not vendor
**Raises:** Never raises exceptions
**Usage:**
```python
# Login page redirect
@router.get("/vendor/{vendor_code}/login")
async def vendor_login_page(
request: Request,
vendor_code: str = Path(...),
current_user: Optional[User] = Depends(get_current_vendor_optional)
):
if current_user:
# User already logged in, redirect to dashboard
return RedirectResponse(url=f"/vendor/{vendor_code}/dashboard", status_code=302)
# Not logged in, show login form
return templates.TemplateResponse("vendor/login.html", {
"request": request,
"vendor_code": vendor_code
})
```
**Use Cases:**
- Login pages (redirect if already authenticated)
- Public vendor pages with conditional content
- Root redirects based on authentication status
#### `get_current_customer_optional()`
**Purpose:** Check if customer user is authenticated (without enforcing)
**Accepts:** Cookie (`customer_token`) OR Authorization header
**Returns:**
- `User` object with `role="customer"` if authenticated
- `None` if no token, invalid token, or user is not customer
**Raises:** Never raises exceptions
**Usage:**
```python
# Shop login page redirect
@router.get("/shop/account/login")
async def customer_login_page(
request: Request,
current_user: Optional[User] = Depends(get_current_customer_optional)
):
if current_user:
# User already logged in, redirect to account page
return RedirectResponse(url="/shop/account", status_code=302)
# Not logged in, show login form
return templates.TemplateResponse("shop/login.html", {"request": request})
```
**Use Cases:**
- Login pages (redirect if already authenticated)
- Public shop pages with conditional customer content (e.g., "My Orders" link)
- Root redirects based on authentication status
---
### Required vs Optional Dependencies
Understanding when to use each type:
#### Admin Context
| Scenario | Use | Returns | On Auth Failure |
|----------|-----|---------|-----------------|
| **Protected page (dashboard, settings)** | `get_current_admin_from_cookie_or_header` | `User` | Raises 401 exception |
| **Login page** | `get_current_admin_optional` | `Optional[User]` | Returns `None` |
| **API endpoint** | `get_current_admin_api` | `User` | Raises 401 exception |
| **Public page with conditional content** | `get_current_admin_optional` | `Optional[User]` | Returns `None` |
#### Vendor Context
| Scenario | Use | Returns | On Auth Failure |
|----------|-----|---------|-----------------|
| **Protected page (dashboard, products)** | `get_current_vendor_from_cookie_or_header` | `User` | Raises 401 exception |
| **Login page** | `get_current_vendor_optional` | `Optional[User]` | Returns `None` |
| **API endpoint** | `get_current_vendor_api` | `User` | Raises 401 exception |
| **Public page with conditional content** | `get_current_vendor_optional` | `Optional[User]` | Returns `None` |
#### Customer Context
| Scenario | Use | Returns | On Auth Failure |
|----------|-----|---------|-----------------|
| **Protected page (account, orders)** | `get_current_customer_from_cookie_or_header` | `User` | Raises 401 exception |
| **Login page** | `get_current_customer_optional` | `Optional[User]` | Returns `None` |
| **API endpoint** | `get_current_customer_api` | `User` | Raises 401 exception |
| **Public page with conditional content** | `get_current_customer_optional` | `Optional[User]` | Returns `None` |
**Example Flow:**
```python
# ❌ WRONG: Using required auth on login page
@router.get("/admin/login")
async def admin_login_page(
current_user: User = Depends(get_current_admin_from_cookie_or_header)
):
# This will return 401 error if not logged in!
# Login page will never render for unauthenticated users
...
# ✅ CORRECT: Using optional auth on login page
@router.get("/admin/login")
async def admin_login_page(
current_user: Optional[User] = Depends(get_current_admin_optional)
):
# Returns None if not logged in, page renders normally
if current_user:
return RedirectResponse(url="/admin/dashboard")
return templates.TemplateResponse("login.html", ...)
# ✅ CORRECT: Using required auth on protected page
@router.get("/admin/dashboard")
async def admin_dashboard(
current_user: User = Depends(get_current_admin_from_cookie_or_header)
):
# Automatically returns 401 if not authenticated
# Only runs if user is authenticated admin
...
```
### Login Response Format ### Login Response Format
All login endpoints return: All login endpoints return:

View File

@@ -6,7 +6,7 @@
There are three ways depending on the deployment mode: There are three ways depending on the deployment mode:
**⚠️ Important:** This guide describes **customer-facing shop routes**. For vendor dashboard/management routes, see [Vendor Dashboard Documentation](../../vendor/). The shop uses `/vendors/{code}/shop/*` (plural) in path-based mode, while the vendor dashboard uses `/vendor/{code}/*` (singular). **⚠️ Important:** This guide describes **customer-facing shop routes**. For vendor dashboard/management routes, see [Vendor Frontend Architecture](../../frontend/vendor/architecture.md). The shop uses `/vendors/{code}/shop/*` (plural) in path-based mode, while the vendor dashboard uses `/vendor/{code}/*` (singular).
### 1. **SUBDOMAIN MODE** (Production - Recommended) ### 1. **SUBDOMAIN MODE** (Production - Recommended)
``` ```