feat: convert server to TypeScript + add ERP API proxy
- Migrate server/index.js → modular TS structure (config, routes, services) - Add ERP proxy: GET /api/erp/sales proxies Hono ERP API with server-side auth - JWT token cached server-side, auto-refreshes on 401 - ERP credentials stay server-side only (no VITE_ prefix) - Vite dev proxy routes /api/erp → localhost:3001 - Preserve existing Salla OAuth integration Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
62
server/src/services/erpClient.ts
Normal file
62
server/src/services/erpClient.ts
Normal file
@@ -0,0 +1,62 @@
|
||||
import { erp } from '../config';
|
||||
|
||||
let cachedToken: string | null = null;
|
||||
|
||||
async function login(): Promise<string> {
|
||||
const res = await fetch(`${erp.apiUrl}/auth/login`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ username: erp.username, password: erp.password }),
|
||||
});
|
||||
|
||||
if (!res.ok) {
|
||||
throw new Error(`ERP login failed: ${res.status} ${res.statusText}`);
|
||||
}
|
||||
|
||||
const data = await res.json();
|
||||
cachedToken = data.token;
|
||||
return cachedToken;
|
||||
}
|
||||
|
||||
async function getToken(): Promise<string> {
|
||||
if (cachedToken) return cachedToken;
|
||||
return login();
|
||||
}
|
||||
|
||||
export async function fetchSales(startDate: string, endDate: string): Promise<unknown[]> {
|
||||
const token = await getToken();
|
||||
|
||||
const url = new URL(`${erp.apiUrl}/api/getbydate`);
|
||||
url.searchParams.set('startDate', startDate);
|
||||
url.searchParams.set('endDate', endDate);
|
||||
url.searchParams.set('code', erp.apiCode);
|
||||
|
||||
const res = await fetch(url.toString(), {
|
||||
headers: { Authorization: `Bearer ${token}` },
|
||||
});
|
||||
|
||||
// Token expired — re-login and retry once
|
||||
if (res.status === 401) {
|
||||
cachedToken = null;
|
||||
const freshToken = await login();
|
||||
|
||||
const retry = await fetch(url.toString(), {
|
||||
headers: { Authorization: `Bearer ${freshToken}` },
|
||||
});
|
||||
|
||||
if (!retry.ok) {
|
||||
throw new Error(`ERP fetch failed after re-login: ${retry.status}`);
|
||||
}
|
||||
return retry.json();
|
||||
}
|
||||
|
||||
if (!res.ok) {
|
||||
throw new Error(`ERP fetch failed: ${res.status} ${res.statusText}`);
|
||||
}
|
||||
|
||||
return res.json();
|
||||
}
|
||||
|
||||
export function isConfigured(): boolean {
|
||||
return !!(erp.apiUrl && erp.apiCode && erp.username && erp.password);
|
||||
}
|
||||
60
server/src/services/sallaClient.ts
Normal file
60
server/src/services/sallaClient.ts
Normal file
@@ -0,0 +1,60 @@
|
||||
import axios from 'axios';
|
||||
import { salla } from '../config';
|
||||
|
||||
let accessToken = salla.accessToken || null;
|
||||
let refreshToken = salla.refreshToken || null;
|
||||
|
||||
export function getAuthStatus() {
|
||||
return { connected: !!accessToken, hasRefreshToken: !!refreshToken };
|
||||
}
|
||||
|
||||
export function setTokens(access: string, refresh?: string) {
|
||||
accessToken = access;
|
||||
if (refresh) refreshToken = refresh;
|
||||
}
|
||||
|
||||
export async function refreshAccessToken(): Promise<string> {
|
||||
if (!refreshToken) throw new Error('No refresh token available');
|
||||
|
||||
const response = await axios.post('https://accounts.salla.sa/oauth2/token', {
|
||||
client_id: salla.clientId,
|
||||
client_secret: salla.clientSecret,
|
||||
grant_type: 'refresh_token',
|
||||
refresh_token: refreshToken,
|
||||
});
|
||||
|
||||
accessToken = response.data.access_token;
|
||||
if (response.data.refresh_token) {
|
||||
refreshToken = response.data.refresh_token;
|
||||
}
|
||||
|
||||
return accessToken!;
|
||||
}
|
||||
|
||||
export async function callSallaAPI(
|
||||
endpoint: string,
|
||||
method: 'GET' | 'POST' = 'GET',
|
||||
data: unknown = null
|
||||
): Promise<unknown> {
|
||||
if (!accessToken) throw new Error('Not authenticated. Visit /auth/login first.');
|
||||
|
||||
try {
|
||||
const response = await axios({
|
||||
method,
|
||||
url: `https://api.salla.dev/admin/v2${endpoint}`,
|
||||
headers: {
|
||||
Authorization: `Bearer ${accessToken}`,
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
data,
|
||||
});
|
||||
return response.data;
|
||||
} catch (err: unknown) {
|
||||
const axiosErr = err as { response?: { status: number } };
|
||||
if (axiosErr.response?.status === 401) {
|
||||
await refreshAccessToken();
|
||||
return callSallaAPI(endpoint, method, data);
|
||||
}
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user