@jytextiles/medusa-plugin-faire-store-sync 0.2.2 → 0.2.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.medusa/server/src/admin/index.js +189 -281
- package/.medusa/server/src/admin/index.mjs +190 -282
- package/.medusa/server/src/lib/crypto.js +84 -0
- package/.medusa/server/src/lib/faire-client.js +25 -1
- package/.medusa/server/src/modules/faire-sync/service.js +51 -10
- package/README.md +21 -19
- package/package.json +1 -1
- package/src/admin/routes/settings/faire/bulk/page.tsx +55 -0
- package/src/admin/routes/settings/faire/page.tsx +7 -172
- package/src/lib/crypto.ts +84 -0
- package/src/lib/faire-client.ts +24 -0
- package/src/modules/faire-sync/service.ts +57 -9
|
@@ -187,6 +187,13 @@ const FaireSettingsPage = () => {
|
|
|
187
187
|
{connected ? (
|
|
188
188
|
<div className="flex items-center gap-3">
|
|
189
189
|
<StatusBadge color="green">Connected</StatusBadge>
|
|
190
|
+
<Button
|
|
191
|
+
size="small"
|
|
192
|
+
variant="secondary"
|
|
193
|
+
onClick={() => navigate("/settings/faire/bulk")}
|
|
194
|
+
>
|
|
195
|
+
Bulk sync
|
|
196
|
+
</Button>
|
|
190
197
|
<Button
|
|
191
198
|
size="small"
|
|
192
199
|
variant="secondary"
|
|
@@ -230,9 +237,6 @@ const FaireSettingsPage = () => {
|
|
|
230
237
|
)}
|
|
231
238
|
</Container>
|
|
232
239
|
|
|
233
|
-
{/* Bulk operations */}
|
|
234
|
-
<BulkOperations connected={connected} />
|
|
235
|
-
|
|
236
240
|
{/* Recent syncs */}
|
|
237
241
|
<Container className="divide-y p-0">
|
|
238
242
|
<DataTable instance={table}>
|
|
@@ -272,175 +276,6 @@ const InfoField = ({ label, value }: { label: string; value: string }) => (
|
|
|
272
276
|
</div>
|
|
273
277
|
)
|
|
274
278
|
|
|
275
|
-
/**
|
|
276
|
-
* Bulk operations card: push many products to Faire, and pull Faire orders into
|
|
277
|
-
* Medusa. Both run as long-running background workflows; progress is polled.
|
|
278
|
-
*/
|
|
279
|
-
const BulkOperations = ({ connected }: { connected: boolean }) => {
|
|
280
|
-
const [productIds, setProductIds] = useState("")
|
|
281
|
-
const [pushBatch, setPushBatch] = useState<string | null>(null)
|
|
282
|
-
const [pullBatch, setPullBatch] = useState<string | null>(null)
|
|
283
|
-
|
|
284
|
-
const pushMutation = useMutation({
|
|
285
|
-
mutationFn: (ids: string[]) => faireApi.syncBulk(ids),
|
|
286
|
-
onSuccess: (res: any) => {
|
|
287
|
-
setPushBatch(res.batch_id)
|
|
288
|
-
toast.success("Bulk product sync started", {
|
|
289
|
-
description: "Running in the background. Polling progress…",
|
|
290
|
-
})
|
|
291
|
-
},
|
|
292
|
-
onError: (err: any) =>
|
|
293
|
-
toast.error("Failed to start bulk sync", { description: err.message }),
|
|
294
|
-
})
|
|
295
|
-
|
|
296
|
-
const pullMutation = useMutation({
|
|
297
|
-
mutationFn: () => faireApi.ingestOrders(),
|
|
298
|
-
onSuccess: (res: any) => {
|
|
299
|
-
setPullBatch(res.batch_id)
|
|
300
|
-
toast.success("Faire order pull started", {
|
|
301
|
-
description: "Running in the background. Polling progress…",
|
|
302
|
-
})
|
|
303
|
-
},
|
|
304
|
-
onError: (err: any) =>
|
|
305
|
-
toast.error("Failed to start order pull", { description: err.message }),
|
|
306
|
-
})
|
|
307
|
-
|
|
308
|
-
// Poll both batches until finished.
|
|
309
|
-
const pushStatus = useQuery({
|
|
310
|
-
queryKey: ["faire", "bulk", pushBatch],
|
|
311
|
-
enabled: !!pushBatch,
|
|
312
|
-
refetchInterval: (q) =>
|
|
313
|
-
(q.state.data as any)?.progress?.finished ? false : 3000,
|
|
314
|
-
queryFn: () => faireApi.bulkStatus(pushBatch!),
|
|
315
|
-
})
|
|
316
|
-
const pullStatus = useQuery({
|
|
317
|
-
queryKey: ["faire", "ingest", pullBatch],
|
|
318
|
-
enabled: !!pullBatch,
|
|
319
|
-
refetchInterval: (q) =>
|
|
320
|
-
(q.state.data as any)?.progress?.finished ? false : 3000,
|
|
321
|
-
queryFn: () => faireApi.ingestStatus(pullBatch!),
|
|
322
|
-
})
|
|
323
|
-
|
|
324
|
-
const pushProgress = (pushStatus.data as any)?.progress
|
|
325
|
-
const pullProgress = (pullStatus.data as any)?.progress
|
|
326
|
-
|
|
327
|
-
const handlePush = () => {
|
|
328
|
-
const ids = productIds
|
|
329
|
-
.split(/[\s,]+/)
|
|
330
|
-
.map((s) => s.trim())
|
|
331
|
-
.filter(Boolean)
|
|
332
|
-
if (!ids.length) {
|
|
333
|
-
toast.error("Enter at least one product id")
|
|
334
|
-
return
|
|
335
|
-
}
|
|
336
|
-
pushMutation.mutate(ids)
|
|
337
|
-
}
|
|
338
|
-
|
|
339
|
-
return (
|
|
340
|
-
<Container className="divide-y p-0">
|
|
341
|
-
<div className="px-6 py-4">
|
|
342
|
-
<Heading>Bulk operations</Heading>
|
|
343
|
-
<Text className="text-ui-fg-subtle" size="small">
|
|
344
|
-
Push many products to Faire, or pull wholesale orders from Faire into
|
|
345
|
-
Medusa. Both run as long-running background workflows.
|
|
346
|
-
</Text>
|
|
347
|
-
</div>
|
|
348
|
-
<div className="px-6 py-4 flex flex-col gap-6">
|
|
349
|
-
{/* Push products */}
|
|
350
|
-
<div className="flex flex-col gap-3">
|
|
351
|
-
<div className="flex flex-col gap-1">
|
|
352
|
-
<Label>Push products to Faire</Label>
|
|
353
|
-
<Text className="text-ui-fg-subtle" size="small">
|
|
354
|
-
Comma- or line-separated Medusa product IDs.
|
|
355
|
-
</Text>
|
|
356
|
-
</div>
|
|
357
|
-
<textarea
|
|
358
|
-
className="border-ui-border-base rounded-lg border px-3 py-2 text-sm min-h-[72px]"
|
|
359
|
-
placeholder="prod_01..., prod_02..."
|
|
360
|
-
value={productIds}
|
|
361
|
-
onChange={(e) => setProductIds(e.target.value)}
|
|
362
|
-
disabled={!connected || pushMutation.isPending}
|
|
363
|
-
/>
|
|
364
|
-
<div className="flex flex-wrap items-center gap-3">
|
|
365
|
-
<Button
|
|
366
|
-
size="small"
|
|
367
|
-
variant="secondary"
|
|
368
|
-
disabled={!connected}
|
|
369
|
-
onClick={() => navigate("/settings/faire/bulk")}
|
|
370
|
-
>
|
|
371
|
-
Select products
|
|
372
|
-
</Button>
|
|
373
|
-
<Button
|
|
374
|
-
size="small"
|
|
375
|
-
onClick={handlePush}
|
|
376
|
-
disabled={!connected || pushMutation.isPending}
|
|
377
|
-
isLoading={pushMutation.isPending}
|
|
378
|
-
>
|
|
379
|
-
Start bulk push
|
|
380
|
-
</Button>
|
|
381
|
-
{pushBatch && pushProgress && (
|
|
382
|
-
<BulkProgress
|
|
383
|
-
label="Push"
|
|
384
|
-
progress={pushProgress}
|
|
385
|
-
status={(pushStatus.data as any)?.batch?.status}
|
|
386
|
-
/>
|
|
387
|
-
)}
|
|
388
|
-
</div>
|
|
389
|
-
</div>
|
|
390
|
-
|
|
391
|
-
{/* Pull orders */}
|
|
392
|
-
<div className="flex flex-col gap-3">
|
|
393
|
-
<div className="flex flex-col gap-1">
|
|
394
|
-
<Label>Pull Faire orders into Medusa</Label>
|
|
395
|
-
<Text className="text-ui-fg-subtle" size="small">
|
|
396
|
-
Ingests all available Faire orders as Medusa orders (idempotent).
|
|
397
|
-
</Text>
|
|
398
|
-
</div>
|
|
399
|
-
<div className="flex items-center gap-3">
|
|
400
|
-
<Button
|
|
401
|
-
size="small"
|
|
402
|
-
variant="secondary"
|
|
403
|
-
onClick={() => pullMutation.mutate()}
|
|
404
|
-
disabled={!connected || pullMutation.isPending}
|
|
405
|
-
isLoading={pullMutation.isPending}
|
|
406
|
-
>
|
|
407
|
-
Start order pull
|
|
408
|
-
</Button>
|
|
409
|
-
{pullBatch && pullProgress && (
|
|
410
|
-
<BulkProgress
|
|
411
|
-
label="Pull"
|
|
412
|
-
progress={pullProgress}
|
|
413
|
-
status={(pullStatus.data as any)?.batch?.status}
|
|
414
|
-
/>
|
|
415
|
-
)}
|
|
416
|
-
</div>
|
|
417
|
-
</div>
|
|
418
|
-
</div>
|
|
419
|
-
</Container>
|
|
420
|
-
)
|
|
421
|
-
}
|
|
422
|
-
|
|
423
|
-
const BulkProgress = ({
|
|
424
|
-
label,
|
|
425
|
-
progress,
|
|
426
|
-
status,
|
|
427
|
-
}: {
|
|
428
|
-
label: string
|
|
429
|
-
progress: { total: number; done: number; pct: number; finished: boolean }
|
|
430
|
-
status?: string
|
|
431
|
-
}) => (
|
|
432
|
-
<div className="flex items-center gap-3">
|
|
433
|
-
<StatusBadge color={progress.finished ? (status === "failed" ? "red" : "green") : "orange"}>
|
|
434
|
-
{label}: {progress.done}/{progress.total || "?"} ({progress.pct}%)
|
|
435
|
-
</StatusBadge>
|
|
436
|
-
{progress.finished && (
|
|
437
|
-
<Text className="text-ui-fg-subtle" size="small">
|
|
438
|
-
{status === "failed" ? "Completed with errors" : "Done"}
|
|
439
|
-
</Text>
|
|
440
|
-
)}
|
|
441
|
-
</div>
|
|
442
|
-
)
|
|
443
|
-
|
|
444
279
|
export const config = defineRouteConfig({
|
|
445
280
|
label: "Faire",
|
|
446
281
|
icon: BuildingStorefront,
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import crypto from "crypto"
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Symmetric encryption for secrets at rest (Faire OAuth access/refresh tokens).
|
|
5
|
+
*
|
|
6
|
+
* AES-256-GCM, mirroring the host app's `encryption` module so the same
|
|
7
|
+
* `ENCRYPTION_KEY` protects Faire tokens the same way it protects social
|
|
8
|
+
* platform tokens. A dedicated `FAIRE_TOKEN_ENCRYPTION_KEY` takes precedence
|
|
9
|
+
* if set.
|
|
10
|
+
*
|
|
11
|
+
* Stored format (single self-describing string, fits the existing text column):
|
|
12
|
+
* fenc1:<iv_b64>.<authTag_b64>.<ciphertext_b64>
|
|
13
|
+
*
|
|
14
|
+
* Backward/forward compatible:
|
|
15
|
+
* - `encryptSecret` of an empty/nullish value returns it unchanged.
|
|
16
|
+
* - `decryptSecret` of a value WITHOUT the `fenc1:` prefix is returned as-is,
|
|
17
|
+
* so legacy plaintext rows keep working and a missing key degrades to
|
|
18
|
+
* plaintext (with a one-time warning) rather than crashing.
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
const PREFIX = "fenc1:"
|
|
22
|
+
const ALGORITHM = "aes-256-gcm"
|
|
23
|
+
|
|
24
|
+
let warnedNoKey = false
|
|
25
|
+
|
|
26
|
+
/** Resolve a 32-byte key from whatever secret is configured (any length). */
|
|
27
|
+
function resolveKey(): Buffer | null {
|
|
28
|
+
const keyString =
|
|
29
|
+
process.env.FAIRE_TOKEN_ENCRYPTION_KEY ||
|
|
30
|
+
process.env.ENCRYPTION_KEY ||
|
|
31
|
+
process.env.ENCRYPTION_KEY_V1
|
|
32
|
+
if (!keyString) return null
|
|
33
|
+
|
|
34
|
+
// Prefer a base64 32-byte key (matches the host encryption module); otherwise
|
|
35
|
+
// derive a stable 32-byte key from the secret so any value works.
|
|
36
|
+
const asBase64 = Buffer.from(keyString, "base64")
|
|
37
|
+
if (asBase64.length === 32) return asBase64
|
|
38
|
+
return crypto.createHash("sha256").update(keyString, "utf8").digest()
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export function encryptSecret(plaintext: string | null | undefined): string | null {
|
|
42
|
+
if (plaintext == null || plaintext === "") {
|
|
43
|
+
return plaintext ?? null
|
|
44
|
+
}
|
|
45
|
+
const key = resolveKey()
|
|
46
|
+
if (!key) {
|
|
47
|
+
if (!warnedNoKey) {
|
|
48
|
+
// eslint-disable-next-line no-console
|
|
49
|
+
console.warn(
|
|
50
|
+
"[faire-sync] No ENCRYPTION_KEY/FAIRE_TOKEN_ENCRYPTION_KEY set — Faire tokens are stored in plaintext. Set one to encrypt at rest."
|
|
51
|
+
)
|
|
52
|
+
warnedNoKey = true
|
|
53
|
+
}
|
|
54
|
+
return plaintext
|
|
55
|
+
}
|
|
56
|
+
const iv = crypto.randomBytes(12)
|
|
57
|
+
const cipher = crypto.createCipheriv(ALGORITHM, key, iv)
|
|
58
|
+
const ct = Buffer.concat([cipher.update(plaintext, "utf8"), cipher.final()])
|
|
59
|
+
const tag = cipher.getAuthTag()
|
|
60
|
+
return `${PREFIX}${iv.toString("base64")}.${tag.toString("base64")}.${ct.toString("base64")}`
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export function decryptSecret(stored: string | null | undefined): string {
|
|
64
|
+
if (stored == null || stored === "") return ""
|
|
65
|
+
if (!stored.startsWith(PREFIX)) return stored // legacy plaintext / unencrypted
|
|
66
|
+
const key = resolveKey()
|
|
67
|
+
if (!key) {
|
|
68
|
+
throw new Error(
|
|
69
|
+
"[faire-sync] Encrypted Faire token found but no ENCRYPTION_KEY/FAIRE_TOKEN_ENCRYPTION_KEY is set to decrypt it."
|
|
70
|
+
)
|
|
71
|
+
}
|
|
72
|
+
const [ivB64, tagB64, ctB64] = stored.slice(PREFIX.length).split(".")
|
|
73
|
+
const iv = Buffer.from(ivB64, "base64")
|
|
74
|
+
const tag = Buffer.from(tagB64, "base64")
|
|
75
|
+
const ct = Buffer.from(ctB64, "base64")
|
|
76
|
+
const decipher = crypto.createDecipheriv(ALGORITHM, key, iv)
|
|
77
|
+
decipher.setAuthTag(tag)
|
|
78
|
+
return Buffer.concat([decipher.update(ct), decipher.final()]).toString("utf8")
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/** True if the stored value is in the encrypted envelope format. */
|
|
82
|
+
export function isEncrypted(stored: string | null | undefined): boolean {
|
|
83
|
+
return typeof stored === "string" && stored.startsWith(PREFIX)
|
|
84
|
+
}
|
package/src/lib/faire-client.ts
CHANGED
|
@@ -130,6 +130,30 @@ export class FaireClient {
|
|
|
130
130
|
}
|
|
131
131
|
}
|
|
132
132
|
|
|
133
|
+
/**
|
|
134
|
+
* Revoke an OAuth access token on Faire's side.
|
|
135
|
+
*
|
|
136
|
+
* Faire allows only ONE active OAuth token per (app, brand); without revoking,
|
|
137
|
+
* a reconnect fails with 400 "Application is already installed via an active
|
|
138
|
+
* OAuth access token". Non-RFC-6749 body (matches the token endpoint), posted
|
|
139
|
+
* to `/api/external-api-oauth2/revoke`, no auth header.
|
|
140
|
+
*/
|
|
141
|
+
async revokeToken(accessToken: string): Promise<void> {
|
|
142
|
+
if (!accessToken) return
|
|
143
|
+
const revokeUrl = this.tokenUrl.replace(/\/token$/, "/revoke")
|
|
144
|
+
const body = {
|
|
145
|
+
application_token: this.clientId,
|
|
146
|
+
application_secret: this.clientSecret,
|
|
147
|
+
access_token_o_auth: accessToken,
|
|
148
|
+
}
|
|
149
|
+
await this.requestJson<any>(revokeUrl, {
|
|
150
|
+
method: "POST",
|
|
151
|
+
headers: { "Content-Type": "application/json" },
|
|
152
|
+
body: JSON.stringify(body),
|
|
153
|
+
auth: false,
|
|
154
|
+
})
|
|
155
|
+
}
|
|
156
|
+
|
|
133
157
|
async refreshAccessToken(refresh_token: string): Promise<TokenData> {
|
|
134
158
|
const body = {
|
|
135
159
|
application_token: this.clientId,
|
|
@@ -6,6 +6,7 @@ import FaireSyncBatch from "./models/faire-sync-batch"
|
|
|
6
6
|
import FaireWebhookEvent from "./models/faire-webhook-event"
|
|
7
7
|
import FaireOrder from "./models/faire-order"
|
|
8
8
|
import { FaireClient } from "../../lib/faire-client"
|
|
9
|
+
import { encryptSecret, decryptSecret } from "../../lib/crypto"
|
|
9
10
|
import { FairePluginOptions, BrandInfo, TokenData } from "../../lib/types"
|
|
10
11
|
|
|
11
12
|
type ModuleOptions = FairePluginOptions & Record<string, any>
|
|
@@ -33,6 +34,12 @@ function toMedusaError(err: any): MedusaError {
|
|
|
33
34
|
"Faire authorization expired or was already used. Please reconnect."
|
|
34
35
|
)
|
|
35
36
|
}
|
|
37
|
+
if (/already installed/i.test(message)) {
|
|
38
|
+
return new MedusaError(
|
|
39
|
+
MedusaError.Types.NOT_ALLOWED,
|
|
40
|
+
"This Faire app already has an active connection. Disconnect it here first, or uninstall the app from your Faire account (Settings → Apps), then connect again."
|
|
41
|
+
)
|
|
42
|
+
}
|
|
36
43
|
if (/brand/i.test(message) && /(no|not|own)/i.test(message)) {
|
|
37
44
|
return new MedusaError(MedusaError.Types.NOT_FOUND, message)
|
|
38
45
|
}
|
|
@@ -105,6 +112,24 @@ class FaireSyncService extends MedusaService({
|
|
|
105
112
|
return account ?? null
|
|
106
113
|
}
|
|
107
114
|
|
|
115
|
+
/**
|
|
116
|
+
* Return a shallow copy of an account with its at-rest secrets decrypted, for
|
|
117
|
+
* use when calling the Faire API. Tokens are stored encrypted (AES-256-GCM);
|
|
118
|
+
* callers that make API requests must go through this / `ensureFreshToken`.
|
|
119
|
+
*/
|
|
120
|
+
private withDecryptedTokens<T extends { access_token?: any; refresh_token?: any }>(
|
|
121
|
+
account: T | null
|
|
122
|
+
): T | null {
|
|
123
|
+
if (!account) return account
|
|
124
|
+
return {
|
|
125
|
+
...account,
|
|
126
|
+
access_token: decryptSecret(account.access_token),
|
|
127
|
+
refresh_token: account.refresh_token
|
|
128
|
+
? decryptSecret(account.refresh_token)
|
|
129
|
+
: account.refresh_token,
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
108
133
|
async saveAccount(token: TokenData, brand: BrandInfo) {
|
|
109
134
|
const existing = await this.getActiveAccount()
|
|
110
135
|
const expiresAt =
|
|
@@ -116,8 +141,8 @@ class FaireSyncService extends MedusaService({
|
|
|
116
141
|
brand_name: brand.brand_name,
|
|
117
142
|
currency: brand.currency ?? null,
|
|
118
143
|
country: brand.country ?? null,
|
|
119
|
-
access_token: token.access_token,
|
|
120
|
-
refresh_token: token.refresh_token ?? null,
|
|
144
|
+
access_token: encryptSecret(token.access_token),
|
|
145
|
+
refresh_token: encryptSecret(token.refresh_token ?? null),
|
|
121
146
|
token_expires_at: expiresAt,
|
|
122
147
|
brand_info: brand.raw,
|
|
123
148
|
is_active: true,
|
|
@@ -134,6 +159,22 @@ class FaireSyncService extends MedusaService({
|
|
|
134
159
|
async disconnect() {
|
|
135
160
|
const account = await this.getActiveAccount()
|
|
136
161
|
if (!account) return
|
|
162
|
+
// Revoke on Faire's side FIRST. Faire allows only one active OAuth token per
|
|
163
|
+
// (app, brand); if we only clear locally, a later reconnect fails with 400
|
|
164
|
+
// "Application is already installed via an active OAuth access token".
|
|
165
|
+
// Best-effort: a failed revoke must not block local disconnect.
|
|
166
|
+
try {
|
|
167
|
+
const accessToken = decryptSecret((account as any).access_token)
|
|
168
|
+
if (accessToken && this.options_.authMode !== "apiKey") {
|
|
169
|
+
await this.getClient().revokeToken(accessToken)
|
|
170
|
+
}
|
|
171
|
+
} catch (err: any) {
|
|
172
|
+
// eslint-disable-next-line no-console
|
|
173
|
+
console.warn(
|
|
174
|
+
"[faire-sync] Faire token revoke failed on disconnect (clearing locally anyway):",
|
|
175
|
+
err?.message
|
|
176
|
+
)
|
|
177
|
+
}
|
|
137
178
|
await this.updateFaireSyncAccounts({
|
|
138
179
|
id: account.id,
|
|
139
180
|
is_active: false,
|
|
@@ -142,6 +183,11 @@ class FaireSyncService extends MedusaService({
|
|
|
142
183
|
} as any)
|
|
143
184
|
}
|
|
144
185
|
|
|
186
|
+
/**
|
|
187
|
+
* Resolve an account whose access/refresh tokens are DECRYPTED and non-expired,
|
|
188
|
+
* refreshing against Faire if needed. All Faire API callers must use this — the
|
|
189
|
+
* tokens are stored encrypted at rest, so a raw account row is unusable.
|
|
190
|
+
*/
|
|
145
191
|
async ensureFreshToken(account?: any) {
|
|
146
192
|
const acct = account ?? (await this.getActiveAccount())
|
|
147
193
|
if (!acct) {
|
|
@@ -151,15 +197,16 @@ class FaireSyncService extends MedusaService({
|
|
|
151
197
|
)
|
|
152
198
|
}
|
|
153
199
|
if (!acct.token_expires_at) {
|
|
154
|
-
return acct
|
|
200
|
+
return this.withDecryptedTokens(acct)
|
|
155
201
|
}
|
|
156
202
|
const now = Date.now()
|
|
157
203
|
const expiresAt = new Date(acct.token_expires_at).getTime()
|
|
158
204
|
const fiveMinutes = 5 * 60 * 1000
|
|
159
205
|
if (expiresAt - now > fiveMinutes) {
|
|
160
|
-
return acct
|
|
206
|
+
return this.withDecryptedTokens(acct)
|
|
161
207
|
}
|
|
162
|
-
|
|
208
|
+
const refreshToken = acct.refresh_token ? decryptSecret(acct.refresh_token) : ""
|
|
209
|
+
if (!refreshToken) {
|
|
163
210
|
throw new MedusaError(
|
|
164
211
|
MedusaError.Types.NOT_ALLOWED,
|
|
165
212
|
"Faire access token has expired. Please reconnect your Faire account."
|
|
@@ -167,16 +214,17 @@ class FaireSyncService extends MedusaService({
|
|
|
167
214
|
}
|
|
168
215
|
try {
|
|
169
216
|
const client = this.getClient()
|
|
170
|
-
const token = await client.refreshAccessToken(
|
|
171
|
-
|
|
217
|
+
const token = await client.refreshAccessToken(refreshToken)
|
|
218
|
+
const updated = await this.updateFaireSyncAccounts({
|
|
172
219
|
id: acct.id,
|
|
173
|
-
access_token: token.access_token,
|
|
174
|
-
refresh_token: token.refresh_token ??
|
|
220
|
+
access_token: encryptSecret(token.access_token),
|
|
221
|
+
refresh_token: encryptSecret(token.refresh_token ?? refreshToken),
|
|
175
222
|
token_expires_at:
|
|
176
223
|
token.expires_in != null
|
|
177
224
|
? new Date(token.retrieved_at + token.expires_in * 1000)
|
|
178
225
|
: null,
|
|
179
226
|
} as any)
|
|
227
|
+
return this.withDecryptedTokens(updated)
|
|
180
228
|
} catch (err) {
|
|
181
229
|
throw toMedusaError(err)
|
|
182
230
|
}
|