user moderation panel #211
15 changed files with 646 additions and 11 deletions
|
@ -69,7 +69,7 @@
|
||||||
padding: 1rem 1rem;
|
padding: 1rem 1rem;
|
||||||
background-color: $fallback--bg;
|
background-color: $fallback--bg;
|
||||||
background-color: var(--bg, $fallback--bg);
|
background-color: var(--bg, $fallback--bg);
|
||||||
white-space: normal;
|
white-space: pre-wrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
.dialog-modal-footer {
|
.dialog-modal-footer {
|
||||||
|
|
|
@ -24,7 +24,6 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
.mod-modal-panel {
|
.mod-modal-panel {
|
||||||
overflow: hidden;
|
|
||||||
transition: transform;
|
transition: transform;
|
||||||
transition-timing-function: ease-in-out;
|
transition-timing-function: ease-in-out;
|
||||||
transition-duration: 300ms;
|
transition-duration: 300ms;
|
||||||
|
@ -39,6 +38,16 @@
|
||||||
|
|
||||||
.panel-body {
|
.panel-body {
|
||||||
height: inherit;
|
height: inherit;
|
||||||
|
overflow-y: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pagination-container {
|
||||||
|
display: flex;
|
||||||
|
justify-content: right;
|
||||||
|
|
||||||
|
.btn {
|
||||||
|
margin-left: 0.6em;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -35,6 +35,9 @@
|
||||||
<div class="panel-body">
|
<div class="panel-body">
|
||||||
<ModModalContent v-if="modalOpenedOnce" />
|
<ModModalContent v-if="modalOpenedOnce" />
|
||||||
</div>
|
</div>
|
||||||
|
<div class="panel-footer settings-footer">
|
||||||
|
<span id="navigation" />
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</Modal>
|
</Modal>
|
||||||
</template>
|
</template>
|
||||||
|
|
|
@ -2,7 +2,7 @@ import TabSwitcher from 'src/components/tab_switcher/tab_switcher.jsx'
|
||||||
|
|
||||||
import ReportsTab from './tabs/reports_tab/reports_tab.vue'
|
import ReportsTab from './tabs/reports_tab/reports_tab.vue'
|
||||||
// import StatusesTab from './tabs/statuses_tab.vue'
|
// import StatusesTab from './tabs/statuses_tab.vue'
|
||||||
// import UsersTab from './tabs/users_tab.vue'
|
import UsersTab from './tabs/users_tab/users_tab.vue'
|
||||||
|
|
||||||
import { library } from '@fortawesome/fontawesome-svg-core'
|
import { library } from '@fortawesome/fontawesome-svg-core'
|
||||||
import {
|
import {
|
||||||
|
@ -21,9 +21,9 @@ const ModModalContent = {
|
||||||
components: {
|
components: {
|
||||||
TabSwitcher,
|
TabSwitcher,
|
||||||
|
|
||||||
ReportsTab
|
ReportsTab,
|
||||||
// StatusesTab,
|
// StatusesTab,
|
||||||
// UsersTab
|
UsersTab
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
open () {
|
open () {
|
||||||
|
|
|
@ -13,6 +13,13 @@
|
||||||
>
|
>
|
||||||
<ReportsTab />
|
<ReportsTab />
|
||||||
</div>
|
</div>
|
||||||
|
<div
|
||||||
|
:label="$t('moderation.users.users')"
|
||||||
|
icon="users"
|
||||||
|
data-tab-name="users"
|
||||||
|
>
|
||||||
|
<UsersTab />
|
||||||
|
</div>
|
||||||
</tab-switcher>
|
</tab-switcher>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
|
116
src/components/mod_modal/tabs/users_tab/users_tab.js
Normal file
116
src/components/mod_modal/tabs/users_tab/users_tab.js
Normal file
|
@ -0,0 +1,116 @@
|
||||||
|
import BasicUserCard from 'src/components/basic_user_card/basic_user_card.vue'
|
||||||
|
import ModerationTools from 'src/components/moderation_tools/moderation_tools.vue'
|
||||||
|
import Popover from 'src/components/popover/popover.vue'
|
||||||
|
|
||||||
|
import { forEach, every, findKey } from 'lodash'
|
||||||
|
|
||||||
|
import { library } from '@fortawesome/fontawesome-svg-core'
|
||||||
|
import {
|
||||||
|
faFilter,
|
||||||
|
faSearch
|
||||||
|
} from '@fortawesome/free-solid-svg-icons'
|
||||||
|
|
||||||
|
library.add(
|
||||||
|
faFilter,
|
||||||
|
faSearch
|
||||||
|
)
|
||||||
|
|
||||||
|
const UsersTab = {
|
||||||
|
data () {
|
||||||
|
return {
|
||||||
|
searchTerm: null,
|
||||||
|
page: 1,
|
||||||
|
accountType: {
|
||||||
|
local: true,
|
||||||
|
external: false
|
||||||
|
},
|
||||||
|
status: {
|
||||||
|
active: true,
|
||||||
|
deactivated: false,
|
||||||
|
need_approval: false,
|
||||||
|
unconfirmed: false
|
||||||
|
},
|
||||||
|
actorType: {
|
||||||
|
Person: false,
|
||||||
|
Service: false,
|
||||||
|
Application: false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
components: {
|
||||||
|
BasicUserCard,
|
||||||
|
ModerationTools,
|
||||||
|
Popover
|
||||||
|
},
|
||||||
|
created () {
|
||||||
|
this.query()
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
users () { return this.$store.state.users.adminUsers },
|
||||||
|
isActive () {
|
||||||
|
const tabSwitcher = this.$parent
|
||||||
|
return tabSwitcher ? tabSwitcher.isActive('users') : false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
all (filter) { return every(filter, _ => !_) },
|
||||||
|
setAccountType (type = false) {
|
||||||
|
forEach(this.accountType, (k, v) => { this.accountType[v] = false })
|
||||||
|
if (type) {
|
||||||
|
this.accountType[type] = true
|
||||||
|
}
|
||||||
|
|
||||||
|
this.page = 1
|
||||||
|
this.query()
|
||||||
|
},
|
||||||
|
setStatus (status = false) {
|
||||||
|
forEach(this.status, (k, v) => { this.status[v] = false })
|
||||||
|
if (status) {
|
||||||
|
this.status[status] = true
|
||||||
|
}
|
||||||
|
|
||||||
|
this.page = 1
|
||||||
|
this.query()
|
||||||
|
},
|
||||||
|
setActorType (type = false) {
|
||||||
|
forEach(this.actorType, (k, v) => { this.actorType[v] = false })
|
||||||
|
if (type) {
|
||||||
|
this.actorType[type] = true
|
||||||
|
}
|
||||||
|
|
||||||
|
this.page = 1
|
||||||
|
this.query()
|
||||||
|
},
|
||||||
|
search () {
|
||||||
|
this.page = 1
|
||||||
|
this.query()
|
||||||
|
},
|
||||||
|
prevPage () {
|
||||||
|
this.page--
|
||||||
|
this.query()
|
||||||
|
},
|
||||||
|
nextPage () {
|
||||||
|
this.page++
|
||||||
|
this.query()
|
||||||
|
},
|
||||||
|
query () {
|
||||||
|
const params = {}
|
||||||
|
params.actorTypes = [findKey(this.actorType, _ => _)].filter(Boolean)
|
||||||
|
params.filters = [
|
||||||
|
findKey(this.status, _ => _),
|
||||||
|
findKey(this.accountType, _ => _)
|
||||||
|
].filter(Boolean)
|
||||||
|
|
||||||
|
if (this.searchTerm) {
|
||||||
|
params.name = this.searchTerm
|
||||||
|
}
|
||||||
|
if (this.page > 1) {
|
||||||
|
params.page = this.page
|
||||||
|
}
|
||||||
|
|
||||||
|
this.$store.dispatch('fetchUsers', params)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default UsersTab
|
60
src/components/mod_modal/tabs/users_tab/users_tab.scss
Normal file
60
src/components/mod_modal/tabs/users_tab/users_tab.scss
Normal file
|
@ -0,0 +1,60 @@
|
||||||
|
@import '../../../../_variables.scss';
|
||||||
|
|
||||||
|
.users-header {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
|
||||||
|
.right-side {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
|
||||||
|
.search-input-container {
|
||||||
|
padding: 0.8rem;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
|
||||||
|
.search-input {
|
||||||
|
width: 100%;
|
||||||
|
line-height: 1.125rem;
|
||||||
|
font-size: 1rem;
|
||||||
|
padding: 0.5rem;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
.search-icon {
|
||||||
|
margin-right: 0.3em;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.users .user {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
|
||||||
|
.user-moderation {
|
||||||
|
padding-top: 0.6em;
|
||||||
|
position: relative;
|
||||||
|
|
||||||
|
.popover {
|
||||||
|
right: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.user-information {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
padding-bottom: 0.6em;
|
||||||
|
flex: 1;
|
||||||
|
|
||||||
|
.basic-user-card {
|
||||||
|
padding-bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.registration-reason {
|
||||||
|
padding-left: 5.2em;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
211
src/components/mod_modal/tabs/users_tab/users_tab.vue
Normal file
211
src/components/mod_modal/tabs/users_tab/users_tab.vue
Normal file
|
@ -0,0 +1,211 @@
|
||||||
|
<template>
|
||||||
|
<div :label="$t('moderation.users.users')">
|
||||||
|
<div class="content">
|
||||||
|
<div class="users-header">
|
||||||
|
<h2>{{ $t('moderation.users.users') }}</h2>
|
||||||
|
<div class="right-side">
|
||||||
|
<div class="search-input-container">
|
||||||
|
<div class="input-search">
|
||||||
|
<FAIcon
|
||||||
|
class="search-icon fa-scale-110 fa-old-padding"
|
||||||
|
icon="search"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<input
|
||||||
|
v-model="searchTerm"
|
||||||
|
class="search-input"
|
||||||
|
:placeholder="$t('nav.search')"
|
||||||
|
@keyup.enter="search()"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<Popover
|
||||||
|
trigger="click"
|
||||||
|
placement="bottom"
|
||||||
|
:offset="{ y: 5 }"
|
||||||
|
remove-padding
|
||||||
|
>
|
||||||
|
<template v-slot:trigger>
|
||||||
|
<button class="button-unstyled">
|
||||||
|
<FAIcon icon="filter" />
|
||||||
|
</button>
|
||||||
|
</template>
|
||||||
|
<template v-slot:content>
|
||||||
|
<div class="dropdown-menu">
|
||||||
|
<button
|
||||||
|
class="button-default dropdown-item"
|
||||||
|
@click="setAccountType('local')"
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
class="menu-checkbox -radio"
|
||||||
|
:class="{ 'menu-checkbox-checked': accountType.local }"
|
||||||
|
/>{{ $t('moderation.users.filter.local') }}
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
class="button-default dropdown-item"
|
||||||
|
@click="setAccountType('external')"
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
class="menu-checkbox -radio"
|
||||||
|
:class="{ 'menu-checkbox-checked': accountType.external }"
|
||||||
|
/>{{ $t('moderation.users.filter.external') }}
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
class="button-default dropdown-item"
|
||||||
|
@click="setAccountType()"
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
class="menu-checkbox -radio"
|
||||||
|
:class="{ 'menu-checkbox-checked': all(accountType) }"
|
||||||
|
/>{{ $t('moderation.users.filter.all') }}
|
||||||
|
</button>
|
||||||
|
<div
|
||||||
|
role="separator"
|
||||||
|
class="dropdown-divider"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="dropdown-menu">
|
||||||
|
<button
|
||||||
|
class="button-default dropdown-item"
|
||||||
|
@click="setStatus('active')"
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
class="menu-checkbox -radio"
|
||||||
|
:class="{ 'menu-checkbox-checked': status.active }"
|
||||||
|
/>{{ $t('moderation.users.filter.active') }}
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
class="button-default dropdown-item"
|
||||||
|
@click="setStatus('deactivated')"
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
class="menu-checkbox -radio"
|
||||||
|
:class="{ 'menu-checkbox-checked': status.deactivated }"
|
||||||
|
/>{{ $t('moderation.users.filter.deactivated') }}
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
class="button-default dropdown-item"
|
||||||
|
@click="setStatus('need_approval')"
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
class="menu-checkbox -radio"
|
||||||
|
:class="{ 'menu-checkbox-checked': status.need_approval }"
|
||||||
|
/>{{ $t('moderation.users.filter.pending') }}
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
class="button-default dropdown-item"
|
||||||
|
@click="setStatus('unconfirmed')"
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
class="menu-checkbox -radio"
|
||||||
|
:class="{ 'menu-checkbox-checked': status.unconfirmed }"
|
||||||
|
/>{{ $t('moderation.users.filter.unconfirmed') }}
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
class="button-default dropdown-item"
|
||||||
|
@click="setStatus()"
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
class="menu-checkbox -radio"
|
||||||
|
:class="{ 'menu-checkbox-checked': all(status) }"
|
||||||
|
/>{{ $t('moderation.users.filter.all') }}
|
||||||
|
</button>
|
||||||
|
<div
|
||||||
|
role="separator"
|
||||||
|
class="dropdown-divider"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="dropdown-menu">
|
||||||
|
<button
|
||||||
|
class="button-default dropdown-item"
|
||||||
|
@click="setActorType('Person')"
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
class="menu-checkbox -radio"
|
||||||
|
:class="{ 'menu-checkbox-checked': actorType.Person }"
|
||||||
|
/>{{ $t('moderation.users.filter.person') }}
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
class="button-default dropdown-item"
|
||||||
|
@click="setActorType('Service')"
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
class="menu-checkbox -radio"
|
||||||
|
:class="{ 'menu-checkbox-checked': actorType.Service }"
|
||||||
|
/>{{ $t('moderation.users.filter.bot') }}
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
class="button-default dropdown-item"
|
||||||
|
@click="setActorType('Application')"
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
class="menu-checkbox -radio"
|
||||||
|
:class="{ 'menu-checkbox-checked': actorType.Application }"
|
||||||
|
/>{{ $t('moderation.users.filter.application') }}
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
class="button-default dropdown-item"
|
||||||
|
@click="setActorType()"
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
class="menu-checkbox -radio"
|
||||||
|
:class="{ 'menu-checkbox-checked': all(actorType) }"
|
||||||
|
/>{{ $t('moderation.users.filter.all') }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</Popover>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="users">
|
||||||
|
<div v-if="users.length == 0 && page == 1">
|
||||||
|
<p>{{ $t('moderation.users.no_results') }}</p>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
v-for="user in users"
|
||||||
|
:key="user.id"
|
||||||
|
class="user"
|
||||||
|
>
|
||||||
|
<div class="user-information">
|
||||||
|
<BasicUserCard :user="user" />
|
||||||
|
<div
|
||||||
|
v-if="status.need_approval"
|
||||||
|
class="registration-reason"
|
||||||
|
>
|
||||||
|
{{ user.registration_reason }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<ModerationTools
|
||||||
|
:user="user"
|
||||||
|
class="user-moderation"
|
||||||
|
extended="true"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<teleport
|
||||||
|
v-if="isActive"
|
||||||
|
to="#navigation"
|
||||||
|
>
|
||||||
|
<div class="pagination-container">
|
||||||
|
<button
|
||||||
|
class="btn button-default submit"
|
||||||
|
:disabled="page == 1"
|
||||||
|
@click="prevPage"
|
||||||
|
>
|
||||||
|
{{ $t('moderation.previous') }}
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
class="btn button-default"
|
||||||
|
:disabled="users.length < 50"
|
||||||
|
@click="nextPage"
|
||||||
|
>
|
||||||
|
{{ $t('moderation.next') }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</teleport>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script src="./users_tab.js"></script>
|
||||||
|
<style src="./users_tab.scss" lang="scss"></style>
|
|
@ -16,7 +16,8 @@ const QUARANTINE = 'mrf_tag:quarantine'
|
||||||
|
|
||||||
const ModerationTools = {
|
const ModerationTools = {
|
||||||
props: [
|
props: [
|
||||||
'user'
|
'user',
|
||||||
|
'extended'
|
||||||
],
|
],
|
||||||
data () {
|
data () {
|
||||||
return {
|
return {
|
||||||
|
@ -30,7 +31,10 @@ const ModerationTools = {
|
||||||
QUARANTINE
|
QUARANTINE
|
||||||
},
|
},
|
||||||
showDeleteUserDialog: false,
|
showDeleteUserDialog: false,
|
||||||
toggled: false
|
showPasswordTokenDialog: false,
|
||||||
|
toggled: false,
|
||||||
|
passwordResetToken: {},
|
||||||
|
bot: this.user.bot
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
components: {
|
components: {
|
||||||
|
@ -83,6 +87,9 @@ const ModerationTools = {
|
||||||
deleteUserDialog (show) {
|
deleteUserDialog (show) {
|
||||||
this.showDeleteUserDialog = show
|
this.showDeleteUserDialog = show
|
||||||
},
|
},
|
||||||
|
passwordTokenDialog (show) {
|
||||||
|
this.showPasswordTokenDialog = show
|
||||||
|
},
|
||||||
deleteUser () {
|
deleteUser () {
|
||||||
const store = this.$store
|
const store = this.$store
|
||||||
const user = this.user
|
const user = this.user
|
||||||
|
@ -97,6 +104,32 @@ const ModerationTools = {
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
|
approveAccount () {
|
||||||
|
this.$store.state.api.backendInteractor.approveAccount({ nickname: this.user.screen_name })
|
||||||
|
},
|
||||||
|
getPasswordResetToken () {
|
||||||
|
this.$store.state.api.backendInteractor.getPasswordResetToken({ nickname: this.user.screen_name })
|
||||||
|
.then(data => {
|
||||||
|
this.passwordResetToken = data
|
||||||
|
this.passwordTokenDialog(true)
|
||||||
|
})
|
||||||
|
},
|
||||||
|
forcePasswordReset () {
|
||||||
|
this.$store.state.api.backendInteractor.forcePasswordReset({ nickname: this.user.screen_name })
|
||||||
|
},
|
||||||
|
forceDisableMFA () {
|
||||||
|
this.$store.state.api.backendInteractor.forceDisableMFA({ nickname: this.user.screen_name })
|
||||||
|
},
|
||||||
|
toggleBot () {
|
||||||
|
const params = { bot: !this.bot }
|
||||||
|
|
||||||
|
this.$store.state.api.backendInteractor
|
||||||
|
.updateProfile({ params })
|
||||||
|
.then((user) => {
|
||||||
|
this.$store.commit('addNewUsers', [user])
|
||||||
|
this.bot = !this.bot
|
||||||
|
})
|
||||||
|
},
|
||||||
setToggled (value) {
|
setToggled (value) {
|
||||||
this.toggled = value
|
this.toggled = value
|
||||||
}
|
}
|
||||||
|
|
|
@ -5,6 +5,7 @@
|
||||||
class="moderation-tools-popover"
|
class="moderation-tools-popover"
|
||||||
placement="bottom"
|
placement="bottom"
|
||||||
:offset="{ y: 5 }"
|
:offset="{ y: 5 }"
|
||||||
|
:noCenter="extended"
|
||||||
@show="setToggled(true)"
|
@show="setToggled(true)"
|
||||||
@close="setToggled(false)"
|
@close="setToggled(false)"
|
||||||
>
|
>
|
||||||
|
@ -28,6 +29,25 @@
|
||||||
class="dropdown-divider"
|
class="dropdown-divider"
|
||||||
/>
|
/>
|
||||||
</span>
|
</span>
|
||||||
|
<span v-if="!user.approved && user.is_local">
|
||||||
|
<button
|
||||||
|
class="button-default dropdown-item"
|
||||||
|
@click="approveAccount()"
|
||||||
|
>
|
||||||
|
{{ $t('user_card.admin_menu.approve_account') }}
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
class="button-default dropdown-item"
|
||||||
|
@click="deleteUserDialog(true)"
|
||||||
|
>
|
||||||
|
{{ $t('user_card.admin_menu.reject_account') }}
|
||||||
|
</button>
|
||||||
|
</span>
|
||||||
|
<div
|
||||||
|
v-if="!user.approved && user.is_local"
|
||||||
|
role="separator"
|
||||||
|
class="dropdown-divider"
|
||||||
|
/>
|
||||||
<button
|
<button
|
||||||
class="button-default dropdown-item"
|
class="button-default dropdown-item"
|
||||||
@click="toggleActivationStatus()"
|
@click="toggleActivationStatus()"
|
||||||
|
@ -120,6 +140,37 @@
|
||||||
{{ $t('user_card.admin_menu.quarantine') }}
|
{{ $t('user_card.admin_menu.quarantine') }}
|
||||||
</button>
|
</button>
|
||||||
</span>
|
</span>
|
||||||
|
<div
|
||||||
|
v-if="extended && user.is_local"
|
||||||
|
role="separator"
|
||||||
|
class="dropdown-divider"
|
||||||
|
/>
|
||||||
|
<span v-if="extended && user.is_local">
|
||||||
|
<button
|
||||||
|
class="button-default dropdown-item"
|
||||||
|
@click="getPasswordResetToken"
|
||||||
|
>
|
||||||
|
{{ $t('user_card.admin_menu.get_password_reset_token') }}
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
class="button-default dropdown-item"
|
||||||
|
@click="forcePasswordReset"
|
||||||
|
>
|
||||||
|
{{ $t('user_card.admin_menu.force_password_reset') }}
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
class="button-default dropdown-item"
|
||||||
|
@click="forceDisableMFA"
|
||||||
|
>
|
||||||
|
{{ $t('user_card.admin_menu.disable_mfa') }}
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
class="button-default dropdown-item"
|
||||||
|
@click="toggleBot"
|
||||||
|
>
|
||||||
|
{{ $t('user_card.admin_menu.convert_to', { type: !!bot ? 'Person' : 'Service' }) }}
|
||||||
|
</button>
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<template v-slot:trigger>
|
<template v-slot:trigger>
|
||||||
|
@ -140,7 +191,7 @@
|
||||||
<template v-slot:header>
|
<template v-slot:header>
|
||||||
{{ $t('user_card.admin_menu.delete_user') }}
|
{{ $t('user_card.admin_menu.delete_user') }}
|
||||||
</template>
|
</template>
|
||||||
<p>{{ $t('user_card.admin_menu.delete_user_confirmation') }}</p>
|
<p>{{ $t('user_card.admin_menu.delete_user_data_and_deactivate_confirmation') }}</p>
|
||||||
<template v-slot:footer>
|
<template v-slot:footer>
|
||||||
<button
|
<button
|
||||||
class="btn button-default"
|
class="btn button-default"
|
||||||
|
@ -156,6 +207,26 @@
|
||||||
</button>
|
</button>
|
||||||
</template>
|
</template>
|
||||||
</DialogModal>
|
</DialogModal>
|
||||||
|
<DialogModal
|
||||||
|
v-if="showPasswordTokenDialog"
|
||||||
|
:on-cancel="passwordTokenDialog.bind(this, false)"
|
||||||
|
>
|
||||||
|
<template v-slot:header>
|
||||||
|
{{ $t('user_card.admin_menu.password_reset_token') }}
|
||||||
|
</template>
|
||||||
|
<p>
|
||||||
|
{{ $t('user_card.admin_menu.password_reset_token_content', { token: passwordResetToken.token }) }}
|
||||||
|
<a :href="passwordResetToken.link">{{ passwordResetToken.link }}</a>
|
||||||
|
</p>
|
||||||
|
<template v-slot:footer>
|
||||||
|
<button
|
||||||
|
class="btn button-default"
|
||||||
|
@click="passwordTokenDialog(false)"
|
||||||
|
>
|
||||||
|
{{ $t('general.close') }}
|
||||||
|
</button>
|
||||||
|
</template>
|
||||||
|
</DialogModal>
|
||||||
</teleport>
|
</teleport>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
|
@ -31,7 +31,11 @@ const Popover = {
|
||||||
|
|
||||||
// If true, subtract padding when calculating position for the popover,
|
// If true, subtract padding when calculating position for the popover,
|
||||||
// use it when popover offset looks to be different on top vs bottom.
|
// use it when popover offset looks to be different on top vs bottom.
|
||||||
removePadding: Boolean
|
removePadding: Boolean,
|
||||||
|
|
||||||
|
// If true, do not center the popover under the button that called it,
|
||||||
|
// instead aligning it to the right.
|
||||||
|
noCenter: Boolean
|
||||||
},
|
},
|
||||||
data () {
|
data () {
|
||||||
return {
|
return {
|
||||||
|
@ -121,7 +125,10 @@ const Popover = {
|
||||||
: yOffset
|
: yOffset
|
||||||
|
|
||||||
const xOffset = (this.offset && this.offset.x) || 0
|
const xOffset = (this.offset && this.offset.x) || 0
|
||||||
const translateX = anchorWidth * 0.5 - content.offsetWidth * 0.5 + horizOffset + xOffset
|
let translateX = translateX = anchorWidth * 0.5 - content.offsetWidth * 0.5 + horizOffset + xOffset
|
||||||
|
if (this.noCenter) {
|
||||||
|
translateX = 0
|
||||||
|
}
|
||||||
|
|
||||||
// Note, separate translateX and translateY avoids blurry text on chromium,
|
// Note, separate translateX and translateY avoids blurry text on chromium,
|
||||||
// single translate or translate3d resulted in blurry text.
|
// single translate or translate3d resulted in blurry text.
|
||||||
|
|
|
@ -294,7 +294,24 @@
|
||||||
"tags": "Set post restrictions"
|
"tags": "Set post restrictions"
|
||||||
},
|
},
|
||||||
"statuses": "Posts",
|
"statuses": "Posts",
|
||||||
"users": "Users"
|
"next": "Next page",
|
||||||
|
"previous": "Previous page",
|
||||||
|
"users": {
|
||||||
|
"filter": {
|
||||||
|
"active": "Active",
|
||||||
|
"all": "All accounts",
|
||||||
|
"application": "Application",
|
||||||
|
"bot": "Bot",
|
||||||
|
"deactivated": "Deactivated",
|
||||||
|
"external": "External",
|
||||||
|
"local": "Local",
|
||||||
|
"pending": "Pending Approval",
|
||||||
|
"person": "Person",
|
||||||
|
"unconfirmed": "Unconfirmed"
|
||||||
|
},
|
||||||
|
"no_results": "No accounts found",
|
||||||
|
"users": "Users"
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"nav": {
|
"nav": {
|
||||||
"about": "About",
|
"about": "About",
|
||||||
|
@ -1094,18 +1111,26 @@
|
||||||
"user_card": {
|
"user_card": {
|
||||||
"admin_menu": {
|
"admin_menu": {
|
||||||
"activate_account": "Activate account",
|
"activate_account": "Activate account",
|
||||||
|
"approve_account": "Approve account",
|
||||||
|
"convert_to": "Convert to { type }",
|
||||||
"deactivate_account": "Deactivate account",
|
"deactivate_account": "Deactivate account",
|
||||||
"delete_account": "Delete account",
|
"delete_account": "Delete account",
|
||||||
"delete_user": "Delete user",
|
"delete_user": "Delete user",
|
||||||
"delete_user_data_and_deactivate_confirmation": "This will permanently delete the data from this account and deactivate it. Are you absolutely sure?",
|
"delete_user_data_and_deactivate_confirmation": "This will permanently delete the data from this account and deactivate it. Are you absolutely sure?",
|
||||||
"disable_any_subscription": "Disallow following user at all",
|
"disable_any_subscription": "Disallow following user at all",
|
||||||
|
"disable_mfa": "Disable multi-factor authentication",
|
||||||
"disable_remote_subscription": "Disallow following user from remote instances",
|
"disable_remote_subscription": "Disallow following user from remote instances",
|
||||||
"force_nsfw": "Mark all posts as NSFW",
|
"force_nsfw": "Mark all posts as NSFW",
|
||||||
|
"force_password_reset": "Require password reset on next login",
|
||||||
"force_unlisted": "Force posts to be unlisted",
|
"force_unlisted": "Force posts to be unlisted",
|
||||||
|
"get_password_reset_token": "Get password reset token",
|
||||||
"grant_admin": "Grant Admin",
|
"grant_admin": "Grant Admin",
|
||||||
"grant_moderator": "Grant Moderator",
|
"grant_moderator": "Grant Moderator",
|
||||||
"moderation": "Moderation",
|
"moderation": "Moderation",
|
||||||
|
"password_reset_token": "Password reset token",
|
||||||
|
"password_reset_token_content": "Password reset token has been generated: { token }\nYou can also use this link to reset the password: ",
|
||||||
"quarantine": "Disallow user posts from federating",
|
"quarantine": "Disallow user posts from federating",
|
||||||
|
"reject_account": "Reject account",
|
||||||
"revoke_admin": "Revoke Admin",
|
"revoke_admin": "Revoke Admin",
|
||||||
"revoke_moderator": "Revoke Moderator",
|
"revoke_moderator": "Revoke Moderator",
|
||||||
"sandbox": "Force posts to be followers-only",
|
"sandbox": "Force posts to be followers-only",
|
||||||
|
|
|
@ -185,6 +185,9 @@ export const mutations = {
|
||||||
user['followedTagIds'] = []
|
user['followedTagIds'] = []
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
adminSetUsers (state, users) {
|
||||||
|
state.adminUsers = users
|
||||||
|
},
|
||||||
addNewUsers (state, users) {
|
addNewUsers (state, users) {
|
||||||
each(users, (user) => {
|
each(users, (user) => {
|
||||||
if (user.relationship) {
|
if (user.relationship) {
|
||||||
|
@ -294,6 +297,7 @@ export const defaultState = {
|
||||||
currentUser: false,
|
currentUser: false,
|
||||||
users: [],
|
users: [],
|
||||||
usersObject: {},
|
usersObject: {},
|
||||||
|
adminUsers: [],
|
||||||
signUpPending: false,
|
signUpPending: false,
|
||||||
signUpErrors: [],
|
signUpErrors: [],
|
||||||
relationships: {},
|
relationships: {},
|
||||||
|
@ -306,6 +310,14 @@ const users = {
|
||||||
mutations,
|
mutations,
|
||||||
getters,
|
getters,
|
||||||
actions: {
|
actions: {
|
||||||
|
fetchUsers (store, params = false) {
|
||||||
|
return store.rootState.api.backendInteractor.fetchUsers(params)
|
||||||
|
.then((users) => {
|
||||||
|
store.commit('adminSetUsers', users)
|
||||||
|
users.forEach(user => store.dispatch('fetchUserIfMissing', user.id))
|
||||||
|
return users
|
||||||
|
})
|
||||||
|
},
|
||||||
fetchUserIfMissing (store, id) {
|
fetchUserIfMissing (store, id) {
|
||||||
if (!store.getters.findUser(id)) {
|
if (!store.getters.findUser(id)) {
|
||||||
store.dispatch('fetchUser', id)
|
store.dispatch('fetchUser', id)
|
||||||
|
|
|
@ -23,6 +23,10 @@ const NOTIFICATION_READ_URL = '/api/v1/pleroma/notifications/read'
|
||||||
const ADMIN_REPORTS_URL = '/api/v1/pleroma/admin/reports'
|
const ADMIN_REPORTS_URL = '/api/v1/pleroma/admin/reports'
|
||||||
const ADMIN_REPORT_NOTES_URL = id => `/api/v1/pleroma/admin/reports/${id}/notes`
|
const ADMIN_REPORT_NOTES_URL = id => `/api/v1/pleroma/admin/reports/${id}/notes`
|
||||||
const ADMIN_REPORT_NOTE_URL = (report, note) => `/api/v1/pleroma/admin/reports/${report}/notes/${note}`
|
const ADMIN_REPORT_NOTE_URL = (report, note) => `/api/v1/pleroma/admin/reports/${report}/notes/${note}`
|
||||||
|
const ADMIN_PASSWORD_RESET_TOKEN_URL = nickname => `/api/v1/pleroma/admin/users/${nickname}/password_reset`
|
||||||
|
const ADMIN_FORCE_PASSWORD_RESET_URL = '/api/v1/pleroma/admin/users/force_password_reset'
|
||||||
|
const ADMIN_DISABLE_MFA_URL = '/api/v1/pleroma/admin/users/disable_mfa'
|
||||||
|
const ADMIN_APPROVE_ACCOUNT_URL = '/api/v1/pleroma/admin/users/approve'
|
||||||
|
|
||||||
const MFA_SETTINGS_URL = '/api/pleroma/accounts/mfa'
|
const MFA_SETTINGS_URL = '/api/pleroma/accounts/mfa'
|
||||||
const MFA_BACKUP_CODES_URL = '/api/pleroma/accounts/mfa/backup_codes'
|
const MFA_BACKUP_CODES_URL = '/api/pleroma/accounts/mfa/backup_codes'
|
||||||
|
@ -557,6 +561,29 @@ const fetchStatusHistory = ({ status, credentials }) => {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const fetchUsers = ({ filters, name, page, actorTypes, credentials }) => {
|
||||||
|
const url = ADMIN_USERS_URL
|
||||||
|
const params = []
|
||||||
|
|
||||||
|
if (filters) {
|
||||||
|
params.push(['filters', filters.join(',')])
|
||||||
|
}
|
||||||
|
if (name) {
|
||||||
|
params.push(['name', name])
|
||||||
|
}
|
||||||
|
if (page) {
|
||||||
|
params.push(['page', page])
|
||||||
|
}
|
||||||
|
if (actorTypes) {
|
||||||
|
actorTypes.forEach(type => (params.push(['actor_types[]', type])))
|
||||||
|
}
|
||||||
|
|
||||||
|
const queryString = map(params, (param) => `${param[0]}=${param[1]}`).join('&')
|
||||||
|
return promisedRequest({ url: url + `?${queryString}`, credentials })
|
||||||
|
.then((data) => data.users)
|
||||||
|
.then((data) => data.map(parseUser))
|
||||||
|
}
|
||||||
|
|
||||||
const tagUser = ({ tag, credentials, user }) => {
|
const tagUser = ({ tag, credentials, user }) => {
|
||||||
const screenName = user.screen_name
|
const screenName = user.screen_name
|
||||||
const form = {
|
const form = {
|
||||||
|
@ -694,6 +721,42 @@ const deleteNoteFromReport = ({ report, note, credentials }) => {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const getPasswordResetToken = ({ nickname, credentials }) => {
|
||||||
|
const url = ADMIN_PASSWORD_RESET_TOKEN_URL(nickname)
|
||||||
|
|
||||||
|
return fetch(url, { headers: authHeaders(credentials) })
|
||||||
|
.then(data => data.json())
|
||||||
|
}
|
||||||
|
|
||||||
|
const forcePasswordReset = ({ nickname, credentials }) => {
|
||||||
|
return promisedRequest({
|
||||||
|
url: ADMIN_FORCE_PASSWORD_RESET_URL,
|
||||||
|
method: 'PATCH',
|
||||||
|
payload: { nicknames: [nickname] },
|
||||||
|
credentials
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const forceDisableMFA = ({ nickname, credentials }) => {
|
||||||
|
return promisedRequest({
|
||||||
|
url: ADMIN_DISABLE_MFA_URL,
|
||||||
|
method: 'PUT',
|
||||||
|
payload: { nickname },
|
||||||
|
credentials
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const approveAccount = ({ nickname, credentials }) => {
|
||||||
|
return promisedRequest({
|
||||||
|
url: ADMIN_APPROVE_ACCOUNT_URL,
|
||||||
|
method: 'PATCH',
|
||||||
|
payload: {
|
||||||
|
nicknames: [nickname]
|
||||||
|
},
|
||||||
|
credentials
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
const fetchTimeline = ({
|
const fetchTimeline = ({
|
||||||
timeline,
|
timeline,
|
||||||
credentials,
|
credentials,
|
||||||
|
@ -1762,6 +1825,7 @@ const apiService = {
|
||||||
fetchBlocks,
|
fetchBlocks,
|
||||||
fetchOAuthTokens,
|
fetchOAuthTokens,
|
||||||
revokeOAuthToken,
|
revokeOAuthToken,
|
||||||
|
fetchUsers,
|
||||||
tagUser,
|
tagUser,
|
||||||
untagUser,
|
untagUser,
|
||||||
deleteUser,
|
deleteUser,
|
||||||
|
@ -1769,6 +1833,10 @@ const apiService = {
|
||||||
deleteRight,
|
deleteRight,
|
||||||
activateUser,
|
activateUser,
|
||||||
deactivateUser,
|
deactivateUser,
|
||||||
|
getPasswordResetToken,
|
||||||
|
forcePasswordReset,
|
||||||
|
forceDisableMFA,
|
||||||
|
approveAccount,
|
||||||
register,
|
register,
|
||||||
getCaptcha,
|
getCaptcha,
|
||||||
updateProfileImages,
|
updateProfileImages,
|
||||||
|
|
|
@ -43,6 +43,9 @@ export const parseUser = (data) => {
|
||||||
// case for users in "mentions" property for statuses in MastoAPI
|
// case for users in "mentions" property for statuses in MastoAPI
|
||||||
const mastoShort = masto && !data.hasOwnProperty('avatar')
|
const mastoShort = masto && !data.hasOwnProperty('avatar')
|
||||||
|
|
||||||
|
// account format from the admin API
|
||||||
|
const admin = data.hasOwnProperty('actor_type')
|
||||||
|
|
||||||
output.id = String(data.id)
|
output.id = String(data.id)
|
||||||
output._original = data // used for server-side settings
|
output._original = data // used for server-side settings
|
||||||
|
|
||||||
|
@ -139,6 +142,16 @@ export const parseUser = (data) => {
|
||||||
|
|
||||||
// TODO: handle is_local
|
// TODO: handle is_local
|
||||||
output.is_local = !output.screen_name.includes('@')
|
output.is_local = !output.screen_name.includes('@')
|
||||||
|
} else if (admin) {
|
||||||
|
output.bot = data.actor_type === 'Service'
|
||||||
|
output.screen_name = data.nickname
|
||||||
|
output.name = data.display_name
|
||||||
|
output.profile_image_url = data.avatar
|
||||||
|
output.profile_image_url_original = data.avatar
|
||||||
|
output.is_local = data.local
|
||||||
|
output.approved = data.is_approved
|
||||||
|
output.registration_reason = data.registration_reason
|
||||||
|
output.emoji = []
|
||||||
} else {
|
} else {
|
||||||
output.screen_name = data.screen_name
|
output.screen_name = data.screen_name
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue