metatron-open-seo/src/client/features/auth/useHostedAuthRouteGuard.ts
metatroncubeswdev c47b032f1a
Some checks failed
CI / ci (push) Has been cancelled
CI / docker-build (push) Has been cancelled
Publish Docker image / docker (push) Has been cancelled
Upload sourcemaps / upload (push) Has been cancelled
Add team auth mode (backend, inert until AUTH_MODE=team)
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>
2026-09-09 00:51:20 -04:00

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,
};
}