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" });
}
});

View File

@@ -9,11 +9,8 @@ import { resendVerificationEmail as resendVerificationHandler } from "./procedur
import { resetPassword as resetPasswordHandler } from "./procedures/auth/reset-password.js"; import { resetPassword as resetPasswordHandler } from "./procedures/auth/reset-password.js";
import { signup as signupHandler } from "./procedures/auth/signup.js"; import { signup as signupHandler } from "./procedures/auth/signup.js";
import { verifyEmail as verifyEmailHandler } from "./procedures/auth/verify-email.js"; import { verifyEmail as verifyEmailHandler } from "./procedures/auth/verify-email.js";
import { import { authMiddleware, loginRequestMiddleware, os } from "./procedures/base.js";
authMiddleware, import { adminRoutes } from "./procedures/admin/_routes.js";
loginRequestMiddleware,
os,
} from "./procedures/base.js";
import { meDelete } from "./procedures/me/delete.js"; import { meDelete } from "./procedures/me/delete.js";
import { import {
getDeviceInfo, getDeviceInfo,
@@ -250,91 +247,6 @@ const sitesList = os.orgs.sites.list.use(authMiddleware).handler(async () => {
throw new ORPCError("NOT_IMPLEMENTED", { message: "Not implemented" }); throw new ORPCError("NOT_IMPLEMENTED", { message: "Not implemented" });
}); });
// Admin orgs procedures (require superuser - for now just auth, will add superuser middleware later)
const adminOrgsList = os.admin.orgs.list
.use(authMiddleware)
.handler(async () => {
throw new ORPCError("NOT_IMPLEMENTED", { message: "Not implemented" });
});
const adminOrgsGet = os.admin.orgs.get.use(authMiddleware).handler(async () => {
throw new ORPCError("NOT_IMPLEMENTED", { message: "Not implemented" });
});
const adminOrgsCreate = os.admin.orgs.create
.use(authMiddleware)
.handler(async () => {
throw new ORPCError("NOT_IMPLEMENTED", { message: "Not implemented" });
});
const adminOrgsUpdate = os.admin.orgs.update
.use(authMiddleware)
.handler(async () => {
throw new ORPCError("NOT_IMPLEMENTED", { message: "Not implemented" });
});
const adminOrgsDelete = os.admin.orgs.delete
.use(authMiddleware)
.handler(async () => {
throw new ORPCError("NOT_IMPLEMENTED", { message: "Not implemented" });
});
const adminOrgsListSites = os.admin.orgs.listSites
.use(authMiddleware)
.handler(async () => {
throw new ORPCError("NOT_IMPLEMENTED", { message: "Not implemented" });
});
const adminOrgsAddSite = os.admin.orgs.addSite
.use(authMiddleware)
.handler(async () => {
throw new ORPCError("NOT_IMPLEMENTED", { message: "Not implemented" });
});
const adminOrgsRemoveSite = os.admin.orgs.removeSite
.use(authMiddleware)
.handler(async () => {
throw new ORPCError("NOT_IMPLEMENTED", { message: "Not implemented" });
});
// Admin users procedures
const adminUsersList = os.admin.users.list
.use(authMiddleware)
.handler(async () => {
throw new ORPCError("NOT_IMPLEMENTED", { message: "Not implemented" });
});
const adminUsersGet = os.admin.users.get
.use(authMiddleware)
.handler(async () => {
throw new ORPCError("NOT_IMPLEMENTED", { message: "Not implemented" });
});
const adminUsersCreate = os.admin.users.create
.use(authMiddleware)
.handler(async () => {
throw new ORPCError("NOT_IMPLEMENTED", { message: "Not implemented" });
});
const adminUsersUpdate = os.admin.users.update
.use(authMiddleware)
.handler(async () => {
throw new ORPCError("NOT_IMPLEMENTED", { message: "Not implemented" });
});
const adminUsersConfirmEmail = os.admin.users.confirmEmail
.use(authMiddleware)
.handler(async () => {
throw new ORPCError("NOT_IMPLEMENTED", { message: "Not implemented" });
});
// Admin auth procedures
const adminAuthCompleteLogin = os.admin.auth.completeLogin
.use(authMiddleware)
.handler(async () => {
throw new ORPCError("NOT_IMPLEMENTED", { message: "Not implemented" });
});
// Build the router // Build the router
export const router = os.router({ export const router = os.router({
auth: { auth: {
@@ -397,26 +309,5 @@ export const router = os.router({
list: sitesList, list: sitesList,
}, },
}, },
admin: { admin: 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

@@ -76,6 +76,15 @@ publisher-dashboard/
│ │ ├── index.ts # Server entry point (Bun.serve) │ │ ├── index.ts # Server entry point (Bun.serve)
│ │ ├── router.ts │ │ ├── router.ts
│ │ ├── procedures/ │ │ ├── procedures/
│ │ │ ├── base.ts # Middleware (auth, superuser, loginRequest)
│ │ │ ├── auth/ # Auth procedures
│ │ │ ├── me/ # User self-management procedures
│ │ │ └── admin/ # Superuser-only procedures
│ │ │ ├── _routes.ts # Consolidated admin route exports
│ │ │ ├── helpers.ts # Shared transform functions
│ │ │ ├── auth/
│ │ │ ├── orgs/
│ │ │ └── users/
│ │ └── middleware/ │ │ └── middleware/
│ ├── publisher-dashboard/ # SvelteKit frontend │ ├── publisher-dashboard/ # SvelteKit frontend
│ │ ├── package.json │ │ ├── package.json
@@ -2322,6 +2331,12 @@ _Can run parallel to J2-J6_
- [x] **K4**: Implement `admin.orgs.addSite`, `admin.orgs.removeSite` - [x] **K4**: Implement `admin.orgs.addSite`, `admin.orgs.removeSite`
- [x] **K5**: Implement `admin.auth.completeLogin` (dev helper) - [x] **K5**: Implement `admin.auth.completeLogin` (dev helper)
_Implementation notes:_
- Files in `procedures/admin/` with `_routes.ts` for consolidated exports
- Helper functions in `helpers.ts`: `toOrgResponse`, `toUserResponse`, `toSiteResponse`
- Race conditions prevented via transaction-scoped existence checks
- Self-demotion guard in `adminUsersUpdate` prevents superusers from removing their own status
#### Workstream L: Org Pages (Frontend) #### Workstream L: Org Pages (Frontend)
_Depends on: J1-J6, C3_ _Depends on: J1-J6, C3_