| @@ -1034,6 +1034,7 @@ admin/views/index.vue: | ||||
|   dashboard: "ダッシュボード" | ||||
|   instance: "インスタンス" | ||||
|   emoji: "カスタム絵文字" | ||||
|   moderators: "モデレーター" | ||||
|   users: "ユーザー" | ||||
|   update: "更新" | ||||
|   announcements: "お知らせ" | ||||
| @@ -1133,6 +1134,12 @@ admin/views/users.vue: | ||||
|   unverify: "公式アカウントを解除する" | ||||
|   unverified: "公式アカウントを解除しました" | ||||
|  | ||||
| admin/views/moderators.vue: | ||||
|   add-moderator: | ||||
|     title: "モデレーターの登録" | ||||
|     add: "登録" | ||||
|     added: "モデレーターを登録しました" | ||||
|  | ||||
| admin/views/emoji.vue: | ||||
|   add-emoji: | ||||
|     title: "絵文字の登録" | ||||
|   | ||||
| @@ -20,6 +20,7 @@ | ||||
| 		<ul> | ||||
| 			<li @click="nav('dashboard')" :class="{ active: page == 'dashboard' }"><fa icon="home" fixed-width/>{{ $t('dashboard') }}</li> | ||||
| 			<li @click="nav('instance')" :class="{ active: page == 'instance' }"><fa icon="cog" fixed-width/>{{ $t('instance') }}</li> | ||||
| 			<li @click="nav('moderators')" :class="{ active: page == 'moderators' }"><fa :icon="faHeadset" fixed-width/>{{ $t('moderators') }}</li> | ||||
| 			<li @click="nav('users')" :class="{ active: page == 'users' }"><fa icon="users" fixed-width/>{{ $t('users') }}</li> | ||||
| 			<li @click="nav('emoji')" :class="{ active: page == 'emoji' }"><fa :icon="faGrin" fixed-width/>{{ $t('emoji') }}</li> | ||||
| 			<li @click="nav('announcements')" :class="{ active: page == 'announcements' }"><fa icon="broadcast-tower" fixed-width/>{{ $t('announcements') }}</li> | ||||
| @@ -38,6 +39,7 @@ | ||||
| 	<main> | ||||
| 		<div v-if="page == 'dashboard'"><x-dashboard/></div> | ||||
| 		<div v-if="page == 'instance'"><x-instance/></div> | ||||
| 		<div v-if="page == 'moderators'"><x-moderators/></div> | ||||
| 		<div v-if="page == 'users'"><x-users/></div> | ||||
| 		<div v-if="page == 'emoji'"><x-emoji/></div> | ||||
| 		<div v-if="page == 'announcements'"><x-announcements/></div> | ||||
| @@ -54,11 +56,12 @@ import i18n from '../../i18n'; | ||||
| import { version } from '../../config'; | ||||
| import XDashboard from "./dashboard.vue"; | ||||
| import XInstance from "./instance.vue"; | ||||
| import XModerators from "./moderators.vue"; | ||||
| import XEmoji from "./emoji.vue"; | ||||
| import XAnnouncements from "./announcements.vue"; | ||||
| import XHashtags from "./hashtags.vue"; | ||||
| import XUsers from "./users.vue"; | ||||
| import { faArrowLeft } from '@fortawesome/free-solid-svg-icons'; | ||||
| import { faHeadset, faArrowLeft } from '@fortawesome/free-solid-svg-icons'; | ||||
| import { faGrin } from '@fortawesome/free-regular-svg-icons'; | ||||
|  | ||||
| // Detect the user agent | ||||
| @@ -70,6 +73,7 @@ export default Vue.extend({ | ||||
| 	components: { | ||||
| 		XDashboard, | ||||
| 		XInstance, | ||||
| 		XModerators, | ||||
| 		XEmoji, | ||||
| 		XAnnouncements, | ||||
| 		XHashtags, | ||||
| @@ -85,7 +89,8 @@ export default Vue.extend({ | ||||
| 			isMobile, | ||||
| 			navOpend: !isMobile, | ||||
| 			faGrin, | ||||
| 			faArrowLeft | ||||
| 			faArrowLeft, | ||||
| 			faHeadset | ||||
| 		}; | ||||
| 	}, | ||||
| 	methods: { | ||||
|   | ||||
							
								
								
									
										61
									
								
								src/client/app/admin/views/moderators.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										61
									
								
								src/client/app/admin/views/moderators.vue
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,61 @@ | ||||
| <template> | ||||
| <div class="jnhmugbb"> | ||||
| 	<ui-card> | ||||
| 		<div slot="title"><fa icon="plus"/> {{ $t('add-moderator.title') }}</div> | ||||
| 		<section class="fit-top"> | ||||
| 			<ui-input v-model="username" type="text"> | ||||
| 				<span slot="prefix">@</span> | ||||
| 			</ui-input> | ||||
| 			<ui-button @click="add" :disabled="adding">{{ $t('add-moderator.add') }}</ui-button> | ||||
| 		</section> | ||||
| 	</ui-card> | ||||
| </div> | ||||
| </template> | ||||
|  | ||||
| <script lang="ts"> | ||||
| import Vue from 'vue'; | ||||
| import i18n from '../../i18n'; | ||||
| import parseAcct from "../../../../misc/acct/parse"; | ||||
|  | ||||
| export default Vue.extend({ | ||||
| 	i18n: i18n('admin/views/moderators.vue'), | ||||
|  | ||||
| 	data() { | ||||
| 		return { | ||||
| 			username: '', | ||||
| 			adding: false | ||||
| 		}; | ||||
| 	}, | ||||
|  | ||||
| 	methods: { | ||||
| 		async add() { | ||||
| 			this.adding = true; | ||||
|  | ||||
| 			const process = async () => { | ||||
| 				const user = await this.$root.api('users/show', parseAcct(this.username)); | ||||
| 				await this.$root.api('admin/moderators/add', { userId: user.id }); | ||||
| 				this.$root.alert({ | ||||
| 					type: 'success', | ||||
| 					text: this.$t('add-moderator.added') | ||||
| 				}); | ||||
| 			}; | ||||
|  | ||||
| 			await process().catch(e => { | ||||
| 				this.$root.alert({ | ||||
| 					type: 'error', | ||||
| 					text: e | ||||
| 				}); | ||||
| 			}); | ||||
|  | ||||
| 			this.adding = false; | ||||
| 		}, | ||||
| 	} | ||||
| }); | ||||
| </script> | ||||
|  | ||||
| <style lang="stylus" scoped> | ||||
| .jnhmugbb | ||||
| 	@media (min-width 500px) | ||||
| 		padding 16px | ||||
|  | ||||
| </style> | ||||
| @@ -55,7 +55,7 @@ export default Vue.extend({ | ||||
| 							} | ||||
| 					] : [] | ||||
| 				], [ | ||||
| 					this.note.userId == this.$store.state.i.id || this.$store.state.i.isAdmin ? [{ | ||||
| 					this.note.userId == this.$store.state.i.id || this.$store.state.i.isAdmin || this.$store.state.i.isModerator ? [{ | ||||
| 						icon: ['far', 'trash-alt'], | ||||
| 						text: this.$t('delete'), | ||||
| 						action: this.del | ||||
|   | ||||
| @@ -58,7 +58,7 @@ | ||||
| 						<i><fa icon="angle-right"/></i> | ||||
| 					</p> | ||||
| 				</li> | ||||
| 				<li v-if="$store.state.i.isAdmin"> | ||||
| 				<li v-if="$store.state.i.isAdmin || $store.state.i.isModerator"> | ||||
| 					<a href="/admin"> | ||||
| 						<i><fa icon="terminal"/></i> | ||||
| 						<span>{{ $t('admin') }}</span> | ||||
|   | ||||
| @@ -30,7 +30,7 @@ | ||||
| 				<ul> | ||||
| 					<li><a @click="search"><i><fa icon="search"/></i>{{ $t('search') }}<i><fa icon="angle-right"/></i></a></li> | ||||
| 					<li><router-link to="/i/settings" :data-active="$route.name == 'settings'"><i><fa icon="cog"/></i>{{ $t('settings') }}<i><fa icon="angle-right"/></i></router-link></li> | ||||
| 					<li v-if="$store.getters.isSignedIn && $store.state.i.isAdmin"><a href="/admin"><i><fa icon="terminal"/></i><span>{{ $t('admin') }}</span><i><fa icon="angle-right"/></i></a></li> | ||||
| 					<li v-if="$store.getters.isSignedIn && ($store.state.i.isAdmin || $store.state.i.isModerator)"><a href="/admin"><i><fa icon="terminal"/></i><span>{{ $t('admin') }}</span><i><fa icon="angle-right"/></i></a></li> | ||||
| 					<li @click="dark"><p><template v-if="$store.state.device.darkmode"><i><fa icon="moon"/></i></template><template v-else><i><fa :icon="['far', 'moon']"/></i></template><span>{{ $t('darkmode') }}</span></p></li> | ||||
| 				</ul> | ||||
| 			</div> | ||||
|   | ||||
| @@ -99,6 +99,7 @@ export interface ILocalUser extends IUserBase { | ||||
| 	lastUsedAt: Date; | ||||
| 	isCat: boolean; | ||||
| 	isAdmin?: boolean; | ||||
| 	isModerator?: boolean; | ||||
| 	isVerified?: boolean; | ||||
| 	twoFactorSecret: string; | ||||
| 	twoFactorEnabled: boolean; | ||||
| @@ -125,6 +126,7 @@ export interface IRemoteUser extends IUserBase { | ||||
| 	}; | ||||
| 	updatedAt: Date; | ||||
| 	isAdmin: false; | ||||
| 	isModerator: false; | ||||
| } | ||||
|  | ||||
| export type IUser = ILocalUser | IRemoteUser; | ||||
|   | ||||
| @@ -29,6 +29,10 @@ export default (endpoint: string, user: IUser, app: IApp, data: any, file?: any) | ||||
| 		return rej('YOU_ARE_NOT_ADMIN'); | ||||
| 	} | ||||
|  | ||||
| 	if (ep.meta.requireModerator && !user.isAdmin && !user.isModerator) { | ||||
| 		return rej('YOU_ARE_NOT_MODERATOR'); | ||||
| 	} | ||||
|  | ||||
| 	if (app && ep.meta.kind && !app.permission.some(p => p === ep.meta.kind)) { | ||||
| 		return rej('PERMISSION_DENIED'); | ||||
| 	} | ||||
|   | ||||
| @@ -30,6 +30,11 @@ export interface IEndpointMeta { | ||||
| 	 */ | ||||
| 	requireAdmin?: boolean; | ||||
|  | ||||
| 	/** | ||||
| 	 * 管理者またはモデレーターのみ使えるエンドポイントか否か | ||||
| 	 */ | ||||
| 	requireModerator?: boolean; | ||||
|  | ||||
| 	/** | ||||
| 	 * エンドポイントのリミテーションに関するやつ | ||||
| 	 * 省略した場合はリミテーションは無いものとして解釈されます。 | ||||
|   | ||||
| @@ -8,7 +8,7 @@ export const meta = { | ||||
| 	}, | ||||
|  | ||||
| 	requireCredential: true, | ||||
| 	requireAdmin: true, | ||||
| 	requireModerator: true, | ||||
|  | ||||
| 	params: { | ||||
| 		name: { | ||||
|   | ||||
| @@ -8,7 +8,7 @@ export const meta = { | ||||
| 	}, | ||||
|  | ||||
| 	requireCredential: true, | ||||
| 	requireAdmin: true, | ||||
| 	requireModerator: true, | ||||
|  | ||||
| 	params: { | ||||
| 		host: { | ||||
|   | ||||
| @@ -9,7 +9,7 @@ export const meta = { | ||||
| 	}, | ||||
|  | ||||
| 	requireCredential: true, | ||||
| 	requireAdmin: true, | ||||
| 	requireModerator: true, | ||||
|  | ||||
| 	params: { | ||||
| 		id: { | ||||
|   | ||||
| @@ -9,7 +9,7 @@ export const meta = { | ||||
| 	}, | ||||
|  | ||||
| 	requireCredential: true, | ||||
| 	requireAdmin: true, | ||||
| 	requireModerator: true, | ||||
|  | ||||
| 	params: { | ||||
| 		id: { | ||||
|   | ||||
| @@ -8,7 +8,7 @@ export const meta = { | ||||
| 	}, | ||||
|  | ||||
| 	requireCredential: true, | ||||
| 	requireAdmin: true, | ||||
| 	requireModerator: true, | ||||
|  | ||||
| 	params: {} | ||||
| }; | ||||
|   | ||||
							
								
								
									
										45
									
								
								src/server/api/endpoints/admin/moderators/add.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										45
									
								
								src/server/api/endpoints/admin/moderators/add.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,45 @@ | ||||
| import $ from 'cafy'; | ||||
| import ID, { transform } from '../../../../../misc/cafy-id'; | ||||
| import define from '../../../define'; | ||||
| import User from '../../../../../models/user'; | ||||
|  | ||||
| export const meta = { | ||||
| 	desc: { | ||||
| 		'ja-JP': '指定したユーザーをモデレーターにします。', | ||||
| 		'en-US': 'Mark a user as moderator.' | ||||
| 	}, | ||||
|  | ||||
| 	requireCredential: true, | ||||
| 	requireAdmin: true, | ||||
|  | ||||
| 	params: { | ||||
| 		userId: { | ||||
| 			validator: $.type(ID), | ||||
| 			transform: transform, | ||||
| 			desc: { | ||||
| 				'ja-JP': '対象のユーザーID', | ||||
| 				'en-US': 'The user ID' | ||||
| 			} | ||||
| 		}, | ||||
| 	} | ||||
| }; | ||||
|  | ||||
| export default define(meta, (ps) => new Promise(async (res, rej) => { | ||||
| 	const user = await User.findOne({ | ||||
| 		_id: ps.userId | ||||
| 	}); | ||||
|  | ||||
| 	if (user == null) { | ||||
| 		return rej('user not found'); | ||||
| 	} | ||||
|  | ||||
| 	await User.update({ | ||||
| 		_id: user._id | ||||
| 	}, { | ||||
| 		$set: { | ||||
| 			isModerator: true | ||||
| 		} | ||||
| 	}); | ||||
|  | ||||
| 	res(); | ||||
| })); | ||||
							
								
								
									
										45
									
								
								src/server/api/endpoints/admin/moderators/remove.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										45
									
								
								src/server/api/endpoints/admin/moderators/remove.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,45 @@ | ||||
| import $ from 'cafy'; | ||||
| import ID, { transform } from '../../../../../misc/cafy-id'; | ||||
| import define from '../../../define'; | ||||
| import User from '../../../../../models/user'; | ||||
|  | ||||
| export const meta = { | ||||
| 	desc: { | ||||
| 		'ja-JP': '指定したユーザーをモデレーター解除します。', | ||||
| 		'en-US': 'Unmark a user as moderator.' | ||||
| 	}, | ||||
|  | ||||
| 	requireCredential: true, | ||||
| 	requireAdmin: true, | ||||
|  | ||||
| 	params: { | ||||
| 		userId: { | ||||
| 			validator: $.type(ID), | ||||
| 			transform: transform, | ||||
| 			desc: { | ||||
| 				'ja-JP': '対象のユーザーID', | ||||
| 				'en-US': 'The user ID' | ||||
| 			} | ||||
| 		}, | ||||
| 	} | ||||
| }; | ||||
|  | ||||
| export default define(meta, (ps) => new Promise(async (res, rej) => { | ||||
| 	const user = await User.findOne({ | ||||
| 		_id: ps.userId | ||||
| 	}); | ||||
|  | ||||
| 	if (user == null) { | ||||
| 		return rej('user not found'); | ||||
| 	} | ||||
|  | ||||
| 	await User.update({ | ||||
| 		_id: user._id | ||||
| 	}, { | ||||
| 		$set: { | ||||
| 			isModerator: false | ||||
| 		} | ||||
| 	}); | ||||
|  | ||||
| 	res(); | ||||
| })); | ||||
| @@ -10,7 +10,7 @@ export const meta = { | ||||
| 	}, | ||||
|  | ||||
| 	requireCredential: true, | ||||
| 	requireAdmin: true, | ||||
| 	requireModerator: true, | ||||
|  | ||||
| 	params: { | ||||
| 		userId: { | ||||
|   | ||||
| @@ -10,7 +10,7 @@ export const meta = { | ||||
| 	}, | ||||
|  | ||||
| 	requireCredential: true, | ||||
| 	requireAdmin: true, | ||||
| 	requireModerator: true, | ||||
|  | ||||
| 	params: { | ||||
| 		userId: { | ||||
|   | ||||
| @@ -10,7 +10,7 @@ export const meta = { | ||||
| 	}, | ||||
|  | ||||
| 	requireCredential: true, | ||||
| 	requireAdmin: true, | ||||
| 	requireModerator: true, | ||||
|  | ||||
| 	params: { | ||||
| 		userId: { | ||||
|   | ||||
| @@ -8,7 +8,7 @@ export const meta = { | ||||
| 	}, | ||||
|  | ||||
| 	requireCredential: true, | ||||
| 	requireAdmin: true, | ||||
| 	requireModerator: true, | ||||
|  | ||||
| 	params: { | ||||
| 		broadcasts: { | ||||
|   | ||||
| @@ -10,7 +10,7 @@ export const meta = { | ||||
| 	}, | ||||
|  | ||||
| 	requireCredential: true, | ||||
| 	requireAdmin: true, | ||||
| 	requireModerator: true, | ||||
|  | ||||
| 	params: { | ||||
| 		userId: { | ||||
|   | ||||
| @@ -84,7 +84,7 @@ export default define(meta, (ps, me) => new Promise(async (res, rej) => { | ||||
| 		}; | ||||
| 	} | ||||
|  | ||||
| 	if (me && me.isAdmin) { | ||||
| 	if (me && (me.isAdmin || me.isModerator)) { | ||||
| 		response.hidedTags = instance.hidedTags; | ||||
| 		response.recaptchaSecretKey = instance.recaptchaSecretKey; | ||||
| 		response.proxyAccount = instance.proxyAccount; | ||||
|   | ||||
| @@ -38,7 +38,7 @@ export default define(meta, (ps, user) => new Promise(async (res, rej) => { | ||||
| 		return rej('note not found'); | ||||
| 	} | ||||
|  | ||||
| 	if (!user.isAdmin && !note.userId.equals(user._id)) { | ||||
| 	if (!user.isAdmin && !user.isModerator && !note.userId.equals(user._id)) { | ||||
| 		return rej('access denied'); | ||||
| 	} | ||||
|  | ||||
|   | ||||
		Reference in New Issue
	
	Block a user
	 syuilo
					syuilo