mirror of
https://github.com/AmruthPillai/Reactive-Resume.git
synced 2026-07-25 01:15:26 +10:00
fix(auth): avoid dropping session_token Set-Cookie on sign-in (#2718)
* fix credentials sign-in session cookie handling * refactor(auth): rename originWith to withHostname * refactor(auth): preserve localhost/127.0.0.1 sibling trust with LOCAL_ORIGINS * update dependencies, update code style --------- Co-authored-by: Amruth Pillai <im.amruth@gmail.com>
This commit is contained in:
@@ -3,7 +3,6 @@ import { drizzleAdapter } from "better-auth/adapters/drizzle";
|
||||
import { betterAuth } from "better-auth/minimal";
|
||||
import { apiKey, type GenericOAuthConfig, genericOAuth, openAPI, twoFactor } from "better-auth/plugins";
|
||||
import { username } from "better-auth/plugins/username";
|
||||
import { tanstackStartCookies } from "better-auth/tanstack-start";
|
||||
import { and, eq, or } from "drizzle-orm";
|
||||
import { db } from "@/integrations/drizzle/client";
|
||||
import { env } from "@/utils/env";
|
||||
@@ -20,6 +19,24 @@ function isCustomOAuthProviderEnabled() {
|
||||
return Boolean(env.OAUTH_CLIENT_ID) && Boolean(env.OAUTH_CLIENT_SECRET) && (hasDiscovery || hasManual);
|
||||
}
|
||||
|
||||
function getTrustedOrigins(): string[] {
|
||||
const appUrl = new URL(env.APP_URL);
|
||||
const trustedOrigins = new Set<string>([appUrl.origin.replace(/\/$/, "")]);
|
||||
const LOCAL_ORIGINS = ["localhost", "127.0.0.1"];
|
||||
|
||||
if (LOCAL_ORIGINS.includes(appUrl.hostname)) {
|
||||
for (const hostname of LOCAL_ORIGINS) {
|
||||
if (hostname !== appUrl.hostname) {
|
||||
const altUrl = new URL(env.APP_URL);
|
||||
altUrl.hostname = hostname;
|
||||
trustedOrigins.add(altUrl.origin.replace(/\/$/, ""));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return Array.from(trustedOrigins);
|
||||
}
|
||||
|
||||
const getAuthConfig = () => {
|
||||
const authConfigs: GenericOAuthConfig[] = [];
|
||||
|
||||
@@ -69,7 +86,7 @@ const getAuthConfig = () => {
|
||||
database: drizzleAdapter(db, { schema, provider: "pg" }),
|
||||
|
||||
telemetry: { enabled: false },
|
||||
trustedOrigins: [env.APP_URL],
|
||||
trustedOrigins: getTrustedOrigins(),
|
||||
advanced: {
|
||||
database: { generateId },
|
||||
useSecureCookies: env.APP_URL.startsWith("https://"),
|
||||
@@ -229,7 +246,6 @@ const getAuthConfig = () => {
|
||||
}),
|
||||
twoFactor({ issuer: "Reactive Resume" }),
|
||||
genericOAuth({ config: authConfigs }),
|
||||
tanstackStartCookies(),
|
||||
],
|
||||
});
|
||||
};
|
||||
|
||||
+28
-29
@@ -3,7 +3,6 @@ import { t } from "@lingui/core/macro";
|
||||
import { Trans } from "@lingui/react/macro";
|
||||
import { ArrowRightIcon, EyeIcon, EyeSlashIcon } from "@phosphor-icons/react";
|
||||
import { createFileRoute, Link, redirect, useNavigate, useRouter } from "@tanstack/react-router";
|
||||
import type { BetterFetchOption } from "better-auth/client";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { toast } from "sonner";
|
||||
import { useToggle } from "usehooks-ts";
|
||||
@@ -46,37 +45,37 @@ function RouteComponent() {
|
||||
const onSubmit = async (data: FormValues) => {
|
||||
const toastId = toast.loading(t`Signing in...`);
|
||||
|
||||
const fetchOptions: BetterFetchOption = {
|
||||
onSuccess: (context) => {
|
||||
// Check if 2FA is required
|
||||
if (context.data && "twoFactorRedirect" in context.data && context.data.twoFactorRedirect) {
|
||||
toast.dismiss(toastId);
|
||||
navigate({ to: "/auth/verify-2fa", replace: true });
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const isEmail = data.identifier.includes("@");
|
||||
|
||||
// Normal login success
|
||||
router.invalidate();
|
||||
const result = isEmail
|
||||
? await authClient.signIn.email({ email: data.identifier, password: data.password })
|
||||
: await authClient.signIn.username({ username: data.identifier, password: data.password });
|
||||
|
||||
if (result.error) {
|
||||
toast.error(result.error.message, { id: toastId });
|
||||
return;
|
||||
}
|
||||
|
||||
const requiresTwoFactor =
|
||||
result.data &&
|
||||
typeof result.data === "object" &&
|
||||
"twoFactorRedirect" in result.data &&
|
||||
result.data.twoFactorRedirect;
|
||||
|
||||
// Credential check passed, but the account still requires a 2FA verification step.
|
||||
if (requiresTwoFactor) {
|
||||
toast.dismiss(toastId);
|
||||
navigate({ to: "/dashboard", replace: true });
|
||||
},
|
||||
onError: ({ error }) => {
|
||||
toast.error(error.message, { id: toastId });
|
||||
},
|
||||
};
|
||||
navigate({ to: "/auth/verify-2fa", replace: true });
|
||||
return;
|
||||
}
|
||||
|
||||
if (data.identifier.includes("@")) {
|
||||
await authClient.signIn.email({
|
||||
email: data.identifier,
|
||||
password: data.password,
|
||||
fetchOptions,
|
||||
});
|
||||
} else {
|
||||
await authClient.signIn.username({
|
||||
username: data.identifier,
|
||||
password: data.password,
|
||||
fetchOptions,
|
||||
});
|
||||
// Refresh route context so protected routes can read the newly established session.
|
||||
await router.invalidate();
|
||||
toast.dismiss(toastId);
|
||||
navigate({ to: "/dashboard", replace: true });
|
||||
} catch {
|
||||
toast.error(t`Failed to sign in. Please try again.`, { id: toastId });
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
Reference in New Issue
Block a user