cleaned up, refactored and implemented new <ModerationTools>

This commit is contained in:
Henry Jameson 2026-06-10 01:58:49 +03:00
commit e56ea2dbeb
23 changed files with 1124 additions and 975 deletions

View file

@ -4,6 +4,7 @@ export default {
validInnerComponents: ['Text', 'Icon', 'Link', 'Border', 'ButtonUnstyled'], validInnerComponents: ['Text', 'Icon', 'Link', 'Border', 'ButtonUnstyled'],
variants: { variants: {
normal: '.neutral', normal: '.neutral',
info: '.info',
error: '.error', error: '.error',
warning: '.warning', warning: '.warning',
success: '.success', success: '.success',
@ -47,5 +48,11 @@ export default {
background: '--cGreen', background: '--cGreen',
}, },
}, },
{
variant: 'info',
directives: {
background: '--cBlue',
},
},
], ],
} }

View file

@ -31,9 +31,7 @@ const Announcement = {
canEditAnnouncement() { canEditAnnouncement() {
return ( return (
this.currentUser && this.currentUser &&
this.currentUser.privileges.includes( this.currentUser.privileges.has('announcements_manage_announcements')
'announcements_manage_announcements',
)
) )
}, },
content() { content() {

View file

@ -17,7 +17,7 @@ const Interactions = {
allowFollowingMove: allowFollowingMove:
this.$store.state.users.currentUser.allow_following_move, this.$store.state.users.currentUser.allow_following_move,
filterMode: tabModeDict.mentions, filterMode: tabModeDict.mentions,
canSeeReports: this.$store.state.users.currentUser.privileges.includes( canSeeReports: this.$store.state.users.currentUser.has.has(
'reports_manage_reports', 'reports_manage_reports',
), ),
} }

View file

@ -14,7 +14,7 @@ const List = {
}, },
getKey: { getKey: {
type: Function, type: Function,
default: (item) => item, default: (item) => item.id,
}, },
getClass: { getClass: {
type: Function, type: Function,
@ -37,7 +37,7 @@ const List = {
default: null, default: null,
}, },
}, },
emits: ['fetchRequested'], emits: ['fetchRequested', 'select'],
components: { components: {
Checkbox, Checkbox,
}, },
@ -56,11 +56,14 @@ const List = {
allKeys() { allKeys() {
return new Set(this.finalItems.map(this.getKey)) return new Set(this.finalItems.map(this.getKey))
}, },
filteredSelected() { selectedItems() {
return [...this.allKeys.values().filter((key) => this.selected.has(key))] return this.items.filter((item) => this.selected.has(this.getKey(item)))
}, },
allSelected() { allSelected() {
return this.selected.size === this.finalItems.length return (
this.selected.size !== 0 &&
this.selected.size === this.finalItems.length
)
}, },
noneSelected() { noneSelected() {
return this.selected.size === 0 return this.selected.size === 0
@ -101,6 +104,7 @@ const List = {
.catch((error) => { .catch((error) => {
this.loading = false this.loading = false
this.error = error this.error = error
console.error('Error loading list data:', error)
}) })
}, },
reset() { reset() {
@ -124,19 +128,17 @@ const List = {
} }
}, },
isSelected(item) { isSelected(item) {
return this.filteredSelected.indexOf(this.getKey(item)) !== -1 return this.selected.has(this.getKey(item))
}, },
toggle(checked, item) { toggle(checked, item) {
const key = this.getKey(item) const key = this.getKey(item)
const oldChecked = this.isSelected(key)
if (checked !== oldChecked) {
if (checked) { if (checked) {
this.selected.add(key) this.selected.add(key)
} else { } else {
this.selected.delete(key) this.selected.delete(key)
} }
}
this.$emit('selected', this.selected) this.$emit('select', this.selected)
}, },
toggleAll(value) { toggleAll(value) {
if (value) { if (value) {
@ -144,7 +146,7 @@ const List = {
} else { } else {
this.selected = new Set([]) this.selected = new Set([])
} }
this.$emit('selected', this.selected) this.$emit('select', this.selected)
}, },
}, },
} }

View file

@ -19,7 +19,7 @@
<div class="actions"> <div class="actions">
<slot <slot
name="header" name="header"
:selected="filteredSelected" :selected="selectedItems"
/> />
</div> </div>
</div> </div>

View file

@ -1,6 +1,9 @@
import DialogModal from 'src/components/dialog_modal/dialog_modal.vue' import { last } from 'lodash'
import ConfirmModal from 'src/components/confirm_modal/confirm_modal.vue'
import Popover from 'src/components/popover/popover.vue' import Popover from 'src/components/popover/popover.vue'
import { useAdminSettingsStore } from 'src/stores/admin_settings.js'
import { useInstanceStore } from 'src/stores/instance.js' import { useInstanceStore } from 'src/stores/instance.js'
import { useInstanceCapabilitiesStore } from 'src/stores/instance_capabilities.js' import { useInstanceCapabilitiesStore } from 'src/stores/instance_capabilities.js'
@ -16,12 +19,7 @@ const DISABLE_REMOTE_SUBSCRIPTION = 'mrf_tag:disable-remote-subscription'
const DISABLE_ANY_SUBSCRIPTION = 'mrf_tag:disable-any-subscription' const DISABLE_ANY_SUBSCRIPTION = 'mrf_tag:disable-any-subscription'
const SANDBOX = 'mrf_tag:sandbox' const SANDBOX = 'mrf_tag:sandbox'
const QUARANTINE = 'mrf_tag:quarantine' const QUARANTINE = 'mrf_tag:quarantine'
const TAGS = new Set([
const ModerationTools = {
props: ['user'],
data() {
return {
tags: {
FORCE_NSFW, FORCE_NSFW,
STRIP_MEDIA, STRIP_MEDIA,
FORCE_UNLISTED, FORCE_UNLISTED,
@ -29,28 +27,364 @@ const ModerationTools = {
DISABLE_ANY_SUBSCRIPTION, DISABLE_ANY_SUBSCRIPTION,
SANDBOX, SANDBOX,
QUARANTINE, QUARANTINE,
])
const ENTRIES = [
{
check: '!state:activated',
label: 'user_card.admin_menu.activate_account',
}, },
showDeleteUserDialog: false, {
toggled: false, check: 'state:activated',
label: 'user_card.admin_menu.deactivate_account',
},
{
separator: true,
},
{
check: '!state:confirmed',
label: 'user_card.admin_menu.confirm_account',
},
{
check: 'action:resend_confirmation',
conditions: ['!state:confirmed'],
label: 'user_card.admin_menu.resend_confirmation',
},
// No API for revocation
// {
// check: 'state:confirmed',
// label: 'user_card.admin_menu.unconfirm_account',
// },
{
check: '!state:approved',
conditions: ['property:local'],
label: 'user_card.admin_menu.approve_account',
},
// No API for revocation
// {
// check: 'state:approved',
// label: 'user_card.admin_menu.unapprove_account',
// },
{
check: '!state:suggested',
// conditions: ['property:local'], // TODO Should we allow non-local users in suggested?
label: 'user_card.admin_menu.suggest_account',
},
{
check: 'state:suggested',
label: 'user_card.admin_menu.remove_suggested_account',
},
{
separator: true,
},
{
check: '!state:disable_mfa',
label: 'user_card.admin_menu.disable_mfa',
},
{
check: '!state:require_password_change',
label: 'user_card.admin_menu.require_password_change',
},
{
separator: true,
},
{
check: '!rights:moderator',
label: 'user_card.admin_menu.grant_moderator',
conditions: ['property:local', 'state:activated'],
},
{
check: 'rights:moderator',
label: 'user_card.admin_menu.revoke_moderator',
conditions: ['property:local', 'state:activated'],
},
{
check: '!rights:admin',
label: 'user_card.admin_menu.grant_admin',
conditions: ['property:local', 'state:activated'],
},
{
check: 'rights:admin',
label: 'user_card.admin_menu.revoke_admin',
conditions: ['property:local', 'state:activated'],
},
{
separator: true,
},
{
check: FORCE_NSFW,
label: 'user_card.admin_menu.force_nsfw',
},
{
check: STRIP_MEDIA,
label: 'user_card.admin_menu.strip_media',
},
{
check: FORCE_UNLISTED,
label: 'user_card.admin_menu.force_unlisted',
},
{
check: SANDBOX,
label: 'user_card.admin_menu.sandbox',
},
{
check: DISABLE_ANY_SUBSCRIPTION,
conditions: ['property:local'],
label: 'user_card.admin_menu.disable_any_subscription',
},
{
check: DISABLE_REMOTE_SUBSCRIPTION,
conditions: ['property:local'],
label: 'user_card.admin_menu.disable_remote_subscription',
},
{
check: QUARANTINE,
conditions: ['property:local'],
label: 'user_card.admin_menu.quarantine',
},
{
separator: true,
},
{
check: 'action:delete',
label: 'user_card.admin_menu.delete_account',
},
]
const ModerationTools = {
props: {
users: {
type: Array,
required: true,
},
},
created() {
if (this.users.length !== 1) return
useAdminSettingsStore().getUserData({ user: this.users[0] })
},
data() {
return {
open: false,
confirmDialogShow: false,
confirmDialogTitle: null,
confirmDialogContent: null,
confirmDialogConfirm: null,
confirmDialogAction: null,
confirmDialogGroup: null,
confirmDialogName: null,
} }
}, },
components: { components: {
DialogModal, ConfirmModal,
Popover, Popover,
}, },
computed: { computed: {
tagsSet() { ready() {
return new Set(this.user.tags) return this.users.every((u) => u.adminData)
}, },
canGrantRole() { entries() {
return ( return ENTRIES.map(({ check, label, separator, conditions }) => {
this.user.is_local && if (separator) return 'separator'
!this.user.deactivated && const [, negateToken, group, name] = /^([!~]?)([a-z-_]+):([a-z-_]+)$/.exec(
this.$store.state.users.currentUser.role === 'admin' check,
) )
const hasTag = this.tagsSet.has(`${group}:${name}`)
const noTag = this.tagsSet.has(`!${group}:${name}`)
const maybeTag = this.tagsSet.has(`~${group}:${name}`)
// We are checking for condition to show element, i.e. only show "activate" if user is "deactivated"
const checkNegated = (negateToken === '!' || negateToken === '~')
// Naturally, new value should also be the same
const value = checkNegated
const action = (() => {
switch (group) {
case 'rights':
return () => this.setRight(name, value)
case 'state':
return () => this.setStatus(name, value)
case 'mrf_tag':
return () => this.setTag(`${group}:${name}`, noTag)
case 'action': {
switch (name) {
case 'delete': {
return () => this.deleteUsers()
}
case 'resend_confirmation': {
return () => this.resendConfirmationEmail()
}
case 'disable_mfa': {
return () => this.disableMFA()
}
case 'require_password_change': {
return () => this.requirePasswordChange()
}
default:
throw new Error(`Unknown action group: ${name}`)
}
}
default:
throw new Error(`Unknown moderation group: ${group}`)
}
})()
let checkboxClass = ''
if (maybeTag) {
checkboxClass = 'menu-checkbox-indeterminate'
} else if (hasTag) {
checkboxClass = 'menu-checkbox-checked'
}
return {
check,
negateToken,
checkbox: group === 'mrf_tag',
checkboxClass,
conditions,
group,
name,
action,
label,
value,
}
})
.filter((entry) => {
if (entry === 'separator') return true
const { group, name, value, conditions } = entry
if (conditions) {
// Checking that all items match positive criteria
const positive = conditions.every((condition) =>
this.totalSet.has(condition),
)
// Checking that there are no items that don't match criteria
const negative = conditions.some((condition) =>
this.totalSet.has('!' + condition),
)
if (!(positive && !negative)) return false
}
switch (group) {
case 'action': {
return true
}
case 'rights': {
return this.canGrantRole(name, value)
}
case 'state': {
return this.canChangeState(name, value)
}
case 'mrf_tag': {
return this.canUseTagPolicy
}
default: {
throw new Error(`Unknown moderation group: ${group}`)
}
}
})
.reduce((acc, entry, index) => {
if (entry === 'separator') {
if (
acc.length === 0 ||
last(acc) === 'separator' ||
index === ENTRIES.length - 1
) {
return acc
}
}
return [...acc, entry]
}, [])
}, },
canChangeActivationState() { rightsSet() {
return this.privileged('users_manage_activation_state') return this.users.reduce((acc, user) => {
if (user.rights.admin) {
acc.add('rights:admin')
} else {
acc.add('!rights:admin')
}
if (user.rights.moderator) {
acc.add('rights:moderator')
} else {
acc.add('!rights:moderator')
}
return acc
}, new Set())
},
stateSet() {
return this.users.reduce((acc, user) => {
if (!user.deactivated) {
acc.add('state:activated')
} else {
acc.add('!state:activated')
}
if (user.adminData?.is_confirmed) {
acc.add('state:confirmed')
} else {
acc.add('!state:confirmed')
}
if (user.adminData?.is_approved) {
acc.add('state:approved')
} else {
acc.add('!state:approved')
}
if (user.adminData?.is_suggested) {
acc.add('state:suggested')
} else {
acc.add('!state:suggested')
}
return acc
}, new Set())
},
tagsSet() {
const present = new Set()
const missing = new Set()
this.users.forEach((user) => {
TAGS.forEach((tag) => {
if (user.tags.has(tag)) {
present.add(tag)
} else {
missing.add(tag)
}
})
})
const result = new Set()
TAGS.forEach((tag) => {
if (present.has(tag) && missing.has(tag)) {
result.add(`~${tag}`)
} else if (missing.has(tag)) {
result.add(`!${tag}`)
} else {
result.add(tag)
}
})
return result
},
propertySet() {
return this.users.reduce((acc, user) => {
if (user.is_local) {
acc.add('property:local')
} else {
acc.add('!property:local')
}
return acc
}, new Set())
},
disabled() {
return !this.ready || this.users.length === 0
},
totalSet() {
return new Set([
...this.rightsSet,
...this.stateSet,
...this.tagsSet,
...this.propertySet,
])
}, },
canDeleteAccount() { canDeleteAccount() {
return this.privileged('users_delete') return this.privileged('users_delete')
@ -63,87 +397,221 @@ const ModerationTools = {
}, },
}, },
methods: { methods: {
hasTag(tagName) { canGrantRole(name, value) {
return this.tagsSet.has(tagName) const setEntry = `${value ? '!' : ''}rights:${name}`
return (
this.$store.state.users.currentUser.role === 'admin' &&
this.totalSet.has(setEntry)
)
},
canChangeState(name, value) {
let privilege
switch (name) {
// TODO detailed privileges
default: {
privilege = 'users_manage_activation_state'
}
}
const setEntry = `${value ? '!' : ''}state:${name}`
return this.privileged(privilege) && this.totalSet.has(setEntry)
},
doConfirmDialogAction() {
if (typeof this.confirmDialogAction !== 'function') {
console.error('Confirm Dialog action is not a function!!')
}
this.confirmDialogAction()
this.clearConfirmDialog()
},
clearConfirmDialog() {
this.confirmDialogShow = false
this.confirmDialogTitle = null
this.confirmDialogContent = null
this.confirmDialogContent2 = null
this.confirmDialogDanger = false
this.confirmDialogConfirm = null
this.confirmDialogAction = null
this.confirmDialogGroup = null
this.confirmDialogName = null
}, },
privileged(privilege) { privileged(privilege) {
return this.$store.state.users.currentUser.privileges.includes(privilege) return this.$store.state.users.currentUser.privileges.has(privilege)
}, },
toggleTag(tag) { setTag(tag, value) {
const store = this.$store useAdminSettingsStore().setUsersTags({ users: this.users, value, tags: [tag] })
if (this.tagsSet.has(tag)) {
store.state.api.backendInteractor
.untagUser({ user: this.user, tag })
.then((response) => {
if (!response.ok) {
return
}
store.commit('untagUser', { user: this.user, tag })
})
} else {
store.state.api.backendInteractor
.tagUser({ user: this.user, tag })
.then((response) => {
if (!response.ok) {
return
}
store.commit('tagUser', { user: this.user, tag })
})
}
}, },
toggleRight(right) { setRight(right, value) {
const store = this.$store useAdminSettingsStore().setUsersRight({ users: this.users, value, right })
if (this.user.rights[right]) {
store.state.api.backendInteractor
.deleteRight({ user: this.user, right })
.then((response) => {
if (!response.ok) {
return
}
store.commit('updateRight', {
user: this.user,
right,
value: false,
})
})
} else {
store.state.api.backendInteractor
.addRight({ user: this.user, right })
.then((response) => {
if (!response.ok) {
return
}
store.commit('updateRight', { user: this.user, right, value: true })
})
}
}, },
toggleActivationStatus() { setStatus(name, value) {
this.$store.dispatch('toggleActivationStatus', { user: this.user }) const noun = (() => {
switch (name) {
case 'activated':
return 'Activation'
case 'confirmed':
return 'Confirmation'
case 'approved':
return 'Approval'
case 'suggested':
return 'Suggestion'
}
})()
useAdminSettingsStore()[`setUsers${noun}Status`]({
users: this.users,
value,
})
}, },
deleteUserDialog(show) { resendConfirmationEmail() {
this.showDeleteUserDialog = show useAdminSettingsStore().resendConfirmationEmail({ users: this.users })
}, },
deleteUser() { requirePasswordChange() {
const store = this.$store useAdminSettingsStore().requirePasswordChange({ users: this.users })
const user = this.user },
const { id, name } = user disableMFA() {
store.state.api.backendInteractor.deleteUser({ user }).then(() => { this.users.forEach((user) => {
this.$store.dispatch( useAdminSettingsStore().disableMFA({ user })
'markStatusesAsDeleted', })
(status) => user.id === status.user.id, },
) deleteUsers() {
const { id, name } = this.users[0]
useAdminSettingsStore()
.deleteUsers({ users: this.users })
.then((userIds) => {
if (userIds.length > 1) return
const isProfile = const isProfile =
this.$route.name === 'external-user-profile' || this.$route.name === 'external-user-profile' ||
this.$route.name === 'user-profile' this.$route.name === 'user-profile'
const isTargetUser = const isTargetUser =
this.$route.params.name === name || this.$route.params.id === id this.$route.params.name === name || this.$route.params.id === id
if (isProfile && isTargetUser) { if (isProfile && isTargetUser) {
window.history.back() window.history.back()
} }
}) })
}, },
setToggled(value) { setOpen(value) {
this.toggled = value this.open = value
},
maybeShowConfirm(close, { group, name, action, value }) {
close()
this.confirmDialogName = name
this.confirmDialogGroup = group
this.confirmDialogAction = () => action()
switch (group) {
case 'action': {
if (name === 'delete') {
this.confirmDialogShow = true
this.confirmDialogTitle = this.$t(
'user_card.admin_menu.confirm_modal.delete_title',
)
this.confirmDialogDanger = true
this.confirmDialogContent =
'user_card.admin_menu.confirm_modal.delete_content'
this.confirmDialogContent2 =
'user_card.admin_menu.confirm_modal.delete_content_2'
this.confirmDialogConfirm = this.$t(
'user_card.admin_menu.confirm_modal.delete',
)
}
break
}
case 'state': {
switch (name) {
case 'activated': {
this.confirmDialogShow = true
this.confirmDialogTitle = this.$t(
'user_card.admin_menu.confirm_modal.activate_title',
)
this.confirmDialogContent = value
? 'user_card.admin_menu.confirm_modal.activate_content'
: 'user_card.admin_menu.confirm_modal.deactivate_content'
this.confirmDialogConfirm = value
? this.$t('user_card.admin_menu.confirm_modal.activate')
: this.$t('user_card.admin_menu.confirm_modal.deactivate')
break
}
// Confirmation and Approval statuses cannot be revokedn(no API)
case 'confirmed': {
this.confirmDialogTitle = this.$t(
'user_card.admin_menu.confirm_modal.confirm_title',
)
this.confirmDialogContent = //value
/*?*/ 'user_card.admin_menu.confirm_modal.confirm_content'
//: 'user_card.admin_menu.confirm_modal.confirm_revoke_content'
this.confirmDialogConfirm = value
/*?*/ this.$t('user_card.admin_menu.confirm_modal.confirm')
//: this.$t('user_card.admin_menu.confirm_modal.revoke')
break
}
case 'approved': {
this.confirmDialogTitle = this.$t(
'user_card.admin_menu.confirm_modal.approval_title',
)
this.confirmDialogContent = //value
/*?*/ 'user_card.admin_menu.confirm_modal.approval_content'
//: 'user_card.admin_menu.confirm_modal.approval_revoke_content'
this.confirmDialogConfirm = value
/*?*/ this.$t('user_card.admin_menu.confirm_modal.approve')
//: this.$t('user_card.admin_menu.confirm_modal.revoke')
break
}
case 'suggest': {
this.confirmDialogTitle = this.$t(
'user_card.admin_menu.confirm_modal.suggest_title',
)
this.confirmDialogContent = value
? 'user_card.admin_menu.confirm_modal.add_suggest_content'
: 'user_card.admin_menu.confirm_modal.remove_suggest_content'
this.confirmDialogConfirm = value
? this.$t('user_card.admin_menu.confirm_modal.add')
: this.$t('user_card.admin_menu.confirm_modal.remove')
break
}
}
break
}
case 'rights': {
this.confirmDialogTitle = this.$t(
'user_card.admin_menu.confirm_modal.user_rights_title',
)
this.confirmDialogContent = value
? 'user_card.admin_menu.confirm_modal.grant_role_content'
: 'user_card.admin_menu.confirm_modal.revoke_role_content'
this.confirmDialogConfirm = value
? this.$t('user_card.admin_menu.confirm_modal.grant')
: this.$t('user_card.admin_menu.confirm_modal.revoke')
break
}
case 'mrf_tag': {
this.confirmDialogTitle = this.$t(
'user_card.admin_menu.user_tag_title',
)
this.confirmDialogContent = value
? 'user_card.admin_menu.confirm_modal.assign_tag_content'
: 'user_card.admin_menu.confirm_modal.unassign_tag_content'
this.confirmDialogConfirm = value
? this.$t('user_card.admin_menu.confirm_modal.assign')
: this.$t('user_card.admin_menu.confirm_modal.unassign')
break
}
}
if (this.users.length > 1) {
this.confirmDialogShow = true
}
if (!this.confirmDialogShow) {
this.doConfirmDialogAction()
}
}, },
}, },
} }

View file

@ -3,154 +3,37 @@
<Popover <Popover
trigger="click" trigger="click"
class="moderation-tools-popover" class="moderation-tools-popover"
:disabled="disabled"
placement="bottom" placement="bottom"
:offset="{ y: 5 }" :offset="{ y: 5 }"
@show="setToggled(true)" @show="setOpen(true)"
@close="setToggled(false)" @close="setOpen(false)"
> >
<template #content> <template #content="{close}">
<div class="dropdown-menu"> <div class="dropdown-menu">
<template v-if="canGrantRole"> <template v-for="(entry, index) in entries">
<div class="menu-item dropdown-item -icon-space">
<button
class="main-button"
@click="toggleRight(&quot;admin&quot;)"
>
{{ $t(!!user.rights.admin ? 'user_card.admin_menu.revoke_admin' : 'user_card.admin_menu.grant_admin') }}
</button>
</div>
<div class="menu-item dropdown-item -icon-space">
<button
class="main-button"
@click="toggleRight(&quot;moderator&quot;)"
>
{{ $t(!!user.rights.moderator ? 'user_card.admin_menu.revoke_moderator' : 'user_card.admin_menu.grant_moderator') }}
</button>
</div>
<div <div
v-if="canChangeActivationState || canDeleteAccount" v-if="entry === 'separator'"
:key="index"
role="separator" role="separator"
class="dropdown-divider" class="dropdown-divider"
/> />
</template>
<div <div
v-if="canChangeActivationState" v-else
class="menu-item dropdown-item -icon-space" :key="entry.label"
class="menu-item dropdown-item"
:class="entry.checkbox ? '-icon' : '-icon-space'"
> >
<button <button
class="main-button" class="main-button"
@click="toggleActivationStatus()" @click="() => maybeShowConfirm(close, entry)"
>
{{ $t(!!user.deactivated ? 'user_card.admin_menu.activate_account' : 'user_card.admin_menu.deactivate_account') }}
</button>
</div>
<div
v-if="canDeleteAccount"
class="menu-item dropdown-item -icon-space"
>
<button
class="main-button"
@click="deleteUserDialog(true)"
>
{{ $t('user_card.admin_menu.delete_account') }}
</button>
</div>
<template v-if="canUseTagPolicy">
<div
role="separator"
class="dropdown-divider"
/>
<div class="menu-item dropdown-item -icon">
<button
class="main-button"
@click="toggleTag(tags.FORCE_NSFW)"
> >
<span <span
v-if="entry.checkbox"
class="input menu-checkbox" class="input menu-checkbox"
:class="{ 'menu-checkbox-checked': hasTag(tags.FORCE_NSFW) }" :class="entry.checkboxClass"
/> />
{{ $t('user_card.admin_menu.force_nsfw') }} {{ $t(entry.label) }}
</button>
</div>
<div class="menu-item dropdown-item -icon">
<button
class="main-button"
@click="toggleTag(tags.STRIP_MEDIA)"
>
<span
class="input menu-checkbox"
:class="{ 'menu-checkbox-checked': hasTag(tags.STRIP_MEDIA) }"
/>
{{ $t('user_card.admin_menu.strip_media') }}
</button>
</div>
<div class="menu-item dropdown-item -icon">
<button
class="main-button"
@click="toggleTag(tags.FORCE_UNLISTED)"
>
<span
class="input menu-checkbox"
:class="{ 'menu-checkbox-checked': hasTag(tags.FORCE_UNLISTED) }"
/>
{{ $t('user_card.admin_menu.force_unlisted') }}
</button>
</div>
<div class="menu-item dropdown-item -icon">
<button
class="main-button"
@click="toggleTag(tags.SANDBOX)"
>
<span
class="input menu-checkbox"
:class="{ 'menu-checkbox-checked': hasTag(tags.SANDBOX) }"
/>
{{ $t('user_card.admin_menu.sandbox') }}
</button>
</div>
<div
v-if="user.is_local"
class="menu-item dropdown-item -icon"
>
<button
class="main-button"
@click="toggleTag(tags.DISABLE_REMOTE_SUBSCRIPTION)"
>
<span
class="input menu-checkbox"
:class="{ 'menu-checkbox-checked': hasTag(tags.DISABLE_REMOTE_SUBSCRIPTION) }"
/>
{{ $t('user_card.admin_menu.disable_remote_subscription') }}
</button>
</div>
<div
v-if="user.is_local"
class="menu-item dropdown-item -icon"
>
<button
class="main-button"
@click="toggleTag(tags.DISABLE_ANY_SUBSCRIPTION)"
>
<span
class="input menu-checkbox"
:class="{ 'menu-checkbox-checked': hasTag(tags.DISABLE_ANY_SUBSCRIPTION) }"
/>
{{ $t('user_card.admin_menu.disable_any_subscription') }}
</button>
</div>
<div
v-if="user.is_local"
class="menu-item dropdown-item -icon"
>
<button
class="main-button"
@click="toggleTag(tags.QUARANTINE)"
>
<span
class="input menu-checkbox"
:class="{ 'menu-checkbox-checked': hasTag(tags.QUARANTINE) }"
/>
{{ $t('user_card.admin_menu.quarantine') }}
</button> </button>
</div> </div>
</template> </template>
@ -159,37 +42,60 @@
<template #trigger> <template #trigger>
<button <button
class="btn button-default btn-block moderation-tools-button" class="btn button-default btn-block moderation-tools-button"
:class="{ toggled }" :class="{ toggled: open, disabled }"
:disabled="disabled"
> >
{{ $t('user_card.admin_menu.moderation') }} {{ $t('user_card.admin_menu.moderation') }}
<FAIcon icon="chevron-down" /> <FAIcon v-if="ready" icon="chevron-down" />
<span v-else class="loading-spinner">
<FAIcon
class="fa-old-padding"
spin
icon="circle-notch"
/>
</span>
</button> </button>
</template> </template>
</Popover> </Popover>
<teleport to="#modal"> <teleport to="#modal">
<DialogModal <ConfirmModal
v-if="showDeleteUserDialog" v-if="confirmDialogShow"
:on-cancel="deleteUserDialog.bind(this, false)" :title="$t(confirmDialogTitle)"
:confirm-text="confirmDialogConfirm"
:confirm-danger="confirmDialogDanger"
:cancel-text="$t('general.cancel')"
@accepted="doConfirmDialogAction"
@cancelled="clearConfirmDialog"
> >
<template #header> <i18n-t
{{ $t('user_card.admin_menu.delete_user') }} :plural="users.length"
scope="global"
:keypath="confirmDialogContent"
tag="p"
>
<template #user>
<span
v-text="users[0].screen_name_ui"
/>
</template> </template>
<p>{{ $t('user_card.admin_menu.delete_user_confirmation') }}</p> <template #count>
<template #footer> {{ users.length }}
<button
class="btn button-default"
@click="deleteUserDialog(false)"
>
{{ $t('general.cancel') }}
</button>
<button
class="btn button-default danger"
@click="deleteUser()"
>
{{ $t('user_card.admin_menu.delete_user') }}
</button>
</template> </template>
</DialogModal> <template #name>
<code>
{{ confirmDialogName }}
</code>
</template>
</i18n-t>
<p v-if="confirmDialogContent2">
{{ $t(confirmDialogContent2) }}
</p>
<ul v-if="users.length > 1">
<li v-for="user in users">
{{ user.screen_name }}
</li>
</ul>
</ConfirmModal>
</teleport> </teleport>
</div> </div>
</template> </template>

View file

@ -96,6 +96,11 @@
content: ""; content: "";
} }
&.menu-checkbox-indeterminate::after {
font-size: 1.25em;
content: "";
}
&.-radio { &.-radio {
border-radius: 9999px; border-radius: 9999px;
@ -103,6 +108,11 @@
font-size: 2em; font-size: 2em;
content: ""; content: "";
} }
&.menu-checkbox-indeterminate::after {
font-size: 2em;
content: "";
}
} }
} }
} }

View file

@ -1,8 +1,7 @@
import { defineAsyncComponent } from 'vue' import { defineAsyncComponent } from 'vue'
import BasicUserCard from 'src/components/basic_user_card/basic_user_card.vue' import BasicUserCard from 'src/components/basic_user_card/basic_user_card.vue'
import ModerationTools from 'src/components/moderation_tools/moderation_tools.vue'
import { useAdminUsersStore } from 'src/stores/adminUsers.js'
const AdminCard = { const AdminCard = {
props: { props: {
@ -12,23 +11,15 @@ const AdminCard = {
}, },
components: { components: {
BasicUserCard, BasicUserCard,
ModerationTools: defineAsyncComponent( ModerationTools,
() => import('src/components/moderation_tools/moderation_tools.vue'),
),
}, },
computed: { computed: {
user() { user() {
return this.$store.getters.findUser(this.userId) return this.$store.getters.findUser(this.userId)
}, },
userAdminData() {
return useAdminUsersStore().getUser(this.userId)
},
relationship() { relationship() {
return this.$store.getters.relationship(this.userId) return this.$store.getters.relationship(this.userId)
}, },
isLocal() {
return this.user.is_local
},
isAdmin() { isAdmin() {
return this.user.rights.admin return this.user.rights.admin
}, },
@ -38,12 +29,6 @@ const AdminCard = {
isActivated() { isActivated() {
return !this.user.deactivated return !this.user.deactivated
}, },
isApproved() {
return this.userAdminData.is_approved
},
isConfirmed() {
return this.userAdminData.is_confirmed
},
}, },
} }

View file

@ -26,7 +26,7 @@
</label> </label>
<label <label
v-if="isActivated" v-if="isActivated"
class="alert info user-role" class="alert success user-role"
> >
{{ $t('admin_dash.users.indicator.active') }} {{ $t('admin_dash.users.indicator.active') }}
</label> </label>
@ -37,14 +37,32 @@
{{ $t('admin_dash.users.indicator.deactivated') }} {{ $t('admin_dash.users.indicator.deactivated') }}
</label> </label>
<label <label
v-if="isConfirmed" v-if="user.adminData.is_confirmed"
class="alert neutral user-role" class="alert success user-role"
> >
{{ $t('admin_dash.users.indicator.confirmed') }} {{ $t('admin_dash.users.indicator.confirmed') }}
</label> </label>
<label
v-if="!user.adminData.is_confirmed"
class="alert warning user-role"
>
{{ $t('admin_dash.users.indicator.unconfirmed') }}
</label>
<label
v-if="user.adminData.is_approved"
class="alert success user-role"
>
{{ $t('admin_dash.users.indicator.approved') }}
</label>
<label
v-if="!user.adminData.is_approved"
class="alert warning user-role"
>
{{ $t('admin_dash.users.indicator.unapproved') }}
</label>
<ModerationTools <ModerationTools
class="moderation-menu" class="moderation-menu"
:user="user" :users="[user]"
/> />
</div> </div>
</BasicUserCard> </BasicUserCard>

View file

@ -38,7 +38,7 @@ const FrontendsTab = {
}, },
created() { created() {
if (this.user.rights.admin) { if (this.user.rights.admin) {
this.$store.dispatch('loadFrontendsStuff') useAdminSettingsStore().loadFrontendsStuff()
} }
}, },
computed: { computed: {
@ -77,7 +77,7 @@ const FrontendsTab = {
this.working = false this.working = false
}) })
.then(async (response) => { .then(async (response) => {
this.$store.dispatch('loadFrontendsStuff') useAdminSettingsStore().loadFrontendsStuff()
if (response.error) { if (response.error) {
const reason = await response.error.json() const reason = await response.error.json()
useInterfaceStore().pushGlobalNotice({ useInterfaceStore().pushGlobalNotice({

View file

@ -4,15 +4,26 @@ import BasicUserCard from 'src/components/basic_user_card/basic_user_card.vue'
import Checkbox from 'src/components/checkbox/checkbox.vue' import Checkbox from 'src/components/checkbox/checkbox.vue'
import GenericConfirm from 'src/components/confirm_modal/generic_confirm.vue' import GenericConfirm from 'src/components/confirm_modal/generic_confirm.vue'
import List from 'src/components/list/list.vue' import List from 'src/components/list/list.vue'
import Popover from 'src/components/popover/popover.vue' import ModerationTools from 'src/components/moderation_tools/moderation_tools.vue'
import ProgressButton from 'src/components/progress_button/progress_button.vue' import ProgressButton from 'src/components/progress_button/progress_button.vue'
import Select from 'src/components/select/select.vue' import Select from 'src/components/select/select.vue'
import AdminCard from 'src/components/settings_modal/admin_tabs/admin_card.vue' import AdminCard from 'src/components/settings_modal/admin_tabs/admin_card.vue'
import TabSwitcher from 'src/components/tab_switcher/tab_switcher.jsx' import TabSwitcher from 'src/components/tab_switcher/tab_switcher.jsx'
import { useAdminUsersStore } from 'src/stores/adminUsers.js' import { useAdminSettingsStore } from 'src/stores/admin_settings.js'
const UsersTab = { const UsersTab = {
components: {
Checkbox,
Select,
BasicUserCard,
List,
ProgressButton,
AdminCard,
TabSwitcher,
ModerationTools,
GenericConfirm,
},
provide() { provide() {
return { return {
defaultDraftMode: true, defaultDraftMode: true,
@ -90,7 +101,7 @@ const UsersTab = {
local: this.filtersLocal, local: this.filtersLocal,
external: this.filtersExternal, external: this.filtersExternal,
needApproval: this.filtersNeedApproval, needApproval: this.filtersNeedApproval,
unconfirmed: this.filtersUnconfirmeUnconfirmed, unconfirmed: this.filtersUnconfirmed,
} }
return { return {
@ -102,51 +113,15 @@ const UsersTab = {
} }
}, },
}, },
components: {
Checkbox,
Select,
BasicUserCard,
List,
ProgressButton,
AdminCard,
TabSwitcher,
Popover,
GenericConfirm,
},
methods: { methods: {
fetchUsers(page) { fetchUsers(page) {
return useAdminUsersStore() return useAdminSettingsStore()
.fetchAdminUsers({ .fetchAdminUsers({
...this.fetchOptions, ...this.fetchOptions,
page, page,
}) })
.then(({ count, users }) => ({ count, items: users })) .then(({ count, users }) => ({ count, items: users }))
}, },
/**
* show the confirmation box for bulk actions.
* @param {string} box ref name specified for the confirm component
*/
confirmSelection(box) {
this.$refs[box].show()
this.$refs.dropdown.hidePopover()
},
/**
* called when a bulk action was confirmed
* @param {string} action
*/
selectionConfirmed(action) {
const restricted = []
const s = this.$refs.userList.getSelected()
s.forEach((u) => {
if (
restricted.includes(action) !== false ||
u.id !== this.$store.state.users.currentUser.id
) {
const uf = this.$store.getters.findUser(u.id)
this.$store.dispatch(action, this.$store.getters.findUser(u.id))
}
})
},
}, },
watch: { watch: {
fetchOptions() { fetchOptions() {

View file

@ -115,7 +115,7 @@
</Checkbox> </Checkbox>
</div> </div>
<div class="filter"> <div class="filter">
<Checkbox v-model="filtersUncomfirmed"> <Checkbox v-model="filtersUnconfirmed">
{{ $t('admin_dash.users.options.only_unconfirmed') }} {{ $t('admin_dash.users.options.only_unconfirmed') }}
</Checkbox> </Checkbox>
</div> </div>
@ -123,130 +123,15 @@
<List <List
ref="usersList" ref="usersList"
:fetch-function="fetchUsers" :fetch-function="fetchUsers"
@select="onSelect"
selectable selectable
scrollable scrollable
> >
<template #header> <template #header="{selected}">
<Popover <ModerationTools :users="selected" />
ref="dropdown"
trigger="click"
placement="bottom"
>
<template #trigger>
<button
class="button button-default btn"
>
{{ $t('admin_dash.users.actions.title') }}
</button>
</template>
<template #content>
<div class="dropdown-menu">
<div class="menu-item dropdown-item">
<button
class="main-button"
@click="confirmSelection('confirmActivate')"
>
{{ $t('admin_dash.users.actions.activate') }}
</button>
</div>
<div class="menu-item dropdown-item">
<button
class="main-button"
@click="confirmSelection('confirmDeactivate')"
>
{{ $t('admin_dash.users.actions.deactivate') }}
</button>
</div>
<div class="menu-item dropdown-item">
<button
class="main-button"
@click="confirmSelection('confirmDelete')"
>
{{ $t('admin_dash.users.actions.delete_user') }}
</button>
</div>
<div class="menu-item dropdown-item">
<button
class="main-button"
@click="confirmSelection('confirmGrantAdmin')"
>
{{ $t('admin_dash.users.actions.grant_admin') }}
</button>
</div>
<div class="menu-item dropdown-item">
<button
class="main-button"
@click="confirmSelection('confirmRevokeAdmin')"
>
{{ $t('admin_dash.users.actions.revoke_admin') }}
</button>
</div>
<div class="menu-item dropdown-item">
<button
class="main-button"
@click="confirmSelection('confirmGrantModerator')"
>
{{ $t('admin_dash.users.actions.grant_moderator') }}
</button>
</div>
<div class="menu-item dropdown-item">
<button
class="main-button"
@click="confirmSelection('confirmRevokeModerator')"
>
{{ $t('admin_dash.users.actions.revoke_moderator') }}
</button>
</div>
<div class="menu-item dropdown-item">
<button
class="main-button"
@click="confirmSelection('confirmApprove')"
>
{{ $t('admin_dash.users.actions.approve') }}
</button>
</div>
<div class="menu-item dropdown-item">
<button
class="main-button"
@click="confirmSelection('confirmConfirm')"
>
{{ $t('admin_dash.users.actions.confirm') }}
</button>
</div>
<div class="menu-item dropdown-item">
<button
class="main-button"
@click="confirmSelection('confirmResendEmail')"
>
{{ $t('admin_dash.users.actions.resend_confirmation_email') }}
</button>
</div>
<div class="menu-item dropdown-item">
<button
class="main-button"
@click="confirmSelection('confirmRequirePasswordChange')"
>
{{ $t('admin_dash.users.actions.require_password_change') }}
</button>
</div>
<div class="menu-item dropdown-item">
<button
class="main-button"
@click="confirmSelection('confirmDisableMFA')"
>
{{ $t('admin_dash.users.actions.disable_mfa') }}
</button>
</div>
</div>
</template>
</Popover>
</template> </template>
<template #item="{item}"> <template #item="{item}">
<AdminCard <AdminCard :user-id="item.id" />
:user-id="item.id"
:confirmed="item.is_confirmed"
:suggested="item.is_suggested"
/>
</template> </template>
<template #load> <template #load>
<span> loading </span> <span> loading </span>
@ -255,102 +140,6 @@
<span> no users </span> <span> no users </span>
</template> </template>
</List> </List>
<GenericConfirm
ref="confirmActivate"
:title="$t('admin_dash.users.actions.confirm_multi.title')"
:message="$t('admin_dash.users.actions.confirm_multi.activate')"
:cancel-text="$t('admin_dash.users.actions.no')"
:confirm-text="$t('admin_dash.users.actions.yes')"
@action="selectionConfirmed('adminActivateUser')"
/>
<GenericConfirm
ref="confirmDeactivate"
:title="$t('admin_dash.users.actions.confirm_multi.title')"
:message="$t('admin_dash.users.actions.confirm_multi.deactivate')"
:cancel-text="$t('admin_dash.users.actions.no')"
:confirm-text="$t('admin_dash.users.actions.yes')"
@action="selectionConfirmed('adminDeactivateUser')"
/>
<GenericConfirm
ref="confirmDelete"
:title="$t('admin_dash.users.actions.confirm_multi.title')"
:message="$t('admin_dash.users.actions.confirm_multi.delete_user')"
:cancel-text="$t('admin_dash.users.actions.no')"
:confirm-text="$t('admin_dash.users.actions.yes')"
@action="selectionConfirmed('adminDeleteUser')"
/>
<GenericConfirm
ref="confirmGrantAdmin"
:title="$t('admin_dash.users.actions.confirm_multi.title')"
:message="$t('admin_dash.users.actions.confirm_multi.grant_admin')"
:cancel-text="$t('admin_dash.users.actions.no')"
:confirm-text="$t('admin_dash.users.actions.yes')"
@action="selectionConfirmed('adminAddUserToAdminGroup')"
/>
<GenericConfirm
ref="confirmRevokeAdmin"
:title="$t('admin_dash.users.actions.confirm_multi.title')"
:message="$t('admin_dash.users.actions.confirm_multi.revoke_admin')"
:cancel-text="$t('admin_dash.users.actions.no')"
:confirm-text="$t('admin_dash.users.actions.yes')"
@action="selectionConfirmed('adminRemoveUserFromAdminGroup')"
/>
<GenericConfirm
ref="confirmGrantModerator"
:title="$t('admin_dash.users.actions.confirm_multi.title')"
:message="$t('admin_dash.users.actions.confirm_multi.grant_moderator')"
:cancel-text="$t('admin_dash.users.actions.no')"
:confirm-text="$t('admin_dash.users.actions.yes')"
@action="selectionConfirmed('adminAddUserToModeratorGroup')"
/>
<GenericConfirm
ref="confirmRevokeModerator"
:title="$t('admin_dash.users.actions.confirm_multi.title')"
:message="$t('admin_dash.users.actions.confirm_multi.revoke_moderator')"
:cancel-text="$t('admin_dash.users.actions.no')"
:confirm-text="$t('admin_dash.users.actions.yes')"
@action="selectionConfirmed('adminRemoveUserFromModeratorGroup')"
/>
<GenericConfirm
ref="confirmApprove"
:title="$t('admin_dash.users.actions.confirm_multi.title')"
:message="$t('admin_dash.users.actions.confirm_multi.approve')"
:cancel-text="$t('admin_dash.users.actions.no')"
:confirm-text="$t('admin_dash.users.actions.yes')"
@action="selectionConfirmed('adminApproveUser')"
/>
<GenericConfirm
ref="confirmConfirm"
:title="$t('admin_dash.users.actions.confirm_multi.title')"
:message="$t('admin_dash.users.actions.confirm_multi.confirm')"
:cancel-text="$t('admin_dash.users.actions.no')"
:confirm-text="$t('admin_dash.users.actions.yes')"
@action="selectionConfirmed('adminConfirmUser')"
/>
<GenericConfirm
ref="confirmResendEmail"
:title="$t('admin_dash.users.actions.confirm_multi.title')"
:message="$t('admin_dash.users.actions.confirm_multi.resend_confirmation_email')"
:cancel-text="$t('admin_dash.users.actions.no')"
:confirm-text="$t('admin_dash.users.actions.yes')"
@action="selectionConfirmed('adminResendConfirmationEmail')"
/>
<GenericConfirm
ref="confirmRequirePasswordChange"
:title="$t('admin_dash.users.actions.confirm_multi.title')"
:message="$t('admin_dash.users.actions.confirm_multi.require_password_change')"
:cancel-text="$t('admin_dash.users.actions.no')"
:confirm-text="$t('admin_dash.users.actions.yes')"
@action="selectionConfirmed('adminRequirePasswordChange')"
/>
<GenericConfirm
ref="confirmDisableMFA"
:title="$t('admin_dash.users.actions.confirm_multi.title')"
:message="$t('admin_dash.users.actions.confirm_multi.disable_mfa')"
:cancel-text="$t('admin_dash.users.actions.no')"
:confirm-text="$t('admin_dash.users.actions.yes')"
@action="selectionConfirmed('adminDisableMFA')"
/>
</div> </div>
</template> </template>
<script src="./users_tab.js"></script> <script src="./users_tab.js"></script>

View file

@ -1,6 +1,7 @@
import { mapState as mapPiniaState } from 'pinia' import { mapState as mapPiniaState } from 'pinia'
import { mapState } from 'vuex' import { mapState } from 'vuex'
import { useAdminSettingsStore } from 'src/stores/admin_settings.js'
import { useMergedConfigStore } from 'src/stores/merged_config.js' import { useMergedConfigStore } from 'src/stores/merged_config.js'
const SharedComputedObject = () => ({ const SharedComputedObject = () => ({
@ -8,9 +9,11 @@ const SharedComputedObject = () => ({
...mapPiniaState(useMergedConfigStore, { ...mapPiniaState(useMergedConfigStore, {
expertLevel: (store) => store.mergedConfig.expertLevel, expertLevel: (store) => store.mergedConfig.expertLevel,
}), }),
...mapPiniaState(useAdminSettingsStore, {
adminConfig: (store) => store.config,
adminDraft: (store) => store.draft,
}),
...mapState({ ...mapState({
adminConfig: (state) => state.adminSettings.config,
adminDraft: (state) => state.adminSettings.draft,
user: (state) => state.users.currentUser, user: (state) => state.users.currentUser,
}), }),
}) })

View file

@ -229,7 +229,7 @@ export const BUTTONS = [
return ( return (
loggedIn && loggedIn &&
(status.user.id === currentUser.id || (status.user.id === currentUser.id ||
currentUser.privileges.includes('messages_delete')) currentUser.privileges.has('messages_delete'))
) )
}, },
confirm: ({ getters }) => useMergedConfigStore().mergedConfig.modalOnDelete, confirm: ({ getters }) => useMergedConfigStore().mergedConfig.modalOnDelete,

View file

@ -280,7 +280,7 @@ export default {
}, },
}, },
visibleRole() { visibleRole() {
if (!this.newShowRole) { if (!this.user.show_role && !this.user.adminData) {
return return
} }
const rights = this.user.rights const rights = this.user.rights

View file

@ -291,8 +291,7 @@
</button> </button>
<ModerationTools <ModerationTools
v-if="showModerationMenu" v-if="showModerationMenu"
class="moderation-menu" :users="[user]"
:user="user"
/> />
</div> </div>
<div <div

View file

@ -1331,79 +1331,10 @@
"moderator": "Moderator", "moderator": "Moderator",
"active": "Active", "active": "Active",
"deactivated": "Deactivated", "deactivated": "Deactivated",
"confirmed": "Confirmed" "confirmed": "Confirmed",
}, "unconfirmed": "Pending confirmation",
"actions": { "approved": "Approved",
"title": "Actions", "unapproved": "Pending approval"
"yes": "Confirm",
"no": "Abort",
"activate": "Activate",
"deactivate": "Deactivate",
"delete_user": "Delete",
"delete_status": "Delete",
"grant_admin": "Grant Admin",
"revoke_admin": "Revoke Admin",
"grant_moderator": "Grant Moderator",
"revoke_moderator": "Revoke Moderator",
"approve": "Approve",
"confirm": "Confirm",
"require_password_change": "Require Password Change",
"resend_confirmation_email": "Resend Confirmation Email",
"disable_mfa": "Disable MFA",
"confirm_single": {
"title": "Confirm",
"activate": "Activate User?",
"deactivate": "Deactivate User?",
"delete_user": "Delete User?",
"delete_status": "Delete Status?",
"grant_admin": "Grant Admin Privileges?",
"revoke_admin": "Revoke Admin Privileges?",
"grant_moderator": "Grant Moderator Privileges?",
"revoke_moderator": "Revoke Moderator Privileges?",
"approve": "Approve User?",
"confirm": "Confirm User?",
"require_password_change": "Require Password Change?",
"resend_confirmation_email": "Resend Confirmation Email?",
"disable_mfa": "Disable MFA?"
},
"confirm_multi": {
"title": "Confirm For Selection",
"activate": "Activate Selected Users?",
"deactivate": "Deactivate Selected Users?",
"delete_user": "Delete Selected Users?",
"delete_status": "Delete Selected Statuses?",
"grant_admin": "Grant Admin Privileges For Selected Users?",
"revoke_admin": "Revoke Admin Privileges For Selected Users?",
"grant_moderator": "Grant Moderator Privileges For Selected Users?",
"revoke_moderator": "Revoke Moderator Privileges For Selected Users?",
"approve": "Approve Selected Users?",
"confirm": "Confirm Selected Users?",
"require_password_change": "Require Password Change For Selected Users?",
"resend_confirmation_email": "Resend Confirmation Email For Selected Users?",
"disable_mfa": "Disable MFA For Selected Users?"
}
},
"details": {
"title": "User Details",
"overview": "Overview",
"button": "Details",
"id": "ID",
"actor_type": "Actor Type",
"actor_types": {
"person": "Person",
"service": "Service",
"application": "Application"
},
"admin": "Admin",
"moderator": "Moderator",
"local": "Local",
"active": "Active",
"inactive": "Inactive",
"tags": "Tags",
"roles": "Roles",
"status": "Status",
"remote": "Remote",
"account_type": "Account Type"
} }
}, },
"limits": { "limits": {
@ -1845,8 +1776,15 @@
"grant_moderator": "Grant Moderator", "grant_moderator": "Grant Moderator",
"revoke_moderator": "Revoke Moderator", "revoke_moderator": "Revoke Moderator",
"activate_account": "Activate account", "activate_account": "Activate account",
"deactivate_account": "Deactivate account", "deactivate_account": "Deactivate",
"delete_account": "Delete account", "delete_account": "Delete",
"suggest_account": "Add to suggested",
"remove_suggested_account": "Remove from suggested",
"approve_account": "Approve",
"confirm_account": "Confirm",
"disable_mfa": "Disable MFA",
"force_nsfw": "Mark all posts as NSFW", "force_nsfw": "Mark all posts as NSFW",
"strip_media": "Remove media from posts", "strip_media": "Remove media from posts",
"force_unlisted": "Force posts to be unlisted", "force_unlisted": "Force posts to be unlisted",
@ -1854,8 +1792,38 @@
"disable_remote_subscription": "Disallow following user from remote instances", "disable_remote_subscription": "Disallow following user from remote instances",
"disable_any_subscription": "Disallow following user at all", "disable_any_subscription": "Disallow following user at all",
"quarantine": "Disallow user posts from federating", "quarantine": "Disallow user posts from federating",
"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?" "require_password_change": "Require Password Change",
"resend_confirmation": "Resend Confirmation Email",
"confirm_modal": {
"delete_title": "User deletion",
"delete_content": "Delete user {user}? | Delete {count} users?",
"delete_content_2": "This will permanently delete the data from this accounts and deactivate it. Are you absolutely sure?",
"activate_title": "User activation",
"activate_content": "Activate user {user}? | Activate {count} users?",
"deactivate_content": "Dectivate user {user}? | Dectivate {count} users?",
"approval_title": "Approve users",
"approval_content": "Approve user {user}? | Approve {count} users?",
"confirm_title": "Confirm users",
"confirm_content": "Approve user {user}? | Approve {count} users?",
"suggest_title": "Suggest users",
"add_suggest_content": "Add user {user} to suggested list? | Add {count} users to suggested list?",
"rights_title": "Promote users",
"grant_rights_content": "Grant user {user} {name} role? | Grant {count} users {name} role?",
"revoke_rights_content": "Revoke {name} role from user {user}? | Revoke {name} from {count} users?",
"tag_title": "Assign user policy",
"assign_tag_content": "Assign {user} a {name} policy? | Assign {name} policy to {count} users?",
"unassign_tag_content": "Unassign policy {name} from {user}? | Unassign policy {name} from {count} users?",
"delete": "Delete",
"activate": "Activate",
"deactivate": "Deactivate",
"grant": "Grant",
"revoke": "Revoke",
"approve": "Approve",
"confirm": "Confirm",
"assign": "Assign",
"unassign": "Unassign"
}
}, },
"highlight_new": { "highlight_new": {
"disabled": "Don't highlight", "disabled": "Don't highlight",

View file

@ -170,15 +170,11 @@ const unmuteDomain = (store, domain) => {
export const mutations = { export const mutations = {
tagUser(state, { user: { id }, tag }) { tagUser(state, { user: { id }, tag }) {
const user = state.usersObject[id] const user = state.usersObject[id]
const tags = user.tags || [] user.tags.add(tag)
const newTags = tags.concat([tag])
user.tags = newTags
}, },
untagUser(state, { user: { id }, tag }) { untagUser(state, { user: { id }, tag }) {
const user = state.usersObject[id] const user = state.usersObject[id]
const tags = user.tags || [] user.tags.delete(tag)
const newTags = tags.filter((t) => t !== tag)
user.tags = newTags
}, },
updateRight(state, { user: { id }, right, value }) { updateRight(state, { user: { id }, right, value }) {
const user = state.usersObject[id] const user = state.usersObject[id]
@ -186,9 +182,12 @@ export const mutations = {
newRights[right] = value newRights[right] = value
user.rights = newRights user.rights = newRights
}, },
updateActivationStatus(state, { user: { id }, deactivated }) { updateUserAdminData(state, { user }) {
const user = state.usersObject[id] const { id } = user
user.deactivated = deactivated const localUser = state.usersObject[id]
localUser.adminData = user
localUser.deactivated = !user.is_active
localUser.tags = new Set(user.tags)
}, },
setCurrentUser(state, user) { setCurrentUser(state, user) {
state.lastLoginName = user.screen_name state.lastLoginName = user.screen_name
@ -369,10 +368,22 @@ const users = {
getters, getters,
actions: { actions: {
fetchUserIfMissing(store, id) { fetchUserIfMissing(store, id) {
if (!store.getters.findUser(id)) { const user = store.getters.findUser(id)
store.dispatch('fetchUser', id) if (!user) {
return store.dispatch('fetchUser', id)
} else {
return Promise.resolve(user)
} }
}, },
updateUserAdminData(store, { userAdminData }) {
return store
.dispatch('fetchUserIfMissing', userAdminData.id)
.then((user) => {
user.adminData = userAdminData
store.commit('addNewUsers', [user])
return user
})
},
fetchUser(store, id) { fetchUser(store, id) {
return store.rootState.api.backendInteractor return store.rootState.api.backendInteractor
.fetchUser({ id }) .fetchUser({ id })
@ -541,15 +552,6 @@ const users = {
commit('updateUserRelationship', [relationship]), commit('updateUserRelationship', [relationship]),
) )
}, },
toggleActivationStatus({ rootState, commit }, { user }) {
const api = user.deactivated
? rootState.api.backendInteractor.activateUser
: rootState.api.backendInteractor.deactivateUser
api({ user }).then((user) => {
const deactivated = !user.is_active
commit('updateActivationStatus', { user, deactivated })
})
},
registerPushNotifications(store) { registerPushNotifications(store) {
const token = store.state.currentUser.credentials const token = store.state.currentUser.credentials
const vapidPublicKey = useInstanceStore().vapidPublicKey const vapidPublicKey = useInstanceStore().vapidPublicKey

View file

@ -20,12 +20,6 @@ const CHANGE_EMAIL_URL = '/api/pleroma/change_email'
const CHANGE_PASSWORD_URL = '/api/pleroma/change_password' const CHANGE_PASSWORD_URL = '/api/pleroma/change_password'
const MOVE_ACCOUNT_URL = '/api/pleroma/move_account' const MOVE_ACCOUNT_URL = '/api/pleroma/move_account'
const ALIASES_URL = '/api/pleroma/aliases' const ALIASES_URL = '/api/pleroma/aliases'
const TAG_USER_URL = '/api/pleroma/admin/users/tag'
const PERMISSION_GROUP_URL = (screenName, right) =>
`/api/pleroma/admin/users/${screenName}/permission_group/${right}`
const ACTIVATE_USER_URL = '/api/pleroma/admin/users/activate'
const DEACTIVATE_USER_URL = '/api/pleroma/admin/users/deactivate'
const ADMIN_USERS_URL = '/api/v1/pleroma/admin/users'
const SUGGESTIONS_URL = '/api/v1/suggestions' const SUGGESTIONS_URL = '/api/v1/suggestions'
const NOTIFICATION_SETTINGS_URL = '/api/pleroma/notification_settings' const NOTIFICATION_SETTINGS_URL = '/api/pleroma/notification_settings'
const NOTIFICATION_READ_URL = '/api/v1/pleroma/notifications/read' const NOTIFICATION_READ_URL = '/api/v1/pleroma/notifications/read'
@ -121,7 +115,6 @@ const PLEROMA_CHAT_MESSAGES_URL = (id) => `/api/v1/pleroma/chats/${id}/messages`
const PLEROMA_CHAT_READ_URL = (id) => `/api/v1/pleroma/chats/${id}/read` const PLEROMA_CHAT_READ_URL = (id) => `/api/v1/pleroma/chats/${id}/read`
const PLEROMA_DELETE_CHAT_MESSAGE_URL = (chatId, messageId) => const PLEROMA_DELETE_CHAT_MESSAGE_URL = (chatId, messageId) =>
`/api/v1/pleroma/chats/${chatId}/messages/${messageId}` `/api/v1/pleroma/chats/${chatId}/messages/${messageId}`
const PLEROMA_ADMIN_REPORTS = '/api/v1/pleroma/admin/reports'
const PLEROMA_BACKUP_URL = '/api/v1/pleroma/backups' const PLEROMA_BACKUP_URL = '/api/v1/pleroma/backups'
const PLEROMA_ANNOUNCEMENTS_URL = '/api/v1/pleroma/admin/announcements' const PLEROMA_ANNOUNCEMENTS_URL = '/api/v1/pleroma/admin/announcements'
const PLEROMA_POST_ANNOUNCEMENT_URL = '/api/v1/pleroma/admin/announcements' const PLEROMA_POST_ANNOUNCEMENT_URL = '/api/v1/pleroma/admin/announcements'
@ -138,6 +131,7 @@ const PLEROMA_BOOKMARK_FOLDERS_URL = '/api/v1/pleroma/bookmark_folders'
const PLEROMA_BOOKMARK_FOLDER_URL = (id) => const PLEROMA_BOOKMARK_FOLDER_URL = (id) =>
`/api/v1/pleroma/bookmark_folders/${id}` `/api/v1/pleroma/bookmark_folders/${id}`
const PLEROMA_ADMIN_REPORTS = '/api/v1/pleroma/admin/reports'
const PLEROMA_ADMIN_CONFIG_URL = '/api/v1/pleroma/admin/config' const PLEROMA_ADMIN_CONFIG_URL = '/api/v1/pleroma/admin/config'
const PLEROMA_ADMIN_DESCRIPTIONS_URL = const PLEROMA_ADMIN_DESCRIPTIONS_URL =
'/api/v1/pleroma/admin/config/descriptions' '/api/v1/pleroma/admin/config/descriptions'
@ -145,7 +139,10 @@ const PLEROMA_ADMIN_FRONTENDS_URL = '/api/v1/pleroma/admin/frontends'
const PLEROMA_ADMIN_FRONTENDS_INSTALL_URL = const PLEROMA_ADMIN_FRONTENDS_INSTALL_URL =
'/api/v1/pleroma/admin/frontends/install' '/api/v1/pleroma/admin/frontends/install'
const PLEROMA_ADMIN_USERS_URL = ({ const PLEROMA_ADMIN_USERS_URL = '/api/v1/pleroma/admin/users'
const PLEROMA_ADMIN_USERS_URL_SHOW = (nickname) =>
`/api/v1/pleroma/admin/users/${nickname}`
const PLEROMA_ADMIN_USERS_URL_LIST = ({
page, page,
pageSize, pageSize,
filters = {}, filters = {},
@ -177,13 +174,18 @@ const PLEROMA_ADMIN_USERS_URL = ({
.join(',') .join(',')
return `/api/v1/pleroma/admin/users?page=${page}&page_size=${pageSize}&filters=${filters_str}&query=${query}&name=${name}&email=${email}` return `/api/v1/pleroma/admin/users?page=${page}&page_size=${pageSize}&filters=${filters_str}&query=${query}&name=${name}&email=${email}`
} }
const PLEROMA_ADMIN_MODIFY_GROUP_URL = (nickname, group) => const PLEROMA_ADMIN_TAG_USER_URL = '/api/pleroma/admin/users/tag'
`/api/v1/pleroma/admin/users/${nickname}/permission_group/${group}` const PLEROMA_ADMIN_PERMISSION_GROUP_URL = (right) =>
const PLEROMA_ADMIN_CONFIRM_USER_URL = `/api/pleroma/admin/users/permission_group/${right}`
const PLEROMA_ADMIN_ACTIVATE_USERS_URL = '/api/pleroma/admin/users/activate'
const PLEROMA_ADMIN_DEACTIVATE_USERS_URL = '/api/pleroma/admin/users/deactivate'
const PLEROMA_ADMIN_SUGGEST_USERS_URL = '/api/pleroma/admin/users/suggest'
const PLEROMA_ADMIN_UNSUGGEST_USERS_URL = '/api/pleroma/admin/users/unsuggest'
const PLEROMA_ADMIN_APPROVE_USERS_URL = '/api/v1/pleroma/admin/users/approve'
const PLEROMA_ADMIN_CONFIRM_USERS_URL =
'/api/v1/pleroma/admin/users/confirm_email' '/api/v1/pleroma/admin/users/confirm_email'
const PLEROMA_ADMIN_RESEND_CONFIRMATION_EMAIL_URL = const PLEROMA_ADMIN_RESEND_CONFIRMATION_EMAIL_URL =
'/api/v1/pleroma/admin/users/resend_confirmation_email' '/api/v1/pleroma/admin/users/resend_confirmation_email'
const PLEROMA_ADMIN_APPROVE_URL = '/api/v1/pleroma/admin/users/approve'
const PLEROMA_ADMIN_LIST_STATUSES_URL = (id, pageSize, godmode, withReblogs) => const PLEROMA_ADMIN_LIST_STATUSES_URL = (id, pageSize, godmode, withReblogs) =>
`/api/v1/pleroma/admin/users/${id}/statuses?page_size=${pageSize}&godmode=${godmode}&with_reblogs=${withReblogs}` `/api/v1/pleroma/admin/users/${id}/statuses?page_size=${pageSize}&godmode=${godmode}&with_reblogs=${withReblogs}`
const PLEROMA_ADMIN_CHANGE_STATUS_SCOPE_URL = (id) => const PLEROMA_ADMIN_CHANGE_STATUS_SCOPE_URL = (id) =>
@ -251,8 +253,11 @@ const promisedRequest = ({
} }
} }
return fetch(url, options).then((response) => { return fetch(url, options).then((response) => {
return new Promise((resolve, reject) => return new Promise((resolve, reject) => {
response // 204 is "No content", which fails to parse json (as you'd might think)
if (response.ok && response.status === 204) resolve()
return response
.json() .json()
.then((json) => { .then((json) => {
if (!response.ok) { if (!response.ok) {
@ -276,8 +281,8 @@ const promisedRequest = ({
response, response,
), ),
) )
}), })
) })
}) })
} }
@ -769,92 +774,118 @@ const fetchStatusHistory = ({ status, credentials }) => {
}) })
} }
const tagUser = ({ tag, credentials, user }) => { const adminSetUsersTags = ({
const screenName = user.screen_name tags,
const form = { credentials,
nicknames: [screenName], value,
tags: [tag], screen_names: nicknames,
} }) => {
const headers = authHeaders(credentials)
headers['Content-Type'] = 'application/json'
return fetch(TAG_USER_URL, {
method: 'PUT',
headers,
body: JSON.stringify(form),
})
}
const untagUser = ({ tag, credentials, user }) => {
const screenName = user.screen_name
const body = {
nicknames: [screenName],
tags: [tag],
}
const headers = authHeaders(credentials)
headers['Content-Type'] = 'application/json'
return fetch(TAG_USER_URL, {
method: 'DELETE',
headers,
body: JSON.stringify(body),
})
}
const addRight = ({ right, credentials, user }) => {
const screenName = user.screen_name
return fetch(PERMISSION_GROUP_URL(screenName, right), {
method: 'POST',
headers: authHeaders(credentials),
body: {},
})
}
const deleteRight = ({ right, credentials, user }) => {
const screenName = user.screen_name
return fetch(PERMISSION_GROUP_URL(screenName, right), {
method: 'DELETE',
headers: authHeaders(credentials),
body: {},
})
}
const activateUser = ({ credentials, user: { screen_name: nickname } }) => {
return promisedRequest({ return promisedRequest({
url: ACTIVATE_USER_URL, url: PLEROMA_ADMIN_TAG_USER_URL,
method: value ? 'PUT' : 'DELETE',
credentials,
payload: {
nicknames,
tags,
},
})
}
const adminSetUsersRight = ({
right,
credentials,
value,
screen_names: nicknames,
}) => {
return promisedRequest({
url: PLEROMA_ADMIN_PERMISSION_GROUP_URL(right),
method: value ? 'POST' : 'DELETE',
credentials,
payload: {
nicknames,
},
})
}
const adminSetUsersActivationStatus = ({
credentials,
screen_names: nicknames,
value,
}) => {
return promisedRequest({
url: value
? PLEROMA_ADMIN_ACTIVATE_USERS_URL
: PLEROMA_ADMIN_DEACTIVATE_USERS_URL,
method: 'PATCH', method: 'PATCH',
credentials, credentials,
payload: { payload: {
nicknames: [nickname], nicknames,
}, },
}).then((response) => response.users[0]) }).then((response) => response.users)
} }
const deactivateUser = ({ credentials, user: { screen_name: nickname } }) => { const adminSetUsersApprovalStatus = ({
credentials,
screen_names: nicknames,
}) => {
return promisedRequest({ return promisedRequest({
url: DEACTIVATE_USER_URL, url: PLEROMA_ADMIN_APPROVE_USERS_URL,
method: 'PATCH', method: 'PATCH',
credentials, credentials,
payload: { payload: {
nicknames: [nickname], nicknames,
}, },
}).then((response) => response.users[0]) }).then((response) => response.users)
} }
const deleteUser = ({ credentials, user: { screen_name: nickname } }) => { const adminSetUsersConfirmationStatus = ({
const r = promisedRequest({ credentials,
url: ADMIN_USERS_URL, screen_names: nicknames,
}) => {
return promisedRequest({
url: PLEROMA_ADMIN_CONFIRM_USERS_URL,
method: 'PATCH',
credentials,
payload: {
nicknames,
},
}).then((response) => response.users)
}
const adminSetUsersSuggestionStatus = ({
credentials,
screen_names: nicknames,
value,
}) => {
return promisedRequest({
url: value
? PLEROMA_ADMIN_SUGGEST_USERS_URL
: PLEROMA_ADMIN_UNSUGGEST_USERS_URL,
method: 'PATCH',
credentials,
payload: {
nicknames,
},
}).then((response) => response.users)
}
const adminGetUserData = ({ credentials, screen_name: nickname }) => {
return promisedRequest({
url: PLEROMA_ADMIN_USERS_URL_SHOW(nickname),
method: 'GET',
credentials,
})
}
const adminDeleteAccounts = ({ credentials, screen_names: nicknames }) => {
return promisedRequest({
url: PLEROMA_ADMIN_USERS_URL,
method: 'DELETE', method: 'DELETE',
credentials, credentials,
payload: { payload: {
nicknames: [nickname], nicknames,
}, },
}) })
return r.then((response) => response.users[0])
} }
const fetchTimeline = ({ const fetchTimeline = ({
@ -1668,75 +1699,57 @@ const dismissAnnouncement = ({ id, credentials }) => {
const adminListUsers = ({ opts, credentials }) => { const adminListUsers = ({ opts, credentials }) => {
// the reported list is hardly useful because standards are for dating i guess, // the reported list is hardly useful because standards are for dating i guess,
// so make sure to fetchIfMissing right afterward using this call // so make sure to fetchIfMissing right afterward using this call
const url = PLEROMA_ADMIN_USERS_URL(opts) const url = PLEROMA_ADMIN_USERS_URL_LIST(opts)
return promisedRequest({ return promisedRequest({
url: url, url: url,
credentials, credentials,
method: 'GET', method: 'GET',
}).then((data) => ({ }).then((data) => ({
...data, ...data,
users: data.users.map(parseUser), users: data.users,
})) }))
} }
const adminAddUserToAdminGroup = ({ user, credentials }) => {
const url = PLEROMA_ADMIN_MODIFY_GROUP_URL(user.name_html, 'admin')
return promisedRequest({ url: url, credentials, method: 'POST' })
}
const adminRemoveUserFromAdminGroup = ({ user, credentials }) => {
const url = PLEROMA_ADMIN_MODIFY_GROUP_URL(user.name_html, 'admin')
return promisedRequest({ url: url, credentials, method: 'DELETE' })
}
const adminAddUserToModeratorGroup = ({ user, credentials }) => {
const url = PLEROMA_ADMIN_MODIFY_GROUP_URL(user.name_html, 'moderator')
return promisedRequest({ url: url, credentials, method: 'POST' })
}
const adminRemoveUserFromModeratorGroup = ({ user, credentials }) => {
const url = PLEROMA_ADMIN_MODIFY_GROUP_URL(user.name_html, 'moderator')
return promisedRequest({ url: url, credentials, method: 'DELETE' })
}
const adminConfirmUser = ({ user: { screen_name: nickname }, credentials }) => {
const url = PLEROMA_ADMIN_CONFIRM_USER_URL
return promisedRequest({
url: url,
credentials,
method: 'PATCH',
payload: {
nicknames: [nickname],
},
})
}
const adminResendConfirmationEmail = ({ const adminResendConfirmationEmail = ({
user: { screen_name: nickname }, screen_names: nicknames,
credentials, credentials,
}) => { }) => {
const url = PLEROMA_ADMIN_RESEND_CONFIRMATION_EMAIL_URL const url = PLEROMA_ADMIN_RESEND_CONFIRMATION_EMAIL_URL
return promisedRequest({ return promisedRequest({
url: url, url,
credentials, credentials,
method: 'PATCH', method: 'PATCH',
payload: { payload: {
nicknames: [nickname], nicknames,
}, },
}) })
} }
const adminApproveUser = ({ user: { screen_name: nickname }, credentials }) => { const adminRequirePasswordChange = ({
const url = PLEROMA_ADMIN_APPROVE_URL user: { screen_names: nicknames },
const r = promisedRequest({ credentials,
url: url, }) => {
const url = PLEROMA_ADMIN_REQUIRE_PASSWORD_CHANGE_URL
return promisedRequest({
url,
credentials, credentials,
method: 'PATCH', method: 'PATCH',
payload: { payload: {
nicknames: [nickname], nicknames,
},
})
}
const adminDisableMFA = ({ user: { screen_name: nickname }, credentials }) => {
const url = PLEROMA_ADMIN_DISABLE_MFA_URL
return promisedRequest({
url,
credentials,
method: 'PUT',
payload: {
nickname,
}, },
}) })
return r
} }
const adminListStatuses = ({ const adminListStatuses = ({
@ -1775,33 +1788,6 @@ const adminChangeStatusScope = ({
}) })
} }
const adminRequirePasswordChange = ({
user: { screen_name: nickname },
credentials,
}) => {
const url = PLEROMA_ADMIN_REQUIRE_PASSWORD_CHANGE_URL
return promisedRequest({
url,
credentials,
method: 'PATCH',
payload: {
nicknames: [nickname],
},
})
}
const adminDisableMFA = ({ user: { screen_name: nickname }, credentials }) => {
const url = PLEROMA_ADMIN_DISABLE_MFA_URL
return promisedRequest({
url,
credentials,
method: 'PUT',
payload: {
nickname,
},
})
}
const announcementToPayload = ({ content, startsAt, endsAt, allDay }) => { const announcementToPayload = ({ content, startsAt, endsAt, allDay }) => {
const payload = { content } const payload = { content }
@ -2438,13 +2424,6 @@ const apiService = {
fetchBlocks, fetchBlocks,
fetchOAuthTokens, fetchOAuthTokens,
revokeOAuthToken, revokeOAuthToken,
tagUser,
untagUser,
deleteUser,
addRight,
deleteRight,
activateUser,
deactivateUser,
register, register,
getCaptcha, getCaptcha,
updateProfileImages, updateProfileImages,
@ -2533,13 +2512,15 @@ const apiService = {
updateBookmarkFolder, updateBookmarkFolder,
deleteBookmarkFolder, deleteBookmarkFolder,
adminListUsers, adminListUsers,
adminAddUserToAdminGroup, adminGetUserData,
adminRemoveUserFromAdminGroup,
adminAddUserToModeratorGroup,
adminRemoveUserFromModeratorGroup,
adminConfirmUser,
adminResendConfirmationEmail, adminResendConfirmationEmail,
adminApproveUser, adminDeleteAccounts,
adminSetUsersRight,
adminSetUsersTags,
adminSetUsersApprovalStatus,
adminSetUsersConfirmationStatus,
adminSetUsersActivationStatus,
adminSetUsersSuggestionStatus,
adminListStatuses, adminListStatuses,
adminChangeStatusScope, adminChangeStatusScope,
adminRequirePasswordChange, adminRequirePasswordChange,

View file

@ -17,12 +17,13 @@ import { isStatusNotification } from '../notification_utils/notification_utils.j
export const parseUser = (data) => { export const parseUser = (data) => {
const output = {} const output = {}
output._original = data // used for server-side settings
// case for users in "mentions" property for statuses in MastoAPI // case for users in "mentions" property for statuses in MastoAPI
const mastoShort = !Object.hasOwn(data, 'avatar') const mastoShort = !Object.hasOwn(data, 'avatar')
output.inLists = null output.inLists = null
output.id = String(data.id) output.id = String(data.id)
output._original = data // used for server-side settings
output.screen_name = data.acct output.screen_name = data.acct
output.fqn = data.fqn output.fqn = data.fqn
@ -118,9 +119,9 @@ export const parseUser = (data) => {
output.birthday = data.pleroma.birthday output.birthday = data.pleroma.birthday
if (data.pleroma.privileges) { if (data.pleroma.privileges) {
output.privileges = data.pleroma.privileges output.privileges = new Set(data.pleroma.privileges)
} else if (data.pleroma.is_admin) { } else if (data.pleroma.is_admin) {
output.privileges = [ output.privileges = new Set([
'users_read', 'users_read',
'users_manage_invites', 'users_manage_invites',
'users_manage_activation_state', 'users_manage_activation_state',
@ -135,11 +136,11 @@ export const parseUser = (data) => {
'announcements_manage_announcements', 'announcements_manage_announcements',
'emoji_manage_emoji', 'emoji_manage_emoji',
'statistics_read', 'statistics_read',
] ])
} else if (data.pleroma.is_moderator) { } else if (data.pleroma.is_moderator) {
output.privileges = ['messages_delete', 'reports_manage_reports'] output.privileges = new Set(['messages_delete', 'reports_manage_reports'])
} else { } else {
output.privileges = [] output.privileges = new Set()
} }
} }
@ -149,7 +150,7 @@ export const parseUser = (data) => {
output.fields = data.source.fields output.fields = data.source.fields
if (data.source.pleroma) { if (data.source.pleroma) {
output.no_rich_text = data.source.pleroma.no_rich_text output.no_rich_text = data.source.pleroma.no_rich_text
output.show_role = data.source.pleroma.show_role output.show_role = typeof data.source.pleroma.show_role === 'boolean' ? data.source.pleroma.show_role : true
output.discoverable = data.source.pleroma.discoverable output.discoverable = data.source.pleroma.discoverable
output.show_birthday = data.pleroma.show_birthday output.show_birthday = data.pleroma.show_birthday
output.actor_type = data.source.pleroma.actor_type output.actor_type = data.source.pleroma.actor_type
@ -168,7 +169,7 @@ export const parseUser = (data) => {
if (data.pleroma) { if (data.pleroma) {
output.follow_request_count = data.pleroma.follow_request_count output.follow_request_count = data.pleroma.follow_request_count
output.tags = data.pleroma.tags output.tags = new Set(data.pleroma.tags)
// deactivated was changed to is_active in Pleroma 2.3.0 // deactivated was changed to is_active in Pleroma 2.3.0
// so check if is_active is present // so check if is_active is present
@ -181,7 +182,7 @@ export const parseUser = (data) => {
output.unread_chat_count = data.pleroma.unread_chat_count output.unread_chat_count = data.pleroma.unread_chat_count
} }
output.tags = output.tags || [] output.tags = output.tags || new Set()
output.rights = output.rights || {} output.rights = output.rights || {}
output.notification_settings = output.notification_settings || {} output.notification_settings = output.notification_settings || {}

View file

@ -19,23 +19,14 @@ export const newUserFlags = {
export const useAdminSettingsStore = defineStore('adminSettings', { export const useAdminSettingsStore = defineStore('adminSettings', {
state: () => ({ state: () => ({
...cloneDeep(defaultState), ...cloneDeep(defaultState),
backendInteractor: window.vuex.state.api.backendInteractor,
}), }),
actions: { actions: {
// Configuration Stuff
setInstanceAdminNoDbConfig() { setInstanceAdminNoDbConfig() {
this.loaded = false this.loaded = false
this.dbConfigEnabled = false this.dbConfigEnabled = false
}, },
setAvailableFrontends({ frontends }) {
this.frontends = frontends.map((f) => {
f.installedRefs = f.installed_refs
if (f.name === 'pleroma-fe') {
f.refs = ['master', 'develop']
} else {
f.refs = [f.ref]
}
return f
})
},
updateAdminSettings({ config, modifiedPaths }) { updateAdminSettings({ config, modifiedPaths }) {
this.loaded = true this.loaded = true
this.dbConfigEnabled = true this.dbConfigEnabled = true
@ -59,130 +50,9 @@ export const useAdminSettingsStore = defineStore('adminSettings', {
resetAdminDraft() { resetAdminDraft() {
this.draft = cloneDeep(this.config) this.draft = cloneDeep(this.config)
}, },
async fetchAdminUsers(opts) {
const data = await window.vuex.state.api.backendInteractor.adminListUsers(
{
opts,
},
)
data.users.forEach((user) =>
window.vuex.dispatch('fetchUserIfMissing', user.id),
)
return data
},
adminAddUserToAdminGroup(user) {
window.vuex.state.api.backendInteractor
.adminAddUserToAdminGroup({ user })
.then((res) =>
window.vuex.commit('updateRight', {
user,
right: 'admin',
value: res.is_admin,
}),
)
},
adminRemoveUserFromAdminGroup(user) {
// prevent revokation of own rights
if (user.id !== window.vuex.state.users.currentUser.id) {
return window.vuex.state.api.backendInteractor
.adminRemoveUserFromAdminGroup({ user })
.then((res) =>
window.vuex.commit('updateRight', {
user,
right: 'admin',
value: res.is_admin,
}),
)
}
},
adminAddUserToModeratorGroup(user) {
return window.vuex.state.api.backendInteractor
.adminAddUserToModeratorGroup({ user })
.then((res) =>
window.vuex.commit('updateRight', {
user,
right: 'moderator',
value: res.is_moderator,
}),
)
},
adminRemoveUserFromModeratorGroup(user) {
// prevent revokation of own rights
if (user.id !== window.vuex.state.users.currentUser.id) {
return window.vuex.state.api.backendInteractor
.adminRemoveUserFromModeratorGroup({ user })
.then((res) =>
window.vuex.commit('updateRight', {
user,
right: 'moderator',
value: res.is_moderator,
}),
)
}
},
adminActivateUser(user) {
return window.vuex.state.api.backendInteractor
.activateUser({ user })
.then((res) => {
const deactivated = !res.is_active
window.vuex.commit('updateActivationStatus', { user, deactivated })
})
},
adminDeactivateUser(user) {
return window.vuex.state.api.backendInteractor
.deactivateUser({ user })
.then((res) => {
const deactivated = !res.is_active
window.vuex.commit('updateActivationStatus', { user, deactivated })
})
},
adminDeleteUser(user) {
return window.vuex.state.api.backendInteractor.deleteUser({ user })
},
adminConfirmUser(user) {
return window.vuex.state.api.backendInteractor
.adminConfirmUser({ user })
.then(() => window.vuex.dispatch('fetchUser', user.id))
},
adminResendConfirmationEmail(user) {
return window.vuex.state.api.backendInteractor.adminResendConfirmationEmail(
{ user },
)
},
adminApproveUser(user) {
return window.vuex.state.api.backendInteractor.adminApproveUser({ user })
},
adminListStatuses({ userId, opts }) {
return window.vuex.state.api.backendInteractor.adminListStatuses({
userId,
opts,
})
},
adminChangeStatusScope({ opts }) {
return window.vuex.state.api.backendInteractor.adminChangeStatusScope({
opts,
})
},
adminDisableMFA(user) {
return window.vuex.state.api.backendInteractor.adminDisableMFA({ user })
},
adminTagUser({ user, tag }) {
return window.vuex.state.api.backendInteractor.tagUser({ user, tag })
},
adminUntagUser({ user, tag }) {
return window.vuex.state.api.backendInteractor.untagUser({ user, tag })
},
loadFrontendsStuff() {
window.vuex.state.api.backendInteractor
.fetchAvailableFrontends()
.then((frontends) => this.setAvailableFrontends({ frontends }))
},
loadAdminStuff() { loadAdminStuff() {
window.vuex.state.api.backendInteractor this.backendInteractor.fetchInstanceDBConfig().then((backendDbConfig) => {
.fetchInstanceDBConfig()
.then((backendDbConfig) => {
if (backendDbConfig.error) { if (backendDbConfig.error) {
if (backendDbConfig.error.status === 400) { if (backendDbConfig.error.status === 400) {
backendDbConfig.error.json().then((errorJson) => { backendDbConfig.error.json().then((errorJson) => {
@ -196,7 +66,7 @@ export const useAdminSettingsStore = defineStore('adminSettings', {
} }
}) })
if (this.descriptions === null) { if (this.descriptions === null) {
window.vuex.state.api.backendInteractor this.backendInteractor
.fetchInstanceConfigDescriptions() .fetchInstanceConfigDescriptions()
.then((backendDescriptions) => .then((backendDescriptions) =>
this.setInstanceAdminDescriptions({ backendDescriptions }), this.setInstanceAdminDescriptions({ backendDescriptions }),
@ -408,5 +278,172 @@ export const useAdminSettingsStore = defineStore('adminSettings', {
this.setInstanceAdminSettings({ backendDbConfig }), this.setInstanceAdminSettings({ backendDbConfig }),
) )
}, },
// Frontends Stuff
loadFrontendsStuff() {
this.backendInteractor
.fetchAvailableFrontends()
.then((frontends) => this.setAvailableFrontends({ frontends }))
},
setAvailableFrontends({ frontends }) {
this.frontends = frontends.map((f) => {
f.installedRefs = f.installed_refs
if (f.name === 'pleroma-fe') {
f.refs = ['master', 'develop']
} else {
f.refs = [f.ref]
}
return f
})
},
// Statuses stuff
listStatuses({ userId, opts }) {
return this.backendInteractor.adminListStatuses({
userId,
opts,
})
},
changeStatusScope({ opts }) {
return this.backendInteractor.adminChangeStatusScope({
opts,
})
},
// Users stuff
async fetchAdminUsers(opts) {
const adminData = await this.backendInteractor.adminListUsers({
opts,
})
adminData.users = await Promise.all(
adminData.users.map(
async (userAdminData) =>
await window.vuex.dispatch('updateUserAdminData', {
userAdminData,
}),
),
)
return adminData
},
async getUserData({ user }) {
const api = this.backendInteractor.adminGetUserData
const { screen_name } = user
const result = await api({ screen_name })
window.vuex.commit('updateUserAdminData', { user: result })
},
async deleteUsers({ users }) {
const screen_names = users.map((u) => u.screen_name)
const api = this.backendInteractor.adminDeleteAccounts
const resultUserIds = await api({ screen_names })
resultUserIds.forEach((userId) => {
window.vuex.dispatch(
'markStatusesAsDeleted',
(status) => userId === status.user.id,
)
// TODO when migrated to pinia, also remove user
})
return resultUserIds
},
resendConfirmationEmail({ users }) {
const screen_names = users.map((u) => u.screen_name)
return this.backendInteractor.adminResendConfirmationEmail({
screen_names,
})
},
requirePasswordChange({ users }) {
const screen_names = users.map((u) => u.screen_name)
return this.backendInteractor.adminRequirePasswordChange({
screen_names,
})
},
// Singular only!
disableMFA({ user }) {
return this.backendInteractor.adminDisableMFA(user)
},
async setUsersTags({ users, tags, value }) {
const screen_names = users.map((u) => u.screen_name)
const api = this.backendInteractor.adminSetUsersTags
await api({
screen_names,
tags,
value,
})
users.forEach((user) => {
this.getUserData({ user })
})
},
async setUsersRight({ users, right, value }) {
const screen_names = users.map((u) => u.screen_name)
const api = this.backendInteractor.adminSetUsersRight
await api({
screen_names,
right,
value,
})
users.forEach((user) => {
window.vuex.commit('updateRight', { user, right, value })
})
},
async setUsersActivationStatus({ users, value }) {
const screen_names = users.map((u) => u.screen_name)
const api = this.backendInteractor.adminSetUsersActivationStatus
const resultUsers = await api({
screen_names,
value,
})
resultUsers.forEach((user) => {
window.vuex.commit('updateUserAdminData', { user })
})
},
async setUsersSuggestionStatus({ users, value }) {
const screen_names = users.map((u) => u.screen_name)
const api = this.backendInteractor.adminSetUsersSuggestionStatus
const resultUsers = await api({
screen_names,
value,
})
resultUsers.forEach((user) => {
window.vuex.commit('updateUserAdminData', { user })
})
},
async setUsersConfirmationStatus({ users }) {
const screen_names = users.map((u) => u.screen_name)
const api = this.backendInteractor.adminSetUsersConfirmationStatus
await api({ screen_names })
users.forEach((user) => {
this.getUserData({ user })
})
},
async setUsersApprovalStatus({ users }) {
const screen_names = users.map((u) => u.screen_name)
const api = this.backendInteractor.adminSetUsersApprovalStatus
const resultUsers = await api({
screen_names,
})
resultUsers.forEach((user) => {
window.vuex.commit('updateUserAdminData', { user })
})
},
}, },
}) })

View file

@ -29,7 +29,7 @@ export const useAnnouncementsStore = defineStore('announcements', {
const currentUser = window.vuex.state.users.currentUser const currentUser = window.vuex.state.users.currentUser
const isAdmin = const isAdmin =
currentUser && currentUser &&
currentUser.privileges.includes('announcements_manage_announcements') currentUser.privileges.has('announcements_manage_announcements')
const getAnnouncements = async () => { const getAnnouncements = async () => {
if (!isAdmin) { if (!isAdmin) {