Skip to content

Commit 9d601f0

Browse files
committed
dashboardLoader/Action can access the context data
1 parent 43a2924 commit 9d601f0

4 files changed

Lines changed: 49 additions & 32 deletions

File tree

apps/webapp/app/routes/_app.orgs.$organizationSlug.settings.roles/route.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -56,8 +56,8 @@ export const loader = dashboardLoader(
5656
},
5757
authorization: { action: "read", resource: { type: "members" } },
5858
},
59-
async ({ params }) => {
60-
const orgId = await resolveOrgIdFromSlug(params.organizationSlug);
59+
async ({ context }) => {
60+
const orgId = context.organizationId;
6161
if (!orgId) {
6262
throw new Response("Not Found", { status: 404 });
6363
}

apps/webapp/app/routes/_app.orgs.$organizationSlug.settings.team/route.tsx

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -99,8 +99,8 @@ export const loader = dashboardLoader(
9999
},
100100
authorization: { action: "read", resource: { type: "members" } },
101101
},
102-
async ({ user, ability, params }) => {
103-
const orgId = await resolveOrgIdFromSlug(params.organizationSlug);
102+
async ({ user, ability, context }) => {
103+
const orgId = context.organizationId;
104104
if (!orgId) {
105105
throw new Response("Not Found", { status: 404 });
106106
}
@@ -155,7 +155,7 @@ export const action = dashboardAction(
155155
// gated by the existing model layer; purchase-seats by the
156156
// SetSeatsAddOnService). Per-intent ability checks happen inside.
157157
},
158-
async ({ user, ability, request, params }) => {
158+
async ({ user, ability, request, params, context }) => {
159159
const userId = user.id;
160160
const { organizationSlug } = params;
161161
invariant(organizationSlug, "organizationSlug not found");
@@ -167,7 +167,7 @@ export const action = dashboardAction(
167167
if (!ability.can("manage", { type: "members" })) {
168168
return json({ ok: false, error: "Unauthorized" } as const, { status: 403 });
169169
}
170-
const orgId = await resolveOrgIdFromSlug(organizationSlug);
170+
const orgId = context.organizationId;
171171
if (!orgId) {
172172
return json({ ok: false, error: "Organization not found" } as const, { status: 404 });
173173
}

apps/webapp/app/services/routeBuilders/dashboardBuilder.server.ts

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -30,10 +30,16 @@ function isAuthorized(ability: RbacAbility, authorization: AuthorizationOption):
3030
return ability.can(authorization.action, authorization.resource);
3131
}
3232

33-
export async function authenticateAndAuthorize<TParams, TSearchParams>(
33+
type AuthScope = { organizationId?: string; projectId?: string };
34+
35+
export async function authenticateAndAuthorize<
36+
TParams,
37+
TSearchParams,
38+
TContext extends AuthScope
39+
>(
3440
request: Request,
3541
rawParams: unknown,
36-
options: DashboardLoaderOptions<TParams, TSearchParams>
42+
options: DashboardLoaderOptions<TParams, TSearchParams, TContext>
3743
): Promise<
3844
| { ok: false; response: Response }
3945
| {
@@ -42,6 +48,7 @@ export async function authenticateAndAuthorize<TParams, TSearchParams>(
4248
ability: RbacAbility;
4349
params: unknown;
4450
searchParams: unknown;
51+
context: TContext;
4552
}
4653
> {
4754
let parsedParams: any = undefined;
@@ -75,7 +82,9 @@ export async function authenticateAndAuthorize<TParams, TSearchParams>(
7582
parsedSearchParams = parsed.data;
7683
}
7784

78-
const ctx = options.context ? await options.context(parsedParams, request) : {};
85+
const ctx = (options.context
86+
? await options.context(parsedParams, request)
87+
: ({} as TContext)) as TContext;
7988
const auth = await rbac.authenticateSession(request, ctx);
8089
if (!auth.ok) {
8190
if (auth.reason === "unauthenticated") {
@@ -94,5 +103,6 @@ export async function authenticateAndAuthorize<TParams, TSearchParams>(
94103
ability: auth.ability,
95104
params: parsedParams,
96105
searchParams: parsedSearchParams,
106+
context: ctx,
97107
};
98108
}

apps/webapp/app/services/routeBuilders/dashboardBuilder.ts

Lines changed: 30 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -43,19 +43,23 @@ export type AuthorizationOption =
4343
resource: RbacResource | RbacResource[];
4444
};
4545

46-
export type DashboardLoaderOptions<TParams, TSearchParams> = {
46+
// Plugin-side scope: whatever the route's `context` returns must include
47+
// these (or just be `{}` when the route doesn't scope by org/project).
48+
// rbac.authenticateSession reads them off the value to filter UserRole.
49+
type AuthScope = { organizationId?: string; projectId?: string };
50+
51+
export type DashboardLoaderOptions<TParams, TSearchParams, TContext extends AuthScope> = {
4752
params?: TParams;
4853
searchParams?: TSearchParams;
49-
// Optional: provides organizationId / projectId to rbac.authenticateSession
50-
// when the route's ability check needs it. The default fallback
51-
// ignores context; an installed plugin may use it to scope the
52-
// returned ability.
54+
// Resolves any per-request data the handler + auth check both need
55+
// (typically org/project lookups from URL params). The returned object
56+
// is fed to `rbac.authenticateSession` as the auth scope AND passed
57+
// through to the handler in `args.context`, so the route does each
58+
// lookup once.
5359
context?: (
5460
params: InferZod<TParams>,
5561
request: Request
56-
) =>
57-
| { organizationId?: string; projectId?: string }
58-
| Promise<{ organizationId?: string; projectId?: string }>;
62+
) => TContext | Promise<TContext>;
5963
authorization?: AuthorizationOption;
6064
// Where to send unauthenticated requests. Defaults to /login with a
6165
// redirectTo back to the original path.
@@ -65,21 +69,25 @@ export type DashboardLoaderOptions<TParams, TSearchParams> = {
6569
unauthorizedRedirect?: string;
6670
};
6771

68-
export type DashboardLoaderHandlerArgs<TParams, TSearchParams> = {
72+
export type DashboardLoaderHandlerArgs<TParams, TSearchParams, TContext> = {
6973
params: InferZod<TParams>;
7074
searchParams: InferZod<TSearchParams>;
7175
user: SessionUser;
7276
ability: RbacAbility;
77+
context: TContext;
7378
request: Request;
7479
};
7580

7681
export function dashboardLoader<
7782
TParams extends AnyZodSchema | undefined = undefined,
7883
TSearchParams extends AnyZodSchema | undefined = undefined,
84+
TContext extends AuthScope = AuthScope,
7985
TReturn extends Response = Response
8086
>(
81-
options: DashboardLoaderOptions<TParams, TSearchParams>,
82-
handler: (args: DashboardLoaderHandlerArgs<TParams, TSearchParams>) => Promise<TReturn>
87+
options: DashboardLoaderOptions<TParams, TSearchParams, TContext>,
88+
handler: (
89+
args: DashboardLoaderHandlerArgs<TParams, TSearchParams, TContext>
90+
) => Promise<TReturn>
8391
) {
8492
return async function loader({ request, params }: LoaderFunctionArgs): Promise<TReturn> {
8593
// Server-only — see comment at top. Node caches the module after the
@@ -93,30 +101,28 @@ export function dashboardLoader<
93101
searchParams: result.searchParams as InferZod<TSearchParams>,
94102
user: result.user,
95103
ability: result.ability,
104+
context: result.context as TContext,
96105
request,
97106
});
98107
};
99108
}
100109

101-
export type DashboardActionOptions<TParams, TSearchParams> = DashboardLoaderOptions<
102-
TParams,
103-
TSearchParams
104-
>;
110+
export type DashboardActionOptions<TParams, TSearchParams, TContext extends AuthScope> =
111+
DashboardLoaderOptions<TParams, TSearchParams, TContext>;
105112

106-
export type DashboardActionHandlerArgs<TParams, TSearchParams> = DashboardLoaderHandlerArgs<
107-
TParams,
108-
TSearchParams
109-
> & {
110-
request: Request;
111-
};
113+
export type DashboardActionHandlerArgs<TParams, TSearchParams, TContext> =
114+
DashboardLoaderHandlerArgs<TParams, TSearchParams, TContext>;
112115

113116
export function dashboardAction<
114117
TParams extends AnyZodSchema | undefined = undefined,
115118
TSearchParams extends AnyZodSchema | undefined = undefined,
119+
TContext extends AuthScope = AuthScope,
116120
TReturn extends Response = Response
117121
>(
118-
options: DashboardActionOptions<TParams, TSearchParams>,
119-
handler: (args: DashboardActionHandlerArgs<TParams, TSearchParams>) => Promise<TReturn>
122+
options: DashboardActionOptions<TParams, TSearchParams, TContext>,
123+
handler: (
124+
args: DashboardActionHandlerArgs<TParams, TSearchParams, TContext>
125+
) => Promise<TReturn>
120126
) {
121127
return async function action({ request, params }: ActionFunctionArgs): Promise<TReturn> {
122128
const { authenticateAndAuthorize } = await import("./dashboardBuilder.server");
@@ -128,6 +134,7 @@ export function dashboardAction<
128134
searchParams: result.searchParams as InferZod<TSearchParams>,
129135
user: result.user,
130136
ability: result.ability,
137+
context: result.context as TContext,
131138
request,
132139
});
133140
};

0 commit comments

Comments
 (0)