Files
platform/src/apps/officer-web/Screens/Authentication/LandingPage/Login.tsx
T
pastilhasandClaude Opus 5 b27410a6fc sign in with an email or a username
Only an address worked, though usernames are already unique and already
validated as /^[a-zA-Z0-9._-]+$/ — which is what makes this safe: no `@`, so
the two namespaces cannot overlap and there is nothing to guess. An `@` decides
which lookup runs, and it decides it exactly.

The field is still called `email` on the wire, because every client sends that
name and renaming it would break them for nothing.

Fixed a latent bug while passing: the JWT took `email` from the REQUEST rather
than the row. Identical while an address was the only way in — and a username
in the `email` claim the moment it was not, leaving the DAV door, the managed
home path and everything else keyed on email looking up a user who does not
exist.

The login field is `type="text"`, not `type="email"`, or the browser refuses to
submit a username and validates someone out of an account they have.
`autoComplete="username"`, which is what password managers store beside a
password for a field that takes either.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-19 11:10:39 +00:00

112 lines
4.0 KiB
TypeScript

import { Input } from '@/components/ui/input';
import { Label } from '@/components/ui/label';
import { Button } from '@/components/ui/button';
import { Link } from 'react-router';
import { Card } from '@/components/Card';
import { useState } from 'react';
import { toast } from 'sonner';
import { useForm } from 'hooks/useForm';
import { useAuth } from 'hooks/useAuth';
import { useGlobal } from 'hooks/useGlobal';
const initialState: LoginFormState = {
// email: 'pastilhas@pastilhas.dev',
// password: '',
};
export function Login() {
const [isSubmitting, setIsSubmitting] = useState(false);
const { state, formRef, update, isValid } = useForm<LoginFormState>(initialState, validateForm);
const { signin } = useAuth();
const [, setDuckHidden] = useGlobal('DUCK_HIDDEN', false);
const handleSubmit = async (ev: React.FormEvent) => {
ev.preventDefault();
if (!isValid || isSubmitting) return;
setIsSubmitting(true);
setDuckHidden(true);
try {
await signin({ email: state.email!, password: state.password! });
window.location.href = '/';
} catch (ex) {
const error = ex as { message?: string };
toast.error(error.message || 'Login failed. Please try again.');
update({ ...state, password: '' });
} finally {
setIsSubmitting(false);
}
};
return (
<Card className="md:py-6 md:px-24 md:min-w-[480px] flex flex-col gap-6">
<div className="text-center">
<div className="text-duck-dark text-2xl font-bold">Welcome Back</div>
<div className="text-duck-dark/60">Sign in to your account</div>
</div>
<form ref={formRef} onSubmit={handleSubmit} className="grid gap-4">
<Label className="grid gap-2">
<span className="text-duck-dark/70">Email or username</span>
<Input
className="h-11 bg-background/60 border-duck-dark/20 text-duck-dark placeholder:text-duck-dark/40"
// `text`, not `email`: the browser refuses to submit a username under type="email", so the
// field would have validated the user out of an account they have.
type="text"
name="email"
placeholder="you@example.com"
// `username` rather than `email`, which is what password managers expect for a field that
// takes either — and it is the value they already store beside the password.
autoComplete="username"
/>
</Label>
<Label className="grid gap-2">
<span className="text-duck-dark/70">Password</span>
<Input
className="h-11 bg-background/60 border-duck-dark/20 text-duck-dark placeholder:text-duck-dark/40"
type="password"
name="password"
placeholder="Your password"
autoComplete="current-password"
/>
</Label>
<div className="pt-2">
<Button
type="submit"
disabled={!isValid || isSubmitting}
className="w-full h-11 bg-duck-yellow hover:bg-duck-yellow/90 text-duck-teal font-bold text-lg transition-all duration-200 hover:scale-105 cursor-pointer disabled:opacity-50 disabled:hover:scale-100"
>
{isSubmitting ? 'Signing in...' : 'Sign In'}
</Button>
</div>
<div className="text-center text-sm text-duck-dark/60 grid gap-1">
{/* <p> */}
{/* Don't have an account?{' '} */}
{/* <Link to="/auth/register" className="text-duck-forest underline hover:text-duck-forest/80"> */}
{/* Register */}
{/* </Link> */}
{/* </p> */}
<p>
<Link to="/auth/forgot-password" className="text-duck-forest underline hover:text-duck-forest/80">
Forgot password?
</Link>
</p>
</div>
</form>
</Card>
);
}
type LoginFormState = {
email?: string;
password?: string;
};
const validateForm = (state: Partial<LoginFormState>) => {
const { email, password } = state;
if (!email || !password) return false;
return true;
};