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 with superuser privileges */
|
||||||
user: SessionUser & { isSuperuser: true };
|
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 { authMiddleware } from "./auth.js";
|
||||||
export { loginRequestMiddleware } from "./login-request.js";
|
export { loginRequestMiddleware } from "./login-request.js";
|
||||||
|
export { orgMemberMiddleware } from "./org-member.js";
|
||||||
export { os } from "./os.js";
|
export { os } from "./os.js";
|
||||||
export { superuserMiddleware } from "./superuser.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";
|
import { os } from "./os.js";
|
||||||
|
|
||||||
const superuserCheck = os.middleware(
|
const superuserCheck = os.middleware(
|
||||||
async ({ context, next }: { context: AuthenticatedContext; next: () => Promise<unknown> }) => {
|
async ({
|
||||||
|
context,
|
||||||
|
next,
|
||||||
|
}: {
|
||||||
|
context: AuthenticatedContext;
|
||||||
|
next: () => Promise<unknown>;
|
||||||
|
}) => {
|
||||||
if (!context.user.isSuperuser) {
|
if (!context.user.isSuperuser) {
|
||||||
throw new ORPCError("FORBIDDEN", {
|
throw new ORPCError("FORBIDDEN", {
|
||||||
message: "Superuser access required",
|
message: "Superuser access required",
|
||||||
|
|||||||
@@ -9,15 +9,22 @@ import type {
|
|||||||
APIContext,
|
APIContext,
|
||||||
AuthenticatedContext,
|
AuthenticatedContext,
|
||||||
LoginRequestContext,
|
LoginRequestContext,
|
||||||
|
OrgMemberContext,
|
||||||
} from "../context.js";
|
} from "../context.js";
|
||||||
|
|
||||||
// Re-export middlewares and os from the middlewares folder
|
// Re-export middlewares and os from the middlewares folder
|
||||||
export {
|
export {
|
||||||
authMiddleware,
|
authMiddleware,
|
||||||
loginRequestMiddleware,
|
loginRequestMiddleware,
|
||||||
|
orgMemberMiddleware,
|
||||||
os,
|
os,
|
||||||
superuserMiddleware,
|
superuserMiddleware,
|
||||||
} from "../middlewares/index.js";
|
} from "../middlewares/index.js";
|
||||||
|
|
||||||
// Type exports for use in procedure files
|
// 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