parent
dc9a19b9c7
commit
56d571c0f0
23 changed files with 191 additions and 17 deletions
|
@ -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: "絵文字の登録"
|
||||||
|
|
|
@ -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: {
|
||||||
|
|
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'],
|
icon: ['far', 'trash-alt'],
|
||||||
text: this.$t('delete'),
|
text: this.$t('delete'),
|
||||||
action: this.del
|
action: this.del
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -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');
|
||||||
}
|
}
|
||||||
|
|
|
@ -30,6 +30,11 @@ export interface IEndpointMeta {
|
||||||
*/
|
*/
|
||||||
requireAdmin?: boolean;
|
requireAdmin?: boolean;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 管理者またはモデレーターのみ使えるエンドポイントか否か
|
||||||
|
*/
|
||||||
|
requireModerator?: boolean;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* エンドポイントのリミテーションに関するやつ
|
* エンドポイントのリミテーションに関するやつ
|
||||||
* 省略した場合はリミテーションは無いものとして解釈されます。
|
* 省略した場合はリミテーションは無いものとして解釈されます。
|
||||||
|
|
|
@ -8,7 +8,7 @@ export const meta = {
|
||||||
},
|
},
|
||||||
|
|
||||||
requireCredential: true,
|
requireCredential: true,
|
||||||
requireAdmin: true,
|
requireModerator: true,
|
||||||
|
|
||||||
params: {
|
params: {
|
||||||
name: {
|
name: {
|
||||||
|
|
|
@ -8,7 +8,7 @@ export const meta = {
|
||||||
},
|
},
|
||||||
|
|
||||||
requireCredential: true,
|
requireCredential: true,
|
||||||
requireAdmin: true,
|
requireModerator: true,
|
||||||
|
|
||||||
params: {
|
params: {
|
||||||
host: {
|
host: {
|
||||||
|
|
|
@ -9,7 +9,7 @@ export const meta = {
|
||||||
},
|
},
|
||||||
|
|
||||||
requireCredential: true,
|
requireCredential: true,
|
||||||
requireAdmin: true,
|
requireModerator: true,
|
||||||
|
|
||||||
params: {
|
params: {
|
||||||
id: {
|
id: {
|
||||||
|
|
|
@ -9,7 +9,7 @@ export const meta = {
|
||||||
},
|
},
|
||||||
|
|
||||||
requireCredential: true,
|
requireCredential: true,
|
||||||
requireAdmin: true,
|
requireModerator: true,
|
||||||
|
|
||||||
params: {
|
params: {
|
||||||
id: {
|
id: {
|
||||||
|
|
|
@ -8,7 +8,7 @@ export const meta = {
|
||||||
},
|
},
|
||||||
|
|
||||||
requireCredential: true,
|
requireCredential: true,
|
||||||
requireAdmin: true,
|
requireModerator: true,
|
||||||
|
|
||||||
params: {}
|
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,
|
requireCredential: true,
|
||||||
requireAdmin: true,
|
requireModerator: true,
|
||||||
|
|
||||||
params: {
|
params: {
|
||||||
userId: {
|
userId: {
|
||||||
|
|
|
@ -10,7 +10,7 @@ export const meta = {
|
||||||
},
|
},
|
||||||
|
|
||||||
requireCredential: true,
|
requireCredential: true,
|
||||||
requireAdmin: true,
|
requireModerator: true,
|
||||||
|
|
||||||
params: {
|
params: {
|
||||||
userId: {
|
userId: {
|
||||||
|
|
|
@ -10,7 +10,7 @@ export const meta = {
|
||||||
},
|
},
|
||||||
|
|
||||||
requireCredential: true,
|
requireCredential: true,
|
||||||
requireAdmin: true,
|
requireModerator: true,
|
||||||
|
|
||||||
params: {
|
params: {
|
||||||
userId: {
|
userId: {
|
||||||
|
|
|
@ -8,7 +8,7 @@ export const meta = {
|
||||||
},
|
},
|
||||||
|
|
||||||
requireCredential: true,
|
requireCredential: true,
|
||||||
requireAdmin: true,
|
requireModerator: true,
|
||||||
|
|
||||||
params: {
|
params: {
|
||||||
broadcasts: {
|
broadcasts: {
|
||||||
|
|
|
@ -10,7 +10,7 @@ export const meta = {
|
||||||
},
|
},
|
||||||
|
|
||||||
requireCredential: true,
|
requireCredential: true,
|
||||||
requireAdmin: true,
|
requireModerator: true,
|
||||||
|
|
||||||
params: {
|
params: {
|
||||||
userId: {
|
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.hidedTags = instance.hidedTags;
|
||||||
response.recaptchaSecretKey = instance.recaptchaSecretKey;
|
response.recaptchaSecretKey = instance.recaptchaSecretKey;
|
||||||
response.proxyAccount = instance.proxyAccount;
|
response.proxyAccount = instance.proxyAccount;
|
||||||
|
|
|
@ -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');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue