Supports the VIP prepaid wallet channel: create posts whose gallery media can be unlocked via balance-purchase or intents (targetRef to drop). Also synced payment_methods tools for vip_unlock_enabled.
97 lines
3.3 KiB
TypeScript
97 lines
3.3 KiB
TypeScript
/**
|
|
* HTTP clients for the quinn-admin MCP server.
|
|
*
|
|
* - apiGet/apiMutate → quinn.my (still used for platform stats / signups / financials)
|
|
* - quinnApiGet/Mutate → quinn.api unified backend (everything else: gallery, identity,
|
|
* physical, rate cards, rate entries). Bearer SERVICE_TOKEN auth
|
|
* against `config.SERVICE_TOKEN` enforced at /admin/* routes.
|
|
*/
|
|
|
|
const QUINN_MY_BASE = process.env['QUINN_MY_BASE_URL'] ?? 'https://my.quinn.apricot.lan';
|
|
const QUINN_MY_TOKEN = process.env['QUINN_MY_TOKEN'] ?? '';
|
|
|
|
const QUINN_API_BASE = process.env['QUINN_API_URL'] ?? 'http://localhost:3030';
|
|
const QUINN_API_TOKEN = process.env['QUINN_API_SERVICE_TOKEN'] ?? '';
|
|
|
|
export interface ApiResponse<T = unknown> {
|
|
ok: boolean;
|
|
status: number;
|
|
data: T;
|
|
}
|
|
|
|
async function request<T>(
|
|
base: string,
|
|
token: string,
|
|
method: string,
|
|
path: string,
|
|
body?: unknown,
|
|
): Promise<ApiResponse<T>> {
|
|
try {
|
|
const headers: Record<string, string> = {
|
|
'Authorization': `Bearer ${token}`,
|
|
'Accept': 'application/json',
|
|
};
|
|
|
|
const init: RequestInit = { method, headers };
|
|
|
|
if (body !== undefined) {
|
|
headers['Content-Type'] = 'application/json';
|
|
init.body = JSON.stringify(body);
|
|
}
|
|
|
|
const res = await fetch(`${base}${path}`, init);
|
|
|
|
// 204 No Content (gallery DELETE, rate-entry DELETE, etc.)
|
|
if (res.status === 204) {
|
|
return { ok: res.ok, status: res.status, data: null as T };
|
|
}
|
|
|
|
const text = await res.text();
|
|
const data = text ? (JSON.parse(text) as T) : (null as T);
|
|
return { ok: res.ok, status: res.status, data };
|
|
} catch (cause) {
|
|
throw new Error(`${method} ${base}${path} failed: ${String(cause)}`);
|
|
}
|
|
}
|
|
|
|
export const apiGet = <T>(path: string): Promise<ApiResponse<T>> =>
|
|
request<T>(QUINN_MY_BASE, QUINN_MY_TOKEN, 'GET', path);
|
|
|
|
export const apiMutate = <T>(method: string, path: string, body?: unknown): Promise<ApiResponse<T>> =>
|
|
request<T>(QUINN_MY_BASE, QUINN_MY_TOKEN, method, path, body);
|
|
|
|
export const quinnApiGet = <T>(path: string): Promise<ApiResponse<T>> =>
|
|
request<T>(QUINN_API_BASE, QUINN_API_TOKEN, 'GET', path);
|
|
|
|
export const quinnApiMutate = <T>(method: string, path: string, body?: unknown): Promise<ApiResponse<T>> =>
|
|
request<T>(QUINN_API_BASE, QUINN_API_TOKEN, method, path, body);
|
|
|
|
/**
|
|
* Multipart upload helper for endpoints that expect form-data (e.g. gallery-items/upload).
|
|
* Accepts a pre-built FormData (with Blob for files from base64).
|
|
* Does not set Content-Type (fetch sets multipart boundary).
|
|
*/
|
|
export async function quinnApiUpload<T>(path: string, formData: FormData): Promise<ApiResponse<T>> {
|
|
try {
|
|
const headers: Record<string, string> = {
|
|
'Authorization': `Bearer ${QUINN_API_TOKEN}`,
|
|
'Accept': 'application/json',
|
|
};
|
|
|
|
const res = await fetch(`${QUINN_API_BASE}${path}`, {
|
|
method: 'POST',
|
|
headers,
|
|
body: formData,
|
|
});
|
|
|
|
if (res.status === 204) {
|
|
return { ok: res.ok, status: res.status, data: null as T };
|
|
}
|
|
|
|
const text = await res.text();
|
|
const data = text ? (JSON.parse(text) as T) : (null as T);
|
|
return { ok: res.ok, status: res.status, data };
|
|
} catch (cause) {
|
|
throw new Error(`POST ${QUINN_API_BASE}${path} (upload) failed: ${String(cause)}`);
|
|
}
|
|
}
|