From c0966365f34a118d2d3244fcba4ee098f7417bae Mon Sep 17 00:00:00 2001 From: RevIQ Date: Fri, 9 Jan 2026 17:00:04 +0800 Subject: [PATCH] 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 --- .../src/procedures/admin/_routes.ts | 43 +++++++ .../procedures/admin/auth/complete-login.ts | 32 +++++ .../src/procedures/admin/helpers.ts | 35 ++++++ .../src/procedures/admin/orgs/create.ts | 58 +++++++++ .../src/procedures/admin/orgs/delete.ts | 36 ++++++ .../src/procedures/admin/orgs/get.ts | 22 ++++ .../src/procedures/admin/orgs/list.ts | 14 +++ .../src/procedures/admin/orgs/sites.ts | 97 +++++++++++++++ .../src/procedures/admin/orgs/update.ts | 50 ++++++++ .../procedures/admin/users/confirm-email.ts | 24 ++++ .../src/procedures/admin/users/create.ts | 63 ++++++++++ .../src/procedures/admin/users/get.ts | 22 ++++ .../src/procedures/admin/users/list.ts | 14 +++ .../src/procedures/admin/users/update.ts | 51 ++++++++ apps/api-server/src/router.ts | 115 +----------------- docs/initial-app.md | 15 +++ 16 files changed, 579 insertions(+), 112 deletions(-) create mode 100644 apps/api-server/src/procedures/admin/_routes.ts create mode 100644 apps/api-server/src/procedures/admin/auth/complete-login.ts create mode 100644 apps/api-server/src/procedures/admin/helpers.ts create mode 100644 apps/api-server/src/procedures/admin/orgs/create.ts create mode 100644 apps/api-server/src/procedures/admin/orgs/delete.ts create mode 100644 apps/api-server/src/procedures/admin/orgs/get.ts create mode 100644 apps/api-server/src/procedures/admin/orgs/list.ts create mode 100644 apps/api-server/src/procedures/admin/orgs/sites.ts create mode 100644 apps/api-server/src/procedures/admin/orgs/update.ts create mode 100644 apps/api-server/src/procedures/admin/users/confirm-email.ts create mode 100644 apps/api-server/src/procedures/admin/users/create.ts create mode 100644 apps/api-server/src/procedures/admin/users/get.ts create mode 100644 apps/api-server/src/procedures/admin/users/list.ts create mode 100644 apps/api-server/src/procedures/admin/users/update.ts diff --git a/apps/api-server/src/procedures/admin/_routes.ts b/apps/api-server/src/procedures/admin/_routes.ts new file mode 100644 index 0000000..89988c4 --- /dev/null +++ b/apps/api-server/src/procedures/admin/_routes.ts @@ -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, + }, +}; diff --git a/apps/api-server/src/procedures/admin/auth/complete-login.ts b/apps/api-server/src/procedures/admin/auth/complete-login.ts new file mode 100644 index 0000000..d49a1bd --- /dev/null +++ b/apps/api-server/src/procedures/admin/auth/complete-login.ts @@ -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(); + }); diff --git a/apps/api-server/src/procedures/admin/helpers.ts b/apps/api-server/src/procedures/admin/helpers.ts new file mode 100644 index 0000000..9043a46 --- /dev/null +++ b/apps/api-server/src/procedures/admin/helpers.ts @@ -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) => ({ + 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) => ({ + 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) => ({ + id: site.id, + domain: site.domain, + createdAt: site.created_at, +}); diff --git a/apps/api-server/src/procedures/admin/orgs/create.ts b/apps/api-server/src/procedures/admin/orgs/create.ts new file mode 100644 index 0000000..3c0307b --- /dev/null +++ b/apps/api-server/src/procedures/admin/orgs/create.ts @@ -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 }; + }); diff --git a/apps/api-server/src/procedures/admin/orgs/delete.ts b/apps/api-server/src/procedures/admin/orgs/delete.ts new file mode 100644 index 0000000..8b1e609 --- /dev/null +++ b/apps/api-server/src/procedures/admin/orgs/delete.ts @@ -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(); + }); + }); diff --git a/apps/api-server/src/procedures/admin/orgs/get.ts b/apps/api-server/src/procedures/admin/orgs/get.ts new file mode 100644 index 0000000..13117a6 --- /dev/null +++ b/apps/api-server/src/procedures/admin/orgs/get.ts @@ -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); + }); diff --git a/apps/api-server/src/procedures/admin/orgs/list.ts b/apps/api-server/src/procedures/admin/orgs/list.ts new file mode 100644 index 0000000..13f1ad5 --- /dev/null +++ b/apps/api-server/src/procedures/admin/orgs/list.ts @@ -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); + }); diff --git a/apps/api-server/src/procedures/admin/orgs/sites.ts b/apps/api-server/src/procedures/admin/orgs/sites.ts new file mode 100644 index 0000000..250ebe6 --- /dev/null +++ b/apps/api-server/src/procedures/admin/orgs/sites.ts @@ -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" }); + } + }); diff --git a/apps/api-server/src/procedures/admin/orgs/update.ts b/apps/api-server/src/procedures/admin/orgs/update.ts new file mode 100644 index 0000000..d2b2bb1 --- /dev/null +++ b/apps/api-server/src/procedures/admin/orgs/update.ts @@ -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" }); + } + }); diff --git a/apps/api-server/src/procedures/admin/users/confirm-email.ts b/apps/api-server/src/procedures/admin/users/confirm-email.ts new file mode 100644 index 0000000..a2fa6f1 --- /dev/null +++ b/apps/api-server/src/procedures/admin/users/confirm-email.ts @@ -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" }); + } + }); diff --git a/apps/api-server/src/procedures/admin/users/create.ts b/apps/api-server/src/procedures/admin/users/create.ts new file mode 100644 index 0000000..e749584 --- /dev/null +++ b/apps/api-server/src/procedures/admin/users/create.ts @@ -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(); + } + }); + }); diff --git a/apps/api-server/src/procedures/admin/users/get.ts b/apps/api-server/src/procedures/admin/users/get.ts new file mode 100644 index 0000000..b914e30 --- /dev/null +++ b/apps/api-server/src/procedures/admin/users/get.ts @@ -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); + }); diff --git a/apps/api-server/src/procedures/admin/users/list.ts b/apps/api-server/src/procedures/admin/users/list.ts new file mode 100644 index 0000000..e1c6d27 --- /dev/null +++ b/apps/api-server/src/procedures/admin/users/list.ts @@ -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); + }); diff --git a/apps/api-server/src/procedures/admin/users/update.ts b/apps/api-server/src/procedures/admin/users/update.ts new file mode 100644 index 0000000..8d6ef17 --- /dev/null +++ b/apps/api-server/src/procedures/admin/users/update.ts @@ -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" }); + } + }); diff --git a/apps/api-server/src/router.ts b/apps/api-server/src/router.ts index cdf8caa..629ebe6 100644 --- a/apps/api-server/src/router.ts +++ b/apps/api-server/src/router.ts @@ -9,11 +9,8 @@ import { resendVerificationEmail as resendVerificationHandler } from "./procedur import { resetPassword as resetPasswordHandler } from "./procedures/auth/reset-password.js"; import { signup as signupHandler } from "./procedures/auth/signup.js"; import { verifyEmail as verifyEmailHandler } from "./procedures/auth/verify-email.js"; -import { - authMiddleware, - loginRequestMiddleware, - os, -} from "./procedures/base.js"; +import { authMiddleware, loginRequestMiddleware, os } from "./procedures/base.js"; +import { adminRoutes } from "./procedures/admin/_routes.js"; import { meDelete } from "./procedures/me/delete.js"; import { getDeviceInfo, @@ -250,91 +247,6 @@ const sitesList = os.orgs.sites.list.use(authMiddleware).handler(async () => { 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 export const router = os.router({ auth: { @@ -397,26 +309,5 @@ export const router = os.router({ list: sitesList, }, }, - admin: { - 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, - }, - }, + admin: adminRoutes, }); diff --git a/docs/initial-app.md b/docs/initial-app.md index e9cfbee..8ecb739 100644 --- a/docs/initial-app.md +++ b/docs/initial-app.md @@ -76,6 +76,15 @@ publisher-dashboard/ │ │ ├── index.ts # Server entry point (Bun.serve) │ │ ├── router.ts │ │ ├── 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/ │ ├── publisher-dashboard/ # SvelteKit frontend │ │ ├── package.json @@ -2322,6 +2331,12 @@ _Can run parallel to J2-J6_ - [x] **K4**: Implement `admin.orgs.addSite`, `admin.orgs.removeSite` - [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) _Depends on: J1-J6, C3_