@teleporthq/teleport-plugin-next-workflows 0.44.8 → 0.44.9
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/__tests__/_helpers/load-server-runtime.ts +6 -2
- package/__tests__/_helpers/run-account-delete-route.ts +140 -0
- package/__tests__/account-delete-current-password.test.ts +73 -0
- package/__tests__/account-delete-password-check.test.ts +113 -0
- package/__tests__/account-delete-visitor-details.test.ts +206 -0
- package/__tests__/auth-password-change-ends-sessions.test.ts +178 -0
- package/__tests__/data-api-invalid-body.test.ts +110 -0
- package/__tests__/data-node-not-awaited.test.ts +7 -6
- package/__tests__/invoice-payment-line.test.ts +68 -0
- package/__tests__/invoice-tender.test.ts +116 -0
- package/__tests__/pending-work-after-reply.test.ts +294 -0
- package/__tests__/runtime-condition-operands.test.ts +86 -0
- package/__tests__/webhook-delivery-lease.test.ts +68 -6
- package/dist/cjs/account-delete-route-generator.d.ts +7 -2
- package/dist/cjs/account-delete-route-generator.d.ts.map +1 -1
- package/dist/cjs/account-delete-route-generator.js +90 -43
- package/dist/cjs/account-delete-route-generator.js.map +1 -1
- package/dist/cjs/api-route-generator.d.ts.map +1 -1
- package/dist/cjs/api-route-generator.js +12 -13
- package/dist/cjs/api-route-generator.js.map +1 -1
- package/dist/cjs/auth-generator.d.ts.map +1 -1
- package/dist/cjs/auth-generator.js +34 -1
- package/dist/cjs/auth-generator.js.map +1 -1
- package/dist/cjs/await-result.d.ts +13 -5
- package/dist/cjs/await-result.d.ts.map +1 -1
- package/dist/cjs/await-result.js +19 -8
- package/dist/cjs/await-result.js.map +1 -1
- package/dist/cjs/data-api-route-generator.d.ts.map +1 -1
- package/dist/cjs/data-api-route-generator.js +9 -1
- package/dist/cjs/data-api-route-generator.js.map +1 -1
- package/dist/cjs/executor-generator.d.ts.map +1 -1
- package/dist/cjs/executor-generator.js +18 -6
- package/dist/cjs/executor-generator.js.map +1 -1
- package/dist/cjs/invoice/invoice-html-code.d.ts.map +1 -1
- package/dist/cjs/invoice/invoice-html-code.js +96 -5
- package/dist/cjs/invoice/invoice-html-code.js.map +1 -1
- package/dist/cjs/nodes/account/account-delete-current.d.ts.map +1 -1
- package/dist/cjs/nodes/account/account-delete-current.js +4 -1
- package/dist/cjs/nodes/account/account-delete-current.js.map +1 -1
- package/dist/cjs/server-runtime-code.d.ts.map +1 -1
- package/dist/cjs/server-runtime-code.js +21 -0
- package/dist/cjs/server-runtime-code.js.map +1 -1
- package/dist/cjs/transactional-email-code.d.ts +7 -0
- package/dist/cjs/transactional-email-code.d.ts.map +1 -1
- package/dist/cjs/transactional-email-code.js +5 -5
- package/dist/cjs/transactional-email-code.js.map +1 -1
- package/dist/cjs/tsconfig.tsbuildinfo +1 -1
- package/dist/cjs/vercel-functions.d.ts +34 -0
- package/dist/cjs/vercel-functions.d.ts.map +1 -0
- package/dist/cjs/vercel-functions.js +62 -0
- package/dist/cjs/vercel-functions.js.map +1 -0
- package/dist/cjs/webhook-delivery-lease/webhook-delivery-lease-code.d.ts +6 -0
- package/dist/cjs/webhook-delivery-lease/webhook-delivery-lease-code.d.ts.map +1 -1
- package/dist/cjs/webhook-delivery-lease/webhook-delivery-lease-code.js +26 -11
- package/dist/cjs/webhook-delivery-lease/webhook-delivery-lease-code.js.map +1 -1
- package/dist/cjs/workflow-project-plugin.d.ts.map +1 -1
- package/dist/cjs/workflow-project-plugin.js +3 -0
- package/dist/cjs/workflow-project-plugin.js.map +1 -1
- package/dist/esm/account-delete-route-generator.d.ts +7 -2
- package/dist/esm/account-delete-route-generator.d.ts.map +1 -1
- package/dist/esm/account-delete-route-generator.js +90 -43
- package/dist/esm/account-delete-route-generator.js.map +1 -1
- package/dist/esm/api-route-generator.d.ts.map +1 -1
- package/dist/esm/api-route-generator.js +12 -13
- package/dist/esm/api-route-generator.js.map +1 -1
- package/dist/esm/auth-generator.d.ts.map +1 -1
- package/dist/esm/auth-generator.js +34 -1
- package/dist/esm/auth-generator.js.map +1 -1
- package/dist/esm/await-result.d.ts +13 -5
- package/dist/esm/await-result.d.ts.map +1 -1
- package/dist/esm/await-result.js +18 -7
- package/dist/esm/await-result.js.map +1 -1
- package/dist/esm/data-api-route-generator.d.ts.map +1 -1
- package/dist/esm/data-api-route-generator.js +9 -1
- package/dist/esm/data-api-route-generator.js.map +1 -1
- package/dist/esm/executor-generator.d.ts.map +1 -1
- package/dist/esm/executor-generator.js +18 -6
- package/dist/esm/executor-generator.js.map +1 -1
- package/dist/esm/invoice/invoice-html-code.d.ts.map +1 -1
- package/dist/esm/invoice/invoice-html-code.js +96 -5
- package/dist/esm/invoice/invoice-html-code.js.map +1 -1
- package/dist/esm/nodes/account/account-delete-current.d.ts.map +1 -1
- package/dist/esm/nodes/account/account-delete-current.js +4 -1
- package/dist/esm/nodes/account/account-delete-current.js.map +1 -1
- package/dist/esm/server-runtime-code.d.ts.map +1 -1
- package/dist/esm/server-runtime-code.js +21 -0
- package/dist/esm/server-runtime-code.js.map +1 -1
- package/dist/esm/transactional-email-code.d.ts +7 -0
- package/dist/esm/transactional-email-code.d.ts.map +1 -1
- package/dist/esm/transactional-email-code.js +5 -5
- package/dist/esm/transactional-email-code.js.map +1 -1
- package/dist/esm/tsconfig.tsbuildinfo +1 -1
- package/dist/esm/vercel-functions.d.ts +34 -0
- package/dist/esm/vercel-functions.d.ts.map +1 -0
- package/dist/esm/vercel-functions.js +57 -0
- package/dist/esm/vercel-functions.js.map +1 -0
- package/dist/esm/webhook-delivery-lease/webhook-delivery-lease-code.d.ts +6 -0
- package/dist/esm/webhook-delivery-lease/webhook-delivery-lease-code.d.ts.map +1 -1
- package/dist/esm/webhook-delivery-lease/webhook-delivery-lease-code.js +26 -11
- package/dist/esm/webhook-delivery-lease/webhook-delivery-lease-code.js.map +1 -1
- package/dist/esm/workflow-project-plugin.d.ts.map +1 -1
- package/dist/esm/workflow-project-plugin.js +3 -0
- package/dist/esm/workflow-project-plugin.js.map +1 -1
- package/package.json +2 -2
- package/src/account-delete-route-generator.ts +97 -45
- package/src/api-route-generator.ts +12 -13
- package/src/auth-generator.ts +34 -1
- package/src/await-result.ts +19 -7
- package/src/data-api-route-generator.ts +9 -1
- package/src/executor-generator.ts +18 -6
- package/src/invoice/invoice-html-code.ts +96 -5
- package/src/nodes/account/account-delete-current.ts +4 -1
- package/src/server-runtime-code.ts +22 -0
- package/src/transactional-email-code.ts +12 -5
- package/src/vercel-functions.ts +67 -0
- package/src/webhook-delivery-lease/webhook-delivery-lease-code.ts +26 -11
- package/src/workflow-project-plugin.ts +3 -0
|
@@ -4,9 +4,14 @@
|
|
|
4
4
|
*
|
|
5
5
|
* The client node calls this route, which:
|
|
6
6
|
* 1. resolves the current user from the NextAuth session (never trusting a
|
|
7
|
-
* client-supplied id),
|
|
7
|
+
* client-supplied id) and, when the request carries a password, goes on
|
|
8
|
+
* only if it is the account's own,
|
|
8
9
|
* 2. runs a single DB transaction that detaches + anonymises business/legal
|
|
9
|
-
* records (orders, withdrawal requests, bookings
|
|
10
|
+
* records (orders, withdrawal requests, bookings, downloads, gift cards
|
|
11
|
+
* the user bought or received) so tracking survives, takes the user's
|
|
12
|
+
* email out of the activity log and the sent-email ledger, leaves the
|
|
13
|
+
* invoices already issued as they are (an invoice must keep naming its
|
|
14
|
+
* buyer),
|
|
10
15
|
* deletes the user's personal data (reviews, cart, favourites, chat
|
|
11
16
|
* history), deletes the rows the user owns in the project's own tables —
|
|
12
17
|
* the ones the AI made for the site, each through the one column the
|
|
@@ -100,10 +105,29 @@ const ANONYMIZE_TABLES = [
|
|
|
100
105
|
'teleport_subscriptions',
|
|
101
106
|
'teleport_abandoned_carts',
|
|
102
107
|
'teleport_voucher_redemptions',
|
|
108
|
+
'teleport_downloads',
|
|
103
109
|
];
|
|
104
110
|
const EMAIL_COLUMNS = ['billing_email', 'shipping_email', 'customer_email', 'email'];
|
|
105
111
|
// A booking also names the customer and carries their phone; both are cleared.
|
|
106
|
-
|
|
112
|
+
// A download keeps its count against the order's limit, without who fetched it
|
|
113
|
+
// from where.
|
|
114
|
+
const CLEARED_COLUMNS = {
|
|
115
|
+
teleport_bookings: ['customer_name', 'customer_phone'],
|
|
116
|
+
teleport_downloads: ['ip_address'],
|
|
117
|
+
};
|
|
118
|
+
// Tables that name the user only by their email: each column holding it takes
|
|
119
|
+
// the anonymised address (a gift card stays redeemable by its code) or a blank
|
|
120
|
+
// (an activity-log line keeps what happened, not who did it).
|
|
121
|
+
const EMAIL_REFERENCES = [
|
|
122
|
+
{ table: 'teleport_gift_cards', column: 'purchaser_email', replacement: 'anonymised' },
|
|
123
|
+
{ table: 'teleport_gift_cards', column: 'recipient_email', replacement: 'anonymised' },
|
|
124
|
+
{ table: 'teleport_events', column: 'actor', replacement: 'blank' },
|
|
125
|
+
];
|
|
126
|
+
const SENT_EMAILS_TABLE = 'teleport_sent_emails';
|
|
127
|
+
|
|
128
|
+
function anonymisedEmailOf(userId) {
|
|
129
|
+
return String(DELETED_EMAIL_PATTERN).split('{{userId}}').join(String(userId));
|
|
130
|
+
}
|
|
107
131
|
|
|
108
132
|
async function tableColumns(client, table) {
|
|
109
133
|
var r = await client.query(
|
|
@@ -146,6 +170,36 @@ async function deleteRowsByEmail(client, table, column, email) {
|
|
|
146
170
|
await client.query('DELETE FROM ' + table + ' WHERE ' + column + ' = $1', [email]);
|
|
147
171
|
}
|
|
148
172
|
|
|
173
|
+
async function replaceEmailReferences(client, email, anonymisedEmail) {
|
|
174
|
+
if (!email) { return; }
|
|
175
|
+
for (var i = 0; i < EMAIL_REFERENCES.length; i++) {
|
|
176
|
+
var ref = EMAIL_REFERENCES[i];
|
|
177
|
+
var cols = await tableColumns(client, ref.table);
|
|
178
|
+
if (cols.indexOf(ref.column) === -1) { continue; }
|
|
179
|
+
await client.query(
|
|
180
|
+
'UPDATE ' + ref.table + ' SET ' + ref.column + ' = $1 WHERE lower(' + ref.column + ') = lower($2)',
|
|
181
|
+
[ref.replacement === 'anonymised' ? anonymisedEmail : '', email]
|
|
182
|
+
);
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
// The emails the site sent the user stay on record (which email, when, through
|
|
187
|
+
// which provider), without the address, the body or the values it was filled
|
|
188
|
+
// with.
|
|
189
|
+
async function blankSentEmailsTo(client, email) {
|
|
190
|
+
if (!email) { return; }
|
|
191
|
+
var cols = await tableColumns(client, SENT_EMAILS_TABLE);
|
|
192
|
+
if (cols.indexOf('recipient_email') === -1) { return; }
|
|
193
|
+
var sets = ["recipient_email = ''"];
|
|
194
|
+
if (cols.indexOf('recipients') !== -1) { sets.push('recipients = NULL'); }
|
|
195
|
+
if (cols.indexOf('body_html') !== -1) { sets.push("body_html = ''"); }
|
|
196
|
+
if (cols.indexOf('payload') !== -1) { sets.push('payload = NULL'); }
|
|
197
|
+
await client.query(
|
|
198
|
+
'UPDATE ' + SENT_EMAILS_TABLE + ' SET ' + sets.join(', ') + ' WHERE lower(recipient_email) = lower($1)',
|
|
199
|
+
[email]
|
|
200
|
+
);
|
|
201
|
+
}
|
|
202
|
+
|
|
149
203
|
// Rows the user owns in the project's OWN tables — the ones the AI made for
|
|
150
204
|
// this site (saved items, requests, reviews …). Their link to the user is
|
|
151
205
|
// ON DELETE SET NULL, so they outlived the account with a blank owner, and the
|
|
@@ -259,44 +313,34 @@ async function deleteOwnedRows(client, userId) {
|
|
|
259
313
|
}
|
|
260
314
|
}
|
|
261
315
|
|
|
262
|
-
//
|
|
263
|
-
//
|
|
264
|
-
//
|
|
265
|
-
//
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
'
|
|
269
|
-
|
|
270
|
-
'customer_state',
|
|
271
|
-
'customer_zip',
|
|
272
|
-
'customer_country',
|
|
273
|
-
'customer_vat',
|
|
274
|
-
'customer_phone',
|
|
275
|
-
];
|
|
276
|
-
|
|
277
|
-
async function anonymizeInvoices(client, ordersTable, invoicesTable, userId, syntheticEmail) {
|
|
278
|
-
var invCols = await tableColumns(client, invoicesTable);
|
|
279
|
-
if (invCols.indexOf('order_id') === -1) { return; }
|
|
280
|
-
var orderCols = await tableColumns(client, ordersTable);
|
|
281
|
-
if (orderCols.indexOf('user_id') === -1) { return; }
|
|
282
|
-
var sets = [];
|
|
283
|
-
var params = [];
|
|
284
|
-
if (invCols.indexOf('customer_email') !== -1) {
|
|
285
|
-
params.push(syntheticEmail);
|
|
286
|
-
sets.push('customer_email = $' + params.length);
|
|
316
|
+
// The password a request confirms the deletion with, or null when it names
|
|
317
|
+
// none. A request without one deletes as before; one with a password deletes
|
|
318
|
+
// only when it is the account's own (the editor's delete-account form asks for
|
|
319
|
+
// it).
|
|
320
|
+
function readRequestPassword(req) {
|
|
321
|
+
var body = req.body;
|
|
322
|
+
if (typeof body === 'string') {
|
|
323
|
+
try { body = JSON.parse(body); } catch (e) { body = null; }
|
|
287
324
|
}
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
325
|
+
if (!body || typeof body !== 'object' || body.password === undefined || body.password === null) {
|
|
326
|
+
return null;
|
|
327
|
+
}
|
|
328
|
+
return String(body.password);
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
// The sign-in check (utils/auth/hash-password.js verifyPassword): bcrypt over
|
|
332
|
+
// the SHA-256 of the password and AUTH_PEPPER. An account without a stored
|
|
333
|
+
// password (a social sign-in) matches no password.
|
|
334
|
+
function passwordMatches(password, storedHash) {
|
|
335
|
+
if (!storedHash || password.length > 1000) { return false; }
|
|
336
|
+
var bcrypt = require('bcryptjs');
|
|
337
|
+
var crypto = require('crypto');
|
|
338
|
+
var prepared = crypto.createHash('sha256').update(password + (process.env.AUTH_PEPPER || '')).digest('base64');
|
|
339
|
+
try {
|
|
340
|
+
return bcrypt.compareSync(prepared, String(storedHash));
|
|
341
|
+
} catch (e) {
|
|
342
|
+
return false;
|
|
292
343
|
}
|
|
293
|
-
if (sets.length === 0) { return; }
|
|
294
|
-
params.push(userId);
|
|
295
|
-
await client.query(
|
|
296
|
-
'UPDATE ' + invoicesTable + ' SET ' + sets.join(', ') +
|
|
297
|
-
' WHERE order_id IN (SELECT id FROM ' + ordersTable + ' WHERE user_id = $' + params.length + ')',
|
|
298
|
-
params
|
|
299
|
-
);
|
|
300
344
|
}
|
|
301
345
|
|
|
302
346
|
${generateFillTemplateFn()}
|
|
@@ -313,6 +357,7 @@ ${generateProviderSendFunction(provider, {
|
|
|
313
357
|
source: 'account-delete',
|
|
314
358
|
sourceRef: 'api/account/delete-current',
|
|
315
359
|
relativePrefix: '../../..',
|
|
360
|
+
withoutRecipientDetails: true,
|
|
316
361
|
})}
|
|
317
362
|
|
|
318
363
|
// \`locale\` is the language of the page the deletion was requested from
|
|
@@ -328,7 +373,7 @@ async function sendFarewellEmail(toEmail, tokenValues, userId, locale) {
|
|
|
328
373
|
var copy = resolveFarewellEmailCopy(locale);
|
|
329
374
|
var subject = fillTemplate(copy.subject, tokenValues);
|
|
330
375
|
var html = fillTemplate(copy.body, tokenValues);
|
|
331
|
-
await __sendProviderEmail({ from: from, fromName: EMAIL_FROM_NAME, to: toEmail, subject: subject, html: html, apiKey: apiKey, tokenValues: tokenValues, userId: userId });
|
|
376
|
+
await __sendProviderEmail({ from: from, fromName: EMAIL_FROM_NAME, to: toEmail, recordedTo: anonymisedEmailOf(userId), subject: subject, html: html, apiKey: apiKey, tokenValues: tokenValues, userId: userId });
|
|
332
377
|
}
|
|
333
378
|
|
|
334
379
|
module.exports = async function handler(req, res) {
|
|
@@ -348,10 +393,12 @@ module.exports = async function handler(req, res) {
|
|
|
348
393
|
return res.status(401).json({ error: 'Unauthenticated' });
|
|
349
394
|
}
|
|
350
395
|
|
|
396
|
+
var requestPassword = readRequestPassword(req);
|
|
351
397
|
var client = getClient();
|
|
352
398
|
var userEmail = '';
|
|
353
399
|
var userName = '';
|
|
354
400
|
var userFound = false;
|
|
401
|
+
var passwordRefused = false;
|
|
355
402
|
var failed = false;
|
|
356
403
|
// try/finally so the pg connection is ALWAYS closed — every early exit
|
|
357
404
|
// (not-found, error) leaves via the finally, never leaking a connection.
|
|
@@ -364,14 +411,14 @@ module.exports = async function handler(req, res) {
|
|
|
364
411
|
userFound = true;
|
|
365
412
|
userEmail = userRes.rows[0].email || '';
|
|
366
413
|
userName = userRes.rows[0].name || '';
|
|
367
|
-
|
|
368
|
-
|
|
414
|
+
passwordRefused =
|
|
415
|
+
requestPassword !== null && !passwordMatches(requestPassword, userRes.rows[0].password);
|
|
416
|
+
}
|
|
417
|
+
if (userFound && !passwordRefused) {
|
|
418
|
+
var syntheticEmail = anonymisedEmailOf(userId);
|
|
369
419
|
|
|
370
420
|
await client.query('BEGIN');
|
|
371
421
|
try {
|
|
372
|
-
// Scrub invoice PII FIRST — invoices link to the user only through
|
|
373
|
-
// order_id, so this must run while the orders row still carries user_id.
|
|
374
|
-
await anonymizeInvoices(client, 'teleport_orders', 'teleport_invoices', userId, syntheticEmail);
|
|
375
422
|
for (var a = 0; a < ANONYMIZE_TABLES.length; a++) {
|
|
376
423
|
await anonymizeUserRows(client, ANONYMIZE_TABLES[a], userId, syntheticEmail);
|
|
377
424
|
}
|
|
@@ -379,6 +426,8 @@ module.exports = async function handler(req, res) {
|
|
|
379
426
|
await deleteUserRows(client, DELETE_TABLES[d], userId);
|
|
380
427
|
}
|
|
381
428
|
await deleteOwnedRows(client, userId);
|
|
429
|
+
await replaceEmailReferences(client, userEmail, syntheticEmail);
|
|
430
|
+
await blankSentEmailsTo(client, userEmail);
|
|
382
431
|
// password_reset_tokens has no user_id — it is keyed by the email string.
|
|
383
432
|
await deleteRowsByEmail(client, 'password_reset_tokens', 'email', userEmail);
|
|
384
433
|
await client.query('DELETE FROM ' + AUTH_USERS_TABLE + ' WHERE id = $1', [userId]);
|
|
@@ -401,6 +450,9 @@ module.exports = async function handler(req, res) {
|
|
|
401
450
|
if (!userFound) {
|
|
402
451
|
return res.status(404).json({ error: 'Account not found' });
|
|
403
452
|
}
|
|
453
|
+
if (passwordRefused) {
|
|
454
|
+
return res.status(403).json({ error: 'Incorrect password' });
|
|
455
|
+
}
|
|
404
456
|
|
|
405
457
|
// Best-effort farewell email AFTER the successful commit (connection closed).
|
|
406
458
|
try {
|
|
@@ -442,7 +442,7 @@ module.exports = async function handler(req, res) {
|
|
|
442
442
|
for (var h = 0; h < hKeys.length; h++) {
|
|
443
443
|
res.setHeader(hKeys[h], earlyRes.headers[hKeys[h]]);
|
|
444
444
|
}
|
|
445
|
-
await utils.
|
|
445
|
+
await utils.handOffPendingNodePromises(context);
|
|
446
446
|
res.status(earlyRes.status || 500).json(earlyRes.body || {});
|
|
447
447
|
return;
|
|
448
448
|
}
|
|
@@ -453,11 +453,11 @@ module.exports = async function handler(req, res) {
|
|
|
453
453
|
if (result && result.__terminal) break;
|
|
454
454
|
}
|
|
455
455
|
|
|
456
|
-
//
|
|
457
|
-
//
|
|
458
|
-
//
|
|
459
|
-
//
|
|
460
|
-
await utils.
|
|
456
|
+
// Every not-awaited step must still land: the platform may freeze this
|
|
457
|
+
// function the moment the response is sent, which would drop the work. On
|
|
458
|
+
// Vercel it finishes after the reply (waitUntil); elsewhere the reply waits
|
|
459
|
+
// for it (see handOffPendingNodePromises in the server runtime).
|
|
460
|
+
await utils.handOffPendingNodePromises(context);
|
|
461
461
|
delete context.__request;
|
|
462
462
|
delete context.__pendingNodePromises;
|
|
463
463
|
// MUST be stripped before replying: the results payload below IS this
|
|
@@ -469,7 +469,7 @@ module.exports = async function handler(req, res) {
|
|
|
469
469
|
res.status(200).json({ success: true, results: utils.segmentReply(SEGMENT_CONFIG, context, __incomingSnapshot) });
|
|
470
470
|
} catch (error) {
|
|
471
471
|
console.error('Workflow segment error:', error);
|
|
472
|
-
if (__wfContext) { await utils.
|
|
472
|
+
if (__wfContext) { await utils.handOffPendingNodePromises(__wfContext); }
|
|
473
473
|
res.status(500).json({ success: false, error: error.message || 'Internal server error' });
|
|
474
474
|
}
|
|
475
475
|
};
|
|
@@ -876,7 +876,7 @@ module.exports = async function handler(req, res) {
|
|
|
876
876
|
}
|
|
877
877
|
const result = await nodeHandler(resolved, context);
|
|
878
878
|
if (result && result.__earlyResponse) {
|
|
879
|
-
await utils.
|
|
879
|
+
await utils.handOffPendingNodePromises(context);
|
|
880
880
|
if (streamStarted) {
|
|
881
881
|
res.write('data: ' + JSON.stringify({ type: 'error', error: (result.__earlyResponse.body && result.__earlyResponse.body.message) || 'Request rejected' }) + '\\n\\n');
|
|
882
882
|
res.end();
|
|
@@ -901,10 +901,9 @@ module.exports = async function handler(req, res) {
|
|
|
901
901
|
}
|
|
902
902
|
}
|
|
903
903
|
|
|
904
|
-
// See the non-streaming segment route:
|
|
905
|
-
//
|
|
906
|
-
|
|
907
|
-
await utils.settlePendingNodePromises(context);
|
|
904
|
+
// See the non-streaming segment route: every not-awaited step must still
|
|
905
|
+
// land, after the reply on Vercel, before it anywhere else.
|
|
906
|
+
await utils.handOffPendingNodePromises(context);
|
|
908
907
|
delete context.__request;
|
|
909
908
|
delete context.__pendingNodePromises;
|
|
910
909
|
// MUST be stripped before replying: the results payload below IS this
|
|
@@ -922,7 +921,7 @@ module.exports = async function handler(req, res) {
|
|
|
922
921
|
}
|
|
923
922
|
} catch (error) {
|
|
924
923
|
console.error('Streaming workflow segment error:', error);
|
|
925
|
-
if (__wfContext) { await utils.
|
|
924
|
+
if (__wfContext) { await utils.handOffPendingNodePromises(__wfContext); }
|
|
926
925
|
if (streamStarted) {
|
|
927
926
|
try {
|
|
928
927
|
res.write('data: ' + JSON.stringify({ type: 'error', error: error.message || 'Internal server error' }) + '\\n\\n');
|
package/src/auth-generator.ts
CHANGED
|
@@ -673,7 +673,9 @@ const generateProvidersSetup = (auth: UIDLAuthentication): string => {
|
|
|
673
673
|
if (!verifyPassword(String(credentials.password), user.password)) {
|
|
674
674
|
throw new Error('InvalidCredentials');
|
|
675
675
|
}
|
|
676
|
-
|
|
676
|
+
const signedIn = sanitizeUser(user);
|
|
677
|
+
signedIn[TOKEN_PASSWORD_FINGERPRINT] = passwordFingerprint(user.password);
|
|
678
|
+
return signedIn;
|
|
677
679
|
} catch (err) {
|
|
678
680
|
if (err && err.message === 'InvalidCredentials') throw err;
|
|
679
681
|
// A hashing/serialisation fault is an infrastructure problem, not a
|
|
@@ -758,6 +760,22 @@ const USER_REFRESH_INTERVAL_MS = ${USER_REFRESH_INTERVAL_MS};
|
|
|
758
760
|
// internal timestamp on every binding that enumerates the user object.
|
|
759
761
|
const TOKEN_REFRESH_STAMP = '__userRefreshedAt';
|
|
760
762
|
|
|
763
|
+
// A short fingerprint of the account's stored password hash, carried in the
|
|
764
|
+
// token from sign-in (or from the first refresh of a token that has none). A
|
|
765
|
+
// password change, a reset included, changes it, and the refresh then ends the
|
|
766
|
+
// session: every session opened before the change is signed out at its next
|
|
767
|
+
// refresh. Keyed with NEXTAUTH_SECRET; never the hash itself, and like the
|
|
768
|
+
// stamp above never copied onto \`session.user\`.
|
|
769
|
+
const TOKEN_PASSWORD_FINGERPRINT = '__passwordFingerprint';
|
|
770
|
+
|
|
771
|
+
function passwordFingerprint(storedHash) {
|
|
772
|
+
return require('crypto')
|
|
773
|
+
.createHmac('sha256', String(process.env.NEXTAUTH_SECRET || ''))
|
|
774
|
+
.update('teleport-password-fingerprint:v1|' + (storedHash ? String(storedHash) : ''))
|
|
775
|
+
.digest('base64')
|
|
776
|
+
.slice(0, 16);
|
|
777
|
+
}
|
|
778
|
+
|
|
761
779
|
const authOptions = {
|
|
762
780
|
providers: providers,
|
|
763
781
|
pages: {
|
|
@@ -803,6 +821,7 @@ const authOptions = {
|
|
|
803
821
|
if (params.trigger !== 'update' && isFresh) {
|
|
804
822
|
return token;
|
|
805
823
|
}
|
|
824
|
+
let passwordChanged = false;
|
|
806
825
|
try {
|
|
807
826
|
if (token && token.email && typeof findUserByEmail === 'function') {
|
|
808
827
|
const fresh = await findUserByEmail(String(token.email));
|
|
@@ -813,10 +832,23 @@ const authOptions = {
|
|
|
813
832
|
token[fk[i]] = safe[fk[i]];
|
|
814
833
|
}
|
|
815
834
|
}
|
|
835
|
+
if (fresh) {
|
|
836
|
+
const currentFingerprint = passwordFingerprint(fresh.password);
|
|
837
|
+
if (typeof token[TOKEN_PASSWORD_FINGERPRINT] !== 'string') {
|
|
838
|
+
token[TOKEN_PASSWORD_FINGERPRINT] = currentFingerprint;
|
|
839
|
+
} else if (token[TOKEN_PASSWORD_FINGERPRINT] !== currentFingerprint) {
|
|
840
|
+
passwordChanged = true;
|
|
841
|
+
}
|
|
842
|
+
}
|
|
816
843
|
}
|
|
817
844
|
} catch (e) {
|
|
818
845
|
// Keep the existing token on any DB hiccup — never sign the user out.
|
|
819
846
|
}
|
|
847
|
+
if (passwordChanged) {
|
|
848
|
+
// NextAuth answers a jwt callback that throws by clearing the session
|
|
849
|
+
// cookie and returning no session: the visitor is signed out.
|
|
850
|
+
throw new Error('The account password changed since this session signed in');
|
|
851
|
+
}
|
|
820
852
|
// Stamped even when the read threw, so an unreachable database costs one
|
|
821
853
|
// attempt per interval instead of one per request.
|
|
822
854
|
if (token) {
|
|
@@ -830,6 +862,7 @@ const authOptions = {
|
|
|
830
862
|
if (token && session.user) {
|
|
831
863
|
const skip = { iat: 1, exp: 1, jti: 1, sub: 1 };
|
|
832
864
|
skip[TOKEN_REFRESH_STAMP] = 1;
|
|
865
|
+
skip[TOKEN_PASSWORD_FINGERPRINT] = 1;
|
|
833
866
|
const keys = Object.keys(token);
|
|
834
867
|
for (let i = 0; i < keys.length; i++) {
|
|
835
868
|
if (!skip[keys[i]]) {
|
package/src/await-result.ts
CHANGED
|
@@ -15,6 +15,15 @@ export const DATA_NODE_TYPES: ReadonlyArray<string> = [
|
|
|
15
15
|
'data-delete-item',
|
|
16
16
|
]
|
|
17
17
|
|
|
18
|
+
/**
|
|
19
|
+
* The other steps the author may choose not to wait for: server work whose
|
|
20
|
+
* result nothing downstream needs (a cash order's invoice, rendered, stored
|
|
21
|
+
* and mailed while the buyer is already on the confirmation page). The
|
|
22
|
+
* editor offers the choice on the same steps (`supportsAwaitResultOption` in
|
|
23
|
+
* teleport-gui's `@teleport/workflow-schema`) — a PAIRED EDIT.
|
|
24
|
+
*/
|
|
25
|
+
export const NOT_AWAITABLE_SERVER_NODE_TYPES: ReadonlyArray<string> = ['ecommerce-generate-invoice']
|
|
26
|
+
|
|
18
27
|
/**
|
|
19
28
|
* Config key carrying the author's await choice. Only `false` opts out —
|
|
20
29
|
* `undefined` (never configured) and `true` both mean "await", so every
|
|
@@ -22,14 +31,17 @@ export const DATA_NODE_TYPES: ReadonlyArray<string> = [
|
|
|
22
31
|
*/
|
|
23
32
|
export const AWAIT_RESULT_CONFIG_KEY = 'awaitResult'
|
|
24
33
|
|
|
25
|
-
const
|
|
34
|
+
const AWAIT_OPTIONAL_NODE_TYPE_SET = new Set<string>([
|
|
35
|
+
...DATA_NODE_TYPES,
|
|
36
|
+
...NOT_AWAITABLE_SERVER_NODE_TYPES,
|
|
37
|
+
])
|
|
26
38
|
|
|
27
39
|
/**
|
|
28
|
-
* True when this node runs fire-and-forget: the runtime starts the
|
|
40
|
+
* True when this node runs fire-and-forget: the runtime starts the step,
|
|
29
41
|
* publishes `null` under the node's id and moves straight on to the next node.
|
|
30
42
|
*/
|
|
31
43
|
export const isFireAndForgetNode = (node: UIDLWorkflowNode | undefined | null): boolean => {
|
|
32
|
-
if (!node || !
|
|
44
|
+
if (!node || !AWAIT_OPTIONAL_NODE_TYPE_SET.has(node.type)) {
|
|
33
45
|
return false
|
|
34
46
|
}
|
|
35
47
|
const config = node.config as Record<string, unknown> | undefined
|
|
@@ -41,10 +53,10 @@ export const isFireAndForgetNode = (node: UIDLWorkflowNode | undefined | null):
|
|
|
41
53
|
* response — i.e. every node it holds is fire-and-forget, so nothing
|
|
42
54
|
* downstream can read anything the segment produces.
|
|
43
55
|
*
|
|
44
|
-
* The API route itself still
|
|
45
|
-
*
|
|
46
|
-
*
|
|
47
|
-
*
|
|
56
|
+
* The API route itself still lands each step (a serverless function may be
|
|
57
|
+
* frozen the moment it responds, which would drop an in-flight write: on
|
|
58
|
+
* Vercel the step finishes after the reply, elsewhere the reply waits); what
|
|
59
|
+
* this flag removes is the CLIENT's wait on that round trip.
|
|
48
60
|
*/
|
|
49
61
|
export const isFireAndForgetSegment = (segment: WorkflowSegment): boolean =>
|
|
50
62
|
segment.env === 'server' &&
|
|
@@ -1219,7 +1219,15 @@ module.exports = async function handler(req, res) {
|
|
|
1219
1219
|
}
|
|
1220
1220
|
|
|
1221
1221
|
var operation = params[params.length - 1];
|
|
1222
|
-
var body
|
|
1222
|
+
var body;
|
|
1223
|
+
try {
|
|
1224
|
+
body = typeof req.body === 'string' ? JSON.parse(req.body) : req.body;
|
|
1225
|
+
} catch (e) {
|
|
1226
|
+
body = null;
|
|
1227
|
+
}
|
|
1228
|
+
if (!body || typeof body !== 'object' || Array.isArray(body)) {
|
|
1229
|
+
return res.status(400).json({ error: 'Invalid request body. Expected a JSON object.' });
|
|
1230
|
+
}
|
|
1223
1231
|
var client = getClient();
|
|
1224
1232
|
|
|
1225
1233
|
try {
|
|
@@ -655,9 +655,15 @@ function normalizeComparisonOperator(op) {
|
|
|
655
655
|
.toLowerCase();
|
|
656
656
|
}
|
|
657
657
|
|
|
658
|
+
// The editor's templates write a comparison's sides as leftOperand /
|
|
659
|
+
// rightOperand; everything else writes leftValue / rightValue.
|
|
660
|
+
function comparisonSide(config, valueKey, operandKey) {
|
|
661
|
+
return config[valueKey] !== undefined ? config[valueKey] : config[operandKey];
|
|
662
|
+
}
|
|
663
|
+
|
|
658
664
|
function evaluateSingleComparison(config, context) {
|
|
659
|
-
var rawLeft = resolveValue(config
|
|
660
|
-
var rawRight = resolveValue(config
|
|
665
|
+
var rawLeft = resolveValue(comparisonSide(config, 'leftValue', 'leftOperand'), context);
|
|
666
|
+
var rawRight = resolveValue(comparisonSide(config, 'rightValue', 'rightOperand'), context);
|
|
661
667
|
const op = normalizeComparisonOperator(config.operator || 'equals');
|
|
662
668
|
var isUnary = op === 'is-empty' || op === 'is-not-empty' || op === 'is-truthy' || op === 'is-falsy' ||
|
|
663
669
|
op === 'is-null' || op === 'is-not-null' || op === 'is-true' || op === 'is-false';
|
|
@@ -667,7 +673,7 @@ function evaluateSingleComparison(config, context) {
|
|
|
667
673
|
switch (op) {
|
|
668
674
|
case '===': return left === right;
|
|
669
675
|
case '!==': return left !== right;
|
|
670
|
-
case '==': case 'equals': return left == right;
|
|
676
|
+
case '=': case '==': case 'equals': return left == right;
|
|
671
677
|
case '!=': case 'not-equals': return left != right;
|
|
672
678
|
case '>': case 'greater-than': return left > right;
|
|
673
679
|
case '<': case 'less-than': return left < right;
|
|
@@ -809,10 +815,16 @@ var __DATA_NODE_TYPES = {
|
|
|
809
815
|
'data-delete-item': true
|
|
810
816
|
};
|
|
811
817
|
|
|
812
|
-
//
|
|
813
|
-
|
|
818
|
+
// Mirrors NOT_AWAITABLE_SERVER_NODE_TYPES in await-result.ts — PAIRED EDIT.
|
|
819
|
+
var __NOT_AWAITABLE_SERVER_NODE_TYPES = {
|
|
820
|
+
'ecommerce-generate-invoice': true
|
|
821
|
+
};
|
|
822
|
+
|
|
823
|
+
// A data node (or a step listed above) the author opted out of awaiting. Only
|
|
824
|
+
// an explicit \`false\` opts out, so a config written before the option existed
|
|
825
|
+
// keeps awaiting.
|
|
814
826
|
function isFireAndForgetNode(node) {
|
|
815
|
-
if (!node || !__DATA_NODE_TYPES[node.type]) return false;
|
|
827
|
+
if (!node || !(__DATA_NODE_TYPES[node.type] || __NOT_AWAITABLE_SERVER_NODE_TYPES[node.type])) return false;
|
|
816
828
|
return !!node.config && node.config.awaitResult === false;
|
|
817
829
|
}
|
|
818
830
|
|
|
@@ -265,6 +265,54 @@ function formatDateValue(raw) {
|
|
|
265
265
|
}
|
|
266
266
|
}
|
|
267
267
|
|
|
268
|
+
// How the invoice says it was paid: the method in words, with the provider that
|
|
269
|
+
// took the money when a provider did ("Card (Stripe)", "Paid online" for a
|
|
270
|
+
// booking paid through the site, "Cash on delivery", "Gift card"), and the
|
|
271
|
+
// provider's payment id as "Ref. pi_…". The stored values are ids.
|
|
272
|
+
var PAYMENT_PROVIDER_LABELS = {
|
|
273
|
+
stripe: 'Stripe', paypal: 'PayPal', mollie: 'Mollie', razorpay: 'Razorpay',
|
|
274
|
+
square: 'Square', paddle: 'Paddle', coingate: 'CoinGate',
|
|
275
|
+
};
|
|
276
|
+
var PAYMENT_METHOD_LABELS = {
|
|
277
|
+
card: 'Card',
|
|
278
|
+
online: 'Paid online',
|
|
279
|
+
cash_on_delivery: 'Cash on delivery',
|
|
280
|
+
cod: 'Cash on delivery',
|
|
281
|
+
cash: 'Cash',
|
|
282
|
+
store_pickup: 'Paid at pickup',
|
|
283
|
+
pickup: 'Paid at pickup',
|
|
284
|
+
gift_card: 'Gift card',
|
|
285
|
+
bank_transfer: 'Bank transfer',
|
|
286
|
+
};
|
|
287
|
+
var PAYMENT_METHODS_NAMING_PROVIDER = { card: true, online: true };
|
|
288
|
+
|
|
289
|
+
function humanizePaymentId(value) {
|
|
290
|
+
var words = String(value).replace(/[_-]+/g, ' ').trim();
|
|
291
|
+
return words.charAt(0).toUpperCase() + words.slice(1);
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
function paymentProviderLabel(provider) {
|
|
295
|
+
var id = String(provider || '').trim().toLowerCase();
|
|
296
|
+
return PAYMENT_PROVIDER_LABELS[id] || (id ? humanizePaymentId(id) : '');
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
function paymentMethodLabel(method, provider) {
|
|
300
|
+
var id = String(method || '').trim().toLowerCase();
|
|
301
|
+
var providerLabel = paymentProviderLabel(provider);
|
|
302
|
+
if (PAYMENT_METHOD_LABELS[id]) {
|
|
303
|
+
return PAYMENT_METHOD_LABELS[id] +
|
|
304
|
+
(PAYMENT_METHODS_NAMING_PROVIDER[id] && providerLabel ? ' (' + providerLabel + ')' : '');
|
|
305
|
+
}
|
|
306
|
+
if (PAYMENT_PROVIDER_LABELS[id]) return 'Paid online (' + PAYMENT_PROVIDER_LABELS[id] + ')';
|
|
307
|
+
if (!id) return providerLabel ? 'Paid online (' + providerLabel + ')' : '';
|
|
308
|
+
return humanizePaymentId(id);
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
function paymentReference(paymentId) {
|
|
312
|
+
var ref = String(paymentId || '').trim();
|
|
313
|
+
return ref ? 'Ref. ' + ref : '';
|
|
314
|
+
}
|
|
315
|
+
|
|
268
316
|
function buildInvoiceDataScope(invoiceData) {
|
|
269
317
|
var contextCompany = extractStaticAttrsFromContext(TEMPLATE_COMPONENT_UIDL).Company || {};
|
|
270
318
|
var sym = invoiceData.currencySymbol || '$';
|
|
@@ -311,10 +359,13 @@ function buildInvoiceDataScope(invoiceData) {
|
|
|
311
359
|
var giftCardLast4 = String(invoiceData.giftCardLast4 || '').trim();
|
|
312
360
|
var totalNumber = Number(invoiceData.total);
|
|
313
361
|
if (!isFinite(totalNumber)) totalNumber = 0;
|
|
362
|
+
// What is left to pay after the gift card. The invoice records what the
|
|
363
|
+
// provider or the courier was to collect; once that is paid, nothing is left.
|
|
314
364
|
var amountDueNumber = Number(invoiceData.amountDue);
|
|
315
365
|
if (invoiceData.amountDue == null || !isFinite(amountDueNumber)) {
|
|
316
366
|
amountDueNumber = Math.max(0, totalNumber - giftCardAmountNumber);
|
|
317
367
|
}
|
|
368
|
+
if (isSettledInvoice(invoiceData.status)) amountDueNumber = 0;
|
|
318
369
|
|
|
319
370
|
// A paid invoice asks for nothing: where the due date would print, it says
|
|
320
371
|
// when it was paid. The order keeps no payment method finer than its
|
|
@@ -376,9 +427,9 @@ function buildInvoiceDataScope(invoiceData) {
|
|
|
376
427
|
realCustomer.hasVat = realCustomer.vat ? 'true' : 'false';
|
|
377
428
|
|
|
378
429
|
var realPayment = {
|
|
379
|
-
method: invoiceData.paymentMethod
|
|
380
|
-
provider: invoiceData.paymentProvider
|
|
381
|
-
intentId: invoiceData.paymentIntentId
|
|
430
|
+
method: paymentMethodLabel(invoiceData.paymentMethod, invoiceData.paymentProvider),
|
|
431
|
+
provider: paymentProviderLabel(invoiceData.paymentProvider),
|
|
432
|
+
intentId: paymentReference(invoiceData.paymentIntentId),
|
|
382
433
|
orderId: invoiceData.orderId || '',
|
|
383
434
|
};
|
|
384
435
|
|
|
@@ -746,6 +797,39 @@ function renderComponentReference(content, scope, depth) {
|
|
|
746
797
|
return rendered;
|
|
747
798
|
}
|
|
748
799
|
|
|
800
|
+
// A paid invoice asks for nothing, so, as in the email, its amount-due line is
|
|
801
|
+
// left out. The template shows that line whenever a gift card was used, which
|
|
802
|
+
// cannot tell whether the rest was paid since. The line is the innermost
|
|
803
|
+
// container holding the field (the editor lays each summary line out as a row
|
|
804
|
+
// of texts), and only when the field is all it holds: a container that also
|
|
805
|
+
// holds a block or another field is the merchant's own, and stays.
|
|
806
|
+
var AMOUNT_DUE_FIELD = /\\bInvoice\\??\\.amountDue\\b/;
|
|
807
|
+
|
|
808
|
+
function isPaidAmountDueLine(content, scope) {
|
|
809
|
+
var inv = scope.invoiceData && scope.invoiceData.Invoice;
|
|
810
|
+
if (!inv || !isSettledInvoice(inv.status)) return false;
|
|
811
|
+
var fields = { amountDue: 0, other: 0 };
|
|
812
|
+
return countLineFields(content.children, fields) && fields.amountDue > 0 && fields.other === 0;
|
|
813
|
+
}
|
|
814
|
+
|
|
815
|
+
// Counts the fields a line prints into \`fields\`; false when it holds anything
|
|
816
|
+
// but text, which makes it a block rather than a line.
|
|
817
|
+
function countLineFields(children, fields) {
|
|
818
|
+
var list = children || [];
|
|
819
|
+
for (var i = 0; i < list.length; i++) {
|
|
820
|
+
var child = list[i];
|
|
821
|
+
if (!child || child.type === 'static') continue;
|
|
822
|
+
if (child.type === 'expr') {
|
|
823
|
+
if (AMOUNT_DUE_FIELD.test(String(child.content))) fields.amountDue++;
|
|
824
|
+
else fields.other++;
|
|
825
|
+
continue;
|
|
826
|
+
}
|
|
827
|
+
var isText = child.type === 'element' && child.content && child.content.elementType === 'text';
|
|
828
|
+
if (!isText || !countLineFields(child.content.children, fields)) return false;
|
|
829
|
+
}
|
|
830
|
+
return true;
|
|
831
|
+
}
|
|
832
|
+
|
|
749
833
|
function renderNode(node, scope, componentName, depth) {
|
|
750
834
|
if (!node) return '';
|
|
751
835
|
if (typeof depth !== 'number') depth = 0;
|
|
@@ -801,6 +885,10 @@ function renderNode(node, scope, componentName, depth) {
|
|
|
801
885
|
return fragChildren.map(function (c) { return renderNode(c, scope, componentName, depth); }).join('');
|
|
802
886
|
}
|
|
803
887
|
|
|
888
|
+
if (elementType === 'container' && isPaidAmountDueLine(content, scope)) {
|
|
889
|
+
return '';
|
|
890
|
+
}
|
|
891
|
+
|
|
804
892
|
var tag = content.semanticType || elementTypeToTag(elementType);
|
|
805
893
|
var baseClass = classNameForNode(componentName, content.name);
|
|
806
894
|
var referencedClasses = collectReferencedStyleClasses(content.referencedStyles);
|
|
@@ -1034,11 +1122,14 @@ function buildFallbackInvoiceHtml(scope) {
|
|
|
1034
1122
|
}
|
|
1035
1123
|
totalsRows += '<div style="display:flex;justify-content:space-between;font-weight:bold;border-top:2px solid #333;padding-top:4px;"><span>Total</span><span>' + escapeHtml(inv.total) + '</span></div>';
|
|
1036
1124
|
// A gift card settles part (or all) of the total, so what is left to pay is
|
|
1037
|
-
// printed beneath it — 0.00 included, which is the buyer's proof of that
|
|
1125
|
+
// printed beneath it — 0.00 included, which is the buyer's proof of that —
|
|
1126
|
+
// until the invoice is paid, whose paid date says nothing is left.
|
|
1038
1127
|
if (inv.hasGiftCard === 'true') {
|
|
1039
1128
|
var giftCardLabel = 'Paid by gift card' + (inv.giftCardLast4 ? ' \\u2022\\u2022\\u2022\\u2022' + inv.giftCardLast4 : '');
|
|
1040
1129
|
totalsRows += '<div style="display:flex;justify-content:space-between;margin-top:4px;margin-bottom:4px;"><span>' + escapeHtml(giftCardLabel) + '</span><span>\\u2212' + escapeHtml(inv.giftCardAmount) + '</span></div>';
|
|
1041
|
-
|
|
1130
|
+
if (!isSettledInvoice(inv.status)) {
|
|
1131
|
+
totalsRows += '<div style="display:flex;justify-content:space-between;font-weight:bold;"><span>Amount due</span><span>' + escapeHtml(inv.amountDue) + '</span></div>';
|
|
1132
|
+
}
|
|
1042
1133
|
}
|
|
1043
1134
|
parts.push('<div style="display:flex;justify-content:flex-end;margin-top:16px;"><div style="width:240px;">' +
|
|
1044
1135
|
totalsRows +
|
|
@@ -18,10 +18,13 @@ async function account_delete_current(config: any, context: Record<string, unkno
|
|
|
18
18
|
if (typeof runLocale === 'string' && runLocale) {
|
|
19
19
|
deleteHeaders['x-teleport-locale'] = runLocale
|
|
20
20
|
}
|
|
21
|
+
// A template that asks for the password passes what the visitor typed; the
|
|
22
|
+
// route checks it the way sign-in does and deletes nothing when it is wrong.
|
|
23
|
+
const password = config && typeof config.password === 'string' ? config.password : ''
|
|
21
24
|
const response = await fetch(baseUrl + '/api/account/delete-current', {
|
|
22
25
|
method: 'POST',
|
|
23
26
|
headers: deleteHeaders,
|
|
24
|
-
body:
|
|
27
|
+
body: JSON.stringify(password ? { password } : {}),
|
|
25
28
|
})
|
|
26
29
|
|
|
27
30
|
let data: any = {}
|