From f236b44f2634ce686ecdb00dc12ceb97521bd6c8 Mon Sep 17 00:00:00 2001 From: syuilo <4439005+syuilo@users.noreply.github.com> Date: Fri, 21 Feb 2025 18:15:33 +0900 Subject: [PATCH] wip --- .../1740121393164-system-accounts.js | 9 +- .../1740129169650-system-accounts-2.js | 18 ++ .../backend/src/core/AbuseReportService.ts | 2 +- packages/backend/src/core/CoreModule.ts | 24 +-- .../src/core/CreateSystemUserService.ts | 86 ---------- .../backend/src/core/InstanceActorService.ts | 57 ------- .../backend/src/core/ProxyAccountService.ts | 28 ---- packages/backend/src/core/RelayService.ts | 33 +--- packages/backend/src/core/SignupService.ts | 2 +- .../backend/src/core/SystemAccountService.ts | 155 ++++++++++++++++++ .../src/core/activitypub/ApResolverService.ts | 2 +- .../src/core/entities/MetaEntityService.ts | 6 +- packages/backend/src/models/Meta.ts | 12 -- .../src/server/NodeinfoServerService.ts | 1 + .../api/endpoints/admin/accounts/create.ts | 2 +- .../src/server/api/endpoints/admin/meta.ts | 6 - .../server/api/endpoints/admin/update-meta.ts | 5 - .../endpoints/admin/update-proxy-account.ts | 90 ++++++++++ 18 files changed, 291 insertions(+), 247 deletions(-) create mode 100644 packages/backend/migration/1740129169650-system-accounts-2.js delete mode 100644 packages/backend/src/core/CreateSystemUserService.ts delete mode 100644 packages/backend/src/core/InstanceActorService.ts delete mode 100644 packages/backend/src/core/ProxyAccountService.ts create mode 100644 packages/backend/src/core/SystemAccountService.ts create mode 100644 packages/backend/src/server/api/endpoints/admin/update-proxy-account.ts diff --git a/packages/backend/migration/1740121393164-system-accounts.js b/packages/backend/migration/1740121393164-system-accounts.js index 7cdfbe5157..96eff33166 100644 --- a/packages/backend/migration/1740121393164-system-accounts.js +++ b/packages/backend/migration/1740121393164-system-accounts.js @@ -14,8 +14,15 @@ export class SystemAccounts1740121393164 { const instanceActor = await queryRunner.query(`SELECT "id" FROM "user" WHERE "username" = 'instance.actor'`); if (instanceActor.length > 0) { - await queryRunner.query(`INSERT INTO "system_account" ("id", "userId", "type") VALUES ('TODO', '${instanceActor[0].id}', 'instance.actor')`); + await queryRunner.query(`INSERT INTO "system_account" ("id", "userId", "type") VALUES ('${instanceActor[0].id}', '${instanceActor[0].id}', 'actor')`); } + + const relayActor = await queryRunner.query(`SELECT "id" FROM "user" WHERE "username" = 'relay.actor'`); + if (relayActor.length > 0) { + await queryRunner.query(`INSERT INTO "system_account" ("id", "userId", "type") VALUES ('${relayActor[0].id}', '${relayActor[0].id}', 'relay')`); + } + + // TODO: proxy } async down(queryRunner) { diff --git a/packages/backend/migration/1740129169650-system-accounts-2.js b/packages/backend/migration/1740129169650-system-accounts-2.js new file mode 100644 index 0000000000..07270855bf --- /dev/null +++ b/packages/backend/migration/1740129169650-system-accounts-2.js @@ -0,0 +1,18 @@ +/* + * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-License-Identifier: AGPL-3.0-only + */ + +export class SystemAccounts21740129169650 { + name = 'SystemAccounts21740129169650' + + async up(queryRunner) { + await queryRunner.query(`ALTER TABLE "meta" DROP CONSTRAINT "FK_ab1bc0c1e209daa77b8e8d212ad"`); + await queryRunner.query(`ALTER TABLE "meta" DROP COLUMN "proxyAccountId"`); + } + + async down(queryRunner) { + await queryRunner.query(`ALTER TABLE "meta" ADD "proxyAccountId" character varying(32)`); + await queryRunner.query(`ALTER TABLE "meta" ADD CONSTRAINT "FK_ab1bc0c1e209daa77b8e8d212ad" FOREIGN KEY ("proxyAccountId") REFERENCES "user"("id") ON DELETE SET NULL ON UPDATE NO ACTION`); + } +} diff --git a/packages/backend/src/core/AbuseReportService.ts b/packages/backend/src/core/AbuseReportService.ts index 0b022d3b08..a357870eef 100644 --- a/packages/backend/src/core/AbuseReportService.ts +++ b/packages/backend/src/core/AbuseReportService.ts @@ -10,7 +10,7 @@ import { bindThis } from '@/decorators.js'; import type { AbuseUserReportsRepository, MiAbuseUserReport, MiUser, UsersRepository } from '@/models/_.js'; import { AbuseReportNotificationService } from '@/core/AbuseReportNotificationService.js'; import { QueueService } from '@/core/QueueService.js'; -import { InstanceActorService } from '@/core/InstanceActorService.js'; +import { InstanceActorService } from '@/core/SystemAccountService.js'; import { ApRendererService } from '@/core/activitypub/ApRendererService.js'; import { ModerationLogService } from '@/core/ModerationLogService.js'; import { IdService } from './IdService.js'; diff --git a/packages/backend/src/core/CoreModule.ts b/packages/backend/src/core/CoreModule.ts index 734d135648..dc85a23e5b 100644 --- a/packages/backend/src/core/CoreModule.ts +++ b/packages/backend/src/core/CoreModule.ts @@ -24,7 +24,6 @@ import { AppLockService } from './AppLockService.js'; import { AchievementService } from './AchievementService.js'; import { AvatarDecorationService } from './AvatarDecorationService.js'; import { CaptchaService } from './CaptchaService.js'; -import { CreateSystemUserService } from './CreateSystemUserService.js'; import { CustomEmojiService } from './CustomEmojiService.js'; import { DeleteAccountService } from './DeleteAccountService.js'; import { DownloadService } from './DownloadService.js'; @@ -37,7 +36,7 @@ import { HashtagService } from './HashtagService.js'; import { HttpRequestService } from './HttpRequestService.js'; import { IdService } from './IdService.js'; import { ImageProcessingService } from './ImageProcessingService.js'; -import { InstanceActorService } from './InstanceActorService.js'; +import { SystemAccountService } from './SystemAccountService.js'; import { InternalStorageService } from './InternalStorageService.js'; import { MetaService } from './MetaService.js'; import { MfmService } from './MfmService.js'; @@ -69,7 +68,6 @@ import { UserSuspendService } from './UserSuspendService.js'; import { UserAuthService } from './UserAuthService.js'; import { VideoProcessingService } from './VideoProcessingService.js'; import { UserWebhookService } from './UserWebhookService.js'; -import { ProxyAccountService } from './ProxyAccountService.js'; import { UtilityService } from './UtilityService.js'; import { FileInfoService } from './FileInfoService.js'; import { SearchService } from './SearchService.js'; @@ -167,7 +165,6 @@ const $AppLockService: Provider = { provide: 'AppLockService', useExisting: AppL const $AchievementService: Provider = { provide: 'AchievementService', useExisting: AchievementService }; const $AvatarDecorationService: Provider = { provide: 'AvatarDecorationService', useExisting: AvatarDecorationService }; const $CaptchaService: Provider = { provide: 'CaptchaService', useExisting: CaptchaService }; -const $CreateSystemUserService: Provider = { provide: 'CreateSystemUserService', useExisting: CreateSystemUserService }; const $CustomEmojiService: Provider = { provide: 'CustomEmojiService', useExisting: CustomEmojiService }; const $DeleteAccountService: Provider = { provide: 'DeleteAccountService', useExisting: DeleteAccountService }; const $DownloadService: Provider = { provide: 'DownloadService', useExisting: DownloadService }; @@ -180,7 +177,6 @@ const $HashtagService: Provider = { provide: 'HashtagService', useExisting: Hash const $HttpRequestService: Provider = { provide: 'HttpRequestService', useExisting: HttpRequestService }; const $IdService: Provider = { provide: 'IdService', useExisting: IdService }; const $ImageProcessingService: Provider = { provide: 'ImageProcessingService', useExisting: ImageProcessingService }; -const $InstanceActorService: Provider = { provide: 'InstanceActorService', useExisting: InstanceActorService }; const $InternalStorageService: Provider = { provide: 'InternalStorageService', useExisting: InternalStorageService }; const $MetaService: Provider = { provide: 'MetaService', useExisting: MetaService }; const $MfmService: Provider = { provide: 'MfmService', useExisting: MfmService }; @@ -191,7 +187,7 @@ const $NotePiningService: Provider = { provide: 'NotePiningService', useExisting const $NoteReadService: Provider = { provide: 'NoteReadService', useExisting: NoteReadService }; const $NotificationService: Provider = { provide: 'NotificationService', useExisting: NotificationService }; const $PollService: Provider = { provide: 'PollService', useExisting: PollService }; -const $ProxyAccountService: Provider = { provide: 'ProxyAccountService', useExisting: ProxyAccountService }; +const $SystemAccountService: Provider = { provide: 'SystemAccountService', useExisting: SystemAccountService }; const $PushNotificationService: Provider = { provide: 'PushNotificationService', useExisting: PushNotificationService }; const $QueryService: Provider = { provide: 'QueryService', useExisting: QueryService }; const $ReactionService: Provider = { provide: 'ReactionService', useExisting: ReactionService }; @@ -318,7 +314,6 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting AchievementService, AvatarDecorationService, CaptchaService, - CreateSystemUserService, CustomEmojiService, DeleteAccountService, DownloadService, @@ -331,7 +326,6 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting HttpRequestService, IdService, ImageProcessingService, - InstanceActorService, InternalStorageService, MetaService, MfmService, @@ -342,7 +336,7 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting NoteReadService, NotificationService, PollService, - ProxyAccountService, + SystemAccountService, PushNotificationService, QueryService, ReactionService, @@ -465,7 +459,6 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting $AchievementService, $AvatarDecorationService, $CaptchaService, - $CreateSystemUserService, $CustomEmojiService, $DeleteAccountService, $DownloadService, @@ -478,7 +471,6 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting $HttpRequestService, $IdService, $ImageProcessingService, - $InstanceActorService, $InternalStorageService, $MetaService, $MfmService, @@ -489,7 +481,7 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting $NoteReadService, $NotificationService, $PollService, - $ProxyAccountService, + $SystemAccountService, $PushNotificationService, $QueryService, $ReactionService, @@ -613,7 +605,6 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting AchievementService, AvatarDecorationService, CaptchaService, - CreateSystemUserService, CustomEmojiService, DeleteAccountService, DownloadService, @@ -626,7 +617,6 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting HttpRequestService, IdService, ImageProcessingService, - InstanceActorService, InternalStorageService, MetaService, MfmService, @@ -637,7 +627,7 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting NoteReadService, NotificationService, PollService, - ProxyAccountService, + SystemAccountService, PushNotificationService, QueryService, ReactionService, @@ -759,7 +749,6 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting $AchievementService, $AvatarDecorationService, $CaptchaService, - $CreateSystemUserService, $CustomEmojiService, $DeleteAccountService, $DownloadService, @@ -772,7 +761,6 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting $HttpRequestService, $IdService, $ImageProcessingService, - $InstanceActorService, $InternalStorageService, $MetaService, $MfmService, @@ -783,7 +771,7 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting $NoteReadService, $NotificationService, $PollService, - $ProxyAccountService, + $SystemAccountService, $PushNotificationService, $QueryService, $ReactionService, diff --git a/packages/backend/src/core/CreateSystemUserService.ts b/packages/backend/src/core/CreateSystemUserService.ts deleted file mode 100644 index 6c5b0f6a36..0000000000 --- a/packages/backend/src/core/CreateSystemUserService.ts +++ /dev/null @@ -1,86 +0,0 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - -import { randomUUID } from 'node:crypto'; -import { Inject, Injectable } from '@nestjs/common'; -import bcrypt from 'bcryptjs'; -import { IsNull, DataSource } from 'typeorm'; -import { genRsaKeyPair } from '@/misc/gen-key-pair.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 { DI } from '@/di-symbols.js'; -import generateNativeUserToken from '@/misc/generate-native-user-token.js'; -import { bindThis } from '@/decorators.js'; - -@Injectable() -export class CreateSystemUserService { - constructor( - @Inject(DI.db) - private db: DataSource, - - private idService: IdService, - ) { - } - - @bindThis - public async createSystemUser(username: string): Promise { - const password = randomUUID(); - - // Generate hash of password - const salt = await bcrypt.genSalt(8); - const hash = await bcrypt.hash(password, salt); - - // Generate secret - const secret = generateNativeUserToken(); - - const keyPair = await genRsaKeyPair(); - - 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 user is already exists'); - - account = await transactionalEntityManager.insert(MiUser, { - id: this.idService.gen(), - username: username, - usernameLower: username.toLowerCase(), - host: null, - token: secret, - isRoot: false, - isLocked: true, - isExplorable: false, - isBot: true, - }).then(x => transactionalEntityManager.findOneByOrFail(MiUser, x.identifiers[0])); - - await transactionalEntityManager.insert(MiUserKeypair, { - publicKey: keyPair.publicKey, - privateKey: keyPair.privateKey, - userId: account.id, - }); - - await transactionalEntityManager.insert(MiUserProfile, { - userId: account.id, - autoAcceptFollowed: false, - password: hash, - }); - - await transactionalEntityManager.insert(MiUsedUsername, { - createdAt: new Date(), - username: username.toLowerCase(), - }); - }); - - return account; - } -} diff --git a/packages/backend/src/core/InstanceActorService.ts b/packages/backend/src/core/InstanceActorService.ts deleted file mode 100644 index 22c47297a3..0000000000 --- a/packages/backend/src/core/InstanceActorService.ts +++ /dev/null @@ -1,57 +0,0 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - -import { Inject, Injectable } from '@nestjs/common'; -import { IsNull, Not } from 'typeorm'; -import type { MiLocalUser } from '@/models/User.js'; -import type { UsersRepository } from '@/models/_.js'; -import { MemorySingleCache } from '@/misc/cache.js'; -import { DI } from '@/di-symbols.js'; -import { CreateSystemUserService } from '@/core/CreateSystemUserService.js'; -import { bindThis } from '@/decorators.js'; - -const ACTOR_USERNAME = 'instance.actor' as const; - -@Injectable() -export class InstanceActorService { - private cache: MemorySingleCache; - - constructor( - @Inject(DI.usersRepository) - private usersRepository: UsersRepository, - - private createSystemUserService: CreateSystemUserService, - ) { - this.cache = new MemorySingleCache(Infinity); - } - - @bindThis - public async realLocalUsersPresent(): Promise { - return await this.usersRepository.existsBy({ - host: IsNull(), - username: Not(ACTOR_USERNAME), - }); - } - - @bindThis - public async getInstanceActor(): Promise { - const cached = this.cache.get(); - if (cached) return cached; - - const user = await this.usersRepository.findOneBy({ - host: IsNull(), - username: ACTOR_USERNAME, - }) as MiLocalUser | undefined; - - if (user) { - this.cache.set(user); - return user; - } else { - const created = await this.createSystemUserService.createSystemUser(ACTOR_USERNAME) as MiLocalUser; - this.cache.set(created); - return created; - } - } -} diff --git a/packages/backend/src/core/ProxyAccountService.ts b/packages/backend/src/core/ProxyAccountService.ts deleted file mode 100644 index c3ff2a68d3..0000000000 --- a/packages/backend/src/core/ProxyAccountService.ts +++ /dev/null @@ -1,28 +0,0 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - -import { Inject, Injectable } from '@nestjs/common'; -import type { MiMeta, UsersRepository } from '@/models/_.js'; -import type { MiLocalUser } from '@/models/User.js'; -import { DI } from '@/di-symbols.js'; -import { bindThis } from '@/decorators.js'; - -@Injectable() -export class ProxyAccountService { - constructor( - @Inject(DI.meta) - private meta: MiMeta, - - @Inject(DI.usersRepository) - private usersRepository: UsersRepository, - ) { - } - - @bindThis - public async fetch(): Promise { - if (this.meta.proxyAccountId == null) return null; - return await this.usersRepository.findOneByOrFail({ id: this.meta.proxyAccountId }) as MiLocalUser; - } -} diff --git a/packages/backend/src/core/RelayService.ts b/packages/backend/src/core/RelayService.ts index db32114346..9120de1f9f 100644 --- a/packages/backend/src/core/RelayService.ts +++ b/packages/backend/src/core/RelayService.ts @@ -4,53 +4,34 @@ */ import { Inject, Injectable } from '@nestjs/common'; -import { IsNull } from 'typeorm'; -import type { MiLocalUser, MiUser } from '@/models/User.js'; -import type { RelaysRepository, UsersRepository } from '@/models/_.js'; +import type { MiUser } from '@/models/User.js'; +import type { RelaysRepository } from '@/models/_.js'; import { IdService } from '@/core/IdService.js'; import { MemorySingleCache } from '@/misc/cache.js'; import type { MiRelay } from '@/models/Relay.js'; import { QueueService } from '@/core/QueueService.js'; -import { CreateSystemUserService } from '@/core/CreateSystemUserService.js'; import { ApRendererService } from '@/core/activitypub/ApRendererService.js'; import { DI } from '@/di-symbols.js'; import { deepClone } from '@/misc/clone.js'; import { bindThis } from '@/decorators.js'; - -const ACTOR_USERNAME = 'relay.actor' as const; +import { SystemAccountService } from '@/core/SystemAccountService.js'; @Injectable() export class RelayService { private relaysCache: MemorySingleCache; constructor( - @Inject(DI.usersRepository) - private usersRepository: UsersRepository, - @Inject(DI.relaysRepository) private relaysRepository: RelaysRepository, private idService: IdService, private queueService: QueueService, - private createSystemUserService: CreateSystemUserService, + private systemAccountService: SystemAccountService, private apRendererService: ApRendererService, ) { this.relaysCache = new MemorySingleCache(1000 * 60 * 10); // 10m } - @bindThis - private async getRelayActor(): Promise { - const user = await this.usersRepository.findOneBy({ - host: IsNull(), - username: ACTOR_USERNAME, - }); - - if (user) return user as MiLocalUser; - - const created = await this.createSystemUserService.createSystemUser(ACTOR_USERNAME); - return created as MiLocalUser; - } - @bindThis public async addRelay(inbox: string): Promise { const relay = await this.relaysRepository.insertOne({ @@ -59,8 +40,8 @@ export class RelayService { status: 'requesting', }); - const relayActor = await this.getRelayActor(); - const follow = await this.apRendererService.renderFollowRelay(relay, relayActor); + const relayActor = await this.systemAccountService.fetch('relay'); + const follow = this.apRendererService.renderFollowRelay(relay, relayActor); const activity = this.apRendererService.addContext(follow); this.queueService.deliver(relayActor, activity, relay.inbox, false); @@ -77,7 +58,7 @@ export class RelayService { throw new Error('relay not found'); } - const relayActor = await this.getRelayActor(); + const relayActor = await this.systemAccountService.fetch('relay'); const follow = this.apRendererService.renderFollowRelay(relay, relayActor); const undo = this.apRendererService.renderUndo(follow, relayActor); const activity = this.apRendererService.addContext(undo); diff --git a/packages/backend/src/core/SignupService.ts b/packages/backend/src/core/SignupService.ts index 3865392b7f..814ae4209d 100644 --- a/packages/backend/src/core/SignupService.ts +++ b/packages/backend/src/core/SignupService.ts @@ -16,7 +16,7 @@ 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 { InstanceActorService } from '@/core/SystemAccountService.js'; import { bindThis } from '@/decorators.js'; import UsersChart from '@/core/chart/charts/users.js'; import { UtilityService } from '@/core/UtilityService.js'; diff --git a/packages/backend/src/core/SystemAccountService.ts b/packages/backend/src/core/SystemAccountService.ts new file mode 100644 index 0000000000..31617f0564 --- /dev/null +++ b/packages/backend/src/core/SystemAccountService.ts @@ -0,0 +1,155 @@ +/* + * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-License-Identifier: AGPL-3.0-only + */ + +import { randomUUID } from 'node:crypto'; +import { Inject, Injectable } from '@nestjs/common'; +import { DataSource, IsNull } from 'typeorm'; +import bcrypt from 'bcryptjs'; +import { MiLocalUser, MiUser } from '@/models/User.js'; +import { MiSystemAccount, MiUsedUsername, MiUserKeypair, MiUserProfile, type UsersRepository, type SystemAccountsRepository } from '@/models/_.js'; +import type { UserProfilesRepository } from '@/models/_.js'; +import { MemoryKVCache } from '@/misc/cache.js'; +import { DI } from '@/di-symbols.js'; +import { bindThis } from '@/decorators.js'; +import generateNativeUserToken from '@/misc/generate-native-user-token.js'; +import { IdService } from '@/core/IdService.js'; +import { genRsaKeyPair } from '@/misc/gen-key-pair.js'; + +export const SYSTEM_ACCOUNT_TYPES = ['actor', 'relay', 'proxy'] as const; + +@Injectable() +export class SystemAccountService { + private cache: MemoryKVCache; + + constructor( + @Inject(DI.db) + private db: DataSource, + + @Inject(DI.systemAccountsRepository) + private systemAccountsRepository: SystemAccountsRepository, + + @Inject(DI.usersRepository) + private usersRepository: UsersRepository, + + @Inject(DI.userProfilesRepository) + private userProfilesRepository: UserProfilesRepository, + + private idService: IdService, + ) { + this.cache = new MemoryKVCache(1000 * 60 * 10); // 10m + } + + @bindThis + public async fetch(type: typeof SYSTEM_ACCOUNT_TYPES[number]): Promise { + const cached = this.cache.get(type); + if (cached) return cached; + + const systemAccount = await this.systemAccountsRepository.findOne({ + where: { type: type }, + relations: ['user'], + }); + + if (systemAccount) { + this.cache.set(type, systemAccount.user as MiLocalUser); + return systemAccount.user as MiLocalUser; + } else { + const created = await this.createCorrespondingUser(type, { + username: `system.${type}`, + }); + this.cache.set(type, created); + return created; + } + } + + @bindThis + private async createCorrespondingUser(type: typeof SYSTEM_ACCOUNT_TYPES[number], extra: { + username: string; + name?: string; + }): Promise { + const password = randomUUID(); + + // Generate hash of password + const salt = await bcrypt.genSalt(8); + const hash = await bcrypt.hash(password, salt); + + // Generate secret + const secret = generateNativeUserToken(); + + const keyPair = await genRsaKeyPair(); + + let account!: MiUser; + + // Start transaction + await this.db.transaction(async transactionalEntityManager => { + const exist = await transactionalEntityManager.findOneBy(MiUser, { + usernameLower: extra.username.toLowerCase(), + host: IsNull(), + }); + + if (exist) throw new Error('the user is already exists'); + + account = await transactionalEntityManager.insert(MiUser, { + id: this.idService.gen(), + username: extra.username, + usernameLower: extra.username.toLowerCase(), + host: null, + token: secret, + isRoot: false, + isLocked: true, + isExplorable: false, + isBot: true, + name: extra.name, + }).then(x => transactionalEntityManager.findOneByOrFail(MiUser, x.identifiers[0])); + + await transactionalEntityManager.insert(MiUserKeypair, { + publicKey: keyPair.publicKey, + privateKey: keyPair.privateKey, + userId: account.id, + }); + + await transactionalEntityManager.insert(MiUserProfile, { + userId: account.id, + autoAcceptFollowed: false, + password: hash, + }); + + await transactionalEntityManager.insert(MiUsedUsername, { + createdAt: new Date(), + username: extra.username.toLowerCase(), + }); + + await transactionalEntityManager.insert(MiSystemAccount, { + id: this.idService.gen(), + userId: account.id, + type: type, + }); + }); + + return account as MiLocalUser; + } + + @bindThis + public async updateCorrespondingUserProfile(type: typeof SYSTEM_ACCOUNT_TYPES[number], extra: { + name?: string; + description?: string; + }): Promise { + const user = await this.fetch(type); + + const updates = {} as Partial; + if (extra.name !== undefined) updates.name = extra.name; + + await this.usersRepository.update(user.id, updates); + + const profileUpdates = {} as Partial; + if (extra.description !== undefined) profileUpdates.description = extra.description; + + await this.userProfilesRepository.update(user.id, profileUpdates); + + const updated = await this.usersRepository.findOneByOrFail({ id: user.id }) as MiLocalUser; + this.cache.set(type, updated); + + return updated; + } +} diff --git a/packages/backend/src/core/activitypub/ApResolverService.ts b/packages/backend/src/core/activitypub/ApResolverService.ts index 52cc569140..9f5ec7a543 100644 --- a/packages/backend/src/core/activitypub/ApResolverService.ts +++ b/packages/backend/src/core/activitypub/ApResolverService.ts @@ -6,7 +6,7 @@ import { Inject, Injectable } from '@nestjs/common'; import { IsNull, Not } from 'typeorm'; import type { MiLocalUser, MiRemoteUser } from '@/models/User.js'; -import { InstanceActorService } from '@/core/InstanceActorService.js'; +import { InstanceActorService } from '@/core/SystemAccountService.js'; import type { NotesRepository, PollsRepository, NoteReactionsRepository, UsersRepository, FollowRequestsRepository, MiMeta } from '@/models/_.js'; import type { Config } from '@/config.js'; import { HttpRequestService } from '@/core/HttpRequestService.js'; diff --git a/packages/backend/src/core/entities/MetaEntityService.ts b/packages/backend/src/core/entities/MetaEntityService.ts index ec0b5360f4..31598aec4b 100644 --- a/packages/backend/src/core/entities/MetaEntityService.ts +++ b/packages/backend/src/core/entities/MetaEntityService.ts @@ -12,7 +12,7 @@ import type { AdsRepository } from '@/models/_.js'; import { MAX_NOTE_TEXT_LENGTH } from '@/const.js'; import { bindThis } from '@/decorators.js'; import { UserEntityService } from '@/core/entities/UserEntityService.js'; -import { InstanceActorService } from '@/core/InstanceActorService.js'; +import { InstanceActorService } from '@/core/SystemAccountService.js'; import type { Config } from '@/config.js'; import { DI } from '@/di-symbols.js'; import { DEFAULT_POLICIES } from '@/core/RoleService.js'; @@ -148,14 +148,12 @@ export class MetaEntityService { const packed = await this.pack(instance); - const proxyAccount = instance.proxyAccountId ? await this.userEntityService.pack(instance.proxyAccountId).catch(() => null) : null; - const packDetailed: Packed<'MetaDetailed'> = { ...packed, cacheRemoteFiles: instance.cacheRemoteFiles, cacheRemoteSensitiveFiles: instance.cacheRemoteSensitiveFiles, requireSetup: !await this.instanceActorService.realLocalUsersPresent(), - proxyAccountName: proxyAccount ? proxyAccount.username : null, + proxyAccountName: proxyAccount ? proxyAccount.username : null, // TODO features: { localTimeline: instance.policies.ltlAvailable, globalTimeline: instance.policies.gtlAvailable, diff --git a/packages/backend/src/models/Meta.ts b/packages/backend/src/models/Meta.ts index ad5e31ad6f..4be13e6ad7 100644 --- a/packages/backend/src/models/Meta.ts +++ b/packages/backend/src/models/Meta.ts @@ -172,18 +172,6 @@ export class MiMeta { }) public cacheRemoteSensitiveFiles: boolean; - @Column({ - ...id(), - nullable: true, - }) - public proxyAccountId: MiUser['id'] | null; - - @ManyToOne(type => MiUser, { - onDelete: 'SET NULL', - }) - @JoinColumn() - public proxyAccount: MiUser | null; - @Column('boolean', { default: false, }) diff --git a/packages/backend/src/server/NodeinfoServerService.ts b/packages/backend/src/server/NodeinfoServerService.ts index 9a641007ee..8355497762 100644 --- a/packages/backend/src/server/NodeinfoServerService.ts +++ b/packages/backend/src/server/NodeinfoServerService.ts @@ -70,6 +70,7 @@ export class NodeinfoServerService { const activeHalfyear = null; const activeMonth = null; + // TODO const proxyAccount = meta.proxyAccountId ? await this.userEntityService.pack(meta.proxyAccountId).catch(() => null) : null; const basePolicies = { ...DEFAULT_POLICIES, ...meta.policies }; diff --git a/packages/backend/src/server/api/endpoints/admin/accounts/create.ts b/packages/backend/src/server/api/endpoints/admin/accounts/create.ts index d30131a62f..f3cee12a99 100644 --- a/packages/backend/src/server/api/endpoints/admin/accounts/create.ts +++ b/packages/backend/src/server/api/endpoints/admin/accounts/create.ts @@ -9,7 +9,7 @@ import { Endpoint } from '@/server/api/endpoint-base.js'; import type { UsersRepository } from '@/models/_.js'; import { SignupService } from '@/core/SignupService.js'; import { UserEntityService } from '@/core/entities/UserEntityService.js'; -import { InstanceActorService } from '@/core/InstanceActorService.js'; +import { InstanceActorService } from '@/core/SystemAccountService.js'; import { localUsernameSchema, passwordSchema } from '@/models/User.js'; import { DI } from '@/di-symbols.js'; import type { Config } from '@/config.js'; diff --git a/packages/backend/src/server/api/endpoints/admin/meta.ts b/packages/backend/src/server/api/endpoints/admin/meta.ts index 912c8defbe..0ecf9c0d4f 100644 --- a/packages/backend/src/server/api/endpoints/admin/meta.ts +++ b/packages/backend/src/server/api/endpoints/admin/meta.ts @@ -231,11 +231,6 @@ export const meta = { type: 'boolean', optional: false, nullable: false, }, - proxyAccountId: { - type: 'string', - optional: false, nullable: true, - format: 'id', - }, email: { type: 'string', optional: false, nullable: true, @@ -608,7 +603,6 @@ export default class extends Endpoint { // eslint- sensitiveMediaDetectionSensitivity: instance.sensitiveMediaDetectionSensitivity, setSensitiveFlagAutomatically: instance.setSensitiveFlagAutomatically, enableSensitiveMediaDetectionForVideos: instance.enableSensitiveMediaDetectionForVideos, - proxyAccountId: instance.proxyAccountId, email: instance.email, smtpSecure: instance.smtpSecure, smtpHost: instance.smtpHost, diff --git a/packages/backend/src/server/api/endpoints/admin/update-meta.ts b/packages/backend/src/server/api/endpoints/admin/update-meta.ts index 38ef0d1de8..d05cdbb6a3 100644 --- a/packages/backend/src/server/api/endpoints/admin/update-meta.ts +++ b/packages/backend/src/server/api/endpoints/admin/update-meta.ts @@ -88,7 +88,6 @@ export const paramDef = { sensitiveMediaDetectionSensitivity: { type: 'string', enum: ['medium', 'low', 'high', 'veryLow', 'veryHigh'] }, setSensitiveFlagAutomatically: { type: 'boolean' }, enableSensitiveMediaDetectionForVideos: { type: 'boolean' }, - proxyAccountId: { type: 'string', format: 'misskey:id', nullable: true }, maintainerName: { type: 'string', nullable: true }, maintainerEmail: { type: 'string', nullable: true }, langs: { @@ -387,10 +386,6 @@ export default class extends Endpoint { // eslint- set.enableSensitiveMediaDetectionForVideos = ps.enableSensitiveMediaDetectionForVideos; } - if (ps.proxyAccountId !== undefined) { - set.proxyAccountId = ps.proxyAccountId; - } - if (ps.maintainerName !== undefined) { set.maintainerName = ps.maintainerName; } diff --git a/packages/backend/src/server/api/endpoints/admin/update-proxy-account.ts b/packages/backend/src/server/api/endpoints/admin/update-proxy-account.ts new file mode 100644 index 0000000000..1322327dd1 --- /dev/null +++ b/packages/backend/src/server/api/endpoints/admin/update-proxy-account.ts @@ -0,0 +1,90 @@ +/* + * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-License-Identifier: AGPL-3.0-only + */ + +import { Inject, Injectable } from '@nestjs/common'; +import type { + UsersRepository, +} from '@/models/_.js'; +import { Endpoint } from '@/server/api/endpoint-base.js'; +import { DI } from '@/di-symbols.js'; +import { RoleService } from '@/core/RoleService.js'; +import { + descriptionSchema, + nameSchema, +} from '@/models/User.js'; +import { ApiError } from '@/server/api/error.js'; +import { UserEntityService } from '@/core/entities/UserEntityService.js'; +import { ModerationLogService } from '@/core/ModerationLogService.js'; +import { SystemAccountService } from '@/core/SystemAccountService.js'; + +export const meta = { + tags: ['admin'], + + requireCredential: true, + requireModerator: true, + kind: 'write:admin:update-proxy-account', + + errors: { + accessDenied: { + message: 'Only administrators can edit members of the role.', + code: 'ACCESS_DENIED', + id: '101f9105-27cb-489c-842a-69b6d2092c03', + }, + }, + + res: { + type: 'object', + nullable: false, optional: false, + ref: 'UserDetailed', + }, + + required: [], +} as const; + +export const paramDef = { + type: 'object', + properties: { + name: { ...nameSchema, nullable: true }, + description: { ...descriptionSchema, nullable: true }, + avatarId: { type: 'string', format: 'misskey:id', nullable: true }, + bannerId: { type: 'string', format: 'misskey:id', nullable: true }, + }, +} as const; + +@Injectable() +export default class extends Endpoint { // eslint-disable-line import/no-default-export + constructor( + @Inject(DI.usersRepository) + private usersRepository: UsersRepository, + + private roleService: RoleService, + private userEntityService: UserEntityService, + private moderationLogService: ModerationLogService, + private systemAccountService: SystemAccountService, + ) { + super(meta, paramDef, async (ps, me) => { + const _me = await this.usersRepository.findOneByOrFail({ id: me.id }); + if (!await this.roleService.isModerator(_me)) { + throw new ApiError(meta.errors.accessDenied); + } + + const proxy = await this.systemAccountService.updateCorrespondingUserProfile('proxy', { + description: ps.description, + }); + + const updated = await this.userEntityService.pack(proxy.id, proxy, { + schema: 'MeDetailed', + }); + + this.moderationLogService.log(me, 'updateUser', { + userId: proxy.id, + userUsername: proxy.username, + userHost: proxy.host, + }); + + return updated; + }); + } +}