11import './env' ;
2+ import './stripe' ;
23
4+ import { decryptUserEmail , hashUserEmail } from '@deeplib/data' ;
5+ import {
6+ GroupJoinInvitationModel ,
7+ GroupJoinRequestModel ,
8+ GroupMemberModel ,
9+ PageModel ,
10+ SessionModel ,
11+ UserModel ,
12+ UserPageModel ,
13+ } from '@deeplib/db' ;
314import {
415 sendBrevoMail ,
516 sendMailjetMail ,
617 sendSendGridMail ,
718} from '@deeplib/mail' ;
819import { mainLogger } from '@stdlib/misc' ;
20+ import { raw } from 'objection' ;
921import readline from 'readline' ;
1022
1123import { dataAbstraction } from './data/data-abstraction' ;
1224import { initKnex } from './data/knex' ;
25+ import { stripe } from './stripe' ;
1326
1427initKnex ( ) ;
1528
@@ -22,6 +35,8 @@ function showHelp() {
2235 console . log ( 'Available commands:' ) ;
2336 console . log ( '- hget <prefix> <suffix> <field>' ) ;
2437 console . log ( '- hset <prefix> <suffix> <field> <value>' ) ;
38+ console . log ( '- delete-user <userId>' ) ;
39+ console . log ( '- hash-email <email>' ) ;
2540 console . log ( '- send-sendgrid-test-mail <from> <to>' ) ;
2641 console . log ( '- send-brevo-test-mail <from> <to>' ) ;
2742 console . log ( '- send-mailjet-test-mail <from> <to>' ) ;
@@ -51,6 +66,16 @@ async function handleCommand(command: string) {
5166 } ) ;
5267 break ;
5368
69+ case 'delete-user' :
70+ await deleteUser ( args [ 0 ] ) ;
71+ break ;
72+
73+ case 'hash-email' :
74+ mainLogger . info (
75+ `Result: '\\x${ Buffer . from ( hashUserEmail ( args [ 0 ] ) ) . toString ( 'hex' ) } '` ,
76+ ) ;
77+ break ;
78+
5479 case 'send-sendgrid-test-mail' :
5580 await sendSendGridMail ( {
5681 from : { name : 'DeepNotes' , email : args [ 0 ] } ,
@@ -89,6 +114,180 @@ async function handleCommand(command: string) {
89114 }
90115}
91116
117+ async function deleteUser ( userId : string ) {
118+ await dataAbstraction ( ) . transaction ( async ( dtrx ) => {
119+ // Check if any group has more than one member
120+
121+ const memberships = await GroupMemberModel . query ( dtrx . trx )
122+ . where ( 'group_members.user_id' , userId )
123+ . leftJoin (
124+ GroupMemberModel . query ( dtrx . trx )
125+ . groupBy ( 'group_id' )
126+ . select ( 'group_id' )
127+ . count ( '* as member_count' )
128+ . as ( 'member_counts' ) ,
129+ 'group_members.group_id' ,
130+ 'member_counts.group_id' ,
131+ )
132+ . leftJoin (
133+ GroupMemberModel . query ( dtrx . trx )
134+ . where ( 'role' , 'owner' )
135+ . groupBy ( 'group_id' )
136+ . select ( 'group_id' )
137+ . count ( '* as owner_count' )
138+ . as ( 'owner_counts' ) ,
139+ 'group_members.group_id' ,
140+ 'owner_counts.group_id' ,
141+ )
142+ . select (
143+ 'group_members.group_id' ,
144+ raw ( 'COALESCE(member_counts.member_count, 0) as member_count' ) ,
145+ raw ( 'COALESCE(owner_counts.owner_count, 0) as owner_count' ) ,
146+ ) ;
147+
148+ if (
149+ memberships . some (
150+ ( count ) =>
151+ ( count as any ) . member_count > 1 && ( count as any ) . owner_count <= 1 ,
152+ )
153+ ) {
154+ throw {
155+ message :
156+ 'Some groups would be left without an owner. Transfer ownership before deleting your account.' ,
157+ code : 'BAD_REQUEST' ,
158+ } ;
159+ }
160+
161+ const idsOfGroupsToDelete = memberships
162+ . filter ( ( membership ) => ( membership as any ) . member_count <= 1 )
163+ . map ( ( membership ) => membership . group_id ) ;
164+
165+ // Get all user data
166+
167+ const [
168+ groupPageIds ,
169+ invitations ,
170+ requests ,
171+ visitedPageIds ,
172+ sessions ,
173+ user ,
174+ ] = await Promise . all ( [
175+ PageModel . query ( dtrx . trx )
176+ . whereIn ( 'group_id' , idsOfGroupsToDelete )
177+ . select ( 'pages.id' ) ,
178+
179+ GroupJoinInvitationModel . query ( dtrx . trx )
180+ . where ( 'user_id' , userId )
181+ . select ( 'group_id' ) ,
182+ GroupJoinRequestModel . query ( dtrx . trx )
183+ . where ( 'user_id' , userId )
184+ . select ( 'group_id' ) ,
185+
186+ UserPageModel . query ( dtrx . trx ) . where ( 'user_id' , userId ) . select ( 'page_id' ) ,
187+
188+ SessionModel . query ( dtrx . trx )
189+ . where ( 'user_id' , userId )
190+ . whereNot ( 'invalidated' , true )
191+ . select ( 'id' ) ,
192+
193+ UserModel . query ( dtrx . trx )
194+ . findById ( userId )
195+ . select ( 'encrypted_email' , 'personal_group_id' , 'customer_id' ) ,
196+ ] ) ;
197+
198+ if ( user == null ) {
199+ throw {
200+ message : 'User not found' ,
201+ code : 'NOT_FOUND' ,
202+ } ;
203+ }
204+
205+ // Delete all user data
206+
207+ await Promise . all ( [
208+ ...groupPageIds . map ( ( page ) =>
209+ dataAbstraction ( ) . delete ( 'page' , page . id , {
210+ dtrx,
211+ cacheOnly : true ,
212+ } ) ,
213+ ) ,
214+
215+ ...invitations . map ( ( invitation ) =>
216+ dataAbstraction ( ) . delete (
217+ 'group-join-invitation' ,
218+ `${ invitation . group_id } :${ userId } ` ,
219+ { dtrx, cacheOnly : true } ,
220+ ) ,
221+ ) ,
222+ ...requests . map ( ( request ) =>
223+ dataAbstraction ( ) . delete (
224+ 'group-join-request' ,
225+ `${ request . group_id } :${ userId } ` ,
226+ { dtrx, cacheOnly : true } ,
227+ ) ,
228+ ) ,
229+ ...memberships . map ( ( member ) =>
230+ dataAbstraction ( ) . delete (
231+ 'group-member' ,
232+ `${ member . group_id } :${ userId } ` ,
233+ {
234+ dtrx,
235+ cacheOnly : true ,
236+ } ,
237+ ) ,
238+ ) ,
239+
240+ ...idsOfGroupsToDelete . map ( ( groupId ) =>
241+ dataAbstraction ( ) . delete ( 'group' , groupId , {
242+ dtrx,
243+ } ) ,
244+ ) ,
245+
246+ ...visitedPageIds . map ( ( page ) =>
247+ dataAbstraction ( ) . delete ( 'user-page' , `${ userId } :${ page . page_id } ` , {
248+ dtrx,
249+ cacheOnly : true ,
250+ } ) ,
251+ ) ,
252+
253+ ...sessions . map ( ( session ) =>
254+ dataAbstraction ( ) . patch (
255+ 'session' ,
256+ session . id ,
257+ { invalidated : true } ,
258+ { dtrx, cacheOnly : true } ,
259+ ) ,
260+ ) ,
261+
262+ ...( user . customer_id != null
263+ ? [
264+ dataAbstraction ( ) . delete ( 'customer' , user . customer_id , {
265+ dtrx,
266+ cacheOnly : true ,
267+ } ) ,
268+ ]
269+ : [ ] ) ,
270+
271+ dataAbstraction ( ) . delete (
272+ 'email' ,
273+ decryptUserEmail ( user . encrypted_email ) ,
274+ {
275+ dtrx,
276+ cacheOnly : true ,
277+ } ,
278+ ) ,
279+
280+ dataAbstraction ( ) . delete ( 'user' , userId , { dtrx } ) ,
281+ ] ) ;
282+
283+ // Delete Stripe customer
284+
285+ if ( user ?. customer_id != null ) {
286+ await stripe . customers . del ( user . customer_id ) ;
287+ }
288+ } ) ;
289+ }
290+
92291function requestCommand ( ) {
93292 readlineInterface . question ( '' , async ( command ) => {
94293 if ( command === 'exit' ) {
0 commit comments