mirror of
https://github.com/jwinterm/modoboa.git
synced 2026-10-25 05:58:42 +01:00
WIP: sieve filters
This commit is contained in:
@@ -50,4 +50,25 @@ export default {
|
||||
setARMessage(data) {
|
||||
return repository.put(`${resource}/armessage/`, data)
|
||||
},
|
||||
getFilterSets() {
|
||||
return repository.get(`${resource}/filtersets/`)
|
||||
},
|
||||
createFilterSet(data) {
|
||||
return repository.post(`${resource}/filtersets/`, data)
|
||||
},
|
||||
getFilterConditionTemplates() {
|
||||
return repository.get(`${resource}/filtersets/condition_templates/`)
|
||||
},
|
||||
getFilterActionTemplates() {
|
||||
return repository.get(`${resource}/filtersets/action_templates/`)
|
||||
},
|
||||
getFilters(filterSetName) {
|
||||
return repository.get(`${resource}/filtersets/${filterSetName}/filters/`)
|
||||
},
|
||||
createFilter(filterSetName, data) {
|
||||
return repository.post(
|
||||
`${resource}/filtersets/${filterSetName}/filters/`,
|
||||
data
|
||||
)
|
||||
},
|
||||
}
|
||||
|
||||
@@ -0,0 +1,275 @@
|
||||
<template>
|
||||
<v-card
|
||||
:title="title"
|
||||
>
|
||||
<v-card-text>
|
||||
<v-form ref="formRef">
|
||||
<v-text-field
|
||||
v-model="form.name"
|
||||
:label="$gettext('Name')"
|
||||
variant="outlined"
|
||||
density="compact"
|
||||
:rules="[rules.required]"
|
||||
/>
|
||||
<div>
|
||||
<h4>{{ $gettext('Conditions') }}</h4>
|
||||
<v-radio-group
|
||||
v-model="form.match_type"
|
||||
color="primary"
|
||||
inline
|
||||
>
|
||||
<v-radio
|
||||
:label="$gettext('All of the following')"
|
||||
value="allof"
|
||||
/>
|
||||
<v-radio
|
||||
:label="$gettext('Any of the following')"
|
||||
value="anyof"
|
||||
/>
|
||||
<v-radio
|
||||
:label="$gettext('All messages')"
|
||||
value="all"
|
||||
/>
|
||||
</v-radio-group>
|
||||
<div v-for="(condition, index) in form.conditions" class="d-flex align-center">
|
||||
<v-select
|
||||
v-model="condition.name"
|
||||
:label="$gettext('Choose a header')"
|
||||
:items="conditionTemplates"
|
||||
item-title="label"
|
||||
item-value="name"
|
||||
variant="outlined"
|
||||
density="compact"
|
||||
:rules="[rules.required]"
|
||||
/>
|
||||
<v-select
|
||||
v-model="condition.operator"
|
||||
:label="$gettext('Choose an operator')"
|
||||
:items="getConditionOperators(condition.name)"
|
||||
item-title="label"
|
||||
item-value="name"
|
||||
variant="outlined"
|
||||
class="ml-2"
|
||||
density="compact"
|
||||
:rules="[rules.required]"
|
||||
/>
|
||||
<v-text-field
|
||||
v-model="condition.value"
|
||||
:label="$gettext('Value')"
|
||||
variant="outlined"
|
||||
class="ml-2"
|
||||
density="compact"
|
||||
:type="getConditionType(index)"
|
||||
:rules="[rules.required]"
|
||||
/>
|
||||
<v-btn
|
||||
v-if="index === 0"
|
||||
icon="mdi-plus"
|
||||
variant="text"
|
||||
:title="$gettext('Add condition')"
|
||||
color="primary"
|
||||
@click="addCondition"
|
||||
/>
|
||||
<v-btn
|
||||
v-else
|
||||
icon="mdi-trash-can"
|
||||
variant="text"
|
||||
:title="$gettext('Remove condition')"
|
||||
color="error"
|
||||
@click="removeCondition(index)"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<h4>{{ $gettext('Actions') }}</h4>
|
||||
<div v-for="(action, index) in form.actions" class="d-flex align-start mt-2">
|
||||
<v-select
|
||||
v-model="action.name"
|
||||
:label="$gettext('Choose an action')"
|
||||
:items="actionTemplates"
|
||||
item-title="label"
|
||||
item-value="name"
|
||||
variant="outlined"
|
||||
density="compact"
|
||||
class="flex-grow-0"
|
||||
/>
|
||||
<div class="mx-2 d-flex flex-column flex-grow-1">
|
||||
<template v-for="arg in getActionArguments(action.name)">
|
||||
<v-text-field
|
||||
v-if="arg.type === 'string' || arg.type === 'list'"
|
||||
v-model="action.args[arg.name]"
|
||||
density="compact"
|
||||
variant="outlined"
|
||||
:rules="[rules.required]"
|
||||
/>
|
||||
<v-checkbox
|
||||
v-if="arg.type === 'boolean'"
|
||||
v-model="action.args[arg.name]"
|
||||
:label="arg.label"
|
||||
/>
|
||||
</template>
|
||||
</div>
|
||||
<v-btn
|
||||
v-if="index === 0"
|
||||
icon="mdi-plus"
|
||||
variant="text"
|
||||
:title="$gettext('Add action')"
|
||||
color="primary"
|
||||
@click="addAction"
|
||||
/>
|
||||
<v-btn
|
||||
v-else
|
||||
icon="mdi-trash-can"
|
||||
variant="text"
|
||||
:title="$gettext('Remove action')"
|
||||
color="error"
|
||||
@click="removeAction(index)"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</v-form>
|
||||
</v-card-text>
|
||||
<v-divider></v-divider>
|
||||
<v-card-actions>
|
||||
<v-spacer></v-spacer>
|
||||
<v-btn
|
||||
:text="$gettext('Cancel')"
|
||||
variant="plain"
|
||||
@click="close"
|
||||
></v-btn>
|
||||
<v-btn
|
||||
color="primary"
|
||||
:text="submitLabel"
|
||||
@click="submit"
|
||||
></v-btn>
|
||||
</v-card-actions>
|
||||
</v-card>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { computed, ref, watch } from 'vue'
|
||||
import { useGettext } from 'vue3-gettext'
|
||||
import accountApi from '@/api/account'
|
||||
import rules from '@/plugins/rules'
|
||||
|
||||
const props = defineProps({
|
||||
filterSet: {
|
||||
type: String,
|
||||
default: null
|
||||
},
|
||||
filter: {
|
||||
type: Object,
|
||||
required: false,
|
||||
default: null
|
||||
}
|
||||
})
|
||||
const emit = defineEmits(['close'])
|
||||
const { $gettext } = useGettext()
|
||||
|
||||
const conditionTemplates = ref([])
|
||||
const actionTemplates = ref([])
|
||||
const formRef = ref()
|
||||
const form = ref(getInitialFormContent())
|
||||
|
||||
const title = computed(() => {
|
||||
return (props.filter) ? $gettext('Edit filter') : $gettext('New filter')
|
||||
})
|
||||
const submitLabel = computed(() => {
|
||||
return (props.filter) ? $gettext('Update') : $gettext('Create')
|
||||
})
|
||||
|
||||
watch(() => props.filter, (value) => {
|
||||
if (value) {
|
||||
form.value = JSON.parse(JSON.stringify(props.filter))
|
||||
} else {
|
||||
form.value = getInitialFormContent()
|
||||
}
|
||||
}, { immediate: true })
|
||||
|
||||
function close() {
|
||||
emit('close')
|
||||
form.value = getInitialFormContent()
|
||||
}
|
||||
|
||||
function getInitialFormContent() {
|
||||
return {
|
||||
match_type: 'anyof',
|
||||
conditions: [{}],
|
||||
actions: [{
|
||||
args: {}
|
||||
}]
|
||||
}
|
||||
}
|
||||
|
||||
function getConditionOperators(condition) {
|
||||
for (const template of conditionTemplates.value) {
|
||||
if (template.name === condition) {
|
||||
return template.operators
|
||||
}
|
||||
}
|
||||
return []
|
||||
}
|
||||
|
||||
function getConditionType(index) {
|
||||
for (const template of conditionTemplates.value) {
|
||||
if (template.name === form.value.conditions[index].name) {
|
||||
for (const operator of template.operators) {
|
||||
if (operator.name === form.value.conditions[index].operator) {
|
||||
return (operator.type === 'string') ? 'text' : 'number'
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function getActionArguments(action) {
|
||||
for (const template of actionTemplates.value) {
|
||||
if (template.name === action) {
|
||||
return template.args
|
||||
}
|
||||
}
|
||||
return []
|
||||
}
|
||||
|
||||
function addCondition() {
|
||||
form.value.conditions.push({
|
||||
name: conditionTemplates.value[0].name,
|
||||
operator: conditionTemplates.value[0].operators[0]
|
||||
})
|
||||
}
|
||||
|
||||
function removeCondition(index) {
|
||||
form.value.conditions.splice(index, 1)
|
||||
}
|
||||
|
||||
function addAction() {
|
||||
form.value.actions.push({
|
||||
name: actionTemplates.value[0].name,
|
||||
args: {}
|
||||
})
|
||||
}
|
||||
|
||||
function removeAction(index) {
|
||||
form.value.actions.splice(index, 1)
|
||||
}
|
||||
|
||||
async function submit() {
|
||||
const { valid } = await formRef.value.validate()
|
||||
if (!valid) {
|
||||
return
|
||||
}
|
||||
accountApi.createFilter(props.filterSet, form.value).then(() => {
|
||||
|
||||
})
|
||||
}
|
||||
|
||||
accountApi.getFilterConditionTemplates().then(resp => {
|
||||
conditionTemplates.value = resp.data
|
||||
form.value.conditions[0].name = conditionTemplates.value[0].name
|
||||
form.value.conditions[0].operator = conditionTemplates.value[0].operators[0]
|
||||
})
|
||||
accountApi.getFilterActionTemplates().then(resp => {
|
||||
actionTemplates.value = resp.data
|
||||
form.value.actions[0].name = actionTemplates.value[0].name
|
||||
})
|
||||
</script>
|
||||
@@ -0,0 +1,62 @@
|
||||
<template>
|
||||
<v-card
|
||||
:title="$gettext('Create a new filter set')"
|
||||
>
|
||||
<v-card-text>
|
||||
<v-form ref="formRef">
|
||||
<v-text-field
|
||||
v-model="form.name"
|
||||
:label="$gettext('Name')"
|
||||
variant="outlined"
|
||||
:rules="[rules.required]"
|
||||
/>
|
||||
<v-switch
|
||||
:label="$gettext('Active')"
|
||||
v-model="form.active"
|
||||
color="primary"
|
||||
/>
|
||||
</v-form>
|
||||
</v-card-text>
|
||||
<v-divider></v-divider>
|
||||
<v-card-actions>
|
||||
<v-spacer></v-spacer>
|
||||
<v-btn
|
||||
:text="$gettext('Cancel')"
|
||||
variant="plain"
|
||||
@click="close"
|
||||
></v-btn>
|
||||
<v-btn
|
||||
color="primary"
|
||||
:text="$gettext('Create')"
|
||||
@click="submit"
|
||||
></v-btn>
|
||||
</v-card-actions>
|
||||
</v-card>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref } from 'vue'
|
||||
import rules from '@/plugins/rules'
|
||||
import accountApi from '@/api/account'
|
||||
|
||||
const emit = defineEmits(['close'])
|
||||
|
||||
const form = ref({})
|
||||
const formRef = ref()
|
||||
|
||||
function close() {
|
||||
formRef.value.reset()
|
||||
form.value = {}
|
||||
emit('close')
|
||||
}
|
||||
|
||||
async function submit() {
|
||||
const { valid } = await formRef.value.validate()
|
||||
if (!valid) {
|
||||
return
|
||||
}
|
||||
accountApi.createFilterSet(form.value).then(() => {
|
||||
close()
|
||||
})
|
||||
}
|
||||
</script>
|
||||
@@ -257,6 +257,11 @@ const mainMenuItems = [
|
||||
]
|
||||
|
||||
const userSettingsMenuItems = [
|
||||
{
|
||||
text: $gettext('Filters'),
|
||||
to: { name: 'AccountFilters' },
|
||||
icon: 'mdi-filter'
|
||||
},
|
||||
{
|
||||
text: $gettext('Settings'),
|
||||
to: { name: 'AccountSettings' },
|
||||
|
||||
@@ -256,6 +256,15 @@ const routes = [
|
||||
requiresAuth: true,
|
||||
},
|
||||
children: [
|
||||
{
|
||||
path: 'filters',
|
||||
name: 'AccountFilters',
|
||||
component: () => import('@/views/account/FiltersView.vue'),
|
||||
meta: {
|
||||
layout: 'account',
|
||||
requiresAuth: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
path: ':tab?',
|
||||
name: 'AccountSettings',
|
||||
@@ -283,18 +292,15 @@ router.beforeEach((to, from, next) => {
|
||||
if (to.meta.requiresAuth !== undefined) {
|
||||
const authStore = useAuthStore()
|
||||
authStore.initialize().then(() => {
|
||||
if (!authStore.isAuthenticated) {
|
||||
next({ name: 'Login' })
|
||||
} else {
|
||||
/*
|
||||
authStore.validateAccess()
|
||||
/*
|
||||
if (to.meta.allowedRoles !== undefined) {
|
||||
if (to.meta.allowedRoles.indexOf(authStore.authUser.role) === -1) {
|
||||
next({ name: 'Dashboard' })
|
||||
return
|
||||
}
|
||||
if (to.meta.allowedRoles.indexOf(authStore.authUser.role) === -1) {
|
||||
next({ name: 'Dashboard' })
|
||||
return
|
||||
}
|
||||
}*/
|
||||
next()
|
||||
}
|
||||
next()
|
||||
})
|
||||
} else {
|
||||
next()
|
||||
|
||||
@@ -3,15 +3,28 @@ import Cookies from 'js-cookie'
|
||||
import { defineStore } from 'pinia'
|
||||
import { computed, ref } from 'vue'
|
||||
import gettext from '@/plugins/gettext'
|
||||
import { UserManager } from 'oidc-client-ts'
|
||||
|
||||
import repository from '@/api/repository'
|
||||
import accountApi from '@/api/account'
|
||||
import accountsApi from '@/api/accounts'
|
||||
import authApi from '@/api/auth'
|
||||
// import authApi from '@/api/auth'
|
||||
|
||||
export const useAuthStore = defineStore('auth', () => {
|
||||
const authUser = ref({})
|
||||
const isAuthenticated = ref(false)
|
||||
const manager = new UserManager({
|
||||
authority: '/api/o',
|
||||
client_id: 'LVQbfIIX3khWR3nDvix1u9yEGHZUxcx53bhJ7FlD',
|
||||
// Is client_secret necessary?
|
||||
client_secret:
|
||||
'fXzG7tq23eXrM7RK41pXHRseuRWnxe4gV0p4A4YXJlF9jva7QfsDft1g7ESYU9AXR35QE0lo2Ti696OXsqb9JHLjtrPxaYmgMSRmRwLCce5hVr3FB2QhfSOThFYhcuuR',
|
||||
metadata: {
|
||||
authorization_endpoint: '/api/o/authorize/',
|
||||
token_endpoint: '/api/o/token/',
|
||||
},
|
||||
scope: 'read write',
|
||||
})
|
||||
|
||||
const userHasMailbox = computed(() => {
|
||||
return authUser.value.mailbox !== null
|
||||
@@ -26,31 +39,35 @@ export const useAuthStore = defineStore('auth', () => {
|
||||
}
|
||||
|
||||
async function initialize() {
|
||||
const token = Cookies.get('token')
|
||||
if (!token) {
|
||||
if (isAuthenticated.value) {
|
||||
return
|
||||
}
|
||||
repository.defaults.headers.common.Authorization = `Bearer ${token}`
|
||||
const user = await manager.getUser()
|
||||
if (!user) {
|
||||
return
|
||||
}
|
||||
repository.defaults.headers.common.Authorization = `Bearer ${user.access_token}`
|
||||
repository.defaults.headers.post['Content-Type'] = 'application/json'
|
||||
return fetchUser()
|
||||
}
|
||||
|
||||
async function login(payload) {
|
||||
const resp = await authApi.requestToken(payload)
|
||||
const cookiesAttributes = { sameSite: 'strict' }
|
||||
if (payload.rememberMe) {
|
||||
cookiesAttributes.expires = 90
|
||||
async function validateAccess() {
|
||||
const user = await manager.getUser()
|
||||
if (!user || user.expired) {
|
||||
manager.signinRedirect()
|
||||
}
|
||||
}
|
||||
|
||||
const cookie = Cookies.withAttributes(cookiesAttributes)
|
||||
cookie.set('token', resp.data.access)
|
||||
cookie.set('refreshToken', resp.data.refresh)
|
||||
async function login(payload) {
|
||||
await manager.signinResourceOwnerCredentials(payload)
|
||||
return initialize()
|
||||
// const cookiesAttributes = { sameSite: 'strict' }
|
||||
// if (payload.rememberMe) {
|
||||
// cookiesAttributes.expires = 90
|
||||
// }
|
||||
}
|
||||
async function $reset() {
|
||||
delete repository.defaults.headers.common.Authorization
|
||||
Cookies.remove('token')
|
||||
Cookies.remove('refreshToken')
|
||||
authUser.value = {}
|
||||
isAuthenticated.value = false
|
||||
}
|
||||
@@ -85,12 +102,13 @@ export const useAuthStore = defineStore('auth', () => {
|
||||
return {
|
||||
authUser,
|
||||
isAuthenticated,
|
||||
userHasMailbox,
|
||||
validateAccess,
|
||||
fetchUser,
|
||||
initialize,
|
||||
login,
|
||||
$reset,
|
||||
updateAccount,
|
||||
finalizeTFASetup,
|
||||
userHasMailbox,
|
||||
}
|
||||
})
|
||||
|
||||
@@ -0,0 +1,165 @@
|
||||
<template>
|
||||
<v-toolbar flat>
|
||||
<v-toolbar-title>{{ $gettext('Message filters') }}</v-toolbar-title>
|
||||
</v-toolbar>
|
||||
|
||||
<div class="d-flex bg-white align-center pa-4">
|
||||
<v-autocomplete
|
||||
v-model="currentFilterSet"
|
||||
:label="$gettext('Select a filter set')"
|
||||
:items="filterSets"
|
||||
item-title="name"
|
||||
return-object
|
||||
variant="outlined"
|
||||
hide-details
|
||||
class="flex-grow-0"
|
||||
style="width: 200px"
|
||||
/>
|
||||
<v-btn
|
||||
color="primary"
|
||||
icon="mdi-plus"
|
||||
size="small"
|
||||
class="ml-4"
|
||||
@click="showCreationForm = true"
|
||||
:title="$gettext('Add filter set')"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-if="currentFilterSet"
|
||||
class="bg-white mt-4 pa-2"
|
||||
>
|
||||
<div class="d-flex align-center">
|
||||
<v-spacer />
|
||||
<v-btn
|
||||
color="primary"
|
||||
:title="$gettext('Add new filter')"
|
||||
@click="showFilterForm = true"
|
||||
>
|
||||
<v-icon icon="mdi-plus" />
|
||||
{{ $gettext('Add') }}
|
||||
</v-btn>
|
||||
</div>
|
||||
<v-data-table
|
||||
:headers="headers"
|
||||
:items="filters"
|
||||
>
|
||||
<template #[`item.enabled`]="{ item }">
|
||||
{{ $yesno(item.enabled) }}
|
||||
</template>
|
||||
<template #[`item.filter_actions`]="{ item }">
|
||||
<v-menu offset-y>
|
||||
<template #activator="{ props }">
|
||||
<v-btn
|
||||
icon="mdi-dots-horizontal"
|
||||
variant="text"
|
||||
v-bind="props"
|
||||
>
|
||||
</v-btn>
|
||||
</template>
|
||||
<MenuItems :items="filterActions" :obj="item" />
|
||||
</v-menu>
|
||||
</template>
|
||||
</v-data-table>
|
||||
</div>
|
||||
|
||||
<v-dialog
|
||||
v-model="showFilterSetForm"
|
||||
max-width="600"
|
||||
>
|
||||
<FilterSetForm
|
||||
@close="closeFilterSetForm"
|
||||
/>
|
||||
</v-dialog>
|
||||
|
||||
<v-dialog
|
||||
v-model="showFilterForm"
|
||||
max-width="800"
|
||||
>
|
||||
<FilterForm
|
||||
v-if="currentFilterSet"
|
||||
:filter-set="currentFilterSet.name"
|
||||
:filter="selectedFilter"
|
||||
@close="closeFilterForm"
|
||||
/>
|
||||
</v-dialog>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, watch } from 'vue'
|
||||
import { useGettext } from 'vue3-gettext'
|
||||
import accountApi from '@/api/account'
|
||||
import FilterSetForm from '@/components/account/FilterSetForm.vue'
|
||||
import FilterForm from '@/components/account/FilterForm.vue'
|
||||
import MenuItems from '@/components/tools/MenuItems.vue'
|
||||
|
||||
const { $gettext } = useGettext()
|
||||
|
||||
const filterSets = ref([])
|
||||
const filters = ref([])
|
||||
const currentFilterSet = ref()
|
||||
const selectedFilter = ref(null)
|
||||
const showFilterSetForm = ref(false)
|
||||
const showFilterForm = ref(false)
|
||||
|
||||
const headers = [
|
||||
{ title: $gettext('Name'), key: 'name' },
|
||||
{ title: $gettext('Active'), key: 'enabled' },
|
||||
{ title: '', key: 'filter_actions', align: 'end' }
|
||||
]
|
||||
|
||||
watch(currentFilterSet, (value) => {
|
||||
accountApi.getFilters(value.name).then(resp => {
|
||||
filters.value = resp.data
|
||||
})
|
||||
})
|
||||
|
||||
function closeFilterSetForm() {
|
||||
showFilterSetForm.value = false
|
||||
fetchFilterSets()
|
||||
}
|
||||
|
||||
function closeFilterForm() {
|
||||
showFilterForm.value = false
|
||||
selectedFilter.value = null
|
||||
}
|
||||
|
||||
function fetchFilterSets() {
|
||||
accountApi.getFilterSets().then(resp => {
|
||||
filterSets.value = resp.data
|
||||
})
|
||||
}
|
||||
|
||||
function editFilter(filter) {
|
||||
selectedFilter.value = filter
|
||||
showFilterForm.value = true
|
||||
}
|
||||
|
||||
function deleteFilter(filter) {
|
||||
}
|
||||
|
||||
const filterActions = [
|
||||
{
|
||||
label: $gettext('Edit'),
|
||||
icon: 'mdi-circle-edit-outline',
|
||||
onClick: editFilter
|
||||
},
|
||||
{
|
||||
label: $gettext('Delete'),
|
||||
icon: 'mdi-delete-outline',
|
||||
onClick: deleteFilter,
|
||||
color: 'red'
|
||||
}
|
||||
]
|
||||
|
||||
fetchFilterSets()
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.v-toolbar {
|
||||
background-color: #f7f8fa !important;
|
||||
}
|
||||
.v-tabs {
|
||||
background-color: #f7f8fa !important;
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user