Files
Reactive-Resume/apps/web/src/features/auth/pages/login.tsx
T
Amruth Pillai fe9b59e111 fix: restore MCP OAuth registration and authorization (#3421)
* fix: align MCP OAuth provider schema and authorization flow

* test: isolate OpenAPI generation from OAuth initialization

* fix: accept auth routes without a callback query

* fix: require explicit OAuth consent and preserve signed requests

* test: verify OAuth audiences through real MCP initialization

* test(e2e): isolate OAuth token audience validation
2026-09-05 09:33:15 -07:00

257 lines
8.0 KiB
TypeScript

import { t } from "@lingui/core/macro";
import { Trans } from "@lingui/react/macro";
import { ArrowRightIcon, EyeIcon, EyeSlashIcon } from "@phosphor-icons/react";
import { useQuery } from "@tanstack/react-query";
import { Link, useNavigate, useRouter, useSearch } from "@tanstack/react-router";
import { useEffect, useRef } from "react";
import { useToggle } from "usehooks-ts";
import z from "zod";
import { Button } from "@reactive-resume/ui/components/button";
import { FormControl, FormDescription, FormItem, FormLabel, FormMessage } from "@reactive-resume/ui/components/form";
import { Input } from "@reactive-resume/ui/components/input";
import { toast } from "@reactive-resume/ui/components/toast";
import { authClient } from "@/libs/auth/client";
import { orpc } from "@/libs/orpc/client";
import { useAppForm } from "@/libs/tanstack-form";
import { SocialAuth } from "../components/social-auth";
import { getAuthRedirectOptions, getOAuthPasskeyOptions, getOAuthSignInOptions, isOAuthRedirect } from "../redirect";
const formSchema = z.object({
identifier: z.string().trim().toLowerCase(),
password: z.string().trim().min(6).max(64),
});
type Props = {
disableEmailAuth: boolean;
disableSignups: boolean;
};
export function LoginPage({ disableEmailAuth, disableSignups }: Props) {
const router = useRouter();
const { callbackURL, reauthenticate } = useSearch({ from: "/auth" });
const navigate = useNavigate();
const hasStartedConditionalPasskeyRef = useRef(false);
const [showPassword, toggleShowPassword] = useToggle(false);
const { data: providers = {} } = useQuery(orpc.auth.providers.list.queryOptions());
const form = useAppForm({
defaultValues: { identifier: "", password: "" },
validators: { onSubmit: formSchema },
onSubmit: async ({ value }) => {
const toastId = toast.add({ type: "loading", description: t`Signing in...` });
try {
const isEmail = value.identifier.includes("@");
const result = isEmail
? await authClient.signIn.email({
email: value.identifier,
password: value.password,
...getOAuthSignInOptions(callbackURL),
})
: await authClient.signIn.username({
username: value.identifier,
password: value.password,
...getOAuthSignInOptions(callbackURL),
});
if (result.error) {
toast.add({
type: "error",
description:
result.error.message ||
t({
comment: "Fallback toast when sign-in fails and no server error message is available",
message: "Failed to sign in. Please try again.",
}),
id: toastId,
});
return;
}
const requiresTwoFactor =
result.data &&
typeof result.data === "object" &&
"twoFactorRedirect" in result.data &&
result.data.twoFactorRedirect;
if (requiresTwoFactor) {
toast.close(toastId);
void navigate({ to: "/auth/verify-2fa", search: { callbackURL, reauthenticate }, replace: true });
return;
}
toast.close(toastId);
if (isOAuthRedirect(result.data)) return;
await router.invalidate();
void navigate(getAuthRedirectOptions(callbackURL));
} catch {
toast.add({ type: "error", description: t`Failed to sign in. Please try again.`, id: toastId });
}
},
});
useEffect(() => {
if (!("passkey" in providers)) return;
if (typeof window === "undefined") return;
if (!("PublicKeyCredential" in window)) return;
if (!PublicKeyCredential.isConditionalMediationAvailable) return;
if (hasStartedConditionalPasskeyRef.current) return;
hasStartedConditionalPasskeyRef.current = true;
void PublicKeyCredential.isConditionalMediationAvailable().then(async (isAvailable) => {
if (!isAvailable) return;
const { data, error } = await authClient.signIn.passkey({
autoFill: true,
...getOAuthPasskeyOptions(callbackURL),
});
if (error || isOAuthRedirect(data)) return;
await router.invalidate();
void navigate(getAuthRedirectOptions(callbackURL));
});
}, [providers, router, navigate, callbackURL]);
return (
<>
<div className="space-y-1 text-center">
<h1 className="font-semibold text-2xl tracking-tight">
<Trans comment="Title on the login page">Sign in to your account</Trans>
</h1>
{!disableSignups && (
<div className="text-muted-foreground">
<Trans>
Don't have an account?{" "}
<Button
variant="link"
nativeButton={false}
className="h-auto gap-1.5 px-1! py-0"
render={
<Link to="/auth/register" search={{ callbackURL, reauthenticate }}>
<Trans comment="Call-to-action link from login page to account registration page">
Create one now
</Trans>{" "}
<ArrowRightIcon />
</Link>
}
/>
</Trans>
</div>
)}
</div>
{!disableEmailAuth && (
<form
className="space-y-6"
onSubmit={(event) => {
event.preventDefault();
event.stopPropagation();
void form.handleSubmit();
}}
>
<form.Field name="identifier">
{(field) => (
<FormItem hasError={field.state.meta.isTouched && field.state.meta.errors.length > 0}>
<FormLabel>
<Trans comment="Label for login identifier input that accepts email or username">Email Address</Trans>
</FormLabel>
<FormControl
render={
<Input
autoComplete="section-login username webauthn"
placeholder={t({
comment: "Example email placeholder for login identifier field",
message: "john.doe@example.com",
})}
className="lowercase"
name={field.name}
value={field.state.value}
onBlur={field.handleBlur}
onChange={(event) => field.handleChange(event.target.value)}
/>
}
/>
<FormMessage errors={field.state.meta.errors} />
<FormDescription>
<Trans>You can also sign in with your username.</Trans>
</FormDescription>
</FormItem>
)}
</form.Field>
<form.Field name="password">
{(field) => (
<FormItem hasError={field.state.meta.isTouched && field.state.meta.errors.length > 0}>
<div className="flex items-center justify-between">
<FormLabel>
<Trans comment="Label for password input on login form">Password</Trans>
</FormLabel>
<Button
tabIndex={-1}
variant="link"
nativeButton={false}
className="h-auto p-0 text-xs leading-none"
render={
<Link to="/auth/forgot-password">
<Trans comment="Link label to password reset page from login form">Forgot Password?</Trans>
</Link>
}
/>
</div>
<div className="flex items-center gap-x-1.5">
<FormControl
render={
<Input
min={6}
max={64}
type={showPassword ? "text" : "password"}
autoComplete="section-login current-password"
name={field.name}
value={field.state.value}
onBlur={field.handleBlur}
onChange={(event) => field.handleChange(event.target.value)}
/>
}
/>
<Button
size="icon"
variant="ghost"
onClick={toggleShowPassword}
aria-label={
showPassword
? t({
comment: "Accessible label for button that hides the password in login form",
message: "Hide password",
})
: t({
comment: "Accessible label for button that reveals the password in login form",
message: "Show password",
})
}
>
{showPassword ? <EyeIcon /> : <EyeSlashIcon />}
</Button>
</div>
<FormMessage errors={field.state.meta.errors} />
</FormItem>
)}
</form.Field>
<Button type="submit" className="w-full">
<Trans comment="Primary action button label on login form">Sign in</Trans>
</Button>
</form>
)}
<SocialAuth />
</>
);
}