Introduces a fourth AUTH_MODE, `team`: Better Auth email/password with the
existing organization/member/role/invitation stack, but none of the hosted
SaaS coupling (no Autumn billing, Turnstile, Loops email, Google social
login, onboarding chat, PostHog, disposable-email block, dub referrals).
- auth-mode.ts: add `team`; add isTeamAuthMode / isSessionAuthMode /
isSessionClientAuthMode ("is there a login session?" vs isHostedAuthMode's
"is this the billed product?").
- auth.ts: createAuth() builds a valid instance for `team` — verification
off, self-serve signup disabled, no captcha/Loops/social. hasTeamAuthConfig
(BETTER_AUTH_URL + BETTER_AUTH_SECRET only) + hasSessionAuthConfig.
- ensure-user: resolve.ts routes `team` through resolveHostedContext;
requireHostedSession + selfHostedOAuth callback accept any session mode.
- api/auth/$.ts: mount the Better Auth handler for `team` too.
- Client: route guards, sidebar account menu / sign-out, settings
Organization tab, invitation accept, and error cards switch from
isHostedClientAuthMode to isSessionClientAuthMode where they mean "has a
session". Sign-in goes straight to the email form (no Google button);
sign-up shows an invite-only notice.
- selfhost-preflight: validate `team` (requires BETTER_AUTH_URL +
BETTER_AUTH_SECRET >= 32 chars).
- .env.example: document `team`.
Ships inert: AUTH_MODE stays local_noauth. tsc / oxlint / knip clean;
test suite unchanged (1164 pass, 1 pre-existing Windows-CRLF failure in
samSkills.test.ts).
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
68 lines
1.7 KiB
TypeScript
68 lines
1.7 KiB
TypeScript
import { useNavigate } from "@tanstack/react-router";
|
|
import { useEffect } from "react";
|
|
import { useSession } from "@/lib/auth-client";
|
|
import {
|
|
isEmailVerificationBypassed,
|
|
isHostedClientAuthMode,
|
|
isSessionClientAuthMode,
|
|
} from "@/lib/auth-mode";
|
|
import {
|
|
getCurrentAuthRedirectFromHref,
|
|
getSignInSearch,
|
|
getVerifyEmailSearch,
|
|
} from "@/lib/auth-redirect";
|
|
|
|
export function useHostedAuthRouteGuard() {
|
|
const navigate = useNavigate();
|
|
const { data: session, isPending } = useSession();
|
|
// `hosted` and `team` both require a Better Auth session; only `hosted` has
|
|
// an email-verification step (team has no transactional email).
|
|
const isSessionMode = isSessionClientAuthMode();
|
|
const isHostedMode = isHostedClientAuthMode();
|
|
const emailVerified =
|
|
!isHostedMode ||
|
|
session?.user?.emailVerified === true ||
|
|
isEmailVerificationBypassed();
|
|
|
|
useEffect(() => {
|
|
if (isPending || !isSessionMode) {
|
|
return;
|
|
}
|
|
|
|
const redirectTo = getCurrentAuthRedirectFromHref(window.location.href);
|
|
|
|
if (!session?.user?.id) {
|
|
void navigate({
|
|
to: "/sign-in",
|
|
search: getSignInSearch(redirectTo),
|
|
replace: true,
|
|
});
|
|
return;
|
|
}
|
|
|
|
if (!emailVerified) {
|
|
void navigate({
|
|
to: "/verify-email",
|
|
search: getVerifyEmailSearch(session.user.email, redirectTo),
|
|
replace: true,
|
|
});
|
|
}
|
|
}, [
|
|
isPending,
|
|
isSessionMode,
|
|
emailVerified,
|
|
session?.user?.email,
|
|
session?.user?.id,
|
|
navigate,
|
|
]);
|
|
|
|
const hasVerifiedHostedSession =
|
|
!isPending && Boolean(session?.user?.id) && emailVerified;
|
|
|
|
return {
|
|
isHostedMode,
|
|
isSessionMode,
|
|
canRenderAuthenticatedContent: !isSessionMode || hasVerifiedHostedSession,
|
|
};
|
|
}
|