import type { Logger } from '@sim/logger' import { NextResponse } from 'next/server' import { processFilesToUserFiles, type RawFileInput } from '@/lib/uploads/utils/file-utils' import { downloadServableFileFromStorage } from '@/lib/uploads/utils/file-utils.server' import { assertToolFileAccess } from '@/app/api/files/authorization' import { mapPsp, UPTIMEROBOT_API_BASE } from '@/tools/uptimerobot/types' /** Fields shared by the PSP create and update routes (before the files). */ interface PspFormFields { friendlyName?: string | null monitorIds?: string | null status?: string | null password?: string | null customDomain?: string | null hideUrlLinks?: boolean | null noIndex?: boolean | null logo?: unknown icon?: unknown } /** * Appends a single optional image file (logo or icon) to the form after * downloading it from storage and verifying the caller may access it. * * @returns an error `NextResponse` if the file is invalid or access is denied, * otherwise `null`. */ async function appendPspImage( form: FormData, field: 'logo' | 'icon', file: unknown, userId: string, requestId: string, logger: Logger ): Promise { const userFiles = processFilesToUserFiles([file as RawFileInput], requestId, logger) if (userFiles.length === 0) { // A file was supplied but could not be resolved to a stored UserFile (e.g. a // bare string reference). Surface it rather than silently dropping the image. return NextResponse.json( { success: false, error: `Invalid ${field} file: expected an uploaded file reference` }, { status: 400 } ) } const userFile = userFiles[0] const denied = await assertToolFileAccess(userFile.key, userId, requestId, logger) if (denied) return denied const { buffer, contentType } = await downloadServableFileFromStorage(userFile, requestId, logger) const mimeType = contentType || userFile.type || 'application/octet-stream' form.append(field, new Blob([new Uint8Array(buffer)], { type: mimeType }), userFile.name) return null } /** * Builds the multipart form for a PSP request, downloads any referenced * logo/icon files, forwards the request to UptimeRobot, and returns a typed * `{ success, output: { psp } }` envelope as a `NextResponse`. */ export async function forwardPspRequest(options: { apiKey: string method: 'POST' | 'PATCH' path: string fields: PspFormFields userId: string requestId: string logger: Logger }): Promise { const { apiKey, method, path, fields, userId, requestId, logger } = options const form = new FormData() if (fields.friendlyName) form.append('friendlyName', fields.friendlyName) if (fields.status) form.append('status', fields.status) if (fields.password) form.append('password', fields.password) if (fields.customDomain) form.append('customDomain', fields.customDomain) if (typeof fields.hideUrlLinks === 'boolean') { form.append('hideUrlLinks', String(fields.hideUrlLinks)) } if (typeof fields.noIndex === 'boolean') form.append('noIndex', String(fields.noIndex)) if (fields.monitorIds) { for (const id of fields.monitorIds.split(',')) { const trimmed = id.trim() if (trimmed) form.append('monitorIds', trimmed) } } if (fields.logo) { const denied = await appendPspImage(form, 'logo', fields.logo, userId, requestId, logger) if (denied) return denied } if (fields.icon) { const denied = await appendPspImage(form, 'icon', fields.icon, userId, requestId, logger) if (denied) return denied } const response = await fetch(`${UPTIMEROBOT_API_BASE}${path}`, { method, headers: { Authorization: `Bearer ${apiKey}`, Accept: 'application/json' }, body: form, }) const text = await response.text() if (!response.ok) { let message: string | undefined try { message = JSON.parse(text)?.message } catch { message = undefined } logger.error(`[${requestId}] UptimeRobot PSP request failed`, { status: response.status, body: text, }) return NextResponse.json( { success: false, error: message || `UptimeRobot API error (HTTP ${response.status})` }, { status: response.status } ) } // A successful PSP create/update must return the PspDto object. An empty or // non-object body is unexpected — reject it rather than mapping a phantom PSP // (id: 0, empty name, null images) back to the workflow. if (!text) { logger.error(`[${requestId}] UptimeRobot returned an empty PSP response`) return NextResponse.json( { success: false, error: 'UptimeRobot returned an unexpected response' }, { status: 502 } ) } let data: Record try { const parsed = JSON.parse(text) if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) { throw new Error('Expected a PSP object response') } data = parsed as Record } catch { logger.error(`[${requestId}] UptimeRobot returned an unexpected PSP response`, { body: text }) return NextResponse.json( { success: false, error: 'UptimeRobot returned an unexpected response' }, { status: 502 } ) } // A real PspDto always carries a positive numeric `id` and a non-empty // `friendlyName` (both spec-required). If they are absent, the body is a `{}` // or metadata envelope, not a status page — surface the provider error rather // than mapping a phantom PSP. if (typeof data.id !== 'number' || data.id < 1 || !data.friendlyName) { logger.error(`[${requestId}] UptimeRobot returned a PSP response without core fields`, { body: text, }) return NextResponse.json( { success: false, error: 'UptimeRobot returned an unexpected response' }, { status: 502 } ) } return NextResponse.json({ success: true, output: { psp: mapPsp(data) } }) }