diff --git a/src/middleware/api-key-scope.ts b/src/middleware/api-key-scope.ts index fc02e16..3bee2de 100644 --- a/src/middleware/api-key-scope.ts +++ b/src/middleware/api-key-scope.ts @@ -1,12 +1,19 @@ import type { ApiKeyScope } from "../modules/api-keys/api-keys.schema.js"; import { ForbiddenError } from "../utils/errors.js"; -function routeScope(method: string, routePath: string): ApiKeyScope | "admin:all" { +function routeScope(method: string, routePath: string): ApiKeyScope | "admin:all" | null { + // null = any authenticated API key may call this endpoint (no additional scope needed) + if (routePath === "/auth/me") return null; + if (method === "GET" && routePath.startsWith("/admin/employees")) return "read:employees"; if (method === "GET" && (routePath.startsWith("/admin/sessions") || routePath === "/attendance/my-sessions")) { return "read:sessions"; } - if ((method === "POST" && routePath === "/expenses") || (method === "PATCH" && routePath.startsWith("/admin/expenses/"))) { + if ( + (method === "POST" && routePath === "/expenses") || + (method === "GET" && routePath === "/expenses/my") || + (method === "PATCH" && routePath.startsWith("/admin/expenses/")) + ) { return "write:expenses"; } return "admin:all"; @@ -19,6 +26,7 @@ export function hasApiKeyScope(scopes: ApiKeyScope[], required: ApiKeyScope | "a export function enforceApiKeyScope(method: string, routePath: string, scopes: ApiKeyScope[]): void { const required = routeScope(method.toUpperCase(), routePath); + if (required === null) return; // endpoint accessible to any authenticated API key if (!hasApiKeyScope(scopes, required)) { throw new ForbiddenError(`API key missing required scope: ${required}`); } diff --git a/src/modules/admin/monitoring.repository.ts b/src/modules/admin/monitoring.repository.ts index 9cda4de..b8a236a 100644 --- a/src/modules/admin/monitoring.repository.ts +++ b/src/modules/admin/monitoring.repository.ts @@ -93,14 +93,16 @@ export const monitoringRepository = { page: number, limit: number, ): Promise<{ data: AdminSession[]; total: number }> { - const { data, error, count } = await applyPagination( - orgTable(request, "admin_sessions") - .select(MONITORING_COLS, { count: "exact" }) - .eq("admin_id", request.user.sub) - .order("started_at", { ascending: false }), - page, - limit, - ); + // When auth is via API key, request.user.sub is "api_key:" — not a real + // users.id UUID. Filtering by admin_id would cause a DB type error. Instead, + // return all org sessions (API key callers have org-level access, not user-level). + let baseQuery = orgTable(request, "admin_sessions") + .select(MONITORING_COLS, { count: "exact" }) + .order("started_at", { ascending: false }); + if (request.authType !== "api_key") { + baseQuery = baseQuery.eq("admin_id", request.user.sub); + } + const { data, error, count } = await applyPagination(baseQuery, page, limit); if (error) { throw new Error(`Failed to fetch monitoring history: ${error.message}`); diff --git a/src/modules/analytics/analytics.controller.ts b/src/modules/analytics/analytics.controller.ts index 925fc86..7176e68 100644 --- a/src/modules/analytics/analytics.controller.ts +++ b/src/modules/analytics/analytics.controller.ts @@ -8,6 +8,7 @@ import type { LeaderboardQuery, } from "./analytics.schema.js"; import { ok, handleError } from "../../utils/response.js"; +import { BadRequestError } from "../../utils/errors.js"; /** * Analytics controller — delegates to service and returns consistent @@ -51,7 +52,12 @@ export const analyticsController = { ): Promise { try { const query = request.query as UserSummaryQuery; - const userId = query.userId ?? request.user.sub; + // When authenticating via API key, request.user.sub is "api_key:" which + // is not a valid users.id UUID. Require an explicit userId in that case. + const userId = query.userId ?? (request.authType === "api_key" ? null : request.user.sub); + if (!userId) { + throw new BadRequestError("userId query parameter is required when using API key authentication"); + } const data = await analyticsService.getUserSummary( request, userId, diff --git a/src/modules/auth/auth.routes.ts b/src/modules/auth/auth.routes.ts index 4bb9f7e..1ae7162 100644 --- a/src/modules/auth/auth.routes.ts +++ b/src/modules/auth/auth.routes.ts @@ -5,7 +5,8 @@ import { authenticate } from "../../middleware/auth.js"; const authMeResponseSchema = z.object({ success: z.literal(true), data: z.object({ - id: z.string().uuid(), + // sub can be a UUID (JWT) or "api_key:" (API key auth) — accept both + id: z.string(), email: z.string().email().optional(), role: z.enum(["ADMIN", "EMPLOYEE"]), orgId: z.string().uuid(),