@teleporthq/teleport-plugin-next-workflows 0.44.1 → 0.44.5
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__/account-delete-owned-rows.test.ts +488 -0
- package/dist/cjs/account-delete-route-generator.d.ts +9 -6
- package/dist/cjs/account-delete-route-generator.d.ts.map +1 -1
- package/dist/cjs/account-delete-route-generator.js +131 -1
- package/dist/cjs/account-delete-route-generator.js.map +1 -1
- package/dist/cjs/tsconfig.tsbuildinfo +1 -1
- package/dist/esm/account-delete-route-generator.d.ts +9 -6
- package/dist/esm/account-delete-route-generator.d.ts.map +1 -1
- package/dist/esm/account-delete-route-generator.js +131 -1
- package/dist/esm/account-delete-route-generator.js.map +1 -1
- package/dist/esm/tsconfig.tsbuildinfo +1 -1
- package/package.json +3 -3
- package/src/account-delete-route-generator.ts +140 -7
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.generateAccountDeleteRoute = exports.accountDeleteRouteDependencies = void 0;
|
|
4
|
+
const teleport_shared_1 = require("@teleporthq/teleport-shared");
|
|
4
5
|
const pg_client_code_1 = require("./pg-client-code");
|
|
5
6
|
const session_cookie_resolver_1 = require("./session-cookie-resolver");
|
|
6
7
|
const transactional_email_code_1 = require("./transactional-email-code");
|
|
@@ -35,11 +36,22 @@ const DELETE_TABLES = [
|
|
|
35
36
|
'teleport_cart',
|
|
36
37
|
'teleport_ai_chat_messages',
|
|
37
38
|
'teleport_ai_chat_conversations',
|
|
39
|
+
// A push subscription is the capability to notify the user's device.
|
|
40
|
+
'teleport_push_subscriptions',
|
|
38
41
|
];
|
|
39
42
|
// Business / legal records that must survive: the user link is nulled and any
|
|
40
43
|
// stored personal email is replaced with a synthetic, self-describing address.
|
|
41
|
-
const ANONYMIZE_TABLES = [
|
|
44
|
+
const ANONYMIZE_TABLES = [
|
|
45
|
+
'teleport_orders',
|
|
46
|
+
'teleport_withdrawal_requests',
|
|
47
|
+
'teleport_bookings',
|
|
48
|
+
'teleport_subscriptions',
|
|
49
|
+
'teleport_abandoned_carts',
|
|
50
|
+
'teleport_voucher_redemptions',
|
|
51
|
+
];
|
|
42
52
|
const EMAIL_COLUMNS = ['billing_email', 'shipping_email', 'customer_email', 'email'];
|
|
53
|
+
// A booking also names the customer and carries their phone; both are cleared.
|
|
54
|
+
const CLEARED_COLUMNS = { teleport_bookings: ['customer_name', 'customer_phone'] };
|
|
43
55
|
|
|
44
56
|
async function tableColumns(client, table) {
|
|
45
57
|
var r = await client.query(
|
|
@@ -62,6 +74,10 @@ async function anonymizeUserRows(client, table, userId, syntheticEmail) {
|
|
|
62
74
|
sets.push(EMAIL_COLUMNS[i] + ' = $' + params.length);
|
|
63
75
|
}
|
|
64
76
|
}
|
|
77
|
+
var cleared = CLEARED_COLUMNS[table] || [];
|
|
78
|
+
for (var c = 0; c < cleared.length; c++) {
|
|
79
|
+
if (cols.indexOf(cleared[c]) !== -1) { sets.push(cleared[c] + ' = NULL'); }
|
|
80
|
+
}
|
|
65
81
|
await client.query('UPDATE ' + table + ' SET ' + sets.join(', ') + ' WHERE user_id = $1', params);
|
|
66
82
|
}
|
|
67
83
|
|
|
@@ -78,6 +94,119 @@ async function deleteRowsByEmail(client, table, column, email) {
|
|
|
78
94
|
await client.query('DELETE FROM ' + table + ' WHERE ' + column + ' = $1', [email]);
|
|
79
95
|
}
|
|
80
96
|
|
|
97
|
+
// Rows the user owns in the project's OWN tables — the ones the AI made for
|
|
98
|
+
// this site (saved items, requests, reviews …). Their link to the user is
|
|
99
|
+
// ON DELETE SET NULL, so they outlived the account with a blank owner, and the
|
|
100
|
+
// "My …" pages read "owner = me OR owner IS NULL": every other signed-in
|
|
101
|
+
// visitor saw them. Platform tables keep the explicit handling above —
|
|
102
|
+
// deleted, anonymised, or kept as the business's records.
|
|
103
|
+
//
|
|
104
|
+
// Each table is attributed through exactly ONE column, the one its "My …"
|
|
105
|
+
// pages read it by: resolveOwnerColumn below is the AI pipeline's own
|
|
106
|
+
// decision, shipped here as text, so the two sides cannot disagree. Rows go
|
|
107
|
+
// only through an "owner" column (whose the row is: author, submitter,
|
|
108
|
+
// sender); an "actor" column (who approved, reviewed, was assigned or
|
|
109
|
+
// addressed) names somebody else's row and leaves the table alone.
|
|
110
|
+
${teleport_shared_1.OwnerColumns.generateOwnerColumnResolverCode()}
|
|
111
|
+
|
|
112
|
+
const PLATFORM_TABLE_PREFIX = 'teleport_';
|
|
113
|
+
const PLATFORM_TABLES = [AUTH_USERS_TABLE, 'password_reset_tokens'];
|
|
114
|
+
|
|
115
|
+
function isPlatformTable(table) {
|
|
116
|
+
return PLATFORM_TABLES.indexOf(table) !== -1 || table.indexOf(PLATFORM_TABLE_PREFIX) === 0;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function quoteIdentifier(name) {
|
|
120
|
+
return '"' + String(name).split('"').join('""') + '"';
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// The (table, column) pairs through which the project's own tables name their
|
|
124
|
+
// owner: the resolved owner column when it references the users table — or
|
|
125
|
+
// references nothing, in older projects whose link was dropped. One that
|
|
126
|
+
// references another table (account_id -> accounts) is a domain link and the
|
|
127
|
+
// table is left alone. Links are read from pg_constraint rather than
|
|
128
|
+
// information_schema: constraint names are only unique per table, so a join
|
|
129
|
+
// by name can pair the wrong tables.
|
|
130
|
+
async function ownedRowLinks(client) {
|
|
131
|
+
var columns = await client.query(
|
|
132
|
+
"SELECT c.table_name, c.column_name, c.ordinal_position FROM information_schema.columns c" +
|
|
133
|
+
" JOIN information_schema.tables t ON t.table_schema = c.table_schema AND t.table_name = c.table_name" +
|
|
134
|
+
" WHERE c.table_schema = 'public' AND t.table_type = 'BASE TABLE'" +
|
|
135
|
+
" ORDER BY c.table_name, c.ordinal_position"
|
|
136
|
+
);
|
|
137
|
+
var links = await client.query(
|
|
138
|
+
"SELECT cl.relname AS table_name, a.attname AS column_name," +
|
|
139
|
+
" rcl.relname AS referenced_table, ra.attname AS referenced_column" +
|
|
140
|
+
" FROM pg_constraint k" +
|
|
141
|
+
" JOIN pg_class cl ON cl.oid = k.conrelid" +
|
|
142
|
+
" JOIN pg_namespace n ON n.oid = cl.relnamespace" +
|
|
143
|
+
" JOIN pg_attribute a ON a.attrelid = k.conrelid AND a.attnum = k.conkey[1]" +
|
|
144
|
+
" JOIN pg_class rcl ON rcl.oid = k.confrelid" +
|
|
145
|
+
" JOIN pg_attribute ra ON ra.attrelid = k.confrelid AND ra.attnum = k.confkey[1]" +
|
|
146
|
+
" WHERE k.contype = 'f' AND n.nspname = 'public' AND array_length(k.conkey, 1) = 1"
|
|
147
|
+
);
|
|
148
|
+
var referenced = {};
|
|
149
|
+
for (var i = 0; i < links.rows.length; i++) {
|
|
150
|
+
var link = links.rows[i];
|
|
151
|
+
referenced[link.table_name + '.' + link.column_name] = link.referenced_table + '.' + link.referenced_column;
|
|
152
|
+
}
|
|
153
|
+
var tables = [];
|
|
154
|
+
var columnsOf = {};
|
|
155
|
+
for (var j = 0; j < columns.rows.length; j++) {
|
|
156
|
+
var row = columns.rows[j];
|
|
157
|
+
if (!columnsOf[row.table_name]) {
|
|
158
|
+
columnsOf[row.table_name] = [];
|
|
159
|
+
tables.push(row.table_name);
|
|
160
|
+
}
|
|
161
|
+
columnsOf[row.table_name].push({ name: row.column_name, ordinalPosition: Number(row.ordinal_position) });
|
|
162
|
+
}
|
|
163
|
+
var owned = [];
|
|
164
|
+
for (var t = 0; t < tables.length; t++) {
|
|
165
|
+
var table = tables[t];
|
|
166
|
+
if (isPlatformTable(table)) { continue; }
|
|
167
|
+
var resolved = resolveOwnerColumn(columnsOf[table]);
|
|
168
|
+
if (!resolved || resolved.kind !== 'owner') { continue; }
|
|
169
|
+
var target = referenced[table + '.' + resolved.column];
|
|
170
|
+
if (target && target !== AUTH_USERS_TABLE + '.id') { continue; }
|
|
171
|
+
owned.push({ table: table, column: resolved.column });
|
|
172
|
+
}
|
|
173
|
+
return owned;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
// Each table is deleted from under its own savepoint, so one that refuses
|
|
177
|
+
// (another user's row still points at this user's, through a link without
|
|
178
|
+
// ON DELETE CASCADE) stops neither the other tables nor the account from being
|
|
179
|
+
// deleted. A refused table is retried once the rest are done — the blocker may
|
|
180
|
+
// have been this user's own row in another table; what is still refused stays
|
|
181
|
+
// as it always did, and the log names it.
|
|
182
|
+
async function deleteOwnedRows(client, userId) {
|
|
183
|
+
var pending = await ownedRowLinks(client);
|
|
184
|
+
while (pending.length > 0) {
|
|
185
|
+
var refused = [];
|
|
186
|
+
for (var i = 0; i < pending.length; i++) {
|
|
187
|
+
var link = pending[i];
|
|
188
|
+
await client.query('SAVEPOINT owned_rows');
|
|
189
|
+
try {
|
|
190
|
+
await client.query(
|
|
191
|
+
'DELETE FROM ' + quoteIdentifier(link.table) + ' WHERE ' + quoteIdentifier(link.column) + ' = $1',
|
|
192
|
+
[userId]
|
|
193
|
+
);
|
|
194
|
+
await client.query('RELEASE SAVEPOINT owned_rows');
|
|
195
|
+
} catch (err) {
|
|
196
|
+
await client.query('ROLLBACK TO SAVEPOINT owned_rows');
|
|
197
|
+
await client.query('RELEASE SAVEPOINT owned_rows');
|
|
198
|
+
link.error = err && err.message ? err.message : String(err);
|
|
199
|
+
refused.push(link);
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
if (refused.length === pending.length) { break; }
|
|
203
|
+
pending = refused;
|
|
204
|
+
}
|
|
205
|
+
for (var k = 0; k < pending.length; k++) {
|
|
206
|
+
console.error('[account-delete] rows kept in ' + pending[k].table + ' (' + pending[k].column + '): ' + pending[k].error);
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
|
|
81
210
|
// Invoices are a financial/tax record that must survive, but they store the
|
|
82
211
|
// customer's PII denormalised and link to the user only through order_id (no
|
|
83
212
|
// user_id). Scrub the PII in place, matching invoices whose order belongs to the
|
|
@@ -197,6 +326,7 @@ module.exports = async function handler(req, res) {
|
|
|
197
326
|
for (var d = 0; d < DELETE_TABLES.length; d++) {
|
|
198
327
|
await deleteUserRows(client, DELETE_TABLES[d], userId);
|
|
199
328
|
}
|
|
329
|
+
await deleteOwnedRows(client, userId);
|
|
200
330
|
// password_reset_tokens has no user_id — it is keyed by the email string.
|
|
201
331
|
await deleteRowsByEmail(client, 'password_reset_tokens', 'email', userEmail);
|
|
202
332
|
await client.query('DELETE FROM ' + AUTH_USERS_TABLE + ' WHERE id = $1', [userId]);
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"account-delete-route-generator.js","sourceRoot":"","sources":["../../src/account-delete-route-generator.ts"],"names":[],"mappings":";;;
|
|
1
|
+
{"version":3,"file":"account-delete-route-generator.js","sourceRoot":"","sources":["../../src/account-delete-route-generator.ts"],"names":[],"mappings":";;;AAsBA,iEAA0D;AAC1D,qDAAuD;AACvD,uEAAoF;AACpF,yEAOmC;AAuBnC,+EAA+E;AAC/E,oFAAoF;AACvE,QAAA,8BAA8B,GAAG,yDAA8B,CAAA;AAErE,MAAM,0BAA0B,GAAG,CAAC,UAAqC,EAAE,EAAU,EAAE;IAC5F,MAAM,cAAc,GAAG,OAAO,CAAC,kBAAkB,IAAI,OAAO,CAAA;IAC5D,MAAM,QAAQ,GACZ,OAAO,CAAC,aAAa,IAAI,oDAAyB,CAAC,GAAG,CAAC,OAAO,CAAC,aAAa,CAAC;QAC3E,CAAC,CAAC,OAAO,CAAC,aAAa;QACvB,CAAC,CAAC,IAAI,CAAA;IACV,MAAM,mBAAmB,GACvB,OAAO,CAAC,mBAAmB,IAAI,yCAAyC,CAAA;IAE1E,OAAO,GAAG,IAAA,qCAAoB,GAAE;EAChC,IAAA,kEAAwC,GAAE;2BACjB,IAAI,CAAC,SAAS,CAAC,cAAc,CAAC;oBACrC,IAAI,CAAC,SAAS,CAAC,OAAO,CAAC,QAAQ,IAAI,EAAE,CAAC;gCAC1B,IAAI,CAAC,SAAS,CAAC,mBAAmB,CAAC;yBAC1C,IAAI,CAAC,SAAS,CAAC,QAAQ,CAAC;qBAC5B,IAAI,CAAC,SAAS,CAAC,OAAO,CAAC,SAAS,IAAI,EAAE,CAAC;0BAClC,IAAI,CAAC,SAAS,CAAC,OAAO,CAAC,QAAQ,IAAI,EAAE,CAAC;gCAChC,IAAI,CAAC,SAAS,CAAC,OAAO,CAAC,kBAAkB,IAAI,EAAE,CAAC;wBACxD,IAAI,CAAC,SAAS,CAAC,OAAO,CAAC,YAAY,IAAI,+BAA+B,CAAC;0BACrE,IAAI,CAAC,SAAS,CAAC,OAAO,CAAC,aAAa,IAAI,EAAE,CAAC;0BAC3C,IAAI,CAAC,SAAS,CAAC,OAAO,CAAC,kBAAkB,IAAI,EAAE,CAAC;EACxE,IAAA,qDAA0B,EAAC,UAAU,CAAC;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EAiFtC,8BAAY,CAAC,+BAA+B,EAAE;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EA4I9C,IAAA,iDAAsB,GAAE;;EAExB,IAAA,uDAA4B,EAAC;QAC7B,MAAM,EAAE,0BAA0B;QAClC,YAAY,EAAE,eAAe;QAC7B,SAAS,EAAE,iBAAiB;QAC5B,cAAc,EAAE,iBAAiB;KAClC,CAAC;;EAEA,IAAA,uDAA4B,EAAC,QAAQ,EAAE;QACvC,SAAS,EAAE,iBAAiB;QAC5B,MAAM,EAAE,gBAAgB;QACxB,SAAS,EAAE,4BAA4B;QACvC,cAAc,EAAE,UAAU;KAC3B,CAAC;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EA4GA,QAAQ;QACN,CAAC,CAAC;;CAEL;QACG,CAAC,CAAC,EACN;;;CAGC,CAAA;AACD,CAAC,CAAA;AArXY,QAAA,0BAA0B,8BAqXtC"}
|