diff --git a/packages/backend/src/core/DeleteAccountService.ts b/packages/backend/src/core/DeleteAccountService.ts index 385f54b42..d0fed760c 100644 --- a/packages/backend/src/core/DeleteAccountService.ts +++ b/packages/backend/src/core/DeleteAccountService.ts @@ -4,6 +4,7 @@ */ import { Inject, Injectable } from '@nestjs/common'; +import * as Redis from 'ioredis'; import { bindThis } from '@/decorators.js'; import { DI } from '@/di-symbols.js'; import type Logger from '@/logger.js'; @@ -20,6 +21,8 @@ export class DeleteAccountService { public logger: Logger; constructor( + @Inject(DI.redis) + private redisClient: Redis.Redis, @Inject(DI.usersRepository) private usersRepository: UsersRepository, @@ -29,7 +32,7 @@ export class DeleteAccountService { private globalEventService: GlobalEventService, private loggerService: LoggerService, ) { - this.logger = this.loggerService.getLogger('delete-account'); + this.logger = this.loggerService.getLogger('account:delete'); } @bindThis @@ -39,19 +42,38 @@ export class DeleteAccountService { const _user = await this.usersRepository.findOneByOrFail({ id: user.id }); if (_user.isRoot) throw new Error('cannot delete a root account'); - // 物理削除する前にDelete activityを送信する - await this.userSuspendService.doPostSuspend(user).catch(err => this.logger.error(err)); + // 5分間の間に同じアカウントに対して削除リクエストが複数回来た場合、最初のリクエストのみを処理する + const lock = await this.redisClient.set(`account:delete:lock:${user.id}`, Date.now(), 'EX', 60 * 5, 'NX'); + if (lock === null) { + this.logger.warn(`Delete account is already in progress for ${user.id}`); + return; + } - this.queueService.createDeleteAccountJob(user, { - force: me ? await this.roleService.isModerator(me) : false, - soft: soft, - }); + // noinspection ES6MissingAwait APIで呼び出される際にタイムアウトされないように + (async () => { + try { + // 物理削除する前にDelete activityを送信する + await this.userSuspendService.doPostSuspend(user).catch(err => this.logger.error(err)); - await this.usersRepository.update(user.id, { - isDeleted: true, - }); + // noinspection ES6MissingAwait + this.queueService.createDeleteAccountJob(user, { + force: me ? await this.roleService.isModerator(me) : false, + soft: soft, + }); - this.globalEventService.publishInternalEvent('userChangeDeletedState', { id: user.id, isDeleted: true }); + await this.usersRepository.update(user.id, { + isDeleted: true, + }); + + this.globalEventService.publishInternalEvent('userChangeDeletedState', { id: user.id, isDeleted: true }); + } catch (err) { + this.logger.error(`Failed to delete account ${user.id}, request by ${me ? me.id : 'remote'} (soft: ${soft})`, { error: err }); + // すでにcallstackから離れてるので、ここでエラーをthrowしても意味がない + } finally { + // 成功・失敗に関わらずロックを解除 + await this.redisClient.unlink(`account:delete:lock:${user.id}`); + } + })(); } @bindThis diff --git a/packages/backend/src/core/FetchInstanceMetadataService.ts b/packages/backend/src/core/FetchInstanceMetadataService.ts index ffc17fee1..ceca6a27d 100644 --- a/packages/backend/src/core/FetchInstanceMetadataService.ts +++ b/packages/backend/src/core/FetchInstanceMetadataService.ts @@ -41,11 +41,12 @@ export class FetchInstanceMetadataService { private logger: Logger; constructor( + @Inject(DI.redis) + private redisClient: Redis.Redis, + private httpRequestService: HttpRequestService, private loggerService: LoggerService, private federatedInstanceService: FederatedInstanceService, - @Inject(DI.redis) - private redisClient: Redis.Redis, ) { this.logger = this.loggerService.getLogger('metadata', 'cyan'); } diff --git a/packages/backend/src/core/SignupService.ts b/packages/backend/src/core/SignupService.ts index 72d06c3da..76c1dc322 100644 --- a/packages/backend/src/core/SignupService.ts +++ b/packages/backend/src/core/SignupService.ts @@ -6,27 +6,34 @@ import { generateKeyPair } from 'node:crypto'; import { Inject, Injectable } from '@nestjs/common'; import bcrypt from 'bcryptjs'; +import * as Redis from 'ioredis'; import { DataSource, IsNull } from 'typeorm'; +import { bindThis } from '@/decorators.js'; import { DI } from '@/di-symbols.js'; +import type Logger from '@/logger.js'; +import generateUserToken from '@/misc/generate-native-user-token.js'; import type { UsedUsernamesRepository, UsersRepository } from '@/models/_.js'; import { MiUser } from '@/models/User.js'; import { MiUserProfile } from '@/models/UserProfile.js'; -import { IdService } from '@/core/IdService.js'; import { MiUserKeypair } from '@/models/UserKeypair.js'; import { MiUsedUsername } from '@/models/UsedUsername.js'; -import generateUserToken from '@/misc/generate-native-user-token.js'; -import { UserEntityService } from '@/core/entities/UserEntityService.js'; -import { InstanceActorService } from '@/core/InstanceActorService.js'; -import { bindThis } from '@/decorators.js'; -import UsersChart from '@/core/chart/charts/users.js'; -import { UtilityService } from '@/core/UtilityService.js'; +import { IdService } from '@/core/IdService.js'; import { MetaService } from '@/core/MetaService.js'; +import { UtilityService } from '@/core/UtilityService.js'; +import { LoggerService } from '@/core/LoggerService.js'; +import { InstanceActorService } from '@/core/InstanceActorService.js'; +import { UserEntityService } from '@/core/entities/UserEntityService.js'; +import UsersChart from '@/core/chart/charts/users.js'; @Injectable() export class SignupService { + public logger: Logger; + constructor( @Inject(DI.db) private db: DataSource, + @Inject(DI.redis) + private redisClient: Redis.Redis, @Inject(DI.usersRepository) private usersRepository: UsersRepository, @@ -34,13 +41,15 @@ export class SignupService { @Inject(DI.usedUsernamesRepository) private usedUsernamesRepository: UsedUsernamesRepository, - private utilityService: UtilityService, - private userEntityService: UserEntityService, private idService: IdService, private metaService: MetaService, + private utilityService: UtilityService, + private loggerService: LoggerService, private instanceActorService: InstanceActorService, + private userEntityService: UserEntityService, private usersChart: UsersChart, ) { + this.logger = this.loggerService.getLogger('account:create'); } @bindThis @@ -110,47 +119,61 @@ export class SignupService { err ? rej(err) : res([publicKey, privateKey]), )); - let account!: MiUser; + // 5分間のロックを取得 + const lock = await this.redisClient.set(`account:create:lock:${username.toLowerCase()}`, Date.now(), 'EX', 60 * 5, 'NX'); + if (lock === null) { + throw new Error('ALREADY_IN_PROGRESS'); + } - // Start transaction - await this.db.transaction(async transactionalEntityManager => { - const exist = await transactionalEntityManager.findOneBy(MiUser, { - usernameLower: username.toLowerCase(), - host: IsNull(), + try { + let account!: MiUser; + + // Start transaction + await this.db.transaction(async transactionalEntityManager => { + const exist = await transactionalEntityManager.findOneBy(MiUser, { + usernameLower: username.toLowerCase(), + host: IsNull(), + }); + + if (exist) throw new Error(' the username is already used'); + + account = await transactionalEntityManager.save(new MiUser({ + id: this.idService.gen(), + username: username, + usernameLower: username.toLowerCase(), + host: this.utilityService.toPunyNullable(host), + token: secret, + isRoot: isTheFirstUser, + })); + + await transactionalEntityManager.save(new MiUserKeypair({ + publicKey: keyPair[0], + privateKey: keyPair[1], + userId: account.id, + })); + + await transactionalEntityManager.save(new MiUserProfile({ + userId: account.id, + autoAcceptFollowed: true, + password: hash, + })); + + await transactionalEntityManager.save(new MiUsedUsername({ + createdAt: new Date(), + username: username.toLowerCase(), + })); }); - if (exist) throw new Error(' the username is already used'); + this.usersChart.update(account, true); - account = await transactionalEntityManager.save(new MiUser({ - id: this.idService.gen(), - username: username, - usernameLower: username.toLowerCase(), - host: this.utilityService.toPunyNullable(host), - token: secret, - isRoot: isTheFirstUser, - })); - - await transactionalEntityManager.save(new MiUserKeypair({ - publicKey: keyPair[0], - privateKey: keyPair[1], - userId: account.id, - })); - - await transactionalEntityManager.save(new MiUserProfile({ - userId: account.id, - autoAcceptFollowed: true, - password: hash, - })); - - await transactionalEntityManager.save(new MiUsedUsername({ - createdAt: new Date(), - username: username.toLowerCase(), - })); - }); - - this.usersChart.update(account, true); - - return { account, secret }; + return { account, secret }; + } catch (err) { + this.logger.error(`Failed to create account ${username}`, { error: err }); + throw err; + } finally { + // 成功・失敗に関わらずロックを解除 + await this.redisClient.unlink(`account:create:lock:${username.toLowerCase()}`); + } } }