diff --git a/package.json b/package.json index bd540410ddc0..e47fe799379e 100644 --- a/package.json +++ b/package.json @@ -1,12 +1,12 @@ { "name": "misskey", - "version": "2024.3.1-host.4a", + "version": "2024.3.1-host.4b", "codename": "nasubi", "repository": { "type": "git", "url": "https://github.com/MisskeyIO/misskey.git" }, - "packageManager": "pnpm@8.15.4", + "packageManager": "pnpm@8.15.5", "workspaces": [ "packages/frontend", "packages/backend", @@ -49,20 +49,20 @@ "@tensorflow/tfjs-core": "4.17.0", "chokidar": "3.6.0", "lodash": "4.17.21", - "sharp": "0.33.2" + "sharp": "0.33.3" }, "dependencies": { - "cssnano": "6.1.1", + "cssnano": "6.1.2", "execa": "8.0.1", "js-yaml": "4.1.0", "postcss": "8.4.38", - "terser": "5.29.2", + "terser": "5.30.0", "typescript": "5.4.3" }, "devDependencies": { - "@types/node": "20.11.30", - "@typescript-eslint/eslint-plugin": "7.3.1", - "@typescript-eslint/parser": "7.3.1", + "@types/node": "20.12.2", + "@typescript-eslint/eslint-plugin": "7.4.0", + "@typescript-eslint/parser": "7.4.0", "cross-env": "7.0.3", "cypress": "13.7.1", "eslint": "8.57.0", diff --git a/packages/backend/package.json b/packages/backend/package.json index 9884d044b4f4..03c5b5afaf80 100644 --- a/packages/backend/package.json +++ b/packages/backend/package.json @@ -66,12 +66,12 @@ }, "dependencies": { "@authenio/samlify-node-xmllint": "2.0.0", - "@aws-sdk/client-s3": "3.537.0", - "@aws-sdk/lib-storage": "3.537.0", - "@bull-board/api": "5.15.1", - "@bull-board/fastify": "5.15.1", - "@bull-board/ui": "5.15.1", - "@discordapp/twemoji": "15.0.2", + "@aws-sdk/client-s3": "3.540.0", + "@aws-sdk/lib-storage": "3.540.0", + "@bull-board/api": "5.15.3", + "@bull-board/fastify": "5.15.3", + "@bull-board/ui": "5.15.3", + "@discordapp/twemoji": "15.0.3", "@fastify/accepts": "4.3.0", "@fastify/cookie": "9.3.1", "@fastify/cors": "9.0.1", @@ -79,20 +79,20 @@ "@fastify/formbody": "7.4.0", "@fastify/http-proxy": "9.5.0", "@fastify/multipart": "8.2.0", - "@fastify/static": "7.0.1", + "@fastify/static": "7.0.2", "@fastify/view": "9.0.0", "@misskey-dev/sharp-read-bmp": "1.2.0", "@misskey-dev/summaly": "5.1.0", - "@nestjs/common": "10.3.4", - "@nestjs/core": "10.3.4", - "@nestjs/testing": "10.3.4", + "@nestjs/common": "10.3.7", + "@nestjs/core": "10.3.7", + "@nestjs/testing": "10.3.7", "@peertube/http-signature": "1.7.0", "@simplewebauthn/server": "9.0.3", "@sinonjs/fake-timers": "11.2.2", "@smithy/node-http-handler": "2.5.0", "@swc/cli": "0.1.65", "@swc/core": "1.3.107", - "@twemoji/parser": "15.0.0", + "@twemoji/parser": "15.1.0", "accepts": "1.3.8", "ajv": "8.12.0", "archiver": "6.0.1", @@ -100,7 +100,7 @@ "bcryptjs": "2.4.3", "blurhash": "2.0.5", "body-parser": "1.20.2", - "bullmq": "5.4.4", + "bullmq": "5.4.6", "cacheable-lookup": "7.0.0", "cbor": "9.0.2", "chalk": "5.3.0", @@ -166,14 +166,14 @@ "ratelimiter": "3.4.1", "re2": "1.20.10", "redis-lock": "0.1.4", - "reflect-metadata": "0.2.1", + "reflect-metadata": "0.2.2", "rename": "1.0.4", "rss-parser": "3.13.0", "rxjs": "7.8.1", "samlify": "2.8.11", "sanitize-html": "2.13.0", "secure-json-parse": "2.7.0", - "sharp": "0.33.2", + "sharp": "0.33.3", "slacc": "0.0.10", "strict-event-emitter-types": "2.0.0", "stringz": "2.1.0", @@ -194,7 +194,7 @@ "devDependencies": { "@jest/globals": "29.7.0", "@misskey-dev/eslint-plugin": "1.0.0", - "@nestjs/platform-express": "10.3.4", + "@nestjs/platform-express": "10.3.7", "@simplewebauthn/types": "9.0.1", "@swc/jest": "0.2.36", "@types/accepts": "1.3.7", @@ -213,11 +213,11 @@ "@types/jsrsasign": "10.5.13", "@types/mime-types": "2.1.4", "@types/ms": "0.7.34", - "@types/node": "20.11.30", + "@types/node": "20.12.2", "@types/node-forge": "1.3.11", "@types/nodemailer": "6.4.14", "@types/oauth": "0.9.4", - "@types/oauth2orize": "1.11.4", + "@types/oauth2orize": "1.11.5", "@types/oauth2orize-pkce": "0.1.2", "@types/pg": "8.11.4", "@types/pug": "2.0.10", @@ -235,9 +235,9 @@ "@types/vary": "1.1.3", "@types/web-push": "3.6.3", "@types/ws": "8.5.10", - "@typescript-eslint/eslint-plugin": "7.3.1", - "@typescript-eslint/parser": "7.3.1", - "aws-sdk-client-mock": "3.0.1", + "@typescript-eslint/eslint-plugin": "7.4.0", + "@typescript-eslint/parser": "7.4.0", + "aws-sdk-client-mock": "4.0.0", "cross-env": "7.0.3", "eslint": "8.57.0", "eslint-plugin-import": "2.29.1", diff --git a/packages/backend/src/core/DeleteAccountService.ts b/packages/backend/src/core/DeleteAccountService.ts index 385f54b42b74..d0fed760cb9f 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, + }); + + await this.usersRepository.update(user.id, { + isDeleted: true, + }); - this.globalEventService.publishInternalEvent('userChangeDeletedState', { id: 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 ffc17fee19fe..ceca6a27d8c1 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 72d06c3dad46..76c1dc322b8a 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 { 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 { bindThis } from '@/decorators.js'; +import { UserEntityService } from '@/core/entities/UserEntityService.js'; import UsersChart from '@/core/chart/charts/users.js'; -import { UtilityService } from '@/core/UtilityService.js'; -import { MetaService } from '@/core/MetaService.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'); - - 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 }; + this.usersChart.update(account, true); + + 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()}`); + } } } diff --git a/packages/backend/src/server/api/endpoints/admin/accounts/delete.ts b/packages/backend/src/server/api/endpoints/admin/accounts/delete.ts index ed7a1424dde8..ef66e54e2c5e 100644 --- a/packages/backend/src/server/api/endpoints/admin/accounts/delete.ts +++ b/packages/backend/src/server/api/endpoints/admin/accounts/delete.ts @@ -37,6 +37,7 @@ export const paramDef = { type: 'object', properties: { userId: { type: 'string', format: 'misskey:id' }, + soft: { type: 'boolean', default: true, description: 'Since deletion by an administrator is a moderation action, the default is to soft delete.' }, }, required: ['userId'], } as const; @@ -56,8 +57,7 @@ export default class extends Endpoint { // eslint- if (user == null) throw new ApiError(meta.errors.userNotFound); if (await this.roleService.isModerator(user)) throw new ApiError(meta.errors.cannotDeleteModerator); - // 管理者からの削除ということはモデレーション行為なので、soft delete にする - await this.deleteAccountService.deleteAccount(user, true, me); + await this.deleteAccountService.deleteAccount(user, ps.soft, me); }); } } diff --git a/packages/backend/src/server/web/views/base.pug b/packages/backend/src/server/web/views/base.pug index 430dffdedc8a..0389b9793174 100644 --- a/packages/backend/src/server/web/views/base.pug +++ b/packages/backend/src/server/web/views/base.pug @@ -36,7 +36,7 @@ html link(rel='prefetch' href=infoImageUrl) link(rel='prefetch' href=notFoundImageUrl) //- https://github.com/misskey-dev/misskey/issues/9842 - link(rel='stylesheet' href=`/assets/tabler-icons.${version}/tabler-icons.min.css`) + link(rel='stylesheet' href=`/assets/tabler-icons.${version}/dist/tabler-icons.min.css`) link(rel='modulepreload' href=`/vite/${clientEntry.file}`) if !config.clientManifestExists diff --git a/packages/frontend/.storybook/preview-head.html b/packages/frontend/.storybook/preview-head.html index d2e77cb7cf54..6a06aaaa0dd0 100644 --- a/packages/frontend/.storybook/preview-head.html +++ b/packages/frontend/.storybook/preview-head.html @@ -5,7 +5,7 @@ - +