124 lines
4.1 KiB
TypeScript
124 lines
4.1 KiB
TypeScript
import { Outlet, useLoaderData } from "@remix-run/react";
|
|
import { redirect, type LoaderFunctionArgs } from "@remix-run/server-runtime";
|
|
import { RouteErrorDisplay } from "~/components/ErrorDisplay";
|
|
import { DashboardAgent } from "~/components/dashboard-agent/DashboardAgent";
|
|
import { prisma } from "~/db.server";
|
|
import { updateCurrentProjectEnvironmentId } from "~/services/dashboardPreferences.server";
|
|
import { logger } from "~/services/logger.server";
|
|
import { requireUser } from "~/services/session.server";
|
|
import { tenantContext } from "~/services/tenantContext.server";
|
|
import { EnvironmentParamSchema, v3ProjectPath } from "~/utils/pathBuilder";
|
|
import { canAccessDashboardAgent } from "~/v3/canAccessDashboardAgent.server";
|
|
|
|
export const loader = async ({ request, params }: LoaderFunctionArgs) => {
|
|
const user = await requireUser(request);
|
|
const { organizationSlug, projectParam, envParam } = EnvironmentParamSchema.parse(params);
|
|
|
|
const project = await prisma.project.findFirst({
|
|
where: {
|
|
slug: projectParam,
|
|
organization: {
|
|
slug: organizationSlug,
|
|
members: {
|
|
some: {
|
|
userId: user.id,
|
|
},
|
|
},
|
|
},
|
|
deletedAt: null,
|
|
},
|
|
select: {
|
|
id: true,
|
|
externalRef: true,
|
|
organization: { select: { id: true, featureFlags: true } },
|
|
environments: {
|
|
select: {
|
|
id: true,
|
|
type: true,
|
|
slug: true,
|
|
orgMember: {
|
|
select: {
|
|
userId: true,
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
});
|
|
|
|
if (!project) {
|
|
logger.error("Project not found", { params, user });
|
|
throw new Response("Project not Found", { status: 404, statusText: "Project not found" });
|
|
}
|
|
|
|
const environments = project.environments.filter((env) => env.slug === envParam);
|
|
if (environments.length === 0) {
|
|
return redirect(v3ProjectPath({ slug: organizationSlug }, { slug: projectParam }));
|
|
}
|
|
|
|
let environmentId: string | undefined = undefined;
|
|
let environmentType: "DEVELOPMENT" | "PREVIEW" | "STAGING" | "PRODUCTION" | undefined;
|
|
|
|
if (environments.length > 1) {
|
|
const bestEnvironment = environments.find((env) => env.orgMember?.userId === user.id);
|
|
if (!bestEnvironment) {
|
|
throw new Response("Environment not Found", {
|
|
status: 404,
|
|
statusText: "Environment not found",
|
|
});
|
|
}
|
|
|
|
environmentId = bestEnvironment.id;
|
|
environmentType = bestEnvironment.type;
|
|
} else {
|
|
environmentId = environments[0].id;
|
|
environmentType = environments[0].type;
|
|
}
|
|
|
|
// userId is enriched higher up in `_app/route.tsx`; only stamp tenant fields here.
|
|
tenantContext.enrich({
|
|
orgId: project.organization.id,
|
|
projectId: project.id,
|
|
projectRef: project.externalRef,
|
|
envId: environmentId,
|
|
envType: environmentType,
|
|
});
|
|
|
|
await updateCurrentProjectEnvironmentId({ user: user, projectId: project.id, environmentId });
|
|
|
|
// Resolve dashboard-agent access here (single source of truth: global env,
|
|
// admins/impersonators, then the global/per-org feature flag, default off) so
|
|
// the launcher button is hidden when it's not enabled. The org's featureFlags
|
|
// came from the membership-checked project query above, so we pass them in to
|
|
// avoid a second org lookup.
|
|
const hasDashboardAgentAccess = await canAccessDashboardAgent({
|
|
userId: user.id,
|
|
isAdmin: user.admin,
|
|
isImpersonating: user.isImpersonating,
|
|
organizationSlug,
|
|
orgFeatureFlags: (project.organization.featureFlags as Record<string, unknown>) ?? {},
|
|
});
|
|
|
|
return {
|
|
...project,
|
|
hasDashboardAgentAccess,
|
|
};
|
|
};
|
|
|
|
export default function Page() {
|
|
const { hasDashboardAgentAccess } = useLoaderData<typeof loader>();
|
|
return (
|
|
<DashboardAgent hasAccess={hasDashboardAgentAccess}>
|
|
<Outlet />
|
|
</DashboardAgent>
|
|
);
|
|
}
|
|
|
|
// Caught here (inside the project SideMenu's Outlet) rather than at the project
|
|
// layout, so a permission denial or error on any env-scoped page renders in the
|
|
// content pane with the SideMenu intact. RouteErrorDisplay renders the
|
|
// permission panel for a 403 and the generic error otherwise.
|
|
export function ErrorBoundary() {
|
|
return <RouteErrorDisplay />;
|
|
}
|