diff --git a/server/auth/actions.ts b/server/auth/actions.ts index 17ad7cda..4df4c279 100644 --- a/server/auth/actions.ts +++ b/server/auth/actions.ts @@ -126,7 +126,8 @@ export enum ActionsEnum { applyBlueprint = "applyBlueprint", viewLogs = "viewLogs", exportLogs = "exportLogs", - listApprovals = "listApprovals" + listApprovals = "listApprovals", + updateApprovals = "updateApprovals" } export async function checkUserActionPermission( diff --git a/server/db/pg/schema/privateSchema.ts b/server/db/pg/schema/privateSchema.ts index 05456b1a..3900f46a 100644 --- a/server/db/pg/schema/privateSchema.ts +++ b/server/db/pg/schema/privateSchema.ts @@ -298,7 +298,7 @@ export const accessAuditLog = pgTable( ); export const approvals = pgTable("approvals", { - id: serial("id").primaryKey(), + approvalId: serial("approvalId").primaryKey(), timestamp: integer("timestamp").notNull(), // this is EPOCH time in seconds orgId: varchar("orgId") .references(() => orgs.orgId, { diff --git a/server/db/sqlite/schema/privateSchema.ts b/server/db/sqlite/schema/privateSchema.ts index 70c04469..32aa543e 100644 --- a/server/db/sqlite/schema/privateSchema.ts +++ b/server/db/sqlite/schema/privateSchema.ts @@ -290,7 +290,7 @@ export const accessAuditLog = sqliteTable( ); export const approvals = sqliteTable("approvals", { - id: integer("id").primaryKey({ autoIncrement: true }), + approvalId: integer("approvalId").primaryKey({ autoIncrement: true }), timestamp: integer("timestamp").notNull(), // this is EPOCH time in seconds orgId: text("orgId") .references(() => orgs.orgId, { diff --git a/server/private/routers/approvals/index.ts b/server/private/routers/approvals/index.ts index d115a054..40e59cc9 100644 --- a/server/private/routers/approvals/index.ts +++ b/server/private/routers/approvals/index.ts @@ -12,3 +12,4 @@ */ export * from "./listApprovals"; +export * from "./processPendingApproval"; diff --git a/server/private/routers/approvals/listApprovals.ts b/server/private/routers/approvals/listApprovals.ts index e44fc1be..ca774801 100644 --- a/server/private/routers/approvals/listApprovals.ts +++ b/server/private/routers/approvals/listApprovals.ts @@ -72,7 +72,7 @@ async function queryApprovals( const res = await db .select({ - approvalId: approvals.id, + approvalId: approvals.approvalId, orgId: approvals.orgId, clientId: approvals.clientId, decision: approvals.decision, diff --git a/server/private/routers/approvals/processPendingApproval.ts b/server/private/routers/approvals/processPendingApproval.ts new file mode 100644 index 00000000..4e456eb9 --- /dev/null +++ b/server/private/routers/approvals/processPendingApproval.ts @@ -0,0 +1,134 @@ +/* + * This file is part of a proprietary work. + * + * Copyright (c) 2025 Fossorial, Inc. + * All rights reserved. + * + * This file is licensed under the Fossorial Commercial License. + * You may not use this file except in compliance with the License. + * Unauthorized use, copying, modification, or distribution is strictly prohibited. + * + * This file is not licensed under the AGPLv3. + */ + +import logger from "@server/logger"; +import HttpCode from "@server/types/HttpCode"; +import createHttpError from "http-errors"; +import { z } from "zod"; +import { fromError } from "zod-validation-error"; + +import { build } from "@server/build"; +import { approvals, clients, db, orgs } from "@server/db"; +import { getOrgTierData } from "@server/lib/billing"; +import { TierId } from "@server/lib/billing/tiers"; +import response from "@server/lib/response"; +import { and, eq } from "drizzle-orm"; +import type { NextFunction, Request, Response } from "express"; + +const paramsSchema = z.strictObject({ + orgId: z.string(), + approvalId: z.int() +}); + +const bodySchema = z.strictObject({ + decision: z.enum(["approved", "denied"]) +}); + +export async function processPendingApproval( + req: Request, + res: Response, + next: NextFunction +) { + try { + const parsedParams = paramsSchema.safeParse(req.params); + if (!parsedParams.success) { + return next( + createHttpError( + HttpCode.BAD_REQUEST, + fromError(parsedParams.error).toString() + ) + ); + } + + const parsedBody = bodySchema.safeParse(req.body); + + if (!parsedBody.success) { + return next( + createHttpError( + HttpCode.BAD_REQUEST, + fromError(parsedBody.error).toString() + ) + ); + } + + const { orgId, approvalId } = parsedParams.data; + + if (build === "saas") { + const { tier } = await getOrgTierData(orgId); + const subscribed = tier === TierId.STANDARD; + if (!subscribed) { + return next( + createHttpError( + HttpCode.FORBIDDEN, + "This organization's current plan does not support this feature." + ) + ); + } + } + + const updateData = parsedBody.data; + + const approval = await db + .select() + .from(approvals) + .where( + and( + eq(approvals.approvalId, approvalId), + eq(approvals.decision, "pending") + ) + ) + .innerJoin(orgs, eq(approvals.orgId, approvals.orgId)) + .limit(1); + + if (approval.length === 0) { + return next( + createHttpError( + HttpCode.NOT_FOUND, + `Pending Approval with ID ${approvalId} not found` + ) + ); + } + + const [updatedApproval] = await db + .update(approvals) + .set(updateData) + .where(eq(approvals.approvalId, approvalId)) + .returning(); + + // Update user device approval state too + if ( + updatedApproval.type === "user_device" && + updatedApproval.clientId + ) { + await db + .update(clients) + .set({ + approvalState: updateData.decision + }) + .where(eq(clients.clientId, updatedApproval.clientId)); + } + + return response(res, { + data: updatedApproval, + success: true, + error: false, + message: "Approval updated successfully", + status: HttpCode.OK + }); + } catch (error) { + logger.error(error); + return next( + createHttpError(HttpCode.INTERNAL_SERVER_ERROR, "An error occurred") + ); + } +} diff --git a/server/private/routers/external.ts b/server/private/routers/external.ts index 9ecf57f3..7f1eb32f 100644 --- a/server/private/routers/external.ts +++ b/server/private/routers/external.ts @@ -321,6 +321,15 @@ authenticated.get( approval.listApprovals ); +authenticated.put( + "/org/:orgId/approvals/:approvalId", + verifyValidLicense, + verifyOrgAccess, + verifyUserHasAction(ActionsEnum.updateApprovals), + logActionAudit(ActionsEnum.updateApprovals), + approval.processPendingApproval +); + authenticated.get( "/org/:orgId/login-page-branding", verifyValidLicense,