authentication and calendar-sync passwords keyed (214 -> 163)

Authentication reaches zero -- sign-in, bootstrap, forgot and reset password. These
are the first words a new account reads, which is why the brief ranked them above
the larger screens.

DavAppPasswords had the third toLocaleDateString(undefined, ...) in the shell; it now
uses useFormat().date like the other two.

DebugPanel's three labels are keyed too. Worth knowing rather than acting on: that
panel is rendered behind showDebug, which Background.tsx hardcodes to false, so it
never appears. Keyed rather than deleted because removing it is not my call.
This commit is contained in:
2026-08-23 05:15:22 +00:00
parent 7f8455f1af
commit b978e09327
9 changed files with 189 additions and 63 deletions
@@ -7,8 +7,10 @@ import { Input } from '@/components/ui/input';
import { Label } from '@/components/ui/label';
import { Button } from '@/components/ui/button';
import { Card } from '@/components/Card';
import { useTranslation } from '@/lib/i18n';
export function ForgotPassword() {
const { t } = useTranslation();
const client = useClient('/api/auth');
const [isSubmitting, setIsSubmitting] = useState(false);
const [done, setDone] = useState(false);
@@ -34,29 +36,29 @@ export function ForgotPassword() {
<Card className="md:py-12 md:px-24 flex flex-col gap-6">
{done ? (
<div className="text-center">
<h2 className="text-2xl font-bold text-duck-dark mb-2">Check your email</h2>
<h2 className="text-2xl font-bold text-duck-dark mb-2">{t('auth.checkEmail')}</h2>
<div className="text-sm text-duck-dark/60">
<p>
A password reset link has been sent to <strong>{state.email}</strong>.
</p>
<p>Click it to reset your password.</p>
<p>{t('auth.checkEmailHint')}</p>
</div>
</div>
) : (
<>
<div className="text-center">
<div className="text-duck-dark text-2xl font-bold">Forgot Password</div>
<div className="text-duck-dark/60">Enter your email to receive a reset link</div>
<div className="text-duck-dark text-2xl font-bold">{t('auth.forgotTitle')}</div>
<div className="text-duck-dark/60">{t('auth.forgotSubtitle')}</div>
</div>
<form ref={formRef} onSubmit={handleSubmit} className="grid gap-4">
<Label className="grid gap-2">
<span className="text-duck-dark/70">Email</span>
<span className="text-duck-dark/70">{t('auth.email')}</span>
<Input
className="h-11 bg-background/60 border-duck-dark/20 text-duck-dark placeholder:text-duck-dark/40"
type="email"
name="email"
placeholder="you@example.com"
placeholder={t('auth.emailPlaceholder')}
autoComplete="email"
/>
</Label>
@@ -74,7 +76,7 @@ export function ForgotPassword() {
<div className="text-center text-sm text-duck-dark/60">
<p>
<Link to="/" className="text-duck-forest underline hover:text-duck-forest/80">
Back to login
{t('auth.backToLogin')}
</Link>
</p>
</div>
@@ -4,8 +4,10 @@ import { Button } from '@/components/ui/button';
import { Card } from '@/components/Card';
import { cn } from '@/lib/utils';
import { useResetPassword } from './useResetPassword';
import { useTranslation } from '@/lib/i18n';
export function ResetPassword() {
const { t } = useTranslation();
const { formRef, isValid, tokenStatus, isSubmitting, handleSubmit } = useResetPassword();
const loading = tokenStatus === 'loading';
const invalid = tokenStatus === 'invalid';
@@ -15,45 +17,45 @@ export function ResetPassword() {
<>
{loading && (
<Card className="py-12 px-24 flex flex-col gap-6">
<div className="text-center text-duck-dark/60">Verifying...</div>
<div className="text-center text-duck-dark/60">{t('auth.verifying')}</div>
</Card>
)}
{invalid && (
<Card className="py-12 px-24 flex flex-col gap-6">
<div className="text-center">
<div className="text-duck-dark text-2xl font-bold">Invalid Link</div>
<div className="text-duck-dark/60">This reset link is invalid or has expired.</div>
<div className="text-duck-dark text-2xl font-bold">{t('auth.invalidLink')}</div>
<div className="text-duck-dark/60">{t('auth.invalidLinkHint')}</div>
</div>
</Card>
)}
<Card className={cn('flex flex-col gap-6', hideform && 'hidden')}>
<div className="text-center">
<div className="text-duck-dark text-2xl font-bold">Reset Password</div>
<div className="text-duck-dark/60">Enter your new password</div>
<div className="text-duck-dark text-2xl font-bold">{t('auth.resetTitle')}</div>
<div className="text-duck-dark/60">{t('auth.resetSubtitle')}</div>
</div>
<form ref={formRef} onSubmit={handleSubmit} className="grid gap-4">
<div className="grid md:flex gap-4">
<Label className="grid gap-2 flex-1">
<span className="text-duck-dark/70">Password</span>
<span className="text-duck-dark/70">{t('auth.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="Min 12 characters"
placeholder={t('auth.minChars')}
autoComplete="new-password"
/>
</Label>
<Label className="grid gap-2 flex-1">
<span className="text-duck-dark/70">Confirm Password</span>
<span className="text-duck-dark/70">{t('auth.confirmPassword')}</span>
<Input
className="h-11 bg-background/60 border-duck-dark/20 text-duck-dark placeholder:text-duck-dark/40"
type="password"
name="confirmPassword"
placeholder="Confirm your password"
placeholder={t('auth.confirmPlaceholder')}
autoComplete="new-password"
/>
</Label>
@@ -65,7 +67,7 @@ export function ResetPassword() {
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 ? 'Resetting...' : 'Reset Password'}
{isSubmitting ? 'Resetting...' : t('auth.resetTitle')}
</Button>
</div>
</form>
@@ -4,8 +4,10 @@ import { toast } from 'sonner';
import { useMounted } from 'hooks/useMounted';
import { useForm } from 'hooks/useForm';
import { useClient } from 'hooks/useClient';
import { useTranslation } from '@/lib/i18n';
export const useResetPassword = () => {
const { t } = useTranslation();
const isMounted = useMounted();
const navigate = useNavigate();
const client = useClient('/api/auth');
@@ -44,7 +46,7 @@ export const useResetPassword = () => {
password: state.password,
verificationCode,
});
toast.success('Password reset successfully. Please sign in.');
toast.success(t('auth.resetOk'));
navigate('/');
} catch (ex) {
const error = ex as { message?: string };
@@ -7,8 +7,10 @@ import { Input } from '@/components/ui/input';
import { Label } from '@/components/ui/label';
import { Card } from '@/components/Card';
import { useForm } from 'hooks/useForm';
import { useTranslation } from '@/lib/i18n';
export function Bootstrap() {
const { t } = useTranslation();
const authClient = useClient('/api/auth');
const navigate = useNavigate();
const [isSubmitting, setIsSubmitting] = useState(false);
@@ -30,7 +32,7 @@ export function Bootstrap() {
password: state.password,
confirmPassword: state.confirmPassword,
});
toast.success('Administrator account created. Please sign in.');
toast.success(t('auth.bootstrapOk'));
navigate('/');
} catch (ex) {
const error = ex as { message?: string };
@@ -43,35 +45,35 @@ export function Bootstrap() {
return (
<Card className="md:min-w-[480px] flex flex-col gap-6">
<div className="text-center">
<div className="text-duck-dark text-2xl font-bold">Welcome, Admin</div>
<div className="text-duck-dark/60">Create your administrator account</div>
<div className="text-duck-dark text-2xl font-bold">{t('auth.bootstrapTitle')}</div>
<div className="text-duck-dark/60">{t('auth.bootstrapSubtitle')}</div>
</div>
<form ref={formRef} onSubmit={handleBootstrap} className="grid gap-4">
<Label className="grid gap-2">
<span className="text-duck-dark/70">Name</span>
<span className="text-duck-dark/70">{t('auth.name')}</span>
<Input
className="h-11 bg-background/60 border-duck-dark/20 text-duck-dark placeholder:text-duck-dark/40"
type="text"
name="name"
placeholder="Your name"
placeholder={t('auth.namePlaceholder')}
autoComplete="name"
/>
</Label>
<Label className="grid gap-2">
<span className="text-duck-dark/70">Email</span>
<span className="text-duck-dark/70">{t('auth.email')}</span>
<Input
className="h-11 bg-background/60 border-duck-dark/20 text-duck-dark placeholder:text-duck-dark/40"
type="email"
name="email"
placeholder="admin@example.com"
placeholder={t('auth.adminEmailPlaceholder')}
autoComplete="email"
/>
</Label>
<Label className="grid gap-2">
<span className="text-duck-dark/70">Username</span>
<span className="text-duck-dark/70">{t('auth.username')}</span>
<Input
className="h-11 bg-background/60 border-duck-dark/20 text-duck-dark placeholder:text-duck-dark/40"
type="text"
@@ -83,23 +85,23 @@ export function Bootstrap() {
<div className="grid md:flex gap-4">
<Label className="grid gap-2 flex-1">
<span className="text-duck-dark/70">Password</span>
<span className="text-duck-dark/70">{t('auth.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="Min 12 characters"
placeholder={t('auth.minChars')}
autoComplete="new-password"
/>
</Label>
<Label className="grid gap-2 flex-1">
<span className="text-duck-dark/70">Confirm Password</span>
<span className="text-duck-dark/70">{t('auth.confirmPassword')}</span>
<Input
className="h-11 bg-background/60 border-duck-dark/20 text-duck-dark placeholder:text-duck-dark/40"
type="password"
name="confirmPassword"
placeholder="Confirm your password"
placeholder={t('auth.confirmPlaceholder')}
autoComplete="new-password"
/>
</Label>
@@ -8,12 +8,14 @@ import { toast } from 'sonner';
import { useForm } from 'hooks/useForm';
import { useAuth } from 'hooks/useAuth';
import { useGlobal } from 'hooks/useGlobal';
import { useTranslation } from '@/lib/i18n';
const initialState: LoginFormState = {
// email: 'pastilhas@pastilhas.dev',
// password: '',
};
export function Login() {
const { t } = useTranslation();
const [isSubmitting, setIsSubmitting] = useState(false);
const { state, formRef, update, isValid } = useForm<LoginFormState>(initialState, validateForm);
const { signin } = useAuth();
@@ -40,20 +42,20 @@ export function Login() {
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 className="text-duck-dark text-2xl font-bold">{t('auth.signInTitle')}</div>
<div className="text-duck-dark/60">{t('auth.signInSubtitle')}</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>
<span className="text-duck-dark/70">{t('auth.emailOrUsername')}</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"
placeholder={t('auth.emailPlaceholder')}
// `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"
@@ -61,12 +63,12 @@ export function Login() {
</Label>
<Label className="grid gap-2">
<span className="text-duck-dark/70">Password</span>
<span className="text-duck-dark/70">{t('auth.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"
placeholder={t('auth.passwordPlaceholder')}
autoComplete="current-password"
/>
</Label>
@@ -90,7 +92,7 @@ export function Login() {
{/* </p> */}
<p>
<Link to="/auth/forgot-password" className="text-duck-forest underline hover:text-duck-forest/80">
Forgot password?
{t('auth.forgotPassword')}
</Link>
</p>
</div>
@@ -1,3 +1,5 @@
import { useTranslation } from '@/lib/i18n';
type DebugPanelProps = {
fullControlMode: boolean;
cameraInfo: {
@@ -13,9 +15,10 @@ type DebugPanelProps = {
};
export function DebugPanel({ fullControlMode, cameraInfo, modelInfo }: DebugPanelProps) {
const { t } = useTranslation();
return (
<div className="fixed top-4 left-4 bg-black/80 text-white p-4 rounded text-xs font-mono space-y-2 z-[100]">
<div className="font-bold text-blue-400">Controls:</div>
<div className="font-bold text-blue-400">{t('auth.debug.controls')}</div>
<div className={fullControlMode ? 'text-green-300' : 'text-yellow-300'}>
Mode: {fullControlMode ? 'FULL CONTROL' : 'ROTATION ONLY'}
</div>
@@ -29,12 +32,12 @@ export function DebugPanel({ fullControlMode, cameraInfo, modelInfo }: DebugPane
<div>* Ctrl+Alt+Enter: Toggle full control</div>
<div>* Ctrl+Alt+D: Toggle debug panel</div>
<div className="font-bold text-green-400 pt-2">Camera:</div>
<div className="font-bold text-green-400 pt-2">{t('auth.debug.camera')}</div>
<div>Position: [{cameraInfo.cameraPosition.map((v) => v.toFixed(2)).join(', ')}]</div>
<div>Target: [{cameraInfo.target.map((v) => v.toFixed(2)).join(', ')}]</div>
<div>Zoom: {cameraInfo.zoom.toFixed(2)}</div>
<div className="font-bold text-yellow-400 pt-2">Model:</div>
<div className="font-bold text-yellow-400 pt-2">{t('auth.debug.model')}</div>
<div>Position: [{modelInfo.position.map((v) => v.toFixed(2)).join(', ')}]</div>
<div>Rotation: [{modelInfo.rotation.map((v) => v.toFixed(2)).join(', ')}]</div>
<div>Scale: {modelInfo.scale.toFixed(2)}</div>