Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
68 changes: 25 additions & 43 deletions frontend/src/pages/Login.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,11 +10,6 @@ interface ValidationError {
message: string
}

interface PydanticValidationError {
loc?: Array<string | number>
msg?: string
}

export default function Login() {
const navigate = useNavigate()
const { setAuth } = useAuthStore()
Expand All @@ -24,6 +19,10 @@ export default function Login() {
const [errors, setErrors] = useState<ValidationError[]>([])
const [loading, setLoading] = useState(false)

const setError = (message: string) => {
setErrors([{ field: 'general', message }])
}

const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault()
setErrors([])
Expand All @@ -47,42 +46,25 @@ export default function Login() {
setLoading(true)

try {
const tokenData = await authApi.login(trimmedEmail, password)
const tokenData = await authApi.login(email, password)
setAuth(tokenData.access_token, null)
const user = await authApi.getMe(tokenData.access_token)
const user = await authApi.getMe()
setAuth(tokenData.access_token, user)
navigate('/')
} catch (err) {
if (axios.isAxiosError(err)) {
const detail = err.response?.data?.detail
if (detail) {
if (typeof detail === 'object' && detail.field && detail.message) {
// Authentication failure (always field: 'general')
setErrors([{ field: detail.field, message: detail.message }])
} else if (Array.isArray(detail)) {
// Pydantic 422 errors — login uses OAuth2PasswordRequestForm so
// the server field name is 'username'; map it to 'email' for the UI.
const parsed = detail.map((error: PydanticValidationError) => {
const rawField = String(error.loc?.[error.loc.length - 1] ?? 'general')
const field = rawField === 'username' ? 'email' : rawField
return { field, message: error.msg || 'Invalid input' }
})
setErrors(parsed)
} else {
setErrors([{ field: 'general', message: String(detail) }])
}
} else if (err.code === 'ERR_NETWORK') {
setErrors([
{
field: 'general',
message: 'Network error. Please check your connection and try again.',
},
])
const status = err.response?.status
if (status === 401 || status === 403) {
setError('Invalid email or password.')
} else if (status && status >= 500) {
setError('Server error, please try again later.')
} else if (!err.response) {
setError('Unable to connect to server.')
} else {
setErrors([{ field: 'general', message: 'Invalid email or password' }])
setError(err.response.data?.detail ?? 'Something went wrong. Please try again.')
}
} else {
setErrors([{ field: 'general', message: 'An unexpected error occurred. Please try again.' }])
setError('Something went wrong. Please try again.')
}
} finally {
setLoading(false)
Expand All @@ -103,11 +85,11 @@ export default function Login() {
</div>

<form className="space-y-6" onSubmit={handleSubmit}>
{errors.some((e: any) => e.field === 'general') && (
{errors.some((e: ValidationError) => e.field === 'general') && (
<div className="p-3 flex items-start gap-3 text-sm bg-red-50 rounded-lg border border-red-200">
<AlertCircle className="w-5 h-5 text-red-600 flex-shrink-0 mt-0.5" />
<div className="text-red-700">
{errors.find((e: any) => e.field === 'general')?.message}
{errors.find((e: ValidationError) => e.field === 'general')?.message}
</div>
</div>
)}
Expand All @@ -122,15 +104,15 @@ export default function Login() {
required
value={email}
onChange={(e) => setEmail(e.target.value)}
className={`mt-1 block w-full px-3 py-2 border rounded-lg shadow-sm focus:ring-primary-500 focus:border-primary-500 ${
errors.some((e: any) => e.field === 'email')
className={`mt-1 block w-full px-3 py-2 border rounded-lg shadow-sm focus:ring-primary-500 focus:border-primary-500${
errors.some((e: ValidationError) => e.field === 'email')
? 'border-red-300 bg-red-50'
: 'border-gray-300'
}`}
/>
{errors.some((e: any) => e.field === 'email') && (
{errors.some((e: ValidationError) => e.field === 'email') && (
<p className="mt-1 text-sm text-red-600">
{errors.find((e: any) => e.field === 'email')?.message}
{errors.find((e: ValidationError) => e.field === 'email')?.message}
</p>
)}
</div>
Expand All @@ -147,7 +129,7 @@ export default function Login() {
value={password}
onChange={(e) => setPassword(e.target.value)}
className={`block w-full pl-3 pr-10 py-2 border rounded-lg shadow-sm focus:ring-primary-500 focus:border-primary-500 ${
errors.some((e: any) => e.field === 'password')
errors.some((e: ValidationError) => e.field === 'password')
? 'border-red-300 bg-red-50'
: 'border-gray-300'
}`}
Expand All @@ -160,9 +142,9 @@ export default function Login() {
{showPassword ? <EyeOff className="w-5 h-5" /> : <Eye className="w-5 h-5" />}
</button>
</div>
{errors.some((e: any) => e.field === 'password') && (
{errors.some((e: ValidationError) => e.field === 'password') && (
<p className="mt-1 text-sm text-red-600">
{errors.find((e: any) => e.field === 'password')?.message}
{errors.find((e: ValidationError) => e.field === 'password')?.message}
</p>
)}
</div>
Expand All @@ -185,4 +167,4 @@ export default function Login() {
</div>
</div>
)
}
}
29 changes: 14 additions & 15 deletions frontend/src/pages/Register.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -138,7 +138,6 @@ export default function Register() {
setLoading(false)
}
}

return (
<div className="min-h-screen flex items-center justify-center bg-gray-50">
<div className="max-w-md w-full space-y-8 p-8 bg-white rounded-xl shadow-lg">
Expand All @@ -153,11 +152,11 @@ export default function Register() {
</div>

<form className="space-y-6" onSubmit={handleSubmit}>
{errors.some((e: any) => e.field === 'general') && (
{errors.some((e: ValidationError) => e.field === 'general') && (
<div className="p-3 flex items-start gap-3 text-sm bg-red-50 rounded-lg border border-red-200">
<AlertCircle className="w-5 h-5 text-red-600 flex-shrink-0 mt-0.5" />
<div className="text-red-700">
{errors.find((e: any) => e.field === 'general')?.message}
{errors.find((e: ValidationError) => e.field === 'general')?.message}
</div>
</div>
)}
Expand All @@ -173,14 +172,14 @@ export default function Register() {
value={formData.email}
onChange={(e: React.ChangeEvent<HTMLInputElement>) => setFormData({ ...formData, email: e.target.value })}
className={`mt-1 block w-full px-3 py-2 border rounded-lg shadow-sm focus:ring-primary-500 focus:border-primary-500 ${
errors.some((e: any) => e.field === 'email')
errors.some((e: ValidationError) => e.field === 'email')
? 'border-red-300 bg-red-50'
: 'border-gray-300'
}`}
/>
{errors.some((e: any) => e.field === 'email') && (
{errors.some((e: ValidationError) => e.field === 'email') && (
<p className="mt-1 text-sm text-red-600">
{errors.find((e: any) => e.field === 'email')?.message}
{errors.find((e: ValidationError) => e.field === 'email')?.message}
</p>
)}
</div>
Expand All @@ -199,7 +198,7 @@ export default function Register() {
onFocus={() => setShowPasswordRequirements(true)}
onBlur={() => setShowPasswordRequirements(false)}
className={`block w-full pl-3 pr-10 py-2 border rounded-lg shadow-sm focus:ring-primary-500 focus:border-primary-500 ${
errors.some((e: any) => e.field === 'password')
errors.some((e: ValidationError) => e.field === 'password')
? 'border-red-300 bg-red-50'
: 'border-gray-300'
}`}
Expand Down Expand Up @@ -241,9 +240,9 @@ export default function Register() {
</div>
)}

{errors.some((e: any) => e.field === 'password') && (
{errors.some((e: ValidationError) => e.field === 'password') && (
<p className="mt-1 text-sm text-red-600">
{errors.find((e: any) => e.field === 'password')?.message}
{errors.find((e: ValidationError) => e.field === 'password')?.message}
</p>
)}
</div>
Expand All @@ -259,14 +258,14 @@ export default function Register() {
value={formData.full_name}
onChange={(e: React.ChangeEvent<HTMLInputElement>) => setFormData({ ...formData, full_name: e.target.value })}
className={`mt-1 block w-full px-3 py-2 border rounded-lg shadow-sm focus:ring-primary-500 focus:border-primary-500 ${
errors.some((e: any) => e.field === 'full_name')
errors.some((e: ValidationError) => e.field === 'full_name')
? 'border-red-300 bg-red-50'
: 'border-gray-300'
}`}
/>
{errors.some((e: any) => e.field === 'full_name') && (
{errors.some((e: ValidationError) => e.field === 'full_name') && (
<p className="mt-1 text-sm text-red-600">
{errors.find((e: any) => e.field === 'full_name')?.message}
{errors.find((e: ValidationError) => e.field === 'full_name')?.message}
</p>
)}
</div>
Expand All @@ -282,14 +281,14 @@ export default function Register() {
value={formData.company_name}
onChange={(e: React.ChangeEvent<HTMLInputElement>) => setFormData({ ...formData, company_name: e.target.value })}
className={`mt-1 block w-full px-3 py-2 border rounded-lg shadow-sm focus:ring-primary-500 focus:border-primary-500 ${
errors.some((e: any) => e.field === 'company_name')
errors.some((e: ValidationError) => e.field === 'company_name')
? 'border-red-300 bg-red-50'
: 'border-gray-300'
}`}
/>
{errors.some((e: any) => e.field === 'company_name') && (
{errors.some((e: ValidationError) => e.field === 'company_name') && (
<p className="mt-1 text-sm text-red-600">
{errors.find((e: any) => e.field === 'company_name')?.message}
{errors.find((e: ValidationError) => e.field === 'company_name')?.message}
</p>
)}
</div>
Expand Down
Loading