From 73ef3df01f0b32713167f9ba9d9eae4be63a97e4 Mon Sep 17 00:00:00 2001 From: igm Date: Mon, 12 Jan 2026 17:57:15 +0800 Subject: [PATCH] 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 --- .../procedures/admin/auth/complete-login.ts | 77 +++++++------ .../src/procedures/admin/orgs/create.ts | 10 +- .../src/procedures/admin/orgs/delete.ts | 10 +- .../src/procedures/admin/orgs/get.ts | 10 +- .../src/procedures/admin/orgs/list.ts | 10 +- .../src/procedures/admin/orgs/sites.ts | 94 +++++++-------- .../src/procedures/admin/orgs/update.ts | 10 +- .../procedures/admin/users/confirm-email.ts | 35 +++--- .../src/procedures/admin/users/create.ts | 10 +- .../src/procedures/admin/users/get.ts | 10 +- .../src/procedures/admin/users/list.ts | 10 +- .../src/procedures/admin/users/update.ts | 10 +- apps/api-server/src/procedures/auth/logout.ts | 10 +- .../procedures/auth/resend-verification.ts | 7 +- apps/api-server/src/procedures/base.ts | 19 ++- apps/api-server/src/procedures/me/_base.ts | 7 ++ .../src/procedures/me/api-tokens.ts | 26 ++--- .../src/procedures/me/auth-status.ts | 68 ++++++----- apps/api-server/src/procedures/me/delete.ts | 60 +++++----- apps/api-server/src/procedures/me/devices.ts | 42 +++---- apps/api-server/src/procedures/me/get.ts | 62 +++++----- apps/api-server/src/procedures/me/invites.ts | 108 +++++++++--------- apps/api-server/src/procedures/me/passkeys.ts | 26 ++--- apps/api-server/src/procedures/me/sessions.ts | 26 ++--- .../src/procedures/me/set-password.ts | 10 +- .../src/procedures/me/setup-profile.ts | 10 +- .../src/procedures/me/update-profile.ts | 10 +- apps/api-server/src/procedures/orgs/basic.ts | 26 ++--- .../api-server/src/procedures/orgs/invites.ts | 34 +++--- .../src/procedures/orgs/management.ts | 26 ++--- .../api-server/src/procedures/orgs/members.ts | 97 ++++++++-------- apps/api-server/src/procedures/orgs/sites.ts | 10 +- 32 files changed, 500 insertions(+), 480 deletions(-) create mode 100644 apps/api-server/src/procedures/me/_base.ts diff --git a/apps/api-server/src/procedures/admin/auth/complete-login.ts b/apps/api-server/src/procedures/admin/auth/complete-login.ts index 2da9d32..e3af27e 100644 --- a/apps/api-server/src/procedures/admin/auth/complete-login.ts +++ b/apps/api-server/src/procedures/admin/auth/complete-login.ts @@ -3,48 +3,49 @@ */ import { ORPCError } from "@orpc/server"; -import { os, superuserMiddleware } from "../../base.js"; +import { superuserProcedure } from "../../base.js"; -export const adminAuthCompleteLogin = os.admin.auth.completeLogin - .use(superuserMiddleware) - .handler(async ({ input, context }) => { - const email = input.email.toLowerCase(); +export const adminAuthCompleteLogin = + superuserProcedure.admin.auth.completeLogin.handler( + async ({ input, context }) => { + const email = input.email.toLowerCase(); - // First check if any login request exists for this email - const anyRequest = await context.db - .selectFrom("login_requests") - .where("email", "=", email) - .orderBy("created_at", "desc") - .select(["id", "completed_at", "expires_at"]) - .executeTakeFirst(); + // First check if any login request exists for this email + const anyRequest = await context.db + .selectFrom("login_requests") + .where("email", "=", email) + .orderBy("created_at", "desc") + .select(["id", "completed_at", "expires_at"]) + .executeTakeFirst(); - if (!anyRequest) { - throw new ORPCError("NOT_FOUND", { - message: `No login request found for ${email}`, - }); - } + if (!anyRequest) { + throw new ORPCError("NOT_FOUND", { + message: `No login request found for ${email}`, + }); + } - // Check if already completed - if (anyRequest.completed_at) { - throw new ORPCError("BAD_REQUEST", { - message: "Login request already completed", - }); - } + // Check if already completed + if (anyRequest.completed_at) { + throw new ORPCError("BAD_REQUEST", { + message: "Login request already completed", + }); + } - // Check if expired - if (new Date(anyRequest.expires_at) < new Date()) { - throw new ORPCError("BAD_REQUEST", { - message: - "Login request expired (15 min limit). Start a new login flow.", - }); - } + // Check if expired + if (new Date(anyRequest.expires_at) < new Date()) { + throw new ORPCError("BAD_REQUEST", { + message: + "Login request expired (15 min limit). Start a new login flow.", + }); + } - // Complete the login request - await context.db - .updateTable("login_requests") - .set({ completed_at: new Date() }) - .where("id", "=", anyRequest.id) - .execute(); + // Complete the login request + await context.db + .updateTable("login_requests") + .set({ completed_at: new Date() }) + .where("id", "=", anyRequest.id) + .execute(); - return { success: true }; - }); + return { success: true }; + }, + ); diff --git a/apps/api-server/src/procedures/admin/orgs/create.ts b/apps/api-server/src/procedures/admin/orgs/create.ts index 4f0f11e..f11b577 100644 --- a/apps/api-server/src/procedures/admin/orgs/create.ts +++ b/apps/api-server/src/procedures/admin/orgs/create.ts @@ -3,11 +3,10 @@ */ import { ORPCError } from "@orpc/server"; -import { os, superuserMiddleware } from "../../base.js"; +import { superuserProcedure } from "../../base.js"; -export const adminOrgsCreate = os.admin.orgs.create - .use(superuserMiddleware) - .handler(async ({ input, context }) => { +export const adminOrgsCreate = superuserProcedure.admin.orgs.create.handler( + async ({ input, context }) => { const { slug, displayName, ownerEmail } = input; // Find owner user by email (outside transaction - read-only) @@ -54,4 +53,5 @@ export const adminOrgsCreate = os.admin.orgs.create }); return { slug }; - }); + }, +); diff --git a/apps/api-server/src/procedures/admin/orgs/delete.ts b/apps/api-server/src/procedures/admin/orgs/delete.ts index 3a8941d..fa13b6a 100644 --- a/apps/api-server/src/procedures/admin/orgs/delete.ts +++ b/apps/api-server/src/procedures/admin/orgs/delete.ts @@ -3,11 +3,10 @@ */ import { ORPCError } from "@orpc/server"; -import { os, superuserMiddleware } from "../../base.js"; +import { superuserProcedure } from "../../base.js"; -export const adminOrgsDelete = os.admin.orgs.delete - .use(superuserMiddleware) - .handler(async ({ input, context }) => { +export const adminOrgsDelete = superuserProcedure.admin.orgs.delete.handler( + async ({ input, context }) => { const { slug } = input; // Delete org and related records in transaction @@ -34,4 +33,5 @@ export const adminOrgsDelete = os.admin.orgs.delete }); return { success: true }; - }); + }, +); diff --git a/apps/api-server/src/procedures/admin/orgs/get.ts b/apps/api-server/src/procedures/admin/orgs/get.ts index ba879ac..324cc73 100644 --- a/apps/api-server/src/procedures/admin/orgs/get.ts +++ b/apps/api-server/src/procedures/admin/orgs/get.ts @@ -3,12 +3,11 @@ */ import { ORPCError } from "@orpc/server"; -import { os, superuserMiddleware } from "../../base.js"; +import { superuserProcedure } from "../../base.js"; import { toOrgResponse } from "../helpers.js"; -export const adminOrgsGet = os.admin.orgs.get - .use(superuserMiddleware) - .handler(async ({ input, context }) => { +export const adminOrgsGet = superuserProcedure.admin.orgs.get.handler( + async ({ input, context }) => { const org = await context.db .selectFrom("orgs") .where("slug", "=", input.slug) @@ -18,4 +17,5 @@ export const adminOrgsGet = os.admin.orgs.get throw new ORPCError("NOT_FOUND", { message: "Organization not found" }); } return toOrgResponse(org); - }); + }, +); diff --git a/apps/api-server/src/procedures/admin/orgs/list.ts b/apps/api-server/src/procedures/admin/orgs/list.ts index d8dd29c..aef1a41 100644 --- a/apps/api-server/src/procedures/admin/orgs/list.ts +++ b/apps/api-server/src/procedures/admin/orgs/list.ts @@ -2,12 +2,12 @@ * admin.orgs.list - List all organizations */ -import { os, superuserMiddleware } from "../../base.js"; +import { superuserProcedure } from "../../base.js"; import { toOrgResponse } from "../helpers.js"; -export const adminOrgsList = os.admin.orgs.list - .use(superuserMiddleware) - .handler(async ({ context }) => { +export const adminOrgsList = superuserProcedure.admin.orgs.list.handler( + async ({ context }) => { const orgs = await context.db.selectFrom("orgs").selectAll().execute(); return orgs.map(toOrgResponse); - }); + }, +); diff --git a/apps/api-server/src/procedures/admin/orgs/sites.ts b/apps/api-server/src/procedures/admin/orgs/sites.ts index 737799d..1b70da9 100644 --- a/apps/api-server/src/procedures/admin/orgs/sites.ts +++ b/apps/api-server/src/procedures/admin/orgs/sites.ts @@ -4,35 +4,35 @@ */ import { ORPCError } from "@orpc/server"; -import { os, superuserMiddleware } from "../../base.js"; +import { superuserProcedure } from "../../base.js"; import { toSiteResponse } from "../helpers.js"; -export const adminOrgsListSites = os.admin.orgs.listSites - .use(superuserMiddleware) - .handler(async ({ input, context }) => { - const { slug } = input; +export const adminOrgsListSites = + superuserProcedure.admin.orgs.listSites.handler( + async ({ input, context }) => { + const { slug } = input; - const org = await context.db - .selectFrom("orgs") - .where("slug", "=", slug) - .select(["id"]) - .executeTakeFirst(); - if (!org) { - throw new ORPCError("NOT_FOUND", { message: "Organization not found" }); - } + const org = await context.db + .selectFrom("orgs") + .where("slug", "=", slug) + .select(["id"]) + .executeTakeFirst(); + if (!org) { + throw new ORPCError("NOT_FOUND", { message: "Organization not found" }); + } - const sites = await context.db - .selectFrom("org_sites") - .where("org_id", "=", org.id) - .selectAll() - .execute(); + const sites = await context.db + .selectFrom("org_sites") + .where("org_id", "=", org.id) + .selectAll() + .execute(); - return sites.map(toSiteResponse); - }); + return sites.map(toSiteResponse); + }, + ); -export const adminOrgsAddSite = os.admin.orgs.addSite - .use(superuserMiddleware) - .handler(async ({ input, context }) => { +export const adminOrgsAddSite = superuserProcedure.admin.orgs.addSite.handler( + async ({ input, context }) => { const { slug, domain } = input; // Use transaction to prevent race condition on site creation @@ -68,31 +68,33 @@ export const adminOrgsAddSite = os.admin.orgs.addSite }); return { success: true }; - }); + }, +); -export const adminOrgsRemoveSite = os.admin.orgs.removeSite - .use(superuserMiddleware) - .handler(async ({ input, context }) => { - const { slug, domain } = input; +export const adminOrgsRemoveSite = + superuserProcedure.admin.orgs.removeSite.handler( + async ({ input, context }) => { + const { slug, domain } = input; - const org = await context.db - .selectFrom("orgs") - .where("slug", "=", slug) - .select(["id"]) - .executeTakeFirst(); - if (!org) { - throw new ORPCError("NOT_FOUND", { message: "Organization not found" }); - } + const org = await context.db + .selectFrom("orgs") + .where("slug", "=", slug) + .select(["id"]) + .executeTakeFirst(); + if (!org) { + throw new ORPCError("NOT_FOUND", { message: "Organization not found" }); + } - const result = await context.db - .deleteFrom("org_sites") - .where("org_id", "=", org.id) - .where("domain", "=", domain) - .executeTakeFirst(); + const result = await context.db + .deleteFrom("org_sites") + .where("org_id", "=", org.id) + .where("domain", "=", domain) + .executeTakeFirst(); - if (!result.numDeletedRows || result.numDeletedRows === 0n) { - throw new ORPCError("NOT_FOUND", { message: "Site not found" }); - } + if (!result.numDeletedRows || result.numDeletedRows === 0n) { + throw new ORPCError("NOT_FOUND", { message: "Site not found" }); + } - return { success: true }; - }); + return { success: true }; + }, + ); diff --git a/apps/api-server/src/procedures/admin/orgs/update.ts b/apps/api-server/src/procedures/admin/orgs/update.ts index 4c7320c..9d9eb8c 100644 --- a/apps/api-server/src/procedures/admin/orgs/update.ts +++ b/apps/api-server/src/procedures/admin/orgs/update.ts @@ -3,11 +3,10 @@ */ import { ORPCError } from "@orpc/server"; -import { os, superuserMiddleware } from "../../base.js"; +import { superuserProcedure } from "../../base.js"; -export const adminOrgsUpdate = os.admin.orgs.update - .use(superuserMiddleware) - .handler(async ({ input, context }) => { +export const adminOrgsUpdate = superuserProcedure.admin.orgs.update.handler( + async ({ input, context }) => { const { slug, displayName, logoUrl } = input; // Check if there are actual updates to make @@ -48,4 +47,5 @@ export const adminOrgsUpdate = os.admin.orgs.update } return { success: true }; - }); + }, +); diff --git a/apps/api-server/src/procedures/admin/users/confirm-email.ts b/apps/api-server/src/procedures/admin/users/confirm-email.ts index d66bca8..9611c0f 100644 --- a/apps/api-server/src/procedures/admin/users/confirm-email.ts +++ b/apps/api-server/src/procedures/admin/users/confirm-email.ts @@ -3,23 +3,24 @@ */ import { ORPCError } from "@orpc/server"; -import { os, superuserMiddleware } from "../../base.js"; +import { superuserProcedure } from "../../base.js"; -export const adminUsersConfirmEmail = os.admin.users.confirmEmail - .use(superuserMiddleware) - .handler(async ({ input, context }) => { - const result = await context.db - .updateTable("users") - .set({ - email_verified_at: new Date(), - updated_at: new Date(), - }) - .where("email", "=", input.email.toLowerCase()) - .executeTakeFirst(); +export const adminUsersConfirmEmail = + superuserProcedure.admin.users.confirmEmail.handler( + async ({ input, context }) => { + const result = await context.db + .updateTable("users") + .set({ + email_verified_at: new Date(), + updated_at: new Date(), + }) + .where("email", "=", input.email.toLowerCase()) + .executeTakeFirst(); - if (!result.numUpdatedRows || result.numUpdatedRows === 0n) { - throw new ORPCError("NOT_FOUND", { message: "User not found" }); - } + if (!result.numUpdatedRows || result.numUpdatedRows === 0n) { + throw new ORPCError("NOT_FOUND", { message: "User not found" }); + } - return { success: true }; - }); + return { success: true }; + }, + ); diff --git a/apps/api-server/src/procedures/admin/users/create.ts b/apps/api-server/src/procedures/admin/users/create.ts index 47d787a..9eb3b27 100644 --- a/apps/api-server/src/procedures/admin/users/create.ts +++ b/apps/api-server/src/procedures/admin/users/create.ts @@ -3,11 +3,10 @@ */ import { ORPCError } from "@orpc/server"; -import { os, superuserMiddleware } from "../../base.js"; +import { superuserProcedure } from "../../base.js"; -export const adminUsersCreate = os.admin.users.create - .use(superuserMiddleware) - .handler(async ({ input, context }) => { +export const adminUsersCreate = superuserProcedure.admin.users.create.handler( + async ({ input, context }) => { const { email, name, orgSlug, orgRole } = input; const normalizedEmail = email.toLowerCase(); @@ -61,4 +60,5 @@ export const adminUsersCreate = os.admin.users.create }); return { success: true }; - }); + }, +); diff --git a/apps/api-server/src/procedures/admin/users/get.ts b/apps/api-server/src/procedures/admin/users/get.ts index e6c37b2..a0ac0ef 100644 --- a/apps/api-server/src/procedures/admin/users/get.ts +++ b/apps/api-server/src/procedures/admin/users/get.ts @@ -3,12 +3,11 @@ */ import { ORPCError } from "@orpc/server"; -import { os, superuserMiddleware } from "../../base.js"; +import { superuserProcedure } from "../../base.js"; import { toUserResponse } from "../helpers.js"; -export const adminUsersGet = os.admin.users.get - .use(superuserMiddleware) - .handler(async ({ input, context }) => { +export const adminUsersGet = superuserProcedure.admin.users.get.handler( + async ({ input, context }) => { const user = await context.db .selectFrom("users") .where("email", "=", input.email.toLowerCase()) @@ -18,4 +17,5 @@ export const adminUsersGet = os.admin.users.get throw new ORPCError("NOT_FOUND", { message: "User not found" }); } return toUserResponse(user); - }); + }, +); diff --git a/apps/api-server/src/procedures/admin/users/list.ts b/apps/api-server/src/procedures/admin/users/list.ts index 4ef95f5..926c75e 100644 --- a/apps/api-server/src/procedures/admin/users/list.ts +++ b/apps/api-server/src/procedures/admin/users/list.ts @@ -2,12 +2,12 @@ * admin.users.list - List all users */ -import { os, superuserMiddleware } from "../../base.js"; +import { superuserProcedure } from "../../base.js"; import { toUserResponse } from "../helpers.js"; -export const adminUsersList = os.admin.users.list - .use(superuserMiddleware) - .handler(async ({ context }) => { +export const adminUsersList = superuserProcedure.admin.users.list.handler( + async ({ context }) => { const users = await context.db.selectFrom("users").selectAll().execute(); return users.map(toUserResponse); - }); + }, +); diff --git a/apps/api-server/src/procedures/admin/users/update.ts b/apps/api-server/src/procedures/admin/users/update.ts index 964e1b3..f57404c 100644 --- a/apps/api-server/src/procedures/admin/users/update.ts +++ b/apps/api-server/src/procedures/admin/users/update.ts @@ -3,11 +3,10 @@ */ import { ORPCError } from "@orpc/server"; -import { os, superuserMiddleware } from "../../base.js"; +import { superuserProcedure } from "../../base.js"; -export const adminUsersUpdate = os.admin.users.update - .use(superuserMiddleware) - .handler(async ({ input, context }) => { +export const adminUsersUpdate = superuserProcedure.admin.users.update.handler( + async ({ input, context }) => { const { email, isSuperuser } = input; const normalizedEmail = email.toLowerCase(); @@ -46,4 +45,5 @@ export const adminUsersUpdate = os.admin.users.update } return { success: true }; - }); + }, +); diff --git a/apps/api-server/src/procedures/auth/logout.ts b/apps/api-server/src/procedures/auth/logout.ts index c964d7c..cfa1693 100644 --- a/apps/api-server/src/procedures/auth/logout.ts +++ b/apps/api-server/src/procedures/auth/logout.ts @@ -3,7 +3,7 @@ */ import { COOKIE_NAMES, deleteCookie } from "../../utils/cookies.js"; -import { authMiddleware, os } from "../base.js"; +import { authedProcedure } from "../base.js"; /** * Logout handler @@ -11,9 +11,8 @@ import { authMiddleware, os } from "../base.js"; * - Revokes the current session by setting revoked_at to now() * - Clears the session cookie from the response */ -export const logout = os.auth.logout - .use(authMiddleware) - .handler(async ({ context }) => { +export const logout = authedProcedure.auth.logout.handler( + async ({ context }) => { // Revoke the current session await context.db .updateTable("sessions") @@ -25,4 +24,5 @@ export const logout = os.auth.logout deleteCookie(context.resHeaders, COOKIE_NAMES.SESSION_TOKEN); return { success: true }; - }); + }, +); diff --git a/apps/api-server/src/procedures/auth/resend-verification.ts b/apps/api-server/src/procedures/auth/resend-verification.ts index 391c838..b9991fe 100644 --- a/apps/api-server/src/procedures/auth/resend-verification.ts +++ b/apps/api-server/src/procedures/auth/resend-verification.ts @@ -16,11 +16,10 @@ import { generateExpiry, generateSecureBase58Token, } from "../../utils/crypto.js"; -import { authMiddleware, os } from "../base.js"; +import { authedProcedure } from "../base.js"; -export const resendVerificationEmail = os.auth.resendVerificationEmail - .use(authMiddleware) - .handler(async ({ context }) => { +export const resendVerificationEmail = + authedProcedure.auth.resendVerificationEmail.handler(async ({ context }) => { // Check if email is already verified if (context.user.emailVerifiedAt !== null) { // Email already verified, return early diff --git a/apps/api-server/src/procedures/base.ts b/apps/api-server/src/procedures/base.ts index d3cafd3..77b7bd5 100644 --- a/apps/api-server/src/procedures/base.ts +++ b/apps/api-server/src/procedures/base.ts @@ -11,9 +11,7 @@ import type { LoginRequestContext, OrgMemberContext, } from "../context.js"; - -// Re-export middlewares and os from the middlewares folder -export { +import { authMiddleware, loginRequestMiddleware, orgMemberMiddleware, @@ -21,6 +19,21 @@ export { superuserMiddleware, } from "../middlewares/index.js"; +// Re-export middlewares and os +export { + authMiddleware, + loginRequestMiddleware, + orgMemberMiddleware, + os, + superuserMiddleware, +}; + +// Pre-configured procedures with middleware applied +export const authedProcedure = os.use(authMiddleware); +export const superuserProcedure = os.use(superuserMiddleware); +export const loginRequestProcedure = os.use(loginRequestMiddleware); +export const orgMemberProcedure = os.use(orgMemberMiddleware); + // Type exports for use in procedure files export type { APIContext, diff --git a/apps/api-server/src/procedures/me/_base.ts b/apps/api-server/src/procedures/me/_base.ts new file mode 100644 index 0000000..e9b88ac --- /dev/null +++ b/apps/api-server/src/procedures/me/_base.ts @@ -0,0 +1,7 @@ +/** + * Base route for me procedures with auth middleware applied + */ + +import { authedProcedure } from "../base.js"; + +export const meRoute = authedProcedure.me; diff --git a/apps/api-server/src/procedures/me/api-tokens.ts b/apps/api-server/src/procedures/me/api-tokens.ts index b771aba..2c419e5 100644 --- a/apps/api-server/src/procedures/me/api-tokens.ts +++ b/apps/api-server/src/procedures/me/api-tokens.ts @@ -9,7 +9,7 @@ import { hashToken, TOKEN_PREFIX, } from "../../utils/crypto.js"; -import { authMiddleware, os } from "../base.js"; +import { meRoute } from "./_base.js"; /** Token expiration: 365 days */ const TOKEN_EXPIRATION_DAYS = 365; @@ -18,9 +18,8 @@ const TOKEN_EXPIRATION_DAYS = 365; * List all API tokens for the current user * Returns token metadata (not the actual token values) */ -export const listApiTokens = os.me.apiTokens.list - .use(authMiddleware) - .handler(async ({ context }) => { +export const listApiTokens = meRoute.apiTokens.list.handler( + async ({ context }) => { const tokens = await context.db .selectFrom("api_tokens") .select(["id", "name", "last_used_at", "created_at", "expires_at"]) @@ -35,15 +34,15 @@ export const listApiTokens = os.me.apiTokens.list createdAt: token.created_at.toISOString(), expiresAt: token.expires_at.toISOString(), })); - }); + }, +); /** * Create a new API token * Requires superuser status and trusted session */ -export const createApiToken = os.me.apiTokens.create - .use(authMiddleware) - .handler(async ({ input, context }) => { +export const createApiToken = meRoute.apiTokens.create.handler( + async ({ input, context }) => { // Require superuser status if (!context.user.isSuperuser) { throw new ORPCError("FORBIDDEN", { @@ -85,14 +84,14 @@ export const createApiToken = os.me.apiTokens.create token, expiresAt: expiresAt.toISOString(), }; - }); + }, +); /** * Delete an API token */ -export const deleteApiToken = os.me.apiTokens.delete - .use(authMiddleware) - .handler(async ({ input, context }) => { +export const deleteApiToken = meRoute.apiTokens.delete.handler( + async ({ input, context }) => { const result = await context.db .deleteFrom("api_tokens") .where("id", "=", input.tokenId.toString()) @@ -106,4 +105,5 @@ export const deleteApiToken = os.me.apiTokens.delete } return { success: true }; - }); + }, +); diff --git a/apps/api-server/src/procedures/me/auth-status.ts b/apps/api-server/src/procedures/me/auth-status.ts index 6bbf857..171bac2 100644 --- a/apps/api-server/src/procedures/me/auth-status.ts +++ b/apps/api-server/src/procedures/me/auth-status.ts @@ -2,40 +2,38 @@ * Get current user auth status */ -import { authMiddleware, os } from "../base.js"; +import { meRoute } from "./_base.js"; -export const meAuthStatus = os.me.authStatus - .use(authMiddleware) - .handler(async ({ context }) => { - const user = await context.db - .selectFrom("users") - .select([ - "id", - "email", - "display_name", - "full_name", - "phone_number", - "avatar_url", - "email_verified_at", - "is_superuser", - "password_hash", - ]) - .where("id", "=", context.user.id) - .executeTakeFirstOrThrow(); +export const meAuthStatus = meRoute.authStatus.handler(async ({ context }) => { + const user = await context.db + .selectFrom("users") + .select([ + "id", + "email", + "display_name", + "full_name", + "phone_number", + "avatar_url", + "email_verified_at", + "is_superuser", + "password_hash", + ]) + .where("id", "=", context.user.id) + .executeTakeFirstOrThrow(); - return { - user: { - id: user.id, - email: user.email, - displayName: user.display_name, - fullName: user.full_name, - phoneNumber: user.phone_number, - avatarUrl: user.avatar_url, - emailVerified: user.email_verified_at !== null, - needsSetup: user.display_name === null, - isSuperuser: user.is_superuser, - hasPassword: user.password_hash !== null, - }, - auth: context.auth, - }; - }); + return { + user: { + id: user.id, + email: user.email, + displayName: user.display_name, + fullName: user.full_name, + phoneNumber: user.phone_number, + avatarUrl: user.avatar_url, + emailVerified: user.email_verified_at !== null, + needsSetup: user.display_name === null, + isSuperuser: user.is_superuser, + hasPassword: user.password_hash !== null, + }, + auth: context.auth, + }; +}); diff --git a/apps/api-server/src/procedures/me/delete.ts b/apps/api-server/src/procedures/me/delete.ts index e5cf875..635cead 100644 --- a/apps/api-server/src/procedures/me/delete.ts +++ b/apps/api-server/src/procedures/me/delete.ts @@ -5,7 +5,7 @@ import { ORPCError } from "@orpc/server"; import { COOKIE_NAMES, deleteCookie } from "../../utils/cookies.js"; import { verifyPassword } from "../../utils/password.js"; -import { authMiddleware, os } from "../base.js"; +import { meRoute } from "./_base.js"; /** * Delete account handler @@ -14,39 +14,37 @@ import { authMiddleware, os } from "../base.js"; * - Deletes user record (cascades to sessions, devices, passkeys, etc.) * - Clears session cookie */ -export const meDelete = os.me.delete - .use(authMiddleware) - .handler(async ({ input, context }) => { - const { password } = input; +export const meDelete = meRoute.delete.handler(async ({ input, context }) => { + const { password } = input; - // Fetch user with password hash - const user = await context.db - .selectFrom("users") - .select(["password_hash"]) - .where("id", "=", context.user.id) - .executeTakeFirstOrThrow(); + // Fetch user with password hash + const user = await context.db + .selectFrom("users") + .select(["password_hash"]) + .where("id", "=", context.user.id) + .executeTakeFirstOrThrow(); - // Verify password (required for account deletion) - if (!user.password_hash) { - throw new ORPCError("BAD_REQUEST", { - message: - "Cannot delete account without a password. Please set a password first.", - }); - } + // Verify password (required for account deletion) + if (!user.password_hash) { + throw new ORPCError("BAD_REQUEST", { + message: + "Cannot delete account without a password. Please set a password first.", + }); + } - const valid = await verifyPassword(password, user.password_hash); - if (!valid) { - throw new ORPCError("BAD_REQUEST", { message: "Incorrect password" }); - } + const valid = await verifyPassword(password, user.password_hash); + if (!valid) { + throw new ORPCError("BAD_REQUEST", { message: "Incorrect password" }); + } - // Delete user (cascades to sessions, devices, passkeys, etc.) - await context.db - .deleteFrom("users") - .where("id", "=", context.user.id) - .execute(); + // Delete user (cascades to sessions, devices, passkeys, etc.) + await context.db + .deleteFrom("users") + .where("id", "=", context.user.id) + .execute(); - // Clear session cookie - deleteCookie(context.resHeaders, COOKIE_NAMES.SESSION_TOKEN); + // Clear session cookie + deleteCookie(context.resHeaders, COOKIE_NAMES.SESSION_TOKEN); - return { success: true }; - }); + return { success: true }; +}); diff --git a/apps/api-server/src/procedures/me/devices.ts b/apps/api-server/src/procedures/me/devices.ts index 5e4af57..96c2574 100644 --- a/apps/api-server/src/procedures/me/devices.ts +++ b/apps/api-server/src/procedures/me/devices.ts @@ -3,7 +3,7 @@ */ import { ORPCError } from "@orpc/server"; -import { authMiddleware, os } from "../base.js"; +import { meRoute } from "./_base.js"; import { defaultDeviceName, requireDeviceFingerprint } from "./helpers.js"; /** @@ -13,9 +13,8 @@ import { defaultDeviceName, requireDeviceFingerprint } from "./helpers.js"; * @throws BAD_REQUEST if no device fingerprint found * @throws NOT_FOUND if device doesn't exist */ -export const getDeviceInfo = os.me.devices.getInfo - .use(authMiddleware) - .handler(async ({ context }) => { +export const getDeviceInfo = meRoute.devices.getInfo.handler( + async ({ context }) => { const fingerprint = requireDeviceFingerprint(context.reqHeaders); const device = await context.db @@ -39,7 +38,8 @@ export const getDeviceInfo = os.me.devices.getInfo lastUsedAt: device.last_used_at, isTrusted: device.is_trusted, }; - }); + }, +); /** * Trust device handler @@ -48,9 +48,8 @@ export const getDeviceInfo = os.me.devices.getInfo * @throws BAD_REQUEST if no device fingerprint found * @throws NOT_FOUND if device doesn't exist */ -export const trustDevice = os.me.devices.trust - .use(authMiddleware) - .handler(async ({ input, context }) => { +export const trustDevice = meRoute.devices.trust.handler( + async ({ input, context }) => { const { name } = input; const fingerprint = requireDeviceFingerprint(context.reqHeaders); @@ -66,16 +65,16 @@ export const trustDevice = os.me.devices.trust } return { success: true }; - }); + }, +); /** * List trusted devices handler * - Requires authentication * - Returns all trusted devices for the current user */ -export const listTrustedDevices = os.me.devices.listTrusted - .use(authMiddleware) - .handler(async ({ context }) => { +export const listTrustedDevices = meRoute.devices.listTrusted.handler( + async ({ context }) => { const devices = await context.db .selectFrom("user_devices") .selectAll() @@ -94,7 +93,8 @@ export const listTrustedDevices = os.me.devices.listTrusted lastUsedAt: d.last_used_at, isTrusted: d.is_trusted, })); - }); + }, +); /** * Untrust device handler @@ -102,9 +102,8 @@ export const listTrustedDevices = os.me.devices.listTrusted * - Marks device as untrusted by ID * @throws NOT_FOUND if device doesn't exist */ -export const untrustDevice = os.me.devices.untrust - .use(authMiddleware) - .handler(async ({ input, context }) => { +export const untrustDevice = meRoute.devices.untrust.handler( + async ({ input, context }) => { const result = await context.db .updateTable("user_devices") .set({ is_trusted: false }) @@ -117,16 +116,16 @@ export const untrustDevice = os.me.devices.untrust } return { success: true }; - }); + }, +); /** * Revoke all trusted devices handler * - Requires authentication * - Marks all devices as untrusted */ -export const revokeAllTrustedDevices = os.me.devices.revokeAll - .use(authMiddleware) - .handler(async ({ context }) => { +export const revokeAllTrustedDevices = meRoute.devices.revokeAll.handler( + async ({ context }) => { await context.db .updateTable("user_devices") .set({ is_trusted: false }) @@ -134,4 +133,5 @@ export const revokeAllTrustedDevices = os.me.devices.revokeAll .execute(); return { success: true }; - }); + }, +); diff --git a/apps/api-server/src/procedures/me/get.ts b/apps/api-server/src/procedures/me/get.ts index ecd705e..c701e35 100644 --- a/apps/api-server/src/procedures/me/get.ts +++ b/apps/api-server/src/procedures/me/get.ts @@ -2,37 +2,35 @@ * Get current user profile */ -import { authMiddleware, os } from "../base.js"; +import { meRoute } from "./_base.js"; -export const meGet = os.me.get - .use(authMiddleware) - .handler(async ({ context }) => { - const user = await context.db - .selectFrom("users") - .select([ - "id", - "email", - "display_name", - "full_name", - "phone_number", - "avatar_url", - "email_verified_at", - "is_superuser", - "password_hash", - ]) - .where("id", "=", context.user.id) - .executeTakeFirstOrThrow(); +export const meGet = meRoute.get.handler(async ({ context }) => { + const user = await context.db + .selectFrom("users") + .select([ + "id", + "email", + "display_name", + "full_name", + "phone_number", + "avatar_url", + "email_verified_at", + "is_superuser", + "password_hash", + ]) + .where("id", "=", context.user.id) + .executeTakeFirstOrThrow(); - return { - id: user.id, - email: user.email, - displayName: user.display_name, - fullName: user.full_name, - phoneNumber: user.phone_number, - avatarUrl: user.avatar_url, - emailVerified: user.email_verified_at !== null, - needsSetup: user.display_name === null, - isSuperuser: user.is_superuser, - hasPassword: user.password_hash !== null, - }; - }); + return { + id: user.id, + email: user.email, + displayName: user.display_name, + fullName: user.full_name, + phoneNumber: user.phone_number, + avatarUrl: user.avatar_url, + emailVerified: user.email_verified_at !== null, + needsSetup: user.display_name === null, + isSuperuser: user.is_superuser, + hasPassword: user.password_hash !== null, + }; +}); diff --git a/apps/api-server/src/procedures/me/invites.ts b/apps/api-server/src/procedures/me/invites.ts index 0176f96..abc464b 100644 --- a/apps/api-server/src/procedures/me/invites.ts +++ b/apps/api-server/src/procedures/me/invites.ts @@ -3,64 +3,61 @@ */ import { ORPCError } from "@orpc/server"; -import { authMiddleware, os } from "../base.js"; +import { meRoute } from "./_base.js"; /** * List pending invites for the current user * Only returns invites where the user's email matches and email is verified */ -export const listInvites = os.me.invites.list - .use(authMiddleware) - .handler(async ({ context }) => { - // Only show invites if email is verified - if (!context.user.emailVerifiedAt) { - return []; - } +export const listInvites = meRoute.invites.list.handler(async ({ context }) => { + // Only show invites if email is verified + if (!context.user.emailVerifiedAt) { + return []; + } - // Get non-expired invites matching user's email - const invites = await context.db - .selectFrom("org_invites") - .innerJoin("orgs", "orgs.id", "org_invites.org_id") - .innerJoin("users", "users.id", "org_invites.invited_by") - .where("org_invites.email", "=", context.user.email.toLowerCase()) - .where("org_invites.expires_at", ">", new Date()) - .select([ - "org_invites.id", - "org_invites.role", - "org_invites.created_at", - "org_invites.expires_at", - "orgs.id as org_id", - "orgs.slug as org_slug", - "orgs.display_name as org_display_name", - "orgs.logo_url as org_logo_url", - "users.display_name as inviter_name", - "users.email as inviter_email", - ]) - .orderBy("org_invites.created_at", "desc") - .execute(); + // Get non-expired invites matching user's email + const invites = await context.db + .selectFrom("org_invites") + .innerJoin("orgs", "orgs.id", "org_invites.org_id") + .innerJoin("users", "users.id", "org_invites.invited_by") + .where("org_invites.email", "=", context.user.email.toLowerCase()) + .where("org_invites.expires_at", ">", new Date()) + .select([ + "org_invites.id", + "org_invites.role", + "org_invites.created_at", + "org_invites.expires_at", + "orgs.id as org_id", + "orgs.slug as org_slug", + "orgs.display_name as org_display_name", + "orgs.logo_url as org_logo_url", + "users.display_name as inviter_name", + "users.email as inviter_email", + ]) + .orderBy("org_invites.created_at", "desc") + .execute(); - return invites.map((i) => ({ - id: i.id, - org: { - id: i.org_id, - slug: i.org_slug, - displayName: i.org_display_name, - logoUrl: i.org_logo_url, - }, - role: i.role, - invitedBy: i.inviter_name ?? i.inviter_email, - createdAt: i.created_at, - expiresAt: i.expires_at, - })); - }); + return invites.map((i) => ({ + id: i.id, + org: { + id: i.org_id, + slug: i.org_slug, + displayName: i.org_display_name, + logoUrl: i.org_logo_url, + }, + role: i.role, + invitedBy: i.inviter_name ?? i.inviter_email, + createdAt: i.created_at, + expiresAt: i.expires_at, + })); +}); /** * Get a specific invite by ID * Only returns if the invite belongs to the current user's email */ -export const getInvite = os.me.invites.get - .use(authMiddleware) - .handler(async ({ input, context }) => { +export const getInvite = meRoute.invites.get.handler( + async ({ input, context }) => { const { inviteId } = input; // Only show invite if email is verified @@ -111,15 +108,15 @@ export const getInvite = os.me.invites.get createdAt: invite.created_at, expiresAt: invite.expires_at, }; - }); + }, +); /** * Accept an invite by ID * Adds user to org and deletes the invite */ -export const acceptInvite = os.me.invites.accept - .use(authMiddleware) - .handler(async ({ input, context }) => { +export const acceptInvite = meRoute.invites.accept.handler( + async ({ input, context }) => { const { inviteId } = input; // Only allow accepting if email is verified @@ -183,15 +180,15 @@ export const acceptInvite = os.me.invites.accept } return { success: true }; - }); + }, +); /** * Decline an invite * Deletes the invite if it belongs to the current user's email */ -export const declineInvite = os.me.invites.decline - .use(authMiddleware) - .handler(async ({ input, context }) => { +export const declineInvite = meRoute.invites.decline.handler( + async ({ input, context }) => { const { inviteId } = input; // Delete the invite only if it matches user's email @@ -208,4 +205,5 @@ export const declineInvite = os.me.invites.decline } return { success: true }; - }); + }, +); diff --git a/apps/api-server/src/procedures/me/passkeys.ts b/apps/api-server/src/procedures/me/passkeys.ts index a1d341a..af43102 100644 --- a/apps/api-server/src/procedures/me/passkeys.ts +++ b/apps/api-server/src/procedures/me/passkeys.ts @@ -4,16 +4,15 @@ import { ORPCError } from "@orpc/server"; import { getUserPasskeys } from "../../utils/webauthn.js"; -import { authMiddleware, os } from "../base.js"; +import { meRoute } from "./_base.js"; /** * List passkeys handler * - Requires authentication * - Returns all passkeys for the current user */ -export const listPasskeys = os.me.passkeys.list - .use(authMiddleware) - .handler(async ({ context }) => { +export const listPasskeys = meRoute.passkeys.list.handler( + async ({ context }) => { const passkeys = await getUserPasskeys(context.db, context.user.id); return passkeys.map((p) => ({ @@ -22,7 +21,8 @@ export const listPasskeys = os.me.passkeys.list createdAt: p.createdAt, lastUsedAt: p.lastUsedAt, })); - }); + }, +); /** * Rename passkey handler @@ -30,9 +30,8 @@ export const listPasskeys = os.me.passkeys.list * - Updates passkey name * @throws NOT_FOUND if passkey doesn't exist */ -export const renamePasskey = os.me.passkeys.rename - .use(authMiddleware) - .handler(async ({ input, context }) => { +export const renamePasskey = meRoute.passkeys.rename.handler( + async ({ input, context }) => { const { passkeyId, name } = input; const result = await context.db @@ -47,7 +46,8 @@ export const renamePasskey = os.me.passkeys.rename } return { success: true }; - }); + }, +); /** * Delete passkey handler @@ -57,9 +57,8 @@ export const renamePasskey = os.me.passkeys.rename * @throws NOT_FOUND if passkey doesn't exist * @throws BAD_REQUEST if trying to delete last passkey without password */ -export const deletePasskey = os.me.passkeys.delete - .use(authMiddleware) - .handler(async ({ input, context }) => { +export const deletePasskey = meRoute.passkeys.delete.handler( + async ({ input, context }) => { const { passkeyId } = input; // Use transaction to prevent race condition when checking last passkey @@ -96,4 +95,5 @@ export const deletePasskey = os.me.passkeys.delete }); return { success: true }; - }); + }, +); diff --git a/apps/api-server/src/procedures/me/sessions.ts b/apps/api-server/src/procedures/me/sessions.ts index 27f59c6..64f6666 100644 --- a/apps/api-server/src/procedures/me/sessions.ts +++ b/apps/api-server/src/procedures/me/sessions.ts @@ -3,7 +3,7 @@ */ import { ORPCError } from "@orpc/server"; -import { authMiddleware, os } from "../base.js"; +import { meRoute } from "./_base.js"; /** * List sessions handler @@ -11,9 +11,8 @@ import { authMiddleware, os } from "../base.js"; * - Returns all sessions for the current user * - Includes isCurrent flag to identify active session */ -export const listSessions = os.me.sessions.list - .use(authMiddleware) - .handler(async ({ context }) => { +export const listSessions = meRoute.sessions.list.handler( + async ({ context }) => { const sessions = await context.db .selectFrom("sessions") .selectAll() @@ -33,7 +32,8 @@ export const listSessions = os.me.sessions.list isCurrent: s.id === context.session.id, revokedAt: s.revoked_at, })); - }); + }, +); /** * Revoke session handler @@ -42,9 +42,8 @@ export const listSessions = os.me.sessions.list * @throws NOT_FOUND if session doesn't exist * @throws BAD_REQUEST if trying to revoke current session */ -export const revokeSession = os.me.sessions.revoke - .use(authMiddleware) - .handler(async ({ input, context }) => { +export const revokeSession = meRoute.sessions.revoke.handler( + async ({ input, context }) => { const { sessionId } = input; // Prevent revoking current session (use logout instead) @@ -67,16 +66,16 @@ export const revokeSession = os.me.sessions.revoke } return { success: true }; - }); + }, +); /** * Revoke all sessions handler * - Requires authentication * - Revokes all sessions except current */ -export const revokeAllSessions = os.me.sessions.revokeAll - .use(authMiddleware) - .handler(async ({ context }) => { +export const revokeAllSessions = meRoute.sessions.revokeAll.handler( + async ({ context }) => { // Revoke all sessions except current await context.db .updateTable("sessions") @@ -87,4 +86,5 @@ export const revokeAllSessions = os.me.sessions.revokeAll .execute(); return { success: true }; - }); + }, +); diff --git a/apps/api-server/src/procedures/me/set-password.ts b/apps/api-server/src/procedures/me/set-password.ts index beacc87..b899c6c 100644 --- a/apps/api-server/src/procedures/me/set-password.ts +++ b/apps/api-server/src/procedures/me/set-password.ts @@ -8,7 +8,7 @@ import { validatePassword, verifyPassword, } from "../../utils/password.js"; -import { authMiddleware, os } from "../base.js"; +import { meRoute } from "./_base.js"; /** * Set password handler @@ -16,9 +16,8 @@ import { authMiddleware, os } from "../base.js"; * - If user has existing password, currentPassword is required * - Validates new password strength using zxcvbn */ -export const setPassword = os.me.setPassword - .use(authMiddleware) - .handler(async ({ input, context }) => { +export const setPassword = meRoute.setPassword.handler( + async ({ input, context }) => { const { currentPassword, newPassword } = input; // Fetch current password hash @@ -60,4 +59,5 @@ export const setPassword = os.me.setPassword .execute(); return { success: true }; - }); + }, +); diff --git a/apps/api-server/src/procedures/me/setup-profile.ts b/apps/api-server/src/procedures/me/setup-profile.ts index da083ff..cfbdcf9 100644 --- a/apps/api-server/src/procedures/me/setup-profile.ts +++ b/apps/api-server/src/procedures/me/setup-profile.ts @@ -2,11 +2,10 @@ * Setup user profile (initial setup after signup) */ -import { authMiddleware, os } from "../base.js"; +import { meRoute } from "./_base.js"; -export const setupProfile = os.me.setupProfile - .use(authMiddleware) - .handler(async ({ input, context }) => { +export const setupProfile = meRoute.setupProfile.handler( + async ({ input, context }) => { const { displayName, fullName, phoneNumber } = input; await context.db @@ -21,4 +20,5 @@ export const setupProfile = os.me.setupProfile .execute(); return { success: true }; - }); + }, +); diff --git a/apps/api-server/src/procedures/me/update-profile.ts b/apps/api-server/src/procedures/me/update-profile.ts index b23c090..fefdbeb 100644 --- a/apps/api-server/src/procedures/me/update-profile.ts +++ b/apps/api-server/src/procedures/me/update-profile.ts @@ -3,7 +3,7 @@ */ import type { ProfileUpdate } from "./helpers.js"; -import { authMiddleware, os } from "../base.js"; +import { meRoute } from "./_base.js"; /** * Update profile handler @@ -11,9 +11,8 @@ import { authMiddleware, os } from "../base.js"; * - Allows partial updates to display_name, full_name, phone_number, avatar_url * - Automatically sets updated_at timestamp */ -export const updateProfile = os.me.updateProfile - .use(authMiddleware) - .handler(async ({ input, context }) => { +export const updateProfile = meRoute.updateProfile.handler( + async ({ input, context }) => { const updates: Partial = {}; if (input.displayName !== undefined) { updates.display_name = input.displayName; @@ -38,4 +37,5 @@ export const updateProfile = os.me.updateProfile } return { success: true }; - }); + }, +); diff --git a/apps/api-server/src/procedures/orgs/basic.ts b/apps/api-server/src/procedures/orgs/basic.ts index f99f919..8793b4a 100644 --- a/apps/api-server/src/procedures/orgs/basic.ts +++ b/apps/api-server/src/procedures/orgs/basic.ts @@ -3,15 +3,14 @@ */ import { ORPCError } from "@orpc/server"; -import { authMiddleware, os } from "../base.js"; +import { authedProcedure } from "../base.js"; import { getMembership, lookupOrgBySlug } from "./helpers.js"; /** * List all orgs the current user is a member of */ -export const orgsList = os.orgs.list - .use(authMiddleware) - .handler(async ({ context }) => { +export const orgsList = authedProcedure.orgs.list.handler( + async ({ context }) => { const orgs = await context.db .selectFrom("org_members") .innerJoin("orgs", "orgs.id", "org_members.org_id") @@ -33,15 +32,15 @@ export const orgsList = os.orgs.list logoUrl: o.logo_url, createdAt: o.created_at, })); - }); + }, +); /** * Create a new org * The creating user becomes the owner */ -export const orgsCreate = os.orgs.create - .use(authMiddleware) - .handler(async ({ input, context }) => { +export const orgsCreate = authedProcedure.orgs.create.handler( + async ({ input, context }) => { const { slug, displayName } = input; try { @@ -75,15 +74,15 @@ export const orgsCreate = os.orgs.create } throw error; } - }); + }, +); /** * Get a single org by slug * Requires membership */ -export const orgsGet = os.orgs.get - .use(authMiddleware) - .handler(async ({ input, context }) => { +export const orgsGet = authedProcedure.orgs.get.handler( + async ({ input, context }) => { const { slug } = input; // Lookup org and verify membership @@ -97,4 +96,5 @@ export const orgsGet = os.orgs.get logoUrl: org.logoUrl, createdAt: org.createdAt, }; - }); + }, +); diff --git a/apps/api-server/src/procedures/orgs/invites.ts b/apps/api-server/src/procedures/orgs/invites.ts index df65238..492806b 100644 --- a/apps/api-server/src/procedures/orgs/invites.ts +++ b/apps/api-server/src/procedures/orgs/invites.ts @@ -9,16 +9,15 @@ import { generateExpiry, generateSecureBase58Token, } from "../../utils/crypto.js"; -import { authMiddleware, os } from "../base.js"; +import { authedProcedure } from "../base.js"; import { getMembership, lookupOrgBySlug, requireRole } from "./helpers.js"; /** * List pending invites for an org * Requires admin or owner role */ -export const invitesList = os.orgs.invites.list - .use(authMiddleware) - .handler(async ({ input, context }) => { +export const invitesList = authedProcedure.orgs.invites.list.handler( + async ({ input, context }) => { const { slug } = input; // Lookup org and verify admin+ role @@ -52,16 +51,16 @@ export const invitesList = os.orgs.invites.list createdAt: i.created_at, expiresAt: i.expires_at, })); - }); + }, +); /** * Create an invite for a new member * Requires admin or owner role * Only owners can invite new owners (privilege escalation prevention) */ -export const invitesCreate = os.orgs.invites.create - .use(authMiddleware) - .handler(async ({ input, context }) => { +export const invitesCreate = authedProcedure.orgs.invites.create.handler( + async ({ input, context }) => { const { slug, email: rawEmail, role } = input; const email = rawEmail.toLowerCase(); @@ -135,15 +134,15 @@ export const invitesCreate = os.orgs.invites.create }); return { success: true }; - }); + }, +); /** * Cancel a pending invite * Requires admin or owner role */ -export const invitesCancel = os.orgs.invites.cancel - .use(authMiddleware) - .handler(async ({ input, context }) => { +export const invitesCancel = authedProcedure.orgs.invites.cancel.handler( + async ({ input, context }) => { const { slug, inviteId } = input; // Lookup org and verify admin+ role @@ -163,16 +162,16 @@ export const invitesCancel = os.orgs.invites.cancel } return { success: true }; - }); + }, +); /** * Accept an invitation * Token-based lookup, requires auth but no org membership * Handles race condition if user is already a member */ -export const invitesAccept = os.orgs.invites.accept - .use(authMiddleware) - .handler(async ({ input, context }) => { +export const invitesAccept = authedProcedure.orgs.invites.accept.handler( + async ({ input, context }) => { const { token } = input; // Find the invite by token (must not be expired) @@ -235,4 +234,5 @@ export const invitesAccept = os.orgs.invites.accept } return { success: true }; - }); + }, +); diff --git a/apps/api-server/src/procedures/orgs/management.ts b/apps/api-server/src/procedures/orgs/management.ts index 00a72ae..cb60b5f 100644 --- a/apps/api-server/src/procedures/orgs/management.ts +++ b/apps/api-server/src/procedures/orgs/management.ts @@ -3,7 +3,7 @@ */ import { ORPCError } from "@orpc/server"; -import { authMiddleware, os } from "../base.js"; +import { authedProcedure } from "../base.js"; import { countOwners, getMembership, @@ -15,9 +15,8 @@ import { * Update org details * Requires admin or owner role */ -export const orgsUpdate = os.orgs.update - .use(authMiddleware) - .handler(async ({ input, context }) => { +export const orgsUpdate = authedProcedure.orgs.update.handler( + async ({ input, context }) => { const { slug, displayName, logoUrl } = input; // Lookup org and verify membership with admin+ role @@ -41,16 +40,16 @@ export const orgsUpdate = os.orgs.update .execute(); return { success: true }; - }); + }, +); /** * Delete an org * Requires owner role * FK CASCADE handles deleting members, invites, and sites */ -export const orgsDelete = os.orgs.delete - .use(authMiddleware) - .handler(async ({ input, context }) => { +export const orgsDelete = authedProcedure.orgs.delete.handler( + async ({ input, context }) => { const { slug } = input; // Lookup org and verify ownership @@ -61,16 +60,16 @@ export const orgsDelete = os.orgs.delete 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 = os.orgs.leave - .use(authMiddleware) - .handler(async ({ input, context }) => { +export const orgsLeave = authedProcedure.orgs.leave.handler( + async ({ input, context }) => { const { slug } = input; // Lookup org and get membership @@ -98,4 +97,5 @@ export const orgsLeave = os.orgs.leave }); return { success: true }; - }); + }, +); diff --git a/apps/api-server/src/procedures/orgs/members.ts b/apps/api-server/src/procedures/orgs/members.ts index 8bcc681..c397135 100644 --- a/apps/api-server/src/procedures/orgs/members.ts +++ b/apps/api-server/src/procedures/orgs/members.ts @@ -3,7 +3,7 @@ */ import { ORPCError } from "@orpc/server"; -import { authMiddleware, os } from "../base.js"; +import { authedProcedure } from "../base.js"; import { countOwners, getMembership, @@ -15,9 +15,8 @@ import { * List all members of an org * Any member can view the member list */ -export const membersList = os.orgs.members.list - .use(authMiddleware) - .handler(async ({ input, context }) => { +export const membersList = authedProcedure.orgs.members.list.handler( + async ({ input, context }) => { const { slug } = input; // Lookup org and verify membership @@ -48,65 +47,70 @@ export const membersList = os.orgs.members.list role: m.role, createdAt: m.created_at, })); - }); + }, +); /** * Update a member's role * Only owners can change roles * Uses transaction to prevent race condition when demoting owners */ -export const membersUpdateRole = os.orgs.members.updateRole - .use(authMiddleware) - .handler(async ({ input, context }) => { - const { slug, userId, role: newRole } = input; +export const membersUpdateRole = + authedProcedure.orgs.members.updateRole.handler( + async ({ input, context }) => { + const { slug, userId, role: newRole } = 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"); + // 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.transaction().execute(async (trx) => { - // Get the target member's current membership - const targetMember = await trx - .selectFrom("org_members") - .select(["id", "role"]) - .where("org_id", "=", org.id) - .where("user_id", "=", userId) - .executeTakeFirst(); + await context.db.transaction().execute(async (trx) => { + // Get the target member's current membership + const targetMember = await trx + .selectFrom("org_members") + .select(["id", "role"]) + .where("org_id", "=", org.id) + .where("user_id", "=", userId) + .executeTakeFirst(); - if (!targetMember) { - throw new ORPCError("NOT_FOUND", { message: "Member not found" }); - } - - // If demoting an owner, check if they're the last one - if (targetMember.role === "owner" && newRole !== "owner") { - const ownerCount = await countOwners(trx, org.id); - if (ownerCount === 1) { - throw new ORPCError("BAD_REQUEST", { - message: "Cannot demote the only owner", - }); + if (!targetMember) { + throw new ORPCError("NOT_FOUND", { message: "Member not found" }); } - } - // Update the role - await trx - .updateTable("org_members") - .set({ role: newRole }) - .where("id", "=", targetMember.id) - .execute(); - }); + // If demoting an owner, check if they're the last one + if (targetMember.role === "owner" && newRole !== "owner") { + const ownerCount = await countOwners(trx, org.id); + if (ownerCount === 1) { + throw new ORPCError("BAD_REQUEST", { + message: "Cannot demote the only owner", + }); + } + } - return { success: true }; - }); + // Update the role + await trx + .updateTable("org_members") + .set({ role: newRole }) + .where("id", "=", targetMember.id) + .execute(); + }); + + return { success: true }; + }, + ); /** * Remove a member from an org * Owners can remove anyone, admins can only remove members * Uses transaction to prevent race condition when removing owners */ -export const membersRemove = os.orgs.members.remove - .use(authMiddleware) - .handler(async ({ input, context }) => { +export const membersRemove = authedProcedure.orgs.members.remove.handler( + async ({ input, context }) => { const { slug, userId } = input; // Lookup org and verify membership @@ -159,4 +163,5 @@ export const membersRemove = os.orgs.members.remove }); return { success: true }; - }); + }, +); diff --git a/apps/api-server/src/procedures/orgs/sites.ts b/apps/api-server/src/procedures/orgs/sites.ts index c2f3af4..8d3817f 100644 --- a/apps/api-server/src/procedures/orgs/sites.ts +++ b/apps/api-server/src/procedures/orgs/sites.ts @@ -2,16 +2,15 @@ * Org sites procedures - list */ -import { authMiddleware, os } from "../base.js"; +import { authedProcedure } from "../base.js"; import { getMembership, lookupOrgBySlug } from "./helpers.js"; /** * List all sites for an org * Any member can view the site list */ -export const sitesList = os.orgs.sites.list - .use(authMiddleware) - .handler(async ({ input, context }) => { +export const sitesList = authedProcedure.orgs.sites.list.handler( + async ({ input, context }) => { const { slug } = input; // Lookup org and verify membership @@ -31,4 +30,5 @@ export const sitesList = os.orgs.sites.list domain: s.domain, createdAt: s.created_at, })); - }); + }, +);