Moderator system

Closes #2357
This commit is contained in:
syuilo 2018-11-15 04:15:42 +09:00
parent dc9a19b9c7
commit 56d571c0f0
No known key found for this signature in database
GPG key ID: BDC4C49D06AB9D69
23 changed files with 191 additions and 17 deletions

View file

@ -1034,6 +1034,7 @@ admin/views/index.vue:
dashboard: "ダッシュボード" dashboard: "ダッシュボード"
instance: "インスタンス" instance: "インスタンス"
emoji: "カスタム絵文字" emoji: "カスタム絵文字"
moderators: "モデレーター"
users: "ユーザー" users: "ユーザー"
update: "更新" update: "更新"
announcements: "お知らせ" announcements: "お知らせ"
@ -1133,6 +1134,12 @@ admin/views/users.vue:
unverify: "公式アカウントを解除する" unverify: "公式アカウントを解除する"
unverified: "公式アカウントを解除しました" unverified: "公式アカウントを解除しました"
admin/views/moderators.vue:
add-moderator:
title: "モデレーターの登録"
add: "登録"
added: "モデレーターを登録しました"
admin/views/emoji.vue: admin/views/emoji.vue:
add-emoji: add-emoji:
title: "絵文字の登録" title: "絵文字の登録"

View file

@ -20,6 +20,7 @@
<ul> <ul>
<li @click="nav('dashboard')" :class="{ active: page == 'dashboard' }"><fa icon="home" fixed-width/>{{ $t('dashboard') }}</li> <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('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('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('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> <li @click="nav('announcements')" :class="{ active: page == 'announcements' }"><fa icon="broadcast-tower" fixed-width/>{{ $t('announcements') }}</li>
@ -38,6 +39,7 @@
<main> <main>
<div v-if="page == 'dashboard'"><x-dashboard/></div> <div v-if="page == 'dashboard'"><x-dashboard/></div>
<div v-if="page == 'instance'"><x-instance/></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 == 'users'"><x-users/></div>
<div v-if="page == 'emoji'"><x-emoji/></div> <div v-if="page == 'emoji'"><x-emoji/></div>
<div v-if="page == 'announcements'"><x-announcements/></div> <div v-if="page == 'announcements'"><x-announcements/></div>
@ -54,11 +56,12 @@ import i18n from '../../i18n';
import { version } from '../../config'; import { version } from '../../config';
import XDashboard from "./dashboard.vue"; import XDashboard from "./dashboard.vue";
import XInstance from "./instance.vue"; import XInstance from "./instance.vue";
import XModerators from "./moderators.vue";
import XEmoji from "./emoji.vue"; import XEmoji from "./emoji.vue";
import XAnnouncements from "./announcements.vue"; import XAnnouncements from "./announcements.vue";
import XHashtags from "./hashtags.vue"; import XHashtags from "./hashtags.vue";
import XUsers from "./users.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'; import { faGrin } from '@fortawesome/free-regular-svg-icons';
// Detect the user agent // Detect the user agent
@ -70,6 +73,7 @@ export default Vue.extend({
components: { components: {
XDashboard, XDashboard,
XInstance, XInstance,
XModerators,
XEmoji, XEmoji,
XAnnouncements, XAnnouncements,
XHashtags, XHashtags,
@ -85,7 +89,8 @@ export default Vue.extend({
isMobile, isMobile,
navOpend: !isMobile, navOpend: !isMobile,
faGrin, faGrin,
faArrowLeft faArrowLeft,
faHeadset
}; };
}, },
methods: { methods: {

View 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>

View file

@ -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'], icon: ['far', 'trash-alt'],
text: this.$t('delete'), text: this.$t('delete'),
action: this.del action: this.del

View file

@ -58,7 +58,7 @@
<i><fa icon="angle-right"/></i> <i><fa icon="angle-right"/></i>
</p> </p>
</li> </li>
<li v-if="$store.state.i.isAdmin"> <li v-if="$store.state.i.isAdmin || $store.state.i.isModerator">
<a href="/admin"> <a href="/admin">
<i><fa icon="terminal"/></i> <i><fa icon="terminal"/></i>
<span>{{ $t('admin') }}</span> <span>{{ $t('admin') }}</span>

View file

@ -30,7 +30,7 @@
<ul> <ul>
<li><a @click="search"><i><fa icon="search"/></i>{{ $t('search') }}<i><fa icon="angle-right"/></i></a></li> <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><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> <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> </ul>
</div> </div>

View file

@ -99,6 +99,7 @@ export interface ILocalUser extends IUserBase {
lastUsedAt: Date; lastUsedAt: Date;
isCat: boolean; isCat: boolean;
isAdmin?: boolean; isAdmin?: boolean;
isModerator?: boolean;
isVerified?: boolean; isVerified?: boolean;
twoFactorSecret: string; twoFactorSecret: string;
twoFactorEnabled: boolean; twoFactorEnabled: boolean;
@ -125,6 +126,7 @@ export interface IRemoteUser extends IUserBase {
}; };
updatedAt: Date; updatedAt: Date;
isAdmin: false; isAdmin: false;
isModerator: false;
} }
export type IUser = ILocalUser | IRemoteUser; export type IUser = ILocalUser | IRemoteUser;

View file

@ -29,6 +29,10 @@ export default (endpoint: string, user: IUser, app: IApp, data: any, file?: any)
return rej('YOU_ARE_NOT_ADMIN'); 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)) { if (app && ep.meta.kind && !app.permission.some(p => p === ep.meta.kind)) {
return rej('PERMISSION_DENIED'); return rej('PERMISSION_DENIED');
} }

View file

@ -30,6 +30,11 @@ export interface IEndpointMeta {
*/ */
requireAdmin?: boolean; requireAdmin?: boolean;
/**
* 使
*/
requireModerator?: boolean;
/** /**
* *
* *

View file

@ -8,7 +8,7 @@ export const meta = {
}, },
requireCredential: true, requireCredential: true,
requireAdmin: true, requireModerator: true,
params: { params: {
name: { name: {

View file

@ -8,7 +8,7 @@ export const meta = {
}, },
requireCredential: true, requireCredential: true,
requireAdmin: true, requireModerator: true,
params: { params: {
host: { host: {

View file

@ -9,7 +9,7 @@ export const meta = {
}, },
requireCredential: true, requireCredential: true,
requireAdmin: true, requireModerator: true,
params: { params: {
id: { id: {

View file

@ -9,7 +9,7 @@ export const meta = {
}, },
requireCredential: true, requireCredential: true,
requireAdmin: true, requireModerator: true,
params: { params: {
id: { id: {

View file

@ -8,7 +8,7 @@ export const meta = {
}, },
requireCredential: true, requireCredential: true,
requireAdmin: true, requireModerator: true,
params: {} params: {}
}; };

View 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();
}));

View 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();
}));

View file

@ -10,7 +10,7 @@ export const meta = {
}, },
requireCredential: true, requireCredential: true,
requireAdmin: true, requireModerator: true,
params: { params: {
userId: { userId: {

View file

@ -10,7 +10,7 @@ export const meta = {
}, },
requireCredential: true, requireCredential: true,
requireAdmin: true, requireModerator: true,
params: { params: {
userId: { userId: {

View file

@ -10,7 +10,7 @@ export const meta = {
}, },
requireCredential: true, requireCredential: true,
requireAdmin: true, requireModerator: true,
params: { params: {
userId: { userId: {

View file

@ -8,7 +8,7 @@ export const meta = {
}, },
requireCredential: true, requireCredential: true,
requireAdmin: true, requireModerator: true,
params: { params: {
broadcasts: { broadcasts: {

View file

@ -10,7 +10,7 @@ export const meta = {
}, },
requireCredential: true, requireCredential: true,
requireAdmin: true, requireModerator: true,
params: { params: {
userId: { userId: {

View file

@ -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.hidedTags = instance.hidedTags;
response.recaptchaSecretKey = instance.recaptchaSecretKey; response.recaptchaSecretKey = instance.recaptchaSecretKey;
response.proxyAccount = instance.proxyAccount; response.proxyAccount = instance.proxyAccount;

View file

@ -38,7 +38,7 @@ export default define(meta, (ps, user) => new Promise(async (res, rej) => {
return rej('note not found'); 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'); return rej('access denied');
} }