summaryrefslogtreecommitdiff
path: root/packages/backend
diff options
context:
space:
mode:
authorsyuilo <Syuilotan@yahoo.co.jp>2023-01-12 21:02:26 +0900
committerGitHub <noreply@github.com>2023-01-12 21:02:26 +0900
commit2470afaa2e200fb2fc748e0f8eef5e2c215369b6 (patch)
treec270452679996127a9d15c4ba5f97b39bb9ba560 /packages/backend
parentUpdate CHANGELOG.md (diff)
downloadsharkey-2470afaa2e200fb2fc748e0f8eef5e2c215369b6.tar.gz
sharkey-2470afaa2e200fb2fc748e0f8eef5e2c215369b6.tar.bz2
sharkey-2470afaa2e200fb2fc748e0f8eef5e2c215369b6.zip
Role (#9437)
* wip * Update CHANGELOG.md * wip * wip * wip * Update create.ts * wip * wip * Update CHANGELOG.md * wip * wip * wip * wip * wip * wip * wip * Update CHANGELOG.md * wip * wip * Update delete.ts * Update delete.ts * wip * wip * wip * Update account-info.vue * wip * wip * Update settings.vue * Update user-info.vue * wip * Update show-file.ts * Update show-user.ts * wip * wip * Update delete.ts * wip * wip * Update overview.moderators.vue * Create 1673500412259-Role.js * wip * wip * Update roles.vue * 色 * Update roles.vue * integrate silence * wip * wip
Diffstat (limited to 'packages/backend')
-rw-r--r--packages/backend/migration/1673500412259-Role.js37
-rw-r--r--packages/backend/migration/1673515526953-RoleColor.js11
-rw-r--r--packages/backend/migration/1673522856499-RoleIroiro.js13
-rw-r--r--packages/backend/migration/1673524604156-RoleLastUsedAt.js13
-rw-r--r--packages/backend/src/core/CoreModule.ts12
-rw-r--r--packages/backend/src/core/CreateSystemUserService.ts2
-rw-r--r--packages/backend/src/core/DeleteAccountService.ts3
-rw-r--r--packages/backend/src/core/DriveService.ts19
-rw-r--r--packages/backend/src/core/NoteCreateService.ts11
-rw-r--r--packages/backend/src/core/RoleService.ts201
-rw-r--r--packages/backend/src/core/SignupService.ts6
-rw-r--r--packages/backend/src/core/UserCacheService.ts4
-rw-r--r--packages/backend/src/core/entities/RoleEntityService.ts80
-rw-r--r--packages/backend/src/core/entities/UserEntityService.ts19
-rw-r--r--packages/backend/src/di-symbols.ts2
-rw-r--r--packages/backend/src/models/RepositoryModule.ts18
-rw-r--r--packages/backend/src/models/entities/Meta.ts21
-rw-r--r--packages/backend/src/models/entities/Role.ts66
-rw-r--r--packages/backend/src/models/entities/RoleAssignment.ts42
-rw-r--r--packages/backend/src/models/entities/User.ts22
-rw-r--r--packages/backend/src/models/index.ts6
-rw-r--r--packages/backend/src/postgre.ts4
-rw-r--r--packages/backend/src/server/NodeinfoServerService.ts7
-rw-r--r--packages/backend/src/server/api/ApiCallService.ts57
-rw-r--r--packages/backend/src/server/api/EndpointsModule.ts52
-rw-r--r--packages/backend/src/server/api/endpoints.ts34
-rw-r--r--packages/backend/src/server/api/endpoints/admin/accounts/create.ts2
-rw-r--r--packages/backend/src/server/api/endpoints/admin/accounts/delete.ts10
-rw-r--r--packages/backend/src/server/api/endpoints/admin/delete-all-files-of-a-user.ts2
-rw-r--r--packages/backend/src/server/api/endpoints/admin/drive-capacity-override.ts61
-rw-r--r--packages/backend/src/server/api/endpoints/admin/drive/show-file.ts9
-rw-r--r--packages/backend/src/server/api/endpoints/admin/get-index-stats.ts2
-rw-r--r--packages/backend/src/server/api/endpoints/admin/get-table-stats.ts2
-rw-r--r--packages/backend/src/server/api/endpoints/admin/get-user-ips.ts2
-rw-r--r--packages/backend/src/server/api/endpoints/admin/meta.ts9
-rw-r--r--packages/backend/src/server/api/endpoints/admin/moderators/add.ts49
-rw-r--r--packages/backend/src/server/api/endpoints/admin/reset-password.ts4
-rw-r--r--packages/backend/src/server/api/endpoints/admin/roles/assign.ts96
-rw-r--r--packages/backend/src/server/api/endpoints/admin/roles/create.ts75
-rw-r--r--packages/backend/src/server/api/endpoints/admin/roles/delete.ts53
-rw-r--r--packages/backend/src/server/api/endpoints/admin/roles/list.ts39
-rw-r--r--packages/backend/src/server/api/endpoints/admin/roles/show.ts50
-rw-r--r--packages/backend/src/server/api/endpoints/admin/roles/unassign.ts101
-rw-r--r--packages/backend/src/server/api/endpoints/admin/roles/update-default-role-override.ts (renamed from packages/backend/src/server/api/endpoints/admin/moderators/remove.ts)31
-rw-r--r--packages/backend/src/server/api/endpoints/admin/roles/update.ts82
-rw-r--r--packages/backend/src/server/api/endpoints/admin/show-user.ts21
-rw-r--r--packages/backend/src/server/api/endpoints/admin/show-users.ts26
-rw-r--r--packages/backend/src/server/api/endpoints/admin/silence-user.ts55
-rw-r--r--packages/backend/src/server/api/endpoints/admin/suspend-user.ts10
-rw-r--r--packages/backend/src/server/api/endpoints/admin/unsilence-user.ts51
-rw-r--r--packages/backend/src/server/api/endpoints/admin/update-meta.ts15
-rw-r--r--packages/backend/src/server/api/endpoints/antennas/create.ts4
-rw-r--r--packages/backend/src/server/api/endpoints/drive.ts6
-rw-r--r--packages/backend/src/server/api/endpoints/drive/files/delete.ts4
-rw-r--r--packages/backend/src/server/api/endpoints/drive/files/show.ts4
-rw-r--r--packages/backend/src/server/api/endpoints/drive/files/update.ts4
-rw-r--r--packages/backend/src/server/api/endpoints/meta.ts20
-rw-r--r--packages/backend/src/server/api/endpoints/notes/delete.ts6
-rw-r--r--packages/backend/src/server/api/endpoints/notes/global-timeline.ts10
-rw-r--r--packages/backend/src/server/api/endpoints/notes/hybrid-timeline.ts6
-rw-r--r--packages/backend/src/server/api/endpoints/notes/local-timeline.ts10
-rw-r--r--packages/backend/src/server/api/endpoints/users.ts5
-rw-r--r--packages/backend/src/server/api/endpoints/users/report-abuse.ts14
-rw-r--r--packages/backend/src/server/api/endpoints/users/show.ts8
-rw-r--r--packages/backend/src/server/api/stream/channels/global-timeline.ts10
-rw-r--r--packages/backend/src/server/api/stream/channels/hybrid-timeline.ts8
-rw-r--r--packages/backend/src/server/api/stream/channels/local-timeline.ts10
-rw-r--r--packages/backend/src/server/api/stream/types.ts34
-rw-r--r--packages/backend/src/server/web/ClientServerService.ts9
-rw-r--r--packages/backend/src/server/web/boot.js4
70 files changed, 1309 insertions, 486 deletions
diff --git a/packages/backend/migration/1673500412259-Role.js b/packages/backend/migration/1673500412259-Role.js
new file mode 100644
index 0000000000..a8acedf5b7
--- /dev/null
+++ b/packages/backend/migration/1673500412259-Role.js
@@ -0,0 +1,37 @@
+export class Role1673500412259 {
+ name = 'Role1673500412259'
+
+ async up(queryRunner) {
+ await queryRunner.query(`CREATE TABLE "role" ("id" character varying(32) NOT NULL, "createdAt" TIMESTAMP WITH TIME ZONE NOT NULL, "updatedAt" TIMESTAMP WITH TIME ZONE NOT NULL, "name" character varying(256) NOT NULL, "description" character varying(1024) NOT NULL, "isPublic" boolean NOT NULL DEFAULT false, "isModerator" boolean NOT NULL DEFAULT false, "isAdministrator" boolean NOT NULL DEFAULT false, "options" jsonb NOT NULL DEFAULT '{}', CONSTRAINT "PK_b36bcfe02fc8de3c57a8b2391c2" PRIMARY KEY ("id")); COMMENT ON COLUMN "role"."createdAt" IS 'The created date of the Role.'; COMMENT ON COLUMN "role"."updatedAt" IS 'The updated date of the Role.'`);
+ await queryRunner.query(`CREATE TABLE "role_assignment" ("id" character varying(32) NOT NULL, "createdAt" TIMESTAMP WITH TIME ZONE NOT NULL, "userId" character varying(32) NOT NULL, "roleId" character varying(32) NOT NULL, CONSTRAINT "PK_7e79671a8a5db18936173148cb4" PRIMARY KEY ("id")); COMMENT ON COLUMN "role_assignment"."createdAt" IS 'The created date of the RoleAssignment.'; COMMENT ON COLUMN "role_assignment"."userId" IS 'The user ID.'; COMMENT ON COLUMN "role_assignment"."roleId" IS 'The role ID.'`);
+ await queryRunner.query(`CREATE INDEX "IDX_db5b72c16227c97ca88734d5c2" ON "role_assignment" ("userId") `);
+ await queryRunner.query(`CREATE INDEX "IDX_f0de67fd09cd3cd0aabca79994" ON "role_assignment" ("roleId") `);
+ await queryRunner.query(`CREATE UNIQUE INDEX "IDX_0953deda7ce6e1448e935859e5" ON "role_assignment" ("userId", "roleId") `);
+ await queryRunner.query(`ALTER TABLE "user" RENAME COLUMN "isAdmin" TO "isRoot"`);
+ await queryRunner.query(`ALTER TABLE "user" DROP COLUMN "isModerator"`);
+ await queryRunner.query(`ALTER TABLE "user" DROP COLUMN "driveCapacityOverrideMb"`);
+ await queryRunner.query(`ALTER TABLE "meta" DROP COLUMN "disableLocalTimeline"`);
+ await queryRunner.query(`ALTER TABLE "meta" DROP COLUMN "disableGlobalTimeline"`);
+ await queryRunner.query(`ALTER TABLE "meta" DROP COLUMN "localDriveCapacityMb"`);
+ await queryRunner.query(`ALTER TABLE "meta" ADD "defaultRoleOverride" jsonb NOT NULL DEFAULT '{}'`);
+ await queryRunner.query(`ALTER TABLE "role_assignment" ADD CONSTRAINT "FK_db5b72c16227c97ca88734d5c2b" FOREIGN KEY ("userId") REFERENCES "user"("id") ON DELETE CASCADE ON UPDATE NO ACTION`);
+ await queryRunner.query(`ALTER TABLE "role_assignment" ADD CONSTRAINT "FK_f0de67fd09cd3cd0aabca79994d" FOREIGN KEY ("roleId") REFERENCES "role"("id") ON DELETE CASCADE ON UPDATE NO ACTION`);
+ }
+
+ async down(queryRunner) {
+ await queryRunner.query(`ALTER TABLE "role_assignment" DROP CONSTRAINT "FK_f0de67fd09cd3cd0aabca79994d"`);
+ await queryRunner.query(`ALTER TABLE "role_assignment" DROP CONSTRAINT "FK_db5b72c16227c97ca88734d5c2b"`);
+ await queryRunner.query(`ALTER TABLE "meta" DROP COLUMN "defaultRoleOverride"`);
+ await queryRunner.query(`ALTER TABLE "meta" ADD "localDriveCapacityMb" integer NOT NULL DEFAULT '1024'`);
+ await queryRunner.query(`ALTER TABLE "meta" ADD "disableGlobalTimeline" boolean NOT NULL DEFAULT false`);
+ await queryRunner.query(`ALTER TABLE "meta" ADD "disableLocalTimeline" boolean NOT NULL DEFAULT false`);
+ await queryRunner.query(`ALTER TABLE "user" ADD "driveCapacityOverrideMb" integer`);
+ await queryRunner.query(`ALTER TABLE "user" ADD "isModerator" boolean NOT NULL DEFAULT false`);
+ await queryRunner.query(`ALTER TABLE "user" RENAME COLUMN "isRoot" TO "isAdmin"`);
+ await queryRunner.query(`DROP INDEX "public"."IDX_0953deda7ce6e1448e935859e5"`);
+ await queryRunner.query(`DROP INDEX "public"."IDX_f0de67fd09cd3cd0aabca79994"`);
+ await queryRunner.query(`DROP INDEX "public"."IDX_db5b72c16227c97ca88734d5c2"`);
+ await queryRunner.query(`DROP TABLE "role_assignment"`);
+ await queryRunner.query(`DROP TABLE "role"`);
+ }
+}
diff --git a/packages/backend/migration/1673515526953-RoleColor.js b/packages/backend/migration/1673515526953-RoleColor.js
new file mode 100644
index 0000000000..343eedf346
--- /dev/null
+++ b/packages/backend/migration/1673515526953-RoleColor.js
@@ -0,0 +1,11 @@
+export class RoleColor1673515526953 {
+ name = 'RoleColor1673515526953'
+
+ async up(queryRunner) {
+ await queryRunner.query(`ALTER TABLE "role" ADD "color" character varying(256)`);
+ }
+
+ async down(queryRunner) {
+ await queryRunner.query(`ALTER TABLE "role" DROP COLUMN "color"`);
+ }
+}
diff --git a/packages/backend/migration/1673522856499-RoleIroiro.js b/packages/backend/migration/1673522856499-RoleIroiro.js
new file mode 100644
index 0000000000..a1e64d49fe
--- /dev/null
+++ b/packages/backend/migration/1673522856499-RoleIroiro.js
@@ -0,0 +1,13 @@
+export class RoleIroiro1673522856499 {
+ name = 'RoleIroiro1673522856499'
+
+ async up(queryRunner) {
+ await queryRunner.query(`ALTER TABLE "user" DROP COLUMN "isSilenced"`);
+ await queryRunner.query(`ALTER TABLE "role" ADD "canEditMembersByModerator" boolean NOT NULL DEFAULT false`);
+ }
+
+ async down(queryRunner) {
+ await queryRunner.query(`ALTER TABLE "role" DROP COLUMN "canEditMembersByModerator"`);
+ await queryRunner.query(`ALTER TABLE "user" ADD "isSilenced" boolean NOT NULL DEFAULT false`);
+ }
+}
diff --git a/packages/backend/migration/1673524604156-RoleLastUsedAt.js b/packages/backend/migration/1673524604156-RoleLastUsedAt.js
new file mode 100644
index 0000000000..786ef07f5e
--- /dev/null
+++ b/packages/backend/migration/1673524604156-RoleLastUsedAt.js
@@ -0,0 +1,13 @@
+export class RoleLastUsedAt1673524604156 {
+ name = 'RoleLastUsedAt1673524604156'
+
+ async up(queryRunner) {
+ await queryRunner.query(`ALTER TABLE "role" ADD "lastUsedAt" TIMESTAMP WITH TIME ZONE NOT NULL`);
+ await queryRunner.query(`COMMENT ON COLUMN "role"."lastUsedAt" IS 'The last used date of the Role.'`);
+ }
+
+ async down(queryRunner) {
+ await queryRunner.query(`COMMENT ON COLUMN "role"."lastUsedAt" IS 'The last used date of the Role.'`);
+ await queryRunner.query(`ALTER TABLE "role" DROP COLUMN "lastUsedAt"`);
+ }
+}
diff --git a/packages/backend/src/core/CoreModule.ts b/packages/backend/src/core/CoreModule.ts
index 2f17fa389a..0ae1ee32b2 100644
--- a/packages/backend/src/core/CoreModule.ts
+++ b/packages/backend/src/core/CoreModule.ts
@@ -35,6 +35,7 @@ import { PushNotificationService } from './PushNotificationService.js';
import { QueryService } from './QueryService.js';
import { ReactionService } from './ReactionService.js';
import { RelayService } from './RelayService.js';
+import { RoleService } from './RoleService.js';
import { S3Service } from './S3Service.js';
import { SignupService } from './SignupService.js';
import { TwoFactorAuthenticationService } from './TwoFactorAuthenticationService.js';
@@ -97,6 +98,7 @@ import { UserGroupInvitationEntityService } from './entities/UserGroupInvitation
import { UserListEntityService } from './entities/UserListEntityService.js';
import { FlashEntityService } from './entities/FlashEntityService.js';
import { FlashLikeEntityService } from './entities/FlashLikeEntityService.js';
+import { RoleEntityService } from './entities/RoleEntityService.js';
import { ApAudienceService } from './activitypub/ApAudienceService.js';
import { ApDbResolverService } from './activitypub/ApDbResolverService.js';
import { ApDeliverManagerService } from './activitypub/ApDeliverManagerService.js';
@@ -158,6 +160,7 @@ const $PushNotificationService: Provider = { provide: 'PushNotificationService',
const $QueryService: Provider = { provide: 'QueryService', useExisting: QueryService };
const $ReactionService: Provider = { provide: 'ReactionService', useExisting: ReactionService };
const $RelayService: Provider = { provide: 'RelayService', useExisting: RelayService };
+const $RoleService: Provider = { provide: 'RoleService', useExisting: RoleService };
const $S3Service: Provider = { provide: 'S3Service', useExisting: S3Service };
const $SignupService: Provider = { provide: 'SignupService', useExisting: SignupService };
const $TwoFactorAuthenticationService: Provider = { provide: 'TwoFactorAuthenticationService', useExisting: TwoFactorAuthenticationService };
@@ -220,6 +223,7 @@ const $UserGroupInvitationEntityService: Provider = { provide: 'UserGroupInvitat
const $UserListEntityService: Provider = { provide: 'UserListEntityService', useExisting: UserListEntityService };
const $FlashEntityService: Provider = { provide: 'FlashEntityService', useExisting: FlashEntityService };
const $FlashLikeEntityService: Provider = { provide: 'FlashLikeEntityService', useExisting: FlashLikeEntityService };
+const $RoleEntityService: Provider = { provide: 'RoleEntityService', useExisting: RoleEntityService };
const $ApAudienceService: Provider = { provide: 'ApAudienceService', useExisting: ApAudienceService };
const $ApDbResolverService: Provider = { provide: 'ApDbResolverService', useExisting: ApDbResolverService };
@@ -283,6 +287,7 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
QueryService,
ReactionService,
RelayService,
+ RoleService,
S3Service,
SignupService,
TwoFactorAuthenticationService,
@@ -344,6 +349,7 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
UserListEntityService,
FlashEntityService,
FlashLikeEntityService,
+ RoleEntityService,
ApAudienceService,
ApDbResolverService,
ApDeliverManagerService,
@@ -402,6 +408,7 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
$QueryService,
$ReactionService,
$RelayService,
+ $RoleService,
$S3Service,
$SignupService,
$TwoFactorAuthenticationService,
@@ -463,6 +470,7 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
$UserListEntityService,
$FlashEntityService,
$FlashLikeEntityService,
+ $RoleEntityService,
$ApAudienceService,
$ApDbResolverService,
$ApDeliverManagerService,
@@ -522,6 +530,7 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
QueryService,
ReactionService,
RelayService,
+ RoleService,
S3Service,
SignupService,
TwoFactorAuthenticationService,
@@ -582,6 +591,7 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
UserListEntityService,
FlashEntityService,
FlashLikeEntityService,
+ RoleEntityService,
ApAudienceService,
ApDbResolverService,
ApDeliverManagerService,
@@ -640,6 +650,7 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
$QueryService,
$ReactionService,
$RelayService,
+ $RoleService,
$S3Service,
$SignupService,
$TwoFactorAuthenticationService,
@@ -700,6 +711,7 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
$UserListEntityService,
$FlashEntityService,
$FlashLikeEntityService,
+ $RoleEntityService,
$ApAudienceService,
$ApDbResolverService,
$ApDeliverManagerService,
diff --git a/packages/backend/src/core/CreateSystemUserService.ts b/packages/backend/src/core/CreateSystemUserService.ts
index 1e753f65cc..8f887d90f9 100644
--- a/packages/backend/src/core/CreateSystemUserService.ts
+++ b/packages/backend/src/core/CreateSystemUserService.ts
@@ -53,7 +53,7 @@ export class CreateSystemUserService {
usernameLower: username.toLowerCase(),
host: null,
token: secret,
- isAdmin: false,
+ isRoot: false,
isLocked: true,
isExplorable: false,
isBot: true,
diff --git a/packages/backend/src/core/DeleteAccountService.ts b/packages/backend/src/core/DeleteAccountService.ts
index e42c738707..0ac12857c9 100644
--- a/packages/backend/src/core/DeleteAccountService.ts
+++ b/packages/backend/src/core/DeleteAccountService.ts
@@ -23,6 +23,9 @@ export class DeleteAccountService {
id: string;
host: string | null;
}): Promise<void> {
+ 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(e => {});
diff --git a/packages/backend/src/core/DriveService.ts b/packages/backend/src/core/DriveService.ts
index bbdb5fae83..5954abba91 100644
--- a/packages/backend/src/core/DriveService.ts
+++ b/packages/backend/src/core/DriveService.ts
@@ -32,11 +32,12 @@ import { DriveFileEntityService } from '@/core/entities/DriveFileEntityService.j
import { UserEntityService } from '@/core/entities/UserEntityService.js';
import { FileInfoService } from '@/core/FileInfoService.js';
import { bindThis } from '@/decorators.js';
+import { RoleService } from '@/core/RoleService.js';
import type S3 from 'aws-sdk/clients/s3.js';
type AddFileArgs = {
/** User who wish to add file */
- user: { id: User['id']; host: User['host']; driveCapacityOverrideMb: User['driveCapacityOverrideMb'] } | null;
+ user: { id: User['id']; host: User['host'] } | null;
/** File path */
path: string;
/** Name */
@@ -62,7 +63,7 @@ type AddFileArgs = {
type UploadFromUrlArgs = {
url: string;
- user: { id: User['id']; host: User['host']; driveCapacityOverrideMb: User['driveCapacityOverrideMb'] } | null;
+ user: { id: User['id']; host: User['host'] } | null;
folderId?: DriveFolder['id'] | null;
uri?: string | null;
sensitive?: boolean;
@@ -106,6 +107,7 @@ export class DriveService {
private videoProcessingService: VideoProcessingService,
private globalEventService: GlobalEventService,
private queueService: QueueService,
+ private roleService: RoleService,
private driveChart: DriveChart,
private perUserDriveChart: PerUserDriveChart,
private instanceChart: InstanceChart,
@@ -463,15 +465,16 @@ export class DriveService {
//#region Check drive usage
if (user && !isLink) {
const usage = await this.driveFileEntityService.calcDriveUsageOf(user);
- const u = await this.usersRepository.findOneBy({ id: user.id });
- const instance = await this.metaService.fetch();
- let driveCapacity = 1024 * 1024 * (this.userEntityService.isLocalUser(user) ? instance.localDriveCapacityMb : instance.remoteDriveCapacityMb);
-
- if (this.userEntityService.isLocalUser(user) && u?.driveCapacityOverrideMb != null) {
- driveCapacity = 1024 * 1024 * u.driveCapacityOverrideMb;
+ let driveCapacity: number;
+ if (this.userEntityService.isLocalUser(user)) {
+ const role = await this.roleService.getUserRoleOptions(user.id);
+ driveCapacity = 1024 * 1024 * role.driveCapacityMb;
this.registerLogger.debug('drive capacity override applied');
this.registerLogger.debug(`overrideCap: ${driveCapacity}bytes, usage: ${usage}bytes, u+s: ${usage + info.size}bytes`);
+ } else {
+ const instance = await this.metaService.fetch();
+ driveCapacity = 1024 * 1024 * instance.remoteDriveCapacityMb;
}
this.registerLogger.debug(`drive usage is ${usage} (max: ${driveCapacity})`);
diff --git a/packages/backend/src/core/NoteCreateService.ts b/packages/backend/src/core/NoteCreateService.ts
index 6038840406..1c2add5d64 100644
--- a/packages/backend/src/core/NoteCreateService.ts
+++ b/packages/backend/src/core/NoteCreateService.ts
@@ -42,6 +42,7 @@ import { NoteReadService } from '@/core/NoteReadService.js';
import { RemoteUserResolveService } from '@/core/RemoteUserResolveService.js';
import { bindThis } from '@/decorators.js';
import { DB_MAX_NOTE_TEXT_LENGTH } from '@/const.js';
+import { RoleService } from '@/core/RoleService.js';
const mutedWordsCache = new Cache<{ userId: UserProfile['userId']; mutedWords: UserProfile['mutedWords']; }[]>(1000 * 60 * 5);
@@ -186,6 +187,7 @@ export class NoteCreateService {
private remoteUserResolveService: RemoteUserResolveService,
private apDeliverManagerService: ApDeliverManagerService,
private apRendererService: ApRendererService,
+ private roleService: RoleService,
private notesChart: NotesChart,
private perUserNotesChart: PerUserNotesChart,
private activeUsersChart: ActiveUsersChart,
@@ -197,7 +199,6 @@ export class NoteCreateService {
id: User['id'];
username: User['username'];
host: User['host'];
- isSilenced: User['isSilenced'];
createdAt: User['createdAt'];
isBot: User['isBot'];
}, data: Option, silent = false): Promise<Note> {
@@ -224,9 +225,10 @@ export class NoteCreateService {
if (data.channel != null) data.visibleUsers = [];
if (data.channel != null) data.localOnly = true;
- // サイレンス
- if (user.isSilenced && data.visibility === 'public' && data.channel == null) {
- data.visibility = 'home';
+ if (data.visibility === 'public' && data.channel == null) {
+ if ((await this.roleService.getUserRoleOptions(user.id)).canPublicNote) {
+ data.visibility = 'home';
+ }
}
// Renote対象が「ホームまたは全体」以外の公開範囲ならreject
@@ -418,7 +420,6 @@ export class NoteCreateService {
id: User['id'];
username: User['username'];
host: User['host'];
- isSilenced: User['isSilenced'];
createdAt: User['createdAt'];
isBot: User['isBot'];
}, data: Option, silent: boolean, tags: string[], mentionedUsers: MinimumUser[]) {
diff --git a/packages/backend/src/core/RoleService.ts b/packages/backend/src/core/RoleService.ts
new file mode 100644
index 0000000000..6ce7f431ca
--- /dev/null
+++ b/packages/backend/src/core/RoleService.ts
@@ -0,0 +1,201 @@
+import { Inject, Injectable } from '@nestjs/common';
+import Redis from 'ioredis';
+import { In } from 'typeorm';
+import type { Role, RoleAssignment, RoleAssignmentsRepository, RolesRepository, UsersRepository } from '@/models/index.js';
+import { Cache } from '@/misc/cache.js';
+import type { CacheableLocalUser, CacheableUser, ILocalUser, User } from '@/models/entities/User.js';
+import { DI } from '@/di-symbols.js';
+import { bindThis } from '@/decorators.js';
+import { MetaService } from '@/core/MetaService.js';
+import type { OnApplicationShutdown } from '@nestjs/common';
+
+export type RoleOptions = {
+ gtlAvailable: boolean;
+ ltlAvailable: boolean;
+ canPublicNote: boolean;
+ driveCapacityMb: number;
+ antennaLimit: number;
+};
+
+export const DEFAULT_ROLE: RoleOptions = {
+ gtlAvailable: true,
+ ltlAvailable: true,
+ canPublicNote: true,
+ driveCapacityMb: 100,
+ antennaLimit: 5,
+};
+
+@Injectable()
+export class RoleService implements OnApplicationShutdown {
+ private rolesCache: Cache<Role[]>;
+ private roleAssignmentByUserIdCache: Cache<RoleAssignment[]>;
+
+ constructor(
+ @Inject(DI.redisSubscriber)
+ private redisSubscriber: Redis.Redis,
+
+ @Inject(DI.usersRepository)
+ private usersRepository: UsersRepository,
+
+ @Inject(DI.rolesRepository)
+ private rolesRepository: RolesRepository,
+
+ @Inject(DI.roleAssignmentsRepository)
+ private roleAssignmentsRepository: RoleAssignmentsRepository,
+
+ private metaService: MetaService,
+ ) {
+ //this.onMessage = this.onMessage.bind(this);
+
+ this.rolesCache = new Cache<Role[]>(Infinity);
+ this.roleAssignmentByUserIdCache = new Cache<RoleAssignment[]>(Infinity);
+
+ this.redisSubscriber.on('message', this.onMessage);
+ }
+
+ @bindThis
+ private async onMessage(_: string, data: string): Promise<void> {
+ const obj = JSON.parse(data);
+
+ if (obj.channel === 'internal') {
+ const { type, body } = obj.message;
+ switch (type) {
+ case 'roleCreated': {
+ const cached = this.rolesCache.get(null);
+ if (cached) {
+ body.createdAt = new Date(body.createdAt);
+ body.updatedAt = new Date(body.updatedAt);
+ body.lastUsedAt = new Date(body.lastUsedAt);
+ cached.push(body);
+ }
+ break;
+ }
+ case 'roleUpdated': {
+ const cached = this.rolesCache.get(null);
+ if (cached) {
+ const i = cached.findIndex(x => x.id === body.id);
+ if (i > -1) {
+ body.createdAt = new Date(body.createdAt);
+ body.updatedAt = new Date(body.updatedAt);
+ body.lastUsedAt = new Date(body.lastUsedAt);
+ cached[i] = body;
+ }
+ }
+ break;
+ }
+ case 'roleDeleted': {
+ const cached = this.rolesCache.get(null);
+ if (cached) {
+ this.rolesCache.set(null, cached.filter(x => x.id !== body.id));
+ }
+ break;
+ }
+ case 'userRoleAssigned': {
+ const cached = this.roleAssignmentByUserIdCache.get(body.userId);
+ if (cached) {
+ body.createdAt = new Date(body.createdAt);
+ cached.push(body);
+ }
+ break;
+ }
+ case 'userRoleUnassigned': {
+ const cached = this.roleAssignmentByUserIdCache.get(body.userId);
+ if (cached) {
+ this.roleAssignmentByUserIdCache.set(body.userId, cached.filter(x => x.id !== body.id));
+ }
+ break;
+ }
+ default:
+ break;
+ }
+ }
+ }
+
+ @bindThis
+ public async getUserRoles(userId: User['id']) {
+ const assigns = await this.roleAssignmentByUserIdCache.fetch(userId, () => this.roleAssignmentsRepository.findBy({ userId }));
+ const assignedRoleIds = assigns.map(x => x.roleId);
+ const roles = await this.rolesCache.fetch(null, () => this.rolesRepository.findBy({}));
+ return roles.filter(r => assignedRoleIds.includes(r.id));
+ }
+
+ @bindThis
+ public async getUserRoleOptions(userId: User['id'] | null): Promise<RoleOptions> {
+ const meta = await this.metaService.fetch();
+ const baseRoleOptions = { ...DEFAULT_ROLE, ...meta.defaultRoleOverride };
+
+ if (userId == null) return baseRoleOptions;
+
+ const roles = await this.getUserRoles(userId);
+
+ function getOptionValues(option: keyof RoleOptions) {
+ if (roles.length === 0) return [baseRoleOptions[option]];
+ return roles.map(role => (role.options[option] && (role.options[option].useDefault !== true)) ? role.options[option].value : baseRoleOptions[option]);
+ }
+
+ return {
+ gtlAvailable: getOptionValues('gtlAvailable').some(x => x === true),
+ ltlAvailable: getOptionValues('ltlAvailable').some(x => x === true),
+ canPublicNote: getOptionValues('canPublicNote').some(x => x === true),
+ driveCapacityMb: Math.max(...getOptionValues('driveCapacityMb')),
+ antennaLimit: Math.max(...getOptionValues('antennaLimit')),
+ };
+ }
+
+ @bindThis
+ public async isModerator(user: { id: User['id']; isRoot: User['isRoot'] } | null): Promise<boolean> {
+ if (user == null) return false;
+ return user.isRoot || (await this.getUserRoles(user.id)).some(r => r.isModerator || r.isAdministrator);
+ }
+
+ @bindThis
+ public async isAdministrator(user: { id: User['id']; isRoot: User['isRoot'] } | null): Promise<boolean> {
+ if (user == null) return false;
+ return user.isRoot || (await this.getUserRoles(user.id)).some(r => r.isAdministrator);
+ }
+
+ @bindThis
+ public async getModeratorIds(includeAdmins = true): Promise<User['id'][]> {
+ const roles = await this.rolesCache.fetch(null, () => this.rolesRepository.findBy({}));
+ const moderatorRoles = includeAdmins ? roles.filter(r => r.isModerator || r.isAdministrator) : roles.filter(r => r.isModerator);
+ const assigns = moderatorRoles.length > 0 ? await this.roleAssignmentsRepository.findBy({
+ roleId: In(moderatorRoles.map(r => r.id)),
+ }) : [];
+ // TODO: isRootなアカウントも含める
+ return assigns.map(a => a.userId);
+ }
+
+ @bindThis
+ public async getModerators(includeAdmins = true): Promise<User[]> {
+ const ids = await this.getModeratorIds(includeAdmins);
+ const users = ids.length > 0 ? await this.usersRepository.findBy({
+ id: In(ids),
+ }) : [];
+ return users;
+ }
+
+ @bindThis
+ public async getAdministratorIds(): Promise<User['id'][]> {
+ const roles = await this.rolesCache.fetch(null, () => this.rolesRepository.findBy({}));
+ const administratorRoles = roles.filter(r => r.isAdministrator);
+ const assigns = administratorRoles.length > 0 ? await this.roleAssignmentsRepository.findBy({
+ roleId: In(administratorRoles.map(r => r.id)),
+ }) : [];
+ // TODO: isRootなアカウントも含める
+ return assigns.map(a => a.userId);
+ }
+
+ @bindThis
+ public async getAdministrators(): Promise<User[]> {
+ const ids = await this.getAdministratorIds();
+ const users = ids.length > 0 ? await this.usersRepository.findBy({
+ id: In(ids),
+ }) : [];
+ return users;
+ }
+
+ @bindThis
+ public onApplicationShutdown(signal?: string | undefined) {
+ this.redisSubscriber.off('message', this.onMessage);
+ }
+}
diff --git a/packages/backend/src/core/SignupService.ts b/packages/backend/src/core/SignupService.ts
index 9cf203566d..90a7186909 100644
--- a/packages/backend/src/core/SignupService.ts
+++ b/packages/backend/src/core/SignupService.ts
@@ -11,10 +11,10 @@ import { IdService } from '@/core/IdService.js';
import { UserKeypair } from '@/models/entities/UserKeypair.js';
import { UsedUsername } from '@/models/entities/UsedUsername.js';
import generateUserToken from '@/misc/generate-native-user-token.js';
-import UsersChart from './chart/charts/users.js';
import { UserEntityService } from '@/core/entities/UserEntityService.js';
-import { UtilityService } from './UtilityService.js';
import { bindThis } from '@/decorators.js';
+import UsersChart from './chart/charts/users.js';
+import { UtilityService } from './UtilityService.js';
@Injectable()
export class SignupService {
@@ -112,7 +112,7 @@ export class SignupService {
usernameLower: username.toLowerCase(),
host: this.utilityService.toPunyNullable(host),
token: secret,
- isAdmin: (await this.usersRepository.countBy({
+ isRoot: (await this.usersRepository.countBy({
host: IsNull(),
})) === 0,
}));
diff --git a/packages/backend/src/core/UserCacheService.ts b/packages/backend/src/core/UserCacheService.ts
index 423c8993e3..3f735c0c53 100644
--- a/packages/backend/src/core/UserCacheService.ts
+++ b/packages/backend/src/core/UserCacheService.ts
@@ -5,8 +5,8 @@ import { Cache } from '@/misc/cache.js';
import type { CacheableLocalUser, CacheableUser, ILocalUser } from '@/models/entities/User.js';
import { DI } from '@/di-symbols.js';
import { UserEntityService } from '@/core/entities/UserEntityService.js';
-import type { OnApplicationShutdown } from '@nestjs/common';
import { bindThis } from '@/decorators.js';
+import type { OnApplicationShutdown } from '@nestjs/common';
@Injectable()
export class UserCacheService implements OnApplicationShutdown {
@@ -42,8 +42,6 @@ export class UserCacheService implements OnApplicationShutdown {
const { type, body } = obj.message;
switch (type) {
case 'userChangeSuspendedState':
- case 'userChangeSilencedState':
- case 'userChangeModeratorState':
case 'remoteUserUpdated': {
const user = await this.usersRepository.findOneByOrFail({ id: body.id });
this.userByIdCache.set(user.id, user);
diff --git a/packages/backend/src/core/entities/RoleEntityService.ts b/packages/backend/src/core/entities/RoleEntityService.ts
new file mode 100644
index 0000000000..22c4cdff81
--- /dev/null
+++ b/packages/backend/src/core/entities/RoleEntityService.ts
@@ -0,0 +1,80 @@
+import { Inject, Injectable } from '@nestjs/common';
+import { DI } from '@/di-symbols.js';
+import type { RoleAssignmentsRepository, RolesRepository } from '@/models/index.js';
+import { awaitAll } from '@/misc/prelude/await-all.js';
+import type { Packed } from '@/misc/schema.js';
+import type { User } from '@/models/entities/User.js';
+import type { Role } from '@/models/entities/Role.js';
+import { bindThis } from '@/decorators.js';
+import { DEFAULT_ROLE } from '@/core/RoleService.js';
+import { UserEntityService } from './UserEntityService.js';
+
+@Injectable()
+export class RoleEntityService {
+ constructor(
+ @Inject(DI.rolesRepository)
+ private rolesRepository: RolesRepository,
+
+ @Inject(DI.roleAssignmentsRepository)
+ private roleAssignmentsRepository: RoleAssignmentsRepository,
+
+ private userEntityService: UserEntityService,
+ ) {
+ }
+
+ @bindThis
+ public async pack(
+ src: Role['id'] | Role,
+ me?: { id: User['id'] } | null | undefined,
+ options?: {
+ detail?: boolean;
+ },
+ ) {
+ const opts = Object.assign({
+ detail: true,
+ }, options);
+
+ const role = typeof src === 'object' ? src : await this.rolesRepository.findOneByOrFail({ id: src });
+
+ const assigns = await this.roleAssignmentsRepository.findBy({
+ roleId: role.id,
+ });
+
+ const roleOptions = { ...role.options };
+ for (const [k, v] of Object.entries(DEFAULT_ROLE)) {
+ if (roleOptions[k] == null) roleOptions[k] = {
+ useDefault: true,
+ value: v,
+ };
+ }
+
+ return await awaitAll({
+ id: role.id,
+ createdAt: role.createdAt.toISOString(),
+ updatedAt: role.updatedAt.toISOString(),
+ name: role.name,
+ description: role.description,
+ color: role.color,
+ isPublic: role.isPublic,
+ isAdministrator: role.isAdministrator,
+ isModerator: role.isModerator,
+ canEditMembersByModerator: role.canEditMembersByModerator,
+ options: roleOptions,
+ ...(opts.detail ? {
+ users: this.userEntityService.packMany(assigns.map(x => x.userId), me),
+ } : {}),
+ });
+ }
+
+ @bindThis
+ public packMany(
+ roles: any[],
+ me: { id: User['id'] },
+ options?: {
+ detail?: boolean;
+ },
+ ) {
+ return Promise.all(roles.map(x => this.pack(x, me, options)));
+ }
+}
+
diff --git a/packages/backend/src/core/entities/UserEntityService.ts b/packages/backend/src/core/entities/UserEntityService.ts
index a123746220..9a90aec456 100644
--- a/packages/backend/src/core/entities/UserEntityService.ts
+++ b/packages/backend/src/core/entities/UserEntityService.ts
@@ -13,6 +13,8 @@ import type { Instance } from '@/models/entities/Instance.js';
import type { ILocalUser, IRemoteUser, User } from '@/models/entities/User.js';
import { birthdaySchema, descriptionSchema, localUsernameSchema, locationSchema, nameSchema, passwordSchema } from '@/models/entities/User.js';
import type { UsersRepository, UserSecurityKeysRepository, FollowingsRepository, FollowRequestsRepository, BlockingsRepository, MutingsRepository, DriveFilesRepository, NoteUnreadsRepository, ChannelFollowingsRepository, NotificationsRepository, UserNotePiningsRepository, UserProfilesRepository, InstancesRepository, AnnouncementReadsRepository, MessagingMessagesRepository, UserGroupJoiningsRepository, AnnouncementsRepository, AntennaNotesRepository, PagesRepository } from '@/models/index.js';
+import { bindThis } from '@/decorators.js';
+import { RoleService } from '@/core/RoleService.js';
import type { OnModuleInit } from '@nestjs/common';
import type { AntennaService } from '../AntennaService.js';
import type { CustomEmojiService } from '../CustomEmojiService.js';
@@ -41,7 +43,6 @@ function isRemoteUser<T extends { host: User['host'] }>(user: T): user is T & {
function isRemoteUser(user: User | { host: User['host'] }): boolean {
return !isLocalUser(user);
}
-import { bindThis } from '@/decorators.js';
@Injectable()
export class UserEntityService implements OnModuleInit {
@@ -50,6 +51,7 @@ export class UserEntityService implements OnModuleInit {
private pageEntityService: PageEntityService;
private customEmojiService: CustomEmojiService;
private antennaService: AntennaService;
+ private roleService: RoleService;
private userInstanceCache: Cache<Instance | null>;
constructor(
@@ -120,6 +122,7 @@ export class UserEntityService implements OnModuleInit {
//private pageEntityService: PageEntityService,
//private customEmojiService: CustomEmojiService,
//private antennaService: AntennaService,
+ //private roleService: RoleService,
) {
this.userInstanceCache = new Cache<Instance | null>(1000 * 60 * 60 * 3);
}
@@ -130,6 +133,7 @@ export class UserEntityService implements OnModuleInit {
this.pageEntityService = this.moduleRef.get('PageEntityService');
this.customEmojiService = this.moduleRef.get('CustomEmojiService');
this.antennaService = this.moduleRef.get('AntennaService');
+ this.roleService = this.moduleRef.get('RoleService');
}
//#region Validators
@@ -383,6 +387,9 @@ export class UserEntityService implements OnModuleInit {
(profile.ffVisibility === 'followers') && (relation && relation.isFollowing) ? user.followersCount :
null;
+ const isModerator = isMe && opts.detail ? this.roleService.isModerator(user) : null;
+ const isAdmin = isMe && opts.detail ? this.roleService.isAdministrator(user) : null;
+
const falsy = opts.detail ? false : undefined;
const packed = {
@@ -392,8 +399,6 @@ export class UserEntityService implements OnModuleInit {
host: user.host,
avatarUrl: this.getAvatarUrlSync(user),
avatarBlurhash: user.avatar?.blurhash ?? null,
- isAdmin: user.isAdmin ?? falsy,
- isModerator: user.isModerator ?? falsy,
isBot: user.isBot ?? falsy,
isCat: user.isCat ?? falsy,
instance: user.host ? this.userInstanceCache.fetch(user.host,
@@ -418,7 +423,7 @@ export class UserEntityService implements OnModuleInit {
bannerUrl: user.banner ? this.driveFileEntityService.getPublicUrl(user.banner, false) : null,
bannerBlurhash: user.banner?.blurhash ?? null,
isLocked: user.isLocked,
- isSilenced: user.isSilenced ?? falsy,
+ isSilenced: this.roleService.getUserRoleOptions(user.id).then(r => !r.canPublicNote),
isSuspended: user.isSuspended ?? falsy,
description: profile!.description,
location: profile!.location,
@@ -443,14 +448,13 @@ export class UserEntityService implements OnModuleInit {
userId: user.id,
}).then(result => result >= 1)
: false,
- ...(isMe || opts.includeSecrets ? {
- driveCapacityOverrideMb: user.driveCapacityOverrideMb,
- } : {}),
} : {}),
...(opts.detail && isMe ? {
avatarId: user.avatarId,
bannerId: user.bannerId,
+ isModerator: isModerator,
+ isAdmin: isAdmin,
injectFeaturedNote: profile!.injectFeaturedNote,
receiveAnnouncementEmail: profile!.receiveAnnouncementEmail,
alwaysMarkNsfw: profile!.alwaysMarkNsfw,
@@ -484,6 +488,7 @@ export class UserEntityService implements OnModuleInit {
} : {}),
...(opts.includeSecrets ? {
+ role: this.roleService.getUserRoleOptions(user.id),
email: profile!.email,
emailVerified: profile!.emailVerified,
securityKeysList: profile!.twoFactorEnabled
diff --git a/packages/backend/src/di-symbols.ts b/packages/backend/src/di-symbols.ts
index 9719d773ca..3fb0cd4dae 100644
--- a/packages/backend/src/di-symbols.ts
+++ b/packages/backend/src/di-symbols.ts
@@ -69,6 +69,8 @@ export const DI = {
adsRepository: Symbol('adsRepository'),
passwordResetRequestsRepository: Symbol('passwordResetRequestsRepository'),
retentionAggregationsRepository: Symbol('retentionAggregationsRepository'),
+ rolesRepository: Symbol('rolesRepository'),
+ roleAssignmentsRepository: Symbol('roleAssignmentsRepository'),
flashsRepository: Symbol('flashsRepository'),
flashLikesRepository: Symbol('flashLikesRepository'),
//#endregion
diff --git a/packages/backend/src/models/RepositoryModule.ts b/packages/backend/src/models/RepositoryModule.ts
index a5d5a63931..2a235bc6fc 100644
--- a/packages/backend/src/models/RepositoryModule.ts
+++ b/packages/backend/src/models/RepositoryModule.ts
@@ -1,6 +1,6 @@
import { Module } from '@nestjs/common';
import { DI } from '@/di-symbols.js';
-import { User, Note, Announcement, AnnouncementRead, App, NoteFavorite, NoteThreadMuting, NoteReaction, NoteUnread, Notification, Poll, PollVote, UserProfile, UserKeypair, UserPending, AttestationChallenge, UserSecurityKey, UserPublickey, UserList, UserListJoining, UserGroup, UserGroupJoining, UserGroupInvitation, UserNotePining, UserIp, UsedUsername, Following, FollowRequest, Instance, Emoji, DriveFile, DriveFolder, Meta, Muting, Blocking, SwSubscription, Hashtag, AbuseUserReport, RegistrationTicket, AuthSession, AccessToken, Signin, MessagingMessage, Page, PageLike, GalleryPost, GalleryLike, ModerationLog, Clip, ClipNote, Antenna, AntennaNote, PromoNote, PromoRead, Relay, MutedNote, Channel, ChannelFollowing, ChannelNotePining, RegistryItem, Webhook, Ad, PasswordResetRequest, RetentionAggregation, FlashLike, Flash } from './index.js';
+import { User, Note, Announcement, AnnouncementRead, App, NoteFavorite, NoteThreadMuting, NoteReaction, NoteUnread, Notification, Poll, PollVote, UserProfile, UserKeypair, UserPending, AttestationChallenge, UserSecurityKey, UserPublickey, UserList, UserListJoining, UserGroup, UserGroupJoining, UserGroupInvitation, UserNotePining, UserIp, UsedUsername, Following, FollowRequest, Instance, Emoji, DriveFile, DriveFolder, Meta, Muting, Blocking, SwSubscription, Hashtag, AbuseUserReport, RegistrationTicket, AuthSession, AccessToken, Signin, MessagingMessage, Page, PageLike, GalleryPost, GalleryLike, ModerationLog, Clip, ClipNote, Antenna, AntennaNote, PromoNote, PromoRead, Relay, MutedNote, Channel, ChannelFollowing, ChannelNotePining, RegistryItem, Webhook, Ad, PasswordResetRequest, RetentionAggregation, FlashLike, Flash, Role, RoleAssignment } from './index.js';
import type { DataSource } from 'typeorm';
import type { Provider } from '@nestjs/common';
@@ -400,6 +400,18 @@ const $flashLikesRepository: Provider = {
inject: [DI.db],
};
+const $rolesRepository: Provider = {
+ provide: DI.rolesRepository,
+ useFactory: (db: DataSource) => db.getRepository(Role),
+ inject: [DI.db],
+};
+
+const $roleAssignmentsRepository: Provider = {
+ provide: DI.roleAssignmentsRepository,
+ useFactory: (db: DataSource) => db.getRepository(RoleAssignment),
+ inject: [DI.db],
+};
+
@Module({
imports: [
],
@@ -468,6 +480,8 @@ const $flashLikesRepository: Provider = {
$adsRepository,
$passwordResetRequestsRepository,
$retentionAggregationsRepository,
+ $rolesRepository,
+ $roleAssignmentsRepository,
$flashsRepository,
$flashLikesRepository,
],
@@ -536,6 +550,8 @@ const $flashLikesRepository: Provider = {
$adsRepository,
$passwordResetRequestsRepository,
$retentionAggregationsRepository,
+ $rolesRepository,
+ $roleAssignmentsRepository,
$flashsRepository,
$flashLikesRepository,
],
diff --git a/packages/backend/src/models/entities/Meta.ts b/packages/backend/src/models/entities/Meta.ts
index fb25e370d2..0d65a8d17a 100644
--- a/packages/backend/src/models/entities/Meta.ts
+++ b/packages/backend/src/models/entities/Meta.ts
@@ -45,16 +45,6 @@ export class Meta {
@Column('boolean', {
default: false,
})
- public disableLocalTimeline: boolean;
-
- @Column('boolean', {
- default: false,
- })
- public disableGlobalTimeline: boolean;
-
- @Column('boolean', {
- default: false,
- })
public useStarForReactionFallback: boolean;
@Column('varchar', {
@@ -228,12 +218,6 @@ export class Meta {
public enableSensitiveMediaDetectionForVideos: boolean;
@Column('integer', {
- default: 1024,
- comment: 'Drive capacity of a local user (MB)',
- })
- public localDriveCapacityMb: number;
-
- @Column('integer', {
default: 32,
comment: 'Drive capacity of a remote user (MB)',
})
@@ -476,4 +460,9 @@ export class Meta {
default: true,
})
public enableActiveEmailValidation: boolean;
+
+ @Column('jsonb', {
+ default: { },
+ })
+ public defaultRoleOverride: Record<string, any>;
}
diff --git a/packages/backend/src/models/entities/Role.ts b/packages/backend/src/models/entities/Role.ts
new file mode 100644
index 0000000000..34dbc2ce41
--- /dev/null
+++ b/packages/backend/src/models/entities/Role.ts
@@ -0,0 +1,66 @@
+import { Entity, Index, JoinColumn, Column, PrimaryColumn, ManyToOne } from 'typeorm';
+import { id } from '../id.js';
+
+@Entity()
+export class Role {
+ @PrimaryColumn(id())
+ public id: string;
+
+ @Column('timestamp with time zone', {
+ comment: 'The created date of the Role.',
+ })
+ public createdAt: Date;
+
+ @Column('timestamp with time zone', {
+ comment: 'The updated date of the Role.',
+ })
+ public updatedAt: Date;
+
+ @Column('timestamp with time zone', {
+ comment: 'The last used date of the Role.',
+ })
+ public lastUsedAt: Date;
+
+ @Column('varchar', {
+ length: 256,
+ })
+ public name: string;
+
+ @Column('varchar', {
+ length: 1024,
+ })
+ public description: string;
+
+ @Column('varchar', {
+ length: 256, nullable: true,
+ })
+ public color: string | null;
+
+ @Column('boolean', {
+ default: false,
+ })
+ public isPublic: boolean;
+
+ @Column('boolean', {
+ default: false,
+ })
+ public isModerator: boolean;
+
+ @Column('boolean', {
+ default: false,
+ })
+ public isAdministrator: boolean;
+
+ @Column('boolean', {
+ default: false,
+ })
+ public canEditMembersByModerator: boolean;
+
+ @Column('jsonb', {
+ default: { },
+ })
+ public options: Record<string, {
+ useDefault: boolean;
+ value: any;
+ }>;
+}
diff --git a/packages/backend/src/models/entities/RoleAssignment.ts b/packages/backend/src/models/entities/RoleAssignment.ts
new file mode 100644
index 0000000000..e86f2a8999
--- /dev/null
+++ b/packages/backend/src/models/entities/RoleAssignment.ts
@@ -0,0 +1,42 @@
+import { PrimaryColumn, Entity, Index, JoinColumn, Column, ManyToOne } from 'typeorm';
+import { id } from '../id.js';
+import { Role } from './Role.js';
+import { User } from './User.js';
+
+@Entity()
+@Index(['userId', 'roleId'], { unique: true })
+export class RoleAssignment {
+ @PrimaryColumn(id())
+ public id: string;
+
+ @Column('timestamp with time zone', {
+ comment: 'The created date of the RoleAssignment.',
+ })
+ public createdAt: Date;
+
+ @Index()
+ @Column({
+ ...id(),
+ comment: 'The user ID.',
+ })
+ public userId: User['id'];
+
+ @ManyToOne(type => User, {
+ onDelete: 'CASCADE',
+ })
+ @JoinColumn()
+ public user: User | null;
+
+ @Index()
+ @Column({
+ ...id(),
+ comment: 'The role ID.',
+ })
+ public roleId: Role['id'];
+
+ @ManyToOne(type => Role, {
+ onDelete: 'CASCADE',
+ })
+ @JoinColumn()
+ public role: Role | null;
+}
diff --git a/packages/backend/src/models/entities/User.ts b/packages/backend/src/models/entities/User.ts
index 73736f0150..8bd5c9700d 100644
--- a/packages/backend/src/models/entities/User.ts
+++ b/packages/backend/src/models/entities/User.ts
@@ -114,12 +114,6 @@ export class User {
@Column('boolean', {
default: false,
- comment: 'Whether the User is silenced.',
- })
- public isSilenced: boolean;
-
- @Column('boolean', {
- default: false,
comment: 'Whether the User is locked.',
})
public isLocked: boolean;
@@ -138,15 +132,9 @@ export class User {
@Column('boolean', {
default: false,
- comment: 'Whether the User is the admin.',
- })
- public isAdmin: boolean;
-
- @Column('boolean', {
- default: false,
- comment: 'Whether the User is a moderator.',
+ comment: 'Whether the User is the root.',
})
- public isModerator: boolean;
+ public isRoot: boolean;
@Index()
@Column('boolean', {
@@ -218,12 +206,6 @@ export class User {
})
public token: string | null;
- @Column('integer', {
- nullable: true,
- comment: 'Overrides user drive capacity limit',
- })
- public driveCapacityOverrideMb: number | null;
-
constructor(data: Partial<User>) {
if (data == null) return;
diff --git a/packages/backend/src/models/index.ts b/packages/backend/src/models/index.ts
index b132475747..50697597ad 100644
--- a/packages/backend/src/models/index.ts
+++ b/packages/backend/src/models/index.ts
@@ -62,6 +62,8 @@ import { UserSecurityKey } from '@/models/entities/UserSecurityKey.js';
import { Webhook } from '@/models/entities/Webhook.js';
import { Channel } from '@/models/entities/Channel.js';
import { RetentionAggregation } from '@/models/entities/RetentionAggregation.js';
+import { Role } from '@/models/entities/Role.js';
+import { RoleAssignment } from '@/models/entities/RoleAssignment.js';
import { Flash } from '@/models/entities/Flash.js';
import { FlashLike } from '@/models/entities/FlashLike.js';
import type { Repository } from 'typeorm';
@@ -131,6 +133,8 @@ export {
Webhook,
Channel,
RetentionAggregation,
+ Role,
+ RoleAssignment,
Flash,
FlashLike,
};
@@ -199,5 +203,7 @@ export type UserSecurityKeysRepository = Repository<UserSecurityKey>;
export type WebhooksRepository = Repository<Webhook>;
export type ChannelsRepository = Repository<Channel>;
export type RetentionAggregationsRepository = Repository<RetentionAggregation>;
+export type RolesRepository = Repository<Role>;
+export type RoleAssignmentsRepository = Repository<RoleAssignment>;
export type FlashsRepository = Repository<Flash>;
export type FlashLikesRepository = Repository<FlashLike>;
diff --git a/packages/backend/src/postgre.ts b/packages/backend/src/postgre.ts
index 4f6b157d80..c55cb78a6a 100644
--- a/packages/backend/src/postgre.ts
+++ b/packages/backend/src/postgre.ts
@@ -70,6 +70,8 @@ import { UserSecurityKey } from '@/models/entities/UserSecurityKey.js';
import { Webhook } from '@/models/entities/Webhook.js';
import { Channel } from '@/models/entities/Channel.js';
import { RetentionAggregation } from '@/models/entities/RetentionAggregation.js';
+import { Role } from '@/models/entities/Role.js';
+import { RoleAssignment } from '@/models/entities/RoleAssignment.js';
import { Flash } from '@/models/entities/Flash.js';
import { FlashLike } from '@/models/entities/FlashLike.js';
@@ -186,6 +188,8 @@ export const entities = [
Webhook,
UserIp,
RetentionAggregation,
+ Role,
+ RoleAssignment,
Flash,
FlashLike,
...charts,
diff --git a/packages/backend/src/server/NodeinfoServerService.ts b/packages/backend/src/server/NodeinfoServerService.ts
index c4236c8752..19380d13a4 100644
--- a/packages/backend/src/server/NodeinfoServerService.ts
+++ b/packages/backend/src/server/NodeinfoServerService.ts
@@ -10,6 +10,7 @@ import { UserEntityService } from '@/core/entities/UserEntityService.js';
import { bindThis } from '@/decorators.js';
import NotesChart from '@/core/chart/charts/notes.js';
import UsersChart from '@/core/chart/charts/users.js';
+import { DEFAULT_ROLE } from '@/core/RoleService.js';
import type { FastifyInstance, FastifyPluginOptions } from 'fastify';
const nodeinfo2_1path = '/nodeinfo/2.1';
@@ -73,6 +74,8 @@ export class NodeinfoServerService {
const proxyAccount = meta.proxyAccountId ? await this.userEntityService.pack(meta.proxyAccountId).catch(() => null) : null;
+ const baseRoleOptions = { ...DEFAULT_ROLE, ...meta.defaultRoleOverride };
+
return {
software: {
name: 'misskey',
@@ -102,8 +105,8 @@ export class NodeinfoServerService {
repositoryUrl: meta.repositoryUrl,
feedbackUrl: meta.feedbackUrl,
disableRegistration: meta.disableRegistration,
- disableLocalTimeline: meta.disableLocalTimeline,
- disableGlobalTimeline: meta.disableGlobalTimeline,
+ disableLocalTimeline: !baseRoleOptions.ltlAvailable,
+ disableGlobalTimeline: !baseRoleOptions.gtlAvailable,
emailRequiredForSignup: meta.emailRequiredForSignup,
enableHcaptcha: meta.enableHcaptcha,
enableRecaptcha: meta.enableRecaptcha,
diff --git a/packages/backend/src/server/api/ApiCallService.ts b/packages/backend/src/server/api/ApiCallService.ts
index 68f43c7dfc..415fbf08dd 100644
--- a/packages/backend/src/server/api/ApiCallService.ts
+++ b/packages/backend/src/server/api/ApiCallService.ts
@@ -12,6 +12,7 @@ import type { UserIpsRepository } from '@/models/index.js';
import { MetaService } from '@/core/MetaService.js';
import { createTemp } from '@/misc/create-temp.js';
import { bindThis } from '@/decorators.js';
+import { RoleService } from '@/core/RoleService.js';
import { ApiError } from './error.js';
import { RateLimiterService } from './RateLimiterService.js';
import { ApiLoggerService } from './ApiLoggerService.js';
@@ -41,6 +42,7 @@ export class ApiCallService implements OnApplicationShutdown {
private metaService: MetaService,
private authenticateService: AuthenticateService,
private rateLimiterService: RateLimiterService,
+ private roleService: RoleService,
private apiLoggerService: ApiLoggerService,
) {
this.logger = this.apiLoggerService.logger;
@@ -202,7 +204,6 @@ export class ApiCallService implements OnApplicationShutdown {
request: FastifyRequest<{ Body: Record<string, unknown> | undefined, Querystring: Record<string, unknown> }>,
) {
const isSecure = user != null && token == null;
- const isModerator = user != null && (user.isModerator || user.isAdmin);
if (ep.meta.secure && !isSecure) {
throw new ApiError(accessDenied);
@@ -234,30 +235,40 @@ export class ApiCallService implements OnApplicationShutdown {
});
}
- if (ep.meta.requireCredential && user == null) {
- throw new ApiError({
- message: 'Credential required.',
- code: 'CREDENTIAL_REQUIRED',
- id: '1384574d-a912-4b81-8601-c7b1c4085df1',
- httpStatusCode: 401,
- });
- }
-
- if (ep.meta.requireCredential && user!.isSuspended) {
- throw new ApiError({
- message: 'Your account has been suspended.',
- code: 'YOUR_ACCOUNT_SUSPENDED',
- id: 'a8c724b3-6e9c-4b46-b1a8-bc3ed6258370',
- httpStatusCode: 403,
- });
- }
-
- if (ep.meta.requireAdmin && !user!.isAdmin) {
- throw new ApiError(accessDenied, { reason: 'You are not the admin.' });
+ if (ep.meta.requireCredential || ep.meta.requireModerator || ep.meta.requireAdmin) {
+ if (user == null) {
+ throw new ApiError({
+ message: 'Credential required.',
+ code: 'CREDENTIAL_REQUIRED',
+ id: '1384574d-a912-4b81-8601-c7b1c4085df1',
+ httpStatusCode: 401,
+ });
+ } else if (user!.isSuspended) {
+ throw new ApiError({
+ message: 'Your account has been suspended.',
+ code: 'YOUR_ACCOUNT_SUSPENDED',
+ id: 'a8c724b3-6e9c-4b46-b1a8-bc3ed6258370',
+ httpStatusCode: 403,
+ });
+ }
}
- if (ep.meta.requireModerator && !isModerator) {
- throw new ApiError(accessDenied, { reason: 'You are not a moderator.' });
+ if ((ep.meta.requireModerator || ep.meta.requireAdmin) && !user!.isRoot) {
+ const myRoles = await this.roleService.getUserRoles(user!.id);
+ if (ep.meta.requireModerator && !myRoles.some(r => r.isModerator || r.isAdministrator)) {
+ throw new ApiError({
+ message: 'You are not assigned to a moderator role.',
+ code: 'ROLE_PERMISSION_DENIED',
+ id: 'd33d5333-db36-423d-a8f9-1a2b9549da41',
+ });
+ }
+ if (ep.meta.requireAdmin && !myRoles.some(r => r.isAdministrator)) {
+ throw new ApiError({
+ message: 'You are not assigned to an administrator role.',
+ code: 'ROLE_PERMISSION_DENIED',
+ id: 'c3d38592-54c0-429d-be96-5636b0431a61',
+ });
+ }
}
if (token && ep.meta.kind && !token.permission.some(p => p === ep.meta.kind)) {
diff --git a/packages/backend/src/server/api/EndpointsModule.ts b/packages/backend/src/server/api/EndpointsModule.ts
index ab9349966d..c226c4e93c 100644
--- a/packages/backend/src/server/api/EndpointsModule.ts
+++ b/packages/backend/src/server/api/EndpointsModule.ts
@@ -38,8 +38,6 @@ import * as ep___admin_getIndexStats from './endpoints/admin/get-index-stats.js'
import * as ep___admin_getTableStats from './endpoints/admin/get-table-stats.js';
import * as ep___admin_getUserIps from './endpoints/admin/get-user-ips.js';
import * as ep___admin_invite from './endpoints/admin/invite.js';
-import * as ep___admin_moderators_add from './endpoints/admin/moderators/add.js';
-import * as ep___admin_moderators_remove from './endpoints/admin/moderators/remove.js';
import * as ep___admin_promo_create from './endpoints/admin/promo/create.js';
import * as ep___admin_queue_clear from './endpoints/admin/queue/clear.js';
import * as ep___admin_queue_deliverDelayed from './endpoints/admin/queue/deliver-delayed.js';
@@ -55,13 +53,19 @@ import * as ep___admin_serverInfo from './endpoints/admin/server-info.js';
import * as ep___admin_showModerationLogs from './endpoints/admin/show-moderation-logs.js';
import * as ep___admin_showUser from './endpoints/admin/show-user.js';
import * as ep___admin_showUsers from './endpoints/admin/show-users.js';
-import * as ep___admin_silenceUser from './endpoints/admin/silence-user.js';
import * as ep___admin_suspendUser from './endpoints/admin/suspend-user.js';
-import * as ep___admin_unsilenceUser from './endpoints/admin/unsilence-user.js';
import * as ep___admin_unsuspendUser from './endpoints/admin/unsuspend-user.js';
import * as ep___admin_updateMeta from './endpoints/admin/update-meta.js';
import * as ep___admin_deleteAccount from './endpoints/admin/delete-account.js';
import * as ep___admin_updateUserNote from './endpoints/admin/update-user-note.js';
+import * as ep___admin_roles_create from './endpoints/admin/roles/create.js';
+import * as ep___admin_roles_delete from './endpoints/admin/roles/delete.js';
+import * as ep___admin_roles_list from './endpoints/admin/roles/list.js';
+import * as ep___admin_roles_show from './endpoints/admin/roles/show.js';
+import * as ep___admin_roles_update from './endpoints/admin/roles/update.js';
+import * as ep___admin_roles_assign from './endpoints/admin/roles/assign.js';
+import * as ep___admin_roles_unassign from './endpoints/admin/roles/unassign.js';
+import * as ep___admin_roles_updateDefaultRoleOverride from './endpoints/admin/roles/update-default-role-override.js';
import * as ep___announcements from './endpoints/announcements.js';
import * as ep___antennas_create from './endpoints/antennas/create.js';
import * as ep___antennas_delete from './endpoints/antennas/delete.js';
@@ -326,7 +330,6 @@ import * as ep___users_search from './endpoints/users/search.js';
import * as ep___users_show from './endpoints/users/show.js';
import * as ep___users_stats from './endpoints/users/stats.js';
import * as ep___fetchRss from './endpoints/fetch-rss.js';
-import * as ep___admin_driveCapOverride from './endpoints/admin/drive-capacity-override.js';
import * as ep___retention from './endpoints/retention.js';
import { GetterService } from './GetterService.js';
import { ApiLoggerService } from './ApiLoggerService.js';
@@ -369,8 +372,6 @@ const $admin_getIndexStats: Provider = { provide: 'ep:admin/get-index-stats', us
const $admin_getTableStats: Provider = { provide: 'ep:admin/get-table-stats', useClass: ep___admin_getTableStats.default };
const $admin_getUserIps: Provider = { provide: 'ep:admin/get-user-ips', useClass: ep___admin_getUserIps.default };
const $admin_invite: Provider = { provide: 'ep:admin/invite', useClass: ep___admin_invite.default };
-const $admin_moderators_add: Provider = { provide: 'ep:admin/moderators/add', useClass: ep___admin_moderators_add.default };
-const $admin_moderators_remove: Provider = { provide: 'ep:admin/moderators/remove', useClass: ep___admin_moderators_remove.default };
const $admin_promo_create: Provider = { provide: 'ep:admin/promo/create', useClass: ep___admin_promo_create.default };
const $admin_queue_clear: Provider = { provide: 'ep:admin/queue/clear', useClass: ep___admin_queue_clear.default };
const $admin_queue_deliverDelayed: Provider = { provide: 'ep:admin/queue/deliver-delayed', useClass: ep___admin_queue_deliverDelayed.default };
@@ -386,13 +387,19 @@ const $admin_serverInfo: Provider = { provide: 'ep:admin/server-info', useClass:
const $admin_showModerationLogs: Provider = { provide: 'ep:admin/show-moderation-logs', useClass: ep___admin_showModerationLogs.default };
const $admin_showUser: Provider = { provide: 'ep:admin/show-user', useClass: ep___admin_showUser.default };
const $admin_showUsers: Provider = { provide: 'ep:admin/show-users', useClass: ep___admin_showUsers.default };
-const $admin_silenceUser: Provider = { provide: 'ep:admin/silence-user', useClass: ep___admin_silenceUser.default };
const $admin_suspendUser: Provider = { provide: 'ep:admin/suspend-user', useClass: ep___admin_suspendUser.default };
-const $admin_unsilenceUser: Provider = { provide: 'ep:admin/unsilence-user', useClass: ep___admin_unsilenceUser.default };
const $admin_unsuspendUser: Provider = { provide: 'ep:admin/unsuspend-user', useClass: ep___admin_unsuspendUser.default };
const $admin_updateMeta: Provider = { provide: 'ep:admin/update-meta', useClass: ep___admin_updateMeta.default };
const $admin_deleteAccount: Provider = { provide: 'ep:admin/delete-account', useClass: ep___admin_deleteAccount.default };
const $admin_updateUserNote: Provider = { provide: 'ep:admin/update-user-note', useClass: ep___admin_updateUserNote.default };
+const $admin_roles_create: Provider = { provide: 'ep:admin/roles/create', useClass: ep___admin_roles_create.default };
+const $admin_roles_delete: Provider = { provide: 'ep:admin/roles/delete', useClass: ep___admin_roles_delete.default };
+const $admin_roles_list: Provider = { provide: 'ep:admin/roles/list', useClass: ep___admin_roles_list.default };
+const $admin_roles_show: Provider = { provide: 'ep:admin/roles/show', useClass: ep___admin_roles_show.default };
+const $admin_roles_update: Provider = { provide: 'ep:admin/roles/update', useClass: ep___admin_roles_update.default };
+const $admin_roles_assign: Provider = { provide: 'ep:admin/roles/assign', useClass: ep___admin_roles_assign.default };
+const $admin_roles_unassign: Provider = { provide: 'ep:admin/roles/unassign', useClass: ep___admin_roles_unassign.default };
+const $admin_roles_updateDefaultRoleOverride: Provider = { provide: 'ep:admin/roles/update-default-role-override', useClass: ep___admin_roles_updateDefaultRoleOverride.default };
const $announcements: Provider = { provide: 'ep:announcements', useClass: ep___announcements.default };
const $antennas_create: Provider = { provide: 'ep:antennas/create', useClass: ep___antennas_create.default };
const $antennas_delete: Provider = { provide: 'ep:antennas/delete', useClass: ep___antennas_delete.default };
@@ -656,7 +663,6 @@ const $users_searchByUsernameAndHost: Provider = { provide: 'ep:users/search-by-
const $users_search: Provider = { provide: 'ep:users/search', useClass: ep___users_search.default };
const $users_show: Provider = { provide: 'ep:users/show', useClass: ep___users_show.default };
const $users_stats: Provider = { provide: 'ep:users/stats', useClass: ep___users_stats.default };
-const $admin_driveCapOverride: Provider = { provide: 'ep:admin/drive-capacity-override', useClass: ep___admin_driveCapOverride.default };
const $fetchRss: Provider = { provide: 'ep:fetch-rss', useClass: ep___fetchRss.default };
const $retention: Provider = { provide: 'ep:retention', useClass: ep___retention.default };
@@ -704,8 +710,6 @@ const $retention: Provider = { provide: 'ep:retention', useClass: ep___retention
$admin_getTableStats,
$admin_getUserIps,
$admin_invite,
- $admin_moderators_add,
- $admin_moderators_remove,
$admin_promo_create,
$admin_queue_clear,
$admin_queue_deliverDelayed,
@@ -721,13 +725,19 @@ const $retention: Provider = { provide: 'ep:retention', useClass: ep___retention
$admin_showModerationLogs,
$admin_showUser,
$admin_showUsers,
- $admin_silenceUser,
$admin_suspendUser,
- $admin_unsilenceUser,
$admin_unsuspendUser,
$admin_updateMeta,
$admin_deleteAccount,
$admin_updateUserNote,
+ $admin_roles_create,
+ $admin_roles_delete,
+ $admin_roles_list,
+ $admin_roles_show,
+ $admin_roles_update,
+ $admin_roles_assign,
+ $admin_roles_unassign,
+ $admin_roles_updateDefaultRoleOverride,
$announcements,
$antennas_create,
$antennas_delete,
@@ -991,7 +1001,6 @@ const $retention: Provider = { provide: 'ep:retention', useClass: ep___retention
$users_search,
$users_show,
$users_stats,
- $admin_driveCapOverride,
$fetchRss,
$retention,
],
@@ -1033,8 +1042,6 @@ const $retention: Provider = { provide: 'ep:retention', useClass: ep___retention
$admin_getTableStats,
$admin_getUserIps,
$admin_invite,
- $admin_moderators_add,
- $admin_moderators_remove,
$admin_promo_create,
$admin_queue_clear,
$admin_queue_deliverDelayed,
@@ -1050,13 +1057,19 @@ const $retention: Provider = { provide: 'ep:retention', useClass: ep___retention
$admin_showModerationLogs,
$admin_showUser,
$admin_showUsers,
- $admin_silenceUser,
$admin_suspendUser,
- $admin_unsilenceUser,
$admin_unsuspendUser,
$admin_updateMeta,
$admin_deleteAccount,
$admin_updateUserNote,
+ $admin_roles_create,
+ $admin_roles_delete,
+ $admin_roles_list,
+ $admin_roles_show,
+ $admin_roles_update,
+ $admin_roles_assign,
+ $admin_roles_unassign,
+ $admin_roles_updateDefaultRoleOverride,
$announcements,
$antennas_create,
$antennas_delete,
@@ -1318,7 +1331,6 @@ const $retention: Provider = { provide: 'ep:retention', useClass: ep___retention
$users_search,
$users_show,
$users_stats,
- $admin_driveCapOverride,
$fetchRss,
$retention,
],
diff --git a/packages/backend/src/server/api/endpoints.ts b/packages/backend/src/server/api/endpoints.ts
index f9749ad660..1df3240e41 100644
--- a/packages/backend/src/server/api/endpoints.ts
+++ b/packages/backend/src/server/api/endpoints.ts
@@ -37,8 +37,6 @@ import * as ep___admin_getIndexStats from './endpoints/admin/get-index-stats.js'
import * as ep___admin_getTableStats from './endpoints/admin/get-table-stats.js';
import * as ep___admin_getUserIps from './endpoints/admin/get-user-ips.js';
import * as ep___admin_invite from './endpoints/admin/invite.js';
-import * as ep___admin_moderators_add from './endpoints/admin/moderators/add.js';
-import * as ep___admin_moderators_remove from './endpoints/admin/moderators/remove.js';
import * as ep___admin_promo_create from './endpoints/admin/promo/create.js';
import * as ep___admin_queue_clear from './endpoints/admin/queue/clear.js';
import * as ep___admin_queue_deliverDelayed from './endpoints/admin/queue/deliver-delayed.js';
@@ -54,13 +52,19 @@ import * as ep___admin_serverInfo from './endpoints/admin/server-info.js';
import * as ep___admin_showModerationLogs from './endpoints/admin/show-moderation-logs.js';
import * as ep___admin_showUser from './endpoints/admin/show-user.js';
import * as ep___admin_showUsers from './endpoints/admin/show-users.js';
-import * as ep___admin_silenceUser from './endpoints/admin/silence-user.js';
import * as ep___admin_suspendUser from './endpoints/admin/suspend-user.js';
-import * as ep___admin_unsilenceUser from './endpoints/admin/unsilence-user.js';
import * as ep___admin_unsuspendUser from './endpoints/admin/unsuspend-user.js';
import * as ep___admin_updateMeta from './endpoints/admin/update-meta.js';
import * as ep___admin_deleteAccount from './endpoints/admin/delete-account.js';
import * as ep___admin_updateUserNote from './endpoints/admin/update-user-note.js';
+import * as ep___admin_roles_create from './endpoints/admin/roles/create.js';
+import * as ep___admin_roles_delete from './endpoints/admin/roles/delete.js';
+import * as ep___admin_roles_list from './endpoints/admin/roles/list.js';
+import * as ep___admin_roles_show from './endpoints/admin/roles/show.js';
+import * as ep___admin_roles_update from './endpoints/admin/roles/update.js';
+import * as ep___admin_roles_assign from './endpoints/admin/roles/assign.js';
+import * as ep___admin_roles_unassign from './endpoints/admin/roles/unassign.js';
+import * as ep___admin_roles_updateDefaultRoleOverride from './endpoints/admin/roles/update-default-role-override.js';
import * as ep___announcements from './endpoints/announcements.js';
import * as ep___antennas_create from './endpoints/antennas/create.js';
import * as ep___antennas_delete from './endpoints/antennas/delete.js';
@@ -325,7 +329,6 @@ import * as ep___users_search from './endpoints/users/search.js';
import * as ep___users_show from './endpoints/users/show.js';
import * as ep___users_stats from './endpoints/users/stats.js';
import * as ep___fetchRss from './endpoints/fetch-rss.js';
-import * as ep___admin_driveCapOverride from './endpoints/admin/drive-capacity-override.js';
import * as ep___retention from './endpoints/retention.js';
const eps = [
@@ -366,8 +369,6 @@ const eps = [
['admin/get-table-stats', ep___admin_getTableStats],
['admin/get-user-ips', ep___admin_getUserIps],
['admin/invite', ep___admin_invite],
- ['admin/moderators/add', ep___admin_moderators_add],
- ['admin/moderators/remove', ep___admin_moderators_remove],
['admin/promo/create', ep___admin_promo_create],
['admin/queue/clear', ep___admin_queue_clear],
['admin/queue/deliver-delayed', ep___admin_queue_deliverDelayed],
@@ -383,13 +384,19 @@ const eps = [
['admin/show-moderation-logs', ep___admin_showModerationLogs],
['admin/show-user', ep___admin_showUser],
['admin/show-users', ep___admin_showUsers],
- ['admin/silence-user', ep___admin_silenceUser],
['admin/suspend-user', ep___admin_suspendUser],
- ['admin/unsilence-user', ep___admin_unsilenceUser],
['admin/unsuspend-user', ep___admin_unsuspendUser],
['admin/update-meta', ep___admin_updateMeta],
['admin/delete-account', ep___admin_deleteAccount],
['admin/update-user-note', ep___admin_updateUserNote],
+ ['admin/roles/create', ep___admin_roles_create],
+ ['admin/roles/delete', ep___admin_roles_delete],
+ ['admin/roles/list', ep___admin_roles_list],
+ ['admin/roles/show', ep___admin_roles_show],
+ ['admin/roles/update', ep___admin_roles_update],
+ ['admin/roles/assign', ep___admin_roles_assign],
+ ['admin/roles/unassign', ep___admin_roles_unassign],
+ ['admin/roles/update-default-role-override', ep___admin_roles_updateDefaultRoleOverride],
['announcements', ep___announcements],
['antennas/create', ep___antennas_create],
['antennas/delete', ep___antennas_delete],
@@ -653,7 +660,6 @@ const eps = [
['users/search', ep___users_search],
['users/show', ep___users_show],
['users/stats', ep___users_stats],
- ['admin/drive-capacity-override', ep___admin_driveCapOverride],
['fetch-rss', ep___fetchRss],
['retention', ep___retention],
];
@@ -680,14 +686,14 @@ export interface IEndpointMeta {
readonly requireCredential?: boolean;
/**
- * 管理者のみ使えるエンドポイントか否か
+ * isModeratorなロールを必要とするか
*/
- readonly requireAdmin?: boolean;
+ readonly requireModerator?: boolean;
/**
- * 管理者またはモデレーターのみ使えるエンドポイントか否か
+ * isAdministratorなロールを必要とするか
*/
- readonly requireModerator?: boolean;
+ readonly requireAdmin?: boolean;
/**
* エンドポイントのリミテーションに関するやつ
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 c76ece9e05..bac8ae16e5 100644
--- a/packages/backend/src/server/api/endpoints/admin/accounts/create.ts
+++ b/packages/backend/src/server/api/endpoints/admin/accounts/create.ts
@@ -47,7 +47,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
const noUsers = (await this.usersRepository.countBy({
host: IsNull(),
})) === 0;
- if (!noUsers && !me?.isAdmin) throw new Error('access denied');
+ if (!noUsers && !me?.isRoot) throw new Error('access denied');
const { account, secret } = await this.signupService.signup({
username: ps.username,
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 b7081987ca..e9f72676f0 100644
--- a/packages/backend/src/server/api/endpoints/admin/accounts/delete.ts
+++ b/packages/backend/src/server/api/endpoints/admin/accounts/delete.ts
@@ -11,7 +11,7 @@ export const meta = {
tags: ['admin'],
requireCredential: true,
- requireModerator: true,
+ requireAdmin: true,
} as const;
export const paramDef = {
@@ -41,12 +41,8 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
throw new Error('user not found');
}
- if (user.isAdmin) {
- throw new Error('cannot suspend admin');
- }
-
- if (user.isModerator) {
- throw new Error('cannot suspend moderator');
+ if (user.isRoot) {
+ throw new Error('cannot delete a root account');
}
if (this.userEntityService.isLocalUser(user)) {
diff --git a/packages/backend/src/server/api/endpoints/admin/delete-all-files-of-a-user.ts b/packages/backend/src/server/api/endpoints/admin/delete-all-files-of-a-user.ts
index 22b78bf19d..c193ed3fb3 100644
--- a/packages/backend/src/server/api/endpoints/admin/delete-all-files-of-a-user.ts
+++ b/packages/backend/src/server/api/endpoints/admin/delete-all-files-of-a-user.ts
@@ -8,7 +8,7 @@ export const meta = {
tags: ['admin'],
requireCredential: true,
- requireModerator: true,
+ requireAdmin: true,
} as const;
export const paramDef = {
diff --git a/packages/backend/src/server/api/endpoints/admin/drive-capacity-override.ts b/packages/backend/src/server/api/endpoints/admin/drive-capacity-override.ts
deleted file mode 100644
index 665e2a8cce..0000000000
--- a/packages/backend/src/server/api/endpoints/admin/drive-capacity-override.ts
+++ /dev/null
@@ -1,61 +0,0 @@
-import { Inject, Injectable } from '@nestjs/common';
-import { Endpoint } from '@/server/api/endpoint-base.js';
-import type { UsersRepository } from '@/models/index.js';
-import { ModerationLogService } from '@/core/ModerationLogService.js';
-import { DI } from '@/di-symbols.js';
-import { UserEntityService } from '@/core/entities/UserEntityService.js';
-
-export const meta = {
- tags: ['admin'],
-
- requireCredential: true,
- requireModerator: true,
-} as const;
-
-export const paramDef = {
- type: 'object',
- properties: {
- userId: { type: 'string', format: 'misskey:id' },
- overrideMb: { type: 'number', nullable: true },
- },
- required: ['userId', 'overrideMb'],
-} as const;
-
-// eslint-disable-next-line import/no-default-export
-@Injectable()
-export default class extends Endpoint<typeof meta, typeof paramDef> {
- constructor(
- @Inject(DI.usersRepository)
- private usersRepository: UsersRepository,
-
- private userEntityService: UserEntityService,
- private moderationLogService: ModerationLogService,
- ) {
- super(meta, paramDef, async (ps, me) => {
- const user = await this.usersRepository.findOneBy({ id: ps.userId });
-
- if (user == null) {
- throw new Error('user not found');
- }
-
- if (!this.userEntityService.isLocalUser(user)) {
- throw new Error('user is not local user');
- }
-
- /*if (user.isAdmin) {
- throw new Error('cannot suspend admin');
- }
- if (user.isModerator) {
- throw new Error('cannot suspend moderator');
- }*/
-
- await this.usersRepository.update(user.id, {
- driveCapacityOverrideMb: ps.overrideMb,
- });
-
- this.moderationLogService.insertModerationLog(me, 'change-drive-capacity-override', {
- targetId: user.id,
- });
- });
- }
-}
diff --git a/packages/backend/src/server/api/endpoints/admin/drive/show-file.ts b/packages/backend/src/server/api/endpoints/admin/drive/show-file.ts
index 6180eeae2b..6376cb153c 100644
--- a/packages/backend/src/server/api/endpoints/admin/drive/show-file.ts
+++ b/packages/backend/src/server/api/endpoints/admin/drive/show-file.ts
@@ -2,6 +2,7 @@ import { Inject, Injectable } from '@nestjs/common';
import type { DriveFilesRepository } from '@/models/index.js';
import { Endpoint } from '@/server/api/endpoint-base.js';
import { DI } from '@/di-symbols.js';
+import { RoleService } from '@/core/RoleService.js';
import { ApiError } from '../../../error.js';
export const meta = {
@@ -159,6 +160,8 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
constructor(
@Inject(DI.driveFilesRepository)
private driveFilesRepository: DriveFilesRepository,
+
+ private roleService: RoleService,
) {
super(meta, paramDef, async (ps, me) => {
const file = ps.fileId ? await this.driveFilesRepository.findOneBy({ id: ps.fileId }) : await this.driveFilesRepository.findOne({
@@ -175,6 +178,8 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
throw new ApiError(meta.errors.noSuchFile);
}
+ const isModerator = await this.roleService.isModerator(me);
+
return {
id: file.id,
userId: file.userId,
@@ -202,8 +207,8 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
name: file.name,
md5: file.md5,
createdAt: file.createdAt.toISOString(),
- requestIp: me.isAdmin ? file.requestIp : null,
- requestHeaders: me.isAdmin ? file.requestHeaders : null,
+ requestIp: isModerator ? file.requestIp : null,
+ requestHeaders: isModerator ? file.requestHeaders : null,
};
});
}
diff --git a/packages/backend/src/server/api/endpoints/admin/get-index-stats.ts b/packages/backend/src/server/api/endpoints/admin/get-index-stats.ts
index e53d0bfcea..8ffd2b01e7 100644
--- a/packages/backend/src/server/api/endpoints/admin/get-index-stats.ts
+++ b/packages/backend/src/server/api/endpoints/admin/get-index-stats.ts
@@ -5,7 +5,7 @@ import { DI } from '@/di-symbols.js';
export const meta = {
requireCredential: true,
- requireModerator: true,
+ requireAdmin: true,
tags: ['admin'],
} as const;
diff --git a/packages/backend/src/server/api/endpoints/admin/get-table-stats.ts b/packages/backend/src/server/api/endpoints/admin/get-table-stats.ts
index 41014cb167..09d61bd741 100644
--- a/packages/backend/src/server/api/endpoints/admin/get-table-stats.ts
+++ b/packages/backend/src/server/api/endpoints/admin/get-table-stats.ts
@@ -5,7 +5,7 @@ import { DI } from '@/di-symbols.js';
export const meta = {
requireCredential: true,
- requireModerator: true,
+ requireAdmin: true,
tags: ['admin'],
diff --git a/packages/backend/src/server/api/endpoints/admin/get-user-ips.ts b/packages/backend/src/server/api/endpoints/admin/get-user-ips.ts
index 947a673def..bfcc8a700b 100644
--- a/packages/backend/src/server/api/endpoints/admin/get-user-ips.ts
+++ b/packages/backend/src/server/api/endpoints/admin/get-user-ips.ts
@@ -7,7 +7,7 @@ export const meta = {
tags: ['admin'],
requireCredential: true,
- requireAdmin: true,
+ requireModerator: true,
} as const;
export const paramDef = {
diff --git a/packages/backend/src/server/api/endpoints/admin/meta.ts b/packages/backend/src/server/api/endpoints/admin/meta.ts
index 794ea3d5c9..33f162acf9 100644
--- a/packages/backend/src/server/api/endpoints/admin/meta.ts
+++ b/packages/backend/src/server/api/endpoints/admin/meta.ts
@@ -4,6 +4,7 @@ import { Endpoint } from '@/server/api/endpoint-base.js';
import { MetaService } from '@/core/MetaService.js';
import type { Config } from '@/config.js';
import { DI } from '@/di-symbols.js';
+import { DEFAULT_ROLE } from '@/core/RoleService.js';
export const meta = {
tags: ['meta'],
@@ -15,10 +16,6 @@ export const meta = {
type: 'object',
optional: false, nullable: false,
properties: {
- driveCapacityPerLocalUserMb: {
- type: 'number',
- optional: false, nullable: false,
- },
driveCapacityPerRemoteUserMb: {
type: 'number',
optional: false, nullable: false,
@@ -377,9 +374,6 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
repositoryUrl: instance.repositoryUrl,
feedbackUrl: instance.feedbackUrl,
disableRegistration: instance.disableRegistration,
- disableLocalTimeline: instance.disableLocalTimeline,
- disableGlobalTimeline: instance.disableGlobalTimeline,
- driveCapacityPerLocalUserMb: instance.localDriveCapacityMb,
driveCapacityPerRemoteUserMb: instance.remoteDriveCapacityMb,
emailRequiredForSignup: instance.emailRequiredForSignup,
enableHcaptcha: instance.enableHcaptcha,
@@ -451,6 +445,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
deeplIsPro: instance.deeplIsPro,
enableIpLogging: instance.enableIpLogging,
enableActiveEmailValidation: instance.enableActiveEmailValidation,
+ baseRole: { ...DEFAULT_ROLE, ...instance.defaultRoleOverride },
};
});
}
diff --git a/packages/backend/src/server/api/endpoints/admin/moderators/add.ts b/packages/backend/src/server/api/endpoints/admin/moderators/add.ts
deleted file mode 100644
index 2fc5a35e8e..0000000000
--- a/packages/backend/src/server/api/endpoints/admin/moderators/add.ts
+++ /dev/null
@@ -1,49 +0,0 @@
-import { Inject, Injectable } from '@nestjs/common';
-import { Endpoint } from '@/server/api/endpoint-base.js';
-import type { UsersRepository } from '@/models/index.js';
-import { GlobalEventService } from '@/core/GlobalEventService.js';
-import { DI } from '@/di-symbols.js';
-
-export const meta = {
- tags: ['admin'],
-
- requireCredential: true,
- requireAdmin: true,
-} as const;
-
-export const paramDef = {
- type: 'object',
- properties: {
- userId: { type: 'string', format: 'misskey:id' },
- },
- required: ['userId'],
-} as const;
-
-// eslint-disable-next-line import/no-default-export
-@Injectable()
-export default class extends Endpoint<typeof meta, typeof paramDef> {
- constructor(
- @Inject(DI.usersRepository)
- private usersRepository: UsersRepository,
-
- private globalEventService: GlobalEventService,
- ) {
- super(meta, paramDef, async (ps) => {
- const user = await this.usersRepository.findOneBy({ id: ps.userId });
-
- if (user == null) {
- throw new Error('user not found');
- }
-
- if (user.isAdmin) {
- throw new Error('cannot mark as moderator if admin user');
- }
-
- await this.usersRepository.update(user.id, {
- isModerator: true,
- });
-
- this.globalEventService.publishInternalEvent('userChangeModeratorState', { id: user.id, isModerator: true });
- });
- }
-}
diff --git a/packages/backend/src/server/api/endpoints/admin/reset-password.ts b/packages/backend/src/server/api/endpoints/admin/reset-password.ts
index f7d27be9cb..d263f99f6e 100644
--- a/packages/backend/src/server/api/endpoints/admin/reset-password.ts
+++ b/packages/backend/src/server/api/endpoints/admin/reset-password.ts
@@ -50,8 +50,8 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
throw new Error('user not found');
}
- if (user.isAdmin) {
- throw new Error('cannot reset password of admin');
+ if (user.isRoot) {
+ throw new Error('cannot reset password of root');
}
const passwd = rndstr('a-zA-Z0-9', 8);
diff --git a/packages/backend/src/server/api/endpoints/admin/roles/assign.ts b/packages/backend/src/server/api/endpoints/admin/roles/assign.ts
new file mode 100644
index 0000000000..7bfb2f6625
--- /dev/null
+++ b/packages/backend/src/server/api/endpoints/admin/roles/assign.ts
@@ -0,0 +1,96 @@
+import { Inject, Injectable } from '@nestjs/common';
+import { Endpoint } from '@/server/api/endpoint-base.js';
+import type { RoleAssignmentsRepository, RolesRepository, UsersRepository } from '@/models/index.js';
+import { DI } from '@/di-symbols.js';
+import { ApiError } from '@/server/api/error.js';
+import { IdService } from '@/core/IdService.js';
+import { GlobalEventService } from '@/core/GlobalEventService.js';
+import { RoleService } from '@/core/RoleService.js';
+
+export const meta = {
+ tags: ['admin', 'role'],
+
+ requireCredential: true,
+ requireModerator: true,
+
+ errors: {
+ noSuchRole: {
+ message: 'No such role.',
+ code: 'NO_SUCH_ROLE',
+ id: '6503c040-6af4-4ed9-bf07-f2dd16678eab',
+ },
+
+ noSuchUser: {
+ message: 'No such user.',
+ code: 'NO_SUCH_USER',
+ id: '558ea170-f653-4700-94d0-5a818371d0df',
+ },
+
+ accessDenied: {
+ message: 'Only administrators can edit members of the role.',
+ code: 'ACCESS_DENIED',
+ id: '25b5bc31-dc79-4ebd-9bd2-c84978fd052c',
+ },
+ },
+} as const;
+
+export const paramDef = {
+ type: 'object',
+ properties: {
+ roleId: { type: 'string', format: 'misskey:id' },
+ userId: { type: 'string', format: 'misskey:id' },
+ },
+ required: [
+ 'roleId',
+ 'userId',
+ ],
+} as const;
+
+// eslint-disable-next-line import/no-default-export
+@Injectable()
+export default class extends Endpoint<typeof meta, typeof paramDef> {
+ constructor(
+ @Inject(DI.usersRepository)
+ private usersRepository: UsersRepository,
+
+ @Inject(DI.rolesRepository)
+ private rolesRepository: RolesRepository,
+
+ @Inject(DI.roleAssignmentsRepository)
+ private roleAssignmentsRepository: RoleAssignmentsRepository,
+
+ private globalEventService: GlobalEventService,
+ private roleService: RoleService,
+ private idService: IdService,
+ ) {
+ super(meta, paramDef, async (ps, me) => {
+ const role = await this.rolesRepository.findOneBy({ id: ps.roleId });
+ if (role == null) {
+ throw new ApiError(meta.errors.noSuchRole);
+ }
+
+ if (!role.canEditMembersByModerator && !(await this.roleService.isAdministrator(me))) {
+ throw new ApiError(meta.errors.accessDenied);
+ }
+
+ const user = await this.usersRepository.findOneBy({ id: ps.userId });
+ if (user == null) {
+ throw new ApiError(meta.errors.noSuchUser);
+ }
+
+ const date = new Date();
+ const created = await this.roleAssignmentsRepository.insert({
+ id: this.idService.genId(),
+ createdAt: date,
+ roleId: role.id,
+ userId: user.id,
+ }).then(x => this.roleAssignmentsRepository.findOneByOrFail(x.identifiers[0]));
+
+ this.rolesRepository.update(ps.roleId, {
+ lastUsedAt: new Date(),
+ });
+
+ this.globalEventService.publishInternalEvent('userRoleAssigned', created);
+ });
+ }
+}
diff --git a/packages/backend/src/server/api/endpoints/admin/roles/create.ts b/packages/backend/src/server/api/endpoints/admin/roles/create.ts
new file mode 100644
index 0000000000..b04188fac6
--- /dev/null
+++ b/packages/backend/src/server/api/endpoints/admin/roles/create.ts
@@ -0,0 +1,75 @@
+import { Inject, Injectable } from '@nestjs/common';
+import { Endpoint } from '@/server/api/endpoint-base.js';
+import type { RolesRepository } from '@/models/index.js';
+import { GlobalEventService } from '@/core/GlobalEventService.js';
+import { DI } from '@/di-symbols.js';
+import { IdService } from '@/core/IdService.js';
+import { RoleEntityService } from '@/core/entities/RoleEntityService.js';
+
+export const meta = {
+ tags: ['admin', 'role'],
+
+ requireCredential: true,
+ requireAdmin: true,
+} as const;
+
+export const paramDef = {
+ type: 'object',
+ properties: {
+ name: { type: 'string' },
+ description: { type: 'string' },
+ color: { type: 'string', nullable: true },
+ isPublic: { type: 'boolean' },
+ isModerator: { type: 'boolean' },
+ isAdministrator: { type: 'boolean' },
+ canEditMembersByModerator: { type: 'boolean' },
+ options: {
+ type: 'object',
+ },
+ },
+ required: [
+ 'name',
+ 'description',
+ 'color',
+ 'isPublic',
+ 'isModerator',
+ 'isAdministrator',
+ 'canEditMembersByModerator',
+ 'options',
+ ],
+} as const;
+
+// eslint-disable-next-line import/no-default-export
+@Injectable()
+export default class extends Endpoint<typeof meta, typeof paramDef> {
+ constructor(
+ @Inject(DI.rolesRepository)
+ private rolesRepository: RolesRepository,
+
+ private globalEventService: GlobalEventService,
+ private idService: IdService,
+ private roleEntityService: RoleEntityService,
+ ) {
+ super(meta, paramDef, async (ps, me) => {
+ const date = new Date();
+ const created = await this.rolesRepository.insert({
+ id: this.idService.genId(),
+ createdAt: date,
+ updatedAt: date,
+ lastUsedAt: date,
+ name: ps.name,
+ description: ps.description,
+ color: ps.color,
+ isPublic: ps.isPublic,
+ isAdministrator: ps.isAdministrator,
+ isModerator: ps.isModerator,
+ canEditMembersByModerator: ps.canEditMembersByModerator,
+ options: ps.options,
+ }).then(x => this.rolesRepository.findOneByOrFail(x.identifiers[0]));
+
+ this.globalEventService.publishInternalEvent('roleCreated', created);
+
+ return await this.roleEntityService.pack(created, me);
+ });
+ }
+}
diff --git a/packages/backend/src/server/api/endpoints/admin/roles/delete.ts b/packages/backend/src/server/api/endpoints/admin/roles/delete.ts
new file mode 100644
index 0000000000..b56ebdb3ee
--- /dev/null
+++ b/packages/backend/src/server/api/endpoints/admin/roles/delete.ts
@@ -0,0 +1,53 @@
+import { Inject, Injectable } from '@nestjs/common';
+import { Endpoint } from '@/server/api/endpoint-base.js';
+import type { RolesRepository } from '@/models/index.js';
+import { GlobalEventService } from '@/core/GlobalEventService.js';
+import { DI } from '@/di-symbols.js';
+import { ApiError } from '@/server/api/error.js';
+
+export const meta = {
+ tags: ['admin', 'role'],
+
+ requireCredential: true,
+ requireAdmin: true,
+
+ errors: {
+ noSuchRole: {
+ message: 'No such role.',
+ code: 'NO_SUCH_ROLE',
+ id: 'de0d6ecd-8e0a-4253-88ff-74bc89ae3d45',
+ },
+ },
+} as const;
+
+export const paramDef = {
+ type: 'object',
+ properties: {
+ roleId: { type: 'string', format: 'misskey:id' },
+ },
+ required: [
+ 'roleId',
+ ],
+} as const;
+
+// eslint-disable-next-line import/no-default-export
+@Injectable()
+export default class extends Endpoint<typeof meta, typeof paramDef> {
+ constructor(
+ @Inject(DI.rolesRepository)
+ private rolesRepository: RolesRepository,
+
+ private globalEventService: GlobalEventService,
+ ) {
+ super(meta, paramDef, async (ps) => {
+ const role = await this.rolesRepository.findOneBy({ id: ps.roleId });
+ if (role == null) {
+ throw new ApiError(meta.errors.noSuchRole);
+ }
+ await this.rolesRepository.delete({
+ id: ps.roleId,
+ });
+ this.globalEventService.publishInternalEvent('roleDeleted', role);
+ });
+ }
+}
diff --git a/packages/backend/src/server/api/endpoints/admin/roles/list.ts b/packages/backend/src/server/api/endpoints/admin/roles/list.ts
new file mode 100644
index 0000000000..458a8d535b
--- /dev/null
+++ b/packages/backend/src/server/api/endpoints/admin/roles/list.ts
@@ -0,0 +1,39 @@
+import { Inject, Injectable } from '@nestjs/common';
+import { Endpoint } from '@/server/api/endpoint-base.js';
+import type { RolesRepository } from '@/models/index.js';
+import { DI } from '@/di-symbols.js';
+import { ApiError } from '@/server/api/error.js';
+import { RoleEntityService } from '@/core/entities/RoleEntityService.js';
+
+export const meta = {
+ tags: ['admin', 'role'],
+
+ requireCredential: true,
+ requireModerator: true,
+} as const;
+
+export const paramDef = {
+ type: 'object',
+ properties: {
+ },
+ required: [
+ ],
+} as const;
+
+// eslint-disable-next-line import/no-default-export
+@Injectable()
+export default class extends Endpoint<typeof meta, typeof paramDef> {
+ constructor(
+ @Inject(DI.rolesRepository)
+ private rolesRepository: RolesRepository,
+
+ private roleEntityService: RoleEntityService,
+ ) {
+ super(meta, paramDef, async (ps, me) => {
+ const roles = await this.rolesRepository.find({
+ order: { lastUsedAt: 'DESC' },
+ });
+ return await this.roleEntityService.packMany(roles, me, { detail: false });
+ });
+ }
+}
diff --git a/packages/backend/src/server/api/endpoints/admin/roles/show.ts b/packages/backend/src/server/api/endpoints/admin/roles/show.ts
new file mode 100644
index 0000000000..c83f96191d
--- /dev/null
+++ b/packages/backend/src/server/api/endpoints/admin/roles/show.ts
@@ -0,0 +1,50 @@
+import { Inject, Injectable } from '@nestjs/common';
+import { Endpoint } from '@/server/api/endpoint-base.js';
+import type { RolesRepository } from '@/models/index.js';
+import { DI } from '@/di-symbols.js';
+import { ApiError } from '@/server/api/error.js';
+import { RoleEntityService } from '@/core/entities/RoleEntityService.js';
+
+export const meta = {
+ tags: ['admin', 'role'],
+
+ requireCredential: true,
+ requireModerator: true,
+
+ errors: {
+ noSuchRole: {
+ message: 'No such role.',
+ code: 'NO_SUCH_ROLE',
+ id: '07dc7d34-c0d8-49b7-96c6-db3ce64ee0b3',
+ },
+ },
+} as const;
+
+export const paramDef = {
+ type: 'object',
+ properties: {
+ roleId: { type: 'string', format: 'misskey:id' },
+ },
+ required: [
+ 'roleId',
+ ],
+} as const;
+
+// eslint-disable-next-line import/no-default-export
+@Injectable()
+export default class extends Endpoint<typeof meta, typeof paramDef> {
+ constructor(
+ @Inject(DI.rolesRepository)
+ private rolesRepository: RolesRepository,
+
+ private roleEntityService: RoleEntityService,
+ ) {
+ super(meta, paramDef, async (ps) => {
+ const role = await this.rolesRepository.findOneBy({ id: ps.roleId });
+ if (role == null) {
+ throw new ApiError(meta.errors.noSuchRole);
+ }
+ return await this.roleEntityService.pack(role);
+ });
+ }
+}
diff --git a/packages/backend/src/server/api/endpoints/admin/roles/unassign.ts b/packages/backend/src/server/api/endpoints/admin/roles/unassign.ts
new file mode 100644
index 0000000000..141cc5ee89
--- /dev/null
+++ b/packages/backend/src/server/api/endpoints/admin/roles/unassign.ts
@@ -0,0 +1,101 @@
+import { Inject, Injectable } from '@nestjs/common';
+import { Endpoint } from '@/server/api/endpoint-base.js';
+import type { RoleAssignmentsRepository, RolesRepository, UsersRepository } from '@/models/index.js';
+import { DI } from '@/di-symbols.js';
+import { ApiError } from '@/server/api/error.js';
+import { IdService } from '@/core/IdService.js';
+import { GlobalEventService } from '@/core/GlobalEventService.js';
+import { RoleService } from '@/core/RoleService.js';
+
+export const meta = {
+ tags: ['admin', 'role'],
+
+ requireCredential: true,
+ requireModerator: true,
+
+ errors: {
+ noSuchRole: {
+ message: 'No such role.',
+ code: 'NO_SUCH_ROLE',
+ id: '6e519036-a70d-4c76-b679-bc8fb18194e2',
+ },
+
+ noSuchUser: {
+ message: 'No such user.',
+ code: 'NO_SUCH_USER',
+ id: '2b730f78-1179-461b-88ad-d24c9af1a5ce',
+ },
+
+ notAssigned: {
+ message: 'Not assigned.',
+ code: 'NOT_ASSIGNED',
+ id: 'b9060ac7-5c94-4da4-9f55-2047c953df44',
+ },
+
+ accessDenied: {
+ message: 'Only administrators can edit members of the role.',
+ code: 'ACCESS_DENIED',
+ id: '24636eee-e8c1-493e-94b2-e16ad401e262',
+ },
+ },
+} as const;
+
+export const paramDef = {
+ type: 'object',
+ properties: {
+ roleId: { type: 'string', format: 'misskey:id' },
+ userId: { type: 'string', format: 'misskey:id' },
+ },
+ required: [
+ 'roleId',
+ 'userId',
+ ],
+} as const;
+
+// eslint-disable-next-line import/no-default-export
+@Injectable()
+export default class extends Endpoint<typeof meta, typeof paramDef> {
+ constructor(
+ @Inject(DI.usersRepository)
+ private usersRepository: UsersRepository,
+
+ @Inject(DI.rolesRepository)
+ private rolesRepository: RolesRepository,
+
+ @Inject(DI.roleAssignmentsRepository)
+ private roleAssignmentsRepository: RoleAssignmentsRepository,
+
+ private globalEventService: GlobalEventService,
+ private roleService: RoleService,
+ private idService: IdService,
+ ) {
+ super(meta, paramDef, async (ps, me) => {
+ const role = await this.rolesRepository.findOneBy({ id: ps.roleId });
+ if (role == null) {
+ throw new ApiError(meta.errors.noSuchRole);
+ }
+
+ if (!role.canEditMembersByModerator && !(await this.roleService.isAdministrator(me))) {
+ throw new ApiError(meta.errors.accessDenied);
+ }
+
+ const user = await this.usersRepository.findOneBy({ id: ps.userId });
+ if (user == null) {
+ throw new ApiError(meta.errors.noSuchUser);
+ }
+
+ const roleAssignment = await this.roleAssignmentsRepository.findOneBy({ userId: user.id, roleId: role.id });
+ if (roleAssignment == null) {
+ throw new ApiError(meta.errors.notAssigned);
+ }
+
+ await this.roleAssignmentsRepository.delete(roleAssignment.id);
+
+ this.rolesRepository.update(ps.roleId, {
+ lastUsedAt: new Date(),
+ });
+
+ this.globalEventService.publishInternalEvent('userRoleUnassigned', roleAssignment);
+ });
+ }
+}
diff --git a/packages/backend/src/server/api/endpoints/admin/moderators/remove.ts b/packages/backend/src/server/api/endpoints/admin/roles/update-default-role-override.ts
index f0d7a3f12d..35da04efd2 100644
--- a/packages/backend/src/server/api/endpoints/admin/moderators/remove.ts
+++ b/packages/backend/src/server/api/endpoints/admin/roles/update-default-role-override.ts
@@ -1,11 +1,13 @@
import { Inject, Injectable } from '@nestjs/common';
import { Endpoint } from '@/server/api/endpoint-base.js';
-import type { UsersRepository } from '@/models/index.js';
+import type { RolesRepository } from '@/models/index.js';
import { GlobalEventService } from '@/core/GlobalEventService.js';
import { DI } from '@/di-symbols.js';
+import { ApiError } from '@/server/api/error.js';
+import { MetaService } from '@/core/MetaService.js';
export const meta = {
- tags: ['admin'],
+ tags: ['admin', 'role'],
requireCredential: true,
requireAdmin: true,
@@ -14,32 +16,27 @@ export const meta = {
export const paramDef = {
type: 'object',
properties: {
- userId: { type: 'string', format: 'misskey:id' },
+ options: {
+ type: 'object',
+ },
},
- required: ['userId'],
+ required: [
+ 'options',
+ ],
} as const;
// eslint-disable-next-line import/no-default-export
@Injectable()
export default class extends Endpoint<typeof meta, typeof paramDef> {
constructor(
- @Inject(DI.usersRepository)
- private usersRepository: UsersRepository,
-
+ private metaService: MetaService,
private globalEventService: GlobalEventService,
) {
super(meta, paramDef, async (ps) => {
- const user = await this.usersRepository.findOneBy({ id: ps.userId });
-
- if (user == null) {
- throw new Error('user not found');
- }
-
- await this.usersRepository.update(user.id, {
- isModerator: false,
+ await this.metaService.update({
+ defaultRoleOverride: ps.options,
});
-
- this.globalEventService.publishInternalEvent('userChangeModeratorState', { id: user.id, isModerator: false });
+ this.globalEventService.publishInternalEvent('defaultRoleOverrideUpdated', ps.options);
});
}
}
diff --git a/packages/backend/src/server/api/endpoints/admin/roles/update.ts b/packages/backend/src/server/api/endpoints/admin/roles/update.ts
new file mode 100644
index 0000000000..7d97d68e14
--- /dev/null
+++ b/packages/backend/src/server/api/endpoints/admin/roles/update.ts
@@ -0,0 +1,82 @@
+import { Inject, Injectable } from '@nestjs/common';
+import { Endpoint } from '@/server/api/endpoint-base.js';
+import type { RolesRepository } from '@/models/index.js';
+import { GlobalEventService } from '@/core/GlobalEventService.js';
+import { DI } from '@/di-symbols.js';
+import { ApiError } from '@/server/api/error.js';
+
+export const meta = {
+ tags: ['admin', 'role'],
+
+ requireCredential: true,
+ requireAdmin: true,
+
+ errors: {
+ noSuchRole: {
+ message: 'No such role.',
+ code: 'NO_SUCH_ROLE',
+ id: 'cd23ef55-09ad-428a-ac61-95a45e124b32',
+ },
+ },
+} as const;
+
+export const paramDef = {
+ type: 'object',
+ properties: {
+ roleId: { type: 'string', format: 'misskey:id' },
+ name: { type: 'string' },
+ description: { type: 'string' },
+ color: { type: 'string', nullable: true },
+ isPublic: { type: 'boolean' },
+ isModerator: { type: 'boolean' },
+ isAdministrator: { type: 'boolean' },
+ canEditMembersByModerator: { type: 'boolean' },
+ options: {
+ type: 'object',
+ },
+ },
+ required: [
+ 'roleId',
+ 'name',
+ 'description',
+ 'color',
+ 'isPublic',
+ 'isModerator',
+ 'isAdministrator',
+ 'canEditMembersByModerator',
+ 'options',
+ ],
+} as const;
+
+// eslint-disable-next-line import/no-default-export
+@Injectable()
+export default class extends Endpoint<typeof meta, typeof paramDef> {
+ constructor(
+ @Inject(DI.rolesRepository)
+ private rolesRepository: RolesRepository,
+
+ private globalEventService: GlobalEventService,
+ ) {
+ super(meta, paramDef, async (ps) => {
+ const role = await this.rolesRepository.findOneBy({ id: ps.roleId });
+ if (role == null) {
+ throw new ApiError(meta.errors.noSuchRole);
+ }
+
+ const date = new Date();
+ await this.rolesRepository.update(ps.roleId, {
+ updatedAt: date,
+ name: ps.name,
+ description: ps.description,
+ color: ps.color,
+ isPublic: ps.isPublic,
+ isModerator: ps.isModerator,
+ isAdministrator: ps.isAdministrator,
+ canEditMembersByModerator: ps.canEditMembersByModerator,
+ options: ps.options,
+ });
+ const updated = await this.rolesRepository.findOneByOrFail({ id: ps.roleId });
+ this.globalEventService.publishInternalEvent('roleUpdated', updated);
+ });
+ }
+}
diff --git a/packages/backend/src/server/api/endpoints/admin/show-user.ts b/packages/backend/src/server/api/endpoints/admin/show-user.ts
index e4031cf960..3f4ec299af 100644
--- a/packages/backend/src/server/api/endpoints/admin/show-user.ts
+++ b/packages/backend/src/server/api/endpoints/admin/show-user.ts
@@ -2,6 +2,8 @@ import { Inject, Injectable } from '@nestjs/common';
import type { UsersRepository, SigninsRepository, UserProfilesRepository } from '@/models/index.js';
import { Endpoint } from '@/server/api/endpoint-base.js';
import { DI } from '@/di-symbols.js';
+import { RoleService } from '@/core/RoleService.js';
+import { RoleEntityService } from '@/core/entities/RoleEntityService.js';
export const meta = {
tags: ['admin'],
@@ -35,6 +37,9 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
@Inject(DI.signinsRepository)
private signinsRepository: SigninsRepository,
+
+ private roleService: RoleService,
+ private roleEntityService: RoleEntityService,
) {
super(meta, paramDef, async (ps, me) => {
const [user, profile] = await Promise.all([
@@ -46,15 +51,16 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
throw new Error('user not found');
}
+ const isModerator = await this.roleService.isModerator(user);
+ const isSilenced = !(await this.roleService.getUserRoleOptions(user.id)).canPublicNote;
+
const _me = await this.usersRepository.findOneByOrFail({ id: me.id });
- if ((_me.isModerator && !_me.isAdmin) && user.isAdmin) {
+ if (!await this.roleService.isAdministrator(_me) && await this.roleService.isAdministrator(user)) {
throw new Error('cannot show info of admin');
}
- if (!_me.isAdmin) {
+ if (!await this.roleService.isAdministrator(_me)) {
return {
- isModerator: user.isModerator,
- isSilenced: user.isSilenced,
isSuspended: user.isSuspended,
};
}
@@ -66,6 +72,8 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
const signins = await this.signinsRepository.findBy({ userId: user.id });
+ const roles = await this.roleService.getUserRoles(user.id);
+
return {
email: profile.email,
emailVerified: profile.emailVerified,
@@ -80,12 +88,13 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
mutedWords: profile.mutedWords,
mutedInstances: profile.mutedInstances,
mutingNotificationTypes: profile.mutingNotificationTypes,
- isModerator: user.isModerator,
- isSilenced: user.isSilenced,
+ isModerator: isModerator,
+ isSilenced: isSilenced,
isSuspended: user.isSuspended,
lastActiveDate: user.lastActiveDate,
moderationNote: profile.moderationNote,
signins,
+ roles: await this.roleEntityService.packMany(roles, me, { detail: false }),
};
});
}
diff --git a/packages/backend/src/server/api/endpoints/admin/show-users.ts b/packages/backend/src/server/api/endpoints/admin/show-users.ts
index 5a67cf522a..426973f282 100644
--- a/packages/backend/src/server/api/endpoints/admin/show-users.ts
+++ b/packages/backend/src/server/api/endpoints/admin/show-users.ts
@@ -4,6 +4,7 @@ import { Endpoint } from '@/server/api/endpoint-base.js';
import { DI } from '@/di-symbols.js';
import { UserEntityService } from '@/core/entities/UserEntityService.js';
import { sqlLikeEscape } from '@/misc/sql-like-escape.js';
+import { RoleService } from '@/core/RoleService.js';
export const meta = {
tags: ['admin'],
@@ -28,7 +29,7 @@ export const paramDef = {
limit: { type: 'integer', minimum: 1, maximum: 100, default: 10 },
offset: { type: 'integer', default: 0 },
sort: { type: 'string', enum: ['+follower', '-follower', '+createdAt', '-createdAt', '+updatedAt', '-updatedAt', '+lastActiveDate', '-lastActiveDate'] },
- state: { type: 'string', enum: ['all', 'alive', 'available', 'admin', 'moderator', 'adminOrModerator', 'silenced', 'suspended'], default: 'all' },
+ state: { type: 'string', enum: ['all', 'alive', 'available', 'admin', 'moderator', 'adminOrModerator', 'suspended'], default: 'all' },
origin: { type: 'string', enum: ['combined', 'local', 'remote'], default: 'combined' },
username: { type: 'string', nullable: true, default: null },
hostname: {
@@ -49,18 +50,33 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
private usersRepository: UsersRepository,
private userEntityService: UserEntityService,
+ private roleService: RoleService,
) {
super(meta, paramDef, async (ps, me) => {
const query = this.usersRepository.createQueryBuilder('user');
switch (ps.state) {
case 'available': query.where('user.isSuspended = FALSE'); break;
- case 'admin': query.where('user.isAdmin = TRUE'); break;
- case 'moderator': query.where('user.isModerator = TRUE'); break;
- case 'adminOrModerator': query.where('user.isAdmin = TRUE OR user.isModerator = TRUE'); break;
case 'alive': query.where('user.updatedAt > :date', { date: new Date(Date.now() - 1000 * 60 * 60 * 24 * 5) }); break;
- case 'silenced': query.where('user.isSilenced = TRUE'); break;
case 'suspended': query.where('user.isSuspended = TRUE'); break;
+ case 'admin': {
+ const adminIds = await this.roleService.getAdministratorIds();
+ if (adminIds.length === 0) return [];
+ query.where('user.id IN (:...adminIds)', { adminIds: adminIds });
+ break;
+ }
+ case 'moderator': {
+ const moderatorIds = await this.roleService.getModeratorIds(false);
+ if (moderatorIds.length === 0) return [];
+ query.where('user.id IN (:...moderatorIds)', { moderatorIds: moderatorIds });
+ break;
+ }
+ case 'adminOrModerator': {
+ const adminOrModeratorIds = await this.roleService.getModeratorIds();
+ if (adminOrModeratorIds.length === 0) return [];
+ query.where('user.id IN (:...adminOrModeratorIds)', { adminOrModeratorIds: adminOrModeratorIds });
+ break;
+ }
}
switch (ps.origin) {
diff --git a/packages/backend/src/server/api/endpoints/admin/silence-user.ts b/packages/backend/src/server/api/endpoints/admin/silence-user.ts
deleted file mode 100644
index b9dbd211e0..0000000000
--- a/packages/backend/src/server/api/endpoints/admin/silence-user.ts
+++ /dev/null
@@ -1,55 +0,0 @@
-import { Inject, Injectable } from '@nestjs/common';
-import { Endpoint } from '@/server/api/endpoint-base.js';
-import { ModerationLogService } from '@/core/ModerationLogService.js';
-import type { UsersRepository } from '@/models/index.js';
-import { GlobalEventService } from '@/core/GlobalEventService.js';
-import { DI } from '@/di-symbols.js';
-
-export const meta = {
- tags: ['admin'],
-
- requireCredential: true,
- requireModerator: true,
-} as const;
-
-export const paramDef = {
- type: 'object',
- properties: {
- userId: { type: 'string', format: 'misskey:id' },
- },
- required: ['userId'],
-} as const;
-
-// eslint-disable-next-line import/no-default-export
-@Injectable()
-export default class extends Endpoint<typeof meta, typeof paramDef> {
- constructor(
- @Inject(DI.usersRepository)
- private usersRepository: UsersRepository,
-
- private moderationLogService: ModerationLogService,
- private globalEventService: GlobalEventService,
- ) {
- super(meta, paramDef, async (ps, me) => {
- const user = await this.usersRepository.findOneBy({ id: ps.userId });
-
- if (user == null) {
- throw new Error('user not found');
- }
-
- if (user.isAdmin) {
- throw new Error('cannot silence admin');
- }
-
- await this.usersRepository.update(user.id, {
- isSilenced: true,
- });
-
- this.globalEventService.publishInternalEvent('userChangeSilencedState', { id: user.id, isSilenced: true });
-
- this.moderationLogService.insertModerationLog(me, 'silence', {
- targetId: user.id,
- });
- });
- }
-}
diff --git a/packages/backend/src/server/api/endpoints/admin/suspend-user.ts b/packages/backend/src/server/api/endpoints/admin/suspend-user.ts
index 9fc1391570..3ad6c7c484 100644
--- a/packages/backend/src/server/api/endpoints/admin/suspend-user.ts
+++ b/packages/backend/src/server/api/endpoints/admin/suspend-user.ts
@@ -9,6 +9,7 @@ import { UserFollowingService } from '@/core/UserFollowingService.js';
import { DI } from '@/di-symbols.js';
import { UserEntityService } from '@/core/entities/UserEntityService.js';
import { bindThis } from '@/decorators.js';
+import { RoleService } from '@/core/RoleService.js';
export const meta = {
tags: ['admin'],
@@ -41,6 +42,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
private userEntityService: UserEntityService,
private userFollowingService: UserFollowingService,
private userSuspendService: UserSuspendService,
+ private roleService: RoleService,
private moderationLogService: ModerationLogService,
private globalEventService: GlobalEventService,
) {
@@ -51,12 +53,8 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
throw new Error('user not found');
}
- if (user.isAdmin) {
- throw new Error('cannot suspend admin');
- }
-
- if (user.isModerator) {
- throw new Error('cannot suspend moderator');
+ if (await this.roleService.isModerator(user)) {
+ throw new Error('cannot suspend moderator account');
}
await this.usersRepository.update(user.id, {
diff --git a/packages/backend/src/server/api/endpoints/admin/unsilence-user.ts b/packages/backend/src/server/api/endpoints/admin/unsilence-user.ts
deleted file mode 100644
index 3a9d410de0..0000000000
--- a/packages/backend/src/server/api/endpoints/admin/unsilence-user.ts
+++ /dev/null
@@ -1,51 +0,0 @@
-import { Inject, Injectable } from '@nestjs/common';
-import { Endpoint } from '@/server/api/endpoint-base.js';
-import type { UsersRepository } from '@/models/index.js';
-import { GlobalEventService } from '@/core/GlobalEventService.js';
-import { ModerationLogService } from '@/core/ModerationLogService.js';
-import { DI } from '@/di-symbols.js';
-
-export const meta = {
- tags: ['admin'],
-
- requireCredential: true,
- requireModerator: true,
-} as const;
-
-export const paramDef = {
- type: 'object',
- properties: {
- userId: { type: 'string', format: 'misskey:id' },
- },
- required: ['userId'],
-} as const;
-
-// eslint-disable-next-line import/no-default-export
-@Injectable()
-export default class extends Endpoint<typeof meta, typeof paramDef> {
- constructor(
- @Inject(DI.usersRepository)
- private usersRepository: UsersRepository,
-
- private moderationLogService: ModerationLogService,
- private globalEventService: GlobalEventService,
- ) {
- super(meta, paramDef, async (ps, me) => {
- const user = await this.usersRepository.findOneBy({ id: ps.userId });
-
- if (user == null) {
- throw new Error('user not found');
- }
-
- await this.usersRepository.update(user.id, {
- isSilenced: false,
- });
-
- this.globalEventService.publishInternalEvent('userChangeSilencedState', { id: user.id, isSilenced: false });
-
- this.moderationLogService.insertModerationLog(me, 'unsilence', {
- targetId: user.id,
- });
- });
- }
-}
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 795b8460f3..c766494e6b 100644
--- a/packages/backend/src/server/api/endpoints/admin/update-meta.ts
+++ b/packages/backend/src/server/api/endpoints/admin/update-meta.ts
@@ -19,8 +19,6 @@ export const paramDef = {
type: 'object',
properties: {
disableRegistration: { type: 'boolean', nullable: true },
- disableLocalTimeline: { type: 'boolean', nullable: true },
- disableGlobalTimeline: { type: 'boolean', nullable: true },
useStarForReactionFallback: { type: 'boolean', nullable: true },
pinnedUsers: { type: 'array', nullable: true, items: {
type: 'string',
@@ -42,7 +40,6 @@ export const paramDef = {
description: { type: 'string', nullable: true },
defaultLightTheme: { type: 'string', nullable: true },
defaultDarkTheme: { type: 'string', nullable: true },
- localDriveCapacityMb: { type: 'integer' },
remoteDriveCapacityMb: { type: 'integer' },
cacheRemoteFiles: { type: 'boolean' },
emailRequiredForSignup: { type: 'boolean' },
@@ -130,14 +127,6 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
set.disableRegistration = ps.disableRegistration;
}
- if (typeof ps.disableLocalTimeline === 'boolean') {
- set.disableLocalTimeline = ps.disableLocalTimeline;
- }
-
- if (typeof ps.disableGlobalTimeline === 'boolean') {
- set.disableGlobalTimeline = ps.disableGlobalTimeline;
- }
-
if (typeof ps.useStarForReactionFallback === 'boolean') {
set.useStarForReactionFallback = ps.useStarForReactionFallback;
}
@@ -194,10 +183,6 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
set.defaultDarkTheme = ps.defaultDarkTheme;
}
- if (ps.localDriveCapacityMb !== undefined) {
- set.localDriveCapacityMb = ps.localDriveCapacityMb;
- }
-
if (ps.remoteDriveCapacityMb !== undefined) {
set.remoteDriveCapacityMb = ps.remoteDriveCapacityMb;
}
diff --git a/packages/backend/src/server/api/endpoints/antennas/create.ts b/packages/backend/src/server/api/endpoints/antennas/create.ts
index 2378660ec8..08625250c8 100644
--- a/packages/backend/src/server/api/endpoints/antennas/create.ts
+++ b/packages/backend/src/server/api/endpoints/antennas/create.ts
@@ -5,6 +5,7 @@ import type { UserListsRepository, UserGroupJoiningsRepository, AntennasReposito
import { GlobalEventService } from '@/core/GlobalEventService.js';
import { AntennaEntityService } from '@/core/entities/AntennaEntityService.js';
import { DI } from '@/di-symbols.js';
+import { RoleService } from '@/core/RoleService.js';
import { ApiError } from '../../error.js';
export const meta = {
@@ -83,6 +84,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
private userGroupJoiningsRepository: UserGroupJoiningsRepository,
private antennaEntityService: AntennaEntityService,
+ private roleService: RoleService,
private idService: IdService,
private globalEventService: GlobalEventService,
) {
@@ -90,7 +92,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
const currentAntennasCount = await this.antennasRepository.countBy({
userId: me.id,
});
- if (currentAntennasCount > 5) {
+ if (currentAntennasCount > (await this.roleService.getUserRoleOptions(me.id)).antennaLimit) {
throw new ApiError(meta.errors.tooManyAntennas);
}
diff --git a/packages/backend/src/server/api/endpoints/drive.ts b/packages/backend/src/server/api/endpoints/drive.ts
index 6f40225f15..2a06792dcf 100644
--- a/packages/backend/src/server/api/endpoints/drive.ts
+++ b/packages/backend/src/server/api/endpoints/drive.ts
@@ -2,6 +2,7 @@ import { Inject, Injectable } from '@nestjs/common';
import { Endpoint } from '@/server/api/endpoint-base.js';
import { MetaService } from '@/core/MetaService.js';
import { DriveFileEntityService } from '@/core/entities/DriveFileEntityService.js';
+import { RoleService } from '@/core/RoleService.js';
export const meta = {
tags: ['drive', 'account'],
@@ -38,6 +39,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
constructor(
private metaService: MetaService,
private driveFileEntityService: DriveFileEntityService,
+ private roleService: RoleService,
) {
super(meta, paramDef, async (ps, me) => {
const instance = await this.metaService.fetch(true);
@@ -45,8 +47,10 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
// Calculate drive usage
const usage = await this.driveFileEntityService.calcDriveUsageOf(me.id);
+ const myRole = await this.roleService.getUserRoleOptions(me.id);
+
return {
- capacity: 1024 * 1024 * (me.driveCapacityOverrideMb ?? instance.localDriveCapacityMb),
+ capacity: 1024 * 1024 * myRole.driveCapacityMb,
usage: usage,
};
});
diff --git a/packages/backend/src/server/api/endpoints/drive/files/delete.ts b/packages/backend/src/server/api/endpoints/drive/files/delete.ts
index be7b050907..2ced97ee02 100644
--- a/packages/backend/src/server/api/endpoints/drive/files/delete.ts
+++ b/packages/backend/src/server/api/endpoints/drive/files/delete.ts
@@ -4,6 +4,7 @@ import type { DriveFilesRepository } from '@/models/index.js';
import { DriveService } from '@/core/DriveService.js';
import { GlobalEventService } from '@/core/GlobalEventService.js';
import { DI } from '@/di-symbols.js';
+import { RoleService } from '@/core/RoleService.js';
import { ApiError } from '../../../error.js';
export const meta = {
@@ -46,6 +47,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
private driveFilesRepository: DriveFilesRepository,
private driveService: DriveService,
+ private roleService: RoleService,
private globalEventService: GlobalEventService,
) {
super(meta, paramDef, async (ps, me) => {
@@ -55,7 +57,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
throw new ApiError(meta.errors.noSuchFile);
}
- if ((!me.isAdmin && !me.isModerator) && (file.userId !== me.id)) {
+ if (!await this.roleService.isModerator(me) && (file.userId !== me.id)) {
throw new ApiError(meta.errors.accessDenied);
}
diff --git a/packages/backend/src/server/api/endpoints/drive/files/show.ts b/packages/backend/src/server/api/endpoints/drive/files/show.ts
index 474d599cb6..e0a07a3640 100644
--- a/packages/backend/src/server/api/endpoints/drive/files/show.ts
+++ b/packages/backend/src/server/api/endpoints/drive/files/show.ts
@@ -4,6 +4,7 @@ import type { DriveFilesRepository } from '@/models/index.js';
import { Endpoint } from '@/server/api/endpoint-base.js';
import { DriveFileEntityService } from '@/core/entities/DriveFileEntityService.js';
import { DI } from '@/di-symbols.js';
+import { RoleService } from '@/core/RoleService.js';
import { ApiError } from '../../../error.js';
export const meta = {
@@ -62,6 +63,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
private driveFilesRepository: DriveFilesRepository,
private driveFileEntityService: DriveFileEntityService,
+ private roleService: RoleService,
) {
super(meta, paramDef, async (ps, me) => {
let file: DriveFile | null = null;
@@ -84,7 +86,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
throw new ApiError(meta.errors.noSuchFile);
}
- if ((!me.isAdmin && !me.isModerator) && (file.userId !== me.id)) {
+ if (!await this.roleService.isModerator(me) && (file.userId !== me.id)) {
throw new ApiError(meta.errors.accessDenied);
}
diff --git a/packages/backend/src/server/api/endpoints/drive/files/update.ts b/packages/backend/src/server/api/endpoints/drive/files/update.ts
index 9e2c767277..0fe57de6a8 100644
--- a/packages/backend/src/server/api/endpoints/drive/files/update.ts
+++ b/packages/backend/src/server/api/endpoints/drive/files/update.ts
@@ -5,6 +5,7 @@ import { Endpoint } from '@/server/api/endpoint-base.js';
import { DriveFileEntityService } from '@/core/entities/DriveFileEntityService.js';
import { GlobalEventService } from '@/core/GlobalEventService.js';
import { DI } from '@/di-symbols.js';
+import { RoleService } from '@/core/RoleService.js';
import { ApiError } from '../../../error.js';
export const meta = {
@@ -72,6 +73,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
private driveFoldersRepository: DriveFoldersRepository,
private driveFileEntityService: DriveFileEntityService,
+ private roleService: RoleService,
private globalEventService: GlobalEventService,
) {
super(meta, paramDef, async (ps, me) => {
@@ -81,7 +83,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
throw new ApiError(meta.errors.noSuchFile);
}
- if ((!me.isAdmin && !me.isModerator) && (file.userId !== me.id)) {
+ if (!await this.roleService.isModerator(me) && (file.userId !== me.id)) {
throw new ApiError(meta.errors.accessDenied);
}
diff --git a/packages/backend/src/server/api/endpoints/meta.ts b/packages/backend/src/server/api/endpoints/meta.ts
index c44d63d64b..f87fca63e3 100644
--- a/packages/backend/src/server/api/endpoints/meta.ts
+++ b/packages/backend/src/server/api/endpoints/meta.ts
@@ -7,6 +7,7 @@ import { UserEntityService } from '@/core/entities/UserEntityService.js';
import { MetaService } from '@/core/MetaService.js';
import type { Config } from '@/config.js';
import { DI } from '@/di-symbols.js';
+import { DEFAULT_ROLE } from '@/core/RoleService.js';
export const meta = {
tags: ['meta'],
@@ -77,18 +78,6 @@ export const meta = {
type: 'boolean',
optional: false, nullable: false,
},
- disableLocalTimeline: {
- type: 'boolean',
- optional: false, nullable: false,
- },
- disableGlobalTimeline: {
- type: 'boolean',
- optional: false, nullable: false,
- },
- driveCapacityPerLocalUserMb: {
- type: 'number',
- optional: false, nullable: false,
- },
driveCapacityPerRemoteUserMb: {
type: 'number',
optional: false, nullable: false,
@@ -314,9 +303,6 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
repositoryUrl: instance.repositoryUrl,
feedbackUrl: instance.feedbackUrl,
disableRegistration: instance.disableRegistration,
- disableLocalTimeline: instance.disableLocalTimeline,
- disableGlobalTimeline: instance.disableGlobalTimeline,
- driveCapacityPerLocalUserMb: instance.localDriveCapacityMb,
driveCapacityPerRemoteUserMb: instance.remoteDriveCapacityMb,
emailRequiredForSignup: instance.emailRequiredForSignup,
enableHcaptcha: instance.enableHcaptcha,
@@ -353,6 +339,8 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
translatorAvailable: instance.deeplAuthKey != null,
+ baseRole: { ...DEFAULT_ROLE, ...instance.defaultRoleOverride },
+
...(ps.detail ? {
pinnedPages: instance.pinnedPages,
pinnedClipId: instance.pinnedClipId,
@@ -369,8 +357,6 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
response.proxyAccountName = proxyAccount ? proxyAccount.username : null;
response.features = {
registration: !instance.disableRegistration,
- localTimeLine: !instance.disableLocalTimeline,
- globalTimeLine: !instance.disableGlobalTimeline,
emailRequiredForSignup: instance.emailRequiredForSignup,
elasticsearch: this.config.elasticsearch ? true : false,
hcaptcha: instance.enableHcaptcha,
diff --git a/packages/backend/src/server/api/endpoints/notes/delete.ts b/packages/backend/src/server/api/endpoints/notes/delete.ts
index 3c6e7bf768..16c4c01387 100644
--- a/packages/backend/src/server/api/endpoints/notes/delete.ts
+++ b/packages/backend/src/server/api/endpoints/notes/delete.ts
@@ -4,8 +4,9 @@ import type { UsersRepository } from '@/models/index.js';
import { Endpoint } from '@/server/api/endpoint-base.js';
import { NoteDeleteService } from '@/core/NoteDeleteService.js';
import { DI } from '@/di-symbols.js';
-import { ApiError } from '../../error.js';
import { GetterService } from '@/server/api/GetterService.js';
+import { RoleService } from '@/core/RoleService.js';
+import { ApiError } from '../../error.js';
export const meta = {
tags: ['notes'],
@@ -51,6 +52,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
private usersRepository: UsersRepository,
private getterService: GetterService,
+ private roleService: RoleService,
private noteDeleteService: NoteDeleteService,
) {
super(meta, paramDef, async (ps, me) => {
@@ -59,7 +61,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
throw err;
});
- if ((!me.isAdmin && !me.isModerator) && (note.userId !== me.id)) {
+ if (!await this.roleService.isModerator(me) && (note.userId !== me.id)) {
throw new ApiError(meta.errors.accessDenied);
}
diff --git a/packages/backend/src/server/api/endpoints/notes/global-timeline.ts b/packages/backend/src/server/api/endpoints/notes/global-timeline.ts
index b6eaccb5ac..081563493d 100644
--- a/packages/backend/src/server/api/endpoints/notes/global-timeline.ts
+++ b/packages/backend/src/server/api/endpoints/notes/global-timeline.ts
@@ -6,6 +6,7 @@ import { NoteEntityService } from '@/core/entities/NoteEntityService.js';
import { MetaService } from '@/core/MetaService.js';
import ActiveUsersChart from '@/core/chart/charts/active-users.js';
import { DI } from '@/di-symbols.js';
+import { RoleService } from '@/core/RoleService.js';
import { ApiError } from '../../error.js';
export const meta = {
@@ -57,14 +58,13 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
private noteEntityService: NoteEntityService,
private queryService: QueryService,
private metaService: MetaService,
+ private roleService: RoleService,
private activeUsersChart: ActiveUsersChart,
) {
super(meta, paramDef, async (ps, me) => {
- const m = await this.metaService.fetch();
- if (m.disableGlobalTimeline) {
- if (me == null || (!me.isAdmin && !me.isModerator)) {
- throw new ApiError(meta.errors.gtlDisabled);
- }
+ const role = await this.roleService.getUserRoleOptions(me ? me.id : null);
+ if (!role.gtlAvailable) {
+ throw new ApiError(meta.errors.gtlDisabled);
}
//#region Construct query
diff --git a/packages/backend/src/server/api/endpoints/notes/hybrid-timeline.ts b/packages/backend/src/server/api/endpoints/notes/hybrid-timeline.ts
index 58bbf223a1..b2c504448e 100644
--- a/packages/backend/src/server/api/endpoints/notes/hybrid-timeline.ts
+++ b/packages/backend/src/server/api/endpoints/notes/hybrid-timeline.ts
@@ -7,6 +7,7 @@ import ActiveUsersChart from '@/core/chart/charts/active-users.js';
import { MetaService } from '@/core/MetaService.js';
import { NoteEntityService } from '@/core/entities/NoteEntityService.js';
import { DI } from '@/di-symbols.js';
+import { RoleService } from '@/core/RoleService.js';
import { ApiError } from '../../error.js';
export const meta = {
@@ -66,11 +67,12 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
private noteEntityService: NoteEntityService,
private queryService: QueryService,
private metaService: MetaService,
+ private roleService: RoleService,
private activeUsersChart: ActiveUsersChart,
) {
super(meta, paramDef, async (ps, me) => {
- const m = await this.metaService.fetch();
- if (m.disableLocalTimeline && (!me.isAdmin && !me.isModerator)) {
+ const role = await this.roleService.getUserRoleOptions(me.id);
+ if (!role.ltlAvailable) {
throw new ApiError(meta.errors.stlDisabled);
}
diff --git a/packages/backend/src/server/api/endpoints/notes/local-timeline.ts b/packages/backend/src/server/api/endpoints/notes/local-timeline.ts
index d3594814b0..6361edc310 100644
--- a/packages/backend/src/server/api/endpoints/notes/local-timeline.ts
+++ b/packages/backend/src/server/api/endpoints/notes/local-timeline.ts
@@ -7,6 +7,7 @@ import { NoteEntityService } from '@/core/entities/NoteEntityService.js';
import { MetaService } from '@/core/MetaService.js';
import ActiveUsersChart from '@/core/chart/charts/active-users.js';
import { DI } from '@/di-symbols.js';
+import { RoleService } from '@/core/RoleService.js';
import { ApiError } from '../../error.js';
export const meta = {
@@ -62,14 +63,13 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
private noteEntityService: NoteEntityService,
private queryService: QueryService,
private metaService: MetaService,
+ private roleService: RoleService,
private activeUsersChart: ActiveUsersChart,
) {
super(meta, paramDef, async (ps, me) => {
- const m = await this.metaService.fetch();
- if (m.disableLocalTimeline) {
- if (me == null || (!me.isAdmin && !me.isModerator)) {
- throw new ApiError(meta.errors.ltlDisabled);
- }
+ const role = await this.roleService.getUserRoleOptions(me ? me.id : null);
+ if (!role.ltlAvailable) {
+ throw new ApiError(meta.errors.ltlDisabled);
}
//#region Construct query
diff --git a/packages/backend/src/server/api/endpoints/users.ts b/packages/backend/src/server/api/endpoints/users.ts
index b015129a7a..8becb68a34 100644
--- a/packages/backend/src/server/api/endpoints/users.ts
+++ b/packages/backend/src/server/api/endpoints/users.ts
@@ -27,7 +27,7 @@ export const paramDef = {
limit: { type: 'integer', minimum: 1, maximum: 100, default: 10 },
offset: { type: 'integer', default: 0 },
sort: { type: 'string', enum: ['+follower', '-follower', '+createdAt', '-createdAt', '+updatedAt', '-updatedAt'] },
- state: { type: 'string', enum: ['all', 'admin', 'moderator', 'adminOrModerator', 'alive'], default: 'all' },
+ state: { type: 'string', enum: ['all', 'alive'], default: 'all' },
origin: { type: 'string', enum: ['combined', 'local', 'remote'], default: 'local' },
hostname: {
type: 'string',
@@ -54,9 +54,6 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
query.where('user.isExplorable = TRUE');
switch (ps.state) {
- case 'admin': query.andWhere('user.isAdmin = TRUE'); break;
- case 'moderator': query.andWhere('user.isModerator = TRUE'); break;
- case 'adminOrModerator': query.andWhere('user.isAdmin = TRUE OR user.isModerator = TRUE'); break;
case 'alive': query.andWhere('user.updatedAt > :date', { date: new Date(Date.now() - 1000 * 60 * 60 * 24 * 5) }); break;
}
diff --git a/packages/backend/src/server/api/endpoints/users/report-abuse.ts b/packages/backend/src/server/api/endpoints/users/report-abuse.ts
index 13badab727..d19d4007d6 100644
--- a/packages/backend/src/server/api/endpoints/users/report-abuse.ts
+++ b/packages/backend/src/server/api/endpoints/users/report-abuse.ts
@@ -7,8 +7,9 @@ import { GlobalEventService } from '@/core/GlobalEventService.js';
import { MetaService } from '@/core/MetaService.js';
import { EmailService } from '@/core/EmailService.js';
import { DI } from '@/di-symbols.js';
-import { ApiError } from '../../error.js';
import { GetterService } from '@/server/api/GetterService.js';
+import { RoleService } from '@/core/RoleService.js';
+import { ApiError } from '../../error.js';
export const meta = {
tags: ['users'],
@@ -61,6 +62,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
private metaService: MetaService,
private emailService: EmailService,
private getterService: GetterService,
+ private roleService: RoleService,
private globalEventService: GlobalEventService,
) {
super(meta, paramDef, async (ps, me) => {
@@ -74,7 +76,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
throw new ApiError(meta.errors.cannotReportYourself);
}
- if (user.isAdmin) {
+ if (await this.roleService.isAdministrator(user)) {
throw new ApiError(meta.errors.cannotReportAdmin);
}
@@ -90,13 +92,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
// Publish event to moderators
setImmediate(async () => {
- const moderators = await this.usersRepository.find({
- where: [{
- isAdmin: true,
- }, {
- isModerator: true,
- }],
- });
+ const moderators = await this.roleService.getModerators();
for (const moderator of moderators) {
this.globalEventService.publishAdminStream(moderator.id, 'newAbuseUserReport', {
diff --git a/packages/backend/src/server/api/endpoints/users/show.ts b/packages/backend/src/server/api/endpoints/users/show.ts
index fcdaeae1c9..70258ef009 100644
--- a/packages/backend/src/server/api/endpoints/users/show.ts
+++ b/packages/backend/src/server/api/endpoints/users/show.ts
@@ -7,6 +7,7 @@ import { UserEntityService } from '@/core/entities/UserEntityService.js';
import { RemoteUserResolveService } from '@/core/RemoteUserResolveService.js';
import { DI } from '@/di-symbols.js';
import PerUserPvChart from '@/core/chart/charts/per-user-pv.js';
+import { RoleService } from '@/core/RoleService.js';
import { ApiError } from '../../error.js';
import { ApiLoggerService } from '../../ApiLoggerService.js';
import type { FindOptionsWhere } from 'typeorm';
@@ -91,20 +92,21 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
private userEntityService: UserEntityService,
private remoteUserResolveService: RemoteUserResolveService,
+ private roleService: RoleService,
private perUserPvChart: PerUserPvChart,
private apiLoggerService: ApiLoggerService,
) {
super(meta, paramDef, async (ps, me, _1, _2, _3, ip) => {
let user;
- const isAdminOrModerator = me && (me.isAdmin || me.isModerator);
+ const isModerator = await this.roleService.isModerator(me);
if (ps.userIds) {
if (ps.userIds.length === 0) {
return [];
}
- const users = await this.usersRepository.findBy(isAdminOrModerator ? {
+ const users = await this.usersRepository.findBy(isModerator ? {
id: In(ps.userIds),
} : {
id: In(ps.userIds),
@@ -135,7 +137,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
user = await this.usersRepository.findOneBy(q);
}
- if (user == null || (!isAdminOrModerator && user.isSuspended)) {
+ if (user == null || (!isModerator && user.isSuspended)) {
throw new ApiError(meta.errors.noSuchUser);
}
diff --git a/packages/backend/src/server/api/stream/channels/global-timeline.ts b/packages/backend/src/server/api/stream/channels/global-timeline.ts
index 34f782e580..185c813869 100644
--- a/packages/backend/src/server/api/stream/channels/global-timeline.ts
+++ b/packages/backend/src/server/api/stream/channels/global-timeline.ts
@@ -7,6 +7,7 @@ import type { Packed } from '@/misc/schema.js';
import { MetaService } from '@/core/MetaService.js';
import { NoteEntityService } from '@/core/entities/NoteEntityService.js';
import { bindThis } from '@/decorators.js';
+import { RoleService } from '@/core/RoleService.js';
import Channel from '../channel.js';
class GlobalTimelineChannel extends Channel {
@@ -16,6 +17,7 @@ class GlobalTimelineChannel extends Channel {
constructor(
private metaService: MetaService,
+ private roleService: RoleService,
private noteEntityService: NoteEntityService,
id: string,
@@ -27,10 +29,8 @@ class GlobalTimelineChannel extends Channel {
@bindThis
public async init(params: any) {
- const meta = await this.metaService.fetch();
- if (meta.disableGlobalTimeline) {
- if (this.user == null || (!this.user.isAdmin && !this.user.isModerator)) return;
- }
+ const role = await this.roleService.getUserRoleOptions(this.user ? this.user.id : null);
+ if (!role.gtlAvailable) return;
// Subscribe events
this.subscriber.on('notesStream', this.onNote);
@@ -95,6 +95,7 @@ export class GlobalTimelineChannelService {
constructor(
private metaService: MetaService,
+ private roleService: RoleService,
private noteEntityService: NoteEntityService,
) {
}
@@ -103,6 +104,7 @@ export class GlobalTimelineChannelService {
public create(id: string, connection: Channel['connection']): GlobalTimelineChannel {
return new GlobalTimelineChannel(
this.metaService,
+ this.roleService,
this.noteEntityService,
id,
connection,
diff --git a/packages/backend/src/server/api/stream/channels/hybrid-timeline.ts b/packages/backend/src/server/api/stream/channels/hybrid-timeline.ts
index 6c6afb12bf..a0f75f202c 100644
--- a/packages/backend/src/server/api/stream/channels/hybrid-timeline.ts
+++ b/packages/backend/src/server/api/stream/channels/hybrid-timeline.ts
@@ -8,6 +8,7 @@ import { DI } from '@/di-symbols.js';
import { MetaService } from '@/core/MetaService.js';
import { NoteEntityService } from '@/core/entities/NoteEntityService.js';
import { bindThis } from '@/decorators.js';
+import { RoleService } from '@/core/RoleService.js';
import Channel from '../channel.js';
class HybridTimelineChannel extends Channel {
@@ -17,6 +18,7 @@ class HybridTimelineChannel extends Channel {
constructor(
private metaService: MetaService,
+ private roleService: RoleService,
private noteEntityService: NoteEntityService,
id: string,
@@ -28,8 +30,8 @@ class HybridTimelineChannel extends Channel {
@bindThis
public async init(params: any): Promise<void> {
- const meta = await this.metaService.fetch();
- if (meta.disableLocalTimeline && !this.user!.isAdmin && !this.user!.isModerator) return;
+ const role = await this.roleService.getUserRoleOptions(this.user ? this.user.id : null);
+ if (!role.ltlAvailable) return;
// Subscribe events
this.subscriber.on('notesStream', this.onNote);
@@ -112,6 +114,7 @@ export class HybridTimelineChannelService {
constructor(
private metaService: MetaService,
+ private roleService: RoleService,
private noteEntityService: NoteEntityService,
) {
}
@@ -120,6 +123,7 @@ export class HybridTimelineChannelService {
public create(id: string, connection: Channel['connection']): HybridTimelineChannel {
return new HybridTimelineChannel(
this.metaService,
+ this.roleService,
this.noteEntityService,
id,
connection,
diff --git a/packages/backend/src/server/api/stream/channels/local-timeline.ts b/packages/backend/src/server/api/stream/channels/local-timeline.ts
index 54388787ef..7d76f42fe7 100644
--- a/packages/backend/src/server/api/stream/channels/local-timeline.ts
+++ b/packages/backend/src/server/api/stream/channels/local-timeline.ts
@@ -6,6 +6,7 @@ import type { Packed } from '@/misc/schema.js';
import { MetaService } from '@/core/MetaService.js';
import { NoteEntityService } from '@/core/entities/NoteEntityService.js';
import { bindThis } from '@/decorators.js';
+import { RoleService } from '@/core/RoleService.js';
import Channel from '../channel.js';
class LocalTimelineChannel extends Channel {
@@ -15,6 +16,7 @@ class LocalTimelineChannel extends Channel {
constructor(
private metaService: MetaService,
+ private roleService: RoleService,
private noteEntityService: NoteEntityService,
id: string,
@@ -26,10 +28,8 @@ class LocalTimelineChannel extends Channel {
@bindThis
public async init(params: any) {
- const meta = await this.metaService.fetch();
- if (meta.disableLocalTimeline) {
- if (this.user == null || (!this.user.isAdmin && !this.user.isModerator)) return;
- }
+ const role = await this.roleService.getUserRoleOptions(this.user ? this.user.id : null);
+ if (!role.ltlAvailable) return;
// Subscribe events
this.subscriber.on('notesStream', this.onNote);
@@ -92,6 +92,7 @@ export class LocalTimelineChannelService {
constructor(
private metaService: MetaService,
+ private roleService: RoleService,
private noteEntityService: NoteEntityService,
) {
}
@@ -100,6 +101,7 @@ export class LocalTimelineChannelService {
public create(id: string, connection: Channel['connection']): LocalTimelineChannel {
return new LocalTimelineChannel(
this.metaService,
+ this.roleService,
this.noteEntityService,
id,
connection,
diff --git a/packages/backend/src/server/api/stream/types.ts b/packages/backend/src/server/api/stream/types.ts
index ec05be56ee..3bc844f949 100644
--- a/packages/backend/src/server/api/stream/types.ts
+++ b/packages/backend/src/server/api/stream/types.ts
@@ -14,23 +14,33 @@ import type { Page } from '@/models/entities/Page.js';
import type { Packed } from '@/misc/schema.js';
import type { Webhook } from '@/models/entities/Webhook.js';
import type { Meta } from '@/models/entities/Meta.js';
+import { Role, RoleAssignment } from '@/models';
import type Emitter from 'strict-event-emitter-types';
import type { EventEmitter } from 'events';
+// redis通すとDateのインスタンスはstringに変換されるので
+type Serialized<T> = {
+ [K in keyof T]: T[K] extends Date ? string : T[K];
+};
+
//#region Stream type-body definitions
export interface InternalStreamTypes {
- userChangeSuspendedState: { id: User['id']; isSuspended: User['isSuspended']; };
- userChangeSilencedState: { id: User['id']; isSilenced: User['isSilenced']; };
- userChangeModeratorState: { id: User['id']; isModerator: User['isModerator']; };
- userTokenRegenerated: { id: User['id']; oldToken: User['token']; newToken: User['token']; };
- remoteUserUpdated: { id: User['id']; };
- webhookCreated: Webhook;
- webhookDeleted: Webhook;
- webhookUpdated: Webhook;
- antennaCreated: Antenna;
- antennaDeleted: Antenna;
- antennaUpdated: Antenna;
- metaUpdated: Meta,
+ userChangeSuspendedState: Serialized<{ id: User['id']; isSuspended: User['isSuspended']; }>;
+ userTokenRegenerated: Serialized<{ id: User['id']; oldToken: User['token']; newToken: User['token']; }>;
+ remoteUserUpdated: Serialized<{ id: User['id']; }>;
+ defaultRoleOverrideUpdated: Serialized<Role['options']>;
+ roleCreated: Serialized<Role>;
+ roleDeleted: Serialized<Role>;
+ roleUpdated: Serialized<Role>;
+ userRoleAssigned: Serialized<RoleAssignment>;
+ userRoleUnassigned: Serialized<RoleAssignment>;
+ webhookCreated: Serialized<Webhook>;
+ webhookDeleted: Serialized<Webhook>;
+ webhookUpdated: Serialized<Webhook>;
+ antennaCreated: Serialized<Antenna>;
+ antennaDeleted: Serialized<Antenna>;
+ antennaUpdated: Serialized<Antenna>;
+ metaUpdated: Serialized<Meta>;
}
export interface BroadcastTypes {
diff --git a/packages/backend/src/server/web/ClientServerService.ts b/packages/backend/src/server/web/ClientServerService.ts
index 5c29224019..2a764a25b0 100644
--- a/packages/backend/src/server/web/ClientServerService.ts
+++ b/packages/backend/src/server/web/ClientServerService.ts
@@ -29,6 +29,7 @@ import type { ChannelsRepository, ClipsRepository, EmojisRepository, FlashsRepos
import { deepClone } from '@/misc/clone.js';
import { bindThis } from '@/decorators.js';
import { FlashEntityService } from '@/core/entities/FlashEntityService.js';
+import { RoleService } from '@/core/RoleService.js';
import manifest from './manifest.json' assert { type: 'json' };
import { FeedService } from './FeedService.js';
import { UrlPreviewService } from './UrlPreviewService.js';
@@ -83,6 +84,7 @@ export class ClientServerService {
private metaService: MetaService,
private urlPreviewService: UrlPreviewService,
private feedService: FeedService,
+ private roleService: RoleService,
@Inject('queue:system') public systemQueue: SystemQueue,
@Inject('queue:endedPollNotification') public endedPollNotificationQueue: EndedPollNotificationQueue,
@@ -125,7 +127,12 @@ export class ClientServerService {
throw new Error('login required');
}
const user = await this.usersRepository.findOneBy({ token });
- if (user == null || !(user.isAdmin || user.isModerator)) {
+ if (user == null) {
+ reply.code(403);
+ throw new Error('no such user');
+ }
+ const isAdministrator = await this.roleService.isAdministrator(user);
+ if (!isAdministrator) {
reply.code(403);
throw new Error('access denied');
}
diff --git a/packages/backend/src/server/web/boot.js b/packages/backend/src/server/web/boot.js
index 86df3308ec..e2fc27fecd 100644
--- a/packages/backend/src/server/web/boot.js
+++ b/packages/backend/src/server/web/boot.js
@@ -301,6 +301,10 @@
const meta = await res.json();
+ if (meta.version == null) {
+ throw new Error('failed to fetch instance metadata');
+ }
+
if (meta.version != v) {
localStorage.setItem('v', meta.version);
refresh();