summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--CHANGELOG.md8
-rw-r--r--locales/ja-JP.yml29
-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
-rw-r--r--packages/frontend/src/components/MkRolePreview.vue32
-rw-r--r--packages/frontend/src/components/MkUserCardMini.vue2
-rw-r--r--packages/frontend/src/directives/adaptive-bg.ts24
-rw-r--r--packages/frontend/src/directives/index.ts2
-rw-r--r--packages/frontend/src/pages/admin/index.vue5
-rw-r--r--packages/frontend/src/pages/admin/roles.edit.vue65
-rw-r--r--packages/frontend/src/pages/admin/roles.editor.vue193
-rw-r--r--packages/frontend/src/pages/admin/roles.role.vue121
-rw-r--r--packages/frontend/src/pages/admin/roles.vue115
-rw-r--r--packages/frontend/src/pages/admin/settings.vue35
-rw-r--r--packages/frontend/src/pages/admin/users.vue1
-rw-r--r--packages/frontend/src/pages/timeline.vue4
-rw-r--r--packages/frontend/src/pages/user-info.vue121
-rw-r--r--packages/frontend/src/pages/user/home.vue4
-rw-r--r--packages/frontend/src/router.ts17
-rw-r--r--packages/frontend/src/scripts/get-user-menu.ts32
-rw-r--r--packages/frontend/src/ui/deck/tl-column.vue4
89 files changed, 1999 insertions, 610 deletions
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 066faa7e05..256df462d0 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -12,7 +12,7 @@ You should also include the user name that made the change.
## 13.0.0 (unreleased)
### TL;DR
-- New features (Play, new widgets, new charts, 🍪👈, etc)
+- New features (Role system, Misskey Play, New widgets, New charts, 🍪👈, etc)
- Rewriten backend
- Better performance (backend and frontend)
- Various usability improvements
@@ -27,6 +27,11 @@ You should also include the user name that made the change.
- 代わりに今後任意の検索プロバイダを設定できる仕組みを構想しています。その仕組みを使えば今まで通りElasticsearchも利用できます
- Migrate to Yarn Berry (v3.2.1) @ThatOneCalculator
- You may have to `yarn run clean-all`, `sudo corepack enable` and `yarn set version berry` before running `yarn install` if you're still on yarn classic
+- 従来のモデレーターフラグは廃止され、より高度なロール機能が導入されました
+ - これに伴い、アップデートを行うと全てのモデレーターフラグは失われます。そのため、予めモデレーター一覧を記録しておき、アップデート後にモデレーターロールを作りアサインし直してください。
+ - サイレンスはロールに統合されました
+ - ユーザーごとのドライブ容量設定はロールに統合されました
+ - LTL/GTLの解放状態はロールに統合されました
#### For users
- ノートのウォッチ機能が削除されました
@@ -52,6 +57,7 @@ You should also include the user name that made the change.
- API: `instance`エンティティに`latestStatus`、`lastCommunicatedAt`、`latestRequestSentAt`プロパティが含まれなくなりました
### Improvements
+- Role system @syuilo
- Misskey Play @syuilo
- Introduce retention-rate aggregation @syuilo
- Make possible to export favorited notes @syuilo
diff --git a/locales/ja-JP.yml b/locales/ja-JP.yml
index aac89d8fe4..3dd770c60f 100644
--- a/locales/ja-JP.yml
+++ b/locales/ja-JP.yml
@@ -924,6 +924,35 @@ neverShow: "今後表示しない"
remindMeLater: "また後で"
didYouLikeMisskey: "Misskeyを気に入っていただけましたか?"
pleaseDonate: "Misskeyは{host}が使用している無料のソフトウェアです。これからも開発を続けられるように、ぜひ寄付をお願いします!"
+roles: "ロール"
+role: "ロール"
+noramlUser: "一般ユーザー"
+undefined: "未定義"
+assign: "アサイン"
+unassign: "アサインを解除"
+color: "色"
+
+_role:
+ new: "ロールの作成"
+ edit: "ロールの編集"
+ name: "ロール名"
+ description: "ロールの説明"
+ type: "ロールの種類"
+ descriptionOfType: "モデレーターは基本的なモデレーションに関する操作を行えます。\n管理者はインスタンスの全ての設定を変更できます。"
+ isPublic: "ロールを公開"
+ descriptionOfIsPublic: "ロールにアサインされたユーザーを誰でも見ることができます。また、ユーザーのプロフィールでこのロールが表示されます。"
+ options: "オプション"
+ baseRole: "ベースロール"
+ useBaseValue: "ベースロールの値を使用"
+ chooseRoleToAssign: "アサインするロールを選択"
+ canEditMembersByModerator: "モデレーターのメンバー編集を許可"
+ descriptionOfCanEditMembersByModerator: "オンにすると、管理者に加えてモデレーターもこのロールへユーザーをアサイン/アサイン解除できるようになります。オフにすると管理者のみが行えます。"
+ _options:
+ gtlAvailable: "グローバルタイムラインの閲覧"
+ ltlAvailable: "ローカルタイムラインの閲覧"
+ canPublicNote: "パブリック投稿の許可"
+ driveCapacity: "ドライブ容量"
+ antennaMax: "アンテナの作成可能数"
_sensitiveMediaDetection:
description: "機械学習を使って自動でセンシティブなメディアを検出し、モデレーションに役立てることができます。サーバーの負荷が少し増えます。"
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();
diff --git a/packages/frontend/src/components/MkRolePreview.vue b/packages/frontend/src/components/MkRolePreview.vue
new file mode 100644
index 0000000000..ddd7dbb250
--- /dev/null
+++ b/packages/frontend/src/components/MkRolePreview.vue
@@ -0,0 +1,32 @@
+<template>
+<MkA v-adaptive-bg :to="`/admin/roles/${role.id}`" class="_panel" :class="$style.root" tabindex="-1" :style="{ '--color': role.color }">
+ <div :class="$style.title">{{ role.name }}</div>
+ <div :class="$style.description">{{ role.description }}</div>
+</MkA>
+</template>
+
+<script lang="ts" setup>
+import { } from 'vue';
+import * as misskey from 'misskey-js';
+import * as os from '@/os';
+
+const props = defineProps<{
+ role: any;
+}>();
+</script>
+
+<style lang="scss" module>
+.root {
+ display: block;
+ padding: 16px 20px;
+ border-left: solid 6px var(--color);
+}
+
+.title {
+ font-weight: bold;
+}
+
+.description {
+ opacity: 0.7;
+}
+</style>
diff --git a/packages/frontend/src/components/MkUserCardMini.vue b/packages/frontend/src/components/MkUserCardMini.vue
index 1a4c494987..be8a4c408e 100644
--- a/packages/frontend/src/components/MkUserCardMini.vue
+++ b/packages/frontend/src/components/MkUserCardMini.vue
@@ -1,5 +1,5 @@
<template>
-<div :class="[$style.root, { yellow: user.isSilenced, red: user.isSuspended, gray: false }]">
+<div v-adaptive-bg :class="[$style.root, { yellow: user.isSilenced, red: user.isSuspended, gray: false }]">
<MkAvatar class="avatar" :user="user" :disable-link="true" :show-indicator="true"/>
<div class="body">
<span class="name"><MkUserName class="name" :user="user"/></span>
diff --git a/packages/frontend/src/directives/adaptive-bg.ts b/packages/frontend/src/directives/adaptive-bg.ts
new file mode 100644
index 0000000000..313aad7996
--- /dev/null
+++ b/packages/frontend/src/directives/adaptive-bg.ts
@@ -0,0 +1,24 @@
+import { Directive } from 'vue';
+
+export default {
+ mounted(src, binding, vn) {
+ const getBgColor = (el: HTMLElement) => {
+ const style = window.getComputedStyle(el);
+ if (style.backgroundColor && !['rgba(0, 0, 0, 0)', 'rgba(0,0,0,0)', 'transparent'].includes(style.backgroundColor)) {
+ return style.backgroundColor;
+ } else {
+ return el.parentElement ? getBgColor(el.parentElement) : 'transparent';
+ }
+ };
+
+ const parentBg = getBgColor(src.parentElement);
+
+ const myBg = window.getComputedStyle(src).backgroundColor;
+
+ if (parentBg === myBg) {
+ src.style.backgroundColor = 'var(--bg)';
+ } else {
+ src.style.backgroundColor = myBg;
+ }
+ },
+} as Directive;
diff --git a/packages/frontend/src/directives/index.ts b/packages/frontend/src/directives/index.ts
index 93d1b4f43d..a690fd6c42 100644
--- a/packages/frontend/src/directives/index.ts
+++ b/packages/frontend/src/directives/index.ts
@@ -10,6 +10,7 @@ import anim from './anim';
import clickAnime from './click-anime';
import panel from './panel';
import adaptiveBorder from './adaptive-border';
+import adaptiveBg from './adaptive-bg';
export default function(app: App) {
app.directive('userPreview', userPreview);
@@ -23,4 +24,5 @@ export default function(app: App) {
app.directive('click-anime', clickAnime);
app.directive('panel', panel);
app.directive('adaptive-border', adaptiveBorder);
+ app.directive('adaptive-bg', adaptiveBg);
}
diff --git a/packages/frontend/src/pages/admin/index.vue b/packages/frontend/src/pages/admin/index.vue
index c90a1c1b00..1d0d87e422 100644
--- a/packages/frontend/src/pages/admin/index.vue
+++ b/packages/frontend/src/pages/admin/index.vue
@@ -131,6 +131,11 @@ const menuDef = $computed(() => [{
text: i18n.ts.abuseReports,
to: '/admin/abuses',
active: currentPage?.route.name === 'abuses',
+ }, {
+ icon: 'ti ti-badges',
+ text: i18n.ts.roles,
+ to: '/admin/roles',
+ active: currentPage?.route.name === 'roles',
}],
}, {
title: i18n.ts.settings,
diff --git a/packages/frontend/src/pages/admin/roles.edit.vue b/packages/frontend/src/pages/admin/roles.edit.vue
new file mode 100644
index 0000000000..3cb4e2deb9
--- /dev/null
+++ b/packages/frontend/src/pages/admin/roles.edit.vue
@@ -0,0 +1,65 @@
+<template>
+<div>
+ <MkStickyContainer>
+ <template #header><XHeader :actions="headerActions" :tabs="headerTabs"/></template>
+ <MkSpacer :content-max="600">
+ <XEditor :role="role" @created="created" @updated="updated"/>
+ </MkSpacer>
+ </MkStickyContainer>
+</div>
+</template>
+
+<script lang="ts" setup>
+import { computed } from 'vue';
+import XHeader from './_header_.vue';
+import XEditor from './roles.editor.vue';
+import MkInput from '@/components/MkInput.vue';
+import MkSelect from '@/components/MkSelect.vue';
+import MkTextarea from '@/components/MkTextarea.vue';
+import MkFolder from '@/components/MkFolder.vue';
+import MkSwitch from '@/components/MkSwitch.vue';
+import MkButton from '@/components/MkButton.vue';
+import FormSlot from '@/components/form/slot.vue';
+import * as os from '@/os';
+import { i18n } from '@/i18n';
+import { definePageMetadata } from '@/scripts/page-metadata';
+import { useRouter } from '@/router';
+
+const router = useRouter();
+
+const props = defineProps<{
+ id?: string;
+}>();
+
+let role = $ref(null);
+
+if (props.id) {
+ role = await os.api('admin/roles/show', {
+ roleId: props.id,
+ });
+}
+
+function created(r) {
+ router.push('/admin/roles/' + r.id);
+}
+
+function updated() {
+ router.push('/admin/roles/' + role.id);
+}
+
+const headerActions = $computed(() => []);
+
+const headerTabs = $computed(() => []);
+
+definePageMetadata(computed(() => role ? {
+ title: i18n.ts._role.edit + ': ' + role.name,
+ icon: 'ti ti-badge',
+} : {
+ title: i18n.ts._role.new,
+ icon: 'ti ti-badge',
+}));
+</script>
+
+<style lang="scss" module>
+
+</style>
diff --git a/packages/frontend/src/pages/admin/roles.editor.vue b/packages/frontend/src/pages/admin/roles.editor.vue
new file mode 100644
index 0000000000..b6f0cd9f57
--- /dev/null
+++ b/packages/frontend/src/pages/admin/roles.editor.vue
@@ -0,0 +1,193 @@
+<template>
+<div class="_gaps">
+ <MkInput v-model="name" :readonly="readonly">
+ <template #label>{{ i18n.ts._role.name }}</template>
+ </MkInput>
+
+ <MkTextarea v-model="description" :readonly="readonly">
+ <template #label>{{ i18n.ts._role.description }}</template>
+ </MkTextarea>
+
+ <MkInput v-model="color">
+ <template #label>{{ i18n.ts.color }}</template>
+ <template #caption>#RRGGBB</template>
+ </MkInput>
+
+ <MkSelect v-model="roleType" :readonly="readonly">
+ <template #label>{{ i18n.ts._role.type }}</template>
+ <template #caption><div v-html="i18n.ts._role.descriptionOfType.replaceAll('\n', '<br>')"></div></template>
+ <option value="normal">{{ i18n.ts.noramlUser }}</option>
+ <option value="moderator">{{ i18n.ts.moderator }}</option>
+ <option value="administrator">{{ i18n.ts.administrator }}</option>
+ </MkSelect>
+
+ <FormSlot>
+ <template #label>{{ i18n.ts._role.options }}</template>
+ <div class="_gaps_s">
+ <MkFolder>
+ <template #label>{{ i18n.ts._role._options.gtlAvailable }}</template>
+ <template #suffix>{{ options_gtlAvailable_useDefault ? i18n.ts._role.useBaseValue : (options_gtlAvailable_value ? i18n.ts.yes : i18n.ts.no) }}</template>
+ <div class="_gaps">
+ <MkSwitch v-model="options_gtlAvailable_useDefault" :readonly="readonly">
+ <template #label>{{ i18n.ts._role.useBaseValue }}</template>
+ </MkSwitch>
+ <MkSwitch v-model="options_gtlAvailable_value" :disabled="options_gtlAvailable_useDefault" :readonly="readonly">
+ <template #label>{{ i18n.ts.enable }}</template>
+ </MkSwitch>
+ </div>
+ </MkFolder>
+
+ <MkFolder>
+ <template #label>{{ i18n.ts._role._options.ltlAvailable }}</template>
+ <template #suffix>{{ options_ltlAvailable_useDefault ? i18n.ts._role.useBaseValue : (options_ltlAvailable_value ? i18n.ts.yes : i18n.ts.no) }}</template>
+ <div class="_gaps">
+ <MkSwitch v-model="options_ltlAvailable_useDefault" :readonly="readonly">
+ <template #label>{{ i18n.ts._role.useBaseValue }}</template>
+ </MkSwitch>
+ <MkSwitch v-model="options_ltlAvailable_value" :disabled="options_ltlAvailable_useDefault" :readonly="readonly">
+ <template #label>{{ i18n.ts.enable }}</template>
+ </MkSwitch>
+ </div>
+ </MkFolder>
+
+ <MkFolder>
+ <template #label>{{ i18n.ts._role._options.canPublicNote }}</template>
+ <template #suffix>{{ options_canPublicNote_useDefault ? i18n.ts._role.useBaseValue : (options_canPublicNote_value ? i18n.ts.yes : i18n.ts.no) }}</template>
+ <div class="_gaps">
+ <MkSwitch v-model="options_canPublicNote_useDefault" :readonly="readonly">
+ <template #label>{{ i18n.ts._role.useBaseValue }}</template>
+ </MkSwitch>
+ <MkSwitch v-model="options_canPublicNote_value" :disabled="options_canPublicNote_useDefault" :readonly="readonly">
+ <template #label>{{ i18n.ts.enable }}</template>
+ </MkSwitch>
+ </div>
+ </MkFolder>
+
+ <MkFolder>
+ <template #label>{{ i18n.ts._role._options.driveCapacity }}</template>
+ <template #suffix>{{ options_driveCapacityMb_useDefault ? i18n.ts._role.useBaseValue : (options_driveCapacityMb_value + 'MB') }}</template>
+ <div class="_gaps">
+ <MkSwitch v-model="options_driveCapacityMb_useDefault" :readonly="readonly">
+ <template #label>{{ i18n.ts._role.useBaseValue }}</template>
+ </MkSwitch>
+ <MkInput v-model="options_driveCapacityMb_value" :disabled="options_driveCapacityMb_useDefault" type="number" :readonly="readonly">
+ <template #suffix>MB</template>
+ </MkInput>
+ </div>
+ </MkFolder>
+
+ <MkFolder>
+ <template #label>{{ i18n.ts._role._options.antennaMax }}</template>
+ <template #suffix>{{ options_antennaLimit_useDefault ? i18n.ts._role.useBaseValue : (options_antennaLimit_value) }}</template>
+ <div class="_gaps">
+ <MkSwitch v-model="options_antennaLimit_useDefault" :readonly="readonly">
+ <template #label>{{ i18n.ts._role.useBaseValue }}</template>
+ </MkSwitch>
+ <MkInput v-model="options_antennaLimit_value" :disabled="options_antennaLimit_useDefault" type="number" :readonly="readonly">
+ </MkInput>
+ </div>
+ </MkFolder>
+ </div>
+ </FormSlot>
+
+ <MkSwitch v-model="canEditMembersByModerator" :readonly="readonly">
+ <template #label>{{ i18n.ts._role.canEditMembersByModerator }}</template>
+ <template #caption>{{ i18n.ts._role.descriptionOfCanEditMembersByModerator }}</template>
+ </MkSwitch>
+
+ <MkSwitch v-model="isPublic" :readonly="readonly">
+ <template #label>{{ i18n.ts._role.isPublic }}</template>
+ <template #caption>{{ i18n.ts._role.descriptionOfIsPublic }}</template>
+ </MkSwitch>
+
+ <div v-if="!readonly" class="_buttons">
+ <MkButton primary rounded @click="save"><i class="ti ti-check"></i> {{ role ? i18n.ts.save : i18n.ts.create }}</MkButton>
+ </div>
+</div>
+</template>
+
+<script lang="ts" setup>
+import { computed } from 'vue';
+import MkInput from '@/components/MkInput.vue';
+import MkSelect from '@/components/MkSelect.vue';
+import MkTextarea from '@/components/MkTextarea.vue';
+import MkFolder from '@/components/MkFolder.vue';
+import MkSwitch from '@/components/MkSwitch.vue';
+import MkButton from '@/components/MkButton.vue';
+import FormSlot from '@/components/form/slot.vue';
+import * as os from '@/os';
+import { i18n } from '@/i18n';
+
+const emit = defineEmits<{
+ (ev: 'created', payload: any): void;
+ (ev: 'updated'): void;
+}>();
+
+const props = defineProps<{
+ role?: any;
+ readonly?: boolean;
+}>();
+
+const role = props.role;
+
+let name = $ref(role?.name ?? 'New Role');
+let description = $ref(role?.description ?? '');
+let roleType = $ref(role?.isAdministrator ? 'administrator' : role?.isModerator ? 'moderator' : 'normal');
+let color = $ref(role?.color ?? null);
+let isPublic = $ref(role?.isPublic ?? false);
+let canEditMembersByModerator = $ref(role?.canEditMembersByModerator ?? false);
+let options_gtlAvailable_useDefault = $ref(role?.options?.gtlAvailable?.useDefault ?? true);
+let options_gtlAvailable_value = $ref(role?.options?.gtlAvailable?.value ?? false);
+let options_ltlAvailable_useDefault = $ref(role?.options?.ltlAvailable?.useDefault ?? true);
+let options_ltlAvailable_value = $ref(role?.options?.ltlAvailable?.value ?? false);
+let options_canPublicNote_useDefault = $ref(role?.options?.canPublicNote?.useDefault ?? true);
+let options_canPublicNote_value = $ref(role?.options?.canPublicNote?.value ?? false);
+let options_driveCapacityMb_useDefault = $ref(role?.options?.driveCapacityMb?.useDefault ?? true);
+let options_driveCapacityMb_value = $ref(role?.options?.driveCapacityMb?.value ?? 0);
+let options_antennaLimit_useDefault = $ref(role?.options?.antennaLimit?.useDefault ?? true);
+let options_antennaLimit_value = $ref(role?.options?.antennaLimit?.value ?? 0);
+
+function getOptions() {
+ return {
+ gtlAvailable: { useDefault: options_gtlAvailable_useDefault, value: options_gtlAvailable_value },
+ ltlAvailable: { useDefault: options_ltlAvailable_useDefault, value: options_ltlAvailable_value },
+ canPublicNote: { useDefault: options_canPublicNote_useDefault, value: options_canPublicNote_value },
+ driveCapacityMb: { useDefault: options_driveCapacityMb_useDefault, value: options_driveCapacityMb_value },
+ antennaLimit: { useDefault: options_antennaLimit_useDefault, value: options_antennaLimit_value },
+ };
+}
+
+async function save() {
+ if (props.readonly) return;
+ if (role) {
+ os.apiWithDialog('admin/roles/update', {
+ roleId: role.id,
+ name,
+ description,
+ color: color === '' ? null : color,
+ isAdministrator: roleType === 'administrator',
+ isModerator: roleType === 'moderator',
+ isPublic,
+ canEditMembersByModerator,
+ options: getOptions(),
+ });
+ emit('updated');
+ } else {
+ const created = await os.apiWithDialog('admin/roles/create', {
+ name,
+ description,
+ color: color === '' ? null : color,
+ isAdministrator: roleType === 'administrator',
+ isModerator: roleType === 'moderator',
+ isPublic,
+ canEditMembersByModerator,
+ options: getOptions(),
+ });
+ emit('created', created);
+ }
+}
+</script>
+
+<style lang="scss" module>
+
+</style>
diff --git a/packages/frontend/src/pages/admin/roles.role.vue b/packages/frontend/src/pages/admin/roles.role.vue
new file mode 100644
index 0000000000..8c18b02632
--- /dev/null
+++ b/packages/frontend/src/pages/admin/roles.role.vue
@@ -0,0 +1,121 @@
+<template>
+<div>
+ <MkStickyContainer>
+ <template #header><XHeader :actions="headerActions" :tabs="headerTabs"/></template>
+ <MkSpacer :content-max="700">
+ <div class="_gaps">
+ <div class="_buttons">
+ <MkButton primary rounded @click="edit"><i class="ti ti-pencil"></i> {{ i18n.ts.edit }}</MkButton>
+ <MkButton danger rounded @click="del"><i class="ti ti-trash"></i> {{ i18n.ts.delete }}</MkButton>
+ </div>
+ <MkFolder>
+ <template #icon><i class="ti ti-info-circle"></i></template>
+ <template #label>{{ i18n.ts.info }}</template>
+ <XEditor :role="role" readonly/>
+ </MkFolder>
+ <MkFolder default-open>
+ <template #icon><i class="ti ti-users"></i></template>
+ <template #label>{{ i18n.ts.users }}</template>
+ <template #suffix>{{ role.users.length }}</template>
+ <div class="_gaps">
+ <MkButton primary rounded @click="assign"><i class="ti ti-plus"></i> {{ i18n.ts.assign }}</MkButton>
+
+ <div v-for="user in role.users" :key="user.id" :class="$style.userItem">
+ <MkA :class="$style.user" :to="`/user-info/${user.id}`">
+ <MkUserCardMini :user="user"/>
+ </MkA>
+ <button class="_button" :class="$style.unassign" @click="unassign(user, $event)"><i class="ti ti-x"></i></button>
+ </div>
+ </div>
+ </MkFolder>
+ </div>
+ </MkSpacer>
+ </MkStickyContainer>
+</div>
+</template>
+
+<script lang="ts" setup>
+import { computed, reactive } from 'vue';
+import XHeader from './_header_.vue';
+import XEditor from './roles.editor.vue';
+import MkFolder from '@/components/MkFolder.vue';
+import * as os from '@/os';
+import { i18n } from '@/i18n';
+import { definePageMetadata } from '@/scripts/page-metadata';
+import { useRouter } from '@/router';
+import MkButton from '@/components/MkButton.vue';
+import MkUserCardMini from '@/components/MkUserCardMini.vue';
+
+const router = useRouter();
+
+const props = defineProps<{
+ id?: string;
+}>();
+
+const role = reactive(await os.api('admin/roles/show', {
+ roleId: props.id,
+}));
+
+function edit() {
+ router.push('/admin/roles/' + role.id + '/edit');
+}
+
+async function del() {
+ const { canceled } = await os.confirm({
+ type: 'warning',
+ text: i18n.t('deleteAreYouSure', { x: role.name }),
+ });
+ if (canceled) return;
+
+ await os.apiWithDialog('admin/roles/delete', {
+ roleId: role.id,
+ });
+
+ router.push('/admin/roles');
+}
+
+function assign() {
+ os.selectUser().then(async (user) => {
+ await os.apiWithDialog('admin/roles/assign', { roleId: role.id, userId: user.id });
+ role.users.push(user);
+ });
+}
+
+async function unassign(user, ev) {
+ os.popupMenu([{
+ text: i18n.ts.unassign,
+ icon: 'ti ti-x',
+ danger: true,
+ action: async () => {
+ await os.apiWithDialog('admin/roles/unassign', { roleId: role.id, userId: user.id });
+ role.users = role.users.filter(u => u.id !== user.id);
+ },
+ }], ev.currentTarget ?? ev.target);
+}
+
+const headerActions = $computed(() => []);
+
+const headerTabs = $computed(() => []);
+
+definePageMetadata(computed(() => ({
+ title: i18n.ts.role + ': ' + role.name,
+ icon: 'ti ti-badge',
+})));
+</script>
+
+<style lang="scss" module>
+.userItem {
+ display: flex;
+}
+
+.user {
+ flex: 1;
+}
+
+.unassign {
+ width: 32px;
+ height: 32px;
+ margin-left: 8px;
+ align-self: center;
+}
+</style>
diff --git a/packages/frontend/src/pages/admin/roles.vue b/packages/frontend/src/pages/admin/roles.vue
new file mode 100644
index 0000000000..f74a3dcf5a
--- /dev/null
+++ b/packages/frontend/src/pages/admin/roles.vue
@@ -0,0 +1,115 @@
+<template>
+<div>
+ <MkStickyContainer>
+ <template #header><XHeader :actions="headerActions" :tabs="headerTabs"/></template>
+ <MkSpacer :content-max="700">
+ <div class="_gaps">
+ <MkButton primary rounded @click="create"><i class="ti ti-plus"></i> {{ i18n.ts._role.new }}</MkButton>
+ <MkFolder>
+ <template #label>{{ i18n.ts._role.baseRole }}</template>
+ <div class="_gaps">
+ <MkFolder>
+ <template #label>{{ i18n.ts._role._options.gtlAvailable }}</template>
+ <template #suffix>{{ options_gtlAvailable ? i18n.ts.yes : i18n.ts.no }}</template>
+ <MkSwitch v-model="options_gtlAvailable">
+ <template #label>{{ i18n.ts.enable }}</template>
+ </MkSwitch>
+ </MkFolder>
+
+ <MkFolder>
+ <template #label>{{ i18n.ts._role._options.ltlAvailable }}</template>
+ <template #suffix>{{ options_ltlAvailable ? i18n.ts.yes : i18n.ts.no }}</template>
+ <MkSwitch v-model="options_ltlAvailable">
+ <template #label>{{ i18n.ts.enable }}</template>
+ </MkSwitch>
+ </MkFolder>
+
+ <MkFolder>
+ <template #label>{{ i18n.ts._role._options.canPublicNote }}</template>
+ <template #suffix>{{ options_canPublicNote ? i18n.ts.yes : i18n.ts.no }}</template>
+ <MkSwitch v-model="options_canPublicNote">
+ <template #label>{{ i18n.ts.enable }}</template>
+ </MkSwitch>
+ </MkFolder>
+
+ <MkFolder>
+ <template #label>{{ i18n.ts._role._options.driveCapacity }}</template>
+ <template #suffix>{{ options_driveCapacityMb }}MB</template>
+ <MkInput v-model="options_driveCapacityMb" type="number">
+ <template #suffix>MB</template>
+ </MkInput>
+ </MkFolder>
+
+ <MkFolder>
+ <template #label>{{ i18n.ts._role._options.antennaMax }}</template>
+ <template #suffix>{{ options_antennaLimit }}</template>
+ <MkInput v-model="options_antennaLimit" type="number">
+ </MkInput>
+ </MkFolder>
+ <MkButton primary rounded @click="updateBaseRole">{{ i18n.ts.save }}</MkButton>
+ </div>
+ </MkFolder>
+ <div class="_gaps_s">
+ <MkRolePreview v-for="role in roles" :key="role.id" :role="role"/>
+ </div>
+ </div>
+ </MkSpacer>
+ </MkStickyContainer>
+</div>
+</template>
+
+<script lang="ts" setup>
+import { computed } from 'vue';
+import XHeader from './_header_.vue';
+import MkInput from '@/components/MkInput.vue';
+import MkSelect from '@/components/MkSelect.vue';
+import MkPagination from '@/components/MkPagination.vue';
+import MkFolder from '@/components/MkFolder.vue';
+import MkSwitch from '@/components/MkSwitch.vue';
+import MkButton from '@/components/MkButton.vue';
+import MkRolePreview from '@/components/MkRolePreview.vue';
+import * as os from '@/os';
+import { i18n } from '@/i18n';
+import { definePageMetadata } from '@/scripts/page-metadata';
+import { instance } from '@/instance';
+import { useRouter } from '@/router';
+
+const router = useRouter();
+
+const roles = await os.api('admin/roles/list');
+
+let options_gtlAvailable = $ref(instance.baseRole.gtlAvailable);
+let options_ltlAvailable = $ref(instance.baseRole.ltlAvailable);
+let options_canPublicNote = $ref(instance.baseRole.canPublicNote);
+let options_driveCapacityMb = $ref(instance.baseRole.driveCapacityMb);
+let options_antennaLimit = $ref(instance.baseRole.antennaLimit);
+
+async function updateBaseRole() {
+ await os.apiWithDialog('admin/roles/update-default-role-override', {
+ options: {
+ gtlAvailable: options_gtlAvailable,
+ ltlAvailable: options_ltlAvailable,
+ canPublicNote: options_canPublicNote,
+ driveCapacityMb: options_driveCapacityMb,
+ antennaLimit: options_antennaLimit,
+ },
+ });
+}
+
+function create() {
+ router.push('/admin/roles/new');
+}
+
+const headerActions = $computed(() => []);
+
+const headerTabs = $computed(() => []);
+
+definePageMetadata(computed(() => ({
+ title: i18n.ts.roles,
+ icon: 'ti ti-badges',
+})));
+</script>
+
+<style lang="scss" module>
+
+</style>
diff --git a/packages/frontend/src/pages/admin/settings.vue b/packages/frontend/src/pages/admin/settings.vue
index 844ac62bdb..eae822c7c8 100644
--- a/packages/frontend/src/pages/admin/settings.vue
+++ b/packages/frontend/src/pages/admin/settings.vue
@@ -47,14 +47,6 @@
</FormSection>
<FormSection>
- <div class="_gaps_s">
- <MkSwitch v-model="enableLocalTimeline">{{ i18n.ts.enableLocalTimeline }}</MkSwitch>
- <MkSwitch v-model="enableGlobalTimeline">{{ i18n.ts.enableGlobalTimeline }}</MkSwitch>
- <FormInfo>{{ i18n.ts.disablingTimelinesInfo }}</FormInfo>
- </div>
- </FormSection>
-
- <FormSection>
<template #label>{{ i18n.ts.theme }}</template>
<div class="_gaps_m">
@@ -100,19 +92,11 @@
<template #caption>{{ i18n.ts.cacheRemoteFilesDescription }}</template>
</MkSwitch>
- <FormSplit :min-width="280">
- <MkInput v-model="localDriveCapacityMb" type="number">
- <template #label>{{ i18n.ts.driveCapacityPerLocalAccount }}</template>
- <template #suffix>MB</template>
- <template #caption>{{ i18n.ts.inMb }}</template>
- </MkInput>
-
- <MkInput v-model="remoteDriveCapacityMb" type="number" :disabled="!cacheRemoteFiles">
- <template #label>{{ i18n.ts.driveCapacityPerRemoteAccount }}</template>
- <template #suffix>MB</template>
- <template #caption>{{ i18n.ts.inMb }}</template>
- </MkInput>
- </FormSplit>
+ <MkInput v-model="remoteDriveCapacityMb" type="number" :disabled="!cacheRemoteFiles">
+ <template #label>{{ i18n.ts.driveCapacityPerRemoteAccount }}</template>
+ <template #suffix>MB</template>
+ <template #caption>{{ i18n.ts.inMb }}</template>
+ </MkInput>
</div>
</FormSection>
@@ -185,11 +169,8 @@ let backgroundImageUrl: string | null = $ref(null);
let themeColor: any = $ref(null);
let defaultLightTheme: any = $ref(null);
let defaultDarkTheme: any = $ref(null);
-let enableLocalTimeline: boolean = $ref(false);
-let enableGlobalTimeline: boolean = $ref(false);
let pinnedUsers: string = $ref('');
let cacheRemoteFiles: boolean = $ref(false);
-let localDriveCapacityMb: any = $ref(0);
let remoteDriveCapacityMb: any = $ref(0);
let enableRegistration: boolean = $ref(false);
let emailRequiredForSignup: boolean = $ref(false);
@@ -212,11 +193,8 @@ async function init() {
defaultDarkTheme = meta.defaultDarkTheme;
maintainerName = meta.maintainerName;
maintainerEmail = meta.maintainerEmail;
- enableLocalTimeline = !meta.disableLocalTimeline;
- enableGlobalTimeline = !meta.disableGlobalTimeline;
pinnedUsers = meta.pinnedUsers.join('\n');
cacheRemoteFiles = meta.cacheRemoteFiles;
- localDriveCapacityMb = meta.driveCapacityPerLocalUserMb;
remoteDriveCapacityMb = meta.driveCapacityPerRemoteUserMb;
enableRegistration = !meta.disableRegistration;
emailRequiredForSignup = meta.emailRequiredForSignup;
@@ -240,11 +218,8 @@ function save() {
defaultDarkTheme: defaultDarkTheme === '' ? null : defaultDarkTheme,
maintainerName,
maintainerEmail,
- disableLocalTimeline: !enableLocalTimeline,
- disableGlobalTimeline: !enableGlobalTimeline,
pinnedUsers: pinnedUsers.split('\n'),
cacheRemoteFiles,
- localDriveCapacityMb: parseInt(localDriveCapacityMb, 10),
remoteDriveCapacityMb: parseInt(remoteDriveCapacityMb, 10),
disableRegistration: !enableRegistration,
emailRequiredForSignup,
diff --git a/packages/frontend/src/pages/admin/users.vue b/packages/frontend/src/pages/admin/users.vue
index babe76e4ec..fc1c1c1dc5 100644
--- a/packages/frontend/src/pages/admin/users.vue
+++ b/packages/frontend/src/pages/admin/users.vue
@@ -19,7 +19,6 @@
<option value="available">{{ i18n.ts.normal }}</option>
<option value="admin">{{ i18n.ts.administrator }}</option>
<option value="moderator">{{ i18n.ts.moderator }}</option>
- <option value="silenced">{{ i18n.ts.silence }}</option>
<option value="suspended">{{ i18n.ts.suspend }}</option>
</MkSelect>
<MkSelect v-model="origin" style="flex: 1;">
diff --git a/packages/frontend/src/pages/timeline.vue b/packages/frontend/src/pages/timeline.vue
index c11a302260..5ed4dcddcf 100644
--- a/packages/frontend/src/pages/timeline.vue
+++ b/packages/frontend/src/pages/timeline.vue
@@ -34,8 +34,8 @@ import { definePageMetadata } from '@/scripts/page-metadata';
const XTutorial = defineAsyncComponent(() => import('./timeline.tutorial.vue'));
-const isLocalTimelineAvailable = !instance.disableLocalTimeline || ($i != null && ($i.isModerator || $i.isAdmin));
-const isGlobalTimelineAvailable = !instance.disableGlobalTimeline || ($i != null && ($i.isModerator || $i.isAdmin));
+const isLocalTimelineAvailable = ($i == null && instance.baseRole.ltlAvailable) || ($i != null && $i.role.ltlAvailable);
+const isGlobalTimelineAvailable = ($i == null && instance.baseRole.gtlAvailable) || ($i != null && $i.role.gtlAvailable);
const keymap = {
't': focus,
};
diff --git a/packages/frontend/src/pages/user-info.vue b/packages/frontend/src/pages/user-info.vue
index 3d1742cb22..5bc98a56a7 100644
--- a/packages/frontend/src/pages/user-info.vue
+++ b/packages/frontend/src/pages/user-info.vue
@@ -87,18 +87,26 @@
</FormSection>
</div>
<div v-else-if="tab === 'moderation'" class="_gaps_m">
- <MkSwitch v-if="user.host == null && $i.isAdmin && (moderator || !user.isAdmin)" v-model="moderator" @update:model-value="toggleModerator">{{ i18n.ts.moderator }}</MkSwitch>
- <MkSwitch v-model="silenced" @update:model-value="toggleSilence">{{ i18n.ts.silence }}</MkSwitch>
<MkSwitch v-model="suspended" @update:model-value="toggleSuspend">{{ i18n.ts.suspend }}</MkSwitch>
- {{ i18n.ts.reflectMayTakeTime }}
<div>
<MkButton v-if="user.host == null && iAmModerator" inline style="margin-right: 8px;" @click="resetPassword"><i class="ti ti-key"></i> {{ i18n.ts.resetPassword }}</MkButton>
<MkButton v-if="$i.isAdmin" inline danger @click="deleteAccount">{{ i18n.ts.deleteAccount }}</MkButton>
</div>
- <MkTextarea v-model="moderationNote" manual-save>
- <template #label>Moderation note</template>
- </MkTextarea>
<MkFolder>
+ <template #icon><i class="ti ti-badges"></i></template>
+ <template #label>{{ i18n.ts.roles }}</template>
+
+ <div class="_gaps">
+ <MkButton v-if="user.host == null && iAmModerator" primary rounded @click="assignRole"><i class="ti ti-plus"></i> {{ i18n.ts.assign }}</MkButton>
+
+ <div v-for="role in info.roles" :key="role.id" :class="$style.roleItem">
+ <MkRolePreview :class="$style.role" :role="role"/>
+ <button class="_button" :class="$style.roleUnassign" @click="unassignRole(role, $event)"><i class="ti ti-x"></i></button>
+ </div>
+ </div>
+ </MkFolder>
+ <MkFolder>
+ <template #icon><i class="ti ti-password"></i></template>
<template #label>IP</template>
<MkInfo v-if="!iAmAdmin" warn>{{ i18n.ts.requireAdminForView }}</MkInfo>
<MkInfo v-else>The date is the IP address was first acknowledged.</MkInfo>
@@ -110,21 +118,14 @@
</template>
</MkFolder>
<MkFolder>
+ <template #icon><i class="ti ti-cloud"></i></template>
<template #label>{{ i18n.ts.files }}</template>
<MkFileListForAdmin :pagination="filesPagination" view-mode="grid"/>
</MkFolder>
- <FormSection>
- <template #label>Drive Capacity Override</template>
-
- <MkInput v-if="user.host == null" v-model="driveCapacityOverrideMb" inline :manual-save="true" type="number" :placeholder="i18n.t('defaultValueIs', { value: instance.driveCapacityPerLocalUserMb })" @update:model-value="applyDriveCapacityOverride">
- <template #label>{{ i18n.ts.driveCapOverrideLabel }}</template>
- <template #suffix>MB</template>
- <template #caption>
- {{ i18n.ts.driveCapOverrideCaption }}
- </template>
- </MkInput>
- </FormSection>
+ <MkTextarea v-model="moderationNote" manual-save>
+ <template #label>Moderation note</template>
+ </MkTextarea>
</div>
<div v-else-if="tab === 'chart'" class="_gaps_m">
<div class="cmhjzshm">
@@ -180,12 +181,16 @@ import { definePageMetadata } from '@/scripts/page-metadata';
import { i18n } from '@/i18n';
import { iAmAdmin, iAmModerator } from '@/account';
import { instance } from '@/instance';
+import MkRolePreview from '@/components/MkRolePreview.vue';
-const props = defineProps<{
+const props = withDefaults(defineProps<{
userId: string;
-}>();
+ initialTab?: string;
+}>(), {
+ initialTab: 'overview',
+});
-let tab = $ref('overview');
+let tab = $ref(props.initialTab);
let chartSrc = $ref('per-user-notes');
let user = $ref<null | misskey.entities.UserDetailed>();
let init = $ref<ReturnType<typeof createFetcher>>();
@@ -195,7 +200,6 @@ let ap = $ref(null);
let moderator = $ref(false);
let silenced = $ref(false);
let suspended = $ref(false);
-let driveCapacityOverrideMb: number | null = $ref(0);
let moderationNote = $ref('');
const filesPagination = {
endpoint: 'admin/drive/files' as const,
@@ -220,7 +224,6 @@ function createFetcher() {
moderator = info.isModerator;
silenced = info.isSilenced;
suspended = info.isSuspended;
- driveCapacityOverrideMb = user.driveCapacityOverrideMb;
moderationNote = info.moderationNote;
watch($$(moderationNote), async () => {
@@ -257,19 +260,6 @@ async function resetPassword() {
});
}
-async function toggleSilence(v) {
- const confirm = await os.confirm({
- type: 'warning',
- text: v ? i18n.ts.silenceConfirm : i18n.ts.unsilenceConfirm,
- });
- if (confirm.canceled) {
- silenced = !v;
- } else {
- await os.api(v ? 'admin/silence-user' : 'admin/unsilence-user', { userId: user.id });
- await refreshUser();
- }
-}
-
async function toggleSuspend(v) {
const confirm = await os.confirm({
type: 'warning',
@@ -283,11 +273,6 @@ async function toggleSuspend(v) {
}
}
-async function toggleModerator(v) {
- await os.api(v ? 'admin/moderators/add' : 'admin/moderators/remove', { userId: user.id });
- await refreshUser();
-}
-
async function deleteAllFiles() {
const confirm = await os.confirm({
type: 'warning',
@@ -307,22 +292,6 @@ async function deleteAllFiles() {
await refreshUser();
}
-async function applyDriveCapacityOverride() {
- let driveCapOrMb = driveCapacityOverrideMb;
- if (driveCapacityOverrideMb && driveCapacityOverrideMb < 0) {
- driveCapOrMb = null;
- }
- try {
- await os.apiWithDialog('admin/drive-capacity-override', { userId: user.id, overrideMb: driveCapOrMb });
- await refreshUser();
- } catch (err) {
- os.alert({
- type: 'error',
- text: err.toString(),
- });
- }
-}
-
async function deleteAccount() {
const confirm = await os.confirm({
type: 'warning',
@@ -347,6 +316,31 @@ async function deleteAccount() {
}
}
+async function assignRole() {
+ const roles = await os.api('admin/roles/list');
+
+ const { canceled, result: roleId } = await os.select({
+ title: i18n.ts._role.chooseRoleToAssign,
+ items: roles.map(r => ({ text: r.name, value: r.id })),
+ });
+ if (canceled) return;
+
+ await os.apiWithDialog('admin/roles/assign', { roleId, userId: user.id });
+ refreshUser();
+}
+
+async function unassignRole(role, ev) {
+ os.popupMenu([{
+ text: i18n.ts.unassign,
+ icon: 'ti ti-x',
+ danger: true,
+ action: async () => {
+ await os.apiWithDialog('admin/roles/unassign', { roleId: role.id, userId: user.id });
+ refreshUser();
+ },
+ }], ev.currentTarget ?? ev.target);
+}
+
watch(() => props.userId, () => {
init = createFetcher();
}, {
@@ -484,4 +478,19 @@ definePageMetadata(computed(() => ({
margin-left: auto;
}
}
+
+.roleItem {
+ display: flex;
+}
+
+.role {
+ flex: 1;
+}
+
+.roleUnassign {
+ width: 32px;
+ height: 32px;
+ margin-left: 8px;
+ align-self: center;
+}
</style>
diff --git a/packages/frontend/src/pages/user/home.vue b/packages/frontend/src/pages/user/home.vue
index 07d34a794d..eea4d20094 100644
--- a/packages/frontend/src/pages/user/home.vue
+++ b/packages/frontend/src/pages/user/home.vue
@@ -18,7 +18,6 @@
<div class="bottom">
<span class="username"><MkAcct :user="user" :detail="true"/></span>
<span v-if="user.isAdmin" :title="i18n.ts.isAdmin" style="color: var(--badge);"><i class="ti ti-shield"></i></span>
- <span v-if="!user.isAdmin && user.isModerator" :title="i18n.ts.isModerator" style="color: var(--badge);"><i class="ti ti-shield"></i></span>
<span v-if="user.isLocked" :title="i18n.ts.isLocked"><i class="ti ti-lock"></i></span>
<span v-if="user.isBot" :title="i18n.ts.isBot"><i class="ti ti-robot"></i></span>
</div>
@@ -35,7 +34,6 @@
<div class="bottom">
<span class="username"><MkAcct :user="user" :detail="true"/></span>
<span v-if="user.isAdmin" :title="i18n.ts.isAdmin" style="color: var(--badge);"><i class="ti ti-shield"></i></span>
- <span v-if="!user.isAdmin && user.isModerator" :title="i18n.ts.isModerator" style="color: var(--badge);"><i class="ti ti-shield"></i></span>
<span v-if="user.isLocked" :title="i18n.ts.isLocked"><i class="ti ti-lock"></i></span>
<span v-if="user.isBot" :title="i18n.ts.isBot"><i class="ti ti-robot"></i></span>
</div>
@@ -189,7 +187,7 @@ onMounted(() => {
const bd = parseInt(props.user.birthday.split('-')[2]);
if (m === bm && d === bd) {
confetti({
- duration: 1000 * 4
+ duration: 1000 * 4,
});
}
}
diff --git a/packages/frontend/src/router.ts b/packages/frontend/src/router.ts
index 4b9f49f8fd..05dcd7806e 100644
--- a/packages/frontend/src/router.ts
+++ b/packages/frontend/src/router.ts
@@ -37,6 +37,7 @@ export const routes = [{
}, {
path: '/user-info/:userId',
component: page(() => import('./pages/user-info.vue')),
+ hash: 'initialTab',
}, {
path: '/instance-info/:host',
component: page(() => import('./pages/instance-info.vue')),
@@ -352,6 +353,22 @@ export const routes = [{
name: 'ads',
component: page(() => import('./pages/admin/ads.vue')),
}, {
+ path: '/roles/:id/edit',
+ name: 'roles',
+ component: page(() => import('./pages/admin/roles.edit.vue')),
+ }, {
+ path: '/roles/new',
+ name: 'roles',
+ component: page(() => import('./pages/admin/roles.edit.vue')),
+ }, {
+ path: '/roles/:id',
+ name: 'roles',
+ component: page(() => import('./pages/admin/roles.role.vue')),
+ }, {
+ path: '/roles',
+ name: 'roles',
+ component: page(() => import('./pages/admin/roles.vue')),
+ }, {
path: '/database',
name: 'database',
component: page(() => import('./pages/admin/database.vue')),
diff --git a/packages/frontend/src/scripts/get-user-menu.ts b/packages/frontend/src/scripts/get-user-menu.ts
index 7ede64c327..74bd61fd78 100644
--- a/packages/frontend/src/scripts/get-user-menu.ts
+++ b/packages/frontend/src/scripts/get-user-menu.ts
@@ -108,26 +108,6 @@ export function getUserMenu(user, router: Router = mainRouter) {
});
}
- async function toggleSilence() {
- if (!await getConfirmed(i18n.t(user.isSilenced ? 'unsilenceConfirm' : 'silenceConfirm'))) return;
-
- os.apiWithDialog(user.isSilenced ? 'admin/unsilence-user' : 'admin/silence-user', {
- userId: user.id,
- }).then(() => {
- user.isSilenced = !user.isSilenced;
- });
- }
-
- async function toggleSuspend() {
- if (!await getConfirmed(i18n.t(user.isSuspended ? 'unsuspendConfirm' : 'suspendConfirm'))) return;
-
- os.apiWithDialog(user.isSuspended ? 'admin/unsuspend-user' : 'admin/suspend-user', {
- userId: user.id,
- }).then(() => {
- user.isSuspended = !user.isSuspended;
- });
- }
-
function reportAbuse() {
os.popup(defineAsyncComponent(() => import('@/components/MkAbuseReportWindow.vue')), {
user: user,
@@ -218,13 +198,11 @@ export function getUserMenu(user, router: Router = mainRouter) {
if (iAmModerator) {
menu = menu.concat([null, {
- icon: 'ti ti-microphone-2-off',
- text: user.isSilenced ? i18n.ts.unsilence : i18n.ts.silence,
- action: toggleSilence,
- }, {
- icon: 'ti ti-snowflake',
- text: user.isSuspended ? i18n.ts.unsuspend : i18n.ts.suspend,
- action: toggleSuspend,
+ icon: 'ti ti-user-exclamation',
+ text: i18n.ts.moderation,
+ action: () => {
+ router.push('/user-info/' + user.id + '#moderation');
+ },
}]);
}
}
diff --git a/packages/frontend/src/ui/deck/tl-column.vue b/packages/frontend/src/ui/deck/tl-column.vue
index f75e526939..b8a0a504a3 100644
--- a/packages/frontend/src/ui/deck/tl-column.vue
+++ b/packages/frontend/src/ui/deck/tl-column.vue
@@ -45,9 +45,7 @@ onMounted(() => {
if (props.column.tl == null) {
setType();
} else if ($i) {
- disabled = !$i.isModerator && !$i.isAdmin && (
- instance.disableLocalTimeline && ['local', 'social'].includes(props.column.tl) ||
- instance.disableGlobalTimeline && ['global'].includes(props.column.tl));
+ disabled = false; // TODO
}
});