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:
igm
2026-01-12 17:49:03 +08:00
parent b48012c1f6
commit 25c8bab741
5 changed files with 134 additions and 2 deletions

View File

@@ -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";

View 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);

View File

@@ -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",