Files
Saadi Myftija 1eda438a41 feat(webapp): put the admin dashboard behind an env var flag (#4774)
Adds an `ADMIN_DASHBOARD_ENABLED` env var (default: enabled) that turns
the admin dashboard and user impersonation off for an entire instance.

When disabled:
- every admin dashboard page redirects away, and the admin navigation
isn't rendered
- existing impersonation cookies are ignored, and any lingering session
is actively terminated with an audit record
- every flow that could start an impersonation responds 404, and no
impersonation tokens are minted

Stopping an impersonation always works regardless of the flag, so
nothing gets stuck. Machine-to-machine admin API endpoints are not
affected. The variable is documented for self-hosters; instances that
don't set it are unaffected.
2026-08-25 15:37:43 +02:00

60 lines
1.8 KiB
TypeScript

import { type UIMatch } from "@remix-run/react";
import type { UserWithDashboardPreferences } from "~/models/user.server";
import { type loader } from "~/root";
import { useChanged } from "./useChanged";
import { useTypedMatchesData } from "./useTypedMatchData";
import { useIsImpersonating } from "./useOrganizations";
export type User = UserWithDashboardPreferences;
export function useOptionalUser(matches?: UIMatch[]): User | undefined {
const routeMatch = useTypedMatchesData<typeof loader>({
id: "root",
matches,
});
return routeMatch?.user ?? undefined;
}
export function useUser(matches?: UIMatch[]): User {
const maybeUser = useOptionalUser(matches);
if (!maybeUser) {
throw new Error(
"No user found in root loader, but user is required by useUser. If user is optional, try useOptionalUser instead."
);
}
return maybeUser;
}
export function useUserChanged(callback: (user: User | undefined) => void) {
const user = useOptionalUser();
useChanged(user, callback);
}
/**
* Whether the admin has switched to "view as user" for the current
* impersonation session. Display only — see `hasAdminDisplayAccess`.
*/
export function useIsViewingAsUser(matches?: UIMatch[]): boolean {
const routeMatch = useTypedMatchesData<typeof loader>({
id: "root",
matches,
});
return routeMatch?.isViewingAsUser === true;
}
export function useHasAdminAccess(matches?: UIMatch[]): boolean {
const user = useOptionalUser(matches);
const isImpersonating = useIsImpersonating(matches);
const isViewingAsUser = useIsViewingAsUser(matches);
const routeMatch = useTypedMatchesData<typeof loader>({
id: "root",
matches,
});
if (routeMatch?.adminDashboardEnabled === false) return false;
return (Boolean(user?.admin) || isImpersonating) && !isViewingAsUser;
}