/** * LoginPage — real login form (M2-T07). * * Behaviours: * - Renders a Mantine form with username + password fields. * - On submit → POST /api/auth/login via apiClient (no CSRF needed; unauthenticated endpoint). * - On success → invalidate ['session'] so SessionProvider re-fetches, then navigate to the * originally-requested route (from location.state.from) or fall back to '/'. * - On 401 (bad credentials) → show an inline error without leaking the password. * - Already-authenticated users visiting /login → redirect to '/'. */ import { useState } from 'react' import { useNavigate, useLocation, Navigate } from 'react-router-dom' import { useQueryClient } from '@tanstack/react-query' import { Container, Paper, Title, TextInput, PasswordInput, Button, Alert, Stack, Center, } from '@mantine/core' import { useSession } from '../auth/SessionProvider' import apiClient from '../api/client' import { setCsrfToken } from '../api/csrf' // --------------------------------------------------------------------------- // Types // --------------------------------------------------------------------------- interface LocationState { from?: { pathname: string } } // --------------------------------------------------------------------------- // Component // --------------------------------------------------------------------------- export function LoginPage() { const { status } = useSession() const navigate = useNavigate() const location = useLocation() const queryClient = useQueryClient() const [username, setUsername] = useState('') const [password, setPassword] = useState('') const [error, setError] = useState(null) const [loading, setLoading] = useState(false) // Already authenticated → redirect to intended destination or home. if (status === 'authenticated') { const from = (location.state as LocationState)?.from?.pathname ?? '/' return } async function handleSubmit(e: React.FormEvent) { e.preventDefault() setError(null) setLoading(true) try { const res = await apiClient.POST('/api/auth/login', { body: { username, password }, }) if (res.response.status === 401 || !res.data) { // Bad credentials — do not leak the password in the message. setError('Incorrect username or password.') return } // Success: store the CSRF token returned by login (same shape as session response). if (res.data.csrf_token) { setCsrfToken(res.data.csrf_token) } // Refresh session state: invalidate the ['session'] query so SessionProvider // picks up the new authenticated state (which may include force_password_change). await queryClient.invalidateQueries({ queryKey: ['session'] }) // Navigate to the originally-requested route or home. const from = (location.state as LocationState)?.from?.pathname ?? '/' navigate(from, { replace: true }) } catch { // Any unexpected error (network, 5xx, etc.) setError('Login failed. Please try again.') } finally { setLoading(false) } } return (
Sign In {error && ( {error} )}
setUsername(e.currentTarget.value)} required autoComplete="username" data-testid="username-input" /> setPassword(e.currentTarget.value)} required autoComplete="current-password" data-testid="password-input" />
) }