From 65382dc70b4aa930421afea5a098c8e96a7ab48b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E3=81=BE=E3=81=A3=E3=81=A1=E3=82=83=E3=81=A8=E3=83=BC?= =?UTF-8?q?=E3=81=AB=E3=82=85?= <17376330+u1-liquid@users.noreply.github.com> Date: Tue, 23 Jan 2024 20:37:14 +0900 Subject: [PATCH] =?UTF-8?q?feat(role):=20=E3=83=AA=E3=83=A2=E3=83=BC?= =?UTF-8?q?=E3=83=88=E3=81=AE=E3=82=A2=E3=82=A4=E3=82=B3=E3=83=B3=E3=81=A8?= =?UTF-8?q?=E3=83=90=E3=83=8A=E3=83=BC=E3=81=AE=E5=A4=89=E6=9B=B4=E3=82=92?= =?UTF-8?q?=E3=83=AD=E3=83=BC=E3=83=AB=E3=81=A7=E5=88=B6=E9=99=90=E3=81=A7?= =?UTF-8?q?=E3=81=8D=E3=82=8B=E3=82=88=E3=81=86=E3=81=AB=20(MisskeyIO#374)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- locales/en-US.yml | 2 + locales/index.d.ts | 8 ++++ locales/ja-JP.yml | 2 + locales/ko-KR.yml | 5 +++ packages/backend/src/core/RoleService.ts | 6 +++ .../activitypub/models/ApPersonService.ts | 7 +++- .../src/server/api/endpoints/i/update.ts | 11 +++-- packages/frontend/src/const.ts | 2 + .../frontend/src/pages/admin/roles.editor.vue | 40 +++++++++++++++++++ packages/frontend/src/pages/admin/roles.vue | 16 ++++++++ 10 files changed, 94 insertions(+), 5 deletions(-) diff --git a/locales/en-US.yml b/locales/en-US.yml index 9caac50466..8c437712f5 100644 --- a/locales/en-US.yml +++ b/locales/en-US.yml @@ -1620,6 +1620,8 @@ _role: canCreateContent: "Can create contents" canUpdateContent: "Can edit contents" canDeleteContent: "Can delete contents" + canUpdateAvatar: "Can change avatar" + canUpdateBanner: "Can change banner" canInvite: "Can create instance invite codes" inviteLimit: "Invite limit" inviteLimitCycle: "Invite limit cooldown" diff --git a/locales/index.d.ts b/locales/index.d.ts index c5a4849b0f..77b9d3d848 100644 --- a/locales/index.d.ts +++ b/locales/index.d.ts @@ -6456,6 +6456,14 @@ export interface Locale extends ILocale { * コンテンツの削除 */ "canDeleteContent": string; + /** + * アイコンの変更 + */ + "canUpdateAvatar": string; + /** + * バナーの変更 + */ + "canUpdateBanner": string; /** * サーバー招待コードの発行 */ diff --git a/locales/ja-JP.yml b/locales/ja-JP.yml index 7988f60abb..609d2a8fe8 100644 --- a/locales/ja-JP.yml +++ b/locales/ja-JP.yml @@ -1671,6 +1671,8 @@ _role: canCreateContent: "コンテンツの作成" canUpdateContent: "コンテンツの編集" canDeleteContent: "コンテンツの削除" + canUpdateAvatar: "アイコンの変更" + canUpdateBanner: "バナーの変更" canInvite: "サーバー招待コードの発行" inviteLimit: "招待コードの作成可能数" inviteLimitCycle: "招待コードの発行間隔" diff --git a/locales/ko-KR.yml b/locales/ko-KR.yml index a1a7ee3ec0..dccb7af305 100644 --- a/locales/ko-KR.yml +++ b/locales/ko-KR.yml @@ -1650,6 +1650,11 @@ _role: gtlAvailable: "글로벌 타임라인 보이기" ltlAvailable: "로컬 타임라인 보이기" canPublicNote: "공개 노트 허용" + canCreateContent: "컨텐츠 생성 허용" + canUpdateContent: "컨텐츠 수정 허용" + canDeleteContent: "컨텐츠 삭제 허용" + canUpdateAvatar: "아바타 변경 허용" + canUpdateBanner: "배너 변경 허용" canInvite: "서버 초대 코드 발행" inviteLimit: "초대 한도" inviteLimitCycle: "초대 발급 간격" diff --git a/packages/backend/src/core/RoleService.ts b/packages/backend/src/core/RoleService.ts index 1e1db2523a..b0e2cfbfae 100644 --- a/packages/backend/src/core/RoleService.ts +++ b/packages/backend/src/core/RoleService.ts @@ -39,6 +39,8 @@ export type RolePolicies = { canCreateContent: boolean; canUpdateContent: boolean; canDeleteContent: boolean; + canUpdateAvatar: boolean; + canUpdateBanner: boolean; canInvite: boolean; inviteLimit: number; inviteLimitCycle: number; @@ -70,6 +72,8 @@ export const DEFAULT_POLICIES: RolePolicies = { canCreateContent: true, canUpdateContent: true, canDeleteContent: true, + canUpdateAvatar: true, + canUpdateBanner: true, canInvite: false, inviteLimit: 0, inviteLimitCycle: 60 * 24 * 7, @@ -337,6 +341,8 @@ export class RoleService implements OnApplicationShutdown, OnModuleInit { canCreateContent: calc('canCreateContent', vs => vs.some(v => v === true)), canUpdateContent: calc('canUpdateContent', vs => vs.some(v => v === true)), canDeleteContent: calc('canDeleteContent', vs => vs.some(v => v === true)), + canUpdateAvatar: calc('canUpdateAvatar', vs => vs.some(v => v === true)), + canUpdateBanner: calc('canUpdateBanner', vs => vs.some(v => v === true)), canInvite: calc('canInvite', vs => vs.some(v => v === true)), inviteLimit: calc('inviteLimit', vs => Math.max(...vs)), inviteLimitCycle: calc('inviteLimitCycle', vs => Math.max(...vs)), diff --git a/packages/backend/src/core/activitypub/models/ApPersonService.ts b/packages/backend/src/core/activitypub/models/ApPersonService.ts index b1becbabab..772c792817 100644 --- a/packages/backend/src/core/activitypub/models/ApPersonService.ts +++ b/packages/backend/src/core/activitypub/models/ApPersonService.ts @@ -20,6 +20,7 @@ import type Logger from '@/logger.js'; import type { MiNote } from '@/models/Note.js'; import type { IdService } from '@/core/IdService.js'; import type { MfmService } from '@/core/MfmService.js'; +import type { RoleService } from '@/core/RoleService.js'; import { toArray } from '@/misc/prelude/array.js'; import type { GlobalEventService } from '@/core/GlobalEventService.js'; import type { FederatedInstanceService } from '@/core/FederatedInstanceService.js'; @@ -75,6 +76,7 @@ export class ApPersonService implements OnModuleInit { private instanceChart: InstanceChart; private apLoggerService: ApLoggerService; private accountMoveService: AccountMoveService; + private roleService: RoleService; private logger: Logger; constructor( @@ -123,6 +125,7 @@ export class ApPersonService implements OnModuleInit { this.instanceChart = this.moduleRef.get('InstanceChart'); this.apLoggerService = this.moduleRef.get('ApLoggerService'); this.accountMoveService = this.moduleRef.get('AccountMoveService'); + this.roleService = this.moduleRef.get('RoleService'); this.logger = this.apLoggerService.logger; } @@ -462,6 +465,8 @@ export class ApPersonService implements OnModuleInit { throw new Error('unexpected schema of person url: ' + url); } + const policy = await this.roleService.getUserPolicies(exist.id); + const updates = { lastFetchedAt: new Date(), inbox: person.inbox, @@ -477,7 +482,7 @@ export class ApPersonService implements OnModuleInit { movedToUri: person.movedTo ?? null, alsoKnownAs: person.alsoKnownAs ?? null, isExplorable: person.discoverable, - ...(await this.resolveAvatarAndBanner(exist, person.icon, person.image).catch(() => ({}))), + ...((policy.canUpdateAvatar || policy.canUpdateBanner) ? await this.resolveAvatarAndBanner(exist, policy.canUpdateAvatar ? person.icon : exist.avatarUrl, policy.canUpdateBanner ? person.image : exist.bannerUrl).catch(() => ({})) : {}), } as Partial & Pick; const moving = ((): boolean => { diff --git a/packages/backend/src/server/api/endpoints/i/update.ts b/packages/backend/src/server/api/endpoints/i/update.ts index 1ee02bde31..d8ecd78420 100644 --- a/packages/backend/src/server/api/endpoints/i/update.ts +++ b/packages/backend/src/server/api/endpoints/i/update.ts @@ -233,6 +233,7 @@ export default class extends Endpoint { // eslint- const updates = {} as Partial; const profileUpdates = {} as Partial; + const policy = await this.roleService.getUserPolicies(user.id); const profile = await this.userProfilesRepository.findOneByOrFail({ userId: user.id }); @@ -245,7 +246,7 @@ export default class extends Endpoint { // eslint- if (ps.followersVisibility !== undefined) profileUpdates.followersVisibility = ps.followersVisibility; if (ps.mutedWords !== undefined) { const length = ps.mutedWords.length; - if (length > (await this.roleService.getUserPolicies(user.id)).wordMuteLimit) { + if (length > policy.wordMuteLimit) { throw new ApiError(meta.errors.tooManyMutedWords); } @@ -279,13 +280,14 @@ export default class extends Endpoint { // eslint- if (typeof ps.injectFeaturedNote === 'boolean') profileUpdates.injectFeaturedNote = ps.injectFeaturedNote; if (typeof ps.receiveAnnouncementEmail === 'boolean') profileUpdates.receiveAnnouncementEmail = ps.receiveAnnouncementEmail; if (typeof ps.alwaysMarkNsfw === 'boolean') { - if ((await roleService.getUserPolicies(user.id)).alwaysMarkNsfw) throw new ApiError(meta.errors.restrictedByRole); + if (policy.alwaysMarkNsfw) throw new ApiError(meta.errors.restrictedByRole); profileUpdates.alwaysMarkNsfw = ps.alwaysMarkNsfw; } if (typeof ps.autoSensitive === 'boolean') profileUpdates.autoSensitive = ps.autoSensitive; if (ps.emailNotificationTypes !== undefined) profileUpdates.emailNotificationTypes = ps.emailNotificationTypes; if (ps.avatarId) { + if (!policy.canUpdateAvatar) throw new ApiError(meta.errors.restrictedByRole); const avatar = await this.driveFilesRepository.findOneBy({ id: ps.avatarId }); if (avatar == null || avatar.userId !== user.id) throw new ApiError(meta.errors.noSuchAvatar); @@ -301,6 +303,7 @@ export default class extends Endpoint { // eslint- } if (ps.bannerId) { + if (!policy.canUpdateBanner) throw new ApiError(meta.errors.restrictedByRole); const banner = await this.driveFilesRepository.findOneBy({ id: ps.bannerId }); if (banner == null || banner.userId !== user.id) throw new ApiError(meta.errors.noSuchBanner); @@ -317,13 +320,13 @@ export default class extends Endpoint { // eslint- if (ps.avatarDecorations) { const decorations = await this.avatarDecorationService.getAll(true); - const [myRoles, myPolicies] = await Promise.all([this.roleService.getUserRoles(user.id), this.roleService.getUserPolicies(user.id)]); + const myRoles = await this.roleService.getUserRoles(user.id); const allRoles = await this.roleService.getRoles(); const decorationIds = decorations .filter(d => d.roleIdsThatCanBeUsedThisDecoration.filter(roleId => allRoles.some(r => r.id === roleId)).length === 0 || myRoles.some(r => d.roleIdsThatCanBeUsedThisDecoration.includes(r.id))) .map(d => d.id); - if (ps.avatarDecorations.length > myPolicies.avatarDecorationLimit) throw new ApiError(meta.errors.restrictedByRole); + if (ps.avatarDecorations.length > policy.avatarDecorationLimit) throw new ApiError(meta.errors.restrictedByRole); updates.avatarDecorations = ps.avatarDecorations.filter(d => decorationIds.includes(d.id)).map(d => ({ id: d.id, diff --git a/packages/frontend/src/const.ts b/packages/frontend/src/const.ts index 90d33da928..55f4dbc37e 100644 --- a/packages/frontend/src/const.ts +++ b/packages/frontend/src/const.ts @@ -78,6 +78,8 @@ export const ROLE_POLICIES = [ 'canCreateContent', 'canUpdateContent', 'canDeleteContent', + 'canUpdateAvatar', + 'canUpdateBanner', 'canInvite', 'inviteLimit', 'inviteLimitCycle', diff --git a/packages/frontend/src/pages/admin/roles.editor.vue b/packages/frontend/src/pages/admin/roles.editor.vue index cd8a492f67..d9aa671bd4 100644 --- a/packages/frontend/src/pages/admin/roles.editor.vue +++ b/packages/frontend/src/pages/admin/roles.editor.vue @@ -220,6 +220,46 @@ SPDX-License-Identifier: AGPL-3.0-only + + + +
+ + + + + + + + + +
+
+ + + + +
+ + + + + + + + + +
+
+