@teleporthq/teleport-plugin-next-workflows 0.45.2 → 0.45.4
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__/analytics-track-outcome.test.ts +71 -3
- package/__tests__/auth-password-security.test.ts +101 -0
- package/__tests__/automations-bus.test.ts +85 -2
- package/__tests__/security-lists-parity.test.ts +2 -2
- package/__tests__/security-runtime.test.ts +37 -20
- package/dist/cjs/auth-generator.d.ts.map +1 -1
- package/dist/cjs/auth-generator.js +48 -19
- package/dist/cjs/auth-generator.js.map +1 -1
- package/dist/cjs/automations/event-bus-source.d.ts.map +1 -1
- package/dist/cjs/automations/event-bus-source.js +37 -23
- package/dist/cjs/automations/event-bus-source.js.map +1 -1
- package/dist/cjs/nodes/account/account-request-password-reset.d.ts.map +1 -1
- package/dist/cjs/nodes/account/account-request-password-reset.js +3 -2
- package/dist/cjs/nodes/account/account-request-password-reset.js.map +1 -1
- package/dist/cjs/nodes/analytics/analytics-track-outcome.d.ts.map +1 -1
- package/dist/cjs/nodes/analytics/analytics-track-outcome.js +17 -4
- package/dist/cjs/nodes/analytics/analytics-track-outcome.js.map +1 -1
- package/dist/cjs/password-reset-route-generator.d.ts +2 -0
- package/dist/cjs/password-reset-route-generator.d.ts.map +1 -1
- package/dist/cjs/password-reset-route-generator.js +17 -10
- package/dist/cjs/password-reset-route-generator.js.map +1 -1
- package/dist/cjs/security-runtime/security-runtime-code.d.ts +3 -2
- package/dist/cjs/security-runtime/security-runtime-code.d.ts.map +1 -1
- package/dist/cjs/security-runtime/security-runtime-code.js +17 -13
- package/dist/cjs/security-runtime/security-runtime-code.js.map +1 -1
- package/dist/cjs/tsconfig.tsbuildinfo +1 -1
- package/dist/esm/auth-generator.d.ts.map +1 -1
- package/dist/esm/auth-generator.js +48 -19
- package/dist/esm/auth-generator.js.map +1 -1
- package/dist/esm/automations/event-bus-source.d.ts.map +1 -1
- package/dist/esm/automations/event-bus-source.js +37 -23
- package/dist/esm/automations/event-bus-source.js.map +1 -1
- package/dist/esm/nodes/account/account-request-password-reset.d.ts.map +1 -1
- package/dist/esm/nodes/account/account-request-password-reset.js +3 -2
- package/dist/esm/nodes/account/account-request-password-reset.js.map +1 -1
- package/dist/esm/nodes/analytics/analytics-track-outcome.d.ts.map +1 -1
- package/dist/esm/nodes/analytics/analytics-track-outcome.js +17 -4
- package/dist/esm/nodes/analytics/analytics-track-outcome.js.map +1 -1
- package/dist/esm/password-reset-route-generator.d.ts +2 -0
- package/dist/esm/password-reset-route-generator.d.ts.map +1 -1
- package/dist/esm/password-reset-route-generator.js +16 -9
- package/dist/esm/password-reset-route-generator.js.map +1 -1
- package/dist/esm/security-runtime/security-runtime-code.d.ts +3 -2
- package/dist/esm/security-runtime/security-runtime-code.d.ts.map +1 -1
- package/dist/esm/security-runtime/security-runtime-code.js +17 -13
- package/dist/esm/security-runtime/security-runtime-code.js.map +1 -1
- package/dist/esm/tsconfig.tsbuildinfo +1 -1
- package/package.json +2 -2
- package/src/auth-generator.ts +48 -19
- package/src/automations/event-bus-source.ts +37 -23
- package/src/nodes/account/account-request-password-reset.ts +3 -2
- package/src/nodes/analytics/analytics-track-outcome.ts +17 -4
- package/src/password-reset-route-generator.ts +18 -9
- package/src/security-runtime/security-runtime-code.ts +17 -13
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@teleporthq/teleport-plugin-next-workflows",
|
|
3
|
-
"version": "0.45.
|
|
3
|
+
"version": "0.45.4",
|
|
4
4
|
"description": "A plugin for workflow code generation in Next.js projects.",
|
|
5
5
|
"author": "teleportHQ",
|
|
6
6
|
"license": "MIT",
|
|
@@ -69,5 +69,5 @@
|
|
|
69
69
|
"@teleporthq/teleport-shared": "^0.45.0",
|
|
70
70
|
"@teleporthq/teleport-types": "^0.45.0"
|
|
71
71
|
},
|
|
72
|
-
"gitHead": "
|
|
72
|
+
"gitHead": "4fc86526c794a1304f0298809bf3530b4d380486"
|
|
73
73
|
}
|
package/src/auth-generator.ts
CHANGED
|
@@ -730,13 +730,15 @@ export const PASSWORD_RESETS_TABLE = 'teleport_password_resets'
|
|
|
730
730
|
const PG_PASSWORD_RESET_FUNCTIONS = `
|
|
731
731
|
// Password-reset links (pages/api/auth/forgot-password + reset-password). Only
|
|
732
732
|
// the SHA-256 of a link's token is stored, bound to the account id, with its
|
|
733
|
-
// expiry
|
|
734
|
-
//
|
|
733
|
+
// expiry. The editor makes the table with the other account tables, and a site's
|
|
734
|
+
// database role may read and write it without being allowed to create one
|
|
735
|
+
// (CREATE ... IF NOT EXISTS asks for that permission even when the table is
|
|
736
|
+
// there), so a create is attempted only for a table that really is missing. The
|
|
737
|
+
// earlier password_reset_tokens table and its rows are never read.
|
|
735
738
|
var PASSWORD_RESETS_TABLE = '${PASSWORD_RESETS_TABLE}';
|
|
736
|
-
var
|
|
739
|
+
var UNDEFINED_TABLE_CODE = '42P01';
|
|
737
740
|
|
|
738
|
-
async function
|
|
739
|
-
if (__passwordResetsTableReady) return;
|
|
741
|
+
async function createPasswordResetsTable(client) {
|
|
740
742
|
await client.query('BEGIN');
|
|
741
743
|
try {
|
|
742
744
|
await client.query('SELECT pg_advisory_xact_lock(hashtext($1))', [PASSWORD_RESETS_TABLE]);
|
|
@@ -748,28 +750,53 @@ async function ensurePasswordResetsTable(client) {
|
|
|
748
750
|
await client.query('COMMIT');
|
|
749
751
|
} catch (err) {
|
|
750
752
|
try { await client.query('ROLLBACK'); } catch (_e) {}
|
|
751
|
-
throw err;
|
|
753
|
+
throw new Error('[password-reset] the table ' + PASSWORD_RESETS_TABLE + ' is missing and could not be created: ' + (err && err.message ? err.message : String(err)));
|
|
754
|
+
}
|
|
755
|
+
}
|
|
756
|
+
|
|
757
|
+
// Throws when a link could not be kept. Asked for every address, with an account
|
|
758
|
+
// or without, so a table that cannot be used refuses them all alike.
|
|
759
|
+
async function ensurePasswordResetsUsable() {
|
|
760
|
+
const client = getClient();
|
|
761
|
+
try {
|
|
762
|
+
await client.connect();
|
|
763
|
+
try {
|
|
764
|
+
await client.query('SELECT 1 FROM ' + PASSWORD_RESETS_TABLE + ' LIMIT 1');
|
|
765
|
+
} catch (err) {
|
|
766
|
+
if (!err || err.code !== UNDEFINED_TABLE_CODE) throw err;
|
|
767
|
+
await createPasswordResetsTable(client);
|
|
768
|
+
}
|
|
769
|
+
} finally {
|
|
770
|
+
try { await client.end(); } catch (_e) {}
|
|
752
771
|
}
|
|
753
|
-
__passwordResetsTableReady = true;
|
|
754
772
|
}
|
|
755
773
|
|
|
756
774
|
// One live link per account: a new one ends the ones sent before it.
|
|
775
|
+
async function writePasswordReset(client, userId, tokenHash, ttlSeconds) {
|
|
776
|
+
await client.query(
|
|
777
|
+
'UPDATE ' + PASSWORD_RESETS_TABLE + ' SET used_at = NOW() WHERE user_id = $1 AND used_at IS NULL',
|
|
778
|
+
[String(userId)]
|
|
779
|
+
);
|
|
780
|
+
await client.query(
|
|
781
|
+
'INSERT INTO ' + PASSWORD_RESETS_TABLE + " (token_hash, user_id, expires_at) VALUES ($1, $2, NOW() + ($3::int * INTERVAL '1 second'))",
|
|
782
|
+
[tokenHash, String(userId), ttlSeconds]
|
|
783
|
+
);
|
|
784
|
+
await client.query(
|
|
785
|
+
'DELETE FROM ' + PASSWORD_RESETS_TABLE + " WHERE expires_at < NOW() - INTERVAL '1 day'"
|
|
786
|
+
);
|
|
787
|
+
}
|
|
788
|
+
|
|
757
789
|
async function storePasswordReset(userId, tokenHash, ttlSeconds) {
|
|
758
790
|
const client = getClient();
|
|
759
791
|
try {
|
|
760
792
|
await client.connect();
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
[tokenHash, String(userId), ttlSeconds]
|
|
769
|
-
);
|
|
770
|
-
await client.query(
|
|
771
|
-
'DELETE FROM ' + PASSWORD_RESETS_TABLE + " WHERE expires_at < NOW() - INTERVAL '1 day'"
|
|
772
|
-
);
|
|
793
|
+
try {
|
|
794
|
+
await writePasswordReset(client, userId, tokenHash, ttlSeconds);
|
|
795
|
+
} catch (err) {
|
|
796
|
+
if (!err || err.code !== UNDEFINED_TABLE_CODE) throw err;
|
|
797
|
+
await createPasswordResetsTable(client);
|
|
798
|
+
await writePasswordReset(client, userId, tokenHash, ttlSeconds);
|
|
799
|
+
}
|
|
773
800
|
} finally {
|
|
774
801
|
try { await client.end(); } catch (_e) {}
|
|
775
802
|
}
|
|
@@ -825,6 +852,7 @@ const PASSWORD_RESET_SUPPORTED = true;`
|
|
|
825
852
|
const UNSUPPORTED_PASSWORD_RESET_FUNCTIONS = `
|
|
826
853
|
// Password-reset links need a Postgres database; this project's user store is
|
|
827
854
|
// not one, so the reset routes answer without ever sending a link.
|
|
855
|
+
async function ensurePasswordResetsUsable() { throw new Error('Password reset needs a Postgres database'); }
|
|
828
856
|
async function storePasswordReset() { throw new Error('Password reset needs a Postgres database'); }
|
|
829
857
|
async function consumePasswordReset() { return null; }
|
|
830
858
|
async function passwordResetIsLive() { return false; }
|
|
@@ -1349,6 +1377,7 @@ ${
|
|
|
1349
1377
|
module.exports.checkSignInAllowed = checkSignInAllowed;
|
|
1350
1378
|
module.exports.countSignInFailure = countSignInFailure;
|
|
1351
1379
|
module.exports.SIGN_IN_VISITOR_HEADER = SIGN_IN_VISITOR_HEADER;
|
|
1380
|
+
module.exports.ensurePasswordResetsUsable = ensurePasswordResetsUsable;
|
|
1352
1381
|
module.exports.storePasswordReset = storePasswordReset;
|
|
1353
1382
|
module.exports.consumePasswordReset = consumePasswordReset;
|
|
1354
1383
|
module.exports.passwordResetIsLive = passwordResetIsLive;
|
|
@@ -89,10 +89,15 @@ async function saveWait(wait) {
|
|
|
89
89
|
|
|
90
90
|
// The signed requests the dispatch route has taken, each kept until its
|
|
91
91
|
// signature can no longer verify: the same request again is refused. The
|
|
92
|
-
//
|
|
93
|
-
//
|
|
94
|
-
//
|
|
92
|
+
// editor makes the table with the other automation tables, and a site's
|
|
93
|
+
// database role may read and write it without being allowed to create one
|
|
94
|
+
// (CREATE ... IF NOT EXISTS asks for that permission even when the table is
|
|
95
|
+
// there), so a create is attempted only for a table that really is missing,
|
|
96
|
+
// two instances taking turns under the lock. The rows that are over are
|
|
97
|
+
// deleted at most once per instance every SIGNED_REQUESTS_CLEANUP_MS, a
|
|
98
|
+
// bounded batch at a time.
|
|
95
99
|
var SIGNED_REQUESTS_TABLE = '${SIGNED_REQUESTS_TABLE}';
|
|
100
|
+
var UNDEFINED_TABLE_CODE = '42P01';
|
|
96
101
|
var LOCK_SIGNED_REQUESTS_SQL = 'SELECT pg_advisory_xact_lock(hashtext($1))';
|
|
97
102
|
var CREATE_SIGNED_REQUESTS_SQL = 'CREATE TABLE IF NOT EXISTS ' + SIGNED_REQUESTS_TABLE +
|
|
98
103
|
' (request_key TEXT PRIMARY KEY, expires_at BIGINT NOT NULL)';
|
|
@@ -103,34 +108,43 @@ var FORGET_SIGNED_REQUESTS_SQL = 'DELETE FROM ' + SIGNED_REQUESTS_TABLE + ' WHER
|
|
|
103
108
|
var TAKE_SIGNED_REQUEST_SQL = 'INSERT INTO ' + SIGNED_REQUESTS_TABLE + ' (request_key, expires_at) VALUES ($1, $2) ' +
|
|
104
109
|
'ON CONFLICT (request_key) DO NOTHING RETURNING request_key';
|
|
105
110
|
var SIGNED_REQUESTS_CLEANUP_MS = 600000;
|
|
106
|
-
var __signedRequestsReady = false;
|
|
107
111
|
var __signedRequestsCleanedAt = 0;
|
|
108
112
|
|
|
113
|
+
async function createSignedRequestsTable(client) {
|
|
114
|
+
await client.query('BEGIN');
|
|
115
|
+
try {
|
|
116
|
+
await client.query(LOCK_SIGNED_REQUESTS_SQL, [SIGNED_REQUESTS_TABLE]);
|
|
117
|
+
await client.query(CREATE_SIGNED_REQUESTS_SQL);
|
|
118
|
+
await client.query(INDEX_SIGNED_REQUESTS_SQL);
|
|
119
|
+
await client.query('COMMIT');
|
|
120
|
+
} catch (error) {
|
|
121
|
+
try { await client.query('ROLLBACK'); } catch (_e) {}
|
|
122
|
+
throw new Error('[automations] the table ' + SIGNED_REQUESTS_TABLE + ' is missing and could not be created: ' + errorMessage(error));
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
async function recordSignedRequest(client, key, expiresAt) {
|
|
127
|
+
var nowMs = Date.now();
|
|
128
|
+
if (nowMs - __signedRequestsCleanedAt >= SIGNED_REQUESTS_CLEANUP_MS) {
|
|
129
|
+
__signedRequestsCleanedAt = nowMs;
|
|
130
|
+
await client.query(FORGET_SIGNED_REQUESTS_SQL, [Math.floor(nowMs / 1000)]);
|
|
131
|
+
}
|
|
132
|
+
var taken = await client.query(TAKE_SIGNED_REQUEST_SQL, [key, expiresAt]);
|
|
133
|
+
return !!(taken && taken.rows && taken.rows[0]);
|
|
134
|
+
}
|
|
135
|
+
|
|
109
136
|
// true the first time a signed request (its key) is taken, false every time
|
|
110
137
|
// after while it is kept (until expiresAt, unix seconds). Throws when the
|
|
111
138
|
// database cannot say: the route then refuses.
|
|
112
139
|
async function takeSignedRequest(key, expiresAt) {
|
|
113
140
|
return withClient(async function (client) {
|
|
114
|
-
|
|
115
|
-
await client
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
await client.query('COMMIT');
|
|
121
|
-
} catch (error) {
|
|
122
|
-
try { await client.query('ROLLBACK'); } catch (_e) {}
|
|
123
|
-
throw error;
|
|
124
|
-
}
|
|
125
|
-
__signedRequestsReady = true;
|
|
126
|
-
}
|
|
127
|
-
var nowMs = Date.now();
|
|
128
|
-
if (nowMs - __signedRequestsCleanedAt >= SIGNED_REQUESTS_CLEANUP_MS) {
|
|
129
|
-
__signedRequestsCleanedAt = nowMs;
|
|
130
|
-
await client.query(FORGET_SIGNED_REQUESTS_SQL, [Math.floor(nowMs / 1000)]);
|
|
141
|
+
try {
|
|
142
|
+
return await recordSignedRequest(client, key, expiresAt);
|
|
143
|
+
} catch (error) {
|
|
144
|
+
if (!error || error.code !== UNDEFINED_TABLE_CODE) throw error;
|
|
145
|
+
await createSignedRequestsTable(client);
|
|
146
|
+
return recordSignedRequest(client, key, expiresAt);
|
|
131
147
|
}
|
|
132
|
-
var taken = await client.query(TAKE_SIGNED_REQUEST_SQL, [key, expiresAt]);
|
|
133
|
-
return !!(taken && taken.rows && taken.rows[0]);
|
|
134
148
|
});
|
|
135
149
|
}
|
|
136
150
|
|
|
@@ -5,8 +5,9 @@ import { NodeHandlerGenerator, handlerToString } from '../types'
|
|
|
5
5
|
// server: whether the address has an account, the link's token (minted there,
|
|
6
6
|
// stored hashed), the page it opens and the email itself (provider, sender and
|
|
7
7
|
// template baked from this node's config at generation). The route answers the
|
|
8
|
-
// same whether the address has an account or not, so this node
|
|
9
|
-
//
|
|
8
|
+
// same whether the address has an account or not, so this node reports success
|
|
9
|
+
// for a well-formed request unless the site could not keep a link, for any
|
|
10
|
+
// address.
|
|
10
11
|
async function account_request_password_reset(config: any, context: Record<string, unknown>) {
|
|
11
12
|
const email = config && typeof config.email === 'string' ? config.email.trim() : ''
|
|
12
13
|
if (!email) {
|
|
@@ -14,6 +14,11 @@ import { NodeHandlerGenerator } from '../types'
|
|
|
14
14
|
* carries the ids the trigger stamped on the context (`__tpa`, set by every
|
|
15
15
|
* generated trigger) plus whatever first-touch snapshot the caller binds in
|
|
16
16
|
* `attribution` (the checkout stamps `utm_*`, `referrer_host`, `landing_path`).
|
|
17
|
+
* A snapshot naming a `session_id` is the visit that placed an order, read back
|
|
18
|
+
* by a run outside that visit (a payment webhook, a cron, an admin page): its
|
|
19
|
+
* `session_id`, `visitor_id` and `exp` stand in for the trigger's ids. Otherwise
|
|
20
|
+
* the visitor id is the tracker's alone: a browser may still hold a persistent
|
|
21
|
+
* id from before its consent was withdrawn.
|
|
17
22
|
*
|
|
18
23
|
* Never throws: an outcome that cannot be reported answers `{ sent: false }`
|
|
19
24
|
* and the workflow continues — analytics must never cost a sale.
|
|
@@ -60,22 +65,30 @@ const SERVER_BODY = `async function analytics_track_outcome(config, context) {
|
|
|
60
65
|
if (baseUrl.indexOf('http') !== 0 || !publicKey || !serverKey) {
|
|
61
66
|
return { sent: false, reason: 'not-configured' };
|
|
62
67
|
}
|
|
68
|
+
// The service dedupes on this id and drops an outcome without one.
|
|
69
|
+
if (config.outcomeId == null || config.outcomeId === '') {
|
|
70
|
+
return { sent: false, reason: 'no-outcome-id' };
|
|
71
|
+
}
|
|
63
72
|
var ids = context && context.__tpa && typeof context.__tpa === 'object' ? context.__tpa : {};
|
|
64
73
|
var attribution = config.attribution && typeof config.attribution === 'object' ? config.attribution : {};
|
|
74
|
+
var visit = attribution.session_id
|
|
75
|
+
? { sessionId: attribution.session_id, visitorId: attribution.visitor_id, exp: attribution.exp }
|
|
76
|
+
: ids;
|
|
65
77
|
var outcome = {
|
|
66
78
|
name: name,
|
|
67
79
|
value: config.value == null || config.value === '' ? null : Number(config.value),
|
|
68
80
|
currency: config.currency == null ? null : String(config.currency),
|
|
69
81
|
outcomeId: config.outcomeId == null ? null : String(config.outcomeId),
|
|
70
82
|
recordId: config.recordId == null ? null : String(config.recordId),
|
|
71
|
-
sessionId:
|
|
72
|
-
visitorId:
|
|
73
|
-
path:
|
|
74
|
-
exp:
|
|
83
|
+
sessionId: visit.sessionId || null,
|
|
84
|
+
visitorId: visit.visitorId || null,
|
|
85
|
+
path: visit.path || null,
|
|
86
|
+
exp: visit.exp || null,
|
|
75
87
|
attribution: {
|
|
76
88
|
utmSource: attribution.utm_source || attribution.utmSource || null,
|
|
77
89
|
utmMedium: attribution.utm_medium || attribution.utmMedium || null,
|
|
78
90
|
utmCampaign: attribution.utm_campaign || attribution.utmCampaign || null,
|
|
91
|
+
utmContent: attribution.utm_content || attribution.utmContent || null,
|
|
79
92
|
referrerHost: attribution.referrer_host || attribution.referrerHost || null,
|
|
80
93
|
landingPath: attribution.landing_path || attribution.landingPath || null
|
|
81
94
|
}
|
|
@@ -55,6 +55,10 @@ export const DEFAULT_RESET_PAGE_PATH = '/reset-password'
|
|
|
55
55
|
export const FORGOT_PASSWORD_ANSWER =
|
|
56
56
|
'If an account exists for that email, we sent it a link to reset the password.'
|
|
57
57
|
|
|
58
|
+
/** What the route answers, to every address, when a link could not be kept. */
|
|
59
|
+
export const FORGOT_PASSWORD_NOT_SENT =
|
|
60
|
+
'We could not send the reset link. Please try again in a moment.'
|
|
61
|
+
|
|
58
62
|
/**
|
|
59
63
|
* The sessionStorage key the reset page reads the link's token from (mirrored
|
|
60
64
|
* in teleport-gui `RESET_TOKEN_STORAGE_KEY`). The published site's first head
|
|
@@ -333,7 +337,9 @@ module.exports = async function handler(req, res) {
|
|
|
333
337
|
return __security.sendClientError(res, 503, 'We could not reach the account service. Please try again in a moment.', err);
|
|
334
338
|
}
|
|
335
339
|
|
|
340
|
+
var link = null;
|
|
336
341
|
try {
|
|
342
|
+
await authUtils.ensurePasswordResetsUsable();
|
|
337
343
|
// Counted for every address, with an account or without, so neither the
|
|
338
344
|
// answer nor its timing differs.
|
|
339
345
|
var recipient = await __security.rateLimit({
|
|
@@ -345,17 +351,20 @@ module.exports = async function handler(req, res) {
|
|
|
345
351
|
if (user && userId != null && user.email && !recipient.allowed) {
|
|
346
352
|
console.warn('[forgot-password] reset link not sent: this address was sent ${FORGOT_PASSWORD_RECIPIENT_LIMIT} in the last hour');
|
|
347
353
|
} else if (user && userId != null && user.email) {
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
var token = crypto.randomBytes(32).toString('hex');
|
|
352
|
-
await authUtils.storePasswordReset(userId, sha256(token), TOKEN_TTL_SECONDS);
|
|
353
|
-
var resetUrl = trustedOrigin(req) + RESET_PAGE_PATH + TOKEN_LINK_PREFIX + token;
|
|
354
|
-
await sendResetEmail(user, resetUrl, token, __emailLocale.resolveRequestLocale(req));
|
|
355
|
-
}
|
|
354
|
+
var token = crypto.randomBytes(32).toString('hex');
|
|
355
|
+
await authUtils.storePasswordReset(userId, sha256(token), TOKEN_TTL_SECONDS);
|
|
356
|
+
link = { token: token, url: trustedOrigin(req) + RESET_PAGE_PATH + TOKEN_LINK_PREFIX + token };
|
|
356
357
|
}
|
|
357
358
|
} catch (err) {
|
|
358
|
-
|
|
359
|
+
await waitUntilElapsed(startedAt, MIN_RESPONSE_MS);
|
|
360
|
+
return __security.sendClientError(res, 503, ${JSON.stringify(FORGOT_PASSWORD_NOT_SENT)}, err);
|
|
361
|
+
}
|
|
362
|
+
if (link) {
|
|
363
|
+
try {
|
|
364
|
+
await sendResetEmail(user, link.url, link.token, __emailLocale.resolveRequestLocale(req));
|
|
365
|
+
} catch (err) {
|
|
366
|
+
console.error('[forgot-password] could not send a reset link:', err && err.message ? err.message : err);
|
|
367
|
+
}
|
|
359
368
|
}
|
|
360
369
|
|
|
361
370
|
await waitUntilElapsed(startedAt, MIN_RESPONSE_MS);
|
|
@@ -38,8 +38,9 @@ export const securityRuntimeSpecifier = (relativePrefix: string): string =>
|
|
|
38
38
|
* itself, an IPv6 one its /64 (an IPv4 tail counted as the two groups it
|
|
39
39
|
* fills). Every rate limit keyed by visitor uses it.
|
|
40
40
|
* - `rateLimit` counts fixed windows in the app's Postgres database (table
|
|
41
|
-
* `teleport_rate_limits`,
|
|
42
|
-
*
|
|
41
|
+
* `teleport_rate_limits`, which the editor makes, created here under an
|
|
42
|
+
* advisory lock only when it is really missing), so every instance of a
|
|
43
|
+
* serverless app counts together. Only an app with no
|
|
43
44
|
* database (or no `pg` driver) counts in a bounded per-instance map; a
|
|
44
45
|
* database error counts in that map for a short while and is logged once.
|
|
45
46
|
* A request waits on the database at most 1.5 s; after a failure one
|
|
@@ -102,6 +103,9 @@ var CLEANUP_MAX_BATCHES = 3;
|
|
|
102
103
|
var CLEANUP_QUERY_TIMEOUT_MS = 5000;
|
|
103
104
|
var MEMORY_MAX_WINDOWS = 10000;
|
|
104
105
|
|
|
106
|
+
// A site's database role may read and write its tables without being allowed
|
|
107
|
+
// to create one, and CREATE ... IF NOT EXISTS asks for that permission even when
|
|
108
|
+
// the table is there: these run only for a table that really is missing.
|
|
105
109
|
var CREATE_SQL = 'CREATE TABLE IF NOT EXISTS "' + RATE_LIMITS_TABLE + '" (' +
|
|
106
110
|
'"key" TEXT NOT NULL, "window_start" BIGINT NOT NULL, "count" INTEGER NOT NULL DEFAULT 0, ' +
|
|
107
111
|
'"expires_at" BIGINT NOT NULL, PRIMARY KEY ("key", "window_start"))';
|
|
@@ -298,7 +302,6 @@ function __memoryHit(storageKey, windowStart, windowSeconds, nowSec) {
|
|
|
298
302
|
return entry.count;
|
|
299
303
|
}
|
|
300
304
|
|
|
301
|
-
var __tableReady = false;
|
|
302
305
|
// Healthy, every request asks the database. After a failure, requests count in
|
|
303
306
|
// memory until __databaseRetryAt; then ONE request probes (it pushes the retry
|
|
304
307
|
// time back before it asks), so the requests beside it never wait on a
|
|
@@ -377,12 +380,15 @@ async function __databaseHit(storageKey, windowStart, expiresAt, nowSec) {
|
|
|
377
380
|
return within(client.query(sql, values || []), QUERY_TIMEOUT_MS);
|
|
378
381
|
};
|
|
379
382
|
var createTable = async function () {
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
383
|
+
try {
|
|
384
|
+
await run('BEGIN');
|
|
385
|
+
await run(LOCK_SQL, [RATE_LIMITS_TABLE]);
|
|
386
|
+
await run(CREATE_SQL);
|
|
387
|
+
await run(INDEX_SQL);
|
|
388
|
+
await run('COMMIT');
|
|
389
|
+
} catch (err) {
|
|
390
|
+
throw new Error('the table ' + RATE_LIMITS_TABLE + ' is missing and could not be created: ' + __errorText(err));
|
|
391
|
+
}
|
|
386
392
|
};
|
|
387
393
|
var hit = function () {
|
|
388
394
|
return run(HIT_SQL, [storageKey, windowStart, expiresAt]);
|
|
@@ -390,15 +396,13 @@ async function __databaseHit(storageKey, windowStart, expiresAt, nowSec) {
|
|
|
390
396
|
var handedToCleanup = false;
|
|
391
397
|
try {
|
|
392
398
|
await within(client.connect(), CONNECT_TIMEOUT_MS);
|
|
393
|
-
if (!__tableReady) await createTable();
|
|
394
399
|
var result;
|
|
395
400
|
try {
|
|
396
401
|
result = await hit();
|
|
397
402
|
} catch (err) {
|
|
398
|
-
// The table
|
|
399
|
-
// older backup): create it
|
|
403
|
+
// The table is not there (dropped, or the database restored from an
|
|
404
|
+
// older backup): create it and count this request in it.
|
|
400
405
|
if (!err || err.code !== '42P01') throw err;
|
|
401
|
-
__tableReady = false;
|
|
402
406
|
await createTable();
|
|
403
407
|
result = await hit();
|
|
404
408
|
}
|