Disable Portal UI for guests (#6936)

# Description of Changes
Disallow SaaS guests from accessing the portal. One day we might want to
make this better so they can go there but then have to sign up before
doing anything useful, but this is the easiest way to disallow it for
now.
This commit is contained in:
James Brunton
2026-07-09 11:44:19 +00:00
committed by GitHub
parent 9d11918bd8
commit f29500c138
2 changed files with 36 additions and 12 deletions
@@ -4,9 +4,14 @@ import { render, screen } from "@testing-library/react";
import { allowConsole } from "@app/tests/failOnConsole";
// Controllable auth state for the mocked provider.
const authState: { session: unknown; loading: boolean } = {
const authState: {
session: unknown;
loading: boolean;
isAnonymous: boolean;
} = {
session: null,
loading: false,
isAnonymous: false,
};
vi.mock("@app/auth", () => ({
@@ -23,9 +28,10 @@ describe("PortalAuthBoundary — SaaS", () => {
beforeEach(() => {
authState.session = null;
authState.loading = false;
authState.isAnonymous = false;
});
it("renders the portal when a Supabase session is present (no login)", () => {
it("renders the portal when a real (non-guest) Supabase session is present", () => {
authState.session = { user: { id: "u1" }, access_token: "tok" };
render(
<PortalAuthBoundary>
@@ -35,6 +41,20 @@ describe("PortalAuthBoundary — SaaS", () => {
expect(screen.getByTestId("portal")).toBeInTheDocument();
});
it("gates (does not render the portal) for an anonymous guest session", () => {
authState.session = { user: { id: "guest" }, access_token: "tok" };
authState.isAnonymous = true;
// The gate bounces a guest to the editor; jsdom doesn't implement
// navigation, so absorb that incidental warning.
allowConsole.error(/not implemented|navigation/i);
render(
<PortalAuthBoundary>
<div data-testid="portal">PORTAL</div>
</PortalAuthBoundary>,
);
expect(screen.queryByTestId("portal")).not.toBeInTheDocument();
});
it("gates (does not render the portal) when there is no session", () => {
authState.session = null;
// The gate bounces to /login; jsdom doesn't implement navigation, so absorb
@@ -3,6 +3,7 @@ import { AuthProvider } from "@app/auth";
import { useAuth } from "@app/auth/context";
import { Spinner } from "@app/ui";
import { ensureSaasSupabase } from "@portal/auth/saasSupabase";
import { EDITOR_URL } from "@portal/auth/editorUrl";
function FullScreen({ children }: { children: ReactNode }) {
return (
@@ -20,19 +21,22 @@ function FullScreen({ children }: { children: ReactNode }) {
}
/**
* SaaS gate: viewing your own usage is not admin-gated, so require only a session
* (not portalAccess). No session → bounce to the editor's Supabase login, which
* returns here signed in. This is deliberately laxer than the self-hosted
* RequirePortalAccess admin gate.
* SaaS gate: viewing your own usage is not admin-gated, so any real (signed-in,
* non-guest) account may enter - deliberately laxer than the self-hosted
* RequirePortalAccess admin gate. But an anonymous guest session has no account
* to view or manage, so it is not eligible: bounce it to the editor (where a
* guest can sign up), mirroring the self-hosted forbidden path. No session at
* all -> the editor's Supabase login, which returns here signed in.
*/
function SaasPortalGate({ children }: { children: ReactNode }) {
const { session, loading } = useAuth();
const { session, loading, isAnonymous } = useAuth();
const blocked = !loading && (!session || isAnonymous);
useEffect(() => {
if (!loading && !session) {
window.location.href = "/login";
}
}, [loading, session]);
if (loading || !session) {
if (!blocked) return;
// Guest (has a session but anonymous) -> editor; no session -> login.
window.location.href = session ? EDITOR_URL : "/login";
}, [blocked, session]);
if (loading || blocked) {
return (
<FullScreen>
<Spinner size="lg" />