Files
publisher-dashboard/apps/api-server/src/procedures/orgs/management.ts
igm 73ef3df01f Add pre-configured procedures and use them throughout codebase
- Add authedProcedure, superuserProcedure, loginRequestProcedure,
  orgMemberProcedure in base.ts
- Create procedures/me/_base.ts with meRoute = authedProcedure.me
- Update all me procedures to use meRoute.X.handler()
- Update auth/logout and auth/resend-verification to use authedProcedure
- Update all admin procedures to use superuserProcedure
- Update all orgs procedures to use authedProcedure

This reduces boilerplate and makes middleware usage consistent.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-12 17:57:15 +08:00

102 lines
2.8 KiB
TypeScript

/**
* Org management procedures - update, delete, leave
*/
import { ORPCError } from "@orpc/server";
import { authedProcedure } from "../base.js";
import {
countOwners,
getMembership,
lookupOrgBySlug,
requireRole,
} from "./helpers.js";
/**
* Update org details
* Requires admin or owner role
*/
export const orgsUpdate = authedProcedure.orgs.update.handler(
async ({ input, context }) => {
const { slug, displayName, logoUrl } = input;
// Lookup org and verify membership with admin+ role
const org = await lookupOrgBySlug(context.db, slug);
const membership = await getMembership(context.db, org.id, context.user.id);
requireRole(membership, "admin");
// Build update object with only provided fields
const updates: Record<string, unknown> = { updated_at: new Date() };
if (displayName !== undefined) {
updates.display_name = displayName;
}
if (logoUrl !== undefined) {
updates.logo_url = logoUrl;
}
await context.db
.updateTable("orgs")
.set(updates)
.where("id", "=", org.id)
.execute();
return { success: true };
},
);
/**
* Delete an org
* Requires owner role
* FK CASCADE handles deleting members, invites, and sites
*/
export const orgsDelete = authedProcedure.orgs.delete.handler(
async ({ input, context }) => {
const { slug } = input;
// Lookup org and verify ownership
const org = await lookupOrgBySlug(context.db, slug);
const membership = await getMembership(context.db, org.id, context.user.id);
requireRole(membership, "owner");
await context.db.deleteFrom("orgs").where("id", "=", org.id).execute();
return { success: true };
},
);
/**
* Leave an org
* Cannot leave if you're the only owner
* Uses transaction to prevent race condition where multiple owners leave simultaneously
*/
export const orgsLeave = authedProcedure.orgs.leave.handler(
async ({ input, context }) => {
const { slug } = input;
// Lookup org and get membership
const org = await lookupOrgBySlug(context.db, slug);
const membership = await getMembership(context.db, org.id, context.user.id);
await context.db.transaction().execute(async (trx) => {
// If user is an owner, check if they're the last one (with lock)
if (membership.role === "owner") {
const ownerCount = await countOwners(trx, org.id);
if (ownerCount === 1) {
throw new ORPCError("BAD_REQUEST", {
message:
"Cannot leave as the only owner. Transfer ownership or delete the organization.",
});
}
}
// Remove membership
await trx
.deleteFrom("org_members")
.where("org_id", "=", org.id)
.where("user_id", "=", context.user.id)
.execute();
});
return { success: true };
},
);