Files
Eric Allam b119a52e08 Implement MFA (#2244)
* Adds a new route for logging in with mfa

* New path for security page

* Adds “Security” link to account side menu

* Update the Switch component to allow label positions left and right

* Optionally hide the Close button in the Dialog title bar

* Installs `qrcode` react package for generating QR codes.

* CopyButton component now takes children

* New Security route for setting up MFA

* Adds new OTP package for the chadcn InputOTP component

* Adds new InputOTP chadcn component

* Adds InputOTP chadcn component to the MFA login screen

* InputOTP component supports variant styles

* Improvements to form handling

* Show a confirmation modal before you can disable MFA

* Revert redirect back to the dashboard for now

* Implement MFA enabling and disabling

* Refactor and cleanup mfa management code

* More cleanup

* Handle errors in the management action

* Implement mfa login flow

* recovery code input should be password

* Implement rate limiting on the mfa validation endpoint

* Better error ux

* Implement mfa emails and apply James' updates

* Use latest @better-auth/utils

* Improvements via CodeRabbit review

---------

Co-authored-by: James Ritchie <james@trigger.dev>
2025-07-08 13:44:47 +01:00

67 lines
1.9 KiB
TypeScript

import { redirect } from "@remix-run/node";
import { getUserById } from "~/models/user.server";
import { authenticator } from "./auth.server";
import { getImpersonationId } from "./impersonation.server";
export async function getUserId(request: Request): Promise<string | undefined> {
const impersonatedUserId = await getImpersonationId(request);
if (impersonatedUserId) return impersonatedUserId;
let authUser = await authenticator.isAuthenticated(request);
return authUser?.userId;
}
export async function getUser(request: Request) {
const userId = await getUserId(request);
if (userId === undefined) return null;
const user = await getUserById(userId);
if (user) return user;
throw await logout(request);
}
export async function requireUserId(request: Request, redirectTo?: string) {
const userId = await getUserId(request);
if (!userId) {
const url = new URL(request.url);
const searchParams = new URLSearchParams([
["redirectTo", redirectTo ?? `${url.pathname}${url.search}`],
]);
throw redirect(`/login?${searchParams}`);
}
return userId;
}
export type UserFromSession = Awaited<ReturnType<typeof requireUser>>;
export async function requireUser(request: Request) {
const userId = await requireUserId(request);
const impersonationId = await getImpersonationId(request);
const user = await getUserById(userId);
if (user) {
return {
id: user.id,
email: user.email,
name: user.name,
displayName: user.displayName,
avatarUrl: user.avatarUrl,
admin: user.admin,
createdAt: user.createdAt,
updatedAt: user.updatedAt,
dashboardPreferences: user.dashboardPreferences,
confirmedBasicDetails: user.confirmedBasicDetails,
mfaEnabledAt: user.mfaEnabledAt,
isImpersonating: !!impersonationId,
};
}
throw await logout(request);
}
export async function logout(request: Request) {
return redirect("/logout");
}