diff --git a/backend/app/schemas/user.py b/backend/app/schemas/user.py index 0969b2f..c27b79c 100644 --- a/backend/app/schemas/user.py +++ b/backend/app/schemas/user.py @@ -7,15 +7,18 @@ from pydantic import BaseModel, EmailStr class UserBase(BaseModel): username: str - email: EmailStr class UserCreate(UserBase): + """Input schema — strict EmailStr validation for user-supplied email.""" + email: EmailStr password: str class UserResponse(UserBase): + """Response schema — email is serialised as-is from DB (no re-validation).""" id: int + email: str created_at: datetime class Config: diff --git a/frontend/src/app/api/auth/login/route.ts b/frontend/src/app/api/auth/login/route.ts new file mode 100644 index 0000000..3e7a59d --- /dev/null +++ b/frontend/src/app/api/auth/login/route.ts @@ -0,0 +1,46 @@ +/** + * Next.js Route Handler for login. + * + * Handles SHA-256 password hashing server-side so the login flow works + * in non-secure HTTP contexts (e.g. http://192.168.x.x) where the browser + * blocks window.crypto.subtle (Web Crypto API requires a Secure Context). + * + * This handler intercepts /api/auth/login before the next.config rewrite + * forwards it to the backend, computes the SHA-256 hash using Node.js + * built-in crypto, then proxies the request to the backend. + */ +import { createHash } from 'node:crypto'; +import { NextRequest, NextResponse } from 'next/server'; + +const BACKEND_URL = process.env.BACKEND_URL ?? 'http://localhost:8000'; + +export async function POST(request: NextRequest) { + let body: { username: string; password: string }; + try { + body = await request.json(); + } catch { + return NextResponse.json({ detail: 'Invalid request body' }, { status: 400 }); + } + + const { username, password } = body; + + // Hash the raw password with SHA-256 (matches backend verify_password expectation) + const hashedPassword = createHash('sha256').update(password).digest('hex'); + + const backendResponse = await fetch(`${BACKEND_URL}/api/auth/login`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ username, password: hashedPassword }), + }); + + const data = await backendResponse.json(); + const response = NextResponse.json(data, { status: backendResponse.status }); + + // Forward the HttpOnly access_token cookie set by the backend + const setCookie = backendResponse.headers.get('set-cookie'); + if (setCookie) { + response.headers.set('set-cookie', setCookie); + } + + return response; +} diff --git a/frontend/src/lib/api.ts b/frontend/src/lib/api.ts index d85333f..68042d5 100644 --- a/frontend/src/lib/api.ts +++ b/frontend/src/lib/api.ts @@ -1,13 +1,5 @@ const API_URL = process.env.NEXT_PUBLIC_API_URL || ''; -async function hashPassword(password: string): Promise { - const encoder = new TextEncoder(); - const data = encoder.encode(password); - const hashBuffer = await crypto.subtle.digest('SHA-256', data); - const hashArray = Array.from(new Uint8Array(hashBuffer)); - return hashArray.map(b => b.toString(16).padStart(2, '0')).join(''); -} - class ApiClient { private baseUrl: string; @@ -61,15 +53,17 @@ class ApiClient { } async login(username: string, password: string) { - const hashedPassword = await hashPassword(password); - + // Raw password is sent to the Next.js Route Handler (/api/auth/login), + // which hashes it server-side with Node.js crypto and forwards to the backend. + // This avoids dependency on window.crypto.subtle, which is only available + // in Secure Contexts (HTTPS or localhost) and fails over plain HTTP + LAN IP. const response = await fetch(`${this.baseUrl}/api/auth/login`, { method: 'POST', headers: { 'Content-Type': 'application/json', }, credentials: 'include', - body: JSON.stringify({ username, password: hashedPassword }), + body: JSON.stringify({ username, password }), }); if (!response.ok) {