diff options
| author | syuilo <Syuilotan@yahoo.co.jp> | 2023-01-12 21:02:26 +0900 |
|---|---|---|
| committer | GitHub <noreply@github.com> | 2023-01-12 21:02:26 +0900 |
| commit | 2470afaa2e200fb2fc748e0f8eef5e2c215369b6 (patch) | |
| tree | c270452679996127a9d15c4ba5f97b39bb9ba560 /packages/backend/src/server/api/endpoints/admin | |
| parent | Update CHANGELOG.md (diff) | |
| download | sharkey-2470afaa2e200fb2fc748e0f8eef5e2c215369b6.tar.gz sharkey-2470afaa2e200fb2fc748e0f8eef5e2c215369b6.tar.bz2 sharkey-2470afaa2e200fb2fc748e0f8eef5e2c215369b6.zip | |
Role (#9437)
* wip
* Update CHANGELOG.md
* wip
* wip
* wip
* Update create.ts
* wip
* wip
* Update CHANGELOG.md
* wip
* wip
* wip
* wip
* wip
* wip
* wip
* Update CHANGELOG.md
* wip
* wip
* Update delete.ts
* Update delete.ts
* wip
* wip
* wip
* Update account-info.vue
* wip
* wip
* Update settings.vue
* Update user-info.vue
* wip
* Update show-file.ts
* Update show-user.ts
* wip
* wip
* Update delete.ts
* wip
* wip
* Update overview.moderators.vue
* Create 1673500412259-Role.js
* wip
* wip
* Update roles.vue
* 色
* Update roles.vue
* integrate silence
* wip
* wip
Diffstat (limited to 'packages/backend/src/server/api/endpoints/admin')
25 files changed, 569 insertions, 288 deletions
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; } |