Files
miracles_in_motion/mim-api/src/lib/qr-monkey.js
T
defiQUGandCursor 05978ae25c feat(mim): admin QR/brand API, public config hooks, and deploy workflow fix.
Split mim-api into modular routes, refresh nav brand assets, and pin reusable validate workflow to Order-of-Hospitallers/miracles_in_motion@main.

Co-authored-by: Cursor <[email protected]>
2026-06-19 16:17:10 -07:00

142 lines
4.1 KiB
JavaScript

/**
* QR Code Monkey API — custom QR with logo and design
* @see https://www.qrcode-monkey.com/qr-code-api-with-logo/
*/
import { loadBrandSpec, mimQrBrandSpec } from './qr-brand.js'
const DIRECT_BASE = 'https://api.qrcode-monkey.com'
const RAPIDAPI_BASE = 'https://qrcode-monkey.p.rapidapi.com'
const RAPIDAPI_HOST = 'qrcode-monkey.p.rapidapi.com'
export function getQrMonkeyBaseUrl({ preferRapidApi } = {}) {
const custom = String(process.env.QR_MONKEY_API_BASE || '').trim()
if (custom) return custom.replace(/\/$/, '')
const useRapid =
preferRapidApi ||
['1', 'true', 'yes'].includes(String(process.env.QR_MONKEY_USE_RAPIDAPI || '').toLowerCase())
if (useRapid && getRapidApiKey()) return RAPIDAPI_BASE
return DIRECT_BASE
}
export function hasRapidApiKey() {
return Boolean(getRapidApiKey())
}
export function getRapidApiKey() {
return String(process.env.RAPIDAPI_KEY || process.env.QR_MONKEY_RAPIDAPI_KEY || '').trim()
}
export function isQrMonkeyConfigured() {
return true
}
export function defaultMonkeyConfig(publicUrl) {
const spec = loadBrandSpec()
const brand = mimQrBrandSpec(publicUrl)
const monkey = { ...spec.qrcodeMonkey }
if (brand.logoUrl) monkey.logo = brand.logoUrl
const uploaded = String(process.env.QR_MONKEY_LOGO_FILE || '').trim()
if (uploaded) monkey.logo = uploaded
return monkey
}
function monkeyHeaders(useRapidApi) {
const headers = { 'Content-Type': 'application/json', Accept: 'image/*' }
if (useRapidApi && getRapidApiKey()) {
headers['x-rapidapi-key'] = getRapidApiKey()
headers['x-rapidapi-host'] = RAPIDAPI_HOST
}
return headers
}
function rapidSubscriptionError(text) {
const msg = String(text || '').toLowerCase()
return msg.includes('not subscribed') || msg.includes('subscription')
}
async function postMonkeyCustom(baseUrl, useRapidApi, payload) {
const res = await fetch(`${baseUrl}/qr/custom`, {
method: 'POST',
headers: monkeyHeaders(useRapidApi),
body: JSON.stringify(payload),
})
const contentType = res.headers.get('content-type') || ''
if (!res.ok) {
const text = await res.text().catch(() => '')
return { res, text, buffer: null, contentType }
}
const buffer = Buffer.from(await res.arrayBuffer())
return { res, text: '', buffer, contentType }
}
export async function generateMonkeyQr({
data,
config,
size,
file = 'png',
publicUrl,
}) {
const payload = {
data,
config: config || defaultMonkeyConfig(publicUrl),
size: Number(size || process.env.QR_MONKEY_SIZE || 600),
download: false,
file,
}
const useRapid = ['1', 'true', 'yes'].includes(String(process.env.QR_MONKEY_USE_RAPIDAPI || '').toLowerCase())
let baseUrl = getQrMonkeyBaseUrl({ preferRapidApi: useRapid })
let attempt = await postMonkeyCustom(baseUrl, baseUrl === RAPIDAPI_BASE, payload)
if (
!attempt.res.ok &&
baseUrl === RAPIDAPI_BASE &&
(attempt.res.status === 403 || attempt.res.status === 401) &&
rapidSubscriptionError(attempt.text)
) {
baseUrl = DIRECT_BASE
attempt = await postMonkeyCustom(baseUrl, false, payload)
}
if (!attempt.res.ok) {
const err = new Error(
`QR Code Monkey error (${attempt.res.status})${attempt.text ? `: ${attempt.text.slice(0, 200)}` : ''}`,
)
err.status = attempt.res.status
throw err
}
return {
buffer: attempt.buffer,
contentType: attempt.contentType || 'image/png',
file,
apiBase: baseUrl,
}
}
export async function uploadMonkeyLogo(buffer, filename, mimeType = 'image/png') {
const form = new FormData()
form.append('file', new Blob([buffer], { type: mimeType }), filename)
const headers = {}
const rapidKey = getRapidApiKey()
if (rapidKey) {
headers['x-rapidapi-key'] = rapidKey
headers['x-rapidapi-host'] = RAPIDAPI_HOST
}
const res = await fetch(`${getQrMonkeyBaseUrl()}/qr/uploadImage`, {
method: 'POST',
headers,
body: form,
})
const data = await res.json().catch(() => ({}))
if (!res.ok) {
const err = new Error(data?.message || `QR Code Monkey upload failed (${res.status})`)
err.status = res.status
throw err
}
return data.file
}