fix(backend): アカウントの作成と削除の途中でリトライが発生しても無視するように (MisskeyIO#580)
This commit is contained in:
parent
1fb7fb8187
commit
acc10c0709
|
@ -4,6 +4,7 @@
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { Inject, Injectable } from '@nestjs/common';
|
import { Inject, Injectable } from '@nestjs/common';
|
||||||
|
import * as Redis from 'ioredis';
|
||||||
import { bindThis } from '@/decorators.js';
|
import { bindThis } from '@/decorators.js';
|
||||||
import { DI } from '@/di-symbols.js';
|
import { DI } from '@/di-symbols.js';
|
||||||
import type Logger from '@/logger.js';
|
import type Logger from '@/logger.js';
|
||||||
|
@ -20,6 +21,8 @@ export class DeleteAccountService {
|
||||||
public logger: Logger;
|
public logger: Logger;
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
|
@Inject(DI.redis)
|
||||||
|
private redisClient: Redis.Redis,
|
||||||
@Inject(DI.usersRepository)
|
@Inject(DI.usersRepository)
|
||||||
private usersRepository: UsersRepository,
|
private usersRepository: UsersRepository,
|
||||||
|
|
||||||
|
@ -29,7 +32,7 @@ export class DeleteAccountService {
|
||||||
private globalEventService: GlobalEventService,
|
private globalEventService: GlobalEventService,
|
||||||
private loggerService: LoggerService,
|
private loggerService: LoggerService,
|
||||||
) {
|
) {
|
||||||
this.logger = this.loggerService.getLogger('delete-account');
|
this.logger = this.loggerService.getLogger('account:delete');
|
||||||
}
|
}
|
||||||
|
|
||||||
@bindThis
|
@bindThis
|
||||||
|
@ -39,19 +42,38 @@ export class DeleteAccountService {
|
||||||
const _user = await this.usersRepository.findOneByOrFail({ id: user.id });
|
const _user = await this.usersRepository.findOneByOrFail({ id: user.id });
|
||||||
if (_user.isRoot) throw new Error('cannot delete a root account');
|
if (_user.isRoot) throw new Error('cannot delete a root account');
|
||||||
|
|
||||||
// 物理削除する前にDelete activityを送信する
|
// 5分間の間に同じアカウントに対して削除リクエストが複数回来た場合、最初のリクエストのみを処理する
|
||||||
await this.userSuspendService.doPostSuspend(user).catch(err => this.logger.error(err));
|
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, {
|
// noinspection ES6MissingAwait APIで呼び出される際にタイムアウトされないように
|
||||||
force: me ? await this.roleService.isModerator(me) : false,
|
(async () => {
|
||||||
soft: soft,
|
try {
|
||||||
});
|
// 物理削除する前にDelete activityを送信する
|
||||||
|
await this.userSuspendService.doPostSuspend(user).catch(err => this.logger.error(err));
|
||||||
|
|
||||||
await this.usersRepository.update(user.id, {
|
// noinspection ES6MissingAwait
|
||||||
isDeleted: true,
|
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
|
@bindThis
|
||||||
|
|
|
@ -41,11 +41,12 @@ export class FetchInstanceMetadataService {
|
||||||
private logger: Logger;
|
private logger: Logger;
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
|
@Inject(DI.redis)
|
||||||
|
private redisClient: Redis.Redis,
|
||||||
|
|
||||||
private httpRequestService: HttpRequestService,
|
private httpRequestService: HttpRequestService,
|
||||||
private loggerService: LoggerService,
|
private loggerService: LoggerService,
|
||||||
private federatedInstanceService: FederatedInstanceService,
|
private federatedInstanceService: FederatedInstanceService,
|
||||||
@Inject(DI.redis)
|
|
||||||
private redisClient: Redis.Redis,
|
|
||||||
) {
|
) {
|
||||||
this.logger = this.loggerService.getLogger('metadata', 'cyan');
|
this.logger = this.loggerService.getLogger('metadata', 'cyan');
|
||||||
}
|
}
|
||||||
|
|
|
@ -6,27 +6,34 @@
|
||||||
import { generateKeyPair } from 'node:crypto';
|
import { generateKeyPair } from 'node:crypto';
|
||||||
import { Inject, Injectable } from '@nestjs/common';
|
import { Inject, Injectable } from '@nestjs/common';
|
||||||
import bcrypt from 'bcryptjs';
|
import bcrypt from 'bcryptjs';
|
||||||
|
import * as Redis from 'ioredis';
|
||||||
import { DataSource, IsNull } from 'typeorm';
|
import { DataSource, IsNull } from 'typeorm';
|
||||||
|
import { bindThis } from '@/decorators.js';
|
||||||
import { DI } from '@/di-symbols.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 type { UsedUsernamesRepository, UsersRepository } from '@/models/_.js';
|
||||||
import { MiUser } from '@/models/User.js';
|
import { MiUser } from '@/models/User.js';
|
||||||
import { MiUserProfile } from '@/models/UserProfile.js';
|
import { MiUserProfile } from '@/models/UserProfile.js';
|
||||||
import { IdService } from '@/core/IdService.js';
|
|
||||||
import { MiUserKeypair } from '@/models/UserKeypair.js';
|
import { MiUserKeypair } from '@/models/UserKeypair.js';
|
||||||
import { MiUsedUsername } from '@/models/UsedUsername.js';
|
import { MiUsedUsername } from '@/models/UsedUsername.js';
|
||||||
import generateUserToken from '@/misc/generate-native-user-token.js';
|
import { IdService } from '@/core/IdService.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 { MetaService } from '@/core/MetaService.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()
|
@Injectable()
|
||||||
export class SignupService {
|
export class SignupService {
|
||||||
|
public logger: Logger;
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
@Inject(DI.db)
|
@Inject(DI.db)
|
||||||
private db: DataSource,
|
private db: DataSource,
|
||||||
|
@Inject(DI.redis)
|
||||||
|
private redisClient: Redis.Redis,
|
||||||
|
|
||||||
@Inject(DI.usersRepository)
|
@Inject(DI.usersRepository)
|
||||||
private usersRepository: UsersRepository,
|
private usersRepository: UsersRepository,
|
||||||
|
@ -34,13 +41,15 @@ export class SignupService {
|
||||||
@Inject(DI.usedUsernamesRepository)
|
@Inject(DI.usedUsernamesRepository)
|
||||||
private usedUsernamesRepository: UsedUsernamesRepository,
|
private usedUsernamesRepository: UsedUsernamesRepository,
|
||||||
|
|
||||||
private utilityService: UtilityService,
|
|
||||||
private userEntityService: UserEntityService,
|
|
||||||
private idService: IdService,
|
private idService: IdService,
|
||||||
private metaService: MetaService,
|
private metaService: MetaService,
|
||||||
|
private utilityService: UtilityService,
|
||||||
|
private loggerService: LoggerService,
|
||||||
private instanceActorService: InstanceActorService,
|
private instanceActorService: InstanceActorService,
|
||||||
|
private userEntityService: UserEntityService,
|
||||||
private usersChart: UsersChart,
|
private usersChart: UsersChart,
|
||||||
) {
|
) {
|
||||||
|
this.logger = this.loggerService.getLogger('account:create');
|
||||||
}
|
}
|
||||||
|
|
||||||
@bindThis
|
@bindThis
|
||||||
|
@ -110,47 +119,61 @@ export class SignupService {
|
||||||
err ? rej(err) : res([publicKey, privateKey]),
|
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
|
try {
|
||||||
await this.db.transaction(async transactionalEntityManager => {
|
let account!: MiUser;
|
||||||
const exist = await transactionalEntityManager.findOneBy(MiUser, {
|
|
||||||
usernameLower: username.toLowerCase(),
|
// Start transaction
|
||||||
host: IsNull(),
|
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({
|
return { account, secret };
|
||||||
id: this.idService.gen(),
|
} catch (err) {
|
||||||
username: username,
|
this.logger.error(`Failed to create account ${username}`, { error: err });
|
||||||
usernameLower: username.toLowerCase(),
|
throw err;
|
||||||
host: this.utilityService.toPunyNullable(host),
|
} finally {
|
||||||
token: secret,
|
// 成功・失敗に関わらずロックを解除
|
||||||
isRoot: isTheFirstUser,
|
await this.redisClient.unlink(`account:create:lock:${username.toLowerCase()}`);
|
||||||
}));
|
}
|
||||||
|
|
||||||
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 };
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue