Openstatus www.openstatus.dev
6
fork

Configure Feed

Select the types of activity you want to include in your feed.

feat: include api limits (#354)

authored by

Maximilian Kaske and committed by
GitHub
509cba36 b666f002

+73 -16
+1
apps/server/package.json
··· 12 12 "@hono/zod-openapi": "0.4.0", 13 13 "zod": "3.21.4", 14 14 "@openstatus/db": "workspace:*", 15 + "@openstatus/plans": "workspace:*", 15 16 "@openstatus/tinybird": "workspace:*", 16 17 "@openstatus/upstash": "workspace:*", 17 18 "@unkey/api": "0.6.22",
+3
apps/server/src/v1/index.ts
··· 1 1 import { OpenAPIHono } from "@hono/zod-openapi"; 2 2 3 + import type { Plan } from "@openstatus/plans"; 4 + 3 5 import { incidentApi } from "./incident"; 4 6 import { middleware } from "./middleware"; 5 7 import { monitorApi } from "./monitor"; 6 8 7 9 export type Variables = { 8 10 workspaceId: string; 11 + workspacePlan: Plan; 9 12 }; 10 13 11 14 export const api = new OpenAPIHono<{ Variables: Variables }>();
+17 -11
apps/server/src/v1/middleware.ts
··· 1 1 import { verifyKey } from "@unkey/api"; 2 2 import type { Context, Env, Next } from "hono"; 3 3 4 + import type { Variables } from "./index"; 5 + import { getLimitByWorkspaceId } from "./utils"; 6 + 4 7 export async function middleware( 5 - c: Context< 6 - { 7 - Variables: { 8 - workspaceId: string; 9 - }; 10 - }, 11 - "/*", 12 - {} 13 - >, 8 + c: Context<{ Variables: Variables }, "/*", {}>, 14 9 next: Next, 15 10 ) { 16 11 const key = c.req.header("x-openstatus-key"); ··· 19 14 if (process.env.NODE_ENV === "production") { 20 15 const { error, result } = await verifyKey(key); 21 16 22 - if (error) return c.text("Bad Request", 400); 17 + if (error) return c.text("Internal Server Error", 500); 23 18 24 19 if (!result.valid) return c.text("Unauthorized", 401); 20 + 21 + if (!result.ownerId) return c.text("Unauthorized", 401); 22 + 23 + const plan = await getLimitByWorkspaceId(parseInt(result.ownerId)); 24 + 25 + c.set("workspacePlan", plan); 25 26 c.set("workspaceId", `${result.ownerId}`); 26 27 } else { 27 28 // REMINDER: localhost only 28 - c.set("workspaceId", "1"); 29 + const ownerId = 1; 30 + 31 + const plan = await getLimitByWorkspaceId(ownerId); 32 + 33 + c.set("workspacePlan", plan); 34 + c.set("workspaceId", `${ownerId}`); 29 35 } 30 36 await next(); 31 37 }
+19 -3
apps/server/src/v1/monitor.ts
··· 1 1 import { createRoute, OpenAPIHono, z } from "@hono/zod-openapi"; 2 2 3 - import { db, eq } from "@openstatus/db"; 3 + import { db, eq, sql } from "@openstatus/db"; 4 4 import { 5 5 availableRegions, 6 6 METHODS, ··· 262 262 schema: MonitorSchema, 263 263 }, 264 264 }, 265 - description: "Create a monitor", 265 + description: "Create a monitor", 266 266 }, 267 267 400: { 268 268 content: { ··· 277 277 278 278 monitorApi.openapi(postRoute, async (c) => { 279 279 const workspaceId = Number(c.get("workspaceId")); 280 + const workspacePlan = c.get("workspacePlan"); 280 281 console.log({ workspaceId }); 281 282 const input = c.req.valid("json"); 283 + 284 + const count = ( 285 + await db.select({ count: sql<number>`count(*)` }).from(monitor) 286 + )[0].count; 287 + 288 + if (count >= workspacePlan.limits.monitors) 289 + return c.jsonT({ code: 403, message: "Forbidden" }); 290 + 291 + if (!workspacePlan.limits.periodicity.includes(input.periodicity)) 292 + return c.jsonT({ code: 403, message: "Forbidden" }); 293 + 282 294 const { headers, ...rest } = input; 283 295 const _newMonitor = await db 284 296 .insert(monitor) ··· 298 310 const putRoute = createRoute({ 299 311 method: "put", 300 312 tags: ["monitor"], 301 - path: "/", 313 + path: "/:id", 302 314 request: { 303 315 params: ParamsSchema, 304 316 body: { ··· 333 345 monitorApi.openapi(putRoute, async (c) => { 334 346 const input = c.req.valid("json"); 335 347 const workspaceId = Number(c.get("workspaceId")); 348 + const workspacePlan = c.get("workspacePlan"); 336 349 const { id } = c.req.valid("param"); 337 350 338 351 if (!id) return c.jsonT({ code: 400, message: "Bad Request" }); 352 + 353 + if (!workspacePlan.limits.periodicity.includes(input.periodicity)) 354 + return c.jsonT({ code: 403, message: "Forbidden" }); 339 355 340 356 const _monitor = await db 341 357 .select()
+27
apps/server/src/v1/utils.ts
··· 1 + import { db, eq } from "@openstatus/db"; 2 + import { selectWorkspaceSchema, workspace } from "@openstatus/db/src/schema"; 3 + import { allPlans } from "@openstatus/plans"; 4 + 5 + /** 6 + * TODO: move the plan limit into the Unkey `{ meta }` to avoid an additional db call. 7 + * When an API Key is created, we need to include the `{ meta: { plan: "free" } }` to the key. 8 + * Then, we can just read the plan from the key and use it in the middleware. 9 + * Don't forget to update the key whenever a user changes their plan. (via `stripeRoute` webhook) 10 + * 11 + * That remindes me we need to downgrade the frequency/periodicity of monitors to 10m if the user downgrades their plan. 12 + */ 13 + 14 + export async function getWorkspace(id: number) { 15 + const _workspace = await db 16 + .select() 17 + .from(workspace) 18 + .where(eq(workspace.id, id)) 19 + .get(); 20 + 21 + return selectWorkspaceSchema.parse(_workspace); 22 + } 23 + 24 + export async function getLimitByWorkspaceId(id: number) { 25 + const { plan } = await getWorkspace(id); 26 + return allPlans[plan]; 27 + }
bun.lockb

This is a binary file and will not be displayed.

+2 -1
package.json
··· 25 25 "apps/*", 26 26 "packages/*", 27 27 "packages/config/*", 28 - "packages/integrations/*" 28 + "packages/integrations/*", 29 + "packages/notifications/*" 29 30 ] 30 31 }
+1 -1
packages/plans/index.ts
··· 2 2 3 3 import type { periodicityEnum } from "@openstatus/db/src/schema"; 4 4 5 - type Plan = { 5 + export type Plan = { 6 6 limits: { 7 7 monitors: number; 8 8 "status-pages": number;
+3
pnpm-lock.yaml
··· 81 81 '@openstatus/db': 82 82 specifier: workspace:* 83 83 version: link:../../packages/db 84 + '@openstatus/plans': 85 + specifier: workspace:* 86 + version: link:../../packages/plans 84 87 '@openstatus/tinybird': 85 88 specifier: workspace:* 86 89 version: link:../../packages/tinybird