- 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>
102 lines
2.8 KiB
TypeScript
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 };
|
|
},
|
|
);
|