diff --git a/CHANGELOG.md b/CHANGELOG.md index ac740f23..6a53ed8f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -16,6 +16,8 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). - Support pagination of local emoji packs and files - Add MRF Activity Expiration setting - Add ability to disable multi-factor authentication for a user +- Add ability to manually evict and ban URLs from the Pleroma MediaProxy cache +- Add Invalidation settings on MediaProxy tab - Ability to configure S3 settings on Upload tab - Show number of open reports in Sidebar Menu - Add confirmation message when deleting a user @@ -30,6 +32,8 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). - Remove ability to moderate users that don't have valid nickname - Displays both labels and description in the header of group of settiings - Ability to add custom values in Pleroma.Upload.Filter.Mogrify setting +- Change types of the following settings: ':groups', ':replace', ':federated_timeline_removal', ':reject', ':match_actor'. Update functions that parses and wraps settings data according to this change. +- Move rendering Crontab setting from a separate component to EditableKeyword component - Show only those MRF settings that have been enabled in MRF Policies setting - Move Auto Linker settings to Link Formatter Tab as its configuration was moved to :pleroma, Pleroma.Formatter diff --git a/src/api/mediaProxyCache.js b/src/api/mediaProxyCache.js new file mode 100644 index 00000000..0822d984 --- /dev/null +++ b/src/api/mediaProxyCache.js @@ -0,0 +1,34 @@ +import request from '@/utils/request' +import { getToken } from '@/utils/auth' +import { baseName } from './utils' + +export async function listBannedUrls(page, authHost, token) { + return await request({ + baseURL: baseName(authHost), + url: `/api/pleroma/admin/media_proxy_caches?page=${page}`, + method: 'get', + headers: authHeaders(token) + }) +} + +export async function purgeUrls(urls, ban, authHost, token) { + return await request({ + baseURL: baseName(authHost), + url: `/api/pleroma/admin/media_proxy_caches/purge`, + method: 'post', + headers: authHeaders(token), + data: { urls, ban } + }) +} + +export async function removeBannedUrls(urls, authHost, token) { + return await request({ + baseURL: baseName(authHost), + url: `/api/pleroma/admin/media_proxy_caches/delete`, + method: 'post', + headers: authHeaders(token), + data: { urls } + }) +} + +const authHeaders = (token) => token ? { 'Authorization': `Bearer ${getToken()}` } : {} diff --git a/src/lang/en.js b/src/lang/en.js index 3b644597..478a5cc7 100644 --- a/src/lang/en.js +++ b/src/lang/en.js @@ -65,8 +65,11 @@ export default { externalLink: 'External Link', users: 'Users', reports: 'Reports', + invites: 'Invites', + statuses: 'Statuses', settings: 'Settings', moderationLog: 'Moderation Log', + mediaProxyCache: 'MediaProxy Cache', 'emoji-packs': 'Emoji packs' }, navbar: { @@ -89,6 +92,19 @@ export default { pleromaFELoginFailed: 'Failed to login via PleromaFE, please login with username/password', pleromaFELoginSucceed: 'Logged in via PleromaFE' }, + mediaProxyCache: { + mediaProxyCache: 'MediaProxy Cache', + ban: 'Ban', + url: 'URL', + evict: 'Evict', + evictedMessage: 'This URL was evicted', + actions: 'Actions', + remove: 'Remove from Cachex', + evictObjectsHeader: 'Evict object from the MediaProxy cache', + listBannedUrlsHeader: 'List of all banned MediaProxy URLs', + multipleInput: 'You can enter a single URL or several comma separated links', + removeSelected: 'Remove Selected' + }, documentation: { documentation: 'Documentation', github: 'Github Repository' diff --git a/src/router/index.js b/src/router/index.js index 40d4d7cd..b61e7b44 100644 --- a/src/router/index.js +++ b/src/router/index.js @@ -16,7 +16,7 @@ const settings = { path: 'index', component: () => import('@/views/settings/index'), name: 'Settings', - meta: { title: 'Settings', icon: 'settings', noCache: true } + meta: { title: 'settings', icon: 'settings', noCache: true } } ] } @@ -30,7 +30,7 @@ const statuses = { path: 'index', component: () => import('@/views/statuses/index'), name: 'Statuses', - meta: { title: 'Statuses', icon: 'form', noCache: true } + meta: { title: 'statuses', icon: 'form', noCache: true } } ] } @@ -44,7 +44,7 @@ const reports = { path: 'index', component: () => import('@/views/reports/index'), name: 'Reports', - meta: { title: 'Reports', icon: 'documentation', noCache: true } + meta: { title: 'reports', icon: 'documentation', noCache: true } } ] } @@ -58,7 +58,7 @@ const invites = { path: 'index', component: () => import('@/views/invites/index'), name: 'Invites', - meta: { title: 'Invites', icon: 'guide', noCache: true } + meta: { title: 'invites', icon: 'guide', noCache: true } } ] } @@ -72,7 +72,7 @@ const emojiPacks = { path: 'index', component: () => import('@/views/emojiPacks/index'), name: 'Emoji Packs', - meta: { title: 'Emoji Packs', icon: 'eye-open', noCache: true } + meta: { title: 'emoji-packs', icon: 'eye-open', noCache: true } } ] } @@ -91,6 +91,20 @@ const moderationLog = { ] } +const mediaProxyCacheDisabled = disabledFeatures.includes('media-proxy-cache') +const mediaProxyCache = { + path: '/media_proxy_cache', + component: Layout, + children: [ + { + path: 'index', + component: () => import('@/views/mediaProxyCache/index'), + name: 'MediaProxy Cache', + meta: { title: 'mediaProxyCache', icon: 'example', noCache: true } + } + ] +} + export const constantRouterMap = [ { path: '/redirect', @@ -159,6 +173,7 @@ export const asyncRouterMap = [ ...(invitesDisabled ? [] : [invites]), ...(emojiPacksDisabled ? [] : [emojiPacks]), ...(moderationLogDisabled ? [] : [moderationLog]), + ...(mediaProxyCacheDisabled ? [] : [mediaProxyCache]), ...(settingsDisabled ? [] : [settings]), { path: '/users/:id', diff --git a/src/store/index.js b/src/store/index.js index e2fcd651..bd4a6e5b 100644 --- a/src/store/index.js +++ b/src/store/index.js @@ -5,6 +5,7 @@ import emojiPacks from './modules/emojiPacks' import errorLog from './modules/errorLog' import getters from './getters' import invites from './modules/invites' +import mediaProxyCache from './modules/mediaProxyCache' import moderationLog from './modules/moderationLog' import peers from './modules/peers' import permission from './modules/permission' @@ -24,8 +25,9 @@ const store = new Vuex.Store({ app, errorLog, emojiPacks, - moderationLog, invites, + mediaProxyCache, + moderationLog, peers, permission, relays, diff --git a/src/store/modules/mediaProxyCache.js b/src/store/modules/mediaProxyCache.js new file mode 100644 index 00000000..815c14e4 --- /dev/null +++ b/src/store/modules/mediaProxyCache.js @@ -0,0 +1,53 @@ +import { listBannedUrls, purgeUrls, removeBannedUrls } from '@/api/mediaProxyCache' +import { Message } from 'element-ui' +import i18n from '@/lang' + +const mediaProxyCache = { + state: { + bannedUrls: [], + bannedUrlsCount: 0, + currentPage: 1, + loading: false + }, + mutations: { + SET_BANNED_URLS: (state, urls) => { + state.bannedUrls = urls.map(el => { return { url: el } }) + }, + SET_BANNED_URLS_COUNT: (state, count) => { + state.bannedUrlsCount = count + }, + SET_LOADING: (state, status) => { + state.loading = status + }, + SET_PAGE: (state, page) => { + state.currentPage = page + } + }, + actions: { + async ListBannedUrls({ commit, getters }, page) { + commit('SET_LOADING', true) + const response = await listBannedUrls(page, getters.authHost, getters.token) + commit('SET_BANNED_URLS', response.data.urls) + // commit('SET_BANNED_URLS_COUNT', count) + commit('SET_PAGE', page) + commit('SET_LOADING', false) + }, + async PurgeUrls({ dispatch, getters, state }, { urls, ban }) { + await purgeUrls(urls, ban, getters.authHost, getters.token) + Message({ + message: i18n.t('mediaProxyCache.evictedMessage'), + type: 'success', + duration: 5 * 1000 + }) + if (ban) { + dispatch('ListBannedUrls', state.currentPage) + } + }, + async RemoveBannedUrls({ dispatch, getters, state }, urls) { + await removeBannedUrls(urls, getters.authHost, getters.token) + dispatch('ListBannedUrls', state.currentPage) + } + } +} + +export default mediaProxyCache diff --git a/src/store/modules/normalizers.js b/src/store/modules/normalizers.js index 349cebff..c7fc33e8 100644 --- a/src/store/modules/normalizers.js +++ b/src/store/modules/normalizers.js @@ -71,18 +71,16 @@ export const parseTuples = (tuples, key) => { return [...acc, { [mascot.tuple[0]]: { ...mascot.tuple[1], id: `f${(~~(Math.random() * 1e8)).toString(16)}` }}] }, []) } else if (Array.isArray(item.tuple[1]) && - (item.tuple[0] === ':groups' || item.tuple[0] === ':replace' || item.tuple[0] === ':retries')) { - accum[item.tuple[0]] = item.tuple[1].reduce((acc, group) => { - return [...acc, { [group.tuple[0]]: { value: group.tuple[1], id: `f${(~~(Math.random() * 1e8)).toString(16)}` }}] - }, []) - } else if (item.tuple[0] === ':crontab') { - accum[item.tuple[0]] = item.tuple[1].reduce((acc, group) => { - return { ...acc, [group.tuple[1]]: group.tuple[0] } - }, {}) - } else if (item.tuple[0] === ':match_actor') { - accum[item.tuple[0]] = Object.keys(item.tuple[1]).reduce((acc, regex) => { - return [...acc, { [regex]: { value: item.tuple[1][regex], id: `f${(~~(Math.random() * 1e8)).toString(16)}` }}] - }, []) + (item.tuple[0] === ':groups' || item.tuple[0] === ':replace' || item.tuple[0] === ':retries' || item.tuple[0] === ':headers' || item.tuple[0] === ':crontab')) { + if (item.tuple[0] === ':crontab') { + accum[item.tuple[0]] = item.tuple[1].reduce((acc, group) => { + return [...acc, { [group.tuple[1]]: { value: group.tuple[0], id: `f${(~~(Math.random() * 1e8)).toString(16)}` }}] + }, []) + } else { + accum[item.tuple[0]] = item.tuple[1].reduce((acc, group) => { + return [...acc, { [group.tuple[0]]: { value: group.tuple[1], id: `f${(~~(Math.random() * 1e8)).toString(16)}` }}] + }, []) + } } else if (item.tuple[0] === ':icons') { accum[item.tuple[0]] = item.tuple[1].map(icon => { return Object.keys(icon).map(name => { @@ -103,7 +101,13 @@ export const parseTuples = (tuples, key) => { } else if (item.tuple[0] === ':ip') { accum[item.tuple[0]] = item.tuple[1].tuple.join('.') } else if (item.tuple[1] && typeof item.tuple[1] === 'object') { - accum[item.tuple[0]] = parseObject(item.tuple[1]) + if (item.tuple[0] === ':params' || item.tuple[0] === ':match_actor') { + accum[item.tuple[0]] = Object.keys(item.tuple[1]).reduce((acc, key) => { + return [...acc, { [key]: { value: item.tuple[1][key], id: `f${(~~(Math.random() * 1e8)).toString(16)}` }}] + }, []) + } else { + accum[item.tuple[0]] = parseObject(item.tuple[1]) + } } else { accum[item.tuple[0]] = item.tuple[1] } @@ -214,11 +218,11 @@ export const wrapUpdatedSettings = (group, settings, currentState) => { const wrapValues = (settings, currentState) => { return Object.keys(settings).map(setting => { const [type, value] = settings[setting] - if ( - type === 'keyword' || - type.includes('keyword') || - type.includes('tuple') && type.includes('list') || - setting === ':replace' + if (type === 'keyword' || + (Array.isArray(type) && ( + type.includes('keyword') || + (type.includes('tuple') && type.includes('list')) + )) ) { return { 'tuple': [setting, wrapValues(value, currentState)] } } else if (prependWithСolon(type, value)) { @@ -231,15 +235,16 @@ const wrapValues = (settings, currentState) => { return { 'tuple': [value, setting] } } else if (type === 'map') { const mapValue = Object.keys(value).reduce((acc, key) => { - acc[key] = setting === ':match_actor' ? value[key] : value[key][1] + acc[key] = value[key][1] return acc }, {}) - const mapCurrentState = setting === ':match_actor' - ? currentState[setting].reduce((acc, element) => { - return { ...acc, ...{ [Object.keys(element)[0]]: Object.values(element)[0].value }} - }, {}) - : currentState[setting] - return { 'tuple': [setting, { ...mapCurrentState, ...mapValue }] } + return { 'tuple': [setting, { ...currentState[setting], ...mapValue }] } + } else if (type.includes('map')) { + const mapValue = Object.keys(value).reduce((acc, key) => { + acc[key] = value[key][1] + return acc + }, {}) + return { 'tuple': [setting, mapValue] } } else if (setting === ':ip') { const ip = value.split('.').map(s => parseInt(s, 10)) return { 'tuple': [setting, { 'tuple': ip }] } diff --git a/src/views/mediaProxyCache/index.vue b/src/views/mediaProxyCache/index.vue new file mode 100644 index 00000000..6c37082b --- /dev/null +++ b/src/views/mediaProxyCache/index.vue @@ -0,0 +1,157 @@ + + + + + diff --git a/src/views/settings/components/Inputs.vue b/src/views/settings/components/Inputs.vue index 8bbd8121..0639994e 100644 --- a/src/views/settings/components/Inputs.vue +++ b/src/views/settings/components/Inputs.vue @@ -95,15 +95,14 @@ - - + @@ -129,7 +128,6 @@ - - diff --git a/src/views/settings/components/inputComponents/EditableKeywordInput.vue b/src/views/settings/components/inputComponents/EditableKeywordInput.vue index 009b2033..727ff0da 100644 --- a/src/views/settings/components/inputComponents/EditableKeywordInput.vue +++ b/src/views/settings/components/inputComponents/EditableKeywordInput.vue @@ -1,12 +1,13 @@