diff --git a/README.md b/README.md index e03379b9e3..6234c78971 100644 --- a/README.md +++ b/README.md @@ -633,6 +633,11 @@ The bundled Docker, local and Helm nginx configs proxy Live Browser WebSocket up Browser login uses `HttpOnly` session cookies. The login page offers a "keep me signed in" option that extends the browser session when the request is HTTPS (including trusted `X-Forwarded-Proto: https`) or localhost HTTP. The localhost exception uses the direct request `Host` and ignores forwarded host headers. Public HTTP deployments, including many temporary sandbox URLs, fall back to session cookies by default. DeerFlow never stores the password in browser storage; the UI may remember only the email address. +Password forms accept non-ASCII email addresses and trim surrounding email whitespace. +Sign-in requires at least six password characters; creating an account or choosing +a new password requires eight. These checks run before sending a request. +Passwords, including surrounding spaces, are submitted unchanged. + OIDC/SSO authorization endpoints may include provider query parameters such as `?prompt=login`. DeerFlow preserves those parameters when adding the login request, including for configured `authorization_endpoint` overrides. Administrators can suspend and restore accounts from **Settings → Users** (admin-only; the section is hidden for ordinary users). The page lists each account's email, role, and status and exposes the same enable/disable operation as `PATCH /api/v1/admin/users/{id}`; the last remaining active admin cannot be disabled, and the Gateway's rejection detail is shown verbatim. A disabled account is rejected by every authentication surface, and its live sessions end on the next request with a login page that states the account has been disabled. Password login for a disabled account deliberately still answers "incorrect email or password" so the state is not disclosed to whoever holds the credentials. diff --git a/frontend/AGENTS.md b/frontend/AGENTS.md index 6589ef9a8d..bc440ec4a2 100644 --- a/frontend/AGENTS.md +++ b/frontend/AGENTS.md @@ -113,6 +113,8 @@ Confirmed logout unmounts query consumers immediately; public and unknown-auth routes still render with an independent client. Keep auth outside the query provider. Superseded auth refreshes cannot restore an earlier owner. +Auth: trim emails; validate raw passwords. + See [project structure](README.md#project-structure) for the directory map and [src/AGENTS.md](src/AGENTS.md) for component and domain ownership. diff --git a/frontend/src/app/(auth)/login/page.tsx b/frontend/src/app/(auth)/login/page.tsx index ef956e2d5c..d6d857ea1e 100644 --- a/frontend/src/app/(auth)/login/page.tsx +++ b/frontend/src/app/(auth)/login/page.tsx @@ -10,6 +10,7 @@ import { Button } from "@/components/ui/button"; import { FlickeringGrid } from "@/components/ui/flickering-grid"; import { Input } from "@/components/ui/input"; import { AuthDisabledContext, useAuth } from "@/core/auth/AuthProvider"; +import { isValidEmail } from "@/core/auth/email"; import { resolveAuthNextPath } from "@/core/auth/next-path"; import { loadRememberLoginPreference, @@ -151,14 +152,31 @@ export default function LoginPage() { e.preventDefault(); setError(""); setShowSsoHint(false); - setLoading(true); if (!isLogin && !regularSignupAllowed) { setError(t.login.adminSetupRequiredDescription); - setLoading(false); return; } + // The form is `noValidate`, so the browser no longer rejects addresses + // its `type="email"` rule dislikes (e.g. a non-ASCII local part, which + // the backend accepts — #7488). Apply the permissive JS check instead. + const address = email.trim(); + if (!isValidEmail(address)) { + setError(t.login.errors.invalid_email); + return; + } + + if (password.length < (isLogin ? 6 : 8)) { + setError( + isLogin + ? t.login.errors.password_too_short + : t.login.errors.registration_password_too_short, + ); + return; + } + + setLoading(true); try { const endpoint = isLogin ? "/api/v1/auth/login/local" @@ -167,9 +185,9 @@ export default function LoginPage() { ? new URLSearchParams({ password, remember_me: String(rememberMe), - username: email, + username: address, }) - : JSON.stringify({ email, password, remember_me: rememberMe }); + : JSON.stringify({ email: address, password, remember_me: rememberMe }); const headers: HeadersInit = isLogin ? { "Content-Type": "application/x-www-form-urlencoded" } @@ -202,7 +220,7 @@ export default function LoginPage() { return; } - saveRememberLoginPreference({ email, rememberMe }); + saveRememberLoginPreference({ email: address, rememberMe }); // Both login and register set a cookie — redirect to workspace router.push(redirectPath); @@ -276,7 +294,7 @@ export default function LoginPage() { )} -