diff --git a/server/src/honoMiddlewares/adminAuthMiddleware.ts b/server/src/honoMiddlewares/adminAuthMiddleware.ts new file mode 100644 index 000000000..1f0eddb1f --- /dev/null +++ b/server/src/honoMiddlewares/adminAuthMiddleware.ts @@ -0,0 +1,28 @@ +import { ErrCode, RecaseError } from "@autumn/shared"; +import type { Context, Next } from "hono"; +import type { HonoEnv } from "@/honoUtils/HonoEnv.js"; +import { auth } from "@/utils/auth.js"; +import { ADMIN_USER_IDs } from "@/utils/constants.js"; + +/** + * Admin auth middleware for Hono + * Validates that the user is an admin user + */ +export const adminAuthMiddleware = async (c: Context, next: Next) => { + const data = await auth.api.getSession({ + headers: c.req.raw.headers, + }); + + if ( + !ADMIN_USER_IDs.includes(data?.session?.userId || "") && + !ADMIN_USER_IDs.includes(data?.session?.impersonatedBy || "") + ) { + throw new RecaseError({ + message: "Method not allowed", + code: ErrCode.InvalidRequest, + statusCode: 403, + }); + } + + await next(); +}; diff --git a/server/src/initHono.ts b/server/src/initHono.ts index 56c594d87..b1196b2b0 100644 --- a/server/src/initHono.ts +++ b/server/src/initHono.ts @@ -1,13 +1,11 @@ import { getRequestListener } from "@hono/node-server"; -import { type Context, Hono } from "hono"; +import { Hono } from "hono"; import { cors } from "hono/cors"; -import type { ContentfulStatusCode } from "hono/utils/http-status"; import { vercelWebhookRouter } from "./external/vercel/vercelWebhookRouter.js"; import { handleConnectWebhook } from "./external/webhooks/connectWebhookRouter.js"; import { analyticsMiddleware } from "./honoMiddlewares/analyticsMiddleware.js"; import { apiVersionMiddleware } from "./honoMiddlewares/apiVersionMiddleware.js"; import { baseMiddleware } from "./honoMiddlewares/baseMiddleware.js"; -import { betterAuthMiddleware } from "./honoMiddlewares/betterAuthMiddleware.js"; import { errorMiddleware } from "./honoMiddlewares/errorMiddleware.js"; import { orgConfigMiddleware } from "./honoMiddlewares/orgConfigMiddleware.js"; import { queryMiddleware } from "./honoMiddlewares/queryMiddleware.js"; @@ -19,18 +17,17 @@ import { handleHealthCheck } from "./honoUtils/handleHealthCheck.js"; import { balancesRouter } from "./internal/balances/balancesRouter.js"; import { billingRouter } from "./internal/billing/billingRouter.js"; import { cusRouter } from "./internal/customers/cusRouter.js"; -import { internalCusRouter } from "./internal/customers/internalCusRouter.js"; import { entityRouter } from "./internal/entities/entityRouter.js"; import { featureRouter } from "./internal/features/featureRouter.js"; import { handleOAuthCallback } from "./internal/orgs/handlers/stripeHandlers/handleOAuthCallback.js"; import { honoOrgRouter } from "./internal/orgs/orgRouter.js"; import { platformBetaRouter } from "./internal/platform/platformBeta/platformBetaRouter.js"; -import { internalProductRouter } from "./internal/products/internalProductRouter.js"; import { honoProductBetaRouter, honoProductRouter, migrationRouter, } from "./internal/products/productRouter.js"; +import { internalRouter } from "./routers/internalRouter.js"; import { auth } from "./utils/auth.js"; const ALLOWED_ORIGINS = [ @@ -112,12 +109,6 @@ export const createHonoApp = () => { app.use("/v1/*", refreshCacheMiddleware); app.use("/v1/*", queryMiddleware()); - // General org rate limiter for all other /v1/* routes - // app.use("/v1/*", generalRateLimiter); - app.get("/v1/test-incident", (c: Context) => { - return c.json({ message: "Hello, world!" }, 520 as ContentfulStatusCode); - }); - app.route("v1", billingRouter); app.route("v1", balancesRouter); app.route("v1", migrationRouter); @@ -134,11 +125,7 @@ export const createHonoApp = () => { app.route("v1/platform/beta", platformBetaRouter); app.route("v1/organization", honoOrgRouter); - // Internal/dashboard routes - use betterAuthMiddleware for session auth - app.use("/products/*", betterAuthMiddleware); - app.route("/products", internalProductRouter); - app.use("/customers/*", betterAuthMiddleware); - app.route("/customers", internalCusRouter); + app.route("", internalRouter); app.onError(errorMiddleware); diff --git a/server/src/internal/admin/adminRouter.ts b/server/src/internal/admin/adminRouter.ts index d466b5802..b17e16449 100644 --- a/server/src/internal/admin/adminRouter.ts +++ b/server/src/internal/admin/adminRouter.ts @@ -1,164 +1,9 @@ -import { member, organizations, user } from "@autumn/shared"; -import { and, desc, eq, gt, gte, ilike, inArray, lt, or } from "drizzle-orm"; -import { Router } from "express"; -import { handleFrontendReqError } from "@/utils/errorUtils.js"; -import type { ExtendedRequest } from "@/utils/models/Request.js"; +import { Hono } from "hono"; +import type { HonoEnv } from "../../honoUtils/HonoEnv"; +import { handleListAdminOrgs } from "./handleListAdminOrgs"; +import { handleListAdminUsers } from "./handleListAdminUsers"; -export const adminRouter: Router = Router(); +export const honoAdminRouter = new Hono(); -// adminRouter.post() - -adminRouter.get("/users", async (req: any, res: any) => { - try { - const { db } = req as ExtendedRequest; - - let { search, after, before } = req.query; - - if (after) { - after = { - id: after.split(",")[0], - createdAt: new Date(after.split(",")[1]), - }; - } else if (before) { - before = { - id: before.split(",")[0], - createdAt: new Date(before.split(",")[1]), - }; - } - - const users = await db - .select() - .from(user) - .where( - and( - search - ? or( - ilike(user.email, `%${search as string}%`), - ilike(user.name, `%${search as string}%`), - ilike(user.id, `%${search as string}%`), - ) - : undefined, - after - ? or( - lt(user.createdAt, after.createdAt), - or( - and( - eq(user.createdAt, after.createdAt), - lt(user.id, after.id), - ), - ), - ) - : undefined, - before - ? or( - gte(user.createdAt, before.createdAt), - or( - and( - eq(user.createdAt, before.createdAt), - gt(user.id, before.id), - ), - ), - ) - : undefined, - ), - ) - .orderBy(desc(user.createdAt), desc(user.id)) - .limit(21); - - res.json({ - rows: users.slice(0, 20), - hasNextPage: users.length > 20, - }); - } catch (error) { - handleFrontendReqError({ - res, - req, - error, - action: "admin: search users", - }); - } -}); - -adminRouter.get("/orgs", async (req: any, res: any) => { - try { - const { db } = req as ExtendedRequest; - - let { search, after, before } = req.query; - - if (after) { - after = { - id: after.split(",")[0], - createdAt: new Date(after.split(",")[1]), - }; - } else if (before) { - before = { - id: before.split(",")[0], - createdAt: new Date(before.split(",")[1]), - }; - } - - const orgs = await db - .select() - .from(organizations) - .where( - and( - search - ? or( - ilike(organizations.name, `%${search as string}%`), - ilike(organizations.id, `%${search as string}%`), - ilike(organizations.slug, `%${search as string}%`), - ) - : undefined, - after - ? or( - lt(organizations.createdAt, after.createdAt), - or( - and( - eq(organizations.createdAt, after.createdAt), - lt(organizations.id, after.id), - ), - ), - ) - : undefined, - before - ? or( - gte(organizations.createdAt, before.createdAt), - or( - and( - eq(organizations.createdAt, before.createdAt), - gt(organizations.id, before.id), - ), - ), - ) - : undefined, - ), - ) - .orderBy(desc(organizations.createdAt), desc(organizations.id)) - .limit(21); - - const orgIds = orgs.map((org) => org.id); - - const memberships = await db - .select() - .from(member) - .leftJoin(user, eq(member.userId, user.id)) - .where(inArray(member.organizationId, orgIds)); - - res.json({ - rows: orgs.slice(0, 20).map((org) => ({ - ...org, - users: memberships - .filter((membership) => membership.member.organizationId === org.id) - .map((membership) => membership.user), - })), - hasNextPage: orgs.length > 20, - }); - } catch (error) { - handleFrontendReqError({ - res, - req, - error, - action: "admin: search orgs", - }); - } -}); +honoAdminRouter.get("/users", ...handleListAdminUsers); +honoAdminRouter.get("/orgs", ...handleListAdminOrgs); diff --git a/server/src/internal/admin/handleListAdminOrgs.ts b/server/src/internal/admin/handleListAdminOrgs.ts new file mode 100644 index 000000000..aa257a1cf --- /dev/null +++ b/server/src/internal/admin/handleListAdminOrgs.ts @@ -0,0 +1,89 @@ +import { member, organizations, user } from "@autumn/shared"; +import { and, desc, eq, gt, gte, ilike, inArray, lt, or } from "drizzle-orm"; +import { createRoute } from "../../honoMiddlewares/routeHandler"; + +export const handleListAdminOrgs = createRoute({ + handler: async (c) => { + const ctx = c.get("ctx"); + const { db } = ctx; + + const { search, after: afterQuery, before: beforeQuery } = c.req.query(); + + let after, + before: + | { + id: string; + createdAt: Date; + } + | undefined; + + if (afterQuery) { + after = { + id: afterQuery.split(",")[0], + createdAt: new Date(afterQuery.split(",")[1]), + }; + } else if (beforeQuery) { + before = { + id: beforeQuery.split(",")[0], + createdAt: new Date(beforeQuery.split(",")[1]), + }; + } + + const orgs = await db + .select() + .from(organizations) + .where( + and( + search + ? or( + ilike(organizations.name, `%${search as string}%`), + ilike(organizations.id, `%${search as string}%`), + ilike(organizations.slug, `%${search as string}%`), + ) + : undefined, + after + ? or( + lt(organizations.createdAt, after.createdAt), + or( + and( + eq(organizations.createdAt, after.createdAt), + lt(organizations.id, after.id), + ), + ), + ) + : undefined, + before + ? or( + gte(organizations.createdAt, before.createdAt), + or( + and( + eq(organizations.createdAt, before.createdAt), + gt(organizations.id, before.id), + ), + ), + ) + : undefined, + ), + ) + .orderBy(desc(organizations.createdAt), desc(organizations.id)) + .limit(21); + + const orgIds = orgs.map((org) => org.id); + + const memberships = await db + .select() + .from(member) + .leftJoin(user, eq(member.userId, user.id)) + .where(inArray(member.organizationId, orgIds)); + + return c.json({ + rows: orgs.slice(0, 20).map((org) => ({ + ...org, + users: memberships + .filter((membership) => membership.member.organizationId === org.id) + .map((membership) => membership.user), + })), + hasNextPage: orgs.length > 20, + }); + }, +}); diff --git a/server/src/internal/admin/handleListAdminUsers.ts b/server/src/internal/admin/handleListAdminUsers.ts new file mode 100644 index 000000000..f5bfe0070 --- /dev/null +++ b/server/src/internal/admin/handleListAdminUsers.ts @@ -0,0 +1,76 @@ +import { user } from "@autumn/shared"; +import { and, desc, eq, gt, gte, ilike, lt, or } from "drizzle-orm"; +import { createRoute } from "../../honoMiddlewares/routeHandler"; + +export const handleListAdminUsers = createRoute({ + handler: async (c) => { + const ctx = c.get("ctx"); + const { db } = ctx; + + const { search, after: afterQuery, before: beforeQuery } = c.req.query(); + + let after, + before: + | { + id: string; + createdAt: Date; + } + | undefined; + + if (afterQuery) { + after = { + id: afterQuery.split(",")[0], + createdAt: new Date(afterQuery.split(",")[1]), + }; + } else if (beforeQuery) { + before = { + id: beforeQuery.split(",")[0], + createdAt: new Date(beforeQuery.split(",")[1]), + }; + } + + const users = await db + .select() + .from(user) + .where( + and( + search + ? or( + ilike(user.email, `%${search as string}%`), + ilike(user.name, `%${search as string}%`), + ilike(user.id, `%${search as string}%`), + ) + : undefined, + after + ? or( + lt(user.createdAt, after.createdAt), + or( + and( + eq(user.createdAt, after.createdAt), + lt(user.id, after.id), + ), + ), + ) + : undefined, + before + ? or( + gte(user.createdAt, before.createdAt), + or( + and( + eq(user.createdAt, before.createdAt), + gt(user.id, before.id), + ), + ), + ) + : undefined, + ), + ) + .orderBy(desc(user.createdAt), desc(user.id)) + .limit(21); + + return c.json({ + rows: users.slice(0, 20), + hasNextPage: users.length > 20, + }); + }, +}); diff --git a/server/src/internal/api/apiRouter.ts b/server/src/internal/api/apiRouter.ts index c2cd3c278..ad2e77679 100644 --- a/server/src/internal/api/apiRouter.ts +++ b/server/src/internal/api/apiRouter.ts @@ -8,7 +8,6 @@ import { analyticsRouter } from "../analytics/analyticsRouter.js"; import { attachRouter } from "../customers/attach/attachRouter.js"; import cancelRouter from "../customers/cancel/cancelRouter.js"; import { expressCusRouter } from "../customers/cusRouter.js"; -import { handleGetOrg } from "../orgs/handlers/handleGetOrg.js"; import { platformRouter } from "../platform/platformLegacy/platformRouter.js"; import { expressProductRouter } from "../products/productRouter.js"; import { componentRouter } from "./components/componentRouter.js"; @@ -44,11 +43,6 @@ apiRouter.use("/platform", platformRouter); apiRouter.use("/products", expressProductRouter); apiRouter.use("/customers", expressCusRouter); -// // Used for tests... -// apiRouter.post("/organization/stripe", ...handleConnectStripe); -// apiRouter.delete("/organization/stripe", ...handleDeleteStripe); -apiRouter.get("/organization", handleGetOrg); - export { apiRouter }; // Features diff --git a/server/src/internal/mainRouter.ts b/server/src/internal/mainRouter.ts index 23265ca03..a5eff5fe3 100644 --- a/server/src/internal/mainRouter.ts +++ b/server/src/internal/mainRouter.ts @@ -5,30 +5,26 @@ import { autumnHandler } from "autumn-js/express"; import { Router } from "express"; import rateLimit from "express-rate-limit"; import { createStripeCli } from "@/external/connect/createStripeCli.js"; -import { withAuth, withOrgAuth } from "../middleware/authMiddleware.js"; -import { adminRouter } from "./admin/adminRouter.js"; -import { withAdminAuth } from "./admin/withAdminAuth.js"; +import { withOrgAuth } from "../middleware/authMiddleware.js"; import { analyticsRouter } from "./analytics/internalAnalyticsRouter.js"; import { trmnlRouter } from "./api/trmnl/trmnlRouter.js"; import { cusRouter } from "./customers/internalCusRouter.js"; import { devRouter } from "./dev/devRouter.js"; import { InvoiceService } from "./invoices/InvoiceService.js"; import { onboardingRouter } from "./orgs/onboarding/onboardingRouter.js"; -import { orgRouter } from "./orgs/orgRouter.js"; + import { expressProductRouter } from "./products/internalProductRouter.js"; import { viewsRouter } from "./saved-views/savedViewsRouter.js"; -import { userRouter } from "./users/userRouter.js"; const mainRouter: Router = Router(); -mainRouter.get("", async (req: any, res) => { - res.status(200).json({ message: "Hello World" }); -}); +// mainRouter.get("", async (req: any, res) => { +// res.status(200).json({ message: "Hello World" }); +// }); -mainRouter.use("/admin", withAdminAuth, adminRouter); -mainRouter.use("/users", withAuth, userRouter); +// mainRouter.use("/users", withAuth, userRouter); mainRouter.use("/onboarding", withOrgAuth, onboardingRouter); -mainRouter.use("/organization", withOrgAuth, orgRouter); + mainRouter.use("/products", withOrgAuth, expressProductRouter); mainRouter.use("/dev", devRouter); mainRouter.use("/customers", withOrgAuth, cusRouter); diff --git a/server/src/internal/orgs/handlers/handleDeleteOrg.ts b/server/src/internal/orgs/handlers/crudHandlers/handleDeleteOrg.ts similarity index 85% rename from server/src/internal/orgs/handlers/handleDeleteOrg.ts rename to server/src/internal/orgs/handlers/crudHandlers/handleDeleteOrg.ts index 0093e04d5..a2f50eeb2 100644 --- a/server/src/internal/orgs/handlers/handleDeleteOrg.ts +++ b/server/src/internal/orgs/handlers/crudHandlers/handleDeleteOrg.ts @@ -1,15 +1,14 @@ import { AppEnv, customers, ErrCode, type Organization } from "@autumn/shared"; import { and, eq } from "drizzle-orm"; -import type { Response } from "express"; import { deauthorizeAccount, deleteConnectedAccount, } from "@/external/connect/connectUtils.js"; import type { Logger } from "@/external/logtail/logtailUtils.js"; import { deleteSvixApp } from "@/external/svix/svixHelpers.js"; -import RecaseError, { handleFrontendReqError } from "@/utils/errorUtils.js"; -import type { ExtendedRequest } from "@/utils/models/Request.js"; -import { deleteStripeWebhook } from "../orgUtils.js"; +import RecaseError from "@/utils/errorUtils.js"; +import { createRoute } from "../../../../honoMiddlewares/routeHandler.js"; +import { deleteStripeWebhook } from "../../orgUtils.js"; const deleteSvixWebhooks = async ({ org, @@ -98,9 +97,10 @@ export const deleteStripeAccounts = async ({ } }; -export const handleDeleteOrg = async (req: ExtendedRequest, res: Response) => { - try { - const { org, db, logger } = req; +export const handleDeleteOrg = createRoute({ + handler: async (c) => { + const ctx = c.get("ctx"); + const { org, db, logger } = ctx; // 1. Check if any customers const hasCustomers = await db.query.customers.findFirst({ @@ -134,15 +134,8 @@ export const handleDeleteOrg = async (req: ExtendedRequest, res: Response) => { and(eq(customers.org_id, org.id), eq(customers.env, AppEnv.Sandbox)), ); - res.status(200).json({ + return c.json({ message: "Org deleted", }); - } catch (error) { - handleFrontendReqError({ - res, - error, - req, - action: "delete-org", - }); - } -}; + }, +}); diff --git a/server/src/internal/orgs/handlers/crudHandlers/handleGetOrg.ts b/server/src/internal/orgs/handlers/crudHandlers/handleGetOrg.ts new file mode 100644 index 000000000..a0a69767e --- /dev/null +++ b/server/src/internal/orgs/handlers/crudHandlers/handleGetOrg.ts @@ -0,0 +1,10 @@ +import { createRoute } from "../../../../honoMiddlewares/routeHandler.js"; +import { createOrgResponse } from "../../orgUtils.js"; + +export const handleGetOrg = createRoute({ + handler: async (c) => { + const ctx = c.get("ctx"); + const { org, env } = ctx; + return c.json(createOrgResponse({ org, env })); + }, +}); diff --git a/server/src/internal/orgs/handlers/handleGetInvites.ts b/server/src/internal/orgs/handlers/handleGetInvites.ts deleted file mode 100644 index 958b11a00..000000000 --- a/server/src/internal/orgs/handlers/handleGetInvites.ts +++ /dev/null @@ -1,61 +0,0 @@ -import { invitation, user as userTable } from "@autumn/shared"; -import { and, eq, gt } from "drizzle-orm"; -import type { - ExtendedRequest, - ExtendedResponse, -} from "@/utils/models/Request.js"; - -export const handleGetInvites = async ( - req: ExtendedRequest, - res: ExtendedResponse, -) => { - try { - const { userId, db } = req; - - const user = await db.query.user.findFirst({ - where: eq(userTable.id, userId ?? ""), - }); - - const invites = await db.query.invitation.findMany({ - where: and( - eq(invitation.status, "pending"), - eq(invitation.email, user?.email ?? ""), - gt(invitation.expiresAt, new Date()), - ), - with: { - inviter: true, - organization: true, - }, - }); - - // const joinRequests = await db - // .select({ - // id: orgJoinRequests.id, - // organizationId: orgJoinRequests.organizationId, - // organizationName: organizations.name, - // role: orgJoinRequests.role, - // status: orgJoinRequests.status, - // createdAt: orgJoinRequests.createdAt, - // inviterName: user.name, - // inviterEmail: user.email, - // }) - // .from(orgJoinRequests) - // .innerJoin( - // organizations, - // eq(orgJoinRequests.organizationId, organizations.id) - // ) - // .innerJoin(user, eq(orgJoinRequests.inviterId, user.id)) - // .where( - // and( - // eq(orgJoinRequests.userId, userId), - // eq(orgJoinRequests.status, "pending") - // ) - // ) - // .orderBy(orgJoinRequests.createdAt); - - res.status(200).json({ invites }); - } catch (error) { - console.error("Error fetching join requests:", error); - res.status(500).json({ message: "Internal server error" }); - } -}; diff --git a/server/src/internal/orgs/handlers/handleGetOrg.ts b/server/src/internal/orgs/handlers/handleGetOrg.ts deleted file mode 100644 index 3334d73ae..000000000 --- a/server/src/internal/orgs/handlers/handleGetOrg.ts +++ /dev/null @@ -1,26 +0,0 @@ -import type { - ExtendedRequest, - ExtendedResponse, -} from "@/utils/models/Request.js"; -import { routeHandler } from "@/utils/routerUtils.js"; -import { OrgService } from "../OrgService.js"; -import { createOrgResponse } from "../orgUtils.js"; - -export const handleGetOrg = async (req: any, res: any) => - routeHandler({ - req, - res, - action: "get org", - handler: async (req: ExtendedRequest, res: ExtendedResponse) => { - if (!req.orgId) { - res.status(400).json({ - message: "Missing orgId", - }); - return; - } - - const org = await OrgService.getFromReq(req); - - res.status(200).json(createOrgResponse({ org, env: req.env })); - }, - }); diff --git a/server/src/internal/orgs/handlers/handleGetOrgMembers.ts b/server/src/internal/orgs/handlers/handleGetOrgMembers.ts deleted file mode 100644 index 2c737768e..000000000 --- a/server/src/internal/orgs/handlers/handleGetOrgMembers.ts +++ /dev/null @@ -1,98 +0,0 @@ -import { session as authSession, member } from "@autumn/shared"; -import { and, eq } from "drizzle-orm"; -import { handleFrontendReqError } from "@/utils/errorUtils.js"; -import { OrgService } from "../OrgService.js"; - -export const handleGetOrgMembers = async (req: any, res: any) => { - try { - const { org, db } = req; - const orgId = org.id; - - const memberships = await OrgService.getMembers({ db, orgId }); - const invites = await OrgService.getInvites({ db, orgId }); - - res.status(200).json({ - memberships, - invites, - }); - } catch (error) { - console.error("Error getting org members:", error); - handleFrontendReqError({ - req, - error, - res, - action: "get org members", - }); - } -}; - -export const handleRemoveMember = async (req: any, res: any) => { - try { - const { org, db } = req; - const { memberId, userId } = req.body; - const orgId = org.id; - - // First check if the member exists by memberId - let existingMember = await db.query.member.findFirst({ - where: and(eq(member.id, memberId), eq(member.organizationId, orgId)), - }); - - if (!existingMember) { - // Try to find by userId as fallback - const memberByUserId = await db.query.member.findFirst({ - where: and(eq(member.userId, userId), eq(member.organizationId, orgId)), - }); - - if (!memberByUserId) { - console.warn( - `Member not found by userId either: userId=${userId}, orgId=${orgId}`, - ); - return res.status(404).json({ - message: "Member not found in this organization", - code: "MEMBER_NOT_FOUND", - }); - } - - // Use the member found by userId - existingMember = memberByUserId; - } - - // Remove member from database using the found member - await db - .delete(member) - .where( - and(eq(member.id, existingMember.id), eq(member.organizationId, orgId)), - ); - - // Revoke all sessions for this user in this organization - try { - await db - .delete(authSession) - .where( - and( - eq(authSession.userId, existingMember.userId), - eq(authSession.activeOrganizationId, orgId), - ), - ); - } catch (error) { - // Log but don't fail the request if session revocation fails - req.logger?.warn( - `Failed to revoke sessions for user ${existingMember.userId} in org ${orgId}:`, - error, - ); - console.warn("Session revocation failed:", error); - } - - res.status(200).json({ - message: "Member removed successfully", - }); - } catch (error) { - console.error("Error in handleRemoveMember:", error); - handleFrontendReqError({ - req, - error, - res, - action: "remove member", - }); - } -}; diff --git a/server/src/internal/orgs/handlers/handleGetUploadUrl.ts b/server/src/internal/orgs/handlers/handleGetUploadUrl.ts index cc96bfb54..4a69788d6 100644 --- a/server/src/internal/orgs/handlers/handleGetUploadUrl.ts +++ b/server/src/internal/orgs/handlers/handleGetUploadUrl.ts @@ -1,32 +1,25 @@ import { ErrCode } from "@autumn/shared"; -import { logger } from "@/external/logtail/logtailUtils.js"; import { getUploadUrl } from "@/external/supabase/storageUtils.js"; -import RecaseError, { handleFrontendReqError } from "@/utils/errorUtils.js"; +import RecaseError from "@/utils/errorUtils.js"; +import { createRoute } from "../../../honoMiddlewares/routeHandler"; -export const handleGetUploadUrl = async (req: any, res: any) => { - try { - const { org } = req; +export const handleGetUploadUrl = createRoute({ + handler: async (c) => { + const ctx = c.get("ctx"); + const { org } = ctx; const path = `logo/${org.id}`; if (!process.env.SUPABASE_URL || !process.env.SUPABASE_SERVICE_KEY) { - logger.warn("Supabase storage not set up"); - res.status(400).json({ + throw new RecaseError({ message: "Supabase storage not set up", code: ErrCode.SupabaseNotFound, + statusCode: 400, }); - return; } const data = await getUploadUrl({ path }); - res.status(200).json(data); - } catch (error) { - handleFrontendReqError({ - req, - error, - res, - action: "get upload url", - }); - } -}; + return c.json(data); + }, +}); diff --git a/server/src/internal/orgs/handlers/memberHandlers/handleGetInvites.ts b/server/src/internal/orgs/handlers/memberHandlers/handleGetInvites.ts new file mode 100644 index 000000000..d49efe183 --- /dev/null +++ b/server/src/internal/orgs/handlers/memberHandlers/handleGetInvites.ts @@ -0,0 +1,28 @@ +import { invitation, user as userTable } from "@autumn/shared"; +import { and, eq, gt } from "drizzle-orm"; +import { createRoute } from "../../../../honoMiddlewares/routeHandler"; + +export const handleGetInvites = createRoute({ + handler: async (c) => { + const ctx = c.get("ctx"); + const { userId, db } = ctx; + + const user = await db.query.user.findFirst({ + where: eq(userTable.id, userId ?? ""), + }); + + const invites = await db.query.invitation.findMany({ + where: and( + eq(invitation.status, "pending"), + eq(invitation.email, user?.email ?? ""), + gt(invitation.expiresAt, new Date()), + ), + with: { + inviter: true, + organization: true, + }, + }); + + return c.json({ invites }); + }, +}); diff --git a/server/src/internal/orgs/handlers/memberHandlers/handleGetOrgMembers.ts b/server/src/internal/orgs/handlers/memberHandlers/handleGetOrgMembers.ts new file mode 100644 index 000000000..bbb63b8da --- /dev/null +++ b/server/src/internal/orgs/handlers/memberHandlers/handleGetOrgMembers.ts @@ -0,0 +1,17 @@ +import { createRoute } from "../../../../honoMiddlewares/routeHandler.js"; +import { OrgService } from "../../OrgService.js"; + +export const handleGetOrgMembers = createRoute({ + handler: async (c) => { + const ctx = c.get("ctx"); + const { db, org } = ctx; + + const memberships = await OrgService.getMembers({ db, orgId: org.id }); + const invites = await OrgService.getInvites({ db, orgId: org.id }); + + return c.json({ + memberships, + invites, + }); + }, +}); diff --git a/server/src/internal/orgs/handlers/memberHandlers/handleRemoveMember.ts b/server/src/internal/orgs/handlers/memberHandlers/handleRemoveMember.ts new file mode 100644 index 000000000..aba30c83c --- /dev/null +++ b/server/src/internal/orgs/handlers/memberHandlers/handleRemoveMember.ts @@ -0,0 +1,70 @@ +import { session as authSession, member, RecaseError } from "@autumn/shared"; +import { and, eq } from "drizzle-orm"; +import { z } from "zod/v4"; +import { createRoute } from "../../../../honoMiddlewares/routeHandler"; + +export const handleRemoveMember = createRoute({ + body: z.object({ + memberId: z.string(), + userId: z.string(), + }), + handler: async (c) => { + const ctx = c.get("ctx"); + const { memberId, userId } = c.req.valid("json"); + const { org, db } = ctx; + + // First check if the member exists by memberId + let existingMember = await db.query.member.findFirst({ + where: and(eq(member.id, memberId), eq(member.organizationId, org.id)), + }); + + if (!existingMember) { + // Try to find by userId as fallback + const memberByUserId = await db.query.member.findFirst({ + where: and( + eq(member.userId, userId), + eq(member.organizationId, org.id), + ), + }); + + if (!memberByUserId) { + throw new RecaseError({ + message: "Member not found in this organization", + code: "MEMBER_NOT_FOUND", + statusCode: 404, + }); + } + + // Use the member found by userId + existingMember = memberByUserId; + } + + // Remove member from database using the found member + await db + .delete(member) + .where( + and( + eq(member.id, existingMember.id), + eq(member.organizationId, org.id), + ), + ); + + // Revoke all sessions for this user in this organization + try { + await db + .delete(authSession) + .where( + and( + eq(authSession.userId, existingMember.userId), + eq(authSession.activeOrganizationId, org.id), + ), + ); + } catch (error) { + console.warn(`Session revocation failed: ${error}`); + } + + return c.json({ + message: "Member removed successfully", + }); + }, +}); diff --git a/server/src/internal/orgs/orgRouter.ts b/server/src/internal/orgs/orgRouter.ts index 8e141b8cd..f180b9a3e 100644 --- a/server/src/internal/orgs/orgRouter.ts +++ b/server/src/internal/orgs/orgRouter.ts @@ -1,13 +1,7 @@ -import express, { type Router } from "express"; import { Hono } from "hono"; import type { HonoEnv } from "@/honoUtils/HonoEnv.js"; -import { handleDeleteOrg } from "./handlers/handleDeleteOrg.js"; -import { handleGetInvites } from "./handlers/handleGetInvites.js"; -import { handleGetOrg } from "./handlers/handleGetOrg.js"; -import { - handleGetOrgMembers, - handleRemoveMember, -} from "./handlers/handleGetOrgMembers.js"; +import { handleDeleteOrg } from "./handlers/crudHandlers/handleDeleteOrg.js"; +import { handleGetOrg } from "./handlers/crudHandlers/handleGetOrg.js"; import { handleGetUploadUrl } from "./handlers/handleGetUploadUrl.js"; import { handleResetDefaultAccount } from "./handlers/handleResetDefaultAccount.js"; import { handleUpdateOrg } from "./handlers/handleUpdateOrg.js"; @@ -15,30 +9,25 @@ import { handleGetVercelSink, handleUpsertVercelConfig, } from "./handlers/handleVercelConfig.js"; +import { handleGetInvites } from "./handlers/memberHandlers/handleGetInvites.js"; +import { handleGetOrgMembers } from "./handlers/memberHandlers/handleGetOrgMembers.js"; +import { handleRemoveMember } from "./handlers/memberHandlers/handleRemoveMember.js"; import { handleConnectStripe } from "./handlers/stripeHandlers/handleConnectStripe.js"; import { handleDeleteStripe } from "./handlers/stripeHandlers/handleDeleteStripe.js"; import { handleGetOAuthUrl } from "./handlers/stripeHandlers/handleGetOAuthUrl.js"; import { handleGetStripeAccount } from "./handlers/stripeHandlers/handleGetStripeAccount.js"; -export const orgRouter: Router = express.Router(); -orgRouter.get("/members", handleGetOrgMembers); -orgRouter.post("/remove-member", handleRemoveMember); -orgRouter.get("/upload_url", handleGetUploadUrl); -orgRouter.get("/invites", handleGetInvites as any); -orgRouter.delete("", handleDeleteOrg as any); +export const internalOrgRouter = new Hono(); -orgRouter.delete("/delete-user", async (req: any, res) => { - res.status(200).json({ - message: "User deleted", - }); -}); - -orgRouter.get("", handleGetOrg); - -// orgRouter.post("/stripe", handleConnectStripe); +internalOrgRouter.get("", ...handleGetOrg); +internalOrgRouter.delete("", ...handleDeleteOrg); +internalOrgRouter.get("/members", ...handleGetOrgMembers); +internalOrgRouter.post("/remove-member", ...handleRemoveMember); +internalOrgRouter.get("/upload_url", ...handleGetUploadUrl); +internalOrgRouter.get("/invites", ...handleGetInvites); export const honoOrgRouter = new Hono(); - +honoOrgRouter.get("", ...handleGetOrg); honoOrgRouter.patch("", ...handleUpdateOrg); honoOrgRouter.get("/stripe", ...handleGetStripeAccount); honoOrgRouter.delete("/stripe", ...handleDeleteStripe); diff --git a/server/src/routers/internalRouter.ts b/server/src/routers/internalRouter.ts new file mode 100644 index 000000000..0d78cc65b --- /dev/null +++ b/server/src/routers/internalRouter.ts @@ -0,0 +1,30 @@ +import { Hono } from "hono"; +import { adminAuthMiddleware } from "../honoMiddlewares/adminAuthMiddleware"; +import { analyticsMiddleware } from "../honoMiddlewares/analyticsMiddleware"; +import { apiVersionMiddleware } from "../honoMiddlewares/apiVersionMiddleware"; +import { betterAuthMiddleware } from "../honoMiddlewares/betterAuthMiddleware"; +import { orgConfigMiddleware } from "../honoMiddlewares/orgConfigMiddleware"; +import { queryMiddleware } from "../honoMiddlewares/queryMiddleware"; +import { refreshCacheMiddleware } from "../honoMiddlewares/refreshCacheMiddleware"; +import type { HonoEnv } from "../honoUtils/HonoEnv"; +import { honoAdminRouter } from "../internal/admin/adminRouter"; +import { internalCusRouter } from "../internal/customers/internalCusRouter"; +import { internalOrgRouter } from "../internal/orgs/orgRouter"; +import { internalProductRouter } from "../internal/products/internalProductRouter"; + +export const internalRouter = new Hono(); + +// Internal/dashboard routes - use betterAuthMiddleware for session auth +internalRouter.use("*", betterAuthMiddleware); +internalRouter.use("*", orgConfigMiddleware); +internalRouter.use("*", apiVersionMiddleware); +internalRouter.use("*", analyticsMiddleware); +internalRouter.use("*", refreshCacheMiddleware); +internalRouter.use("*", queryMiddleware()); + +internalRouter.use("/admin/*", adminAuthMiddleware); +internalRouter.route("admin", honoAdminRouter); + +internalRouter.route("organization", internalOrgRouter); +internalRouter.route("/products", internalProductRouter); +internalRouter.route("/customers", internalCusRouter);