add server action proxies

This commit is contained in:
miloschwartz
2025-09-25 17:14:25 -07:00
parent 21f0cd6e3f
commit e555d3c496
9 changed files with 663 additions and 164 deletions

View File

@@ -28,7 +28,6 @@ import { AxiosResponse } from "axios";
import { formatAxiosError } from "@app/lib/api";
import { LockIcon, FingerprintIcon } from "lucide-react";
import { createApiClient } from "@app/lib/api";
import { useEnvContext } from "@app/hooks/useEnvContext";
import {
InputOTP,
InputOTPGroup,
@@ -42,6 +41,14 @@ import { GenerateOidcUrlResponse } from "@server/routers/idp";
import { Separator } from "./ui/separator";
import { useTranslations } from "next-intl";
import { startAuthentication } from "@simplewebauthn/browser";
import {
generateOidcUrlProxy,
loginProxy,
securityKeyStartProxy,
securityKeyVerifyProxy
} from "@app/actions/server";
import { redirect as redirectTo } from "next/navigation";
import { useEnvContext } from "@app/hooks/useEnvContext";
export type LoginFormIDP = {
idpId: number;
@@ -70,6 +77,9 @@ export default function LoginForm({ redirect, onLogin, idps }: LoginFormProps) {
const [showSecurityKeyPrompt, setShowSecurityKeyPrompt] = useState(false);
const t = useTranslations();
const currentHost = typeof window !== "undefined" ? window.location.hostname : "";
const expectedHost = new URL(env.app.dashboardUrl).host;
const isExpectedHost = currentHost === expectedHost;
const formSchema = z.object({
email: z.string().email({ message: t("emailInvalid") }),
@@ -102,39 +112,39 @@ export default function LoginForm({ redirect, onLogin, idps }: LoginFormProps) {
try {
// Start WebAuthn authentication without email
const startRes = await api.post(
"/auth/security-key/authenticate/start",
{}
);
const startResponse = await securityKeyStartProxy({});
if (!startRes) {
setError(
t("securityKeyAuthError", {
defaultValue:
"Failed to start security key authentication"
})
);
if (startResponse.error) {
setError(startResponse.message);
return;
}
const { tempSessionId, ...options } = startRes.data.data;
const { tempSessionId, ...options } = startResponse.data!;
// Perform WebAuthn authentication
try {
const credential = await startAuthentication(options);
const credential = await startAuthentication({
optionsJSON: {
...options,
userVerification: options.userVerification as
| "required"
| "preferred"
| "discouraged"
}
});
// Verify authentication
const verifyRes = await api.post(
"/auth/security-key/authenticate/verify",
const verifyResponse = await securityKeyVerifyProxy(
{ credential },
{
headers: {
"X-Temp-Session-Id": tempSessionId
}
}
tempSessionId
);
if (verifyRes) {
if (verifyResponse.error) {
setError(verifyResponse.message);
return;
}
if (verifyResponse.success) {
if (onLogin) {
await onLogin();
}
@@ -208,30 +218,44 @@ export default function LoginForm({ redirect, onLogin, idps }: LoginFormProps) {
setShowSecurityKeyPrompt(false);
try {
const res = await api.post<AxiosResponse<LoginResponse>>(
"/auth/login",
{
email,
password,
code
const response = await loginProxy({
email,
password,
code
});
if (response.error) {
setError(response.message);
return;
}
const data = response.data;
// Handle case where data is null (e.g., already logged in)
if (!data) {
if (onLogin) {
await onLogin();
}
);
return;
}
const data = res.data.data;
if (data?.useSecurityKey) {
if (data.useSecurityKey) {
await initiateSecurityKeyAuth();
return;
}
if (data?.codeRequested) {
if (data.codeRequested) {
setMfaRequested(true);
setLoading(false);
mfaForm.reset();
return;
}
if (data?.emailVerificationRequired) {
if (data.emailVerificationRequired) {
if (!isExpectedHost) {
setError(t("emailVerificationRequired", { dashboardUrl: env.app.dashboardUrl }));
return;
}
if (redirect) {
router.push(`/auth/verify-email?redirect=${redirect}`);
} else {
@@ -240,7 +264,11 @@ export default function LoginForm({ redirect, onLogin, idps }: LoginFormProps) {
return;
}
if (data?.twoFactorSetupRequired) {
if (data.twoFactorSetupRequired) {
if (!isExpectedHost) {
setError(t("twoFactorSetupRequired", { dashboardUrl: env.app.dashboardUrl }));
return;
}
const setupUrl = `/auth/2fa/setup?email=${encodeURIComponent(email)}${redirect ? `&redirect=${encodeURIComponent(redirect)}` : ""}`;
router.push(setupUrl);
return;
@@ -275,25 +303,26 @@ export default function LoginForm({ redirect, onLogin, idps }: LoginFormProps) {
}
async function loginWithIdp(idpId: number) {
let redirectUrl: string | undefined;
try {
const res = await api.post<AxiosResponse<GenerateOidcUrlResponse>>(
`/auth/idp/${idpId}/oidc/generate-url`,
{
redirectUrl: redirect || "/"
}
const data = await generateOidcUrlProxy(
idpId,
redirect || "/"
);
console.log(res);
if (!res) {
setError(t("loginError"));
const url = data.data?.redirectUrl;
if (data.error) {
setError(data.message);
return;
}
const data = res.data.data;
window.location.href = data.redirectUrl;
} catch (e) {
console.error(formatAxiosError(e));
if (url) {
redirectUrl = url;
}
} catch (e: any) {
setError(e.message || t("loginError"));
console.error(e);
}
if (redirectUrl) {
redirectTo(redirectUrl);
}
}
@@ -355,7 +384,7 @@ export default function LoginForm({ redirect, onLogin, idps }: LoginFormProps) {
<div className="text-center">
<Link
href={`/auth/reset-password${form.getValues().email ? `?email=${form.getValues().email}` : ""}`}
href={`${env.app.dashboardUrl}/auth/reset-password${form.getValues().email ? `?email=${form.getValues().email}` : ""}`}
className="text-sm text-muted-foreground"
>
{t("passwordForgot")}