pleroma-fe/src/stores/admin_settings.js

451 lines
13 KiB
JavaScript
Raw Normal View History

2026-01-06 16:23:17 +02:00
import { cloneDeep, differenceWith, flatten, get, isEqual, set } from 'lodash'
2026-06-09 13:11:21 +03:00
import { defineStore } from 'pinia'
export const defaultState = {
frontends: [],
2023-03-27 22:57:50 +03:00
loaded: false,
needsReboot: null,
config: null,
2023-03-19 21:27:07 +02:00
modifiedPaths: null,
descriptions: null,
2023-03-27 22:57:50 +03:00
draft: null,
2026-01-06 16:22:52 +02:00
dbConfigEnabled: null,
}
export const newUserFlags = {
2026-01-06 16:22:52 +02:00
...defaultState.flagStorage,
}
2026-06-09 13:11:21 +03:00
export const useAdminSettingsStore = defineStore('adminSettings', {
state: () => ({
2026-01-06 16:22:52 +02:00
...cloneDeep(defaultState),
backendInteractor: window.vuex.state.api.backendInteractor,
2026-06-09 13:11:21 +03:00
}),
actions: {
// Configuration Stuff
2026-06-09 13:11:21 +03:00
setInstanceAdminNoDbConfig() {
this.loaded = false
this.dbConfigEnabled = false
2023-03-27 22:57:50 +03:00
},
2026-06-09 13:11:21 +03:00
updateAdminSettings({ config, modifiedPaths }) {
this.loaded = true
this.dbConfigEnabled = true
this.config = config
this.modifiedPaths = modifiedPaths
2023-03-19 21:27:07 +02:00
},
2026-06-09 13:11:21 +03:00
updateAdminDescriptions({ descriptions }) {
this.descriptions = descriptions
},
2026-06-09 13:11:21 +03:00
updateAdminDraft({ path, value }) {
const [group, key, subkey] = path
const parent = [group, key, subkey]
2026-06-09 13:11:21 +03:00
set(this.draft, path, value)
// force-updating grouped draft to trigger refresh of group settings
if (path.length > parent.length) {
2026-06-09 13:11:21 +03:00
set(this.draft, parent, cloneDeep(get(this.draft, parent)))
}
},
2026-06-09 13:11:21 +03:00
resetAdminDraft() {
this.draft = cloneDeep(this.config)
2026-01-06 16:22:52 +02:00
},
2026-06-09 13:11:21 +03:00
loadAdminStuff() {
this.backendInteractor.fetchInstanceDBConfig().then((backendDbConfig) => {
if (backendDbConfig.error) {
if (backendDbConfig.error.status === 400) {
backendDbConfig.error.json().then((errorJson) => {
if (/configurable_from_database/.test(errorJson.error)) {
this.setInstanceAdminNoDbConfig()
}
})
2023-03-27 22:57:50 +03:00
}
} else {
this.setInstanceAdminSettings({ backendDbConfig })
}
})
2026-06-09 13:11:21 +03:00
if (this.descriptions === null) {
this.backendInteractor
2026-01-06 16:22:52 +02:00
.fetchInstanceConfigDescriptions()
.then((backendDescriptions) =>
2026-06-09 13:11:21 +03:00
this.setInstanceAdminDescriptions({ backendDescriptions }),
2026-01-06 16:22:52 +02:00
)
2023-03-27 22:57:50 +03:00
}
},
2026-06-09 13:11:21 +03:00
setInstanceAdminSettings({ backendDbConfig }) {
const config = this.config || {}
const modifiedPaths = new Set()
2025-12-09 13:21:46 +02:00
2026-01-06 16:22:52 +02:00
backendDbConfig.configs.forEach((c) => {
const path = [c.group, c.key]
if (c.db) {
// Path elements can contain dot, therefore we use ' -> ' as a separator instead
// Using strings for modified paths for easier searching
2026-01-06 16:22:52 +02:00
c.db.forEach((x) => modifiedPaths.add([...path, x].join(' -> ')))
}
2025-12-09 13:21:46 +02:00
// we need to preserve tuples on second level only, possibly third
// but it's not a case right now.
const convert = (value, preserveTuples, preserveTuplesLv2) => {
if (Array.isArray(value) && value.length > 0 && value[0].tuple) {
2025-12-09 13:21:46 +02:00
if (!preserveTuples) {
return value.reduce((acc, c) => {
if (c.tuple == null) {
return {
...acc,
[c]: c,
}
}
2026-01-06 16:22:52 +02:00
return {
...acc,
[c.tuple[0]]: convert(c.tuple[1], preserveTuplesLv2),
}
2025-12-09 13:21:46 +02:00
}, {})
} else {
2026-01-06 16:22:52 +02:00
return value.map((x) => x.tuple)
2025-12-09 13:21:46 +02:00
}
} else {
2025-12-09 13:21:46 +02:00
if (!preserveTuples) {
return value
} else {
return value.tuple
}
}
}
2025-12-09 13:21:46 +02:00
// for most stuff we want maps since those are more convenient
// however this doesn't allow for multiple values per same key
// so for those cases we want to preserve tuples as-is
// right now it's made exclusively for :pleroma.:rate_limit
// so it might not work properly elsewhere
2026-01-06 16:22:52 +02:00
const preserveTuples = path.find((x) => x === ':rate_limit')
2025-12-09 13:21:46 +02:00
set(config, path, convert(c.value, false, preserveTuples))
})
2025-12-08 13:50:08 +02:00
// patching http adapter config to be easier to handle
const adapter = config[':pleroma'][':http'][':adapter']
if (Array.isArray(adapter)) {
config[':pleroma'][':http'][':adapter'] = {
[':ssl_options']: {
2026-01-06 16:22:52 +02:00
[':versions']: [],
},
2025-12-08 13:50:08 +02:00
}
}
2026-06-09 13:11:21 +03:00
this.updateAdminSettings({ config, modifiedPaths })
this.resetAdminDraft()
},
2026-06-09 13:11:21 +03:00
setInstanceAdminDescriptions({ backendDescriptions }) {
2026-01-06 16:22:52 +02:00
const convert = (
{ children, description, label, key = '<ROOT>', group, suggestions },
path,
acc,
) => {
const newPath = group ? [group, key] : [key]
2023-03-19 21:27:07 +02:00
const obj = { description, label, suggestions }
if (Array.isArray(children)) {
2026-01-06 16:22:52 +02:00
children.forEach((c) => {
convert(c, newPath, obj)
2023-03-19 21:27:07 +02:00
})
}
set(acc, newPath, obj)
}
const descriptions = {}
2025-12-08 17:09:07 +02:00
2026-01-06 16:22:52 +02:00
backendDescriptions.forEach((d) => convert(d, '', descriptions))
2026-06-09 13:11:21 +03:00
this.updateAdminDescriptions({ descriptions })
2023-03-19 21:27:07 +02:00
},
// This action takes draft state, diffs it with live config state and then pushes
// only differences between the two. Difference detection only work up to subkey (third) level.
2026-06-09 13:11:21 +03:00
pushAdminDraft() {
// TODO cleanup paths in modifiedPaths
const convert = (value) => {
if (typeof value !== 'object') {
return value
} else if (Array.isArray(value)) {
return value.map(convert)
} else {
return Object.entries(value).map(([k, v]) => ({ tuple: [k, v] }))
}
}
// Getting all group-keys used in config
const allGroupKeys = flatten(
2026-06-09 13:11:21 +03:00
Object.entries(this.config).map(([group, lv1data]) =>
2026-01-06 16:22:52 +02:00
Object.keys(lv1data).map((key) => ({ group, key })),
),
)
// Only using group-keys where there are changes detected
const changedGroupKeys = allGroupKeys.filter(({ group, key }) => {
2026-06-09 13:11:21 +03:00
return !isEqual(this.config[group][key], this.draft[group][key])
})
// Here we take all changed group-keys and get all changed subkeys
const changed = changedGroupKeys.map(({ group, key }) => {
2026-06-09 13:11:21 +03:00
const config = this.config[group][key]
const draft = this.draft[group][key]
// We convert group-key value into entries arrays
const eConfig = Object.entries(config)
const eDraft = Object.entries(draft)
// Then those entries array we diff so only changed subkey entries remain
// We use the diffed array to reconstruct the object and then shove it into convert()
2026-01-06 16:22:52 +02:00
return {
group,
key,
value: convert(
Object.fromEntries(differenceWith(eDraft, eConfig, isEqual)),
),
}
})
2026-01-06 16:22:52 +02:00
2026-06-09 13:11:21 +03:00
window.vuex.state.api.backendInteractor
2026-01-06 16:22:52 +02:00
.pushInstanceDBConfig({
payload: {
configs: changed,
},
})
2026-06-09 13:11:21 +03:00
.then(() =>
window.vuex.state.api.backendInteractor.fetchInstanceDBConfig(),
)
2026-01-06 16:22:52 +02:00
.then((backendDbConfig) =>
2026-06-09 13:11:21 +03:00
this.setInstanceAdminSettings({ backendDbConfig }),
2026-01-06 16:22:52 +02:00
)
},
2026-06-09 13:11:21 +03:00
pushAdminSetting({ path, value }) {
2026-01-06 16:22:52 +02:00
const [group, key, ...rest] = Array.isArray(path)
? path
: path.split(/\./g)
const clone = {} // not actually cloning the entire thing to avoid excessive writes
set(clone, rest, value)
// TODO cleanup paths in modifiedPaths
const convert = (value) => {
if (typeof value !== 'object') {
return value
} else if (Array.isArray(value)) {
return value.map(convert)
} else {
return Object.entries(value).map(([k, v]) => ({ tuple: [k, v] }))
}
}
2026-06-09 13:11:21 +03:00
window.vuex.state.api.backendInteractor
2026-01-06 16:22:52 +02:00
.pushInstanceDBConfig({
payload: {
configs: [
{
group,
key,
value: convert(clone),
},
],
},
})
2026-06-09 13:11:21 +03:00
.then(() =>
window.vuex.state.api.backendInteractor.fetchInstanceDBConfig(),
)
2026-01-06 16:22:52 +02:00
.then((backendDbConfig) =>
2026-06-09 13:11:21 +03:00
this.setInstanceAdminSettings({ backendDbConfig }),
2026-01-06 16:22:52 +02:00
)
},
2026-06-09 13:11:21 +03:00
resetAdminSetting({ path }) {
2026-01-06 16:22:52 +02:00
const [group, key, subkey] = Array.isArray(path)
? path
: path.split(/\./g)
2026-06-09 13:11:21 +03:00
this.modifiedPaths.delete(path)
2026-06-09 13:11:21 +03:00
return window.vuex.state.api.backendInteractor
2026-01-06 16:22:52 +02:00
.pushInstanceDBConfig({
payload: {
configs: [
{
group,
key,
delete: true,
subkeys: [subkey],
},
],
},
})
2026-06-09 13:11:21 +03:00
.then(() =>
window.vuex.state.api.backendInteractor.fetchInstanceDBConfig(),
)
2026-01-06 16:22:52 +02:00
.then((backendDbConfig) =>
2026-06-09 13:11:21 +03:00
this.setInstanceAdminSettings({ backendDbConfig }),
2026-01-06 16:22:52 +02:00
)
},
// 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
2026-06-10 15:49:29 +03:00
async fetchUsers(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 }) {
2026-06-10 14:39:58 +03:00
const { screen_name } = user
return this.backendInteractor.adminDisableMFA({ screen_name })
},
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 })
})
},
2026-01-06 16:22:52 +02:00
},
2026-06-09 13:11:21 +03:00
})