Add orgMemberMiddleware for org-scoped procedures
- Add OrgInfo, OrgMembership, OrgMemberContext types to context.ts - Create org-member.ts middleware that: - Chains with authMiddleware - Takes input with org slug - Looks up org and verifies membership - Adds org and membership info to context - Export from middlewares/index.ts and procedures/base.ts Also simplify superuserMiddleware to use authMiddleware.concat() Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -115,3 +115,34 @@ export interface SuperuserContext extends AuthenticatedContext {
|
||||
/** User with superuser privileges */
|
||||
user: SessionUser & { isSuperuser: true };
|
||||
}
|
||||
|
||||
/**
|
||||
* Organization info in context
|
||||
*/
|
||||
export interface OrgInfo {
|
||||
id: number;
|
||||
slug: string;
|
||||
displayName: string;
|
||||
logoUrl: string | null;
|
||||
createdAt: Date;
|
||||
}
|
||||
|
||||
/**
|
||||
* User's membership in an org
|
||||
*/
|
||||
export interface OrgMembership {
|
||||
id: number;
|
||||
role: "owner" | "admin" | "member";
|
||||
createdAt: Date;
|
||||
}
|
||||
|
||||
/**
|
||||
* Org member context for org-scoped procedures
|
||||
* Requires user to be a member of the org
|
||||
*/
|
||||
export interface OrgMemberContext extends AuthenticatedContext {
|
||||
/** The organization */
|
||||
org: OrgInfo;
|
||||
/** User's membership in the org */
|
||||
membership: OrgMembership;
|
||||
}
|
||||
|
||||
@@ -4,5 +4,6 @@
|
||||
|
||||
export { authMiddleware } from "./auth.js";
|
||||
export { loginRequestMiddleware } from "./login-request.js";
|
||||
export { orgMemberMiddleware } from "./org-member.js";
|
||||
export { os } from "./os.js";
|
||||
export { superuserMiddleware } from "./superuser.js";
|
||||
|
||||
87
apps/api-server/src/middlewares/org-member.ts
Normal file
87
apps/api-server/src/middlewares/org-member.ts
Normal file
@@ -0,0 +1,87 @@
|
||||
/**
|
||||
* Org member middleware - authenticates and verifies org membership
|
||||
*
|
||||
* This middleware chains authMiddleware first, then looks up the org
|
||||
* and verifies the user is a member. Adds org and membership to context.
|
||||
*
|
||||
* Input must include `slug` (the org slug).
|
||||
*/
|
||||
|
||||
import type {
|
||||
AuthenticatedContext,
|
||||
OrgInfo,
|
||||
OrgMembership,
|
||||
} from "../context.js";
|
||||
import { ORPCError } from "@orpc/server";
|
||||
import { authMiddleware } from "./auth.js";
|
||||
import { os } from "./os.js";
|
||||
|
||||
interface OrgSlugInput {
|
||||
slug: string;
|
||||
}
|
||||
|
||||
const orgMemberCheck = os.middleware(
|
||||
async (
|
||||
{
|
||||
context,
|
||||
next,
|
||||
}: {
|
||||
context: AuthenticatedContext;
|
||||
next: (opts: {
|
||||
context: { org: OrgInfo; membership: OrgMembership };
|
||||
}) => Promise<unknown>;
|
||||
},
|
||||
input: OrgSlugInput,
|
||||
) => {
|
||||
const { db } = context;
|
||||
const { slug } = input;
|
||||
|
||||
// Look up org by slug
|
||||
const org = await db
|
||||
.selectFrom("orgs")
|
||||
.select(["id", "slug", "display_name", "logo_url", "created_at"])
|
||||
.where("slug", "=", slug)
|
||||
.executeTakeFirst();
|
||||
|
||||
if (!org) {
|
||||
throw new ORPCError("NOT_FOUND", { message: "Organization not found" });
|
||||
}
|
||||
|
||||
// Check user membership
|
||||
const membership = await db
|
||||
.selectFrom("org_members")
|
||||
.select(["id", "role", "created_at"])
|
||||
.where("org_id", "=", org.id)
|
||||
.where("user_id", "=", context.user.id)
|
||||
.executeTakeFirst();
|
||||
|
||||
if (!membership) {
|
||||
throw new ORPCError("FORBIDDEN", {
|
||||
message: "You are not a member of this organization",
|
||||
});
|
||||
}
|
||||
|
||||
const orgInfo: OrgInfo = {
|
||||
id: org.id,
|
||||
slug: org.slug,
|
||||
displayName: org.display_name,
|
||||
logoUrl: org.logo_url,
|
||||
createdAt: org.created_at,
|
||||
};
|
||||
|
||||
const membershipInfo: OrgMembership = {
|
||||
id: membership.id,
|
||||
role: membership.role,
|
||||
createdAt: membership.created_at,
|
||||
};
|
||||
|
||||
return next({
|
||||
context: {
|
||||
org: orgInfo,
|
||||
membership: membershipInfo,
|
||||
},
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
export const orgMemberMiddleware = authMiddleware.concat(orgMemberCheck);
|
||||
@@ -10,7 +10,13 @@ import { authMiddleware } from "./auth.js";
|
||||
import { os } from "./os.js";
|
||||
|
||||
const superuserCheck = os.middleware(
|
||||
async ({ context, next }: { context: AuthenticatedContext; next: () => Promise<unknown> }) => {
|
||||
async ({
|
||||
context,
|
||||
next,
|
||||
}: {
|
||||
context: AuthenticatedContext;
|
||||
next: () => Promise<unknown>;
|
||||
}) => {
|
||||
if (!context.user.isSuperuser) {
|
||||
throw new ORPCError("FORBIDDEN", {
|
||||
message: "Superuser access required",
|
||||
|
||||
@@ -9,15 +9,22 @@ import type {
|
||||
APIContext,
|
||||
AuthenticatedContext,
|
||||
LoginRequestContext,
|
||||
OrgMemberContext,
|
||||
} from "../context.js";
|
||||
|
||||
// Re-export middlewares and os from the middlewares folder
|
||||
export {
|
||||
authMiddleware,
|
||||
loginRequestMiddleware,
|
||||
orgMemberMiddleware,
|
||||
os,
|
||||
superuserMiddleware,
|
||||
} from "../middlewares/index.js";
|
||||
|
||||
// Type exports for use in procedure files
|
||||
export type { APIContext, AuthenticatedContext, LoginRequestContext };
|
||||
export type {
|
||||
APIContext,
|
||||
AuthenticatedContext,
|
||||
LoginRequestContext,
|
||||
OrgMemberContext,
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user