Refactor admin procedures into separate files

Extract admin procedures from router.ts into dedicated files under
procedures/admin/ with consolidated exports via _routes.ts. Adds shared
helper functions for response transformation and includes race condition
fixes via transaction-scoped existence checks.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
RevIQ
2026-01-09 17:00:04 +08:00
parent 2d445cc47b
commit c0966365f3
16 changed files with 579 additions and 112 deletions

View File

@@ -0,0 +1,43 @@
/**
* Admin routes - consolidated exports for os.router()
*/
import { adminAuthCompleteLogin } from "./auth/complete-login.js";
import { adminOrgsCreate } from "./orgs/create.js";
import { adminOrgsDelete } from "./orgs/delete.js";
import { adminOrgsGet } from "./orgs/get.js";
import { adminOrgsList } from "./orgs/list.js";
import {
adminOrgsAddSite,
adminOrgsListSites,
adminOrgsRemoveSite,
} from "./orgs/sites.js";
import { adminOrgsUpdate } from "./orgs/update.js";
import { adminUsersConfirmEmail } from "./users/confirm-email.js";
import { adminUsersCreate } from "./users/create.js";
import { adminUsersGet } from "./users/get.js";
import { adminUsersList } from "./users/list.js";
import { adminUsersUpdate } from "./users/update.js";
export const adminRoutes = {
orgs: {
list: adminOrgsList,
get: adminOrgsGet,
create: adminOrgsCreate,
update: adminOrgsUpdate,
delete: adminOrgsDelete,
listSites: adminOrgsListSites,
addSite: adminOrgsAddSite,
removeSite: adminOrgsRemoveSite,
},
users: {
list: adminUsersList,
get: adminUsersGet,
create: adminUsersCreate,
update: adminUsersUpdate,
confirmEmail: adminUsersConfirmEmail,
},
auth: {
completeLogin: adminAuthCompleteLogin,
},
};

View File

@@ -0,0 +1,32 @@
/**
* admin.auth.completeLogin - Complete pending login request (dev helper)
*/
import { ORPCError } from "@orpc/server";
import { authMiddleware, os, superuserMiddleware } from "../../base.js";
export const adminAuthCompleteLogin = os.admin.auth.completeLogin
.use(authMiddleware)
.use(superuserMiddleware)
.handler(async ({ input, context }) => {
const loginRequest = await context.db
.selectFrom("login_requests")
.where("email", "=", input.email.toLowerCase())
.where("completed_at", "is", null)
.where("expires_at", ">", new Date())
.orderBy("created_at", "desc")
.select(["id"])
.executeTakeFirst();
if (!loginRequest) {
throw new ORPCError("NOT_FOUND", {
message: "No pending login request found",
});
}
await context.db
.updateTable("login_requests")
.set({ completed_at: new Date() })
.where("id", "=", loginRequest.id)
.execute();
});

View File

@@ -0,0 +1,35 @@
/**
* Admin procedure helpers - shared transformation functions
*/
import type { Selectable } from "kysely";
import type { Orgs, OrgSites, Users } from "@reviq/db-schema";
/** Transform org record to API response format */
export const toOrgResponse = (org: Selectable<Orgs>) => ({
id: org.id,
slug: org.slug,
displayName: org.display_name,
logoUrl: org.logo_url,
createdAt: org.created_at,
});
/** Transform user record to API response format */
export const toUserResponse = (user: Selectable<Users>) => ({
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,
});
/** Transform site record to API response format */
export const toSiteResponse = (site: Selectable<OrgSites>) => ({
id: site.id,
domain: site.domain,
createdAt: site.created_at,
});

View File

@@ -0,0 +1,58 @@
/**
* admin.orgs.create - Create organization with owner
*/
import { ORPCError } from "@orpc/server";
import { authMiddleware, os, superuserMiddleware } from "../../base.js";
export const adminOrgsCreate = os.admin.orgs.create
.use(authMiddleware)
.use(superuserMiddleware)
.handler(async ({ input, context }) => {
const { slug, displayName, ownerEmail } = input;
// Find owner user by email (outside transaction - read-only)
const owner = await context.db
.selectFrom("users")
.where("email", "=", ownerEmail.toLowerCase())
.select(["id"])
.executeTakeFirst();
if (!owner) {
throw new ORPCError("NOT_FOUND", { message: "User not found" });
}
// Create org and owner membership in transaction (with race condition protection)
await context.db.transaction().execute(async (trx) => {
// Check for existing org inside transaction to prevent race condition
const existingOrg = await trx
.selectFrom("orgs")
.where("slug", "=", slug)
.select(["id"])
.executeTakeFirst();
if (existingOrg) {
throw new ORPCError("CONFLICT", {
message: "Organization with this slug already exists",
});
}
const newOrg = await trx
.insertInto("orgs")
.values({
slug,
display_name: displayName,
})
.returning(["id"])
.executeTakeFirstOrThrow();
await trx
.insertInto("org_members")
.values({
org_id: newOrg.id,
user_id: owner.id,
role: "owner",
})
.execute();
});
return { slug };
});

View File

@@ -0,0 +1,36 @@
/**
* admin.orgs.delete - Delete organization and all related records
*/
import { ORPCError } from "@orpc/server";
import { authMiddleware, os, superuserMiddleware } from "../../base.js";
export const adminOrgsDelete = os.admin.orgs.delete
.use(authMiddleware)
.use(superuserMiddleware)
.handler(async ({ input, context }) => {
const { slug } = input;
// Delete org and related records in transaction
await context.db.transaction().execute(async (trx) => {
const org = await trx
.selectFrom("orgs")
.where("slug", "=", slug)
.select(["id"])
.executeTakeFirst();
if (!org) {
throw new ORPCError("NOT_FOUND", { message: "Organization not found" });
}
await trx
.deleteFrom("org_invites")
.where("org_id", "=", org.id)
.execute();
await trx.deleteFrom("org_sites").where("org_id", "=", org.id).execute();
await trx
.deleteFrom("org_members")
.where("org_id", "=", org.id)
.execute();
await trx.deleteFrom("orgs").where("id", "=", org.id).execute();
});
});

View File

@@ -0,0 +1,22 @@
/**
* admin.orgs.get - Get organization by slug
*/
import { ORPCError } from "@orpc/server";
import { authMiddleware, os, superuserMiddleware } from "../../base.js";
import { toOrgResponse } from "../helpers.js";
export const adminOrgsGet = os.admin.orgs.get
.use(authMiddleware)
.use(superuserMiddleware)
.handler(async ({ input, context }) => {
const org = await context.db
.selectFrom("orgs")
.where("slug", "=", input.slug)
.selectAll()
.executeTakeFirst();
if (!org) {
throw new ORPCError("NOT_FOUND", { message: "Organization not found" });
}
return toOrgResponse(org);
});

View File

@@ -0,0 +1,14 @@
/**
* admin.orgs.list - List all organizations
*/
import { authMiddleware, os, superuserMiddleware } from "../../base.js";
import { toOrgResponse } from "../helpers.js";
export const adminOrgsList = os.admin.orgs.list
.use(authMiddleware)
.use(superuserMiddleware)
.handler(async ({ context }) => {
const orgs = await context.db.selectFrom("orgs").selectAll().execute();
return orgs.map(toOrgResponse);
});

View File

@@ -0,0 +1,97 @@
/**
* admin.orgs.listSites, admin.orgs.addSite, admin.orgs.removeSite
* Site management for organizations
*/
import { ORPCError } from "@orpc/server";
import { authMiddleware, os, superuserMiddleware } from "../../base.js";
import { toSiteResponse } from "../helpers.js";
export const adminOrgsListSites = os.admin.orgs.listSites
.use(authMiddleware)
.use(superuserMiddleware)
.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 sites = await context.db
.selectFrom("org_sites")
.where("org_id", "=", org.id)
.selectAll()
.execute();
return sites.map(toSiteResponse);
});
export const adminOrgsAddSite = os.admin.orgs.addSite
.use(authMiddleware)
.use(superuserMiddleware)
.handler(async ({ input, context }) => {
const { slug, domain } = input;
// Use transaction to prevent race condition on site creation
await context.db.transaction().execute(async (trx) => {
const org = await trx
.selectFrom("orgs")
.where("slug", "=", slug)
.select(["id"])
.executeTakeFirst();
if (!org) {
throw new ORPCError("NOT_FOUND", { message: "Organization not found" });
}
// Check if site already exists (inside transaction)
const existingSite = await trx
.selectFrom("org_sites")
.where("domain", "=", domain)
.select(["id"])
.executeTakeFirst();
if (existingSite) {
throw new ORPCError("CONFLICT", {
message: "Site with this domain already exists",
});
}
await trx
.insertInto("org_sites")
.values({
org_id: org.id,
domain,
})
.execute();
});
});
export const adminOrgsRemoveSite = os.admin.orgs.removeSite
.use(authMiddleware)
.use(superuserMiddleware)
.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 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" });
}
});

View File

@@ -0,0 +1,50 @@
/**
* admin.orgs.update - Update organization
*/
import { ORPCError } from "@orpc/server";
import { authMiddleware, os, superuserMiddleware } from "../../base.js";
export const adminOrgsUpdate = os.admin.orgs.update
.use(authMiddleware)
.use(superuserMiddleware)
.handler(async ({ input, context }) => {
const { slug, displayName, logoUrl } = input;
// Check if there are actual updates to make
if (displayName === undefined && logoUrl === undefined) {
// Verify org exists even for no-op
const org = await context.db
.selectFrom("orgs")
.where("slug", "=", slug)
.select(["id"])
.executeTakeFirst();
if (!org) {
throw new ORPCError("NOT_FOUND", { message: "Organization not found" });
}
return;
}
const updates: Partial<{
display_name: string;
logo_url: string | null;
updated_at: Date;
}> = { updated_at: new Date() };
if (displayName !== undefined) {
updates.display_name = displayName;
}
if (logoUrl !== undefined) {
updates.logo_url = logoUrl || null;
}
const result = await context.db
.updateTable("orgs")
.set(updates)
.where("slug", "=", slug)
.executeTakeFirst();
if (!result.numUpdatedRows || result.numUpdatedRows === 0n) {
throw new ORPCError("NOT_FOUND", { message: "Organization not found" });
}
});

View File

@@ -0,0 +1,24 @@
/**
* admin.users.confirmEmail - Confirm a user's email (used by CLI)
*/
import { ORPCError } from "@orpc/server";
import { authMiddleware, os, superuserMiddleware } from "../../base.js";
export const adminUsersConfirmEmail = os.admin.users.confirmEmail
.use(authMiddleware)
.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();
if (!result.numUpdatedRows || result.numUpdatedRows === 0n) {
throw new ORPCError("NOT_FOUND", { message: "User not found" });
}
});

View File

@@ -0,0 +1,63 @@
/**
* admin.users.create - Create passwordless user, optionally add to org
*/
import { ORPCError } from "@orpc/server";
import { authMiddleware, os, superuserMiddleware } from "../../base.js";
export const adminUsersCreate = os.admin.users.create
.use(authMiddleware)
.use(superuserMiddleware)
.handler(async ({ input, context }) => {
const { email, name, orgSlug, orgRole } = input;
const normalizedEmail = email.toLowerCase();
// If orgSlug provided, verify org exists (outside transaction - read-only)
let orgId: number | undefined;
if (orgSlug) {
const org = await context.db
.selectFrom("orgs")
.where("slug", "=", orgSlug)
.select(["id"])
.executeTakeFirst();
if (!org) {
throw new ORPCError("NOT_FOUND", { message: "Organization not found" });
}
orgId = org.id;
}
// Create user in transaction (with race condition protection)
await context.db.transaction().execute(async (trx) => {
// Check for existing user inside transaction to prevent race condition
const existingUser = await trx
.selectFrom("users")
.where("email", "=", normalizedEmail)
.select(["id"])
.executeTakeFirst();
if (existingUser) {
throw new ORPCError("CONFLICT", {
message: "User with this email already exists",
});
}
const newUser = await trx
.insertInto("users")
.values({
email: normalizedEmail,
display_name: name || null,
})
.returning(["id"])
.executeTakeFirstOrThrow();
if (orgId !== undefined) {
await trx
.insertInto("org_members")
.values({
org_id: orgId,
user_id: newUser.id,
role: orgRole || "member",
})
.execute();
}
});
});

View File

@@ -0,0 +1,22 @@
/**
* admin.users.get - Get user by email
*/
import { ORPCError } from "@orpc/server";
import { authMiddleware, os, superuserMiddleware } from "../../base.js";
import { toUserResponse } from "../helpers.js";
export const adminUsersGet = os.admin.users.get
.use(authMiddleware)
.use(superuserMiddleware)
.handler(async ({ input, context }) => {
const user = await context.db
.selectFrom("users")
.where("email", "=", input.email.toLowerCase())
.selectAll()
.executeTakeFirst();
if (!user) {
throw new ORPCError("NOT_FOUND", { message: "User not found" });
}
return toUserResponse(user);
});

View File

@@ -0,0 +1,14 @@
/**
* admin.users.list - List all users
*/
import { authMiddleware, os, superuserMiddleware } from "../../base.js";
import { toUserResponse } from "../helpers.js";
export const adminUsersList = os.admin.users.list
.use(authMiddleware)
.use(superuserMiddleware)
.handler(async ({ context }) => {
const users = await context.db.selectFrom("users").selectAll().execute();
return users.map(toUserResponse);
});

View File

@@ -0,0 +1,51 @@
/**
* admin.users.update - Update user properties (e.g., isSuperuser)
*/
import { ORPCError } from "@orpc/server";
import { authMiddleware, os, superuserMiddleware } from "../../base.js";
export const adminUsersUpdate = os.admin.users.update
.use(authMiddleware)
.use(superuserMiddleware)
.handler(async ({ input, context }) => {
const { email, isSuperuser } = input;
const normalizedEmail = email.toLowerCase();
// Check if there are actual updates to make
if (isSuperuser === undefined) {
// Verify user exists even for no-op
const user = await context.db
.selectFrom("users")
.where("email", "=", normalizedEmail)
.select(["id"])
.executeTakeFirst();
if (!user) {
throw new ORPCError("NOT_FOUND", { message: "User not found" });
}
return;
}
// Prevent superuser from demoting themselves
if (
isSuperuser === false &&
normalizedEmail === context.user.email.toLowerCase()
) {
throw new ORPCError("BAD_REQUEST", {
message: "Cannot remove your own superuser status",
});
}
const result = await context.db
.updateTable("users")
.set({
is_superuser: isSuperuser,
updated_at: new Date(),
})
.where("email", "=", normalizedEmail)
.executeTakeFirst();
if (!result.numUpdatedRows || result.numUpdatedRows === 0n) {
throw new ORPCError("NOT_FOUND", { message: "User not found" });
}
});