import * as crypto from 'crypto'; import { Queue } from 'bull'; import * as admin from 'firebase-admin'; import { Any, EntityManager, IsNull, LessThanOrEqual, MoreThanOrEqual, Repository } from 'typeorm'; import { InjectQueue } from '@nestjs/bull'; import { BadRequestException, Inject, Injectable, OnModuleInit } from '@nestjs/common'; import { InjectEntityManager, InjectRepository } from '@nestjs/typeorm'; import { User, UserCode, UserDeleteRequest } from '../../database/entities'; import { ConfirmDeleteRequestDto } from './delete.dto'; const FIVE_MINUTES = 60 * 5; const ONE_DAY = 60 * 60 * 24; @Injectable() export class UserDeleteService implements OnModuleInit { constructor( @InjectRepository(UserDeleteRequest, 'funki-id') private readonly deleteRequestRepo: Repository, @InjectRepository(UserCode, 'funki-id') private readonly userCodeRepo: Repository, @InjectEntityManager('funki-id') private readonly entityManager: EntityManager, @Inject('TELEGRAM_BOT_TOKEN') private readonly telegramBotToken: string, @InjectQueue('user-delete') private readonly userDeleteQueue: Queue, @Inject('FIREBASE_AUTH') private readonly firebaseAuth: admin.auth.Auth, @Inject('ENV_CONFIG') private readonly configService: { isProduction: boolean }, ) {} public async onModuleInit() { await this.userDeleteQueue.add( 'handle-delete-requests', {}, { repeat: { cron: '*/10 * * * *', // Run every 10 minutes }, removeOnComplete: 10, removeOnFail: 10, }, ); } public async requestDelete(user: User) { if (!user.telegram) { throw new BadRequestException('Telegram account is needed to request for account deletion'); } const code = this.generateCode(); const createdAt = this.getUnix(); const expiredAt = createdAt + FIVE_MINUTES; await this.userCodeRepo.save({ code, userId: user.uid, expiredAt: expiredAt, createdAt }); await this.sendTelegramMessage( user.telegramId, `Your deletion request code is: ${code}. It will expire in 5 minutes`, ); } public async confirmDeleteRequest(uid: string, dto: ConfirmDeleteRequestDto) { const userCode = await this.userCodeRepo.findOne({ where: { code: dto.code, userId: uid, expiredAt: MoreThanOrEqual(this.getUnix()) }, }); if (!userCode) { throw new BadRequestException('Invalid code'); } return await this.entityManager.transaction(async manager => { await manager.getRepository(User).update({ uid }, { hasRequestedDeletion: true }); await manager.getRepository(UserDeleteRequest).save({ userId: uid, reason: dto.reason }); await manager.getRepository(UserCode).delete(userCode); }); } public async requestReactivate(user: User) { if (!user.telegram) { throw new BadRequestException('Telegram account is needed to request for account reactivation'); } const deleteRequest = await this.deleteRequestRepo.findOne({ where: { userId: user.uid, deletedAt: IsNull() } }); if (!deleteRequest) { throw new BadRequestException("You haven't requested for account deletion yet"); } const code = this.generateCode(); const createdAt = this.getUnix(); const expiredAt = createdAt + FIVE_MINUTES; await this.userCodeRepo.save({ code, userId: user.uid, expiredAt: expiredAt, createdAt }); await this.sendTelegramMessage( user.telegramId, `Your reactivation request code is: ${code}. It will expire in 5 minutes`, ); } public async confirmReactivateRequest(uid: string, code: string) { const userCode = await this.userCodeRepo.findOne({ where: { code, userId: uid, expiredAt: MoreThanOrEqual(this.getUnix()) }, }); if (!userCode) { throw new BadRequestException('Invalid code'); } return await this.entityManager.transaction(async manager => { await manager.getRepository(User).update({ uid }, { hasRequestedDeletion: false }); await manager.getRepository(UserDeleteRequest).delete({ userId: uid }); await manager.getRepository(UserCode).delete(userCode); }); } public async handleDeleteRequests() { const currTime = this.getUnix(); const mark = currTime - (this.configService.isProduction ? ONE_DAY * 14 : FIVE_MINUTES); const requests = await this.deleteRequestRepo.find({ where: { deletedAt: IsNull(), requestedAt: LessThanOrEqual(new Date(mark * 1000)) }, }); await this.firebaseAuth.deleteUsers(requests.map(r => r.userId)); return await this.entityManager.transaction(async manager => { if (requests.length === 0) { return; } await manager.getRepository(User).delete({ uid: Any(requests.map(r => r.userId)) }); return await manager .getRepository(UserDeleteRequest) .update({ id: Any(requests.map(r => r.id)) }, { deletedAt: this.getUnix() }); }); } private generateCode() { return crypto .randomInt(0, 10 ** 6 - 1) .toString() .padStart(6, '0'); } private async sendTelegramMessage(telegramId: string, message: string) { const botToken = this.telegramBotToken; const url = `https://api.telegram.org/bot${botToken}/sendMessage`; const res = await fetch(url, { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({ chat_id: telegramId, text: message }), }); if (!res.ok) { throw new Error('Failed to send telegram message'); } return res.json(); } private getUnix() { return Math.round(new Date().getTime() / 1000); } }