parent
13a75abc91
commit
246cead2b1
19 changed files with 404 additions and 63 deletions
|
@ -1151,16 +1151,35 @@ admin/views/charts.vue:
|
||||||
network-usage: "通信量"
|
network-usage: "通信量"
|
||||||
|
|
||||||
admin/views/users.vue:
|
admin/views/users.vue:
|
||||||
suspend-user: "ユーザーの凍結"
|
operation: "操作"
|
||||||
|
username-or-userid: "ユーザー名またはユーザーID"
|
||||||
|
user-not-found: "ユーザーが見つかりません"
|
||||||
|
lookup: "照会"
|
||||||
|
reset-password: "パスワードをリセット"
|
||||||
|
password-updated: "パスワードは現在「{password}」です"
|
||||||
suspend: "凍結"
|
suspend: "凍結"
|
||||||
suspended: "凍結しました"
|
suspended: "凍結しました"
|
||||||
unsuspend: "凍結の解除"
|
unsuspend: "凍結の解除"
|
||||||
unsuspended: "凍結を解除しました"
|
unsuspended: "凍結を解除しました"
|
||||||
verify-user: "ユーザーの公式アカウント設定"
|
|
||||||
verify: "公式アカウントにする"
|
verify: "公式アカウントにする"
|
||||||
verified: "公式アカウントにしました"
|
verified: "公式アカウントにしました"
|
||||||
unverify: "公式アカウントを解除する"
|
unverify: "公式アカウントを解除する"
|
||||||
unverified: "公式アカウントを解除しました"
|
unverified: "公式アカウントを解除しました"
|
||||||
|
users:
|
||||||
|
title: "ユーザー"
|
||||||
|
sort:
|
||||||
|
title: "ソート"
|
||||||
|
createdAtAsc: "登録日時が古い順"
|
||||||
|
createdAtDesc: "登録日時が新しい順"
|
||||||
|
updatedAtAsc: "最終更新日時が古い順"
|
||||||
|
updatedAtDesc: "最終更新日時が新しい順"
|
||||||
|
origin:
|
||||||
|
title: "オリジン"
|
||||||
|
combined: "ローカル+リモート"
|
||||||
|
local: "ローカル"
|
||||||
|
remote: "リモート"
|
||||||
|
createdAt: "登録日時"
|
||||||
|
updatedAt: "更新日時"
|
||||||
|
|
||||||
admin/views/moderators.vue:
|
admin/views/moderators.vue:
|
||||||
add-moderator:
|
add-moderator:
|
||||||
|
|
|
@ -9,7 +9,7 @@
|
||||||
<ui-textarea v-model="announcement.text">
|
<ui-textarea v-model="announcement.text">
|
||||||
<span>{{ $t('text') }}</span>
|
<span>{{ $t('text') }}</span>
|
||||||
</ui-textarea>
|
</ui-textarea>
|
||||||
<ui-horizon-group>
|
<ui-horizon-group class="fit-bottom">
|
||||||
<ui-button @click="save()"><fa :icon="['far', 'save']"/> {{ $t('save') }}</ui-button>
|
<ui-button @click="save()"><fa :icon="['far', 'save']"/> {{ $t('save') }}</ui-button>
|
||||||
<ui-button @click="remove(i)"><fa :icon="['far', 'trash-alt']"/> {{ $t('remove') }}</ui-button>
|
<ui-button @click="remove(i)"><fa :icon="['far', 'trash-alt']"/> {{ $t('remove') }}</ui-button>
|
||||||
</ui-horizon-group>
|
</ui-horizon-group>
|
||||||
|
|
|
@ -38,7 +38,7 @@
|
||||||
<i slot="icon"><fa icon="link"/></i>
|
<i slot="icon"><fa icon="link"/></i>
|
||||||
<span>{{ $t('add-emoji.url') }}</span>
|
<span>{{ $t('add-emoji.url') }}</span>
|
||||||
</ui-input>
|
</ui-input>
|
||||||
<ui-horizon-group>
|
<ui-horizon-group class="fit-bottom">
|
||||||
<ui-button @click="updateEmoji(emoji)"><fa :icon="['far', 'save']"/> {{ $t('emojis.update') }}</ui-button>
|
<ui-button @click="updateEmoji(emoji)"><fa :icon="['far', 'save']"/> {{ $t('emojis.update') }}</ui-button>
|
||||||
<ui-button @click="removeEmoji(emoji)"><fa :icon="['far', 'trash-alt']"/> {{ $t('emojis.remove') }}</ui-button>
|
<ui-button @click="removeEmoji(emoji)"><fa :icon="['far', 'trash-alt']"/> {{ $t('emojis.remove') }}</ui-button>
|
||||||
</ui-horizon-group>
|
</ui-horizon-group>
|
||||||
|
|
|
@ -1,28 +1,63 @@
|
||||||
<template>
|
<template>
|
||||||
<div class="ucnffhbtogqgscfmqcymwmmupoknpfsw">
|
<div class="ucnffhbtogqgscfmqcymwmmupoknpfsw">
|
||||||
<ui-card>
|
<ui-card>
|
||||||
<div slot="title"><fa :icon="faCertificate"/> {{ $t('verify-user') }}</div>
|
<div slot="title"><fa :icon="faTerminal"/> {{ $t('operation') }}</div>
|
||||||
<section class="fit-top">
|
<section class="fit-top">
|
||||||
<ui-input v-model="verifyUsername" type="text">
|
<ui-input v-model="target" type="text">
|
||||||
<span slot="prefix">@</span>
|
<span>{{ $t('username-or-userid') }}</span>
|
||||||
</ui-input>
|
</ui-input>
|
||||||
|
<ui-button @click="resetPassword"><fa :icon="faKey"/> {{ $t('reset-password') }}</ui-button>
|
||||||
<ui-horizon-group>
|
<ui-horizon-group>
|
||||||
<ui-button @click="verifyUser" :disabled="verifying">{{ $t('verify') }}</ui-button>
|
<ui-button @click="verifyUser" :disabled="verifying"><fa :icon="faCertificate"/> {{ $t('verify') }}</ui-button>
|
||||||
<ui-button @click="unverifyUser" :disabled="unverifying">{{ $t('unverify') }}</ui-button>
|
<ui-button @click="unverifyUser" :disabled="unverifying">{{ $t('unverify') }}</ui-button>
|
||||||
</ui-horizon-group>
|
</ui-horizon-group>
|
||||||
|
<ui-horizon-group>
|
||||||
|
<ui-button @click="suspendUser" :disabled="suspending"><fa :icon="faSnowflake"/> {{ $t('suspend') }}</ui-button>
|
||||||
|
<ui-button @click="unsuspendUser" :disabled="unsuspending">{{ $t('unsuspend') }}</ui-button>
|
||||||
|
</ui-horizon-group>
|
||||||
|
<ui-button @click="showUser"><fa :icon="faSearch"/> {{ $t('lookup') }}</ui-button>
|
||||||
|
<ui-textarea v-if="user" :value="user | json5" readonly tall style="margin-top:16px;"></ui-textarea>
|
||||||
</section>
|
</section>
|
||||||
</ui-card>
|
</ui-card>
|
||||||
|
|
||||||
<ui-card>
|
<ui-card>
|
||||||
<div slot="title"><fa :icon="faSnowflake"/> {{ $t('suspend-user') }}</div>
|
<div slot="title"><fa :icon="faUsers"/> {{ $t('users.title') }}</div>
|
||||||
<section class="fit-top">
|
<section class="fit-top">
|
||||||
<ui-input v-model="suspendUsername" type="text">
|
<ui-horizon-group inputs>
|
||||||
<span slot="prefix">@</span>
|
<ui-select v-model="sort">
|
||||||
</ui-input>
|
<span slot="label">{{ $t('users.sort.title') }}</span>
|
||||||
<ui-horizon-group>
|
<option value="-createdAt">{{ $t('users.sort.createdAtAsc') }}</option>
|
||||||
<ui-button @click="suspendUser" :disabled="suspending">{{ $t('suspend') }}</ui-button>
|
<option value="+createdAt">{{ $t('users.sort.createdAtDesc') }}</option>
|
||||||
<ui-button @click="unsuspendUser" :disabled="unsuspending">{{ $t('unsuspend') }}</ui-button>
|
<option value="-updatedAt">{{ $t('users.sort.updatedAtAsc') }}</option>
|
||||||
|
<option value="+updatedAt">{{ $t('users.sort.updatedAtDesc') }}</option>
|
||||||
|
</ui-select>
|
||||||
|
<ui-select v-model="origin">
|
||||||
|
<span slot="label">{{ $t('users.origin.title') }}</span>
|
||||||
|
<option value="combined">{{ $t('users.origin.combined') }}</option>
|
||||||
|
<option value="local">{{ $t('users.origin.local') }}</option>
|
||||||
|
<option value="remote">{{ $t('users.origin.remote') }}</option>
|
||||||
|
</ui-select>
|
||||||
</ui-horizon-group>
|
</ui-horizon-group>
|
||||||
|
<div class="kofvwchc" v-for="user in users">
|
||||||
|
<div>
|
||||||
|
<a :href="user | userPage(null, true)">
|
||||||
|
<mk-avatar class="avatar" :user="user" :disable-link="true"/>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<header>
|
||||||
|
<b>{{ user | userName }}</b>
|
||||||
|
<span class="username">@{{ user | acct }}</span>
|
||||||
|
</header>
|
||||||
|
<div>
|
||||||
|
<span>{{ $t('users.createdAt') }}: <mk-time :time="user.createdAt" mode="detail"/></span>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<span>{{ $t('users.updatedAt') }}: <mk-time :time="user.updatedAt" mode="detail"/></span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<ui-button v-if="existMore" @click="fetchUsers">{{ $t('@.load-more') }}</ui-button>
|
||||||
</section>
|
</section>
|
||||||
</ui-card>
|
</ui-card>
|
||||||
</div>
|
</div>
|
||||||
|
@ -32,7 +67,7 @@
|
||||||
import Vue from 'vue';
|
import Vue from 'vue';
|
||||||
import i18n from '../../i18n';
|
import i18n from '../../i18n';
|
||||||
import parseAcct from "../../../../misc/acct/parse";
|
import parseAcct from "../../../../misc/acct/parse";
|
||||||
import { faCertificate } from '@fortawesome/free-solid-svg-icons';
|
import { faCertificate, faUsers, faTerminal, faSearch, faKey } from '@fortawesome/free-solid-svg-icons';
|
||||||
import { faSnowflake } from '@fortawesome/free-regular-svg-icons';
|
import { faSnowflake } from '@fortawesome/free-regular-svg-icons';
|
||||||
|
|
||||||
export default Vue.extend({
|
export default Vue.extend({
|
||||||
|
@ -40,22 +75,81 @@ export default Vue.extend({
|
||||||
|
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
verifyUsername: null,
|
user: null,
|
||||||
|
target: null,
|
||||||
verifying: false,
|
verifying: false,
|
||||||
unverifying: false,
|
unverifying: false,
|
||||||
suspendUsername: null,
|
|
||||||
suspending: false,
|
suspending: false,
|
||||||
unsuspending: false,
|
unsuspending: false,
|
||||||
faCertificate, faSnowflake
|
sort: '+createdAt',
|
||||||
|
origin: 'combined',
|
||||||
|
limit: 10,
|
||||||
|
offset: 0,
|
||||||
|
users: [],
|
||||||
|
existMore: false,
|
||||||
|
faTerminal, faCertificate, faUsers, faSnowflake, faSearch, faKey
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
|
||||||
|
watch: {
|
||||||
|
sort() {
|
||||||
|
this.users = [];
|
||||||
|
this.offset = 0;
|
||||||
|
this.fetchUsers();
|
||||||
|
},
|
||||||
|
|
||||||
|
origin() {
|
||||||
|
this.users = [];
|
||||||
|
this.offset = 0;
|
||||||
|
this.fetchUsers();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
mounted() {
|
||||||
|
this.fetchUsers();
|
||||||
|
},
|
||||||
|
|
||||||
methods: {
|
methods: {
|
||||||
|
async fetchUser() {
|
||||||
|
try {
|
||||||
|
return await this.$root.api('users/show', this.target.startsWith('@') ? parseAcct(this.target) : { userId: this.target });
|
||||||
|
} catch (e) {
|
||||||
|
if (e == 'user not found') {
|
||||||
|
this.$root.alert({
|
||||||
|
type: 'error',
|
||||||
|
text: this.$t('user-not-found')
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
this.$root.alert({
|
||||||
|
type: 'error',
|
||||||
|
text: e.toString()
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
async showUser() {
|
||||||
|
const user = await this.fetchUser();
|
||||||
|
this.$root.api('admin/show-user', { userId: user.id }).then(info => {
|
||||||
|
this.user = info;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
async resetPassword() {
|
||||||
|
const user = await this.fetchUser();
|
||||||
|
this.$root.api('admin/reset-password', { userId: user.id }).then(res => {
|
||||||
|
this.$root.alert({
|
||||||
|
type: 'success',
|
||||||
|
text: this.$t('password-updated', { password: res.password })
|
||||||
|
});
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
async verifyUser() {
|
async verifyUser() {
|
||||||
this.verifying = true;
|
this.verifying = true;
|
||||||
|
|
||||||
const process = async () => {
|
const process = async () => {
|
||||||
const user = await this.$root.api('users/show', parseAcct(this.verifyUsername));
|
const user = await this.fetchUser();
|
||||||
await this.$root.api('admin/verify-user', { userId: user.id });
|
await this.$root.api('admin/verify-user', { userId: user.id });
|
||||||
this.$root.alert({
|
this.$root.alert({
|
||||||
type: 'success',
|
type: 'success',
|
||||||
|
@ -77,7 +171,7 @@ export default Vue.extend({
|
||||||
this.unverifying = true;
|
this.unverifying = true;
|
||||||
|
|
||||||
const process = async () => {
|
const process = async () => {
|
||||||
const user = await this.$root.api('users/show', parseAcct(this.verifyUsername));
|
const user = await this.fetchUser();
|
||||||
await this.$root.api('admin/unverify-user', { userId: user.id });
|
await this.$root.api('admin/unverify-user', { userId: user.id });
|
||||||
this.$root.alert({
|
this.$root.alert({
|
||||||
type: 'success',
|
type: 'success',
|
||||||
|
@ -99,7 +193,7 @@ export default Vue.extend({
|
||||||
this.suspending = true;
|
this.suspending = true;
|
||||||
|
|
||||||
const process = async () => {
|
const process = async () => {
|
||||||
const user = await this.$root.api('users/show', parseAcct(this.suspendUsername));
|
const user = await this.fetchUser();
|
||||||
await this.$root.api('admin/suspend-user', { userId: user.id });
|
await this.$root.api('admin/suspend-user', { userId: user.id });
|
||||||
this.$root.alert({
|
this.$root.alert({
|
||||||
type: 'success',
|
type: 'success',
|
||||||
|
@ -121,7 +215,7 @@ export default Vue.extend({
|
||||||
this.unsuspending = true;
|
this.unsuspending = true;
|
||||||
|
|
||||||
const process = async () => {
|
const process = async () => {
|
||||||
const user = await this.$root.api('users/show', parseAcct(this.suspendUsername));
|
const user = await this.fetchUser();
|
||||||
await this.$root.api('admin/unsuspend-user', { userId: user.id });
|
await this.$root.api('admin/unsuspend-user', { userId: user.id });
|
||||||
this.$root.alert({
|
this.$root.alert({
|
||||||
type: 'success',
|
type: 'success',
|
||||||
|
@ -137,6 +231,24 @@ export default Vue.extend({
|
||||||
});
|
});
|
||||||
|
|
||||||
this.unsuspending = false;
|
this.unsuspending = false;
|
||||||
|
},
|
||||||
|
|
||||||
|
fetchUsers() {
|
||||||
|
this.$root.api('users', {
|
||||||
|
origin: this.origin,
|
||||||
|
sort: this.sort,
|
||||||
|
offset: this.offset,
|
||||||
|
limit: this.limit + 1
|
||||||
|
}).then(users => {
|
||||||
|
if (users.length == this.limit + 1) {
|
||||||
|
users.pop();
|
||||||
|
this.existMore = true;
|
||||||
|
} else {
|
||||||
|
this.existMore = false;
|
||||||
|
}
|
||||||
|
this.users = this.users.concat(users);
|
||||||
|
this.offset += this.limit;
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
@ -147,4 +259,24 @@ export default Vue.extend({
|
||||||
@media (min-width 500px)
|
@media (min-width 500px)
|
||||||
padding 16px
|
padding 16px
|
||||||
|
|
||||||
|
.kofvwchc
|
||||||
|
display flex
|
||||||
|
padding 16px 0
|
||||||
|
border-top solid 1px var(--faceDivider)
|
||||||
|
|
||||||
|
> div:first-child
|
||||||
|
> a
|
||||||
|
> .avatar
|
||||||
|
width 64px
|
||||||
|
height 64px
|
||||||
|
|
||||||
|
> div:last-child
|
||||||
|
flex 1
|
||||||
|
padding-left 16px
|
||||||
|
|
||||||
|
> header
|
||||||
|
> .username
|
||||||
|
margin-left 8px
|
||||||
|
opacity 0.7
|
||||||
|
|
||||||
</style>
|
</style>
|
||||||
|
|
|
@ -5,7 +5,7 @@
|
||||||
<div class="icon" :class="type"><fa :icon="icon"/></div>
|
<div class="icon" :class="type"><fa :icon="icon"/></div>
|
||||||
<header v-if="title" v-html="title"></header>
|
<header v-if="title" v-html="title"></header>
|
||||||
<div class="body" v-if="text" v-html="text"></div>
|
<div class="body" v-if="text" v-html="text"></div>
|
||||||
<ui-horizon-group no-grow class="buttons" v-if="!splash">
|
<ui-horizon-group no-grow class="buttons fit-bottom" v-if="!splash">
|
||||||
<ui-button @click="ok" primary autofocus>OK</ui-button>
|
<ui-button @click="ok" primary autofocus>OK</ui-button>
|
||||||
<ui-button @click="cancel" v-if="showCancelButton">Cancel</ui-button>
|
<ui-button @click="cancel" v-if="showCancelButton">Cancel</ui-button>
|
||||||
</ui-horizon-group>
|
</ui-horizon-group>
|
||||||
|
|
|
@ -27,9 +27,17 @@ export default Vue.extend({
|
||||||
|
|
||||||
<style lang="stylus" scoped>
|
<style lang="stylus" scoped>
|
||||||
.vnxwkwuf
|
.vnxwkwuf
|
||||||
|
margin 16px 0
|
||||||
|
|
||||||
&.inputs
|
&.inputs
|
||||||
margin 32px 0
|
margin 32px 0
|
||||||
|
|
||||||
|
&.fit-top
|
||||||
|
margin-top 0
|
||||||
|
|
||||||
|
&.fit-bottom
|
||||||
|
margin-bottom 0
|
||||||
|
|
||||||
&:not(.noGrow)
|
&:not(.noGrow)
|
||||||
display flex
|
display flex
|
||||||
|
|
||||||
|
@ -37,5 +45,6 @@ export default Vue.extend({
|
||||||
flex 1
|
flex 1
|
||||||
|
|
||||||
> *:not(:last-child)
|
> *:not(:last-child)
|
||||||
margin-right 16px
|
margin-right 16px !important
|
||||||
|
|
||||||
</style>
|
</style>
|
||||||
|
|
|
@ -18,18 +18,21 @@
|
||||||
:autocomplete="autocomplete"
|
:autocomplete="autocomplete"
|
||||||
:spellcheck="spellcheck"
|
:spellcheck="spellcheck"
|
||||||
@focus="focused = true"
|
@focus="focused = true"
|
||||||
@blur="focused = false">
|
@blur="focused = false"
|
||||||
|
>
|
||||||
</template>
|
</template>
|
||||||
<template v-else>
|
<template v-else>
|
||||||
<input ref="input"
|
<input ref="input"
|
||||||
type="text"
|
type="text"
|
||||||
:value="placeholder"
|
:value="placeholder"
|
||||||
readonly
|
readonly
|
||||||
@click="chooseFile">
|
@click="chooseFile"
|
||||||
|
>
|
||||||
<input ref="file"
|
<input ref="file"
|
||||||
type="file"
|
type="file"
|
||||||
:value="value"
|
:value="value"
|
||||||
@change="onChangeFile">
|
@change="onChangeFile"
|
||||||
|
>
|
||||||
</template>
|
</template>
|
||||||
<div class="suffix" ref="suffix"><slot name="suffix"></slot></div>
|
<div class="suffix" ref="suffix"><slot name="suffix"></slot></div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -325,6 +328,9 @@ root(fill)
|
||||||
margin 6px 0
|
margin 6px 0
|
||||||
font-size 13px
|
font-size 13px
|
||||||
|
|
||||||
|
&:empty
|
||||||
|
display none
|
||||||
|
|
||||||
*
|
*
|
||||||
margin 0
|
margin 0
|
||||||
|
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
<template>
|
<template>
|
||||||
<div class="ui-select" :class="[{ focused, filled }, styl]">
|
<div class="ui-select" :class="[{ focused, disabled, filled, inline }, styl]">
|
||||||
<div class="icon" ref="icon"><slot name="icon"></slot></div>
|
<div class="icon" ref="icon"><slot name="icon"></slot></div>
|
||||||
<div class="input" @click="focus">
|
<div class="input" @click="focus">
|
||||||
<span class="label" ref="label"><slot name="label"></slot></span>
|
<span class="label" ref="label"><slot name="label"></slot></span>
|
||||||
|
@ -7,9 +7,11 @@
|
||||||
<select ref="input"
|
<select ref="input"
|
||||||
:value="v"
|
:value="v"
|
||||||
:required="required"
|
:required="required"
|
||||||
|
:disabled="disabled"
|
||||||
@input="$emit('input', $event.target.value)"
|
@input="$emit('input', $event.target.value)"
|
||||||
@focus="focused = true"
|
@focus="focused = true"
|
||||||
@blur="focused = false">
|
@blur="focused = false"
|
||||||
|
>
|
||||||
<slot></slot>
|
<slot></slot>
|
||||||
</select>
|
</select>
|
||||||
<div class="suffix"><slot name="suffix"></slot></div>
|
<div class="suffix"><slot name="suffix"></slot></div>
|
||||||
|
@ -22,6 +24,11 @@
|
||||||
import Vue from 'vue';
|
import Vue from 'vue';
|
||||||
|
|
||||||
export default Vue.extend({
|
export default Vue.extend({
|
||||||
|
inject: {
|
||||||
|
horizonGrouped: {
|
||||||
|
default: false
|
||||||
|
}
|
||||||
|
},
|
||||||
props: {
|
props: {
|
||||||
value: {
|
value: {
|
||||||
required: false
|
required: false
|
||||||
|
@ -30,12 +37,23 @@ export default Vue.extend({
|
||||||
type: Boolean,
|
type: Boolean,
|
||||||
required: false
|
required: false
|
||||||
},
|
},
|
||||||
|
disabled: {
|
||||||
|
type: Boolean,
|
||||||
|
required: false
|
||||||
|
},
|
||||||
styl: {
|
styl: {
|
||||||
type: String,
|
type: String,
|
||||||
required: false,
|
required: false,
|
||||||
default: 'line'
|
default: 'line'
|
||||||
|
},
|
||||||
|
inline: {
|
||||||
|
type: Boolean,
|
||||||
|
required: false,
|
||||||
|
default(): boolean {
|
||||||
|
return this.horizonGrouped;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
},
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
v: this.value,
|
v: this.value,
|
||||||
|
@ -122,7 +140,7 @@ root(fill)
|
||||||
transition-duration 0.3s
|
transition-duration 0.3s
|
||||||
font-size 16px
|
font-size 16px
|
||||||
line-height 32px
|
line-height 32px
|
||||||
color rgba(#000, 0.54)
|
color var(--inputLabel)
|
||||||
pointer-events none
|
pointer-events none
|
||||||
//will-change transform
|
//will-change transform
|
||||||
transform-origin top left
|
transform-origin top left
|
||||||
|
@ -171,6 +189,9 @@ root(fill)
|
||||||
margin 6px 0
|
margin 6px 0
|
||||||
font-size 13px
|
font-size 13px
|
||||||
|
|
||||||
|
&:empty
|
||||||
|
display none
|
||||||
|
|
||||||
*
|
*
|
||||||
margin 0
|
margin 0
|
||||||
|
|
||||||
|
@ -200,4 +221,14 @@ root(fill)
|
||||||
&:not(.fill)
|
&:not(.fill)
|
||||||
root(false)
|
root(false)
|
||||||
|
|
||||||
|
&.inline
|
||||||
|
display inline-block
|
||||||
|
margin 0
|
||||||
|
|
||||||
|
&.disabled
|
||||||
|
opacity 0.7
|
||||||
|
|
||||||
|
&, *
|
||||||
|
cursor not-allowed !important
|
||||||
|
|
||||||
</style>
|
</style>
|
||||||
|
|
|
@ -1,3 +1,10 @@
|
||||||
|
import Vue from 'vue';
|
||||||
|
import * as JSON5 from 'json5';
|
||||||
|
|
||||||
|
Vue.filter('json5', x => {
|
||||||
|
return JSON5.stringify(x, null, 2);
|
||||||
|
});
|
||||||
|
|
||||||
require('./bytes');
|
require('./bytes');
|
||||||
require('./number');
|
require('./number');
|
||||||
require('./user');
|
require('./user');
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
import Vue from 'vue';
|
import Vue from 'vue';
|
||||||
import getAcct from '../../../../../misc/acct/render';
|
import getAcct from '../../../../../misc/acct/render';
|
||||||
import getUserName from '../../../../../misc/get-user-name';
|
import getUserName from '../../../../../misc/get-user-name';
|
||||||
|
import { url } from '../../../config';
|
||||||
|
|
||||||
Vue.filter('acct', user => {
|
Vue.filter('acct', user => {
|
||||||
return getAcct(user);
|
return getAcct(user);
|
||||||
|
@ -10,6 +11,6 @@ Vue.filter('userName', user => {
|
||||||
return getUserName(user);
|
return getUserName(user);
|
||||||
});
|
});
|
||||||
|
|
||||||
Vue.filter('userPage', (user, path?) => {
|
Vue.filter('userPage', (user, path?, absolute = false) => {
|
||||||
return `/@${Vue.filter('acct')(user)}${(path ? `/${path}` : '')}`;
|
return `${absolute ? url : ''}/@${Vue.filter('acct')(user)}${(path ? `/${path}` : '')}`;
|
||||||
});
|
});
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
export default (acct: string) => {
|
export default (acct: string) => {
|
||||||
|
if (acct.startsWith('@')) acct = acct.substr(1);
|
||||||
const splitted = acct.split('@', 2);
|
const splitted = acct.split('@', 2);
|
||||||
return { username: splitted[0], host: splitted[1] || null };
|
return { username: splitted[0], host: splitted[1] || null };
|
||||||
};
|
};
|
||||||
|
|
|
@ -26,6 +26,7 @@ export default User;
|
||||||
type IUserBase = {
|
type IUserBase = {
|
||||||
_id: mongo.ObjectID;
|
_id: mongo.ObjectID;
|
||||||
createdAt: Date;
|
createdAt: Date;
|
||||||
|
updatedAt?: Date;
|
||||||
deletedAt?: Date;
|
deletedAt?: Date;
|
||||||
followersCount: number;
|
followersCount: number;
|
||||||
followingCount: number;
|
followingCount: number;
|
||||||
|
@ -104,7 +105,6 @@ export interface ILocalUser extends IUserBase {
|
||||||
birthday: string; // 'YYYY-MM-DD'
|
birthday: string; // 'YYYY-MM-DD'
|
||||||
tags: string[];
|
tags: string[];
|
||||||
};
|
};
|
||||||
lastUsedAt: Date;
|
|
||||||
isCat: boolean;
|
isCat: boolean;
|
||||||
isAdmin?: boolean;
|
isAdmin?: boolean;
|
||||||
isModerator?: boolean;
|
isModerator?: boolean;
|
||||||
|
@ -132,7 +132,7 @@ export interface IRemoteUser extends IUserBase {
|
||||||
id: string;
|
id: string;
|
||||||
publicKeyPem: string;
|
publicKeyPem: string;
|
||||||
};
|
};
|
||||||
updatedAt: Date;
|
lastFetchedAt: Date;
|
||||||
isAdmin: false;
|
isAdmin: false;
|
||||||
isModerator: false;
|
isModerator: false;
|
||||||
}
|
}
|
||||||
|
|
|
@ -104,7 +104,7 @@ export async function createNote(value: any, resolver?: Resolver, silent = false
|
||||||
});
|
});
|
||||||
|
|
||||||
// ユーザーの情報が古かったらついでに更新しておく
|
// ユーザーの情報が古かったらついでに更新しておく
|
||||||
if (actor.updatedAt == null || Date.now() - actor.updatedAt.getTime() > 1000 * 60 * 60 * 24) {
|
if (actor.lastFetchedAt == null || Date.now() - actor.lastFetchedAt.getTime() > 1000 * 60 * 60 * 24) {
|
||||||
updatePerson(note.attributedTo);
|
updatePerson(note.attributedTo);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -143,7 +143,7 @@ export async function createPerson(uri: string, resolver?: Resolver): Promise<IU
|
||||||
avatarId: null,
|
avatarId: null,
|
||||||
bannerId: null,
|
bannerId: null,
|
||||||
createdAt: Date.parse(person.published) || null,
|
createdAt: Date.parse(person.published) || null,
|
||||||
updatedAt: new Date(),
|
lastFetchedAt: new Date(),
|
||||||
description: htmlToMFM(person.summary),
|
description: htmlToMFM(person.summary),
|
||||||
followersCount,
|
followersCount,
|
||||||
followingCount,
|
followingCount,
|
||||||
|
@ -298,7 +298,7 @@ export async function updatePerson(uri: string, resolver?: Resolver, hint?: obje
|
||||||
// Update user
|
// Update user
|
||||||
await User.update({ _id: exist._id }, {
|
await User.update({ _id: exist._id }, {
|
||||||
$set: {
|
$set: {
|
||||||
updatedAt: new Date(),
|
lastFetchedAt: new Date(),
|
||||||
inbox: person.inbox,
|
inbox: person.inbox,
|
||||||
sharedInbox: person.sharedInbox,
|
sharedInbox: person.sharedInbox,
|
||||||
featured: person.featured,
|
featured: person.featured,
|
||||||
|
|
57
src/server/api/endpoints/admin/reset-password.ts
Normal file
57
src/server/api/endpoints/admin/reset-password.ts
Normal file
|
@ -0,0 +1,57 @@
|
||||||
|
import $ from 'cafy';
|
||||||
|
import ID, { transform } from '../../../../misc/cafy-id';
|
||||||
|
import define from '../../define';
|
||||||
|
import User from '../../../../models/user';
|
||||||
|
import * as bcrypt from 'bcryptjs';
|
||||||
|
import rndstr from 'rndstr';
|
||||||
|
|
||||||
|
export const meta = {
|
||||||
|
desc: {
|
||||||
|
'ja-JP': '指定したユーザーのパスワードをリセットします。',
|
||||||
|
},
|
||||||
|
|
||||||
|
requireCredential: true,
|
||||||
|
requireModerator: true,
|
||||||
|
|
||||||
|
params: {
|
||||||
|
userId: {
|
||||||
|
validator: $.type(ID),
|
||||||
|
transform: transform,
|
||||||
|
desc: {
|
||||||
|
'ja-JP': '対象のユーザーID',
|
||||||
|
'en-US': 'The user ID which you want to suspend'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
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');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (user.isAdmin) {
|
||||||
|
return rej('cannot reset password of admin');
|
||||||
|
}
|
||||||
|
|
||||||
|
const passwd = rndstr('a-zA-Z0-9', 8);
|
||||||
|
|
||||||
|
// Generate hash of password
|
||||||
|
const hash = bcrypt.hashSync(passwd);
|
||||||
|
|
||||||
|
await User.findOneAndUpdate({
|
||||||
|
_id: user._id
|
||||||
|
}, {
|
||||||
|
$set: {
|
||||||
|
password: hash
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
res({
|
||||||
|
password: passwd
|
||||||
|
});
|
||||||
|
}));
|
40
src/server/api/endpoints/admin/show-user.ts
Normal file
40
src/server/api/endpoints/admin/show-user.ts
Normal file
|
@ -0,0 +1,40 @@
|
||||||
|
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': '指定したユーザーの情報を取得します。',
|
||||||
|
},
|
||||||
|
|
||||||
|
requireCredential: true,
|
||||||
|
requireModerator: true,
|
||||||
|
|
||||||
|
params: {
|
||||||
|
userId: {
|
||||||
|
validator: $.type(ID),
|
||||||
|
transform: transform,
|
||||||
|
desc: {
|
||||||
|
'ja-JP': '対象のユーザーID',
|
||||||
|
'en-US': 'The user ID which you want to suspend'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export default define(meta, (ps, me) => new Promise(async (res, rej) => {
|
||||||
|
const user = await User.findOne({
|
||||||
|
_id: ps.userId
|
||||||
|
});
|
||||||
|
|
||||||
|
if (user == null) {
|
||||||
|
return rej('user not found');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (me.isModerator && user.isAdmin) {
|
||||||
|
return rej('cannot show info of admin');
|
||||||
|
}
|
||||||
|
|
||||||
|
res(user);
|
||||||
|
}));
|
|
@ -17,7 +17,23 @@ export const meta = {
|
||||||
},
|
},
|
||||||
|
|
||||||
sort: {
|
sort: {
|
||||||
validator: $.str.optional.or('+follower|-follower'),
|
validator: $.str.optional.or([
|
||||||
|
'+follower',
|
||||||
|
'-follower',
|
||||||
|
'+createdAt',
|
||||||
|
'-createdAt',
|
||||||
|
'+updatedAt',
|
||||||
|
'-updatedAt',
|
||||||
|
]),
|
||||||
|
},
|
||||||
|
|
||||||
|
origin: {
|
||||||
|
validator: $.str.optional.or([
|
||||||
|
'combined',
|
||||||
|
'local',
|
||||||
|
'remote',
|
||||||
|
]),
|
||||||
|
default: 'local'
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
@ -33,6 +49,22 @@ export default define(meta, (ps, me) => new Promise(async (res, rej) => {
|
||||||
_sort = {
|
_sort = {
|
||||||
followersCount: 1
|
followersCount: 1
|
||||||
};
|
};
|
||||||
|
} else if (ps.sort == '+createdAt') {
|
||||||
|
_sort = {
|
||||||
|
createdAt: -1
|
||||||
|
};
|
||||||
|
} else if (ps.sort == '+updatedAt') {
|
||||||
|
_sort = {
|
||||||
|
updatedAt: -1
|
||||||
|
};
|
||||||
|
} else if (ps.sort == '-createdAt') {
|
||||||
|
_sort = {
|
||||||
|
createdAt: 1
|
||||||
|
};
|
||||||
|
} else if (ps.sort == '-updatedAt') {
|
||||||
|
_sort = {
|
||||||
|
updatedAt: 1
|
||||||
|
};
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
_sort = {
|
_sort = {
|
||||||
|
@ -40,14 +72,17 @@ export default define(meta, (ps, me) => new Promise(async (res, rej) => {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const q =
|
||||||
|
ps.origin == 'local' ? { host: null } :
|
||||||
|
ps.origin == 'remote' ? { host: { $ne: null } } :
|
||||||
|
{};
|
||||||
|
|
||||||
const users = await User
|
const users = await User
|
||||||
.find({
|
.find(q, {
|
||||||
host: null
|
|
||||||
}, {
|
|
||||||
limit: ps.limit,
|
limit: ps.limit,
|
||||||
sort: _sort,
|
sort: _sort,
|
||||||
skip: ps.offset
|
skip: ps.offset
|
||||||
});
|
});
|
||||||
|
|
||||||
res(await Promise.all(users.map(user => pack(user, me))));
|
res(await Promise.all(users.map(user => pack(user, me, { detail: true }))));
|
||||||
}));
|
}));
|
||||||
|
|
|
@ -80,7 +80,7 @@ export default define(meta, (ps, me) => new Promise(async (res, rej) => {
|
||||||
}));
|
}));
|
||||||
|
|
||||||
if (isRemoteUser(user)) {
|
if (isRemoteUser(user)) {
|
||||||
if (user.updatedAt == null || Date.now() - user.updatedAt.getTime() > 1000 * 60 * 60 * 24) {
|
if (user.lastFetchedAt == null || Date.now() - user.lastFetchedAt.getTime() > 1000 * 60 * 60 * 24) {
|
||||||
resolveRemoteUser(ps.username, ps.host, { }, true);
|
resolveRemoteUser(ps.username, ps.host, { }, true);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -633,6 +633,9 @@ function saveReply(reply: INote, note: INote) {
|
||||||
|
|
||||||
function incNotesCountOfUser(user: IUser) {
|
function incNotesCountOfUser(user: IUser) {
|
||||||
User.update({ _id: user._id }, {
|
User.update({ _id: user._id }, {
|
||||||
|
$set: {
|
||||||
|
updatedAt: new Date()
|
||||||
|
},
|
||||||
$inc: {
|
$inc: {
|
||||||
notesCount: 1
|
notesCount: 1
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue