feat: harden identity and recovery operations

This commit is contained in:
2026-08-04 20:24:18 +01:00
parent 7a4ac48aaf
commit eae2a44f46
30 changed files with 1428 additions and 21 deletions

View File

@@ -14,14 +14,39 @@ import { request } from '@/routes/password';
type Props = {
status?: string;
canResetPassword: boolean;
microsoftEnabled: boolean;
};
export default function Login({ status, canResetPassword }: Props) {
export default function Login({
status,
canResetPassword,
microsoftEnabled,
}: Props) {
return (
<>
<Head title="Log in" />
<Head title="Entrar" />
<PasskeyVerify />
<PasskeyVerify
label="Entrar com chave de acesso"
loadingLabel="A autenticar..."
separator="Ou continuar com email"
/>
{microsoftEnabled && (
<>
<Button variant="outline" className="w-full" asChild>
<a href="/auth/microsoft/redirect">
Entrar com a conta Microsoft
</a>
</Button>
<div className="relative text-center text-sm">
<span className="relative z-10 bg-background px-2 text-muted-foreground">
ou utilize a conta local
</span>
<span className="absolute inset-x-0 top-1/2 border-t" />
</div>
</>
)}
<Form
{...store.form()}
@@ -32,7 +57,7 @@ export default function Login({ status, canResetPassword }: Props) {
<>
<div className="grid gap-6">
<div className="grid gap-2">
<Label htmlFor="email">Email address</Label>
<Label htmlFor="email">Email</Label>
<Input
id="email"
type="email"
@@ -41,21 +66,23 @@ export default function Login({ status, canResetPassword }: Props) {
autoFocus
tabIndex={1}
autoComplete="email"
placeholder="email@example.com"
placeholder="nome@escola.pt"
/>
<InputError message={errors.email} />
</div>
<div className="grid gap-2">
<div className="flex items-center">
<Label htmlFor="password">Password</Label>
<Label htmlFor="password">
Palavra-passe
</Label>
{canResetPassword && (
<TextLink
href={request()}
className="ml-auto text-sm"
tabIndex={5}
>
Forgot your password?
Recuperar palavra-passe
</TextLink>
)}
</div>
@@ -65,7 +92,7 @@ export default function Login({ status, canResetPassword }: Props) {
required
tabIndex={2}
autoComplete="current-password"
placeholder="Password"
placeholder="Palavra-passe"
/>
<InputError message={errors.password} />
</div>
@@ -76,7 +103,7 @@ export default function Login({ status, canResetPassword }: Props) {
name="remember"
tabIndex={3}
/>
<Label htmlFor="remember">Remember me</Label>
<Label htmlFor="remember">Manter sessão</Label>
</div>
<Button
@@ -87,7 +114,7 @@ export default function Login({ status, canResetPassword }: Props) {
data-test="login-button"
>
{processing && <Spinner />}
Log in
Entrar
</Button>
</div>

View File

@@ -14,6 +14,12 @@ import { edit } from '@/routes/security';
type Props = {
passwordRules: string;
microsoft: {
enabled: boolean;
linked: boolean;
email: string | null;
lastLoginAt: string | null;
};
} & ManagePasskeysProps &
ManageTwoFactorProps;
@@ -133,6 +139,49 @@ export default function Security(props: Props) {
canManagePasskeys={props.canManagePasskeys}
passkeys={props.passkeys}
/>
{props.microsoft.enabled && (
<div className="space-y-6">
<Heading
variant="small"
title="Conta institucional Microsoft"
description="Associe explicitamente a identidade institucional para poder iniciar sessão pelo Microsoft Entra."
/>
{props.microsoft.linked ? (
<div className="space-y-4 rounded-lg border p-4">
<div>
<p className="font-medium">
Identidade associada
</p>
<p className="text-sm text-muted-foreground">
{props.microsoft.email}
</p>
</div>
<Form
method="delete"
action="/settings/microsoft-identity"
>
{({ processing }) => (
<Button
type="submit"
variant="destructive"
disabled={processing}
>
Remover associação
</Button>
)}
</Form>
</div>
) : (
<Button variant="outline" asChild>
<a href="/settings/microsoft-identity/redirect">
Associar conta Microsoft
</a>
</Button>
)}
</div>
)}
</>
);
}