Uh oh!
There was an error while loading. Please reload this page.
- Notifications
You must be signed in to change notification settings - Fork 3.8k
feat(uptimerobot): add UptimeRobot v3 integration#5229
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Uh oh!
There was an error while loading. Please reload this page.
Changes from all commits
3eaaeaed215550954b0684301512ec4b90b94ac0c5File filter
Filter by extension
Conversations
Uh oh!
There was an error while loading. Please reload this page.
Jump to
Uh oh!
There was an error while loading. Please reload this page.
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -218,6 +218,7 @@ | ||
| "twilio_voice", | ||
| "typeform", | ||
| "upstash", | ||
| "uptimerobot", | ||
| "vanta", | ||
| "vercel", | ||
| "wealthbox", | ||
Large diffs are not rendered by default.
Uh oh!
There was an error while loading. Please reload this page.
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,48 @@ | ||
| import { createLogger } from '@sim/logger' | ||
| import { getErrorMessage } from '@sim/utils/errors' | ||
| import { type NextRequest, NextResponse } from 'next/server' | ||
| import { uptimeRobotCreatePspContract } from '@/lib/api/contracts/tools/uptimerobot' | ||
| import { parseRequest } from '@/lib/api/server' | ||
| import { checkInternalAuth } from '@/lib/auth/hybrid' | ||
| import { generateRequestId } from '@/lib/core/utils/request' | ||
| import { withRouteHandler } from '@/lib/core/utils/with-route-handler' | ||
| import { forwardPspRequest } from '@/app/api/tools/uptimerobot/server-utils' | ||
| export const dynamic = 'force-dynamic' | ||
| const logger = createLogger('UptimeRobotCreatePspAPI') | ||
| export const POST = withRouteHandler(async (request: NextRequest) => { | ||
| const requestId = generateRequestId() | ||
| try { | ||
| const authResult = await checkInternalAuth(request, { requireWorkflowId: false }) | ||
| if (!authResult.success || !authResult.userId) { | ||
| logger.warn(`[${requestId}] Unauthorized UptimeRobot create-psp request: ${authResult.error}`) | ||
| return NextResponse.json( | ||
| { success: false, error: authResult.error || 'Authentication required' }, | ||
| { status: 401 } | ||
| ) | ||
| } | ||
| const parsed = await parseRequest(uptimeRobotCreatePspContract, request, {}) | ||
| if (!parsed.success) return parsed.response | ||
| const body = parsed.data.body | ||
| return forwardPspRequest({ | ||
| apiKey: body.apiKey, | ||
| method: 'POST', | ||
| path: '/psps', | ||
| fields: body, | ||
| userId: authResult.userId, | ||
| requestId, | ||
| logger, | ||
| }) | ||
| } catch (error) { | ||
| logger.error(`[${requestId}] Unexpected error creating status page:`, error) | ||
| return NextResponse.json( | ||
| { success: false, error: getErrorMessage(error, 'Unknown error') }, | ||
| { status: 500 } | ||
| ) | ||
| } | ||
| }) |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,162 @@ | ||
| import type { Logger } from '@sim/logger' | ||
| import { NextResponse } from 'next/server' | ||
| import { processFilesToUserFiles, type RawFileInput } from '@/lib/uploads/utils/file-utils' | ||
| import { downloadFileFromStorage } 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<NextResponse | null> { | ||
| 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 = await downloadFileFromStorage(userFile, requestId, logger) | ||
| const mimeType = 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<NextResponse> { | ||
| 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<string, unknown> | ||
| 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<string, unknown> | ||
| } 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) } }) | ||
waleedlatif1 marked this conversation as resolved.
Uh oh!There was an error while loading. Please reload this page. waleedlatif1 marked this conversation as resolved.
Uh oh!There was an error while loading. Please reload this page. | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,48 @@ | ||
| import { createLogger } from '@sim/logger' | ||
| import { getErrorMessage } from '@sim/utils/errors' | ||
| import { type NextRequest, NextResponse } from 'next/server' | ||
| import { uptimeRobotUpdatePspContract } from '@/lib/api/contracts/tools/uptimerobot' | ||
| import { parseRequest } from '@/lib/api/server' | ||
| import { checkInternalAuth } from '@/lib/auth/hybrid' | ||
| import { generateRequestId } from '@/lib/core/utils/request' | ||
| import { withRouteHandler } from '@/lib/core/utils/with-route-handler' | ||
| import { forwardPspRequest } from '@/app/api/tools/uptimerobot/server-utils' | ||
| export const dynamic = 'force-dynamic' | ||
| const logger = createLogger('UptimeRobotUpdatePspAPI') | ||
| export const POST = withRouteHandler(async (request: NextRequest) => { | ||
| const requestId = generateRequestId() | ||
| try { | ||
| const authResult = await checkInternalAuth(request, { requireWorkflowId: false }) | ||
| if (!authResult.success || !authResult.userId) { | ||
| logger.warn(`[${requestId}] Unauthorized UptimeRobot update-psp request: ${authResult.error}`) | ||
| return NextResponse.json( | ||
| { success: false, error: authResult.error || 'Authentication required' }, | ||
| { status: 401 } | ||
| ) | ||
| } | ||
| const parsed = await parseRequest(uptimeRobotUpdatePspContract, request, {}) | ||
| if (!parsed.success) return parsed.response | ||
| const body = parsed.data.body | ||
| return forwardPspRequest({ | ||
| apiKey: body.apiKey, | ||
| method: 'PATCH', | ||
| path: `/psps/${body.pspId}`, | ||
| fields: body, | ||
| userId: authResult.userId, | ||
| requestId, | ||
| logger, | ||
| }) | ||
| } catch (error) { | ||
| logger.error(`[${requestId}] Unexpected error updating status page:`, error) | ||
| return NextResponse.json( | ||
| { success: false, error: getErrorMessage(error, 'Unknown error') }, | ||
| { status: 500 } | ||
| ) | ||
| } | ||
| }) |
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.