From 5f5423ddcbd5862c7d8c2edce848b7dc9a7f8feb Mon Sep 17 00:00:00 2001 From: Hongbo Wu Date: Wed, 25 Oct 2023 13:02:59 +0800 Subject: [PATCH] add a pubsub service endpoint for cleanup soft deleted users --- packages/api/src/routers/svc/user.ts | 74 ++++++++++++++++++++++++++++ packages/api/src/server.ts | 2 + packages/api/src/services/user.ts | 26 +++++++++- 3 files changed, 101 insertions(+), 1 deletion(-) create mode 100644 packages/api/src/routers/svc/user.ts diff --git a/packages/api/src/routers/svc/user.ts b/packages/api/src/routers/svc/user.ts new file mode 100644 index 000000000..5b4350e59 --- /dev/null +++ b/packages/api/src/routers/svc/user.ts @@ -0,0 +1,74 @@ +/* eslint-disable @typescript-eslint/no-unsafe-member-access */ +/* eslint-disable @typescript-eslint/no-unsafe-assignment */ +import cors from 'cors' +import express from 'express' +import { LessThan } from 'typeorm' +import { StatusType } from '../../entity/user' +import { readPushSubscription } from '../../pubsub' +import { deleteUsers } from '../../services/user' +import { corsConfig } from '../../utils/corsConfig' +import { logger } from '../../utils/logger' + +type CleanupMessage = { + subDays: number +} + +const isCleanupMessage = (obj: any): obj is CleanupMessage => + 'subDays' in obj && !isNaN(obj.subDays) + +const getCleanupMessage = (msgStr: string): CleanupMessage => { + try { + const obj = JSON.parse(msgStr) as unknown + if (isCleanupMessage(obj)) { + return obj + } + } catch (err) { + console.log('error deserializing event: ', { msgStr, err }) + } + + return { + subDays: 0, // default to 0 + } +} + +export function userServiceRouter() { + const router = express.Router() + + router.post( + '/cleanup', + cors(corsConfig), + async (req, res) => { + logger.info('cleanup soft deleted users') + + const { message: msgStr, expired } = readPushSubscription(req) + + if (!msgStr) { + return res.status(200).send('Bad Request') + } + + if (expired) { + logger.info('discarding expired message') + return res.status(200).send('Expired') + } + + const cleanupMessage = getCleanupMessage(msgStr) + const subTime = cleanupMessage.subDays * 1000 * 60 * 60 * 24 // convert days to milliseconds + + try { + const result = await deleteUsers({ + status: StatusType.Deleted, + updatedAt: LessThan(new Date(Date.now() - subTime)), // subDays ago + }) + logger.info('cleanup result', result) + + return res.sendStatus(200) + } catch (error) { + logger.error('error cleaning up users', error) + + return res.sendStatus(500) + } + } + ) + + return router +} diff --git a/packages/api/src/server.ts b/packages/api/src/server.ts index b40a82bb8..1209deb9e 100755 --- a/packages/api/src/server.ts +++ b/packages/api/src/server.ts @@ -31,6 +31,7 @@ import { newsletterServiceRouter } from './routers/svc/newsletters' // import { remindersServiceRouter } from './routers/svc/reminders' import { rssFeedRouter } from './routers/svc/rss_feed' import { uploadServiceRouter } from './routers/svc/upload' +import { userServiceRouter } from './routers/svc/user' import { webhooksServiceRouter } from './routers/svc/webhooks' import { textToSpeechRouter } from './routers/text_to_speech' import { userRouter } from './routers/user_router' @@ -121,6 +122,7 @@ export const createApp = (): { app.use('/svc/pubsub/webhooks', webhooksServiceRouter()) app.use('/svc/pubsub/integrations', integrationsServiceRouter()) app.use('/svc/pubsub/rss-feed', rssFeedRouter()) + app.use('/svc/pubsub/user', userServiceRouter()) // app.use('/svc/reminders', remindersServiceRouter()) app.use('/svc/email-attachment', emailAttachmentRouter()) diff --git a/packages/api/src/services/user.ts b/packages/api/src/services/user.ts index 870d9ce6b..f3d01e489 100644 --- a/packages/api/src/services/user.ts +++ b/packages/api/src/services/user.ts @@ -1,6 +1,8 @@ +import { DeepPartial, FindOptionsWhere, In } from 'typeorm' import { StatusType, User } from '../entity/user' import { authTrx } from '../repository' import { userRepository } from '../repository/user' +import { SetClaimsRole } from '../utils/dictionary' export const deleteUser = async (userId: string) => { await authTrx( @@ -20,6 +22,28 @@ export const updateUser = async (userId: string, update: Partial) => { ) } -export const findUser = async (id: string): Promise => { +export const findActiveUser = async (id: string): Promise => { return userRepository.findOneBy({ id, status: StatusType.Active }) } + +export const findUsersById = async (ids: string[]): Promise => { + return userRepository.findBy({ id: In(ids) }) +} + +export const deleteUsers = async (criteria: FindOptionsWhere) => { + return authTrx( + async (t) => t.getRepository(User).delete(criteria), + undefined, + undefined, + SetClaimsRole.ADMIN + ) +} + +export const createUsers = async (users: DeepPartial[]) => { + return authTrx( + async (t) => t.getRepository(User).save(users), + undefined, + undefined, + SetClaimsRole.ADMIN + ) +}