mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-07-26 09:52:11 +03:00
feat(api): add /api/quota/pools CRUD + usage routes (B/F8)
This commit is contained in:
99
src/app/api/quota/pools/[id]/route.ts
Normal file
99
src/app/api/quota/pools/[id]/route.ts
Normal file
@@ -0,0 +1,99 @@
|
||||
/**
|
||||
* GET /api/quota/pools/[id] — get a single quota pool
|
||||
* PATCH /api/quota/pools/[id] — update pool name/allocations
|
||||
* DELETE /api/quota/pools/[id] — delete pool
|
||||
*
|
||||
* Auth: requireManagementAuth
|
||||
* Zod: PoolUpdateSchema from @/shared/schemas/quota (PATCH only)
|
||||
* Audit: quota.pool.updated / quota.pool.deleted (B26)
|
||||
* Sanitization: all error responses via buildErrorBody (Hard Rule #12, B25)
|
||||
*
|
||||
* Part of: Group B — REST routes for Quota Sharing (plan 22, frente F8).
|
||||
*/
|
||||
|
||||
import { NextResponse } from "next/server";
|
||||
import { buildErrorBody } from "@omniroute/open-sse/utils/error";
|
||||
import { requireManagementAuth } from "@/lib/api/requireManagementAuth";
|
||||
import { PoolUpdateSchema } from "@/shared/schemas/quota";
|
||||
import { getPool, updatePool, deletePool } from "@/lib/localDb";
|
||||
import { logAuditEvent, getAuditRequestContext } from "@/lib/compliance/index";
|
||||
|
||||
export const dynamic = "force-dynamic";
|
||||
|
||||
type RouteParams = { params: Promise<{ id: string }> };
|
||||
|
||||
export async function GET(request: Request, { params }: RouteParams): Promise<Response> {
|
||||
const authError = await requireManagementAuth(request);
|
||||
if (authError) return authError;
|
||||
|
||||
try {
|
||||
const { id } = await params;
|
||||
const pool = getPool(id);
|
||||
if (!pool) {
|
||||
return NextResponse.json(buildErrorBody(404, "Pool not found"), { status: 404 });
|
||||
}
|
||||
return NextResponse.json({ pool });
|
||||
} catch (err) {
|
||||
const message = err instanceof Error ? err.message : "Failed to get pool";
|
||||
return NextResponse.json(buildErrorBody(500, message), { status: 500 });
|
||||
}
|
||||
}
|
||||
|
||||
export async function PATCH(request: Request, { params }: RouteParams): Promise<Response> {
|
||||
const authError = await requireManagementAuth(request);
|
||||
if (authError) return authError;
|
||||
|
||||
try {
|
||||
const { id } = await params;
|
||||
const body = await request.json().catch(() => null);
|
||||
const parsed = PoolUpdateSchema.safeParse(body);
|
||||
if (!parsed.success) {
|
||||
return NextResponse.json(buildErrorBody(400, parsed.error.message), { status: 400 });
|
||||
}
|
||||
|
||||
const pool = updatePool(id, parsed.data);
|
||||
if (!pool) {
|
||||
return NextResponse.json(buildErrorBody(404, "Pool not found"), { status: 404 });
|
||||
}
|
||||
|
||||
const ctx = getAuditRequestContext(request);
|
||||
logAuditEvent({
|
||||
action: "quota.pool.updated",
|
||||
target: id,
|
||||
metadata: parsed.data,
|
||||
ipAddress: ctx.ipAddress ?? undefined,
|
||||
requestId: ctx.requestId,
|
||||
});
|
||||
|
||||
return NextResponse.json({ pool });
|
||||
} catch (err) {
|
||||
const message = err instanceof Error ? err.message : "Failed to update pool";
|
||||
return NextResponse.json(buildErrorBody(500, message), { status: 500 });
|
||||
}
|
||||
}
|
||||
|
||||
export async function DELETE(request: Request, { params }: RouteParams): Promise<Response> {
|
||||
const authError = await requireManagementAuth(request);
|
||||
if (authError) return authError;
|
||||
|
||||
try {
|
||||
const { id } = await params;
|
||||
const existed = deletePool(id);
|
||||
if (!existed) {
|
||||
return NextResponse.json(buildErrorBody(404, "Pool not found"), { status: 404 });
|
||||
}
|
||||
|
||||
const ctx = getAuditRequestContext(request);
|
||||
logAuditEvent({
|
||||
action: "quota.pool.deleted",
|
||||
target: id,
|
||||
ipAddress: ctx.ipAddress ?? undefined,
|
||||
requestId: ctx.requestId,
|
||||
});
|
||||
|
||||
return new Response(null, { status: 204 });
|
||||
} catch (err) {
|
||||
const message = err instanceof Error ? err.message : "Failed to delete pool";
|
||||
return NextResponse.json(buildErrorBody(500, message), { status: 500 });
|
||||
}
|
||||
}
|
||||
80
src/app/api/quota/pools/[id]/usage/route.ts
Normal file
80
src/app/api/quota/pools/[id]/usage/route.ts
Normal file
@@ -0,0 +1,80 @@
|
||||
/**
|
||||
* GET /api/quota/pools/[id]/usage — pool consumption snapshot with dimensions
|
||||
*
|
||||
* Resolves the pool's provider plan to get dimensions, then calls
|
||||
* poolUsageWithDimensions on the concrete store implementation.
|
||||
*
|
||||
* Note on poolUsageWithDimensions availability:
|
||||
* This method is defined on SqliteQuotaStore (and RedisQuotaStore) but is NOT
|
||||
* part of the QuotaStore interface (keeping the interface minimal). F8 accesses
|
||||
* it via dynamic type-narrowing:
|
||||
*
|
||||
* const storeExt = store as { poolUsageWithDimensions?: (...) => Promise<...> };
|
||||
* if (typeof storeExt.poolUsageWithDimensions === "function") { ... }
|
||||
* else { fallback to store.poolUsage(id) }
|
||||
*
|
||||
* This avoids modifying the QuotaStore interface (F6 responsibility) while
|
||||
* still using the richer method when available.
|
||||
*
|
||||
* Auth: requireManagementAuth
|
||||
* Sanitization: all error responses via buildErrorBody (Hard Rule #12, B25)
|
||||
*
|
||||
* Part of: Group B — REST routes for Quota Sharing (plan 22, frente F8).
|
||||
*/
|
||||
|
||||
import { NextResponse } from "next/server";
|
||||
import { buildErrorBody } from "@omniroute/open-sse/utils/error";
|
||||
import { requireManagementAuth } from "@/lib/api/requireManagementAuth";
|
||||
import { getPool } from "@/lib/localDb";
|
||||
import { getQuotaStore } from "@/lib/quota/QuotaStore";
|
||||
import { resolvePlan } from "@/lib/quota/planResolver";
|
||||
import type { PoolUsageSnapshot } from "@/lib/quota/types";
|
||||
|
||||
export const dynamic = "force-dynamic";
|
||||
|
||||
type RouteParams = { params: Promise<{ id: string }> };
|
||||
|
||||
export async function GET(request: Request, { params }: RouteParams): Promise<Response> {
|
||||
const authError = await requireManagementAuth(request);
|
||||
if (authError) return authError;
|
||||
|
||||
try {
|
||||
const { id } = await params;
|
||||
|
||||
// 1. Get pool — 404 if not found
|
||||
const pool = getPool(id);
|
||||
if (!pool) {
|
||||
return NextResponse.json(buildErrorBody(404, "Pool not found"), { status: 404 });
|
||||
}
|
||||
|
||||
// 2. Resolve the provider plan for this pool's connection
|
||||
// Provider name is not stored on pool — use empty string to trigger catalog/empty fallback
|
||||
const plan = resolvePlan(pool.connectionId, "");
|
||||
|
||||
// 3. Get the quota store and call poolUsageWithDimensions when available
|
||||
const store = await getQuotaStore();
|
||||
|
||||
let snapshot: PoolUsageSnapshot;
|
||||
const storeExt = store as unknown as {
|
||||
poolUsageWithDimensions?: (
|
||||
poolId: string,
|
||||
dimensions: Array<{ unit: string; window: string; limit: number }>
|
||||
) => Promise<PoolUsageSnapshot>;
|
||||
};
|
||||
|
||||
if (
|
||||
typeof storeExt.poolUsageWithDimensions === "function" &&
|
||||
plan.dimensions.length > 0
|
||||
) {
|
||||
snapshot = await storeExt.poolUsageWithDimensions(id, plan.dimensions);
|
||||
} else {
|
||||
// Fallback: use the interface-standard poolUsage (dimensions come from stored data only)
|
||||
snapshot = await store.poolUsage(id);
|
||||
}
|
||||
|
||||
return NextResponse.json({ usage: snapshot });
|
||||
} catch (err) {
|
||||
const message = err instanceof Error ? err.message : "Failed to get pool usage";
|
||||
return NextResponse.json(buildErrorBody(500, message), { status: 500 });
|
||||
}
|
||||
}
|
||||
63
src/app/api/quota/pools/route.ts
Normal file
63
src/app/api/quota/pools/route.ts
Normal file
@@ -0,0 +1,63 @@
|
||||
/**
|
||||
* GET /api/quota/pools — list all quota pools with allocations
|
||||
* POST /api/quota/pools — create a new quota pool
|
||||
*
|
||||
* Auth: requireManagementAuth (same pattern as /api/compliance/audit-log)
|
||||
* Zod: PoolCreateSchema from @/shared/schemas/quota
|
||||
* Audit: quota.pool.created logged on POST (B26)
|
||||
* Sanitization: all error responses via buildErrorBody (Hard Rule #12, B25)
|
||||
*
|
||||
* NOT LOCAL_ONLY — does not spawn processes (B18, Hard Rules #15/#17 do not apply).
|
||||
*
|
||||
* Part of: Group B — REST routes for Quota Sharing (plan 22, frente F8).
|
||||
*/
|
||||
|
||||
import { NextResponse } from "next/server";
|
||||
import { buildErrorBody } from "@omniroute/open-sse/utils/error";
|
||||
import { requireManagementAuth } from "@/lib/api/requireManagementAuth";
|
||||
import { PoolCreateSchema } from "@/shared/schemas/quota";
|
||||
import { listPools, createPool } from "@/lib/localDb";
|
||||
import { logAuditEvent, getAuditRequestContext } from "@/lib/compliance/index";
|
||||
|
||||
export const dynamic = "force-dynamic";
|
||||
|
||||
export async function GET(request: Request): Promise<Response> {
|
||||
const authError = await requireManagementAuth(request);
|
||||
if (authError) return authError;
|
||||
|
||||
try {
|
||||
const pools = listPools();
|
||||
return NextResponse.json({ pools });
|
||||
} catch (err) {
|
||||
const message = err instanceof Error ? err.message : "Failed to list pools";
|
||||
return NextResponse.json(buildErrorBody(500, message), { status: 500 });
|
||||
}
|
||||
}
|
||||
|
||||
export async function POST(request: Request): Promise<Response> {
|
||||
const authError = await requireManagementAuth(request);
|
||||
if (authError) return authError;
|
||||
|
||||
try {
|
||||
const body = await request.json().catch(() => null);
|
||||
const parsed = PoolCreateSchema.safeParse(body);
|
||||
if (!parsed.success) {
|
||||
return NextResponse.json(buildErrorBody(400, parsed.error.message), { status: 400 });
|
||||
}
|
||||
|
||||
const pool = createPool(parsed.data);
|
||||
const ctx = getAuditRequestContext(request);
|
||||
logAuditEvent({
|
||||
action: "quota.pool.created",
|
||||
target: pool.id,
|
||||
metadata: { connectionId: pool.connectionId, name: pool.name },
|
||||
ipAddress: ctx.ipAddress ?? undefined,
|
||||
requestId: ctx.requestId,
|
||||
});
|
||||
|
||||
return NextResponse.json({ pool }, { status: 201 });
|
||||
} catch (err) {
|
||||
const message = err instanceof Error ? err.message : "Failed to create pool";
|
||||
return NextResponse.json(buildErrorBody(500, message), { status: 500 });
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user