Makelore 2.0 initial clean snapshot
This commit is contained in:
190
electron/api/routes/user-sync.ts
Normal file
190
electron/api/routes/user-sync.ts
Normal file
@@ -0,0 +1,190 @@
|
||||
import type { IncomingMessage, ServerResponse } from 'http';
|
||||
import { getProviderService } from '../../services/providers/provider-service';
|
||||
import { proxyAwareFetch } from '../../utils/proxy-fetch';
|
||||
import {
|
||||
normalizeUserSyncBootstrap,
|
||||
toServerUserSyncPartner,
|
||||
toServerUserSyncPreferences,
|
||||
toServerUserSyncProviderAccount,
|
||||
type UserSyncBootstrap,
|
||||
} from '../../../shared/user-sync';
|
||||
import type { HostApiContext } from '../context';
|
||||
import { parseJsonBody, sendJson } from '../route-utils';
|
||||
import { WORKS_SQUARE_CONFIG } from '../works-config';
|
||||
|
||||
class UserSyncProxyError extends Error {
|
||||
constructor(
|
||||
public readonly statusCode: number,
|
||||
message: string,
|
||||
) {
|
||||
super(message);
|
||||
}
|
||||
}
|
||||
|
||||
function normalizeWorksBase(value = WORKS_SQUARE_CONFIG.apiBaseUrl): string {
|
||||
const apiBase = value.replace(/\/+$/, '');
|
||||
if (!/^https?:\/\//i.test(apiBase)) {
|
||||
throw new Error('Works Square API base URL must start with http:// or https://');
|
||||
}
|
||||
return apiBase;
|
||||
}
|
||||
|
||||
function createWorksUrl(pathname: string): URL {
|
||||
return new URL(`${normalizeWorksBase()}${pathname}`);
|
||||
}
|
||||
|
||||
function readRequiredAccessToken(req: IncomingMessage): string {
|
||||
const header = req.headers['x-niancode-access-token'];
|
||||
const token = Array.isArray(header) ? header[0] : header;
|
||||
if (!token || !token.trim()) {
|
||||
throw new UserSyncProxyError(400, 'Missing x-niancode-access-token');
|
||||
}
|
||||
return token.trim();
|
||||
}
|
||||
|
||||
async function readResponsePayload(response: Response): Promise<unknown> {
|
||||
const text = await response.text();
|
||||
if (!text.trim()) return null;
|
||||
try {
|
||||
return JSON.parse(text) as unknown;
|
||||
} catch {
|
||||
return text;
|
||||
}
|
||||
}
|
||||
|
||||
function getErrorMessage(payload: unknown, fallback: string): string {
|
||||
if (payload && typeof payload === 'object') {
|
||||
const record = payload as Record<string, unknown>;
|
||||
for (const field of ['msg', 'message', 'error_description', 'error', 'detail']) {
|
||||
const value = record[field];
|
||||
if (typeof value === 'string' && value.trim()) {
|
||||
return value;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (typeof payload === 'string' && payload.trim()) {
|
||||
return payload;
|
||||
}
|
||||
return fallback;
|
||||
}
|
||||
|
||||
async function fetchUserSync(
|
||||
pathname: string,
|
||||
accessToken: string,
|
||||
init: RequestInit,
|
||||
): Promise<unknown> {
|
||||
const response = await proxyAwareFetch(createWorksUrl(pathname).toString(), {
|
||||
...init,
|
||||
headers: {
|
||||
Authorization: `Bearer ${accessToken}`,
|
||||
...(init.headers ?? {}),
|
||||
},
|
||||
});
|
||||
const payload = await readResponsePayload(response);
|
||||
if (!response.ok) {
|
||||
throw new UserSyncProxyError(
|
||||
response.status >= 400 && response.status < 500 ? response.status : 502,
|
||||
getErrorMessage(payload, `Works Square user sync failed (${response.status})`),
|
||||
);
|
||||
}
|
||||
return payload;
|
||||
}
|
||||
|
||||
async function normalizeAndApplyProviderMetadata(payload: unknown): Promise<{
|
||||
bootstrap: UserSyncBootstrap;
|
||||
applied: { providerAccounts: number };
|
||||
}> {
|
||||
const bootstrap = normalizeUserSyncBootstrap(payload);
|
||||
const providerService = getProviderService();
|
||||
let appliedProviderAccounts = 0;
|
||||
for (const account of bootstrap.providerAccounts) {
|
||||
await providerService.upsertSyncedAccountMetadata(account);
|
||||
appliedProviderAccounts += 1;
|
||||
}
|
||||
return {
|
||||
bootstrap,
|
||||
applied: { providerAccounts: appliedProviderAccounts },
|
||||
};
|
||||
}
|
||||
|
||||
export async function handleUserSyncRoutes(
|
||||
req: IncomingMessage,
|
||||
res: ServerResponse,
|
||||
url: URL,
|
||||
_ctx: HostApiContext,
|
||||
): Promise<boolean> {
|
||||
if (!url.pathname.startsWith('/api/user-sync')) {
|
||||
return false;
|
||||
}
|
||||
|
||||
try {
|
||||
const accessToken = readRequiredAccessToken(req);
|
||||
|
||||
if (url.pathname === '/api/user-sync/bootstrap' && req.method === 'GET') {
|
||||
const result = await normalizeAndApplyProviderMetadata(
|
||||
await fetchUserSync('/api/me/bootstrap', accessToken, { method: 'GET' }),
|
||||
);
|
||||
sendJson(res, 200, { success: true, ...result });
|
||||
return true;
|
||||
}
|
||||
|
||||
if (url.pathname === '/api/user-sync/preferences' && req.method === 'PUT') {
|
||||
const body = await parseJsonBody<unknown>(req);
|
||||
const result = await normalizeAndApplyProviderMetadata(
|
||||
await fetchUserSync('/api/me/preferences', accessToken, {
|
||||
method: 'PUT',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(toServerUserSyncPreferences(body)),
|
||||
}),
|
||||
);
|
||||
sendJson(res, 200, { success: true, ...result });
|
||||
return true;
|
||||
}
|
||||
|
||||
if (url.pathname === '/api/user-sync/provider-accounts' && req.method === 'POST') {
|
||||
const body = await parseJsonBody<unknown>(req);
|
||||
const result = await normalizeAndApplyProviderMetadata(
|
||||
await fetchUserSync('/api/me/provider-accounts', accessToken, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(toServerUserSyncProviderAccount(body)),
|
||||
}),
|
||||
);
|
||||
sendJson(res, 200, { success: true, ...result });
|
||||
return true;
|
||||
}
|
||||
|
||||
if (url.pathname === '/api/user-sync/partners' && req.method === 'POST') {
|
||||
const body = await parseJsonBody<unknown>(req);
|
||||
const result = await normalizeAndApplyProviderMetadata(
|
||||
await fetchUserSync('/api/me/partners', accessToken, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(toServerUserSyncPartner(body)),
|
||||
}),
|
||||
);
|
||||
sendJson(res, 200, { success: true, ...result });
|
||||
return true;
|
||||
}
|
||||
|
||||
if (url.pathname.startsWith('/api/user-sync/partners/') && req.method === 'DELETE') {
|
||||
const partnerId = decodeURIComponent(url.pathname.slice('/api/user-sync/partners/'.length));
|
||||
const result = await normalizeAndApplyProviderMetadata(
|
||||
await fetchUserSync(`/api/me/partners/${encodeURIComponent(partnerId)}`, accessToken, {
|
||||
method: 'DELETE',
|
||||
}),
|
||||
);
|
||||
sendJson(res, 200, { success: true, ...result });
|
||||
return true;
|
||||
}
|
||||
|
||||
sendJson(res, 404, { success: false, error: `No route for ${req.method} ${url.pathname}` });
|
||||
return true;
|
||||
} catch (error) {
|
||||
sendJson(res, error instanceof UserSyncProxyError ? error.statusCode : 500, {
|
||||
success: false,
|
||||
error: error instanceof Error ? error.message : String(error),
|
||||
});
|
||||
return true;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user