feat(integrations): clarify REST Shopify and mapping workflows

This commit is contained in:
Inamul-hasan-tec
2026-09-08 16:32:25 +05:30
parent 1d6e96d5af
commit 5032fb3046
10 changed files with 369 additions and 151 deletions
+42 -21
View File
@@ -86,7 +86,6 @@
"integrity": "sha512-RgHBCvtjbOK2gXSNBNIkNoEc9qoVEtau3hj8gEqKQuL3HZAibKarWFEI3Lfm6EYKkLalOh8eSrj9b+ch9H/VBA==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"@babel/code-frame": "^7.29.7",
"@babel/generator": "^7.29.7",
@@ -302,6 +301,27 @@
"integrity": "sha512-lwYN/vDPeNRULcepoE/LO2Pgx+7/RV+S9ARfbc9lr2DtGkOD7pAiruHvbR1RX3Qyf6ja47EWJDMsNK5vK08DJg==",
"license": "MIT"
},
"node_modules/@emnapi/core": {
"version": "1.11.3",
"resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.11.3.tgz",
"integrity": "sha512-zLpS5asjEb7lq8jYLq37N6XKaE41DIexlY1rF/z4/tIl3wo13Sqm28fRyfIsKZD+NZ8mM5RoKkpW/rBcuoSZSg==",
"license": "MIT",
"optional": true,
"dependencies": {
"@emnapi/wasi-threads": "1.2.3",
"tslib": "^2.4.0"
}
},
"node_modules/@emnapi/runtime": {
"version": "1.11.3",
"resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.11.3.tgz",
"integrity": "sha512-Xz4Tpyki7XyrpbUK1jR1AhdAdaXyhhY4lZ3neLodmhpuWfy2PAQN5B46sAiU4liOXGLkHypn/qU+jvfWSCYYLA==",
"license": "MIT",
"optional": true,
"dependencies": {
"tslib": "^2.4.0"
}
},
"node_modules/@emnapi/wasi-threads": {
"version": "1.2.3",
"resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.2.3.tgz",
@@ -625,6 +645,7 @@
"integrity": "sha512-h5LUOzGArYemnW1NMz/DuuQhBi96J6JL2Bk8zE4kvqxB5Sg3jxmCiH4uyOWHDkiKSt5vWlG4FIwCR/DbstcNRQ==",
"dev": true,
"license": "MIT",
"peer": true,
"funding": {
"url": "https://github.com/sponsors/Boshen"
}
@@ -1361,6 +1382,7 @@
"os": [
"android"
],
"peer": true,
"engines": {
"node": "^20.19.0 || >=22.12.0"
}
@@ -1378,6 +1400,7 @@
"os": [
"darwin"
],
"peer": true,
"engines": {
"node": "^20.19.0 || >=22.12.0"
}
@@ -1395,6 +1418,7 @@
"os": [
"darwin"
],
"peer": true,
"engines": {
"node": "^20.19.0 || >=22.12.0"
}
@@ -1412,6 +1436,7 @@
"os": [
"freebsd"
],
"peer": true,
"engines": {
"node": "^20.19.0 || >=22.12.0"
}
@@ -1429,6 +1454,7 @@
"os": [
"linux"
],
"peer": true,
"engines": {
"node": "^20.19.0 || >=22.12.0"
}
@@ -1446,6 +1472,7 @@
"os": [
"linux"
],
"peer": true,
"engines": {
"node": "^20.19.0 || >=22.12.0"
}
@@ -1463,6 +1490,7 @@
"os": [
"linux"
],
"peer": true,
"engines": {
"node": "^20.19.0 || >=22.12.0"
}
@@ -1480,6 +1508,7 @@
"os": [
"linux"
],
"peer": true,
"engines": {
"node": "^20.19.0 || >=22.12.0"
}
@@ -1497,6 +1526,7 @@
"os": [
"linux"
],
"peer": true,
"engines": {
"node": "^20.19.0 || >=22.12.0"
}
@@ -1514,6 +1544,7 @@
"os": [
"linux"
],
"peer": true,
"engines": {
"node": "^20.19.0 || >=22.12.0"
}
@@ -1531,6 +1562,7 @@
"os": [
"linux"
],
"peer": true,
"engines": {
"node": "^20.19.0 || >=22.12.0"
}
@@ -1548,6 +1580,7 @@
"os": [
"openharmony"
],
"peer": true,
"engines": {
"node": "^20.19.0 || >=22.12.0"
}
@@ -1562,6 +1595,7 @@
"dev": true,
"license": "MIT",
"optional": true,
"peer": true,
"dependencies": {
"@emnapi/core": "1.11.2",
"@emnapi/runtime": "1.11.2",
@@ -1578,6 +1612,7 @@
"dev": true,
"license": "MIT",
"optional": true,
"peer": true,
"dependencies": {
"@emnapi/wasi-threads": "1.2.2",
"tslib": "^2.4.0"
@@ -1590,6 +1625,7 @@
"dev": true,
"license": "MIT",
"optional": true,
"peer": true,
"dependencies": {
"tslib": "^2.4.0"
}
@@ -1601,6 +1637,7 @@
"dev": true,
"license": "MIT",
"optional": true,
"peer": true,
"dependencies": {
"tslib": "^2.4.0"
}
@@ -1618,6 +1655,7 @@
"os": [
"win32"
],
"peer": true,
"engines": {
"node": "^20.19.0 || >=22.12.0"
}
@@ -1635,6 +1673,7 @@
"os": [
"win32"
],
"peer": true,
"engines": {
"node": "^20.19.0 || >=22.12.0"
}
@@ -1645,7 +1684,6 @@
"integrity": "sha512-+zEk16yGlz1F9STiRr6uG9hmIXb6nprjLczV/htGptYuLoCuxb+itZ03RKCEeOhBpDDd1NU7qF6x1VLMUp62bw==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"picomatch": "^4.0.4"
},
@@ -2121,7 +2159,6 @@
"integrity": "sha512-fRa09kZTgu8o71KFcDjUFuc7F+dEbZYZmkI0mg5YBTRs0yMKjYHsq/c0urDKeDb+D5qVgXOdFcuu+DZPKOITwA==",
"devOptional": true,
"license": "MIT",
"peer": true,
"dependencies": {
"undici-types": "~7.18.0"
}
@@ -2131,7 +2168,6 @@
"resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.17.tgz",
"integrity": "sha512-MXfmqaVPEVgkBT/aY0aGCkRWWtByiYQXo3xdQ8r5RzuFrPiRn8Gar2tQdXSUQ2GKV3bkXckek89V8wQBY2Q/Aw==",
"license": "MIT",
"peer": true,
"dependencies": {
"csstype": "^3.2.2"
}
@@ -2142,7 +2178,6 @@
"integrity": "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==",
"devOptional": true,
"license": "MIT",
"peer": true,
"peerDependencies": {
"@types/react": "^19.2.0"
}
@@ -2198,7 +2233,6 @@
"integrity": "sha512-PJ5vePq5/ognBbrIcoC5+SHO5dfpeLPzP9FpLkzWrguoYQEeeSjlJpVwOpo1JRSTEi7dRcwNy4h4dzV70PqHcg==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"@typescript-eslint/scope-manager": "8.61.1",
"@typescript-eslint/types": "8.61.1",
@@ -2429,7 +2463,6 @@
"integrity": "sha512-xRQbDb9BnwDafYNn6Vwl839DYVjqXYb1XVGtWAZ1kcDc6iwAL4hg3B1dZlRiuENFeO2H53gFG3in621AdERVAg==",
"dev": true,
"license": "MIT",
"peer": true,
"bin": {
"acorn": "bin/acorn"
},
@@ -2512,7 +2545,6 @@
"integrity": "sha512-Ixm8tFfoKKIPYdCCKYTsqv+Fd4IJ0DQqMyEimo+pxUOMUR9cVPlwTrFt9Avu+3cb6Zp3mAzl+t1MrG2fxxKsxw==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"@babel/types": "^7.26.0"
}
@@ -2573,7 +2605,6 @@
}
],
"license": "MIT",
"peer": true,
"dependencies": {
"baseline-browser-mapping": "^2.10.12",
"caniuse-lite": "^1.0.30001782",
@@ -3025,7 +3056,6 @@
"integrity": "sha512-1y+7C+vi12bUK1IpZeaV3gsH9fHLBmPvYmPx42pvT/E9yG0IC8g3PUZZgp0+JLJl7ZDK0flc2gc+Aw9dpCvIsQ==",
"dev": true,
"license": "MIT",
"peer": true,
"workspaces": [
"packages/*"
],
@@ -4289,7 +4319,6 @@
"resolved": "https://registry.npmjs.org/react/-/react-19.2.7.tgz",
"integrity": "sha512-HNe9WslTbXmFK8o8cmwgAeJFSBvt1bPdHCVKtaaV+WlAN36mpT4hcRpwbf3fY56ar2oIXzsBpOAiIRHAdY0OlQ==",
"license": "MIT",
"peer": true,
"engines": {
"node": ">=0.10.0"
}
@@ -4325,7 +4354,6 @@
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.7.tgz",
"integrity": "sha512-t0BRVXvbiE/o20Hfw669rLbMCDWtYZLvmJigy2f0MxsXF+71pxhR3xOkspmsO8h3ZlNzyibAmtCa3l4lYKk6gQ==",
"license": "MIT",
"peer": true,
"dependencies": {
"scheduler": "^0.27.0"
},
@@ -4344,7 +4372,6 @@
"resolved": "https://registry.npmjs.org/react-hook-form/-/react-hook-form-7.81.0.tgz",
"integrity": "sha512-ocbmr2p5KBMoAfj4WCUvped33lVi1Kd5DuDUvQDnB6VEAacOjPI/jMbtDdbhco4y9ct4xUuCmMY0b/C9L0QHjw==",
"license": "MIT",
"peer": true,
"engines": {
"node": ">=18.0.0"
},
@@ -4360,15 +4387,13 @@
"version": "19.2.7",
"resolved": "https://registry.npmjs.org/react-is/-/react-is-19.2.7.tgz",
"integrity": "sha512-kZFnouyVv7eP/Phmrlo9FK+zcAdriZJvzxXHF1Sl1P377WSGe2G/JxVolhTrB/jeV47lKImhNUsijjHAAbcl/A==",
"license": "MIT",
"peer": true
"license": "MIT"
},
"node_modules/react-redux": {
"version": "9.3.0",
"resolved": "https://registry.npmjs.org/react-redux/-/react-redux-9.3.0.tgz",
"integrity": "sha512-KQopgqFo/p/fgmAs5qz6p5RWaNAzq40WAu7fJIXnQpYxFPbJYtsJPWvGeF2rOBaY/kEuV77AVsX8TsQzKm+A/g==",
"license": "MIT",
"peer": true,
"dependencies": {
"@types/use-sync-external-store": "^0.0.6",
"use-sync-external-store": "^1.4.0"
@@ -4557,8 +4582,7 @@
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/redux/-/redux-5.0.1.tgz",
"integrity": "sha512-M9/ELqF6fy8FwmkpnF0S3YKOqMyoWJ4+CS5Efg2ct3oY9daQvd/Pc71FpGZsVsbl3Cpb+IIcjBDUnnyBdQbq4w==",
"license": "MIT",
"peer": true
"license": "MIT"
},
"node_modules/redux-thunk": {
"version": "3.1.0",
@@ -4822,7 +4846,6 @@
"integrity": "sha512-y2TvuxSZPDyQakkFRPZHKFm+KKVqIisdg9/CZwm9ftvKXLP8NRWj38/ODjNbr43SsoXqNuAisEf1GdCxqWcdBw==",
"dev": true,
"license": "Apache-2.0",
"peer": true,
"bin": {
"tsc": "bin/tsc",
"tsserver": "bin/tsserver"
@@ -4982,7 +5005,6 @@
"resolved": "https://registry.npmjs.org/vite/-/vite-8.0.16.tgz",
"integrity": "sha512-h9bXPmJichP5fLmVQo3PyaGSDE2n3aPuomeAlVRm0JLmt4rY6zmPKd59HYI4LNW8oTK7tlTsuC7l/m7awx9Jcw==",
"license": "MIT",
"peer": true,
"dependencies": {
"lightningcss": "^1.32.0",
"picomatch": "^4.0.4",
@@ -5463,7 +5485,6 @@
"integrity": "sha512-ytENFjIJFl2UwYglde2jchW2Hwm4GJFLDiSXWdTrJQBIN9Fcyp7n4DhxJEiWNAJMV1/BqWfW/kkg71UDcHJyTQ==",
"dev": true,
"license": "MIT",
"peer": true,
"funding": {
"url": "https://github.com/sponsors/colinhacks"
}
+9 -1
View File
@@ -1,5 +1,5 @@
import { useNavigate } from "react-router-dom";
import { Bell, ChevronDown, Building2, Globe, LogOut, Shield, User, Settings, CheckCircle2, ChevronRight, Menu, ShieldAlert } from "lucide-react";
import { Bell, ChevronDown, Building2, Globe, LogOut, Shield, User, Settings, CheckCircle2, ChevronRight, Menu, ShieldAlert, Home } from "lucide-react";
import { useLanguage, type Language } from "../../contexts/LanguageContext";
import { useHeader } from "../../contexts/HeaderContext";
import { useSidebar } from "../../contexts/SidebarContext";
@@ -173,6 +173,14 @@ export function Header() {
{/* Right Side Controls */}
<div className="flex items-center gap-2 sm:gap-4">
<button
onClick={() => window.location.assign(`${import.meta.env.VITE_SAAS_BASE_URL || 'http://localhost:3000'}/dashboard`)}
className="inline-flex items-center gap-2 px-3 py-1.5 rounded-lg text-sm font-semibold text-primary bg-primary/10 hover:bg-primary/15 transition-colors"
title="Return to SaaS home"
>
<Home className="w-4 h-4" />
<span className="hidden lg:inline">SaaS Home</span>
</button>
{/* Standard Non-Platform Tenant Badge */}
{!isPlatformUser && (
<div className="hidden sm:flex items-center gap-2 px-3 py-1.5 rounded-lg text-sm font-medium border border-border bg-surface-muted text-foreground">
@@ -1,95 +1,283 @@
import { useEffect, useState } from "react";
import { Copy, KeyRound, Plus, ShieldCheck, Trash2 } from "lucide-react";
import apiClient from "../../../api/axiosInstance";
import { useAppSelector } from "../../../store";
import { useEffect, useState } from 'react';
import { Copy, Check, KeyRound, Plus, ShieldCheck, Trash2, AlertTriangle, Key } from 'lucide-react';
import apiClient from '../../../api/axiosInstance';
import { useAppSelector } from '../../../store';
type ApiKeyRecord = {
id: string; name: string; prefix: string; scopes: string[]; status: string;
expiresAt: string; lastUsedAt?: string | null; createdAt: string; apiKey?: string;
id: string;
name: string;
prefix: string;
scopes: string[];
status: string;
expiresAt: string;
lastUsedAt?: string | null;
createdAt: string;
apiKey?: string;
};
type ApiResponse<T> = { success: boolean; data: T };
export default function ApiAccessTab() {
const user = useAppSelector((state) => state.auth.user);
const isPlatformUser = user?.type === "platform" || user?.user_type === "platform";
const isSupportMode = isPlatformUser && Boolean(localStorage.getItem("impersonatedTenantId"));
const isPlatformUser = user?.type === 'platform' || user?.user_type === 'platform';
const isSupportMode = isPlatformUser && Boolean(localStorage.getItem('impersonatedTenantId'));
const [keys, setKeys] = useState<ApiKeyRecord[]>([]);
const [name, setName] = useState("");
const [name, setName] = useState('');
const [expiresInDays, setExpiresInDays] = useState(90);
const [createdKey, setCreatedKey] = useState<ApiKeyRecord | null>(null);
const [copied, setCopied] = useState(false);
const [busy, setBusy] = useState(false);
const [error, setError] = useState("");
const [error, setError] = useState<string>('');
const load = async () => {
const response = await apiClient.get<ApiResponse<ApiKeyRecord[]>>("/api/v1/api-keys");
setKeys(response.data || []);
try {
const response: any = await apiClient.get('/api/v1/api-keys');
const rawList = Array.isArray(response) ? response : (Array.isArray(response?.data) ? response.data : []);
setKeys(rawList);
} catch (cause: any) {
const msg = typeof cause?.response?.data?.message === 'string'
? cause.response.data.message
: (typeof cause?.message === 'string' ? cause.message : 'Could not load API keys.');
setError(msg);
}
};
useEffect(() => {
if (!isSupportMode) load().catch((cause: any) => setError(cause?.response?.data?.message || "Could not load API keys."));
if (!isSupportMode) {
load();
}
}, [isSupportMode]);
if (isSupportMode) {
return (
<div className="rounded-lg border border-amber-300 bg-amber-50 p-5 text-sm text-amber-950">
<div className="flex items-center gap-2 font-semibold"><ShieldCheck className="h-4 w-4" />API-key management is protected in Support Mode</div>
<p className="mt-2">Platform support can inspect tenant data for troubleshooting, but cannot create, view, or revoke credentials on the tenant's behalf.</p>
<p className="mt-2">A real tenant administrator must sign in to this workspace and open <strong>Integration Hub API Access</strong> to manage product API keys.</p>
<div className="rounded-xl border border-amber-500/30 bg-amber-500/10 p-5 text-sm text-amber-200">
<div className="flex items-center gap-2 font-semibold"><ShieldCheck className="h-5 w-5 text-amber-400" />API-key management is protected in Support Mode</div>
<p className="mt-2 text-muted-foreground">Platform support can inspect tenant data for troubleshooting, but cannot create, view, or revoke credentials on the tenant's behalf.</p>
<p className="mt-2 text-muted-foreground">A real tenant administrator must sign in to this workspace and open <strong>Integration Hub → API Access</strong> to manage product API keys.</p>
</div>
);
}
const createKey = async () => {
setBusy(true); setError("");
setBusy(true);
setError('');
try {
const response = await apiClient.post<ApiResponse<ApiKeyRecord>>("/api/v1/api-keys", { name, expiresInDays });
setCreatedKey(response.data); setName(""); await load();
const response: any = await apiClient.post('/api/v1/api-keys', { name, expiresInDays });
const record = response?.data || response;
if (record && typeof record === 'object' && !Array.isArray(record)) {
setCreatedKey(record);
}
setName('');
setCopied(false);
await load();
} catch (cause: any) {
setError(cause?.response?.data?.message || "Could not create API key.");
} finally { setBusy(false); }
const msg = typeof cause?.response?.data?.message === 'string'
? cause.response.data.message
: (typeof cause?.message === 'string' ? cause.message : 'Could not create API key.');
setError(msg);
} finally {
setBusy(false);
}
};
const handleCopy = (text: string) => {
if (!text) return;
navigator.clipboard.writeText(text);
setCopied(true);
setTimeout(() => setCopied(false), 3000);
};
const revoke = async (id: string) => {
if (!window.confirm("Revoke this API key? Applications using it will immediately lose access.")) return;
await apiClient.delete(`/api/v1/api-keys/${id}`); await load();
if (!window.confirm('Revoke this API key? Applications using it will immediately lose access.')) return;
try {
await apiClient.delete('/api/v1/api-keys/' + id);
await load();
} catch (cause: any) {
const msg = typeof cause?.response?.data?.message === 'string'
? cause.response.data.message
: (typeof cause?.message === 'string' ? cause.message : 'Could not revoke API key.');
setError(msg);
}
};
return (
<div className="space-y-6">
<div className="rounded-lg border border-blue-200 bg-blue-50 p-4 text-sm text-blue-900">
<div className="flex items-center gap-2 font-semibold"><ShieldCheck className="h-4 w-4" />What this key can do</div>
<p className="mt-2">It can only read products belonging to your tenant through <code>GET /api/v1/external/products</code>. It cannot create, edit, delete, publish, or access another tenant.</p>
</div>
<div className="rounded-lg border border-border p-4">
<h3 className="font-semibold text-foreground">Create a read-only product key</h3>
<div className="mt-3 flex flex-wrap gap-3">
<input aria-label="API key name" value={name} onChange={(event) => setName(event.target.value)} placeholder="Example: Website product reader" maxLength={120} className="min-w-64 rounded-lg border border-border bg-surface px-3 py-2 text-sm" />
<select aria-label="API key expiry" value={expiresInDays} onChange={(event) => setExpiresInDays(Number(event.target.value))} className="rounded-lg border border-border bg-surface px-3 py-2 text-sm">
<option value={30}>30 days</option><option value={90}>90 days</option><option value={180}>180 days</option><option value={365}>365 days</option>
</select>
<button disabled={busy || !name.trim()} onClick={createKey} className="inline-flex items-center gap-2 rounded-lg bg-primary px-4 py-2 text-sm font-medium text-white disabled:opacity-50"><Plus className="h-4 w-4" />Create key</button>
{/* Information Banner */}
<div className="rounded-xl border border-blue-500/20 bg-blue-500/10 p-4 text-sm text-blue-300">
<div className="flex items-center gap-2 font-semibold text-blue-200">
<ShieldCheck className="h-4 w-4 text-blue-400" />
What this key can do
</div>
{error && <p className="mt-2 text-sm text-red-600">{error}</p>}
<p className="mt-1 text-xs text-blue-300/80 leading-relaxed">
Keys generated here grant read-only access to your tenant catalog via <code>GET /api/v1/external/products</code>. They are strictly isolated to your tenant data.
</p>
</div>
{createdKey?.apiKey && (
<div className="rounded-lg border-2 border-amber-300 bg-amber-50 p-4">
<p className="font-semibold text-amber-950">Copy this key now it will never be shown again.</p>
<div className="mt-3 flex gap-2"><code className="min-w-0 flex-1 overflow-x-auto rounded bg-white p-3 text-sm">{createdKey.apiKey}</code><button aria-label="Copy API key" onClick={() => navigator.clipboard.writeText(createdKey.apiKey!)} className="rounded-lg border border-amber-300 bg-white px-3"><Copy className="h-4 w-4" /></button></div>
<button onClick={() => setCreatedKey(null)} className="mt-3 text-sm font-medium text-amber-900 underline">I have saved it securely</button>
{/* Creation Form */}
<div className="rounded-xl border border-border/60 bg-card p-5 shadow-sm">
<h3 className="font-semibold text-foreground text-sm flex items-center gap-2">
<Key className="h-4 w-4 text-primary" />
Generate New API Key
</h3>
<p className="text-xs text-muted-foreground mt-0.5">Create secure tokens for external integrations like Inventory, Mobile Apps, or Storefronts.</p>
<div className="mt-4 flex flex-wrap items-center gap-3">
<input
aria-label="API key name"
value={name}
onChange={(event) => setName(event.target.value)}
placeholder="e.g., Inventory Sync Integration"
maxLength={120}
className="min-w-[280px] flex-1 rounded-lg border border-border/80 bg-background px-3.5 py-2 text-sm text-foreground placeholder:text-muted-foreground focus:outline-none focus:ring-2 focus:ring-primary/40"
/>
<select
aria-label="API key expiry"
value={expiresInDays}
onChange={(event) => setExpiresInDays(Number(event.target.value))}
className="rounded-lg border border-border/80 bg-background px-3.5 py-2 text-sm text-foreground focus:outline-none focus:ring-2 focus:ring-primary/40"
>
<option value={30}>Expires in 30 days</option>
<option value={90}>Expires in 90 days</option>
<option value={180}>Expires in 180 days</option>
<option value={365}>Expires in 1 year</option>
</select>
<button
disabled={busy || !name.trim()}
onClick={createKey}
className="inline-flex items-center gap-2 rounded-lg bg-primary hover:bg-primary/90 px-4 py-2 text-sm font-medium text-white transition-all disabled:opacity-50 shadow-sm"
>
<Plus className="h-4 w-4" />
Generate Key
</button>
</div>
{error ? <p className="mt-2 text-xs font-medium text-red-400">{String(error)}</p> : null}
</div>
{/* Pop-up / Reveal Card when key is created */}
{createdKey && typeof createdKey.apiKey === 'string' && (
<div className="rounded-xl border border-amber-500/40 bg-amber-950/30 p-5 backdrop-blur-sm shadow-md animate-in fade-in duration-200">
<div className="flex items-start gap-2.5">
<AlertTriangle className="h-5 w-5 text-amber-400 shrink-0 mt-0.5" />
<div className="flex-1 min-w-0">
<h4 className="text-sm font-semibold text-amber-200">Save this API key immediately</h4>
<p className="text-xs text-amber-300/80 mt-0.5">For security reasons, this secret key will <strong>never be shown again</strong>. Copy and store it securely.</p>
<div className="mt-3 flex items-center gap-2">
<div className="flex-1 min-w-0 overflow-x-auto rounded-lg border border-amber-500/30 bg-black/40 px-3.5 py-2.5 text-xs font-mono text-amber-100 selection:bg-amber-500 selection:text-black">
{createdKey.apiKey}
</div>
<button
type="button"
onClick={() => handleCopy(createdKey.apiKey!)}
className={copied ? 'inline-flex items-center gap-1.5 rounded-lg px-3.5 py-2.5 text-xs font-medium transition-all shadow-sm shrink-0 bg-emerald-600 text-white' : 'inline-flex items-center gap-1.5 rounded-lg px-3.5 py-2.5 text-xs font-medium transition-all shadow-sm shrink-0 bg-amber-500 hover:bg-amber-400 text-black'}
>
{copied ? (
<>
<Check className="h-4 w-4" />
Copied!
</>
) : (
<>
<Copy className="h-4 w-4" />
Copy Key
</>
)}
</button>
</div>
<div className="mt-3 flex justify-end">
<button
onClick={() => setCreatedKey(null)}
className="text-xs font-medium text-amber-400 hover:text-amber-300 underline underline-offset-2 transition-colors"
>
I have saved it securely (Dismiss)
</button>
</div>
</div>
</div>
</div>
)}
<div className="overflow-hidden rounded-lg border border-border">
<table className="w-full text-left text-sm"><thead className="bg-surface-muted"><tr><th className="p-3">Name</th><th className="p-3">Prefix</th><th className="p-3">Scope</th><th className="p-3">Expires</th><th className="p-3">Last used</th><th className="p-3">Status</th><th className="p-3"></th></tr></thead>
<tbody>{keys.map((key) => <tr key={key.id} className="border-t border-border"><td className="p-3 font-medium"><span className="inline-flex items-center gap-2"><KeyRound className="h-4 w-4" />{key.name}</span></td><td className="p-3 font-mono text-xs">{key.prefix}</td><td className="p-3">products:read</td><td className="p-3">{new Date(key.expiresAt).toLocaleDateString()}</td><td className="p-3">{key.lastUsedAt ? new Date(key.lastUsedAt).toLocaleString() : "Never"}</td><td className="p-3 capitalize">{key.status}</td><td className="p-3 text-right">{key.status === "active" && <button aria-label={`Revoke ${key.name}`} onClick={() => revoke(key.id)} className="text-red-600"><Trash2 className="h-4 w-4" /></button>}</td></tr>)}</tbody>
</table>
{keys.length === 0 && <p className="p-6 text-center text-muted-foreground">No API keys yet.</p>}
{/* Active Keys Table */}
<div className="overflow-hidden rounded-xl border border-border/60 bg-card shadow-sm">
<div className="px-5 py-3.5 border-b border-border/60 bg-muted/30 flex items-center justify-between">
<h4 className="text-xs font-semibold uppercase tracking-wider text-muted-foreground">Active API Keys</h4>
<span className="text-xs text-muted-foreground">{keys.length} key{keys.length === 1 ? '' : 's'}</span>
</div>
<div className="overflow-x-auto">
<table className="w-full text-left text-sm">
<thead className="bg-muted/40 text-xs font-medium text-muted-foreground border-b border-border/60">
<tr>
<th className="py-3 px-5">Key Name</th>
<th className="py-3 px-5">Key Prefix</th>
<th className="py-3 px-5">Scope</th>
<th className="py-3 px-5">Expires</th>
<th className="py-3 px-5">Last Used</th>
<th className="py-3 px-5">Status</th>
<th className="py-3 px-5 text-right">Action</th>
</tr>
</thead>
<tbody className="divide-y divide-border/40">
{keys.map((key) => (
<tr key={key.id} className="hover:bg-muted/20 transition-colors">
<td className="py-3 px-5 font-medium text-foreground">
<span className="inline-flex items-center gap-2">
<KeyRound className="h-3.5 w-3.5 text-primary" />
{String(key.name || '')}
</span>
</td>
<td className="py-3 px-5 font-mono text-xs text-muted-foreground">
<span className="bg-muted/50 px-2 py-0.5 rounded border border-border/40">
{String(key.prefix || '')}…
</span>
</td>
<td className="py-3 px-5 text-xs text-muted-foreground">
<span className="bg-primary/10 text-primary px-2 py-0.5 rounded-full font-medium">
products:read
</span>
</td>
<td className="py-3 px-5 text-xs text-muted-foreground">
{key.expiresAt ? new Date(key.expiresAt).toLocaleDateString() : ''}
</td>
<td className="py-3 px-5 text-xs text-muted-foreground">
{key.lastUsedAt ? new Date(key.lastUsedAt).toLocaleString() : 'Never'}
</td>
<td className="py-3 px-5 text-xs">
<span className="inline-flex items-center gap-1.5 font-medium text-emerald-400 bg-emerald-500/10 px-2 py-0.5 rounded-full capitalize">
<span className="h-1.5 w-1.5 rounded-full bg-emerald-400" />
{String(key.status || 'active')}
</span>
</td>
<td className="py-3 px-5 text-right">
{key.status === 'active' && (
<button
title="Revoke key"
aria-label={'Revoke ' + String(key.name || '')}
onClick={() => revoke(key.id)}
className="inline-flex items-center justify-center h-8 w-8 rounded-lg text-muted-foreground hover:text-red-400 hover:bg-red-500/10 transition-colors"
>
<Trash2 className="h-4 w-4" />
</button>
)}
</td>
</tr>
))}
</tbody>
</table>
</div>
{keys.length === 0 && (
<div className="p-8 text-center">
<KeyRound className="h-8 w-8 text-muted-foreground/40 mx-auto mb-2" />
<p className="text-sm text-muted-foreground">No API keys generated yet.</p>
</div>
)}
</div>
<div className="rounded-lg border border-border p-4 text-sm"><h3 className="font-semibold">Example request</h3><pre className="mt-2 overflow-x-auto rounded bg-slate-950 p-3 text-slate-100">{`curl https://YOUR-PIM/api/v1/external/products \\\n -H "X-API-Key: pim_live_your_key"`}</pre></div>
{/* Code Snippet */}
<div className="rounded-xl border border-border/60 bg-card p-5 text-sm shadow-sm">
<h4 className="font-semibold text-foreground text-xs uppercase tracking-wider text-muted-foreground mb-2">Example Usage</h4>
<pre className="overflow-x-auto rounded-lg bg-black/60 border border-border/40 p-3.5 text-xs font-mono text-emerald-400">
curl http://localhost:5002/api/v1/external/products -H &quot;X-API-Key: pim_live_your_key_here&quot;
</pre>
</div>
</div>
);
}
@@ -1,5 +1,5 @@
import React, { useState } from 'react';
import { Layers, RefreshCw, Save, Check, Plus, ArrowRight } from 'lucide-react';
import { useState } from 'react';
import { Layers, RefreshCw, Save, Plus, ArrowRight } from 'lucide-react';
import { notify } from '../../../services/toast/index';
interface MappingRow {
@@ -77,7 +77,7 @@ export default function FieldMappingsTab() {
<tr className="bg-background border-b border-border text-muted-foreground font-semibold">
<th className="py-3 px-4">Canonical Source Path (PIM)</th>
<th className="py-3 px-2 text-center">Transform</th>
<th className="py-3 px-4">Channel Target Path (Shopify)</th>
<th className="py-3 px-4">Destination Field / JSON Path</th>
<th className="py-3 px-4">Transformation Type</th>
<th className="py-3 px-4">Default Value</th>
<th className="py-3 px-4 text-center">Required</th>
@@ -100,7 +100,7 @@ export const IntegrationTemplateGallery: React.FC<IntegrationTemplateGalleryProp
</button>
</div>
{/* Custom API Card */}
{/* Outbound Custom API Card */}
<div className="bg-surface border border-border hover:border-primary/50 rounded-xl p-4 transition-all shadow-2xs group flex flex-col justify-between">
<div>
<div className="flex items-center justify-between mb-3">
@@ -111,9 +111,9 @@ export const IntegrationTemplateGallery: React.FC<IntegrationTemplateGalleryProp
Custom Wizard
</span>
</div>
<h3 className="font-bold text-foreground text-sm group-hover:text-primary transition-colors">Custom API / Webhook</h3>
<h3 className="font-bold text-foreground text-sm group-hover:text-primary transition-colors">External API / Webhook</h3>
<p className="text-xs text-muted-foreground mt-1 line-clamp-2">
Configure multi-step generic REST/GraphQL endpoints with custom headers and transformations.
Send PIM products outward to a website or application endpoint using its credentials.
</p>
</div>
<button
@@ -121,7 +121,7 @@ export const IntegrationTemplateGallery: React.FC<IntegrationTemplateGalleryProp
onClick={onSelectCustomApi}
className="mt-4 w-full py-2 px-3 bg-surface border border-border hover:bg-background text-foreground rounded-lg text-xs font-semibold shadow-2xs flex items-center justify-center gap-1.5 cursor-pointer transition-colors"
>
Custom Wizard <ArrowRight className="w-3.5 h-3.5 text-muted-foreground" />
Configure Outbound API <ArrowRight className="w-3.5 h-3.5 text-muted-foreground" />
</button>
</div>
</div>
@@ -1,5 +1,5 @@
import React, { useState, useEffect } from 'react';
import { X, ShoppingCart, Zap, Key, Globe, Loader2, CheckCircle2, AlertCircle, Eye, EyeOff, ExternalLink, ArrowLeft } from 'lucide-react';
import { X, ShoppingCart, Zap, Globe, Loader2, CheckCircle2, AlertCircle, Eye, EyeOff, ExternalLink, ArrowLeft } from 'lucide-react';
import { useIntegration } from '../hook/useIntegration';
import { integrationsService } from '../services/integrations.service';
import { useSearchParams } from 'react-router-dom';
@@ -77,7 +77,7 @@ export const ShopifyTemplateModal: React.FC<ShopifyTemplateModalProps> = ({
const created = await createItem({
name,
channel: 'shopify',
integration_type: 'ecommerce',
integrationType: 'shopify',
sync_mode: syncMode,
sync_frequency: syncMode === 'auto' ? 'realtime' : 'manual',
status: 'pending'
@@ -1,27 +1,10 @@
import { useEffect, useState } from "react";
import { RefreshCw, Download, Layers } from "lucide-react";
import { RefreshCw } from "lucide-react";
import { DataTable } from "../../../components/customs/DataTable";
import { StatusBadge, type BadgeVariant } from "../../../components/customs/StatusBadge";
import { Button } from "../../../components/customs/Button";
import { useIntegration } from "../hook/useIntegration";
import { SyncJobStatusModal } from "./SyncJobStatusModal";
import type { SyncJob } from "../types/integrations.types";
const MOCK_JOBS = [
{
id: "JOB-2892",
integration: "Shopify Main Store",
type: "Realtime Sync",
records: "128",
success: "128",
failed: "0",
status: "Completed",
started: "2026-08-30 04:30",
completed: "2026-08-30 04:31",
duration: "1m 02s",
triggered: "Outbox Trigger"
}
];
export default function SyncJobsList() {
const [jobs, setJobs] = useState<any[]>([]);
@@ -27,7 +27,6 @@ import { useChannel } from "../../channels/hook/useChannel";
import type { Integration } from "../types/integrations.types";
import { ConfirmationModal } from "../../../components/modals/ConfirmationModal";
import { StatsCard } from "../../../components/customs/StatsCard";
import { channelsApi } from "../../channels/api/channels.api";
// Import Custom Feature Components & Pre-Built Templates
import { IntegrationHealthBadge } from "../components/IntegrationHealthBadge";
@@ -57,7 +56,7 @@ const INTEGRATION_META: Record<string, { label: string; icon: any; color: string
export default function IntegrationList() {
const navigate = useNavigate();
const [searchParams] = useSearchParams();
const [activeTab, setActiveTab] = useState<"Connections" | "Field Mappings" | "Publishing Rules" | "Sync Jobs" | "Error Center" | "Audit & Logs">("Connections");
const [activeTab, setActiveTab] = useState<"Connections" | "Field Mappings" | "Publishing Rules" | "Sync Jobs" | "Error Center" | "Audit & Logs" | "API Access">("Connections");
const [statusFilter, setStatusFilter] = useState("All Status");
const [typeFilter, setTypeFilter] = useState("All Types");
@@ -78,9 +77,6 @@ export default function IntegrationList() {
useEffect(() => {
fetchItems();
fetchChannels();
Promise.all([channelsApi.getAllJobs(), channelsApi.getErrors()])
.then(([jobs, errors]) => setOperations({ jobs, errors }))
.catch(() => setOperations({ jobs: [], errors: [] }));
}, [fetchItems, fetchChannels]);
// Automatically open Shopify modal into Step 3 when returning from OAuth redirect
@@ -132,8 +128,8 @@ export default function IntegrationList() {
label: "Integration Name",
sortable: true,
render: (_: any, row: Integration) => {
const metaType = row.integrationType || 'ecommerce';
const meta = INTEGRATION_META[metaType] || INTEGRATION_META.ecommerce;
const metaType = row.integrationType || 'custom_api';
const meta = INTEGRATION_META[metaType] || INTEGRATION_META.custom_api;
const Icon = meta.icon;
return (
<div className="flex items-center gap-3">
@@ -153,7 +149,7 @@ export default function IntegrationList() {
label: "Channel",
render: (val: string) => {
const chan = channels.find(c => c.code === val || c.id === val);
return <span className="font-semibold text-xs uppercase px-2 py-0.5 rounded bg-surface border border-border">{chan ? chan.name : val || 'Shopify'}</span>;
return <span className="font-semibold text-xs uppercase px-2 py-0.5 rounded bg-surface border border-border">{chan ? chan.name : val || 'Not assigned'}</span>;
}
},
{
@@ -207,8 +203,10 @@ export default function IntegrationList() {
</button>
<button
type="button"
onClick={() => setCredentialModal({ isOpen: true, integrationId: row.id, name: row.name })}
title="Configure Credentials"
onClick={() => row.integrationType === 'shopify'
? setCredentialModal({ isOpen: true, integrationId: row.id, name: row.name })
: navigate(`${row.id}/edit`)}
title={row.integrationType === 'shopify' ? "Configure Shopify Credentials" : "Configure API Endpoint & Credentials"}
className="p-1.5 rounded-lg border border-border bg-surface hover:bg-background text-foreground cursor-pointer"
>
<Key className="w-3.5 h-3.5 text-muted-foreground" />
@@ -249,6 +247,9 @@ export default function IntegrationList() {
<RefreshCw className={`w-4 h-4 mr-2 ${loading ? 'animate-spin' : ''}`} />
Refresh
</Button>
<Button variant="outline" onClick={() => setActiveTab("API Access")}>
<Key className="w-4 h-4 mr-2" />Generate PIM API Key
</Button>
<Button onClick={() => setShopifyModalOpen(true)} className="bg-emerald-600 hover:bg-emerald-700 text-white">
<Plus className="w-4 h-4 mr-2" />Setup Shopify
</Button>
@@ -257,9 +258,9 @@ export default function IntegrationList() {
/>
{/* Quick Pre-Built Template Gallery */}
<IntegrationTemplateGallery
onSelectShopify={() => setShopifyModalOpen(true)}
onSelectCustomApi={() => navigate("new")}
<IntegrationTemplateGallery
onSelectShopify={() => setShopifyModalOpen(true)}
onSelectCustomApi={() => navigate("new?type=custom_api")}
/>
{/* Stats Cards */}
@@ -303,7 +304,8 @@ export default function IntegrationList() {
{ id: "Publishing Rules", count: 1 },
{ id: "Sync Jobs", count: null },
{ id: "Error Center", count: 0 },
{ id: "Audit & Logs", count: null }
{ id: "Audit & Logs", count: null },
{ id: "API Access", count: null }
].map(tab => (
<button
key={tab.id}
@@ -351,6 +353,15 @@ export default function IntegrationList() {
<option>Pending</option>
<option>Disconnected</option>
</select>
<select
className="h-9 px-3 py-1.5 text-sm border border-border rounded-lg focus:outline-none focus:ring-2 focus:ring-primary text-foreground bg-surface"
value={typeFilter}
onChange={(e) => setTypeFilter(e.target.value)}
>
<option>All Types</option>
<option value="custom_api">Custom API</option>
<option value="shopify">Shopify</option>
</select>
</div>
}
/>
@@ -361,6 +372,7 @@ export default function IntegrationList() {
{activeTab === "Sync Jobs" && <SyncJobsList />}
{activeTab === "Error Center" && <ErrorCenterList />}
{activeTab === "Audit & Logs" && <AuditLogsList />}
{activeTab === "API Access" && <ApiAccessTab />}
</div>
</div>
@@ -1,5 +1,5 @@
import { useState, useEffect } from "react";
import { useNavigate, useParams, useLocation } from "react-router-dom";
import { useNavigate, useParams, useLocation, useSearchParams } from "react-router-dom";
import { useFormik } from "formik";
import * as Yup from "yup";
import {
@@ -90,8 +90,8 @@ const integrationSchema = Yup.object({
storeUrl: Yup.string().when('integrationType', { is: 'shopify', then: schema => schema.required('Permanent .myshopify.com domain is required').matches(/^https:\/\/[a-z0-9][a-z0-9-]*\.myshopify\.com\/?$/i, 'Use https://your-store.myshopify.com') }),
clientId: Yup.string().when('integrationType', { is: 'shopify', then: schema => schema.required('Shopify Client ID is required') }),
clientSecret: Yup.string(),
endpoint: Yup.string().when('integrationType', { is: 'custom_api', then: schema => schema.required('Delivery endpoint is required').url('Enter a complete HTTPS URL').matches(/^https:\/\//i, 'External endpoints must use HTTPS') }),
testEndpoint: Yup.string().url('Enter a complete URL').matches(/^(https:\/\/|$)/i, 'External endpoints must use HTTPS'),
endpoint: Yup.string().when('integrationType', { is: 'custom_api', then: schema => schema.required('Delivery endpoint is required').url('Enter a valid URL').matches(/^(https:\/\/|http:\/\/(localhost|127\.0\.0\.1))/i, 'Endpoints must use HTTPS (or http://localhost for local testing)') }),
testEndpoint: Yup.string().url('Enter a valid URL').matches(/^(https:\/\/|http:\/\/(localhost|127\.0\.0\.1)|$)/i, 'Endpoints must use HTTPS (or http://localhost for local testing)'),
customApiHeaderName: Yup.string().matches(/^[A-Za-z0-9-]*$/, 'Header names may contain letters, numbers and hyphens only'),
});
@@ -99,8 +99,10 @@ export default function NewIntegration() {
const navigate = useNavigate();
const { id } = useParams<{ id?: string }>();
const { pathname } = useLocation();
const [searchParams] = useSearchParams();
const isView = pathname.includes('/view');
const isEdit = Boolean(id) && !isView;
const requestedType = searchParams.get('type') === 'shopify' ? 'shopify' : 'custom_api';
const [activeStep, setActiveStep] = useState('general');
const [showToken, setShowToken] = useState(false);
const [loadingItem, setLoadingItem] = useState(false);
@@ -118,9 +120,9 @@ export default function NewIntegration() {
initialValues: {
name: "",
channel: "",
integrationType: "custom_api",
integrationType: requestedType,
environment: "production",
status: "Connected",
status: "Pending",
description: "",
storeUrl: "",
accessToken: "",
@@ -205,8 +207,8 @@ export default function NewIntegration() {
formik.setValues({
name: item.name,
channel: item.channel,
integrationType: item.integrationType,
environment: item.environment,
integrationType: item.integrationType || "custom_api",
environment: item.environment || "production",
status: item.status,
description: item.description || "",
storeUrl: item.storeUrl || "",
@@ -214,10 +216,10 @@ export default function NewIntegration() {
apiVersion: item.apiVersion || "2026-07",
webhookSecret: item.webhookSecret || "",
shopIdentifier: item.shopIdentifier || "",
syncDirection: item.syncDirection,
syncFrequency: item.syncFrequency,
autoRetry: item.autoRetry,
retryAttempts: item.retryAttempts,
syncDirection: item.syncDirection || "pim_to_channel",
syncFrequency: item.syncFrequency || "manual",
autoRetry: item.autoRetry ?? true,
retryAttempts: item.retryAttempts ?? 3,
// Other credentials
sellerId: item.sellerId || "",
@@ -260,7 +262,7 @@ export default function NewIntegration() {
setConnectionMessage("");
try {
const result = await integrationsService.testConnection(id);
setConnectionMessage(result.message);
setConnectionMessage(result.message || (result.connected ? "Connection successful" : "Connection failed"));
} catch (error: any) {
setConnectionMessage(error?.response?.data?.message || "Connection test failed");
} finally {
@@ -9,83 +9,87 @@ type ApiResponse<T> = {
export const integrationService = {
getAll: async (params?: Record<string, any>): Promise<Integration[]> => {
const res = await apiClient.get<ApiResponse<Integration[]>>('/api/v1/integrations', { params });
const raw: any = res.data;
const res = await apiClient.get<ApiResponse<Integration[]>>('/api/v1/integrations', params);
const raw: any = res;
return (Array.isArray(raw) ? raw : raw?.data || []) as Integration[];
},
getById: async (id: string): Promise<Integration> => {
const res = await apiClient.get<ApiResponse<Integration>>();
const raw: any = res.data;
const res = await apiClient.get<ApiResponse<Integration>>('/api/v1/integrations/' + id);
const raw: any = res;
return (raw?.data || raw) as Integration;
},
create: async (data: IntegrationCreateRequest): Promise<Integration> => {
const res = await apiClient.post<ApiResponse<Integration>>('/api/v1/integrations', data);
const raw: any = res.data;
const raw: any = res;
return (raw?.data || raw) as Integration;
},
update: async (id: string, data: IntegrationUpdateRequest): Promise<Integration> => {
const res = await apiClient.put<ApiResponse<Integration>>(, data);
const raw: any = res.data;
const res = await apiClient.put<ApiResponse<Integration>>('/api/v1/integrations/' + id, data);
const raw: any = res;
return (raw?.data || raw) as Integration;
},
delete: async (id: string): Promise<boolean> => {
const res = await apiClient.delete<ApiResponse<any>>();
return res.success || true;
const res = await apiClient.delete<ApiResponse<any>>('/api/v1/integrations/' + id);
const raw: any = res;
return raw?.success !== undefined ? raw.success : true;
},
getCredentials: async (id: string): Promise<Record<string, string>> => {
const res = await apiClient.get<ApiResponse<Record<string, string>>>();
const raw: any = res.data;
const res = await apiClient.get<ApiResponse<Record<string, string>>>('/api/v1/integrations/' + id + '/credentials');
const raw: any = res;
return (raw?.data || raw) as Record<string, string>;
},
setCredentials: async (id: string, credentialType: string, secretValue: string, expiresAt?: string): Promise<any> => {
const res = await apiClient.post<ApiResponse<any>>(, {
const res = await apiClient.post<ApiResponse<any>>('/api/v1/integrations/' + id + '/credentials', {
credential_type: credentialType,
secret_value: secretValue,
expires_at: expiresAt
});
const raw: any = res.data;
const raw: any = res;
return raw?.data || raw;
},
testConnection: async (id: string): Promise<TestConnectionResult> => {
const res = await apiClient.post<ApiResponse<TestConnectionResult>>();
const raw: any = res.data;
const res = await apiClient.post<ApiResponse<TestConnectionResult>>('/api/v1/integrations/' + id + '/test-connection');
const raw: any = res;
return (raw?.data || raw) as TestConnectionResult;
},
triggerSync: async (id: string, options?: { productId?: string; productIds?: string[] }): Promise<any> => {
const res = await apiClient.post<ApiResponse<any>>(, options || {});
const raw: any = res.data;
const res = await apiClient.post<ApiResponse<any>>('/api/v1/integrations/' + id + '/sync', options || {});
const raw: any = res;
return raw?.data || raw;
},
getSyncJobs: async (id: string): Promise<SyncJob[]> => {
const res = await apiClient.get<ApiResponse<SyncJob[]>>();
const raw: any = res.data;
const res = await apiClient.get<ApiResponse<SyncJob[]>>('/api/v1/integrations/' + id + '/jobs');
const raw: any = res;
return (Array.isArray(raw) ? raw : raw?.data || []) as SyncJob[];
},
getAllSyncJobs: async (): Promise<SyncJob[]> => {
const res = await apiClient.get<ApiResponse<SyncJob[]>>();
const raw: any = res.data;
const res = await apiClient.get<ApiResponse<SyncJob[]>>('/api/v1/integrations/jobs/all');
const raw: any = res;
return (Array.isArray(raw) ? raw : raw?.data || []) as SyncJob[];
},
getSyncItems: async (jobId: string): Promise<SyncItem[]> => {
const res = await apiClient.get<ApiResponse<SyncItem[]>>();
const raw: any = res.data;
const res = await apiClient.get<ApiResponse<SyncItem[]>>('/api/v1/integrations/jobs/' + jobId + '/items');
const raw: any = res;
return (Array.isArray(raw) ? raw : raw?.data || []) as SyncItem[];
},
startShopifyOAuth: async (id: string): Promise<{ authorizationUrl: string; state: string }> => {
const res = await apiClient.post<ApiResponse<any>>();
const raw: any = res.data;
const res = await apiClient.post<ApiResponse<any>>('/api/v1/integrations/' + id + '/shopify/oauth/start');
const raw: any = res;
return raw?.data || raw;
}
};
export const integrationsService = integrationService;
export default integrationService;