stripe controller

This commit is contained in:
Alex Holliday committed 2025-11-30 16:52:48 +00:00
1 parent f476a57748
commit 6eabaebd86
10 files changed
+125 -10

No files matched your search

+2
View File
@@ -13,6 +13,7 @@ export interface IEnvConfig {
SMTP_PORT: number;
SMTP_USER: string;
SMTP_PASS: string;
STRIPE_SECRET: string;
}
dotenv.config();
@@ -30,4 +31,5 @@ export const config: IEnvConfig = {
SMTP_PORT: process.env.SMTP_PORT ? parseInt(process.env.SMTP_PORT, 10) : -1,
SMTP_USER: process.env.SMTP_USER || "not_set",
SMTP_PASS: process.env.SMTP_PASS || "not_set",
STRIPE_SECRET: process.env.STRIPE_SECRET || "not_set",
};
+3 -2
View File
@@ -400,7 +400,7 @@ class MonitorController {
throw new ApiError("Entitlements not found", 500);
}
const result: any = await this.monitorService.import(
const result = await this.monitorService.import(
orgId,
teamId,
userContext.sub,
@@ -414,7 +414,8 @@ class MonitorController {
UNEXPECTED: `Import failed unexpectedly`,
};
const message = result?.message || defaultMessageMap[result?.code] || "Import completed";
const message =
result?.message || defaultMessageMap[result?.code] || "Import completed";
res.status(result?.status ?? 200).json({ message, data: result });
try {
@@ -0,0 +1,50 @@
import type { Request, Response, NextFunction } from "express";
export interface IStripeController {
webhook: (req: Request, res: Response, next: NextFunction) => Promise<void>;
}
class StripeController implements IStripeController {
webhook = async (req: Request, res: Response, next: NextFunction) => {
try {
const sig = req.headers["stripe-signature"] as string | undefined;
const raw = req.body as unknown; // Buffer when express.raw is used
let event: any = undefined;
if (Buffer.isBuffer(raw)) {
const json = raw.toString("utf8");
try {
event = JSON.parse(json);
} catch (_e) {
res.status(401).json({ message: "Unauthorized" });
}
} else if (typeof raw === "string") {
try {
event = JSON.parse(raw);
} catch (_e) {
res.status(400).json({ message: "Invalid JSON payload" });
}
} else if (raw && typeof raw === "object") {
event = raw;
}
// Basic routing by event type (no-ops for now)
const type = event?.type;
switch (type) {
case "checkout.session.completed":
case "customer.subscription.updated":
case "customer.subscription.deleted":
case "invoice.payment_succeeded":
case "invoice.payment_failed":
default:
break;
}
res.status(200);
} catch (error) {
throw error;
}
};
}
export default StripeController;
+1
View File
@@ -14,3 +14,4 @@ export { default as RecoveryController } from "./RecoveryController.js";
export { default as ProfileController } from "./ProfileController.js";
export { default as IncidentsController } from "./IncidentsController.js";
export { default as SettingsController } from "./SettingsController.js";
export { default as StripeController } from "./StripeController.js";
+7
View File
@@ -18,6 +18,13 @@ const createApp = async () => {
const services = await initServices();
const controllers = initControllers(services);
const app = express();
// Stripe webhook requires raw body for signature verification; mount it first
app.post(
"/api/v1/stripe/webhook",
express.raw({ type: "application/json" }) as any,
// Defer to router handler registered later; this preserves raw body
(req, res, next) => next()
);
app.use(express.json());
app.use(cookieParser());
app.use(
+3
View File
@@ -15,6 +15,7 @@ import {
ProfileController,
IncidentsController,
SettingsController,
StripeController,
} from "@/controllers/index.js";
export const initControllers = (services: any) => {
@@ -74,5 +75,7 @@ export const initControllers = (services: any) => {
services.notificationService
);
controllers.stripeController = new StripeController();
return controllers;
};
+5
View File
@@ -18,6 +18,8 @@ import {
SettingsRoutes,
MeRoutes,
} from "@/routes/index.js";
import StripeRoutes from "@/routes/stripe.js";
import StripeController from "@/controllers/StripeController.js";
import { errorHandler } from "@/middleware/ErrorHandler.js";
export const initRoutes = (controllers: any, app: Express) => {
const authRoutes = new AuthRoutes(controllers.authController);
@@ -48,6 +50,7 @@ export const initRoutes = (controllers: any, app: Express) => {
const incidentsRoutes = new IncidentsRoutes(controllers.incidentsController);
const settingsRoutes = new SettingsRoutes(controllers.settingsController);
const meRoutes = new MeRoutes();
const stripeRoutes = new StripeRoutes(controllers.stripeController);
app.use("/api/v1/auth", authRoutes.getRouter());
app.use("/api/v1/invite", inviteRoutes.getRouter());
@@ -69,5 +72,7 @@ export const initRoutes = (controllers: any, app: Express) => {
app.use("/api/v1/incidents", incidentsRoutes.getRouter());
app.use("/api/v1/settings", settingsRoutes.getRouter());
app.use("/api/v1/me", meRoutes.getRouter());
// Stripe webhook uses raw body; mounted separately in index.ts
app.use("/api/v1/stripe", stripeRoutes.getRouter());
app.use(errorHandler);
};
+3
View File
@@ -66,6 +66,9 @@ class MonitorRoutes {
verifyToken,
addUserContext,
verifyTeamPermission([PERMISSIONS.monitors.write]),
enforceMax("monitorsMax", async (req) =>
Monitor.countDocuments({ orgId: req?.user?.orgId })
),
validateBody(monitorImportSchema),
this.controller.import
);
+24
View File
@@ -0,0 +1,24 @@
import { Router } from "express";
import StripeController from "@/controllers/StripeController.js";
// Note: webhook route must be mounted with express.raw at app/init level.
class StripeRoutes {
private router;
private controller;
constructor(stripeController: StripeController) {
this.router = Router();
this.controller = stripeController;
this.initRoutes();
}
initRoutes = () => {
this.router.post("/webhook", this.controller.webhook);
};
getRouter() {
return this.router;
}
}
export default StripeRoutes;
+27 -8
View File
@@ -15,6 +15,24 @@ export interface IImportedMonitor {
n: number;
}
export enum MonitorImportCode {
OK = "OK",
PARTIAL = "PARTIAL",
NO_CAPACITY = "NO_CAPACITY",
UNEXPECTED = "UNEXPECTED",
}
export interface MonitorImportResult {
ok: boolean;
status: number;
code: MonitorImportCode;
attempted: number;
eligible: number;
imported: number;
errors: number;
message?: string;
}
const SERVICE_NAME = "MonitorService";
export interface IMonitorService {
@@ -68,7 +86,7 @@ export interface IMonitorService {
userId: string,
entitlements: Entitlements,
data: { monitors: Array<IImportedMonitor> }
) => Promise<{ imported: number; errors: number }>;
) => Promise<MonitorImportResult>;
}
class MonitorService implements IMonitorService {
@@ -691,7 +709,7 @@ class MonitorService implements IMonitorService {
entitlements: Entitlements,
data: { monitors: Array<IImportedMonitor> }
) => {
const attempted = Array.isArray(data?.monitors) ? data.monitors.length : 0;
const attempted = data.monitors.length ?? 0;
let result: IMonitor[] = [];
let canImport = 0;
try {
@@ -703,12 +721,12 @@ class MonitorService implements IMonitorService {
return {
ok: true,
status: 200,
code: "NO_CAPACITY",
attempted,
code: MonitorImportCode.NO_CAPACITY,
eligible: 0,
imported: 0,
errors: attempted,
} as any;
};
}
// Proceed with import
@@ -732,18 +750,19 @@ class MonitorService implements IMonitorService {
return {
ok: false,
status: 500,
code: "UNEXPECTED",
code: MonitorImportCode.UNEXPECTED,
attempted,
eligible: canImport,
imported: result.length,
errors: Math.max(0, attempted - result.length),
message: error.message,
} as any;
};
}
const imported = result.length;
const errors = Math.max(0, attempted - imported);
const code = imported === attempted ? "OK" : "PARTIAL";
const code =
imported === attempted ? MonitorImportCode.OK : MonitorImportCode.PARTIAL;
const status = imported === attempted ? 200 : 207;
return {
ok: true,
@@ -753,7 +772,7 @@ class MonitorService implements IMonitorService {
eligible: canImport,
imported,
errors,
} as any;
};
};
}