Skip to content

Commit ed856c5

Browse files
committed
feat(webapp): put the whole admin dashboard behind the flag, renamed ADMIN_DASHBOARD_ENABLED
The requireSuper authorization branch now honors the flag, so every admin dashboard page redirects away when disabled; the display-permission booleans and useHasAdminAccess follow suit, hiding the admin nav affordances. The hand-rolled admin.data-stores checks get the same gate.
1 parent d827297 commit ed856c5

19 files changed

Lines changed: 52 additions & 40 deletions
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
---
2+
area: webapp
3+
type: feature
4+
---
5+
6+
The admin dashboard and user impersonation can now be disabled for an entire instance by setting `ADMIN_DASHBOARD_ENABLED=0`.

.server-changes/impersonation-enabled-flag.md

Lines changed: 0 additions & 6 deletions
This file was deleted.

.triggerdotdev/preview-env.yml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
# TEMP - DO NOT MERGE: preview-only override to demo the disabled state.
22
# Revert this commit before merging.
33
web_app:
4-
IMPERSONATION_ENABLED: "0"
4+
ADMIN_DASHBOARD_ENABLED: "0"
55
api:
6-
IMPERSONATION_ENABLED: "0"
6+
ADMIN_DASHBOARD_ENABLED: "0"

apps/webapp/app/env.server.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -332,8 +332,8 @@ const EnvironmentSchema = z
332332
.refine(isValidRegex, "WHITELISTED_EMAILS must be a valid regex.")
333333
.optional(),
334334
ADMIN_EMAILS: z.string().refine(isValidRegex, "ADMIN_EMAILS must be a valid regex.").optional(),
335-
// Instance-level kill switch for user impersonation.
336-
IMPERSONATION_ENABLED: BoolEnv.default(true),
335+
// Instance-level kill switch for the admin dashboard and user impersonation.
336+
ADMIN_DASHBOARD_ENABLED: BoolEnv.default(true),
337337
REMIX_APP_PORT: z.string().optional(),
338338
// Opt-in, dev-only: stream this process's logs over a local telnet/TCP socket on this port.
339339
// Read directly from process.env in server.ts (before this schema loads); declared here for discoverability.

apps/webapp/app/hooks/useUser.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,12 @@ export function useHasAdminAccess(matches?: UIMatch[]): boolean {
4848
const user = useOptionalUser(matches);
4949
const isImpersonating = useIsImpersonating(matches);
5050
const isViewingAsUser = useIsViewingAsUser(matches);
51+
const routeMatch = useTypedMatchesData<typeof loader>({
52+
id: "root",
53+
matches,
54+
});
55+
56+
if (routeMatch?.adminDashboardEnabled === false) return false;
5157

5258
return (Boolean(user?.admin) || isImpersonating) && !isViewingAsUser;
5359
}

apps/webapp/app/models/admin.server.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -18,8 +18,8 @@ const pageSize = 20;
1818

1919
// 404, not 403, so a disabled instance doesn't advertise the feature.
2020
// Stopping an impersonation is deliberately never gated.
21-
export function requireImpersonationEnabled(): void {
22-
if (!env.IMPERSONATION_ENABLED) {
21+
export function requireAdminDashboardEnabled(): void {
22+
if (!env.ADMIN_DASHBOARD_ENABLED) {
2323
throw new Response("Not Found", { status: 404 });
2424
}
2525
}
@@ -226,7 +226,7 @@ export async function redirectWithImpersonation(
226226
currentUser?: { id: string; admin: boolean },
227227
prismaClient: PrismaClientOrTransaction = prisma
228228
) {
229-
requireImpersonationEnabled();
229+
requireAdminDashboardEnabled();
230230

231231
const user = currentUser ?? (await requireUser(request));
232232
if (!user.admin) {
@@ -343,7 +343,7 @@ export async function startImpersonation(
343343

344344
export async function clearImpersonation(request: Request, path: string) {
345345
const authUser = await authenticator.isAuthenticated(request);
346-
// Raw read: stops must audit and clear even with IMPERSONATION_ENABLED off.
346+
// Raw read: stops must audit and clear even with ADMIN_DASHBOARD_ENABLED off.
347347
const targetId = await getRawImpersonationId(request);
348348

349349
if (targetId && authUser?.userId) {

apps/webapp/app/root.tsx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -120,7 +120,7 @@ export const loader = async ({ request }: LoaderFunctionArgs) => {
120120
// impersonating.
121121
// Flag off: terminate lingering impersonation sessions (audit + clear)
122122
// rather than leaving a cookie that would resurrect on a later re-enable.
123-
if (!env.IMPERSONATION_ENABLED && (await getRawImpersonationId(request))) {
123+
if (!env.ADMIN_DASHBOARD_ENABLED && (await getRawImpersonationId(request))) {
124124
const url = new URL(request.url);
125125
throw await clearImpersonation(request, `${url.pathname}${url.search}`);
126126
}
@@ -134,6 +134,7 @@ export const loader = async ({ request }: LoaderFunctionArgs) => {
134134
{
135135
user,
136136
isViewingAsUser,
137+
adminDashboardEnabled: env.ADMIN_DASHBOARD_ENABLED,
137138
toastMessage,
138139
posthogProjectKey,
139140
posthogUiHost,

apps/webapp/app/routes/@.runs.$runParam.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import { z } from "zod";
33
import { prisma } from "~/db.server";
44
import { runStore } from "~/v3/runStore.server";
55
import { controlPlaneResolver } from "~/v3/runOpsMigration/controlPlaneResolver.server";
6-
import { requireImpersonationEnabled } from "~/models/admin.server";
6+
import { requireAdminDashboardEnabled } from "~/models/admin.server";
77
import { redirectWithErrorMessage } from "~/models/message.server";
88
import { requireUser } from "~/services/session.server";
99
import { impersonate, rootPath, v3RunPath, v3RunSpanPath } from "~/utils/pathBuilder";
@@ -14,7 +14,7 @@ const ParamsSchema = z.object({
1414
});
1515

1616
export async function loader({ params, request }: LoaderFunctionArgs) {
17-
requireImpersonationEnabled();
17+
requireAdminDashboardEnabled();
1818

1919
const user = await requireUser(request);
2020

apps/webapp/app/routes/_app.@.orgs.$organizationSlug.$.tsx

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ import { env } from "~/env.server";
1010
import {
1111
clearImpersonation,
1212
findImpersonationTarget,
13-
requireImpersonationEnabled,
13+
requireAdminDashboardEnabled,
1414
startImpersonation,
1515
} from "~/models/admin.server";
1616
import { logger } from "~/services/logger.server";
@@ -27,7 +27,7 @@ import { isSameOriginNavigation } from "~/utils/sameOriginNavigation";
2727
// here would drag server-only modules into the client build.
2828

2929
export async function loader({ request, params }: LoaderFunctionArgs) {
30-
requireImpersonationEnabled();
30+
requireAdminDashboardEnabled();
3131

3232
const user = await requireUser(request);
3333

@@ -104,7 +104,7 @@ function refererOrigin(request: Request): string | undefined {
104104
}
105105

106106
export async function action({ request, params }: ActionFunctionArgs) {
107-
requireImpersonationEnabled();
107+
requireAdminDashboardEnabled();
108108

109109
if (request.method.toLowerCase() !== "post") {
110110
return new Response("Method not allowed", { status: 405 });

apps/webapp/app/routes/admin._index.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ export const loader = dashboardLoader(
3737
}
3838
const result = await adminGetUsers(user.id, searchParams.params.getAll());
3939

40-
return typedjson({ ...result, impersonationEnabled: env.IMPERSONATION_ENABLED });
40+
return typedjson({ ...result, impersonationEnabled: env.ADMIN_DASHBOARD_ENABLED });
4141
}
4242
);
4343

0 commit comments

Comments
 (0)