feat: ロールによるコンテンツの操作の制限 (#120)

This commit is contained in:
まっちゃとーにゅ 2023-07-28 04:21:59 +09:00 committed by GitHub
parent 0bed053b7d
commit 46f8a0435c
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
91 changed files with 228 additions and 11 deletions

View file

@ -1404,6 +1404,9 @@ _role:
gtlAvailable: "Can view the global timeline" gtlAvailable: "Can view the global timeline"
ltlAvailable: "Can view the local timeline" ltlAvailable: "Can view the local timeline"
canPublicNote: "Can send public notes" canPublicNote: "Can send public notes"
canCreateContent: "Can create contents"
canUpdateContent: "Can edit contents"
canDeleteContent: "Can delete contents"
canInvite: "Can create instance invite codes" canInvite: "Can create instance invite codes"
inviteLimit: "Invite limit" inviteLimit: "Invite limit"
inviteLimitCycle: "Invite limit cooldown" inviteLimitCycle: "Invite limit cooldown"

3
locales/index.d.ts vendored
View file

@ -1490,6 +1490,9 @@ export interface Locale {
"gtlAvailable": string; "gtlAvailable": string;
"ltlAvailable": string; "ltlAvailable": string;
"canPublicNote": string; "canPublicNote": string;
"canCreateContent": string;
"canUpdateContent": string;
"canDeleteContent": string;
"canInvite": string; "canInvite": string;
"inviteLimit": string; "inviteLimit": string;
"inviteLimitCycle": string; "inviteLimitCycle": string;

View file

@ -1412,6 +1412,9 @@ _role:
gtlAvailable: "グローバルタイムラインの閲覧" gtlAvailable: "グローバルタイムラインの閲覧"
ltlAvailable: "ローカルタイムラインの閲覧" ltlAvailable: "ローカルタイムラインの閲覧"
canPublicNote: "パブリック投稿の許可" canPublicNote: "パブリック投稿の許可"
canCreateContent: "コンテンツの作成"
canUpdateContent: "コンテンツの編集"
canDeleteContent: "コンテンツの削除"
canInvite: "サーバー招待コードの発行" canInvite: "サーバー招待コードの発行"
inviteLimit: "招待コードの作成可能数" inviteLimit: "招待コードの作成可能数"
inviteLimitCycle: "招待コードの発行間隔" inviteLimitCycle: "招待コードの発行間隔"

View file

@ -20,6 +20,9 @@ export type RolePolicies = {
gtlAvailable: boolean; gtlAvailable: boolean;
ltlAvailable: boolean; ltlAvailable: boolean;
canPublicNote: boolean; canPublicNote: boolean;
canCreateContent: boolean;
canUpdateContent: boolean;
canDeleteContent: boolean;
canInvite: boolean; canInvite: boolean;
inviteLimit: number; inviteLimit: number;
inviteLimitCycle: number; inviteLimitCycle: number;
@ -44,6 +47,9 @@ export const DEFAULT_POLICIES: RolePolicies = {
gtlAvailable: true, gtlAvailable: true,
ltlAvailable: true, ltlAvailable: true,
canPublicNote: true, canPublicNote: true,
canCreateContent: true,
canUpdateContent: true,
canDeleteContent: true,
canInvite: false, canInvite: false,
inviteLimit: 0, inviteLimit: 0,
inviteLimitCycle: 60 * 24 * 7, inviteLimitCycle: 60 * 24 * 7,
@ -287,6 +293,9 @@ export class RoleService implements OnApplicationShutdown {
gtlAvailable: calc('gtlAvailable', vs => vs.some(v => v === true)), gtlAvailable: calc('gtlAvailable', vs => vs.some(v => v === true)),
ltlAvailable: calc('ltlAvailable', vs => vs.some(v => v === true)), ltlAvailable: calc('ltlAvailable', vs => vs.some(v => v === true)),
canPublicNote: calc('canPublicNote', vs => vs.some(v => v === true)), canPublicNote: calc('canPublicNote', vs => vs.some(v => v === true)),
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)),
canInvite: calc('canInvite', vs => vs.some(v => v === true)), canInvite: calc('canInvite', vs => vs.some(v => v === true)),
inviteLimit: calc('inviteLimit', vs => Math.max(...vs)), inviteLimit: calc('inviteLimit', vs => Math.max(...vs)),
inviteLimitCycle: calc('inviteLimitCycle', vs => Math.max(...vs)), inviteLimitCycle: calc('inviteLimitCycle', vs => Math.max(...vs)),

View file

@ -351,8 +351,9 @@ export class UserEntityService implements OnModuleInit {
(profile.ffVisibility === 'followers') && (relation && relation.isFollowing) ? user.followersCount : (profile.ffVisibility === 'followers') && (relation && relation.isFollowing) ? user.followersCount :
null; null;
const isModerator = isMe && opts.detail ? this.roleService.isModerator(user) : null; const isModerator = isMe && opts.detail ? await this.roleService.isModerator(user) : null;
const isAdmin = isMe && opts.detail ? this.roleService.isAdministrator(user) : null; const isAdmin = isMe && opts.detail ? await this.roleService.isAdministrator(user) : null;
const policies = opts.detail ? await this.roleService.getUserPolicies(user.id) : null;
const falsy = opts.detail ? false : undefined; const falsy = opts.detail ? false : undefined;
@ -396,7 +397,8 @@ export class UserEntityService implements OnModuleInit {
bannerUrl: user.bannerUrl, bannerUrl: user.bannerUrl,
bannerBlurhash: user.bannerBlurhash, bannerBlurhash: user.bannerBlurhash,
isLocked: user.isLocked, isLocked: user.isLocked,
isSilenced: this.roleService.getUserPolicies(user.id).then(r => !r.canPublicNote), isSilenced: !policies?.canPublicNote,
isLimited: !(policies?.canCreateContent && policies.canUpdateContent && policies.canDeleteContent),
isSuspended: user.isSuspended ?? falsy, isSuspended: user.isSuspended ?? falsy,
description: profile!.description, description: profile!.description,
location: profile!.location, location: profile!.location,
@ -473,7 +475,7 @@ export class UserEntityService implements OnModuleInit {
emailNotificationTypes: profile!.emailNotificationTypes, emailNotificationTypes: profile!.emailNotificationTypes,
achievements: profile!.achievements, achievements: profile!.achievements,
loggedInDays: profile!.loggedInDates.length, loggedInDays: profile!.loggedInDates.length,
policies: this.roleService.getUserPolicies(user.id), policies: policies,
} : {}), } : {}),
...(opts.includeSecrets ? { ...(opts.includeSecrets ? {

View file

@ -121,6 +121,10 @@ export const packedUserDetailedNotMeOnlySchema = {
type: 'boolean', type: 'boolean',
nullable: false, optional: false, nullable: false, optional: false,
}, },
isLimited: {
type: 'boolean',
nullable: false, optional: false,
},
isSuspended: { isSuspended: {
type: 'boolean', type: 'boolean',
nullable: false, optional: false, nullable: false, optional: false,

View file

@ -51,8 +51,10 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
throw new Error('user not found'); throw new Error('user not found');
} }
const policies = await this.roleService.getUserPolicies(user.id);
const isModerator = await this.roleService.isModerator(user); const isModerator = await this.roleService.isModerator(user);
const isSilenced = !(await this.roleService.getUserPolicies(user.id)).canPublicNote; const isLimited = !(policies.canCreateContent && policies.canUpdateContent && policies.canDeleteContent);
const isSilenced = !policies.canPublicNote;
const _me = await this.usersRepository.findOneByOrFail({ id: me.id }); const _me = await this.usersRepository.findOneByOrFail({ id: me.id });
if (!await this.roleService.isAdministrator(_me) && await this.roleService.isAdministrator(user)) { if (!await this.roleService.isAdministrator(_me) && await this.roleService.isAdministrator(user)) {
@ -80,6 +82,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
mutingNotificationTypes: profile.mutingNotificationTypes, mutingNotificationTypes: profile.mutingNotificationTypes,
isModerator: isModerator, isModerator: isModerator,
isSilenced: isSilenced, isSilenced: isSilenced,
isLimited: isLimited,
isSuspended: user.isSuspended, isSuspended: user.isSuspended,
lastActiveDate: user.lastActiveDate, lastActiveDate: user.lastActiveDate,
moderationNote: profile.moderationNote ?? '', moderationNote: profile.moderationNote ?? '',

View file

@ -12,6 +12,7 @@ export const meta = {
tags: ['antennas'], tags: ['antennas'],
requireCredential: true, requireCredential: true,
requireRolePolicy: 'canCreateContent',
prohibitMoved: true, prohibitMoved: true,

View file

@ -9,6 +9,7 @@ export const meta = {
tags: ['antennas'], tags: ['antennas'],
requireCredential: true, requireCredential: true,
requireRolePolicy: 'canDeleteContent',
kind: 'write:account', kind: 'write:account',

View file

@ -10,6 +10,7 @@ export const meta = {
tags: ['antennas'], tags: ['antennas'],
requireCredential: true, requireCredential: true,
requireRolePolicy: 'canUpdateContent',
prohibitMoved: true, prohibitMoved: true,

View file

@ -17,6 +17,7 @@ export const meta = {
}, },
requireCredential: true, requireCredential: true,
requireRolePolicy: 'canUpdateContent',
kind: 'write:blocks', kind: 'write:blocks',

View file

@ -17,6 +17,7 @@ export const meta = {
}, },
requireCredential: true, requireCredential: true,
requireRolePolicy: 'canUpdateContent',
kind: 'write:blocks', kind: 'write:blocks',

View file

@ -12,6 +12,7 @@ export const meta = {
tags: ['channels'], tags: ['channels'],
requireCredential: true, requireCredential: true,
requireRolePolicy: 'canCreateContent',
prohibitMoved: true, prohibitMoved: true,

View file

@ -9,6 +9,7 @@ export const meta = {
tags: ['channels'], tags: ['channels'],
requireCredential: true, requireCredential: true,
requireRolePolicy: 'canUpdateContent',
prohibitMoved: true, prohibitMoved: true,

View file

@ -10,6 +10,7 @@ export const meta = {
tags: ['channels'], tags: ['channels'],
requireCredential: true, requireCredential: true,
requireRolePolicy: 'canUpdateContent',
prohibitMoved: true, prohibitMoved: true,

View file

@ -8,6 +8,7 @@ export const meta = {
tags: ['channels'], tags: ['channels'],
requireCredential: true, requireCredential: true,
requireRolePolicy: 'canUpdateContent',
prohibitMoved: true, prohibitMoved: true,

View file

@ -9,6 +9,7 @@ export const meta = {
tags: ['channels'], tags: ['channels'],
requireCredential: true, requireCredential: true,
requireRolePolicy: 'canUpdateContent',
prohibitMoved: true, prohibitMoved: true,

View file

@ -10,6 +10,7 @@ export const meta = {
tags: ['channels'], tags: ['channels'],
requireCredential: true, requireCredential: true,
requireRolePolicy: 'canUpdateContent',
kind: 'write:channels', kind: 'write:channels',

View file

@ -14,6 +14,7 @@ export const meta = {
requireCredential: true, requireCredential: true,
prohibitMoved: true, prohibitMoved: true,
requireRolePolicy: 'canUpdateContent',
kind: 'write:account', kind: 'write:account',

View file

@ -11,6 +11,7 @@ export const meta = {
tags: ['clips'], tags: ['clips'],
requireCredential: true, requireCredential: true,
requireRolePolicy: 'canCreateContent',
prohibitMoved: true, prohibitMoved: true,

View file

@ -8,6 +8,7 @@ export const meta = {
tags: ['clips'], tags: ['clips'],
requireCredential: true, requireCredential: true,
requireRolePolicy: 'canDeleteContent',
kind: 'write:account', kind: 'write:account',

View file

@ -9,6 +9,7 @@ export const meta = {
tags: ['account', 'notes', 'clips'], tags: ['account', 'notes', 'clips'],
requireCredential: true, requireCredential: true,
requireRolePolicy: 'canUpdateContent',
prohibitMoved: true, prohibitMoved: true,

View file

@ -8,6 +8,7 @@ export const meta = {
tags: ['clip'], tags: ['clip'],
requireCredential: true, requireCredential: true,
requireRolePolicy: 'canUpdateContent',
prohibitMoved: true, prohibitMoved: true,

View file

@ -9,6 +9,7 @@ export const meta = {
tags: ['clips'], tags: ['clips'],
requireCredential: true, requireCredential: true,
requireRolePolicy: 'canUpdateContent',
prohibitMoved: true, prohibitMoved: true,

View file

@ -14,6 +14,7 @@ export const meta = {
tags: ['drive'], tags: ['drive'],
requireCredential: true, requireCredential: true,
requireRolePolicy: 'canCreateContent',
prohibitMoved: true, prohibitMoved: true,

View file

@ -11,6 +11,7 @@ export const meta = {
tags: ['drive'], tags: ['drive'],
requireCredential: true, requireCredential: true,
requireRolePolicy: 'canDeleteContent',
kind: 'write:drive', kind: 'write:drive',

View file

@ -11,6 +11,7 @@ export const meta = {
tags: ['drive'], tags: ['drive'],
requireCredential: true, requireCredential: true,
requireRolePolicy: 'canUpdateContent',
kind: 'write:drive', kind: 'write:drive',

View file

@ -18,6 +18,7 @@ export const meta = {
description: 'Request the server to download a new drive file from the specified URL.', description: 'Request the server to download a new drive file from the specified URL.',
requireCredential: true, requireCredential: true,
requireRolePolicy: 'canCreateContent',
prohibitMoved: true, prohibitMoved: true,

View file

@ -12,6 +12,7 @@ export const meta = {
tags: ['drive'], tags: ['drive'],
requireCredential: true, requireCredential: true,
requireRolePolicy: 'canCreateContent',
kind: 'write:drive', kind: 'write:drive',

View file

@ -9,6 +9,7 @@ export const meta = {
tags: ['drive'], tags: ['drive'],
requireCredential: true, requireCredential: true,
requireRolePolicy: 'canDeleteContent',
kind: 'write:drive', kind: 'write:drive',

View file

@ -10,6 +10,7 @@ export const meta = {
tags: ['drive'], tags: ['drive'],
requireCredential: true, requireCredential: true,
requireRolePolicy: 'canUpdateContent',
kind: 'write:drive', kind: 'write:drive',

View file

@ -10,6 +10,7 @@ export const meta = {
tags: ['flash'], tags: ['flash'],
requireCredential: true, requireCredential: true,
requireRolePolicy: 'canCreateContent',
prohibitMoved: true, prohibitMoved: true,

View file

@ -8,6 +8,7 @@ export const meta = {
tags: ['flashs'], tags: ['flashs'],
requireCredential: true, requireCredential: true,
requireRolePolicy: 'canDeleteContent',
kind: 'write:flash', kind: 'write:flash',

View file

@ -9,6 +9,7 @@ export const meta = {
tags: ['flash'], tags: ['flash'],
requireCredential: true, requireCredential: true,
requireRolePolicy: 'canUpdateContent',
prohibitMoved: true, prohibitMoved: true,

View file

@ -8,6 +8,7 @@ export const meta = {
tags: ['flash'], tags: ['flash'],
requireCredential: true, requireCredential: true,
requireRolePolicy: 'canUpdateContent',
prohibitMoved: true, prohibitMoved: true,

View file

@ -9,6 +9,7 @@ export const meta = {
tags: ['flash'], tags: ['flash'],
requireCredential: true, requireCredential: true,
requireRolePolicy: 'canUpdateContent',
prohibitMoved: true, prohibitMoved: true,

View file

@ -12,6 +12,7 @@ export const meta = {
tags: ['gallery'], tags: ['gallery'],
requireCredential: true, requireCredential: true,
requireRolePolicy: 'canCreateContent',
prohibitMoved: true, prohibitMoved: true,

View file

@ -8,6 +8,7 @@ export const meta = {
tags: ['gallery'], tags: ['gallery'],
requireCredential: true, requireCredential: true,
requireRolePolicy: 'canDeleteContent',
kind: 'write:gallery', kind: 'write:gallery',

View file

@ -9,6 +9,7 @@ export const meta = {
tags: ['gallery'], tags: ['gallery'],
requireCredential: true, requireCredential: true,
requireRolePolicy: 'canUpdateContent',
prohibitMoved: true, prohibitMoved: true,

View file

@ -8,6 +8,7 @@ export const meta = {
tags: ['gallery'], tags: ['gallery'],
requireCredential: true, requireCredential: true,
requireRolePolicy: 'canUpdateContent',
prohibitMoved: true, prohibitMoved: true,

View file

@ -10,6 +10,7 @@ export const meta = {
tags: ['gallery'], tags: ['gallery'],
requireCredential: true, requireCredential: true,
requireRolePolicy: 'canUpdateContent',
prohibitMoved: true, prohibitMoved: true,

View file

@ -7,6 +7,7 @@ import { DI } from '@/di-symbols.js';
export const meta = { export const meta = {
requireCredential: true, requireCredential: true,
requireRolePolicy: 'canDeleteContent',
secure: true, secure: true,
} as const; } as const;

View file

@ -11,6 +11,8 @@ import { ApiError } from '../../error.js';
export const meta = { export const meta = {
secure: true, secure: true,
requireCredential: true, requireCredential: true,
requireRolePolicy: 'canCreateContent',
prohibitMoved: true, prohibitMoved: true,
limit: { limit: {

View file

@ -10,6 +10,8 @@ import { ApiError } from '../../error.js';
export const meta = { export const meta = {
secure: true, secure: true,
requireCredential: true, requireCredential: true,
requireRolePolicy: 'canUpdateContent',
prohibitMoved: true, prohibitMoved: true,
limit: { limit: {

View file

@ -10,6 +10,8 @@ import { ApiError } from '../../error.js';
export const meta = { export const meta = {
secure: true, secure: true,
requireCredential: true, requireCredential: true,
requireRolePolicy: 'canUpdateContent',
prohibitMoved: true, prohibitMoved: true,
limit: { limit: {

View file

@ -10,7 +10,10 @@ import { ApiError } from '../../error.js';
export const meta = { export const meta = {
secure: true, secure: true,
requireCredential: true, requireCredential: true,
requireRolePolicy: 'canCreateContent',
prohibitMoved: true, prohibitMoved: true,
limit: { limit: {
duration: ms('1hour'), duration: ms('1hour'),
max: 1, max: 1,

View file

@ -23,7 +23,10 @@ export const meta = {
secure: true, secure: true,
requireCredential: true, requireCredential: true,
requireRolePolicy: 'canUpdateContent',
prohibitMoved: true, prohibitMoved: true,
limit: { limit: {
duration: ms('1day'), duration: ms('1day'),
max: 5, max: 5,

View file

@ -8,6 +8,8 @@ export const meta = {
tags: ['account', 'notes'], tags: ['account', 'notes'],
requireCredential: true, requireCredential: true,
requireRolePolicy: 'canUpdateContent',
prohibitMoved: true, prohibitMoved: true,
kind: 'write:account', kind: 'write:account',

View file

@ -8,6 +8,7 @@ export const meta = {
tags: ['account', 'notes'], tags: ['account', 'notes'],
requireCredential: true, requireCredential: true,
requireRolePolicy: 'canUpdateContent',
kind: 'write:account', kind: 'write:account',

View file

@ -12,10 +12,11 @@ import { L_CHARS, secureRndstr } from '@/misc/secure-rndstr.js';
import { ApiError } from '../../error.js'; import { ApiError } from '../../error.js';
export const meta = { export const meta = {
requireCredential: true,
secure: true, secure: true,
requireCredential: true,
requireRolePolicy: 'canUpdateContent',
limit: { limit: {
duration: ms('1hour'), duration: ms('1hour'),
max: 3, max: 3,

View file

@ -30,6 +30,7 @@ export const meta = {
tags: ['account'], tags: ['account'],
requireCredential: true, requireCredential: true,
requireRolePolicy: 'canUpdateContent',
kind: 'write:account', kind: 'write:account',

View file

@ -12,6 +12,7 @@ export const meta = {
tags: ['webhooks'], tags: ['webhooks'],
requireCredential: true, requireCredential: true,
requireRolePolicy: 'canCreateContent',
kind: 'write:account', kind: 'write:account',

View file

@ -9,6 +9,7 @@ export const meta = {
tags: ['webhooks'], tags: ['webhooks'],
requireCredential: true, requireCredential: true,
requireRolePolicy: 'canDeleteContent',
kind: 'write:account', kind: 'write:account',

View file

@ -10,6 +10,7 @@ export const meta = {
tags: ['webhooks'], tags: ['webhooks'],
requireCredential: true, requireCredential: true,
requireRolePolicy: 'canUpdateContent',
kind: 'write:account', kind: 'write:account',

View file

@ -11,6 +11,8 @@ export const meta = {
tags: ['account'], tags: ['account'],
requireCredential: true, requireCredential: true,
requireRolePolicy: 'canUpdateContent',
prohibitMoved: true, prohibitMoved: true,
kind: 'write:mutes', kind: 'write:mutes',

View file

@ -10,6 +10,7 @@ export const meta = {
tags: ['account'], tags: ['account'],
requireCredential: true, requireCredential: true,
requireRolePolicy: 'canUpdateContent',
kind: 'write:mutes', kind: 'write:mutes',

View file

@ -17,6 +17,7 @@ export const meta = {
tags: ['notes'], tags: ['notes'],
requireCredential: true, requireCredential: true,
requireRolePolicy: 'canCreateContent',
prohibitMoved: true, prohibitMoved: true,

View file

@ -12,6 +12,7 @@ export const meta = {
tags: ['notes'], tags: ['notes'],
requireCredential: true, requireCredential: true,
requireRolePolicy: 'canDeleteContent',
kind: 'write:notes', kind: 'write:notes',

View file

@ -12,6 +12,8 @@ export const meta = {
tags: ['notes', 'favorites'], tags: ['notes', 'favorites'],
requireCredential: true, requireCredential: true,
requireRolePolicy: 'canUpdateContent',
prohibitMoved: true, prohibitMoved: true,
kind: 'write:favorites', kind: 'write:favorites',

View file

@ -9,6 +9,7 @@ export const meta = {
tags: ['notes', 'favorites'], tags: ['notes', 'favorites'],
requireCredential: true, requireCredential: true,
requireRolePolicy: 'canUpdateContent',
kind: 'write:favorites', kind: 'write:favorites',

View file

@ -16,6 +16,7 @@ export const meta = {
tags: ['notes'], tags: ['notes'],
requireCredential: true, requireCredential: true,
requireRolePolicy: 'canUpdateContent',
prohibitMoved: true, prohibitMoved: true,

View file

@ -8,6 +8,7 @@ export const meta = {
tags: ['reactions', 'notes'], tags: ['reactions', 'notes'],
requireCredential: true, requireCredential: true,
requireRolePolicy: 'canUpdateContent',
prohibitMoved: true, prohibitMoved: true,

View file

@ -9,6 +9,7 @@ export const meta = {
tags: ['reactions', 'notes'], tags: ['reactions', 'notes'],
requireCredential: true, requireCredential: true,
requireRolePolicy: 'canUpdateContent',
kind: 'write:reactions', kind: 'write:reactions',

View file

@ -12,6 +12,7 @@ export const meta = {
tags: ['notes'], tags: ['notes'],
requireCredential: true, requireCredential: true,
requireRolePolicy: 'canUpdateContent',
kind: 'write:account', kind: 'write:account',

View file

@ -9,6 +9,7 @@ export const meta = {
tags: ['notes'], tags: ['notes'],
requireCredential: true, requireCredential: true,
requireRolePolicy: 'canUpdateContent',
kind: 'write:account', kind: 'write:account',

View file

@ -11,6 +11,7 @@ export const meta = {
tags: ['notes'], tags: ['notes'],
requireCredential: true, requireCredential: true,
requireRolePolicy: 'canDeleteContent',
kind: 'write:notes', kind: 'write:notes',

View file

@ -12,6 +12,7 @@ export const meta = {
tags: ['pages'], tags: ['pages'],
requireCredential: true, requireCredential: true,
requireRolePolicy: 'canCreateContent',
prohibitMoved: true, prohibitMoved: true,

View file

@ -8,6 +8,7 @@ export const meta = {
tags: ['pages'], tags: ['pages'],
requireCredential: true, requireCredential: true,
requireRolePolicy: 'canDeleteContent',
kind: 'write:pages', kind: 'write:pages',

View file

@ -9,6 +9,7 @@ export const meta = {
tags: ['pages'], tags: ['pages'],
requireCredential: true, requireCredential: true,
requireRolePolicy: 'canUpdateContent',
prohibitMoved: true, prohibitMoved: true,

View file

@ -8,6 +8,7 @@ export const meta = {
tags: ['pages'], tags: ['pages'],
requireCredential: true, requireCredential: true,
requireRolePolicy: 'canUpdateContent',
prohibitMoved: true, prohibitMoved: true,

View file

@ -10,6 +10,7 @@ export const meta = {
tags: ['pages'], tags: ['pages'],
requireCredential: true, requireCredential: true,
requireRolePolicy: 'canUpdateContent',
prohibitMoved: true, prohibitMoved: true,

View file

@ -13,6 +13,8 @@ export const meta = {
tags: ['account'], tags: ['account'],
requireCredential: true, requireCredential: true,
requireRolePolicy: 'canUpdateContent',
prohibitMoved: true, prohibitMoved: true,
kind: 'write:mutes', kind: 'write:mutes',

View file

@ -10,6 +10,7 @@ export const meta = {
tags: ['account'], tags: ['account'],
requireCredential: true, requireCredential: true,
requireRolePolicy: 'canUpdateContent',
kind: 'write:mutes', kind: 'write:mutes',

View file

@ -12,7 +12,10 @@ import { UserListService } from '@/core/UserListService.js';
export const meta = { export const meta = {
requireCredential: true, requireCredential: true,
requireRolePolicy: 'canCreateContent',
prohibitMoved: true, prohibitMoved: true,
res: { res: {
type: 'object', type: 'object',
optional: false, nullable: false, optional: false, nullable: false,

View file

@ -12,6 +12,7 @@ export const meta = {
tags: ['lists'], tags: ['lists'],
requireCredential: true, requireCredential: true,
requireRolePolicy: 'canCreateContent',
prohibitMoved: true, prohibitMoved: true,

View file

@ -8,6 +8,7 @@ export const meta = {
tags: ['lists'], tags: ['lists'],
requireCredential: true, requireCredential: true,
requireRolePolicy: 'canDeleteContent',
kind: 'write:account', kind: 'write:account',

View file

@ -7,6 +7,8 @@ import { DI } from '@/di-symbols.js';
export const meta = { export const meta = {
requireCredential: true, requireCredential: true,
requireRolePolicy: 'canUpdateContent',
errors: { errors: {
noSuchList: { noSuchList: {
message: 'No such user list.', message: 'No such user list.',

View file

@ -11,6 +11,7 @@ export const meta = {
tags: ['lists', 'users'], tags: ['lists', 'users'],
requireCredential: true, requireCredential: true,
requireRolePolicy: 'canUpdateContent',
prohibitMoved: true, prohibitMoved: true,

View file

@ -11,6 +11,7 @@ export const meta = {
tags: ['lists', 'users'], tags: ['lists', 'users'],
requireCredential: true, requireCredential: true,
requireRolePolicy: 'canUpdateContent',
prohibitMoved: true, prohibitMoved: true,

View file

@ -6,6 +6,8 @@ import { DI } from '@/di-symbols.js';
export const meta = { export const meta = {
requireCredential: true, requireCredential: true,
requireRolePolicy: 'canUpdateContent',
errors: { errors: {
noSuchList: { noSuchList: {
message: 'No such user list.', message: 'No such user list.',

View file

@ -9,6 +9,7 @@ export const meta = {
tags: ['lists'], tags: ['lists'],
requireCredential: true, requireCredential: true,
requireRolePolicy: 'canUpdateContent',
kind: 'write:account', kind: 'write:account',

View file

@ -10,6 +10,7 @@ export const meta = {
tags: ['account'], tags: ['account'],
requireCredential: true, requireCredential: true,
requireRolePolicy: 'canUpdateContent',
kind: 'write:account', kind: 'write:account',

View file

@ -91,6 +91,7 @@ describe('ユーザー', () => {
bannerBlurhash: user.bannerBlurhash, bannerBlurhash: user.bannerBlurhash,
isLocked: user.isLocked, isLocked: user.isLocked,
isSilenced: user.isSilenced, isSilenced: user.isSilenced,
isLimited: user.isLimited,
isSuspended: user.isSuspended, isSuspended: user.isSuspended,
description: user.description, description: user.description,
location: user.location, location: user.location,
@ -356,6 +357,7 @@ describe('ユーザー', () => {
assert.strictEqual(response.bannerBlurhash, null); assert.strictEqual(response.bannerBlurhash, null);
assert.strictEqual(response.isLocked, false); assert.strictEqual(response.isLocked, false);
assert.strictEqual(response.isSilenced, false); assert.strictEqual(response.isSilenced, false);
assert.strictEqual(response.isLimited, false);
assert.strictEqual(response.isSuspended, false); assert.strictEqual(response.isSuspended, false);
assert.strictEqual(response.description, null); assert.strictEqual(response.description, null);
assert.strictEqual(response.location, null); assert.strictEqual(response.location, null);

View file

@ -99,6 +99,7 @@ export function userDetailed(id = 'someuserid', username = 'miskist', host = 'mi
isModerator: false, isModerator: false,
isMuted: false, isMuted: false,
isSilenced: false, isSilenced: false,
isLimited: false,
isSuspended: false, isSuspended: false,
lang: 'en', lang: 'en',
location: 'Fediverse', location: 'Fediverse',

View file

@ -1,5 +1,5 @@
<template> <template>
<div v-adaptive-bg :class="[$style.root, { yellow: user.isSilenced, red: user.isSuspended, gray: false }]"> <div v-adaptive-bg :class="[$style.root, { yellow: user.isSilenced, gray: user.isLimited, red: user.isSuspended }]">
<MkAvatar class="avatar" :user="user" indicator/> <MkAvatar class="avatar" :user="user" indicator/>
<div class="body"> <div class="body">
<span class="name"><MkUserName class="name" :user="user"/></span> <span class="name"><MkUserName class="name" :user="user"/></span>

View file

@ -56,6 +56,9 @@ export const ROLE_POLICIES = [
'gtlAvailable', 'gtlAvailable',
'ltlAvailable', 'ltlAvailable',
'canPublicNote', 'canPublicNote',
'canCreateContent',
'canUpdateContent',
'canDeleteContent',
'canInvite', 'canInvite',
'inviteLimit', 'inviteLimit',
'inviteLimitCycle', 'inviteLimitCycle',

View file

@ -155,6 +155,66 @@
</div> </div>
</MkFolder> </MkFolder>
<MkFolder v-if="matchQuery([i18n.ts._role._options.canCreateContent, 'canCreateContent'])">
<template #label>{{ i18n.ts._role._options.canCreateContent }}</template>
<template #suffix>
<span v-if="role.policies.canCreateContent.useDefault" :class="$style.useDefaultLabel">{{ i18n.ts._role.useBaseValue }}</span>
<span v-else>{{ role.policies.canCreateContent.value ? i18n.ts.yes : i18n.ts.no }}</span>
<span :class="$style.priorityIndicator"><i :class="getPriorityIcon(role.policies.canCreateContent)"></i></span>
</template>
<div class="_gaps">
<MkSwitch v-model="role.policies.canCreateContent.useDefault" :readonly="readonly">
<template #label>{{ i18n.ts._role.useBaseValue }}</template>
</MkSwitch>
<MkSwitch v-model="role.policies.canCreateContent.value" :disabled="role.policies.canCreateContent.useDefault" :readonly="readonly">
<template #label>{{ i18n.ts.enable }}</template>
</MkSwitch>
<MkRange v-model="role.policies.canCreateContent.priority" :min="0" :max="2" :step="1" easing :textConverter="(v) => v === 0 ? i18n.ts._role._priority.low : v === 1 ? i18n.ts._role._priority.middle : v === 2 ? i18n.ts._role._priority.high : ''">
<template #label>{{ i18n.ts._role.priority }}</template>
</MkRange>
</div>
</MkFolder>
<MkFolder v-if="matchQuery([i18n.ts._role._options.canUpdateContent, 'canUpdateContent'])">
<template #label>{{ i18n.ts._role._options.canUpdateContent }}</template>
<template #suffix>
<span v-if="role.policies.canUpdateContent.useDefault" :class="$style.useDefaultLabel">{{ i18n.ts._role.useBaseValue }}</span>
<span v-else>{{ role.policies.canUpdateContent.value ? i18n.ts.yes : i18n.ts.no }}</span>
<span :class="$style.priorityIndicator"><i :class="getPriorityIcon(role.policies.canUpdateContent)"></i></span>
</template>
<div class="_gaps">
<MkSwitch v-model="role.policies.canUpdateContent.useDefault" :readonly="readonly">
<template #label>{{ i18n.ts._role.useBaseValue }}</template>
</MkSwitch>
<MkSwitch v-model="role.policies.canUpdateContent.value" :disabled="role.policies.canUpdateContent.useDefault" :readonly="readonly">
<template #label>{{ i18n.ts.enable }}</template>
</MkSwitch>
<MkRange v-model="role.policies.canUpdateContent.priority" :min="0" :max="2" :step="1" easing :textConverter="(v) => v === 0 ? i18n.ts._role._priority.low : v === 1 ? i18n.ts._role._priority.middle : v === 2 ? i18n.ts._role._priority.high : ''">
<template #label>{{ i18n.ts._role.priority }}</template>
</MkRange>
</div>
</MkFolder>
<MkFolder v-if="matchQuery([i18n.ts._role._options.canDeleteContent, 'canDeleteContent'])">
<template #label>{{ i18n.ts._role._options.canDeleteContent }}</template>
<template #suffix>
<span v-if="role.policies.canDeleteContent.useDefault" :class="$style.useDefaultLabel">{{ i18n.ts._role.useBaseValue }}</span>
<span v-else>{{ role.policies.canDeleteContent.value ? i18n.ts.yes : i18n.ts.no }}</span>
<span :class="$style.priorityIndicator"><i :class="getPriorityIcon(role.policies.canDeleteContent)"></i></span>
</template>
<div class="_gaps">
<MkSwitch v-model="role.policies.canDeleteContent.useDefault" :readonly="readonly">
<template #label>{{ i18n.ts._role.useBaseValue }}</template>
</MkSwitch>
<MkSwitch v-model="role.policies.canDeleteContent.value" :disabled="role.policies.canDeleteContent.useDefault" :readonly="readonly">
<template #label>{{ i18n.ts.enable }}</template>
</MkSwitch>
<MkRange v-model="role.policies.canDeleteContent.priority" :min="0" :max="2" :step="1" easing :textConverter="(v) => v === 0 ? i18n.ts._role._priority.low : v === 1 ? i18n.ts._role._priority.middle : v === 2 ? i18n.ts._role._priority.high : ''">
<template #label>{{ i18n.ts._role.priority }}</template>
</MkRange>
</div>
</MkFolder>
<MkFolder v-if="matchQuery([i18n.ts._role._options.canInvite, 'canInvite'])"> <MkFolder v-if="matchQuery([i18n.ts._role._options.canInvite, 'canInvite'])">
<template #label>{{ i18n.ts._role._options.canInvite }}</template> <template #label>{{ i18n.ts._role._options.canInvite }}</template>
<template #suffix> <template #suffix>

View file

@ -43,6 +43,30 @@
</MkSwitch> </MkSwitch>
</MkFolder> </MkFolder>
<MkFolder v-if="matchQuery([i18n.ts._role._options.canCreateContent, 'canCreateContent'])">
<template #label>{{ i18n.ts._role._options.canCreateContent }}</template>
<template #suffix>{{ policies.canCreateContent ? i18n.ts.yes : i18n.ts.no }}</template>
<MkSwitch v-model="policies.canCreateContent">
<template #label>{{ i18n.ts.enable }}</template>
</MkSwitch>
</MkFolder>
<MkFolder v-if="matchQuery([i18n.ts._role._options.canUpdateContent, 'canUpdateContent'])">
<template #label>{{ i18n.ts._role._options.canUpdateContent }}</template>
<template #suffix>{{ policies.canUpdateContent ? i18n.ts.yes : i18n.ts.no }}</template>
<MkSwitch v-model="policies.canUpdateContent">
<template #label>{{ i18n.ts.enable }}</template>
</MkSwitch>
</MkFolder>
<MkFolder v-if="matchQuery([i18n.ts._role._options.canDeleteContent, 'canDeleteContent'])">
<template #label>{{ i18n.ts._role._options.canDeleteContent }}</template>
<template #suffix>{{ policies.canDeleteContent ? i18n.ts.yes : i18n.ts.no }}</template>
<MkSwitch v-model="policies.canDeleteContent">
<template #label>{{ i18n.ts.enable }}</template>
</MkSwitch>
</MkFolder>
<MkFolder v-if="matchQuery([i18n.ts._role._options.canInvite, 'canInvite'])"> <MkFolder v-if="matchQuery([i18n.ts._role._options.canInvite, 'canInvite'])">
<template #label>{{ i18n.ts._role._options.canInvite }}</template> <template #label>{{ i18n.ts._role._options.canInvite }}</template>
<template #suffix>{{ policies.canInvite ? i18n.ts.yes : i18n.ts.no }}</template> <template #suffix>{{ policies.canInvite ? i18n.ts.yes : i18n.ts.no }}</template>

View file

@ -11,6 +11,7 @@
<span class="sub"><span class="acct _monospace">@{{ acct(user) }}</span></span> <span class="sub"><span class="acct _monospace">@{{ acct(user) }}</span></span>
<span class="state"> <span class="state">
<span v-if="suspended" class="suspended">Suspended</span> <span v-if="suspended" class="suspended">Suspended</span>
<span v-if="limited" class="limited">Limited</span>
<span v-if="silenced" class="silenced">Silenced</span> <span v-if="silenced" class="silenced">Silenced</span>
<span v-if="moderator" class="moderator">Moderator</span> <span v-if="moderator" class="moderator">Moderator</span>
</span> </span>
@ -219,6 +220,7 @@ let ips = $ref(null);
let ap = $ref(null); let ap = $ref(null);
let moderator = $ref(false); let moderator = $ref(false);
let silenced = $ref(false); let silenced = $ref(false);
let limited = $ref(false);
let suspended = $ref(false); let suspended = $ref(false);
let moderationNote = $ref(''); let moderationNote = $ref('');
const filesPagination = { const filesPagination = {
@ -244,6 +246,7 @@ function createFetcher() {
ips = _ips; ips = _ips;
moderator = info.isModerator; moderator = info.isModerator;
silenced = info.isSilenced; silenced = info.isSilenced;
limited = info.isLimited;
suspended = info.isSuspended; suspended = info.isSuspended;
moderationNote = info.moderationNote; moderationNote = info.moderationNote;
@ -485,7 +488,7 @@ definePageMetadata(computed(() => ({
display: none; display: none;
} }
> .suspended, > .silenced, > .moderator { > .suspended, > .limited, > .silenced, > .moderator {
display: inline-block; display: inline-block;
border: solid 1px; border: solid 1px;
border-radius: 6px; border-radius: 6px;
@ -498,6 +501,11 @@ definePageMetadata(computed(() => ({
border-color: var(--error); border-color: var(--error);
} }
> .limited {
color: var(--error);
border-color: var(--error);
}
> .silenced { > .silenced {
color: var(--warn); color: var(--warn);
border-color: var(--warn); border-color: var(--warn);

View file

@ -2770,6 +2770,7 @@ type UserDetailed = UserLite & {
isModerator: boolean; isModerator: boolean;
isMuted: boolean; isMuted: boolean;
isSilenced: boolean; isSilenced: boolean;
isLimited: boolean;
isSuspended: boolean; isSuspended: boolean;
lang: string | null; lang: string | null;
lastFetchedAt?: DateString; lastFetchedAt?: DateString;

View file

@ -53,6 +53,7 @@ export type UserDetailed = UserLite & {
isModerator: boolean; isModerator: boolean;
isMuted: boolean; isMuted: boolean;
isSilenced: boolean; isSilenced: boolean;
isLimited: boolean;
isSuspended: boolean; isSuspended: boolean;
lang: string | null; lang: string | null;
lastFetchedAt?: DateString; lastFetchedAt?: DateString;