This commit is contained in:
Кирилл Хрычиков 2025-06-12 00:47:38 +03:00
parent 986ab4d463
commit 415d35f6e2
123 changed files with 28022 additions and 866 deletions

71
auth.go Normal file
View File

@ -0,0 +1,71 @@
package main
import (
"crypto/rand"
"myproject/tools"
"net/http"
"strings"
"github.com/gorilla/sessions"
"github.com/labstack/echo/v4"
)
func NewAuthStore() *sessions.CookieStore {
auth := make([]byte, 32)
_, _ = rand.Read(auth)
enc := make([]byte, 16)
_, _ = rand.Read(enc)
s := sessions.NewCookieStore(auth, enc)
s.Options.Secure = false
s.Options.SameSite = http.SameSiteDefaultMode
s.MaxAge(3600)
return s
}
func setAuth(onlyAdmin bool, g *echo.Group) *echo.Group {
g.Use(
func(next echo.HandlerFunc) echo.HandlerFunc {
return func(c echo.Context) error {
sess, err := c.Get("authStore").(*sessions.CookieStore).New(c.Request(), tools.SessionName)
if err != nil {
// journal.Debug(ctx, commerr.Trace(err).Error())
}
userName := sess.Values[tools.UserNameSessionKey]
if userName == nil {
return echo.ErrUnauthorized
}
email, lp, domain := SplitEmail(userName.(string))
c.Set(tools.UserCtxKey, email)
c.Set(tools.LpCtxKey, lp)
c.Set(tools.DomainCtxKey, domain)
return next(c)
}
},
func(next echo.HandlerFunc) echo.HandlerFunc {
return func(c echo.Context) error {
if onlyAdmin && tools.GetUser(c) != "admin" {
return echo.ErrUnauthorized
}
return next(c)
}
},
)
return g
}
func SplitEmail(toSplit string) (email, user, domain string) {
email = strings.TrimSpace(toSplit)
email = strings.ToLower(email)
parts := strings.Split(email, "@")
user = parts[0]
if len(parts) > 1 {
domain = parts[1]
}
return
}

98
echo.go Normal file
View File

@ -0,0 +1,98 @@
package main
import (
"context"
"fmt"
"myproject/tools"
"net"
"net/http"
"strings"
"time"
"github.com/labstack/echo/v4"
"github.com/labstack/echo/v4/middleware"
)
func NewEcho(ctx context.Context, restartMode *bool) *echo.Echo {
timeout := 5 * time.Minute
e := echo.New()
e.HideBanner = true
e.Server.ReadTimeout = timeout
e.Server.WriteTimeout = timeout
e.Server.BaseContext = func(listener net.Listener) context.Context {
return ctx
}
e.HTTPErrorHandler = func(err error, c echo.Context) {
httpError, ok := err.(*echo.HTTPError)
if ok {
errorCode := httpError.Code
switch errorCode {
case http.StatusServiceUnavailable:
tools.Serve503(c)
case http.StatusTooManyRequests:
tools.Serve429(c)
case http.StatusForbidden:
tools.Serve403(c)
case http.StatusUnauthorized:
tools.Serve401(c)
case http.StatusNotFound, http.StatusMethodNotAllowed:
if strings.HasPrefix(c.Request().RequestURI, "/backend") {
switch errorCode {
case http.StatusNotFound:
tools.Serve404(c)
case http.StatusMethodNotAllowed:
tools.Serve405(c)
}
return
}
c.Redirect(http.StatusTemporaryRedirect, fmt.Sprintf("/?redirect=%s", c.Request().RequestURI))
default:
tools.Serve500(c)
}
}
}
authStore := NewAuthStore()
e.Use(
middleware.CORSWithConfig(middleware.CORSConfig{
AllowOrigins: []string{"http://localhost:7777", "http://localhost:8808"},
AllowHeaders: []string{
echo.HeaderOrigin,
echo.HeaderContentType,
echo.HeaderAccept,
echo.HeaderAccessControlAllowOrigin,
echo.HeaderAccessControlAllowCredentials,
echo.HeaderAccessControlAllowHeaders,
echo.HeaderAccessControlRequestHeaders,
echo.HeaderAuthorization,
},
AllowCredentials: true,
}),
middleware.GzipWithConfig(middleware.GzipConfig{
Level: 6,
}),
func(next echo.HandlerFunc) echo.HandlerFunc {
return func(c echo.Context) error {
c.Set("authStore", authStore)
return next(c)
}
},
func(next echo.HandlerFunc) echo.HandlerFunc {
return func(c echo.Context) error {
return next(c)
}
},
func(next echo.HandlerFunc) echo.HandlerFunc {
return func(c echo.Context) error {
c.Set(tools.RootCtxKey, ctx)
return next(c)
}
},
)
return e
}

View File

@ -0,0 +1 @@
VITE_API_URL=http://localhost:8888

1
frontend/.env.production Normal file
View File

@ -0,0 +1 @@
VITE_API_URL=

29
frontend/.gitignore vendored Normal file
View File

@ -0,0 +1,29 @@
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
lerna-debug.log*
node_modules
.DS_Store
dist-ssr
coverage
*.local
/cypress/videos/
/cypress/screenshots/
# Editor directories and files
.vscode/*
!.vscode/extensions.json
.idea
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?
*.tsbuildinfo

View File

@ -1,23 +1,33 @@
# Vue 3 + TypeScript + Vite # js
This template should help get you started developing with Vue 3 and TypeScript in Vite. The template uses Vue This template should help get you started developing with Vue 3 in Vite.
3 `<script setup>` SFCs, check out
the [script setup docs](https://v3.vuejs.org/api/sfc-script-setup.html#sfc-script-setup) to learn more.
## Recommended IDE Setup ## Recommended IDE Setup
- [VS Code](https://code.visualstudio.com/) + [Volar](https://marketplace.visualstudio.com/items?itemName=Vue.volar) [VSCode](https://code.visualstudio.com/) + [Volar](https://marketplace.visualstudio.com/items?itemName=Vue.volar) (and disable Vetur).
## Type Support For `.vue` Imports in TS ## Type Support for `.vue` Imports in TS
Since TypeScript cannot handle type information for `.vue` imports, they are shimmed to be a generic Vue component type TypeScript cannot handle type information for `.vue` imports by default, so we replace the `tsc` CLI with `vue-tsc` for type checking. In editors, we need [Volar](https://marketplace.visualstudio.com/items?itemName=Vue.volar) to make the TypeScript language service aware of `.vue` types.
by default. In most cases this is fine if you don't really care about component prop types outside of templates.
However, if you wish to get actual prop types in `.vue` imports (for example to get props validation when using
manual `h(...)` calls), you can enable Volar's Take Over mode by following these steps:
1. Run `Extensions: Show Built-in Extensions` from VS Code's command palette, look ## Customize configuration
for `TypeScript and JavaScript Language Features`, then right click and select `Disable (Workspace)`. By default,
Take Over mode will enable itself if the default TypeScript extension is disabled.
2. Reload the VS Code window by running `Developer: Reload Window` from the command palette.
You can learn more about Take Over mode [here](https://github.com/johnsoncodehk/volar/discussions/471). See [Vite Configuration Reference](https://vitejs.dev/config/).
## Project Setup
```sh
npm install
```
### Compile and Hot-Reload for Development
```sh
npm run dev
```
### Type-Check, Compile and Minify for Production
```sh
npm run build
```

115
frontend/components.d.ts vendored Normal file
View File

@ -0,0 +1,115 @@
/* eslint-disable */
// @ts-nocheck
// Generated by unplugin-vue-components
// Read more: https://github.com/vuejs/core/pull/3399
export {}
/* prettier-ignore */
declare module 'vue' {
export interface GlobalComponents {
AAlert: typeof import('ant-design-vue/es')['Alert']
ABadge: typeof import('ant-design-vue/es')['Badge']
AButton: typeof import('ant-design-vue/es')['Button']
ACalendar: typeof import('ant-design-vue/es')['Calendar']
ACard: typeof import('ant-design-vue/es')['Card']
ACardGrid: typeof import('ant-design-vue/es')['CardGrid']
ACol: typeof import('ant-design-vue/es')['Col']
ACollapse: typeof import('ant-design-vue/es')['Collapse']
ACollapsePanel: typeof import('ant-design-vue/es')['CollapsePanel']
AConfigProvider: typeof import('ant-design-vue/es')['ConfigProvider']
Actions: typeof import('./src/components/common/rules/Actions.vue')['default']
ADatePicker: typeof import('ant-design-vue/es')['DatePicker']
Add: typeof import('./src/components/admin/domains/Add.vue')['default']
AddressBooks: typeof import('./src/components/common/AddressBooks.vue')['default']
AddressRules: typeof import('./src/components/admin/settings/AddressRules.vue')['default']
ADescriptions: typeof import('ant-design-vue/es')['Descriptions']
ADescriptionsItem: typeof import('ant-design-vue/es')['DescriptionsItem']
ADivider: typeof import('ant-design-vue/es')['Divider']
ADropdown: typeof import('ant-design-vue/es')['Dropdown']
AFlex: typeof import('ant-design-vue/es')['Flex']
AForm: typeof import('ant-design-vue/es')['Form']
AFormItem: typeof import('ant-design-vue/es')['FormItem']
AInput: typeof import('ant-design-vue/es')['Input']
AInputNumber: typeof import('ant-design-vue/es')['InputNumber']
AInputPassword: typeof import('ant-design-vue/es')['InputPassword']
AInputSearch: typeof import('ant-design-vue/es')['InputSearch']
AList: typeof import('ant-design-vue/es')['List']
AListItem: typeof import('ant-design-vue/es')['ListItem']
AMenu: typeof import('ant-design-vue/es')['Menu']
AMenuItem: typeof import('ant-design-vue/es')['MenuItem']
AModal: typeof import('ant-design-vue/es')['Modal']
APagination: typeof import('ant-design-vue/es')['Pagination']
APopconfirm: typeof import('ant-design-vue/es')['Popconfirm']
AProgress: typeof import('ant-design-vue/es')['Progress']
ARadioButton: typeof import('ant-design-vue/es')['RadioButton']
ARadioGroup: typeof import('ant-design-vue/es')['RadioGroup']
ARangePicker: typeof import('ant-design-vue/es')['RangePicker']
AResult: typeof import('ant-design-vue/es')['Result']
ARow: typeof import('ant-design-vue/es')['Row']
ASelect: typeof import('ant-design-vue/es')['Select']
ASelectOption: typeof import('ant-design-vue/es')['SelectOption']
ASpace: typeof import('ant-design-vue/es')['Space']
ASpin: typeof import('ant-design-vue/es')['Spin']
AStatistic: typeof import('ant-design-vue/es')['Statistic']
ASubMenu: typeof import('ant-design-vue/es')['SubMenu']
ASwitch: typeof import('ant-design-vue/es')['Switch']
ATable: typeof import('ant-design-vue/es')['Table']
ATextarea: typeof import('ant-design-vue/es')['Textarea']
ATimePicker: typeof import('ant-design-vue/es')['TimePicker']
ATimeRangePicker: typeof import('ant-design-vue/es')['TimeRangePicker']
ATooltip: typeof import('ant-design-vue/es')['Tooltip']
ATransfer: typeof import('ant-design-vue/es')['Transfer']
AUpload: typeof import('ant-design-vue/es')['Upload']
AvaliableToMe: typeof import('./src/components/user/AddressBooks/AvaliableToMe.vue')['default']
BlockedIPs: typeof import('./src/components/admin/security/BlockedIPs.vue')['default']
Calendars: typeof import('./src/components/common/Calendars.vue')['default']
CalendarsAccess: typeof import('./src/components/common/CalendarsAccess.vue')['default']
Categories: typeof import('./src/components/admin/domains/resources/Categories.vue')['default']
Conditions: typeof import('./src/components/common/rules/Conditions.vue')['default']
Dashboard: typeof import('./src/components/admin/Dashboard.vue')['default']
Day: typeof import('./src/components/common/approval/Day.vue')['default']
DKIM: typeof import('./src/components/admin/domains/DKIM.vue')['default']
Email: typeof import('./src/components/admin/security/black_list/Email.vue')['default']
Export: typeof import('./src/components/admin/domains/mailstorage/Export.vue')['default']
Groups: typeof import('./src/components/admin/domains/userdb/Groups.vue')['default']
IncomingRules: typeof import('./src/components/user/IncomingRules.vue')['default']
Interval: typeof import('./src/components/common/approval/Interval.vue')['default']
IP: typeof import('./src/components/admin/security/black_list/IP.vue')['default']
License: typeof import('./src/components/admin/settings/License.vue')['default']
List: typeof import('./src/components/user/Calendars/EventsPlanner/List.vue')['default']
Logo: typeof import('./src/components/Logo.vue')['default']
Mailboxes: typeof import('./src/components/admin/domains/mailstorage/Mailboxes.vue')['default']
MailboxSharedAccess: typeof import('./src/components/user/MailboxSharedAccess.vue')['default']
MailsPermDeletion: typeof import('./src/components/admin/domains/MailsPermDeletion.vue')['default']
Main: typeof import('./src/components/admin/settings/Main.vue')['default']
Manage: typeof import('./src/components/admin/settings/smtp_queue/Manage.vue')['default']
Migration: typeof import('./src/components/admin/domains/Migration.vue')['default']
MyBooks: typeof import('./src/components/user/AddressBooks/MyBooks.vue')['default']
MyCalendars: typeof import('./src/components/user/Calendars/MyCalendars.vue')['default']
NewOrEdit: typeof import('./src/components/user/Calendars/EventsPlanner/NewOrEdit.vue')['default']
NotImplemented: typeof import('./src/components/NotImplemented.vue')['default']
Offices: typeof import('./src/components/admin/domains/resources/Offices.vue')['default']
OutgoingRules: typeof import('./src/components/admin/domains/OutgoingRules.vue')['default']
PageNotFound: typeof import('./src/components/PageNotFound.vue')['default']
Policy: typeof import('./src/components/admin/domains/cidr_access/Policy.vue')['default']
Pools: typeof import('./src/components/admin/domains/cidr_access/Pools.vue')['default']
Profile: typeof import('./src/components/user/Profile.vue')['default']
RecoveryFolder: typeof import('./src/components/user/RecoveryFolder.vue')['default']
Redirects: typeof import('./src/components/admin/domains/userdb/Redirects.vue')['default']
Resources: typeof import('./src/components/admin/domains/resources/Resources.vue')['default']
RouterLink: typeof import('vue-router')['RouterLink']
RouterView: typeof import('vue-router')['RouterView']
Rules: typeof import('./src/components/common/rules/Rules.vue')['default']
ScriptErrorNotify: typeof import('./src/components/user/Calendars/EventsPlanner/ScriptErrorNotify.vue')['default']
Settings: typeof import('./src/components/admin/domains/mailstorage/Settings.vue')['default']
SettingsDB: typeof import('./src/components/admin/settings/SettingsDB.vue')['default']
SharedAddressBooks: typeof import('./src/components/admin/domains/SharedAddressBooks.vue')['default']
SharedCalendars: typeof import('./src/components/admin/domains/SharedCalendars.vue')['default']
SharedFolders: typeof import('./src/components/common/SharedFolders.vue')['default']
ShareFreeTime: typeof import('./src/components/user/Calendars/ShareFreeTime.vue')['default']
TimezoneSelect: typeof import('./src/components/common/TimezoneSelect.vue')['default']
Users: typeof import('./src/components/admin/domains/userdb/Users.vue')['default']
WorkDaysSelect: typeof import('./src/components/common/WorkDaysSelect.vue')['default']
WorkHoursRangePicker: typeof import('./src/components/common/WorkHoursRangePicker.vue')['default']
}
}

1
frontend/env.d.ts vendored Normal file
View File

@ -0,0 +1 @@
/// <reference types="vite/client" />

View File

@ -1,13 +1,13 @@
<!DOCTYPE html> <!DOCTYPE html>
<html lang="en"> <html lang="en">
<head> <head>
<meta charset="UTF-8"/> <meta charset="UTF-8">
<meta content="width=device-width, initial-scale=1.0" name="viewport"/> <link rel="icon" href="/favicon.ico">
<title>myproject</title> <meta name="viewport" content="width=device-width, initial-scale=1.0">
</head> <title>Tegu</title>
<body> </head>
<div id="app"></div> <body>
<script src="./src/main.ts" type="module"></script> <div id="app"></div>
</body> <script type="module" src="/src/main.ts"></script>
</body>
</html> </html>

File diff suppressed because it is too large Load Diff

View File

@ -1,21 +1,34 @@
{ {
"name": "frontend", "name": "js",
"private": true,
"version": "0.0.0", "version": "0.0.0",
"private": true,
"type": "module", "type": "module",
"scripts": { "scripts": {
"dev": "vite", "dev": "vite --port 7777",
"build": "vue-tsc --noEmit && vite build", "build": "run-p type-check \"build-only {@}\" --",
"preview": "vite preview" "preview": "vite preview",
"build-only": "vite build",
"type-check": "vue-tsc --build --force"
}, },
"dependencies": { "dependencies": {
"vue": "^3.2.37" "@ant-design/icons-vue": "^7.0.1",
"ant-design-vue": "^4.2.3",
"pinia": "^2.1.7",
"pinia-plugin-persistedstate": "^3.2.1",
"vue": "^3.4.21",
"vue-i18n": "^9.13.1",
"vue-router": "^4.3.0"
}, },
"devDependencies": { "devDependencies": {
"@vitejs/plugin-vue": "^3.0.3", "@tsconfig/node20": "^20.1.4",
"typescript": "^4.6.4", "@types/node": "^20.12.5",
"vite": "^3.0.7", "@vitejs/plugin-vue": "^5.0.4",
"vue-tsc": "^1.8.27", "@vue/tsconfig": "^0.5.1",
"@babel/types": "^7.18.10" "less": "^4.3.0",
"less-loader": "^12.2.0",
"npm-run-all2": "^6.1.2",
"typescript": "~5.4.0",
"vite": "^5.2.8",
"vue-tsc": "^2.0.11"
} }
} }

View File

@ -1 +1 @@
bb7ffb87329c9ad4990374471d4ce9a4 5b1aa7f7c882c7e08d0d390c73602d71

BIN
frontend/public/favicon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

View File

@ -1,22 +1,83 @@
<script lang="ts" setup> <script setup lang="ts">
import HelloWorld from "./components/HelloWorld.vue"; import { onMounted, reactive } from "vue";
import { RouterLink, RouterView } from "vue-router";
import zhCN from "ant-design-vue/es/locale/zh_CN";
import ru from "ant-design-vue/es/locale/ru_RU";
import en from "ant-design-vue/es/locale/en_GB";
import dayjs from "dayjs";
import "dayjs/locale/ru";
import "dayjs/locale/en";
import "dayjs/locale/zh-cn";
import updateLocale from "dayjs/plugin/updateLocale";
import localeData from "dayjs/plugin/localeData";
const page = reactive<{
locale: any;
}>({
locale: undefined,
});
onMounted(() => {
dayjs.extend(updateLocale);
dayjs.extend(localeData);
let locales = getBrowserLocales({ languageCodeOnly: true });
let locale = "en";
if (locales && locales.length > 0) {
locale = locales[0];
}
dayjs.updateLocale(locale, {
weekStart: 1,
});
dayjs.locale(locale);
switch (locale) {
case "ru":
page.locale = ru;
break;
case "zh":
page.locale = zhCN;
break;
default:
page.locale = en;
break;
}
});
function getBrowserLocales(options = {}) {
const defaultOptions = {
languageCodeOnly: false,
};
const opt = {
...defaultOptions,
...options,
};
const browserLocales =
navigator.languages === undefined
? [navigator.language]
: navigator.languages;
if (!browserLocales) {
return undefined;
}
return browserLocales.map((locale) => {
const trimmedLocale = locale.trim();
return opt.languageCodeOnly ? trimmedLocale.split(/-|_/)[0] : trimmedLocale;
});
}
</script> </script>
<template> <template>
<img id="logo" alt="Wails logo" src="./assets/images/logo-universal.png" /> <a-config-provider :locale="page.locale">
<HelloWorld /> <div class="root-view">
<RouterView />
</div>
</a-config-provider>
</template> </template>
<style> <style scoped></style>
#logo {
display: block;
width: 50%;
height: 50%;
margin: auto;
padding: 10% 0 0;
background-position: center;
background-repeat: no-repeat;
background-size: 100% 100%;
background-origin: content-box;
}
</style>

View File

@ -0,0 +1,36 @@
/* color palette from <https://github.com/vuejs/theme> */
*,
*::before,
*::after {
box-sizing: border-box;
margin: 0;
}
body {
min-height: 100vh;
color: var(--color-text);
background: var(--color-background);
transition:
color 0.5s,
background-color 0.5s;
line-height: 1.6;
font-family:
Inter,
-apple-system,
BlinkMacSystemFont,
'Segoe UI',
Roboto,
Oxygen,
Ubuntu,
Cantarell,
'Fira Sans',
'Droid Sans',
'Helvetica Neue',
sans-serif;
font-size: 15px;
text-rendering: optimizeLegibility;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}

View File

@ -1,93 +0,0 @@
Copyright 2016 The Nunito Project Authors (contact@sansoxygen.com),
This Font Software is licensed under the SIL Open Font License, Version 1.1.
This license is copied below, and is also available with a FAQ at:
http://scripts.sil.org/OFL
-----------------------------------------------------------
SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
-----------------------------------------------------------
PREAMBLE
The goals of the Open Font License (OFL) are to stimulate worldwide
development of collaborative font projects, to support the font creation
efforts of academic and linguistic communities, and to provide a free and
open framework in which fonts may be shared and improved in partnership
with others.
The OFL allows the licensed fonts to be used, studied, modified and
redistributed freely as long as they are not sold by themselves. The
fonts, including any derivative works, can be bundled, embedded,
redistributed and/or sold with any software provided that any reserved
names are not used by derivative works. The fonts and derivatives,
however, cannot be released under any other type of license. The
requirement for fonts to remain under this license does not apply
to any document created using the fonts or their derivatives.
DEFINITIONS
"Font Software" refers to the set of files released by the Copyright
Holder(s) under this license and clearly marked as such. This may
include source files, build scripts and documentation.
"Reserved Font Name" refers to any names specified as such after the
copyright statement(s).
"Original Version" refers to the collection of Font Software components as
distributed by the Copyright Holder(s).
"Modified Version" refers to any derivative made by adding to, deleting,
or substituting -- in part or in whole -- any of the components of the
Original Version, by changing formats or by porting the Font Software to a
new environment.
"Author" refers to any designer, engineer, programmer, technical
writer or other person who contributed to the Font Software.
PERMISSION & CONDITIONS
Permission is hereby granted, free of charge, to any person obtaining
a copy of the Font Software, to use, study, copy, merge, embed, modify,
redistribute, and sell modified and unmodified copies of the Font
Software, subject to the following conditions:
1) Neither the Font Software nor any of its individual components,
in Original or Modified Versions, may be sold by itself.
2) Original or Modified Versions of the Font Software may be bundled,
redistributed and/or sold with any software, provided that each copy
contains the above copyright notice and this license. These can be
included either as stand-alone text files, human-readable headers or
in the appropriate machine-readable metadata fields within text or
binary files as long as those fields can be easily viewed by the user.
3) No Modified Version of the Font Software may use the Reserved Font
Name(s) unless explicit written permission is granted by the corresponding
Copyright Holder. This restriction only applies to the primary font name as
presented to the users.
4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
Software shall not be used to promote, endorse or advertise any
Modified Version, except to acknowledge the contribution(s) of the
Copyright Holder(s) and the Author(s) or with their explicit written
permission.
5) The Font Software, modified or unmodified, in part or in whole,
must be distributed entirely under this license, and must not be
distributed under any other license. The requirement for fonts to
remain under this license does not apply to any document created
using the Font Software.
TERMINATION
This license becomes null and void if any of the above conditions are
not met.
DISCLAIMER
THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
OTHER DEALINGS IN THE FONT SOFTWARE.

BIN
frontend/src/assets/g69.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 136 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

View File

@ -0,0 +1,47 @@
@import './base.css';
#nav.ant-menu-horizontal>.ant-menu-item:after,#nav.ant-menu-horizontal {
bottom: auto;
}
.root-view {
padding: 8px;
}
.form-input-number {
width: 50%;
min-width: 80px;
}
.disabled-router-link {
pointer-events: none;
}
.main-view {
padding-right: 8px;
padding-left: 8px;
max-height: calc(100vh - 70px);
overflow: auto;
margin: auto;
}
.bw-list-panel-content {
min-width: 600px;
max-width: 60%;
margin: auto;
}
.disabled-div {
pointer-events: none;
opacity: 0.4;
}
.middle-of-screen {
margin: 0;
position: absolute;
top: 50%;
left: 50%;
-ms-transform: translate(-50%, -50%);
transform: translate(-50%, -50%);
}

View File

@ -1,71 +0,0 @@
<script lang="ts" setup>
import {reactive} from 'vue'
import {Greet} from '../../wailsjs/go/main/App'
const data = reactive({
name: "",
resultText: "Please enter your name below 👇",
})
function greet() {
Greet(data.name).then(result => {
data.resultText = result
})
}
</script>
<template>
<main>
<div id="result" class="result">{{ data.resultText }}</div>
<div id="input" class="input-box">
<input id="name" v-model="data.name" autocomplete="off" class="input" type="text"/>
<button class="btn" @click="greet">Greet</button>
</div>
</main>
</template>
<style scoped>
.result {
height: 20px;
line-height: 20px;
margin: 1.5rem auto;
}
.input-box .btn {
width: 60px;
height: 30px;
line-height: 30px;
border-radius: 3px;
border: none;
margin: 0 0 0 20px;
padding: 0 8px;
cursor: pointer;
}
.input-box .btn:hover {
background-image: linear-gradient(to top, #cfd9df 0%, #e2ebf0 100%);
color: #333333;
}
.input-box .input {
border: none;
border-radius: 3px;
outline: none;
height: 30px;
line-height: 30px;
padding: 0 10px;
background-color: rgba(240, 240, 240, 1);
-webkit-font-smoothing: antialiased;
}
.input-box .input:hover {
border: none;
background-color: rgba(255, 255, 255, 1);
}
.input-box .input:focus {
border: none;
background-color: rgba(255, 255, 255, 1);
}
</style>

View File

@ -0,0 +1,35 @@
<template>
<a href="https://mbk-lab.ru/en/" rel="home">
<a-flex style="height: 45px" justify="center" align="center">
<div>
<img
style="margin-right: 4px; margin-top: 4px"
height="51px"
src="@/assets/logo-200x200.png"
/>
</div>
<div style="color: #2a4861">
<div
style="
font-size: 28px;
font-weight: 600;
height: 51px;
margin-top: -8px;
margin-bottom: -16px;
"
>
TEGU
</div>
<div style="font-size: small">{{ props.version }}</div>
</div>
</a-flex>
</a>
</template>
<script setup lang="ts">
import { Flex } from "ant-design-vue";
const props = defineProps<{
version: string;
}>();
</script>

View File

@ -0,0 +1,53 @@
<template>
<div class="not-implemented">
<div class="content">
<div class="icon">🚧</div>
<h1>Not Implemented Yet</h1>
<p class="message">This feature is currently under development.</p>
<p class="sub-message">Please check back later for updates.</p>
</div>
</div>
</template>
<style scoped>
.not-implemented {
display: flex;
justify-content: center;
align-items: center;
min-height: 60vh;
background-color: #f8f9fa;
}
.content {
text-align: center;
padding: 2rem;
background: white;
border-radius: 12px;
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
max-width: 400px;
width: 90%;
}
.icon {
font-size: 4rem;
margin-bottom: 1rem;
}
h1 {
color: #2c3e50;
font-size: 1.8rem;
margin-bottom: 1rem;
font-weight: 600;
}
.message {
color: #34495e;
font-size: 1.1rem;
margin-bottom: 0.5rem;
}
.sub-message {
color: #7f8c8d;
font-size: 0.9rem;
}
</style>

View File

@ -0,0 +1,44 @@
<template>
<a-result
status="404"
title="404"
:sub-title="t('common.not_found.page_not_found')"
>
<template #extra>
<a-button v-if="page.authed" type="primary" @click="backHome">{{
t("common.not_found.return")
}}</a-button>
</template>
</a-result>
</template>
<script lang="ts" setup>
import { useAuthStore } from "@/stores/auth";
import router from "@/router";
import {
RouteAdminDashboard,
RouteLogin,
RouteUserProfile,
} from "@/router/consts";
import { useI18n } from "vue-i18n";
import { onMounted, reactive } from "vue";
const { t } = useI18n();
const page = reactive<{
authed: boolean;
}>({
authed: false,
});
onMounted(() => {
page.authed = useAuthStore().authed;
});
function backHome() {
if (useAuthStore().isAdmin) {
return router.push(RouteAdminDashboard);
} else {
return router.push(RouteUserProfile);
}
}
</script>

View File

@ -0,0 +1,465 @@
<template>
<div style="width: inherit">
<a-row :gutter="[16, 16]" justify="space-around">
<a-col :xs="24" :sm="24" :md="24" :lg="12" :xl="6">
<a-card hoverable style="height: 100%">
<template #title>
{{ t("components.admin.dashboard.license.title") }}
</template>
<a-descriptions :column="1" :labelStyle="{ fontWeight: '600' }">
<a-descriptions-item
:label="t('components.admin.dashboard.license.developer')"
>{{ page.licenseBlock.Developer }}</a-descriptions-item
>
<a-descriptions-item
:label="t('components.admin.dashboard.license.version')"
>
{{ page.licenseBlock.Version }}
</a-descriptions-item>
<a-descriptions-item
:label="t('components.admin.dashboard.license.license')"
>
<div
:class="
page.contentLoaded &&
(page.licenseBlock.Expired ||
page.licenseBlock.Mismatch ||
!page.licenseBlock.License)
? 'warn'
: ''
"
>
{{
page.licenseBlock.Mismatch
? t("components.admin.dashboard.license.license_mismatch")
: !page.licenseBlock.License
? t("components.admin.dashboard.license.license_empty")
: page.licenseBlock.License
}}
</div>
</a-descriptions-item>
</a-descriptions>
</a-card>
</a-col>
<a-col :xs="24" :sm="24" :md="24" :lg="12" :xl="6">
<a-card hoverable style="height: 100%">
<template #title>
{{ t("components.admin.dashboard.mailboxes.title") }}
</template>
<a-descriptions :column="1" :labelStyle="{ fontWeight: '600' }">
<a-descriptions-item
:label="t('components.admin.dashboard.mailboxes.license_allowed')"
>{{ page.mailboxBlock.Total }}</a-descriptions-item
>
<a-descriptions-item
:label="t('components.admin.dashboard.mailboxes.used')"
>{{ page.mailboxBlock.Used }}
</a-descriptions-item>
<a-descriptions-item
:label="t('components.admin.dashboard.mailboxes.remains')"
>
<div :class="page.mailboxBlock.LowMailboxes ? 'warn' : ''">
{{ page.mailboxBlock.Remains }}
</div>
</a-descriptions-item>
<a-descriptions-item
:label="t('components.admin.dashboard.mailboxes.archived')"
>
{{ page.mailboxBlock.Archived }}
</a-descriptions-item>
<a-descriptions-item
v-if="page.mailboxBlock.UsedLastMonth"
:label="t('components.admin.dashboard.mailboxes.used_last_month')"
>{{ page.mailboxBlock.UsedLastMonth }}
</a-descriptions-item>
<a-descriptions-item
v-if="page.mailboxBlock.UsedCurrentMonth"
:label="
t('components.admin.dashboard.mailboxes.used_current_month')
"
>{{ page.mailboxBlock.UsedCurrentMonth }}
</a-descriptions-item>
</a-descriptions>
</a-card>
</a-col>
<a-col :xs="24" :sm="24" :md="24" :lg="24" :xl="12">
<a-card
hoverable
style="height: 100%"
:tab-list="tabListNoTitle"
:active-tab-key="page.detailsKey"
@tabChange="(key: string) =>page.detailsKey = key"
>
<div v-if="page.detailsKey === 'imap'">
<a-descriptions :column="1" :labelStyle="{ fontWeight: '600' }">
<a-descriptions-item
:label="t('components.admin.dashboard.imap.server')"
>
<a-space>
{{ page.imapBlock.Server }}
<a-button
@click="copyToClipboard(page.imapBlock.Server)"
size="small"
>
<CopyOutlined />
</a-button>
</a-space>
</a-descriptions-item>
<a-descriptions-item
:label="t('components.admin.dashboard.imap.port')"
>
<a-space>
{{ page.imapBlock.Port }}
<a-button
@click="copyToClipboard(page.imapBlock.Port.toString())"
size="small"
>
<CopyOutlined />
</a-button>
</a-space>
</a-descriptions-item>
<a-descriptions-item
:label="t('components.admin.dashboard.imap.encryption')"
>{{ page.imapBlock.SSL }}
</a-descriptions-item>
<a-descriptions-item
:label="t('components.admin.dashboard.imap.auth')"
>{{ t("components.admin.dashboard.imap.auth_text") }}
</a-descriptions-item>
</a-descriptions>
</div>
<div v-if="page.detailsKey === 'smtp'">
<a-descriptions :column="1" :labelStyle="{ fontWeight: '600' }">
<a-descriptions-item
:label="t('components.admin.dashboard.smtp.server')"
>
<a-space>
{{ page.smtpBlock.Server }}
<a-button
@click="copyToClipboard(page.smtpBlock.Server)"
size="small"
>
<CopyOutlined />
</a-button>
</a-space>
</a-descriptions-item>
<a-descriptions-item
:label="t('components.admin.dashboard.smtp.port')"
>
<a-space>
{{ page.smtpBlock.Port }}
<a-button
@click="copyToClipboard(page.smtpBlock.Port.toString())"
size="small"
>
<CopyOutlined />
</a-button>
</a-space>
</a-descriptions-item>
<a-descriptions-item
:label="t('components.admin.dashboard.smtp.encryption')"
>{{ page.smtpBlock.SSL }}
</a-descriptions-item>
<a-descriptions-item
:label="t('components.admin.dashboard.smtp.auth')"
>{{ t("components.admin.dashboard.smtp.auth_text") }}
</a-descriptions-item>
</a-descriptions>
</div>
<div v-if="page.detailsKey === 'queue'">
<a-descriptions :column="1" :labelStyle="{ fontWeight: '600' }">
<a-descriptions-item
:label="t('components.admin.dashboard.smtp_queue.type')"
>{{ page.smtpQueueBlock.Type }}</a-descriptions-item
>
<a-descriptions-item
v-if="page.smtpQueueBlock.Host"
:label="t('components.admin.dashboard.smtp_queue.host')"
>
{{ page.smtpQueueBlock.Host }}
</a-descriptions-item>
</a-descriptions>
</div>
<div v-if="page.detailsKey === 'antispam'">
<a-descriptions :column="1" :labelStyle="{ fontWeight: '600' }">
<a-descriptions-item
:label="t('components.admin.dashboard.antispam.type')"
>
{{ page.antispamBlock.Type }}
</a-descriptions-item>
<a-descriptions-item
:label="t('components.admin.dashboard.antispam.status')"
>
{{
page.antispamBlock.Enabled
? t("components.admin.dashboard.antispam.status_enabled")
: t("components.admin.dashboard.antispam.status_disabled")
}}
</a-descriptions-item>
<a-descriptions-item
:label="t('components.admin.dashboard.antispam.host')"
>
{{ page.antispamBlock.Host }}
</a-descriptions-item>
</a-descriptions>
</div>
<div v-if="page.detailsKey === 'dav'">
<a-descriptions :column="1" :labelStyle="{ fontWeight: '600' }">
<a-descriptions-item
:label="t('components.admin.dashboard.dav.books')"
>
<a-space>
{{ page.davBlock.BooksUrl }}
<a-button
v-if="page.davBlock.BooksUrl.startsWith('http')"
@click="copyToClipboard(page.davBlock.BooksUrl)"
size="small"
>
<CopyOutlined />
</a-button>
</a-space>
</a-descriptions-item>
<a-descriptions-item
:label="t('components.admin.dashboard.dav.calendars')"
>
<a-space>
{{ page.davBlock.CalsUrl }}
<a-button
v-if="page.davBlock.CalsUrl.startsWith('http')"
@click="copyToClipboard(page.davBlock.CalsUrl)"
size="small"
>
<CopyOutlined />
</a-button>
</a-space>
</a-descriptions-item>
</a-descriptions>
</div>
</a-card>
</a-col>
</a-row>
<br />
<a-row :gutter="[16, 16]">
<a-col
v-for="domain in page.domains"
:xs="24"
:sm="24"
:md="24"
:lg="12"
:xl="8"
>
<a-card hoverable style="height: 100%">
<template #title> {{ domain.Domain }} </template>
<a-descriptions :column="1" :labelStyle="{ fontWeight: '600' }">
<a-descriptions-item
:label="t('components.admin.dashboard.domain.mailstorage')"
>{{ domain.MailStorage }}
</a-descriptions-item>
<a-descriptions-item
:label="t('components.admin.dashboard.domain.userdb_providers')"
>
<a-descriptions size="small" :column="1">
<a-descriptions-item v-for="prov in domain.UserDBProviders">
{{ prov }}
</a-descriptions-item>
</a-descriptions>
</a-descriptions-item>
<a-descriptions-item
:label="t('components.admin.dashboard.domain.dns_record')"
><a @click="page.dnsModal = { show: true, text: domain.DNS }">{{
t("components.admin.dashboard.domain.dns_show")
}}</a>
</a-descriptions-item>
</a-descriptions>
</a-card>
</a-col>
</a-row>
<a-row justify="space-around">
<a-col> </a-col>
</a-row>
</div>
<a-modal style="width: 70%" v-model:open="page.dnsModal.show">
<a-textarea
style="text-wrap: nowrap; height: 200px"
v-model:value="page.dnsModal.text"
>
</a-textarea>
<template #footer>
<a-button @click="copyToClipboard(page.dnsModal.text)">
<CopyOutlined />
{{ t("components.admin.dashboard.domain.dns_copy") }}
</a-button>
</template>
</a-modal>
</template>
<script setup lang="ts">
import { apiFetch } from "@/composables/apiFetch";
import { copyToClipboard } from "@/composables/misc";
import { CopyOutlined } from "@ant-design/icons-vue";
import { Modal } from "ant-design-vue";
import { onMounted, reactive } from "vue";
import { useI18n } from "vue-i18n";
const { t } = useI18n();
const tabListNoTitle = [
{
key: "imap",
tab: t("components.admin.dashboard.imap.title"),
},
{
key: "smtp",
tab: t("components.admin.dashboard.smtp.title"),
},
{
key: "queue",
tab: t("components.admin.dashboard.smtp_queue.title"),
},
{
key: "antispam",
tab: t("components.admin.dashboard.antispam.title"),
},
{
key: "dav",
tab: t("components.admin.dashboard.dav.title"),
},
];
const page = reactive<{
dnsModal: {
show: boolean;
text: string;
};
contentLoaded: boolean;
detailsKey: string;
licenseBlock: {
Developer: string;
License: string;
Expired: boolean;
Mismatch: boolean;
Version: string;
};
mailboxBlock: {
Total: number;
Used: number;
Remains: number;
Archived: number;
LowMailboxes: boolean;
UsedLastMonth: number;
UsedCurrentMonth: number;
};
smtpQueueBlock: {
Type: string;
Host: string;
};
antispamBlock: {
Type: string;
Enabled: boolean;
Host: string;
};
davBlock: {
BooksUrl: string;
CalsUrl: string;
};
imapBlock: {
Server: string;
Port: number;
SSL: string;
};
smtpBlock: {
Server: string;
Port: number;
SSL: string;
};
domains: {
Domain: string;
MailStorage: string;
UserDBProviders: string[];
DNS: string;
}[];
}>({
detailsKey: "imap",
licenseBlock: {
Developer: "",
License: "",
Expired: false,
Mismatch: false,
Version: "",
},
mailboxBlock: {
Total: 0,
Used: 0,
Remains: 0,
Archived: 0,
LowMailboxes: false,
UsedLastMonth: 0,
UsedCurrentMonth: 0,
},
smtpQueueBlock: {
Type: "",
Host: "",
},
antispamBlock: {
Type: "",
Enabled: false,
Host: "",
},
davBlock: {
BooksUrl: "",
CalsUrl: "",
},
imapBlock: {
Server: "",
Port: 0,
SSL: "",
},
smtpBlock: {
Server: "",
Port: 0,
SSL: "",
},
contentLoaded: false,
domains: [],
dnsModal: {
show: false,
text: "",
},
});
onMounted(() => {
get();
});
async function get() {
const res = await apiFetch("/admin/dashboard");
if (res.error) {
return;
}
page.contentLoaded = true;
page.licenseBlock = res.data.LicenseBlock;
page.mailboxBlock = res.data.MailboxBlock;
page.smtpQueueBlock = res.data.SmtpQueueBlock;
page.antispamBlock = res.data.AntispamBlock;
page.davBlock = res.data.DavBlock;
page.smtpBlock = res.data.SmtpBlock;
page.imapBlock = res.data.ImapBlock;
page.domains = res.data.Domains;
}
</script>
<style scoped>
.warn {
color: red;
}
</style>

View File

@ -0,0 +1,120 @@
<template>
<div>
<a-card class="panel-content">
<a-form :label-col="labelCol" :wrapper-col="wrapperCol" :labelWrap="true">
<a-form-item
:label="$t('components.admin.domains.add.Domain')"
:rules="[{ required: true }]"
>
<a-input v-model:value="page.new.Domain"> </a-input>
</a-form-item>
<a-form-item
:label="$t('components.admin.domains.add.Type')"
:rules="[{ required: true }]"
>
<a-select v-model:value="page.new.Type">
<a-select-option value="pgStorage">PostgreSQL</a-select-option>
<a-select-option value="maildirStorage">Maildir</a-select-option>
</a-select>
</a-form-item>
<a-button
:disabled="!page.new.Type || !page.new.Domain"
class="save-button"
type="primary"
size="large"
@click="save"
>
{{ $t("components.admin.domains.add.create") }}
</a-button>
</a-form>
</a-card>
</div>
</template>
<script setup lang="ts">
import { notifyError, notifySuccess } from "@/composables/alert";
import { apiFetch } from "@/composables/apiFetch";
import { ExclamationCircleOutlined } from "@ant-design/icons-vue";
import { Modal } from "ant-design-vue";
import { createVNode, onMounted, reactive } from "vue";
import { saveAndRestart } from "@/composables/restart";
import { message } from "ant-design-vue";
import { useI18n } from "vue-i18n";
import router from "@/router";
import {
DomainPlaceholder,
RouteAdminDomainsDomainMailStorageSettings,
} from "@/router/consts";
const { t } = useI18n();
interface ConnParams {
Host: string;
Port: number;
Db: string;
User: string;
Pass: string;
MaxConn: number;
}
interface PathParams {
DatabaseDir: string;
}
const page = reactive<{
new: {
Type: string;
Domain: string;
};
loading: boolean;
}>({
loading: false,
new: {
Type: "",
Domain: "",
},
});
const labelCol = { span: 8 };
const wrapperCol = { span: 16 };
onMounted(() => {});
async function save() {
const res = await apiFetch("/admin/domains/add", {
method: "POST",
body: page.new,
});
if (res.error) {
notifyError(res.error);
return;
}
notifySuccess(t("components.admin.domains.add.success"));
router.push({
path: RouteAdminDomainsDomainMailStorageSettings.replace(
DomainPlaceholder,
res.data
),
hash: "#refresh",
});
return;
}
</script>
<style scoped>
.panel-content {
width: 500px;
margin: auto;
}
.save-button {
display: block;
margin: auto;
}
</style>

View File

@ -0,0 +1,156 @@
<template style="max-width: 100vw">
<div>
<a-card v-if="page.contentLoaded" class="panel-content">
<template v-if="page.dkim !== ''">
<a-card
style="line-break: anywhere"
:title="$t('components.admin.domains.dkim.title')"
>
{{ page.dkim }}
</a-card>
<br />
<a-button size="large" class="save-button" danger @click="deleteDKIM">
{{ $t("components.admin.domains.dkim.delete") }}
</a-button>
</template>
<template v-else>
<a-form
:label-col="labelCol"
:wrapper-col="wrapperCol"
:labelWrap="true"
>
<a-form-item :label="$t('components.admin.domains.dkim.selector')">
<a-input v-model:value="page.new.Selector"> </a-input>
</a-form-item>
<a-form-item :label="$t('components.admin.domains.dkim.key_size')">
<a-input-number
:min="0"
:addon-after="$t('common.suffixes.bits')"
class="form-input-number"
v-model:value="page.new.Bits"
>
</a-input-number>
</a-form-item>
<a-button
class="save-button"
type="primary"
size="large"
@click="save"
:loading="page.loading"
>
{{ $t("components.admin.domains.dkim.create") }}
</a-button>
</a-form>
</template>
</a-card>
</div>
</template>
<script setup lang="ts">
import { notifyError, notifySuccess } from "@/composables/alert";
import { apiFetch } from "@/composables/apiFetch";
import { ExclamationCircleOutlined } from "@ant-design/icons-vue";
import { Modal } from "ant-design-vue";
import { createVNode, onMounted, reactive } from "vue";
import { saveAndRestart } from "@/composables/restart";
import { message } from "ant-design-vue";
import { useI18n } from "vue-i18n";
import router from "@/router";
import {
DomainPlaceholder,
RouteAdminDomainsDomainMailStorageSettings,
} from "@/router/consts";
import { useRoute } from "vue-router";
const route = useRoute();
const { t } = useI18n();
const page = reactive<{
contentLoaded: boolean;
domain: string;
dkim: string;
new: {
Selector: string;
Bits: number;
};
loading: boolean;
}>({
contentLoaded: false,
domain: "",
dkim: "",
loading: false,
new: {
Selector: "mail",
Bits: 1024,
},
});
const labelCol = { span: 8 };
const wrapperCol = { span: 16 };
onMounted(() => {
page.domain = route.params.domain as string;
get();
});
async function get() {
const res = await apiFetch(`/admin/domains/${page.domain}/dkim`);
if (res.error) {
notifyError(res.error);
return;
}
page.dkim = res.data;
page.contentLoaded = true;
return;
}
async function save() {
const res = await apiFetch(`/admin/domains/${page.domain}/dkim`, {
method: "POST",
body: page.new,
});
if (res.error) {
notifyError(res.error);
return;
}
notifySuccess(t("components.admin.domains.dkim.success"));
page.dkim = res.data;
return;
}
async function deleteDKIM() {
const res = await apiFetch(`/admin/domains/${page.domain}/dkim`, {
method: "DELETE",
});
if (res.error) {
notifyError(res.error);
return;
}
page.dkim = res.data;
return;
}
</script>
<style scoped>
.panel-content {
width: 600px;
max-width: 60%;
margin: auto;
}
.save-button {
width: 30%;
min-width: 150px;
display: block;
margin: auto;
}
</style>

View File

@ -0,0 +1,61 @@
<template>
<div>
<Rules
:get-get-url="getUrl"
:get-delete-url="getUrl"
:get-save-url="getUrl"
:get-move-url="moveUrl"
:get-enable-url="enableUrl"
:conditions="conditionsList"
:actions="actionsList"
>
<template #condition-value="valueProps">
<ConditionValue
:change="valueProps.change"
:value="valueProps.value"
:type="valueProps.type"
>
</ConditionValue>
</template>
<template #action-value="valueProps">
<ActionValue
:change="valueProps.change"
:value="valueProps.value"
:type="valueProps.type"
:get-folders-url="foldersUrl"
>
</ActionValue>
</template>
</Rules>
</div>
</template>
<script setup lang="ts">
import ConditionValue from "@/components/common/rules/incoming/Conditions.vue";
import ActionValue from "@/components/common/rules/incoming/Actions.vue";
import { conditionsList } from "@/components/common/rules/incoming/Conditions.vue";
import { actionsList } from "@/components/common/rules/incoming/Actions.vue";
import Rules from "@/components/common/rules/Rules.vue";
import { useRoute } from "vue-router";
const route = useRoute();
function getUrl(): string {
return `/admin/domains/${route.params.domain as string}/incoming_rules`;
}
function moveUrl(): string {
return `/admin/domains/${route.params.domain as string}/incoming_rules/move`;
}
function foldersUrl(): string {
return `/admin/domains/${
route.params.domain as string
}/incoming_rules/folders`;
}
function enableUrl(): string {
return `/admin/domains/${
route.params.domain as string
}/incoming_rules/enable`;
}
</script>

View File

@ -0,0 +1,344 @@
<template style="max-width: 100vw">
<div>
<a-card class="panel-content">
<a-form
:class="page.status.Active ? 'disabled-div' : ''"
:label-col="labelCol"
:wrapper-col="wrapperCol"
>
<a-form-item
:label="t(`components.admin.domains.mails_perm_deletion.filter`)"
>
<div style="text-align: center">
<a-flex gap="middle" vertical>
<a-row>
<a-col flex="auto">
<a-radio-group v-model:value="page.status.Condition.Op">
<a-radio-button value="and">{{
t("components.common.rules.edit_conditions_all")
}}</a-radio-button>
<a-radio-button value="or">{{
t("components.common.rules.edit_conditions_any")
}}</a-radio-button>
</a-radio-group>
</a-col>
<a-col flex="100px"> </a-col>
</a-row>
<template v-if="page.renderConditions">
<div v-for="(condition, i) in page.status.Condition.Conditions">
<a-row>
<a-col flex="auto">
<Conditions
:value="condition"
:update="(newVal: string[]) => updateCondition(i, newVal)"
:conditions="conditionsListForMailDeletion"
>
<template #condition-value="valueProps">
<ConditionValue
:change="valueProps.change"
:value="valueProps.value"
:type="valueProps.type"
>
</ConditionValue>
</template>
</Conditions>
</a-col>
<a-col flex="100px">
<a-button
:disabled="i === 0"
danger
@click="deleteCondition(i)"
>
{{ t("components.common.rules.delete") }}
</a-button>
</a-col>
</a-row>
</div>
</template>
<a-row>
<a-col flex="auto">
<a-button
style="width: fit-content; margin: auto"
@click="addCondition"
>
<PlusOutlined> </PlusOutlined>
{{ t("components.common.rules.add_condition") }}
</a-button>
</a-col>
<a-col flex="100px"> </a-col>
</a-row>
</a-flex>
</div>
</a-form-item>
</a-form>
<a-divider></a-divider>
<div>
<a-space style="width: 100%" direction="vertical">
<a-alert
v-if="page.status.Error"
type="error"
showIcon
:message="page.status.Error"
/>
<a-alert
v-if="page.status.Percent === 100 && !page.status.OnlyFind"
type="success"
showIcon
:message="t(`components.admin.domains.mails_perm_deletion.success`)"
/>
<a-progress
v-if="
(page.status.Count || page.status.Percent) &&
!page.status.OnlyFind
"
:percent="page.status.Percent"
/>
<a-row>
<a-col flex="2"> </a-col>
<a-col>
<a-space>
<a-button
:type="page.status.Count === 0 ? `primary` : undefined"
:loading="page.status.OnlyFind && page.status.Active"
:disabled="page.status.Active"
@click="start(true)"
>
{{ t("components.admin.domains.mails_perm_deletion.find") }}
</a-button>
<a-button
danger
:type="`primary`"
:disabled="page.status.Active || !page.status.Count"
:loading="!page.status.OnlyFind && page.status.Active"
@click="confirmDelete"
>
{{ t("components.admin.domains.mails_perm_deletion.delete") }}
{{ page.status.Count }}
</a-button>
</a-space>
</a-col>
</a-row>
</a-space>
</div>
</a-card>
</div>
</template>
<script setup lang="ts">
import { notifyError, notifySuccess } from "@/composables/alert";
import { apiFetch } from "@/composables/apiFetch";
import { Modal } from "ant-design-vue";
import { createVNode, onMounted, onUnmounted, reactive } from "vue";
import { saveAndRestart } from "@/composables/restart";
import { message } from "ant-design-vue";
import Conditions from "@/components/common/rules/Conditions.vue";
import ConditionValue from "@/components/common/rules/incoming/Conditions.vue";
import { conditionsListForMailDeletion } from "@/components/common/rules/incoming/Conditions.vue";
import { useI18n } from "vue-i18n";
import router from "@/router";
import {
DomainPlaceholder,
RouteAdminDomainsDomainMailStorageSettings,
} from "@/router/consts";
import {
ArrowLeftOutlined,
DeleteOutlined,
DownOutlined,
ExclamationCircleOutlined,
HomeOutlined,
PlusOutlined,
UpOutlined,
} from "@ant-design/icons-vue";
import { useRoute } from "vue-router";
import type { Condition } from "@/components/common/rules/Conditions.vue";
import { nextTick } from "vue";
import { getStatusClassNames } from "ant-design-vue/es/_util/statusUtils";
const route = useRoute();
const { t } = useI18n();
const labelCol = { span: 3, style: { "text-align": "left" } };
const wrapperCol = { span: 21, style: { "text-align": "center" } };
interface ConditionStruct {
Op: string;
Conditions: string[][];
}
interface DeleteStatus {
Condition: ConditionStruct;
OnlyFind: boolean;
Active: boolean;
Count: number;
Percent: number;
Error: string;
}
const page = reactive<{
// @ts-ignore
timeout: NodeJS.Timeout | undefined;
interval: number | undefined;
domain: string;
loading: boolean;
valid: boolean;
renderConditions: boolean;
status: DeleteStatus;
}>({
interval: 1000,
timeout: undefined,
domain: "",
loading: false,
renderConditions: true,
valid: true,
status: {
Condition: {
Op: "and",
Conditions: [[...conditionsListForMailDeletion[0].Args]],
},
OnlyFind: true,
Active: false,
Count: 0,
Percent: 0,
Error: "",
},
});
onMounted(() => {
page.domain = route.params.domain as string;
get();
});
onUnmounted(() => {
page.interval = undefined;
clearTimeout(page.timeout);
});
async function get() {
clearTimeout(page.timeout);
page.loading = true;
const res = await apiFetch(
`/admin/domains/${page.domain}/mails_perm_deletion`
);
if (res.error) {
notifyError(res.error);
return;
}
page.loading = false;
page.status = res.data as DeleteStatus;
page.renderConditions = false;
validate();
nextTick(() => {
page.renderConditions = true;
});
if (page.status.Active && page.interval) {
page.timeout = setTimeout(get, page.interval);
}
return;
}
async function start(onlyFind: boolean) {
page.loading = true;
page.status.OnlyFind = onlyFind;
if (page.status.OnlyFind) {
page.status.Count = 0;
}
page.status.Active = true;
page.status.Error = "";
page.status.Percent = 0;
const res = await apiFetch(
`/admin/domains/${page.domain}/mails_perm_deletion`,
{
method: "POST",
body: page.status,
}
);
page.loading = false;
if (res.error) {
notifyError(res.error);
return;
}
return get();
}
function updateCondition(i: number, newVal: string[]) {
page.status.Condition.Conditions[i] = newVal;
validate();
}
function deleteCondition(i: number) {
page.status.Condition.Conditions.splice(i, 1);
page.renderConditions = false;
validate();
nextTick(() => {
page.renderConditions = true;
});
}
function validate() {
let ok = true;
page.status.Condition.Conditions.forEach((args) => {
let cond = conditionsListForMailDeletion.find(
(a: Condition) => a.Value === args[0]
) as Condition;
if (cond.ValIdx != -1 && args[cond.ValIdx] === "") {
ok = false;
}
});
page.valid = ok;
}
function addCondition() {
page.status.Condition.Conditions.push(conditionsListForMailDeletion[0].Args);
validate();
}
function confirmDelete() {
Modal.confirm({
title:
t("components.admin.domains.mails_perm_deletion.delete") +
page.status.Count,
icon: createVNode(ExclamationCircleOutlined),
content: t("components.admin.domains.mails_perm_deletion.delete_text"),
okText: t("components.admin.domains.mails_perm_deletion.ok"),
okType: "danger",
cancelText: t("components.admin.domains.mails_perm_deletion.cancel"),
async onOk() {
start(false);
},
});
}
</script>
<style scoped>
.panel-content {
min-width: 800px;
max-width: 60%;
margin: auto;
}
.save-button {
width: 30%;
min-width: 150px;
display: block;
margin: auto;
}
</style>

View File

@ -0,0 +1,151 @@
<template>
<div>
<a-card class="panel-content">
<a-form :label-col="labelCol" :wrapper-col="wrapperCol" :labelWrap="true">
<a-form-item :label="$t('components.admin.domains.migration.enabled')">
<a-switch v-model:checked="page.settings.Enabled"> </a-switch>
</a-form-item>
<template v-if="page.settings.Enabled">
<a-form-item
:label="$t('components.admin.domains.migration.virtual_domain')"
>
<a-input v-model:value="page.settings.VirtualDomain"> </a-input>
</a-form-item>
<a-form-item
:label="$t('components.admin.domains.migration.other_servers')"
>
<a-textarea v-model:value="page.settings.OtherServersStr">
</a-textarea>
</a-form-item>
<a-form-item
:label="$t('components.admin.domains.migration.local_emails')"
>
<a-textarea v-model:value="page.settings.LocalEmailsStr">
</a-textarea>
</a-form-item>
</template>
<a-space style="display: flex; justify-content: center">
<a-button
:disabled="
(page.settings.Enabled &&
(!page.settings.VirtualDomain ||
!page.settings.OtherServersStr)) ||
!page.contentLoaded
"
class="save-button"
type="primary"
size="large"
style="width: fit-content"
@click="save"
>
{{ $t("components.admin.domains.migration.save") }}
</a-button>
</a-space>
</a-form>
</a-card>
</div>
</template>
<script setup lang="ts">
import { notifyError, notifySuccess } from "@/composables/alert";
import { apiFetch } from "@/composables/apiFetch";
import { ExclamationCircleOutlined } from "@ant-design/icons-vue";
import { Modal } from "ant-design-vue";
import { createVNode, onMounted, reactive } from "vue";
import { saveAndRestart } from "@/composables/restart";
import { message } from "ant-design-vue";
import { useRoute } from "vue-router";
const route = useRoute();
import { useI18n } from "vue-i18n";
const { t } = useI18n();
const page = reactive<{
domain: string;
contentLoaded: boolean;
settings: {
Enabled: boolean;
VirtualDomain: string;
OtherServers: string[];
OtherServersStr: string;
LocalEmails: string[];
LocalEmailsStr: string;
};
}>({
domain: "",
contentLoaded: false,
settings: {
Enabled: false,
VirtualDomain: "",
OtherServers: [],
OtherServersStr: "",
LocalEmails: [],
LocalEmailsStr: "",
},
});
const labelCol = { span: 16, style: { "text-align": "left" } };
const wrapperCol = { span: 8, style: { "text-align": "right" } };
onMounted(() => {
page.domain = route.params.domain as string;
get();
});
function loadTextArea() {
page.settings.OtherServersStr = page.settings.OtherServers.join("\n");
page.settings.LocalEmailsStr = page.settings.LocalEmails.join("\n");
}
function saveTextArea() {
page.settings.OtherServers = page.settings.OtherServersStr.split("\n");
page.settings.LocalEmails = page.settings.LocalEmailsStr.split("\n");
}
async function get() {
const res = await apiFetch(`/admin/domains/${page.domain}/migration`);
if (res.error) {
notifyError(res.error);
return;
}
page.settings = res.data;
page.contentLoaded = true;
loadTextArea();
return;
}
async function save() {
saveTextArea();
const res = await apiFetch(`/admin/domains/${page.domain}/migration`, {
method: "POST",
body: page.settings,
});
if (res.error) {
notifyError(res.error);
return;
}
notifySuccess(t("components.admin.domains.migration.success"));
}
</script>
<style scoped>
.panel-content {
width: 600px;
max-width: 40%;
margin: auto;
}
.input-number {
width: 50%;
}
.save-button {
width: 30%;
min-width: 150px;
display: block;
margin: auto;
}
</style>

View File

@ -0,0 +1,68 @@
<template>
<div>
<Rules
:get-get-url="getUrl"
:get-delete-url="getUrl"
:get-save-url="getUrl"
:get-move-url="moveUrl"
:get-enable-url="enableUrl"
:conditions="conditionsList"
:actions="actionsList"
>
<template #condition-value="valueProps">
<ConditionValue
:change="valueProps.change"
:value="valueProps.value"
:type="valueProps.type"
:get-groups-url="groupsUrl"
>
</ConditionValue>
</template>
<template #action-value="valueProps">
<ActionValue
:change="valueProps.change"
:value="valueProps.value"
:type="valueProps.type"
:get-groups-url="groupsUrl"
:get-group-emails-url="groupEmailsUrl"
>
</ActionValue>
</template>
</Rules>
</div>
</template>
<script setup lang="ts">
import ConditionValue from "@/components/common/rules/outgoing/Conditions.vue";
import ActionValue from "@/components/common/rules/outgoing/Actions.vue";
import { conditionsList } from "@/components/common/rules/outgoing/Conditions.vue";
import { actionsList } from "@/components/common/rules/outgoing/Actions.vue";
import Rules from "@/components/common/rules/Rules.vue";
import { useRoute } from "vue-router";
const route = useRoute();
function getUrl(): string {
return `/admin/domains/${route.params.domain as string}/outgoing_rules`;
}
function moveUrl(): string {
return `/admin/domains/${route.params.domain as string}/outgoing_rules/move`;
}
function groupsUrl(): string {
return `/admin/domains/${
route.params.domain as string
}/outgoing_rules/groups`;
}
function groupEmailsUrl(): string {
return `/admin/domains/${
route.params.domain as string
}/outgoing_rules/group_emails`;
}
function enableUrl(): string {
return `/admin/domains/${
route.params.domain as string
}/outgoing_rules/enable`;
}
</script>

View File

@ -0,0 +1,25 @@
<template>
<AddressBooks
v-if="page.loaded"
:domain="page.domain"
user="admin"
></AddressBooks>
</template>
<script setup lang="ts">
import AddressBooks from "@/components/common/AddressBooks.vue";
import { onMounted, reactive } from "vue";
import { useRoute } from "vue-router";
const route = useRoute();
const page = reactive<{
domain: string;
loaded: boolean;
}>({
domain: "",
loaded: false,
});
onMounted(() => {
page.domain = route.params.domain as string;
page.loaded = true;
});
</script>

View File

@ -0,0 +1,21 @@
<template>
<Calendars v-if="page.loaded" :domain="page.domain" user="admin"></Calendars>
</template>
<script setup lang="ts">
import Calendars from "@/components/common/Calendars.vue";
import { onMounted, reactive } from "vue";
import { useRoute } from "vue-router";
const route = useRoute();
const page = reactive<{
domain: string;
loaded: boolean;
}>({
domain: "",
loaded: false,
});
onMounted(() => {
page.domain = route.params.domain as string;
page.loaded = true;
});
</script>

View File

@ -0,0 +1,25 @@
<template>
<SharedFolders
v-if="page.loaded"
:domain="page.domain"
user="admin"
></SharedFolders>
</template>
<script setup lang="ts">
import SharedFolders from "@/components/common/SharedFolders.vue";
import { onMounted, reactive } from "vue";
import { useRoute } from "vue-router";
const route = useRoute();
const page = reactive<{
domain: string;
loaded: boolean;
}>({
domain: "",
loaded: false,
});
onMounted(() => {
page.domain = route.params.domain as string;
page.loaded = true;
});
</script>

View File

@ -0,0 +1,471 @@
<template>
<div>
<a-card class="panel-content">
<a-form
style="margin-bottom: -24px"
:label-col="labelCol"
:wrapper-col="wrapperCol"
:labelWrap="true"
>
<a-form-item
:label="$t('components.admin.domains.cidr_access.policy.enabled')"
>
<a-switch
:disabled="page.enabled === undefined"
@change="setEnabled"
v-model:checked="page.enabled"
>
</a-switch>
</a-form-item>
</a-form>
</a-card>
<br />
<a-table
class="panel-content"
:columns="columns"
:data-source="page.data"
:loading="page.loading"
bordered
:pagination="false"
>
<template #title>
<a-row>
<a-col style="display: flex; align-items: flex-end" :span="18">
<a-space>
<a-input-search
v-model:value="page.pagination.search"
:placeholder="
t('components.admin.domains.cidr_access.policy.search')
"
enter-button
allow-clear
@search="get"
/>
</a-space>
</a-col>
<a-col :span="6">
<a-space style="display: flex; justify-content: end">
<a-button type="primary" @click="showAddModal(undefined)">
<PlusOutlined />
{{
t("components.admin.domains.cidr_access.policy.add_policy")
}}
</a-button>
<a-popconfirm
v-if="page.withSearch"
:title="
t(
'components.admin.domains.cidr_access.policy.delete_found'
) + '?'
"
:ok-text="t('components.admin.domains.cidr_access.policy.ok')"
:cancel-text="
t('components.admin.domains.cidr_access.policy.cancel')
"
@confirm="deletePolicy(undefined)"
>
<a-button danger>{{
$t("components.admin.domains.cidr_access.policy.delete_found")
}}</a-button>
</a-popconfirm>
<a-popconfirm
v-else
:title="
t('components.admin.domains.cidr_access.policy.delete_all') +
'?'
"
:ok-text="t('components.admin.domains.cidr_access.policy.ok')"
:cancel-text="
t('components.admin.domains.cidr_access.policy.cancel')
"
@confirm="deletePolicy(undefined)"
>
<a-button danger>{{
$t("components.admin.domains.cidr_access.policy.delete_all")
}}</a-button>
</a-popconfirm>
</a-space>
</a-col>
</a-row>
</template>
<template #bodyCell="{ column, record }">
<template v-if="column.key === 'action'">
<a-space>
<a-button type="primary" @click="showAddModal(record)">{{
t("components.admin.domains.cidr_access.policy.properties")
}}</a-button>
<a-popconfirm
:title="
t('components.admin.domains.cidr_access.policy.delete') + '?'
"
:ok-text="t('components.admin.domains.cidr_access.policy.ok')"
:cancel-text="
t('components.admin.domains.cidr_access.policy.cancel')
"
@confirm="deletePolicy(record.GroupName)"
>
<a-button danger>{{
t("components.admin.domains.cidr_access.policy.delete")
}}</a-button>
</a-popconfirm>
</a-space>
</template>
</template>
<template #footer>
<a-flex justify="flex-end">
<a-pagination
v-model:value="page.pagination.current"
:defaultPageSize="page.pagination.size"
:total="page.pagination.total"
@change="pageChange"
:pageSizeOptions="['10', '20', '50', '100']"
v-model:pageSize="page.pagination.size"
:showSizeChanger="true"
/>
</a-flex>
</template>
</a-table>
</div>
<a-modal
style="width: 900px"
v-model:open="page.add.show"
:title="
page.add.isUpdate
? t('components.admin.domains.cidr_access.policy.update_policy') +
page.add.policy.GroupName
: t('components.admin.domains.cidr_access.policy.add_policy')
"
>
<a-divider></a-divider>
<a-space direction="vertical">
<a-select
v-if="!page.add.isUpdate"
:placeholder="
t('components.admin.domains.cidr_access.policy.select_group')
"
style="width: 100%"
:disabled="page.add.avaliableGroups.length === 0"
v-model:value="page.add.policy.GroupName"
:options="page.add.avaliableGroups"
>
</a-select>
<a-transfer
v-model:target-keys="page.add.pools_ids"
:data-source="page.pools"
:render="(item: any) => item.name"
show-search
:titles="[' (avaliable pools)', ' (active pools)']"
:list-style="{
width: '405px',
height: '320px',
}"
/>
</a-space>
<a-divider></a-divider>
<template #footer>
<a-button
type="primary"
:disabled="!page.add.policy.GroupName || page.add.pools_ids.length == 0"
@click="add"
>
{{
page.add.isUpdate
? t("components.admin.domains.cidr_access.policy.update_policy") +
page.add.policy.GroupName
: t("components.admin.domains.cidr_access.policy.add_policy")
}}
</a-button>
</template>
</a-modal>
</template>
<script setup lang="ts">
import { notifyError, notifySuccess } from "@/composables/alert";
import { apiFetch } from "@/composables/apiFetch";
import router from "@/router";
import Export from "@/components/admin/domains/mailstorage/Export.vue";
import {
DomainPlaceholder,
MailboxPlaceholder,
RouteAdminDomainsDomainMailStorageMailboxesExport,
} from "@/router/consts";
import {
ArrowLeftOutlined,
DeleteOutlined,
DownOutlined,
ExclamationCircleOutlined,
HomeOutlined,
PlusOutlined,
} from "@ant-design/icons-vue";
import { Modal } from "ant-design-vue";
import type { ColumnType } from "ant-design-vue/es/table";
import { createVNode, onMounted, reactive } from "vue";
import { useI18n } from "vue-i18n";
import { useRoute } from "vue-router";
const route = useRoute();
const { t } = useI18n();
const labelCol = { span: 16, style: { "text-align": "left" } };
const wrapperCol = { span: 8, style: { "text-align": "right" } };
const columns: ColumnType<any>[] = [
{
title: t("components.admin.domains.cidr_access.policy.col_name"),
dataIndex: "GroupName",
},
{
title: t("components.admin.domains.cidr_access.policy.col_count"),
dataIndex: "Count",
},
{
key: "action",
align: "right",
},
];
interface Pool {
ID: number;
Name: string;
}
interface Policy {
GroupName?: string;
Pools: Pool[];
}
interface TransferElem {
key: string;
name: string;
}
const page = reactive<{
domain: string;
enabled?: boolean;
pools: TransferElem[];
withSearch: boolean;
loading: boolean;
data: any;
pagination: {
current: number;
total: number;
size: number;
search: string;
};
add: {
show: boolean;
pools_ids: string[];
isUpdate: boolean;
avaliableGroups: any[];
policy: Policy;
};
}>({
domain: "",
enabled: undefined,
pools: [],
withSearch: false,
pagination: {
current: 1,
total: 0,
size: 50,
search: "",
},
loading: false,
data: [],
add: {
show: false,
pools_ids: [],
isUpdate: false,
avaliableGroups: [],
policy: {
GroupName: "",
Pools: [],
},
},
});
onMounted(() => {
page.domain = route.params.domain as string;
get();
getEnabled();
getPools();
});
async function getPools() {
const res = await apiFetch(`/admin/domains/${page.domain}/cidr_access/pools`);
if (res.error) {
notifyError(res.error);
return;
}
page.pools = [];
page.pools = (res.data as Array<any>).map((e) => {
return {
key: String(e.ID),
name: e.Name,
};
});
return;
}
async function getEnabled() {
const res = await apiFetch(
`/admin/domains/${page.domain}/cidr_access/policy/enabled`
);
if (res.error) {
notifyError(res.error);
return;
}
page.enabled = res.data;
return;
}
async function setEnabled() {
const res = await apiFetch(
`/admin/domains/${page.domain}/cidr_access/policy/enabled`,
{
method: "POST",
body: {
Enabled: page.enabled,
},
}
);
if (res.error) {
notifyError(res.error);
return;
}
}
async function get() {
page.withSearch = page.pagination.search !== "";
page.loading = true;
const params = new URLSearchParams();
params.append("page", String(page.pagination.current));
params.append("size", String(page.pagination.size));
params.append("search", page.pagination.search);
const res = await apiFetch(
`/admin/domains/${page.domain}/cidr_access/policy?` + params.toString()
);
page.loading = false;
if (res.error) {
notifyError(res.error);
return;
}
page.data = (res.data as any[]).map((e) => {
return {
Count: (e.Pools as any[]).length,
...e,
};
});
page.pagination.total = res.total;
return;
}
function pageChange(current: number) {
page.pagination.current = current;
return get();
}
async function add() {
page.add.policy.Pools = page.add.pools_ids.map((e) => {
return {
ID: Number(e),
Name: "",
};
});
const res = await apiFetch(
`/admin/domains/${page.domain}/cidr_access/policy`,
{
method: "POST",
body: page.add.policy,
}
);
if (res.error) {
notifyError(res.error);
return;
}
page.add.show = false;
return get();
}
async function showAddModal(policy: Policy | undefined) {
if (policy) {
page.add.policy.GroupName = policy.GroupName;
page.add.policy.Pools = policy.Pools;
page.add.isUpdate = true;
page.add.avaliableGroups = [{ value: policy.GroupName }];
page.add.pools_ids = policy.Pools.map((e) => {
return String(e.ID);
});
} else {
page.add.policy.GroupName = undefined;
page.add.policy.Pools = [];
page.add.pools_ids = [];
page.add.isUpdate = false;
const res = await apiFetch(
`/admin/domains/${page.domain}/cidr_access/policy/avaliable_groups`
);
if (res.error) {
notifyError(res.error);
return;
}
page.add.avaliableGroups = [];
page.add.avaliableGroups = (res.data as string[]).map((e: string) => {
return {
value: e,
};
});
}
page.add.show = true;
}
async function deletePolicy(group: string | undefined) {
const params = new URLSearchParams();
params.append("search", page.pagination.search);
const res = await apiFetch(
`/admin/domains/${page.domain}/cidr_access/policy?` + params.toString(),
{
method: "DELETE",
body: {
GroupName: group,
},
}
);
if (res.error) {
notifyError(res.error);
return;
}
return get();
}
</script>
<style scoped>
.panel-content {
min-width: 600px;
max-width: 60%;
margin: auto;
}
</style>

View File

@ -0,0 +1,397 @@
<template>
<div>
<a-table
class="panel-content"
:columns="columns"
:data-source="page.data"
:loading="page.loading"
bordered
:pagination="false"
>
<template #title>
<a-row>
<a-col style="display: flex; align-items: flex-end" :span="18">
<a-space>
<a-input-search
v-model:value="page.pagination.search"
:placeholder="
t('components.admin.domains.cidr_access.pools.search')
"
enter-button
allow-clear
@search="get"
/>
</a-space>
</a-col>
<a-col :span="6">
<a-space style="display: flex; justify-content: end">
<a-button type="primary" @click="showAddModal">
<PlusOutlined />
{{ t("components.admin.domains.cidr_access.pools.add_pool") }}
</a-button>
<a-popconfirm
v-if="page.withSearch"
:title="
t('components.admin.domains.cidr_access.pools.delete_found') +
'?'
"
:ok-text="t('components.admin.domains.cidr_access.pools.ok')"
:cancel-text="
t('components.admin.domains.cidr_access.pools.cancel')
"
@confirm="deletePool(undefined)"
>
<a-button danger>{{
$t("components.admin.domains.cidr_access.pools.delete_found")
}}</a-button>
</a-popconfirm>
<a-popconfirm
v-else
:title="
t('components.admin.domains.cidr_access.pools.delete_all') +
'?'
"
:ok-text="t('components.admin.domains.cidr_access.pools.ok')"
:cancel-text="
t('components.admin.domains.cidr_access.pools.cancel')
"
@confirm="deletePool(undefined)"
>
<a-button danger>{{
$t("components.admin.domains.cidr_access.pools.delete_all")
}}</a-button>
</a-popconfirm>
</a-space>
</a-col>
</a-row>
</template>
<template #bodyCell="{ column, record }">
<template v-if="column.key === 'action'">
<a-space>
<a-button type="primary" @click="showPropModal(record)">{{
t("components.admin.domains.cidr_access.pools.properties")
}}</a-button>
<a-popconfirm
:title="
t('components.admin.domains.cidr_access.pools.delete') + '?'
"
:ok-text="t('components.admin.domains.cidr_access.pools.ok')"
:cancel-text="
t('components.admin.domains.cidr_access.pools.cancel')
"
@confirm="deletePool(record.ID)"
>
<a-button danger>{{
t("components.admin.domains.cidr_access.pools.delete")
}}</a-button>
</a-popconfirm>
</a-space>
</template>
</template>
<template #footer>
<a-flex justify="flex-end">
<a-pagination
v-model:value="page.pagination.current"
:defaultPageSize="page.pagination.size"
:total="page.pagination.total"
@change="pageChange"
:pageSizeOptions="['10', '20', '50', '100']"
v-model:pageSize="page.pagination.size"
:showSizeChanger="true"
/>
</a-flex>
</template>
</a-table>
</div>
<a-modal
style="width: 500px"
v-model:open="page.add.show"
:title="t('components.admin.domains.cidr_access.pools.add_pool')"
>
<a-divider></a-divider>
<a-form :label-col="labelCol" :wrapper-col="wrapperCol" labelWrap>
<a-form-item
:label="$t('components.admin.domains.cidr_access.pools.add_name')"
:rules="[{ required: true }]"
>
<a-input v-model:value="page.add.pool.Name"> </a-input>
</a-form-item>
<a-form-item
:label="$t('components.admin.domains.cidr_access.pools.add_ips')"
:rules="[{ required: true }]"
>
<a-textarea
:placeholder="
$t('components.admin.domains.cidr_access.pools.add_ips_placeholder')
"
v-model:value="page.add.pool.IPsString"
>
</a-textarea>
</a-form-item>
</a-form>
<template #footer>
<a-button
type="primary"
:disabled="!page.add.pool.Name || page.add.pool.IPsString == ''"
@click="add"
>
{{ t("components.admin.domains.cidr_access.pools.add_pool") }}
</a-button>
</template>
</a-modal>
<a-modal
style="width: 500px"
v-model:open="page.props.show"
:title="t('components.admin.domains.cidr_access.pools.properties')"
>
<a-divider></a-divider>
<a-form :label-col="labelCol" :wrapper-col="wrapperCol" labelWrap>
<a-form-item
:label="$t('components.admin.domains.cidr_access.pools.add_name')"
>
<a-input disabled v-model:value="page.props.pool.Name"> </a-input>
</a-form-item>
<a-form-item
:label="$t('components.admin.domains.cidr_access.pools.add_ips')"
>
<a-textarea
:placeholder="
$t('components.admin.domains.cidr_access.pools.add_ips_placeholder')
"
v-model:value="page.props.pool.IPsString"
>
</a-textarea>
</a-form-item>
</a-form>
<template #footer>
<a-button
type="primary"
:disabled="!page.props.pool.Name || page.props.pool.IPsString == ''"
@click="update"
>
{{ t("components.admin.domains.cidr_access.pools.update_pool") }}
</a-button>
</template>
</a-modal>
</template>
<script setup lang="ts">
import { notifyError } from "@/composables/alert";
import { apiFetch } from "@/composables/apiFetch";
import router from "@/router";
import Export from "@/components/admin/domains/mailstorage/Export.vue";
import {
DomainPlaceholder,
MailboxPlaceholder,
RouteAdminDomainsDomainMailStorageMailboxesExport,
} from "@/router/consts";
import {
ArrowLeftOutlined,
DeleteOutlined,
DownOutlined,
ExclamationCircleOutlined,
HomeOutlined,
PlusOutlined,
} from "@ant-design/icons-vue";
import { Modal } from "ant-design-vue";
import type { ColumnType } from "ant-design-vue/es/table";
import { createVNode, onMounted, reactive } from "vue";
import { useI18n } from "vue-i18n";
import { useRoute } from "vue-router";
const route = useRoute();
const { t } = useI18n();
const labelCol = { span: 8 };
const wrapperCol = { span: 16 };
const columns: ColumnType<any>[] = [
{
title: t("components.admin.domains.cidr_access.pools.col_name"),
dataIndex: "Name",
},
{
key: "action",
align: "right",
},
];
interface Pool {
Name: string;
IPs: string[];
IPsString: string;
ID: number;
}
const page = reactive<{
domain: string;
withSearch: boolean;
loading: boolean;
data: any;
pagination: {
current: number;
total: number;
size: number;
search: string;
};
add: {
show: boolean;
pool: Pool;
};
props: {
show: boolean;
pool: Pool;
};
}>({
domain: "",
withSearch: false,
pagination: {
current: 1,
total: 0,
size: 50,
search: "",
},
loading: false,
data: [],
add: {
show: false,
pool: {
ID: 0,
Name: "",
IPs: [],
IPsString: "",
},
},
props: {
show: false,
pool: {
ID: 0,
Name: "",
IPs: [],
IPsString: "",
},
},
});
onMounted(() => {
page.domain = route.params.domain as string;
get();
});
async function get() {
page.withSearch = page.pagination.search !== "";
page.loading = true;
const params = new URLSearchParams();
params.append("page", String(page.pagination.current));
params.append("size", String(page.pagination.size));
params.append("search", page.pagination.search);
const res = await apiFetch(
`/admin/domains/${page.domain}/cidr_access/pools?` + params.toString()
);
page.loading = false;
if (res.error) {
notifyError(res.error);
return;
}
page.data = res.data;
page.pagination.total = res.total;
return;
}
function pageChange(current: number) {
page.pagination.current = current;
return get();
}
async function add() {
page.add.pool.IPs = page.add.pool.IPsString.split("\n");
const res = await apiFetch(
`/admin/domains/${page.domain}/cidr_access/pools`,
{
method: "POST",
body: page.add.pool,
}
);
if (res.error) {
notifyError(res.error);
return;
}
page.add.show = false;
return get();
}
async function update() {
page.props.pool.IPs = page.props.pool.IPsString.split("\n");
const res = await apiFetch(
`/admin/domains/${page.domain}/cidr_access/pools`,
{
method: "POST",
body: page.props.pool,
}
);
if (res.error) {
notifyError(res.error);
return;
}
page.props.show = false;
return get();
}
async function showPropModal(record: any) {
page.props.pool = { ...record };
page.props.pool.IPsString = page.props.pool.IPs.join("\n");
page.props.show = true;
}
async function showAddModal() {
page.add.pool.Name = "";
page.add.pool.ID = 0;
page.add.pool.IPsString = "";
page.add.pool.IPs = [];
page.add.show = true;
}
async function deletePool(id: number | undefined) {
const params = new URLSearchParams();
params.append("search", page.pagination.search);
const res = await apiFetch(
`/admin/domains/${page.domain}/cidr_access/pools?` + params.toString(),
{
method: "DELETE",
body: {
ID: id,
},
}
);
if (res.error) {
notifyError(res.error);
return;
}
return get();
}
</script>
<style scoped>
.panel-content {
min-width: 600px;
max-width: 60%;
margin: auto;
}
</style>

View File

@ -0,0 +1,158 @@
<template>
<a-modal
v-model:open="open"
:title="
t('components.admin.domains.mailstorage.export.title') + ' ' + props.email
"
>
<a-divider></a-divider>
<a-form-item v-if="page.error">
<a-alert type="error" showIcon :message="page.error" />
</a-form-item>
<a-form-item v-if="page.duration">
<a-alert
type="success"
showIcon
:message="
t('components.admin.domains.mailstorage.export.success') +
page.duration
"
/>
</a-form-item>
<a-form :label-col="labelCol" :wrapper-col="wrapperCol" :labelWrap="true">
<a-form-item
v-if="page.hostname"
:label="$t('components.admin.domains.mailstorage.export.hostname')"
>
<a-input :disabled="true" v-model:value="page.hostname"> </a-input>
</a-form-item>
<a-form-item
:label="$t('components.admin.domains.mailstorage.export.catalog')"
>
<a-input
:disabled="page.active || page.loading"
v-model:value="page.targetFolder"
>
</a-input>
</a-form-item>
</a-form>
<a-progress
v-if="page.active || page.percent !== 0"
:percent="page.percent"
/>
<template #footer>
<a-button
@click="start"
v-if="!page.active && !page.duration"
:disabled="page.loading"
type="primary"
>
{{ t("components.admin.domains.mailstorage.export.start_export") }}
</a-button>
</template>
</a-modal>
</template>
<script setup lang="ts">
import { notifyError } from "@/composables/alert";
import { apiFetch } from "@/composables/apiFetch";
import { onMounted, onUnmounted, reactive } from "vue";
import { useI18n } from "vue-i18n";
import { useRoute } from "vue-router";
const route = useRoute();
const { t } = useI18n();
const labelCol = { span: 8 };
const wrapperCol = { span: 16 };
const props = defineProps<{
email: string;
}>();
const open = defineModel("open");
const page = reactive<{
// @ts-ignore
timeout: NodeJS.Timeout | undefined;
interval: number | undefined;
loading: boolean;
domain: string;
targetFolder: string;
hostname: string;
active: boolean;
percent: number;
error: string;
duration: string;
}>({
interval: 1000,
timeout: undefined,
loading: false,
percent: 0,
active: false,
domain: "",
targetFolder: "",
hostname: "",
error: "",
duration: "",
});
onMounted(() => {
page.domain = route.params.domain as string;
get();
});
onUnmounted(() => {
page.interval = undefined;
clearTimeout(page.timeout);
});
async function get() {
clearTimeout(page.timeout);
page.loading = true;
const res = await apiFetch(
`/admin/domains/${page.domain}/mailstorage/mailboxes/${props.email}/export`
);
if (res.error) {
notifyError(res.error);
return;
}
page.loading = false;
page.active = res.data.Active;
page.percent = res.data.Percent;
page.hostname = res.data.Hostname;
page.targetFolder = res.data.TargetFolder;
page.error = res.data.Error;
page.duration = res.data.Duration;
if (page.active && page.interval) {
page.timeout = setTimeout(get, page.interval);
}
return;
}
async function start() {
page.loading = true;
const res = await apiFetch(
`/admin/domains/${page.domain}/mailstorage/mailboxes/${props.email}/export`,
{
method: "POST",
body: {
TargetFolder: page.targetFolder,
},
}
);
page.loading = false;
if (res.error) {
notifyError(res.error);
return;
}
return get();
}
</script>

View File

@ -0,0 +1,747 @@
<template>
<a-table
class="ant-table-striped"
:columns="page.columns"
:data-source="page.data"
:loading="page.loading"
bordered
style="min-width: 830px"
size="small"
:pagination="false"
:rowClassName="
(record: any, index: any) => (record.Archived ? 'striped' : undefined)
"
>
<template #bodyCell="{ column, record }">
<template v-if="column.key === 'email'">
<a-space>
<a-tooltip
:title="
t(`components.admin.domains.mailstorage.mailboxes.in_archive`)
"
>
<LockOutlined v-if="record.Archived" />
</a-tooltip>
{{ record.Email }}
</a-space>
</template>
<template v-if="column.key === 'action'">
<a-dropdown>
<template #overlay>
<a-menu>
<a-menu-item key="1">
<a-button
style="width: 100%"
@click="showRenameWindow(record.Email as string)"
>{{
t("components.admin.domains.mailstorage.mailboxes.rename")
}}</a-button
>
</a-menu-item>
<a-menu-item key="2">
<a-button
style="width: 100%"
@click="
page.export.email = record.Email;
page.export.show = true;
"
>{{
t("components.admin.domains.mailstorage.mailboxes.export")
}}</a-button
>
</a-menu-item>
<a-menu-item v-if="page.type !== 'maildirStorage'" key="3">
<a-button
style="width: 100%"
@click="clearRecovery(record.Email)"
>{{
t(
"components.admin.domains.mailstorage.mailboxes.clear_recovery"
)
}}</a-button
>
</a-menu-item>
<a-menu-item key="4">
<a-button
:disabled="page.multistorageAccessKeys.length < 2"
style="width: 100%"
@click="
showChangeStorage(
record.Email,
record.MultistorageAccessKey
)
"
>{{
t(
"components.admin.domains.mailstorage.mailboxes.change_storage"
)
}}</a-button
>
</a-menu-item>
<a-menu-item key="5">
<a-button
v-if="record.Archived"
style="width: 100%"
@click="setArchiveMailbox(record.Email, false)"
>{{
t(
"components.admin.domains.mailstorage.mailboxes.unarchive"
)
}}</a-button
>
<a-button
v-else
style="width: 100%"
@click="setArchiveMailbox(record.Email, true)"
>{{
t("components.admin.domains.mailstorage.mailboxes.archive")
}}</a-button
>
</a-menu-item>
<a-menu-item key="6">
<a-button
@click="
deleteMailbox(
t(
'components.admin.domains.mailstorage.mailboxes.delete'
) +
' ' +
record.Email +
'?',
record.Email
)
"
style="width: 100%"
danger
:disabled="record.Archived"
>{{
t("components.admin.domains.mailstorage.mailboxes.delete")
}}</a-button
>
</a-menu-item>
</a-menu>
</template>
<a>
{{
t("components.admin.domains.mailstorage.mailboxes.col_actions")
}}
<DownOutlined />
</a>
</a-dropdown>
</template>
</template>
<template #title>
<a-row>
<a-col style="display: flex; align-items: flex-end" :span="12">
<a-space>
<a-input-search
v-model:value="page.pagination.search"
:placeholder="
t('components.admin.domains.mailstorage.mailboxes.search')
"
enter-button
allow-clear
@search="applySearch"
/>
<a-select
style="width: 140px"
v-model:value="page.pagination.access_key"
v-if="page.multistorageAccessKeys.length > 1"
@change="applySearch"
>
<a-select-option value="">{{
t(
"components.admin.domains.mailstorage.mailboxes.storage_filter"
)
}}</a-select-option>
<a-select-option
v-for="key in page.multistorageAccessKeys"
:value="key"
>{{ key }}</a-select-option
>
</a-select>
</a-space>
</a-col>
<a-col :span="12">
<a-space
v-if="page.withSearch"
style="display: flex; justify-content: end"
>
<a-button
v-if="page.type !== 'maildirStorage'"
@click="clearRecovery('')"
>{{
$t(
"components.admin.domains.mailstorage.mailboxes.clear_recovery_found"
)
}}</a-button
>
<a-button
@click="
deleteMailbox(
t(
'components.admin.domains.mailstorage.mailboxes.delete_found'
) + '?',
''
)
"
danger
>{{
$t(
"components.admin.domains.mailstorage.mailboxes.delete_found"
)
}}</a-button
>
</a-space>
<a-space v-else style="display: flex; justify-content: end">
<a-button
v-if="page.type !== 'maildirStorage'"
@click="clearRecovery('')"
>{{
$t(
"components.admin.domains.mailstorage.mailboxes.clear_recovery_all"
)
}}</a-button
>
<a-button
@click="
deleteMailbox(
t(
'components.admin.domains.mailstorage.mailboxes.delete_all'
) + '?',
''
)
"
danger
>{{
$t("components.admin.domains.mailstorage.mailboxes.delete_all")
}}</a-button
>
</a-space>
</a-col>
</a-row>
</template>
<template #footer>
<a-flex justify="flex-end">
<a-pagination
v-model:value="page.pagination.current"
:defaultPageSize="page.pagination.size"
:total="page.pagination.total"
@change="pageChange"
:pageSizeOptions="['10', '20', '50', '100']"
v-model:pageSize="page.pagination.size"
:showSizeChanger="true"
/>
</a-flex>
</template>
</a-table>
<Export
v-if="page.export.show"
v-model:open="page.export.show"
:email="page.export.email"
></Export>
<a-modal
:title="
$t('components.admin.domains.mailstorage.mailboxes.rename') +
' ' +
page.rename.oldEmail
"
v-model:open="page.rename.show"
style="width: 350px"
>
<a-space style="width: 100%" direction="vertical">
<a-alert
v-if="page.rename.error"
type="error"
showIcon
:message="page.rename.error"
closable
/>
<a-input
v-model:value="page.rename.newLp"
:addon-after="'@' + page.domain"
>
</a-input>
</a-space>
<template #footer>
<a-space>
<a-button @click="page.rename.show = false">
{{
$t("components.admin.domains.mailstorage.mailboxes.cancel")
}}</a-button
>
<a-button
:disabled="page.rename.newLp === ''"
:loading="page.rename.loading"
@click="renameMailbox"
type="primary"
>
{{
$t("components.admin.domains.mailstorage.mailboxes.rename")
}}</a-button
>
</a-space>
</template>
</a-modal>
<a-modal
:title="
$t('components.admin.domains.mailstorage.mailboxes.change_storage_for') +
page.change_storage.email
"
v-model:open="page.change_storage.show"
style="width: 350px"
:maskClosable="
!page.change_storage.status.Active && !page.change_storage.loading
"
:closable="
!page.change_storage.status.Active && !page.change_storage.loading
"
>
<a-alert
v-if="page.change_storage.status.Error"
type="error"
showIcon
:message="page.change_storage.status.Error"
/>
<a-progress
v-if="page.change_storage.status.Active"
:percent="page.change_storage.status.Percent"
/>
<a-select
v-if="!page.change_storage.status.Active"
style="width: 100%"
v-model:value="page.change_storage.new_access_key"
>
<a-select-option
v-for="key in page.change_storage.accessKeysVariants"
:value="key"
>{{ key }}</a-select-option
>
</a-select>
<template #footer>
<a-space v-if="!page.change_storage.status.Active">
<a-button @click="page.change_storage.show = false">
{{
$t("components.admin.domains.mailstorage.mailboxes.cancel")
}}</a-button
>
<a-button
:loading="page.change_storage.loading"
@click="changeStorage"
type="primary"
>
{{
$t("components.admin.domains.mailstorage.mailboxes.change")
}}</a-button
>
</a-space>
</template>
</a-modal>
</template>
<script setup lang="ts">
import { notifyError } from "@/composables/alert";
import { apiFetch } from "@/composables/apiFetch";
import router from "@/router";
import Export from "@/components/admin/domains/mailstorage/Export.vue";
import {
DomainPlaceholder,
MailboxPlaceholder,
RouteAdminDomainsDomainMailStorageMailboxesExport,
} from "@/router/consts";
import {
DeleteOutlined,
DownOutlined,
ExclamationCircleOutlined,
HddOutlined,
LockOutlined,
} from "@ant-design/icons-vue";
import { Modal } from "ant-design-vue";
import type { ColumnType } from "ant-design-vue/es/table";
import { createVNode, onMounted, reactive } from "vue";
import { useI18n } from "vue-i18n";
import { useRoute } from "vue-router";
const route = useRoute();
const { t } = useI18n();
const sqlColumns: ColumnType<any>[] = [
{
title: t("components.admin.domains.mailstorage.mailboxes.col_email"),
dataIndex: "Email",
key: "email",
},
{
title: t("components.admin.domains.mailstorage.mailboxes.col_msg_count"),
dataIndex: "Count",
},
{
title: t("components.admin.domains.mailstorage.mailboxes.col_size"),
dataIndex: "Size",
},
{
title: t("components.admin.domains.mailstorage.mailboxes.col_quota"),
dataIndex: "Quota",
},
{
title: t("components.admin.domains.mailstorage.mailboxes.col_quota_used"),
dataIndex: "QuotaUsed",
},
{
title: t(
"components.admin.domains.mailstorage.mailboxes.col_recovery_size"
),
dataIndex: "RecoverySize",
},
{
title: t("components.admin.domains.mailstorage.mailboxes.col_table_prefix"),
dataIndex: "Prefix",
},
{
title: t(
"components.admin.domains.mailstorage.mailboxes.col_table_access_key"
),
dataIndex: "MultistorageAccessKey",
},
{
key: "action",
align: "center",
},
];
const maildirColumns: ColumnType<any>[] = [
{
title: t("components.admin.domains.mailstorage.mailboxes.col_email"),
dataIndex: "Email",
key: "email",
},
{
key: "action",
align: "center",
},
];
interface ChangeStorageStatus {
Active: boolean;
Error: string;
Percent: number;
}
const page = reactive<{
rename: {
show: boolean;
oldEmail: string;
newLp: string;
error: string;
loading: boolean;
};
export: {
show: boolean;
email: string;
};
change_storage: {
started: boolean;
show: boolean;
loading: boolean;
email: string;
new_access_key: string;
accessKeysVariants: string[];
status: ChangeStorageStatus;
timeout: number | undefined;
interval: number | undefined;
};
columns: ColumnType<any>[];
type: string;
multistorageAccessKeys: string[];
domain: string;
withSearch: boolean;
loading: boolean;
data: any;
pagination: {
current: number;
total: number;
size: number;
search: string;
access_key: string;
};
}>({
rename: {
show: false,
oldEmail: "",
newLp: "",
error: "",
loading: false,
},
export: {
show: false,
email: "",
},
change_storage: {
started: false,
interval: 1000,
timeout: undefined,
show: false,
loading: false,
email: "",
new_access_key: "",
accessKeysVariants: [],
status: {
Active: false,
Error: "",
Percent: 0,
},
},
columns: [],
type: "",
multistorageAccessKeys: [],
domain: "",
withSearch: false,
pagination: {
current: 1,
total: 0,
size: 50,
search: "",
access_key: "",
},
loading: false,
data: [],
});
onMounted(() => {
page.domain = route.params.domain as string;
get();
});
async function get() {
page.withSearch = page.pagination.search !== "";
page.loading = true;
const params = new URLSearchParams();
params.append("page", String(page.pagination.current));
params.append("size", String(page.pagination.size));
params.append("search", page.pagination.search);
params.append("access_key", page.pagination.access_key);
const res = await apiFetch(
`/admin/domains/${page.domain}/mailstorage/mailboxes?` + params.toString()
);
page.loading = false;
if (res.error) {
notifyError(res.error);
return;
}
page.type = res.data.Type;
page.multistorageAccessKeys = res.data.MultistorageAccessKeys;
if (page.type === "maildirStorage") {
page.columns = maildirColumns as [];
} else {
page.columns = sqlColumns as [];
}
page.data = res.data.Mailboxes;
page.pagination.total = res.total;
return;
}
function applySearch() {
page.pagination.current = 1;
return get();
}
function pageChange(current: number) {
page.pagination.current = current;
return get();
}
async function clearRecovery(email: string) {
const params = new URLSearchParams();
params.append("search", page.pagination.search);
params.append("email", email);
const res = await apiFetch(
`/admin/domains/${page.domain}/mailstorage/mailboxes/clear_recovery?` +
params.toString(),
{
method: "POST",
}
);
if (res.error) {
notifyError(res.error);
return;
}
return get();
}
async function setArchiveMailbox(email: string, archive: boolean) {
const params = new URLSearchParams();
params.append("email", email);
params.append("archive", String(archive));
const res = await apiFetch(
`/admin/domains/${page.domain}/mailstorage/mailboxes/set_archive?` +
params.toString(),
{
method: "POST",
}
);
if (res.error) {
notifyError(res.error);
return;
}
return get();
}
function showRenameWindow(email: string) {
page.rename.oldEmail = email;
page.rename.newLp = email.split("@")[0];
page.rename.show = true;
page.rename.error = "";
}
async function renameMailbox() {
const params = new URLSearchParams();
params.append("to", page.rename.newLp);
page.rename.loading = true;
const res = await apiFetch(
`/admin/domains/${page.domain}/mailstorage/mailboxes/${page.rename.oldEmail}/rename?` +
params.toString(),
{
method: "POST",
}
);
page.rename.loading = false;
if (res.error) {
if (res.error.includes("exists")) {
page.rename.error = t(
"components.admin.domains.mailstorage.mailboxes.rename_exists"
);
} else {
notifyError(res.error);
}
return;
}
page.rename.show = false;
return get();
}
async function deleteMailbox(title: string, email: string) {
Modal.confirm({
title: title,
icon: createVNode(ExclamationCircleOutlined),
async onOk() {
const params = new URLSearchParams();
params.append("search", page.pagination.search);
params.append("email", email);
const res = await apiFetch(
`/admin/domains/${page.domain}/mailstorage/mailboxes?` +
params.toString(),
{
method: "DELETE",
}
);
if (res.error) {
notifyError(res.error);
return;
}
return get();
},
});
}
async function showChangeStorage(email: string, mailboxAccessKey: string) {
page.change_storage.loading = false;
page.change_storage.email = email;
page.change_storage.new_access_key = "";
page.change_storage.accessKeysVariants = [];
for (let i = 0; i < page.multistorageAccessKeys.length; i++) {
const element = page.multistorageAccessKeys[i];
if (element != mailboxAccessKey) {
if (page.change_storage.new_access_key === "") {
page.change_storage.new_access_key = element;
}
page.change_storage.accessKeysVariants.push(element);
}
}
page.change_storage.show = await getChangeStorage();
}
async function getChangeStorage(): Promise<boolean> {
clearTimeout(page.change_storage.timeout);
const res = await apiFetch(
`/admin/domains/${page.domain}/mailstorage/mailboxes/${page.change_storage.email}/change_storage`
);
if (res.error) {
notifyError(res.error);
return false;
}
if (res.data.Percent === 100 && res.data.Error === "") {
router.go(0);
return false;
}
page.change_storage.status = res.data;
if (page.change_storage.status.Active && page.change_storage.interval) {
page.change_storage.timeout = setTimeout(
getChangeStorage,
page.change_storage.interval
);
} else if (page.change_storage.started) {
router.go(0);
return false;
}
return true;
}
async function changeStorage() {
const params = new URLSearchParams();
params.append("new_access_key", page.change_storage.new_access_key);
page.change_storage.loading = true;
const res = await apiFetch(
`/admin/domains/${page.domain}/mailstorage/mailboxes/${page.change_storage.email}/change_storage?` +
params.toString(),
{
method: "POST",
}
);
page.change_storage.loading = false;
if (res.error) {
notifyError(res.error);
return;
}
page.change_storage.started = true;
getChangeStorage();
return;
}
</script>
<style scoped>
.ant-table-striped :deep(.striped) td {
opacity: 1;
}
</style>

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,328 @@
<template>
<div>
<a-table
class="panel-content"
:columns="columns"
:data-source="page.data"
:loading="page.loading"
bordered
small
:pagination="false"
>
<template #title>
<a-row>
<a-col style="display: flex; align-items: flex-end" :span="18">
<a-space>
<a-input-search
style="width: 300px"
v-model:value="page.pagination.search"
:placeholder="t('common.misc.search')"
enter-button
allow-clear
@search="get"
/>
</a-space>
</a-col>
<a-col :span="6">
<a-space style="display: flex; justify-content: end">
<a-button type="primary" @click="newCategory">
<PlusOutlined />
{{
t(
"components.admin.domains.resources.categories.add_category"
)
}}
</a-button>
</a-space>
</a-col>
</a-row>
</template>
<template #bodyCell="{ column, record }">
<template v-if="column.key === 'action'">
<a-space>
<a-button @click="editCategory(record)">{{
t("common.misc.edit")
}}</a-button>
<a-popconfirm
:title="t('common.misc.delete') + '?'"
:ok-text="t('common.misc.ok')"
:cancel-text="t('common.misc.cancel')"
@confirm="deleteCategory(record.Id)"
>
<a-button danger>{{ t("common.misc.delete") }}</a-button>
</a-popconfirm>
</a-space>
</template>
<template v-if="column.key === 'office_bind'">
{{ record.OfficeBind ? t("common.misc.yes") : t("common.misc.no") }}
</template>
<template v-if="column.key === 'script'">
{{ record.Script ? t("common.misc.yes") : t("common.misc.no") }}
</template>
</template>
<template #footer>
<a-flex justify="flex-end">
<a-pagination
v-model:value="page.pagination.current"
:defaultPageSize="page.pagination.size"
:total="page.pagination.total"
@change="pageChange"
:pageSizeOptions="['10', '20', '50', '100']"
v-model:pageSize="page.pagination.size"
:showSizeChanger="true"
/>
</a-flex>
</template>
</a-table>
</div>
<a-modal
style="width: 400px"
v-model:open="page.addUpdate.show"
:title="
page.addUpdate.Id
? t(`components.admin.domains.resources.categories.edit_category`)
: t(`components.admin.domains.resources.categories.add_category`)
"
>
<a-divider></a-divider>
<a-form :label-col="labelCol" :wrapper-col="wrapperCol" labelWrap>
<a-form-item
:label="$t('components.admin.domains.resources.categories.col_name')"
:rules="[{ required: true }]"
>
<a-input v-model:value="page.addUpdate.Name"> </a-input>
</a-form-item>
<a-form-item
:label="
t('components.admin.domains.resources.categories.col_office_bind')
"
>
<a-switch v-model:checked="page.addUpdate.OfficeBind"></a-switch>
</a-form-item>
<a-form-item
:label="t('components.admin.domains.resources.categories.col_script')"
>
<a-switch v-model:checked="page.addUpdate.Script"></a-switch>
</a-form-item>
</a-form>
<template #footer>
<a-button
type="primary"
:disabled="!page.addUpdate.Name"
@click="addUpdateCategory"
:loading="page.addUpdate.loading"
>
{{
page.addUpdate.Id
? t("common.misc.save_changes")
: t("components.admin.domains.resources.categories.add_category")
}}
</a-button>
</template>
</a-modal>
</template>
<script setup lang="ts">
import { notifyError, notifySuccess } from "@/composables/alert";
import { apiFetch } from "@/composables/apiFetch";
import router from "@/router";
import dayjs, { Dayjs } from "dayjs";
import Export from "@/components/admin/domains/mailstorage/Export.vue";
import {
DomainPlaceholder,
EventIdPlaceholder,
MailboxPlaceholder,
RouteAdminDomainsDomainMailStorageMailboxesExport,
RouteUserCalendarsEventsPlannerEdit,
RouteUserCalendarsEventsPlannerNew,
} from "@/router/consts";
import {
ArrowLeftOutlined,
CheckOutlined,
DeleteOutlined,
DownOutlined,
ExclamationCircleOutlined,
HddOutlined,
HomeOutlined,
PlusOutlined,
} from "@ant-design/icons-vue";
import { Modal } from "ant-design-vue";
import type { ColumnType } from "ant-design-vue/es/table";
import { createVNode, onMounted, reactive } from "vue";
import { useI18n } from "vue-i18n";
import { useRoute } from "vue-router";
import { timeToDateTime } from "@/composables/misc";
import TimezoneSelect from "@/components/common/TimezoneSelect.vue";
import WorkDaysSelect from "@/components/common/WorkDaysSelect.vue";
import WorkHoursRangePicker from "@/components/common/WorkHoursRangePicker.vue";
const route = useRoute();
const { t } = useI18n();
const labelCol = { span: 10, style: {} };
const wrapperCol = { span: 14, style: {} };
const columns: ColumnType<any>[] = [
{
title: t("components.admin.domains.resources.categories.col_name"),
dataIndex: "Name",
},
{
title: t("components.admin.domains.resources.categories.col_office_bind"),
key: "office_bind",
},
{
title: t("components.admin.domains.resources.categories.col_script"),
key: "script",
},
{
title: "",
key: "action",
},
];
const page = reactive<{
domain: string;
loading: boolean;
data: any[];
pagination: {
current: number;
total: number;
size: number;
search: string;
};
addUpdate: {
show: boolean;
Id: number;
Name: string;
OfficeBind: boolean;
Script: boolean;
loading: boolean;
};
}>({
domain: "",
pagination: {
current: 1,
total: 0,
size: 50,
search: "",
},
loading: false,
data: [],
addUpdate: {
loading: false,
show: false,
Id: 0,
Name: "",
OfficeBind: false,
Script: false,
},
});
onMounted(() => {
page.domain = route.params.domain as string;
get();
});
async function get() {
page.loading = true;
const params = new URLSearchParams();
params.append("page", String(page.pagination.current));
params.append("size", String(page.pagination.size));
params.append("search", page.pagination.search);
const res = await apiFetch(
`/admin/domains/${page.domain}/resources/categories?` + params.toString()
);
page.loading = false;
if (res.error) {
notifyError(res.error);
return;
}
page.data = res.data;
page.pagination.total = res.total;
return;
}
function pageChange(current: number) {
page.pagination.current = current;
return get();
}
async function deleteCategory(id: number) {
const res = await apiFetch(
`/admin/domains/${page.domain}/resources/categories/${id}`,
{
method: "DELETE",
}
);
if (res.error) {
notifyError(res.error);
return;
}
notifySuccess(
t("components.admin.domains.resources.categories.delete_success")
);
return get();
}
async function editCategory(record: any) {
page.addUpdate = { ...record };
page.addUpdate.show = true;
}
async function addUpdateCategory() {
page.addUpdate.loading = true;
const res = await apiFetch(
`/admin/domains/${page.domain}/resources/categories`,
{
method: "POST",
body: page.addUpdate,
}
);
page.addUpdate.loading = false;
if (res.error) {
notifyError(res.error);
return;
}
if (page.addUpdate.Id !== 0) {
notifySuccess(
t("components.admin.domains.resources.categories.update_success")
);
} else {
notifySuccess(
t("components.admin.domains.resources.categories.create_success")
);
}
page.addUpdate.show = false;
get();
}
async function newCategory() {
page.addUpdate.Id = 0;
page.addUpdate.Name = "";
page.addUpdate.OfficeBind = false;
page.addUpdate.Script = false;
page.addUpdate.show = true;
}
</script>
<style scoped>
.panel-content {
min-width: 600px;
max-width: 60%;
margin: auto;
}
</style>

View File

@ -0,0 +1,352 @@
<template>
<div>
<a-table
class="panel-content"
:columns="columns"
:data-source="page.data"
:loading="page.loading"
bordered
small
:pagination="false"
>
<template #title>
<a-row>
<a-col style="display: flex; align-items: flex-end" :span="18">
<a-space>
<a-input-search
style="width: 300px"
v-model:value="page.pagination.search"
:placeholder="t('common.misc.search')"
enter-button
allow-clear
@search="get"
/>
</a-space>
</a-col>
<a-col :span="6">
<a-space style="display: flex; justify-content: end">
<a-button type="primary" @click="newOffice">
<PlusOutlined />
{{ t("components.admin.domains.resources.offices.add_office") }}
</a-button>
</a-space>
</a-col>
</a-row>
</template>
<template #bodyCell="{ column, record }">
<template v-if="column.key === 'action'">
<a-space>
<a-button @click="editOffice(record)">{{
t("common.misc.edit")
}}</a-button>
<a-popconfirm
:title="t('common.misc.delete') + '?'"
:ok-text="t('common.misc.ok')"
:cancel-text="t('common.misc.cancel')"
@confirm="deleteOffice(record.Id)"
>
<a-button danger>{{ t("common.misc.delete") }}</a-button>
</a-popconfirm>
</a-space>
</template>
<template v-if="column.key === 'timezone'">
UTC{{ record.Timezone }}
</template>
</template>
<template #footer>
<a-flex justify="flex-end">
<a-pagination
v-model:value="page.pagination.current"
:defaultPageSize="page.pagination.size"
:total="page.pagination.total"
@change="pageChange"
:pageSizeOptions="['10', '20', '50', '100']"
v-model:pageSize="page.pagination.size"
:showSizeChanger="true"
/>
</a-flex>
</template>
</a-table>
</div>
<a-modal
style="width: 400px"
v-model:open="page.addUpdate.show"
:title="
page.addUpdate.Id
? t(`components.admin.domains.resources.offices.edit_office`)
: t(`components.admin.domains.resources.offices.add_office`)
"
>
<a-divider></a-divider>
<a-form :label-col="labelCol" :wrapper-col="wrapperCol" labelWrap>
<a-form-item
:label="$t('components.admin.domains.resources.offices.col_name')"
:rules="[{ required: true }]"
>
<a-input v-model:value="page.addUpdate.Name"> </a-input>
</a-form-item>
<a-form-item
:label="t('components.user.calendars.share_free_time.timezone')"
>
<TimezoneSelect v-model:value="page.addUpdate.Timezone">
</TimezoneSelect>
</a-form-item>
<a-form-item
:label="t('components.user.calendars.share_free_time.work_days')"
>
<WorkDaysSelect v-model:value="page.addUpdate.WorkDays">
</WorkDaysSelect>
</a-form-item>
<a-form-item
:label="t('components.user.calendars.share_free_time.work_hours')"
>
<WorkHoursRangePicker v-model:value="page.addUpdate.workTimeValues">
</WorkHoursRangePicker>
</a-form-item>
</a-form>
<template #footer>
<a-button
type="primary"
:disabled="!page.addUpdate.Name"
@click="addUpdateOffice"
:loading="page.addUpdate.loading"
>
{{
page.addUpdate.Id
? t("common.misc.save_changes")
: t("components.admin.domains.resources.offices.add_office")
}}
</a-button>
</template>
</a-modal>
</template>
<script setup lang="ts">
import { notifyError, notifySuccess } from "@/composables/alert";
import { apiFetch } from "@/composables/apiFetch";
import router from "@/router";
import dayjs, { Dayjs } from "dayjs";
import Export from "@/components/admin/domains/mailstorage/Export.vue";
import {
DomainPlaceholder,
EventIdPlaceholder,
MailboxPlaceholder,
RouteAdminDomainsDomainMailStorageMailboxesExport,
RouteUserCalendarsEventsPlannerEdit,
RouteUserCalendarsEventsPlannerNew,
} from "@/router/consts";
import {
ArrowLeftOutlined,
DeleteOutlined,
DownOutlined,
ExclamationCircleOutlined,
HddOutlined,
HomeOutlined,
PlusOutlined,
} from "@ant-design/icons-vue";
import { Modal } from "ant-design-vue";
import type { ColumnType } from "ant-design-vue/es/table";
import { createVNode, onMounted, reactive } from "vue";
import { useI18n } from "vue-i18n";
import { useRoute } from "vue-router";
import { timeToDateTime } from "@/composables/misc";
import TimezoneSelect from "@/components/common/TimezoneSelect.vue";
import WorkDaysSelect from "@/components/common/WorkDaysSelect.vue";
import WorkHoursRangePicker from "@/components/common/WorkHoursRangePicker.vue";
const route = useRoute();
const { t } = useI18n();
const labelCol = { span: 8, style: {} };
const wrapperCol = { span: 16, style: {} };
const columns: ColumnType<any>[] = [
{
title: t("components.admin.domains.resources.offices.col_name"),
dataIndex: "Name",
},
{
title: t("components.admin.domains.resources.offices.col_timezone"),
key: "timezone",
},
{
title: "",
key: "action",
},
];
const page = reactive<{
domain: string;
loading: boolean;
data: any[];
pagination: {
current: number;
total: number;
size: number;
search: string;
};
addUpdate: {
show: boolean;
Id: number;
Name: string;
Timezone: string;
WorkDays: string[];
WorkTime: string[];
workTimeValues: any[];
loading: boolean;
};
}>({
domain: "",
pagination: {
current: 1,
total: 0,
size: 50,
search: "",
},
loading: false,
data: [],
addUpdate: {
loading: false,
show: false,
Id: 0,
Name: "",
Timezone: "",
WorkDays: [],
WorkTime: [],
workTimeValues: [],
},
});
onMounted(() => {
page.domain = route.params.domain as string;
get();
});
async function get() {
page.loading = true;
const params = new URLSearchParams();
params.append("page", String(page.pagination.current));
params.append("size", String(page.pagination.size));
params.append("search", page.pagination.search);
const res = await apiFetch(
`/admin/domains/${page.domain}/resources/offices?` + params.toString()
);
page.loading = false;
if (res.error) {
notifyError(res.error);
return;
}
page.data = res.data;
page.pagination.total = res.total;
return;
}
function pageChange(current: number) {
page.pagination.current = current;
return get();
}
async function deleteOffice(id: number) {
const res = await apiFetch(
`/admin/domains/${page.domain}/resources/offices/${id}`,
{
method: "DELETE",
}
);
if (res.error) {
notifyError(res.error);
return;
}
notifySuccess(t("components.admin.domains.resources.offices.delete_success"));
return get();
}
async function editOffice(record: any) {
page.addUpdate = { ...record };
page.addUpdate.workTimeValues = [
dayjs(Date.parse("1970-01-01 " + page.addUpdate.WorkTime[0])),
dayjs(Date.parse("1970-01-01 " + page.addUpdate.WorkTime[1])),
];
page.addUpdate.show = true;
}
async function addUpdateOffice() {
page.addUpdate.WorkTime[0] = page.addUpdate.workTimeValues[0].format("HH:mm");
page.addUpdate.WorkTime[1] = page.addUpdate.workTimeValues[1].format("HH:mm");
page.addUpdate.loading = true;
const res = await apiFetch(
`/admin/domains/${page.domain}/resources/offices`,
{
method: "POST",
body: page.addUpdate,
}
);
page.addUpdate.loading = false;
if (res.error) {
notifyError(res.error);
return;
}
if (page.addUpdate.Id !== 0) {
notifySuccess(
t("components.admin.domains.resources.offices.update_success")
);
} else {
notifySuccess(
t("components.admin.domains.resources.offices.create_success")
);
}
page.addUpdate.show = false;
get();
}
function getStringTimezone(): string {
let tz = new Date().getTimezoneOffset();
tz = tz / -60;
let str = "+";
if (tz < 0) {
str = "-";
tz *= -1;
}
if (tz < 10) {
str += "0";
}
return str + tz.toString();
}
async function newOffice() {
page.addUpdate.Id = 0;
page.addUpdate.Name = "";
page.addUpdate.Timezone = getStringTimezone();
page.addUpdate.WorkDays = ["1", "2", "3", "4", "5"];
page.addUpdate.WorkTime = ["10:00", "19:00"];
page.addUpdate.workTimeValues = [
dayjs(Date.parse("1970-01-01 " + page.addUpdate.WorkTime[0])),
dayjs(Date.parse("1970-01-01 " + page.addUpdate.WorkTime[1])),
];
page.addUpdate.show = true;
}
</script>
<style scoped>
.panel-content {
min-width: 400px;
max-width: 40%;
margin: auto;
}
</style>

View File

@ -0,0 +1,608 @@
<template>
<div>
<a-table
class="panel-content"
:columns="columns"
:data-source="page.data"
:loading="page.loading"
bordered
small
:pagination="false"
>
<template #title>
<a-row>
<a-col style="display: flex; align-items: flex-end" :span="18">
<a-space>
<a-input-search
style="width: 300px"
v-model:value="page.pagination.search"
:placeholder="t('common.misc.search')"
enter-button
allow-clear
@search="get"
/>
<a-select v-model:value="page.pagination.category" @change="get">
<a-select-option :value="''">{{
t("components.admin.domains.resources.resources.any_category")
}}</a-select-option>
<a-select-option
v-for="cat in page.categories"
:value="cat.Id.toString()"
>{{ cat.Name }}</a-select-option
>
</a-select>
<a-select v-model:value="page.pagination.office" @change="get">
<a-select-option :value="''">{{
t("components.admin.domains.resources.resources.any_office")
}}</a-select-option>
<a-select-option
v-for="office in page.offices"
:value="office.Id.toString()"
>{{ office.Name }}</a-select-option
>
</a-select>
</a-space>
</a-col>
<a-col :span="6">
<a-space style="display: flex; justify-content: end">
<a-button type="primary" @click="newResource">
<PlusOutlined />
{{
t("components.admin.domains.resources.resources.add_resource")
}}
</a-button>
</a-space>
</a-col>
</a-row>
</template>
<template #bodyCell="{ column, record }">
<template v-if="column.key === 'action'">
<a-space>
<a-button type="primary" @click="showAccessModal(record.Id)">{{
t("components.common.shared_calendars.edit_cal_access")
}}</a-button>
<a-button @click="editResource(record)">{{
t("common.misc.edit")
}}</a-button>
<a-popconfirm
:title="t('common.misc.delete') + '?'"
:ok-text="t('common.misc.ok')"
:cancel-text="t('common.misc.cancel')"
@confirm="deleteResource(record.Id)"
>
<a-button danger>{{ t("common.misc.delete") }}</a-button>
</a-popconfirm>
</a-space>
</template>
</template>
<template #footer>
<a-flex justify="flex-end">
<a-pagination
v-model:value="page.pagination.current"
:defaultPageSize="page.pagination.size"
:total="page.pagination.total"
@change="pageChange"
:pageSizeOptions="['10', '20', '50', '100']"
v-model:pageSize="page.pagination.size"
:showSizeChanger="true"
/>
</a-flex>
</template>
</a-table>
</div>
<a-modal
:width="page.addUpdate.hasScript ? 800 : 500"
v-model:open="page.addUpdate.show"
:title="
page.addUpdate.Id
? t(`components.admin.domains.resources.resources.edit_resource`)
: t(`components.admin.domains.resources.resources.add_resource`)
"
>
<a-divider></a-divider>
<a-form :label-col="labelCol" :wrapper-col="wrapperCol" labelWrap>
<a-form-item
:label="t('components.admin.domains.resources.resources.col_category')"
>
<a-select
v-model:value="page.addUpdate.categoryIdStr"
@change="updateCatMeta"
>
<a-select-option
v-for="cat in page.categories"
:value="cat.Id.toString()"
>{{ cat.Name }}</a-select-option
>
</a-select>
</a-form-item>
<a-form-item
v-if="page.addUpdate.officeBind"
:label="t('components.admin.domains.resources.resources.col_office')"
>
<a-select v-model:value="page.addUpdate.officeIdStr">
<a-select-option
v-for="office in page.offices"
:value="office.Id.toString()"
>{{ office.Name }}</a-select-option
>
</a-select>
</a-form-item>
<a-form-item
v-if="page.addUpdate.hasScript"
:label="$t('components.admin.domains.resources.resources.col_script')"
>
<a-flex gap="middle" vertical>
<a-alert
v-if="page.addUpdate.checkScript.error !== null"
:type="page.addUpdate.checkScript.error ? 'error' : 'success'"
show-icon
>
<template #message>
<div style="white-space: pre-line; font-family: monospace">
{{
page.addUpdate.checkScript.error
? page.addUpdate.checkScript.error
: $t(
"components.admin.domains.resources.resources.check_script_good"
)
}}
</div>
</template>
</a-alert>
<a-textarea v-model:value="page.addUpdate.Script"> </a-textarea>
</a-flex>
</a-form-item>
<template
v-if="
page.addUpdate.categoryIdStr &&
(!page.addUpdate.officeBind || page.addUpdate.officeIdStr)
"
>
<a-form-item
:label="
$t('components.admin.domains.resources.resources.col_show_time_as')
"
>
<a-select v-model:value="page.addUpdate.transparentCalObjectsStr">
<a-select-option value="false">{{
$t("components.admin.domains.resources.resources.time_busy")
}}</a-select-option>
<a-select-option value="true">{{
$t("components.admin.domains.resources.resources.time_free")
}}</a-select-option>
</a-select>
</a-form-item>
<a-form-item
:label="$t('components.admin.domains.resources.resources.col_name')"
:rules="[{ required: true }]"
>
<a-input v-model:value="page.addUpdate.Name"> </a-input>
</a-form-item>
</template>
</a-form>
<template #footer>
<a-space>
<a-button
v-if="page.addUpdate.hasScript"
@click="checkScript"
:loading="page.addUpdate.checkScript.loading"
>
{{ t("components.admin.domains.resources.resources.check_script") }}
</a-button>
<a-button
type="primary"
:disabled="
!page.addUpdate.Name ||
!page.addUpdate.categoryIdStr ||
(page.addUpdate.officeBind && !page.addUpdate.officeIdStr)
"
@click="addUpdateResource"
:loading="page.addUpdate.loading"
>
{{
page.addUpdate.Id
? t("common.misc.save_changes")
: t("components.admin.domains.resources.resources.add_resource")
}}
</a-button>
</a-space>
</template>
</a-modal>
<CalendarsAccess
v-if="page.access.show"
v-model:open="page.access.show"
:id="page.access.Id"
:domain="page.domain"
:type="'group'"
:resources="true"
>
</CalendarsAccess>
</template>
<script setup lang="ts">
import { notifyError, notifySuccess } from "@/composables/alert";
import { apiFetch } from "@/composables/apiFetch";
import router from "@/router";
import dayjs, { Dayjs } from "dayjs";
import Export from "@/components/admin/domains/mailstorage/Export.vue";
import {
DomainPlaceholder,
EventIdPlaceholder,
MailboxPlaceholder,
RouteAdminDomainsDomainMailStorageMailboxesExport,
RouteUserCalendarsEventsPlannerEdit,
RouteUserCalendarsEventsPlannerNew,
} from "@/router/consts";
import {
ArrowLeftOutlined,
CheckOutlined,
DeleteOutlined,
DownOutlined,
ExclamationCircleOutlined,
HddOutlined,
HomeOutlined,
PlusOutlined,
} from "@ant-design/icons-vue";
import { Modal } from "ant-design-vue";
import type { ColumnType } from "ant-design-vue/es/table";
import { createVNode, onMounted, reactive } from "vue";
import { useI18n } from "vue-i18n";
import { useRoute } from "vue-router";
import { timeToDateTime } from "@/composables/misc";
import TimezoneSelect from "@/components/common/TimezoneSelect.vue";
import WorkDaysSelect from "@/components/common/WorkDaysSelect.vue";
import WorkHoursRangePicker from "@/components/common/WorkHoursRangePicker.vue";
import { _cf } from "ant-design-vue/es/_util/cssinjs/hooks/useStyleRegister";
import CalendarsAccess from "@/components/common/CalendarsAccess.vue";
const route = useRoute();
const { t } = useI18n();
const labelCol = { span: 6, style: {} };
const wrapperCol = { span: 18, style: {} };
const columns: ColumnType<any>[] = [
{
title: t("components.admin.domains.resources.resources.col_name"),
dataIndex: "Name",
},
{
title: t("components.admin.domains.resources.resources.col_category"),
dataIndex: "CategoryName",
},
{
title: t("components.admin.domains.resources.resources.col_office"),
dataIndex: "OfficeName",
},
{
title: "",
key: "action",
},
];
interface Category {
Id: number;
Name: string;
OfficeBind: boolean;
Script: boolean;
}
interface Office {
Id: number;
Name: string;
}
const page = reactive<{
domain: string;
loading: boolean;
data: any[];
categories: Category[];
catId2Name: Map<number, string>;
offices: Office[];
officeId2Name: Map<number, string>;
pagination: {
current: number;
total: number;
size: number;
search: string;
category: string;
office: string;
};
addUpdate: {
show: boolean;
Id: number;
Name: string;
CategoryId?: number;
categoryIdStr: string;
OfficeId?: number;
officeIdStr: string;
Script: string | null;
TransparentCalObjects: boolean;
transparentCalObjectsStr: string;
loading: boolean;
officeBind: boolean;
hasScript: boolean;
checkScript: {
loading: boolean;
error: string | null;
};
};
access: {
show: boolean;
Id: number;
};
}>({
domain: "",
pagination: {
current: 1,
total: 0,
size: 50,
search: "",
category: "",
office: "",
},
loading: false,
data: [],
categories: [],
catId2Name: new Map(),
officeId2Name: new Map(),
offices: [],
addUpdate: {
show: false,
Id: 0,
Name: "",
CategoryId: 0,
categoryIdStr: "",
OfficeId: 0,
officeIdStr: "",
Script: "",
loading: false,
officeBind: false,
hasScript: false,
TransparentCalObjects: false,
transparentCalObjectsStr: "false",
checkScript: {
loading: false,
error: null,
},
},
access: {
show: false,
Id: 0,
},
});
onMounted(() => {
page.domain = route.params.domain as string;
load();
});
async function load() {
await loadCategoriesAndOffices();
get();
}
async function loadCategoriesAndOffices() {
loadCategories();
loadOffices();
}
async function loadCategories() {
const res = await apiFetch(
`/admin/domains/${page.domain}/resources/categories`
);
if (res.error) {
notifyError(res.error);
return;
}
page.categories = res.data;
page.categories.forEach((cat) => {
page.catId2Name.set(cat.Id, cat.Name);
});
}
async function loadOffices() {
const res = await apiFetch(`/admin/domains/${page.domain}/resources/offices`);
if (res.error) {
notifyError(res.error);
return;
}
page.offices = res.data;
page.offices.forEach((office) => {
page.officeId2Name.set(office.Id, office.Name);
});
}
async function get() {
page.loading = true;
const params = new URLSearchParams();
params.append("page", String(page.pagination.current));
params.append("size", String(page.pagination.size));
params.append("search", page.pagination.search);
params.append("category", page.pagination.category);
params.append("office", page.pagination.office);
const res = await apiFetch(
`/admin/domains/${page.domain}/resources/resources?` + params.toString()
);
page.loading = false;
if (res.error) {
notifyError(res.error);
return;
}
page.data = res.data;
page.data.forEach((e) => {
e.CategoryName = page.catId2Name.get(e.CategoryId);
e.OfficeName = page.officeId2Name.get(e.OfficeId);
});
page.pagination.total = res.total;
return;
}
function pageChange(current: number) {
page.pagination.current = current;
return get();
}
async function deleteResource(id: number) {
const res = await apiFetch(
`/admin/domains/${page.domain}/resources/resources/${id}`,
{
method: "DELETE",
}
);
if (res.error) {
notifyError(res.error);
return;
}
notifySuccess(
t("components.admin.domains.resources.resources.delete_success")
);
return get();
}
async function editResource(record: any) {
page.addUpdate = { ...record };
page.addUpdate.checkScript = {
loading: false,
error: null,
};
page.addUpdate.categoryIdStr = "";
if (page.addUpdate.CategoryId) {
page.addUpdate.categoryIdStr = page.addUpdate.CategoryId.toString();
}
page.addUpdate.officeIdStr = "";
if (page.addUpdate.OfficeId) {
page.addUpdate.officeIdStr = page.addUpdate.OfficeId.toString();
}
page.addUpdate.transparentCalObjectsStr = String(
page.addUpdate.TransparentCalObjects
);
updateCatMeta();
page.addUpdate.show = true;
}
async function addUpdateResource() {
page.addUpdate.CategoryId = undefined;
if (page.addUpdate.categoryIdStr) {
page.addUpdate.CategoryId = Number(page.addUpdate.categoryIdStr);
}
page.addUpdate.OfficeId = undefined;
if (page.addUpdate.officeIdStr) {
page.addUpdate.OfficeId = Number(page.addUpdate.officeIdStr);
}
if (!page.addUpdate.hasScript) {
page.addUpdate.Script = null;
}
page.addUpdate.TransparentCalObjects = Boolean(
page.addUpdate.transparentCalObjectsStr
);
page.addUpdate.loading = true;
const res = await apiFetch(
`/admin/domains/${page.domain}/resources/resources`,
{
method: "POST",
body: page.addUpdate,
}
);
page.addUpdate.loading = false;
if (res.error) {
notifyError(res.error);
return;
}
if (page.addUpdate.Id !== 0) {
notifySuccess(
t("components.admin.domains.resources.resources.update_success")
);
} else {
notifySuccess(
t("components.admin.domains.resources.resources.create_success")
);
}
page.addUpdate.show = false;
get();
}
async function checkScript() {
page.addUpdate.checkScript.loading = true;
const res = await apiFetch(
`/admin/domains/${page.domain}/resources/resources/check_script`,
{
method: "POST",
body: page.addUpdate.Script,
}
);
page.addUpdate.checkScript.loading = false;
if (res.error) {
notifyError(res.error);
return;
}
page.addUpdate.checkScript.error = res.data.error;
}
function updateCatMeta() {
page.addUpdate.officeBind = false;
page.addUpdate.hasScript = false;
for (let i = 0; i < page.categories.length; i++) {
const cat = page.categories[i];
if (cat.Id.toString() === page.addUpdate.categoryIdStr) {
page.addUpdate.officeBind = cat.OfficeBind;
page.addUpdate.hasScript = cat.Script;
break;
}
}
}
async function newResource() {
page.addUpdate.Id = 0;
page.addUpdate.Name = "";
page.addUpdate.CategoryId = undefined;
page.addUpdate.categoryIdStr = "";
page.addUpdate.OfficeId = undefined;
page.addUpdate.officeIdStr = "";
page.addUpdate.officeBind = false;
page.addUpdate.hasScript = false;
page.addUpdate.Script = null;
page.addUpdate.show = true;
page.addUpdate.checkScript.loading = false;
page.addUpdate.checkScript.error = null;
page.addUpdate.TransparentCalObjects = false;
page.addUpdate.transparentCalObjectsStr = "false";
}
async function showAccessModal(id: number) {
page.access.Id = id;
page.access.show = true;
}
</script>
<style scoped>
.panel-content {
min-width: 800px;
max-width: 80%;
margin: auto;
}
</style>

View File

@ -0,0 +1,115 @@
<template>
<div>
<a-card class="panel-content">
<a-form :label-col="labelCol" :wrapper-col="wrapperCol" :labelWrap="true">
<a-form-item
:label="$t('components.admin.domains.userdb.add.Name')"
:rules="[{ required: true }]"
>
<a-input v-model:value="page.new.Name"> </a-input>
</a-form-item>
<a-form-item
:label="$t('components.admin.domains.userdb.add.Type')"
:rules="[{ required: true }]"
>
<a-select v-model:value="page.new.Type">
<a-select-option value="ldapUserDB">LDAP</a-select-option>
<a-select-option value="jsonFileUserDB">JSON File</a-select-option>
</a-select>
</a-form-item>
<a-button
:disabled="!page.new.Type || !page.new.Name"
class="save-button"
type="primary"
size="large"
@click="save"
>
{{ $t("components.admin.domains.userdb.add.create") }}
</a-button>
</a-form>
</a-card>
</div>
</template>
<script setup lang="ts">
import { notifyError, notifySuccess } from "@/composables/alert";
import { apiFetch } from "@/composables/apiFetch";
import { ExclamationCircleOutlined } from "@ant-design/icons-vue";
import { Modal } from "ant-design-vue";
import { createVNode, onMounted, reactive } from "vue";
import { saveAndRestart } from "@/composables/restart";
import { message } from "ant-design-vue";
import { useI18n } from "vue-i18n";
import router from "@/router";
import {
DomainPlaceholder,
RouteAdminDomainsDomainMailStorageSettings,
RouteAdminDomainsDomainUserDBProviderSettings,
UserdbPlaceholder,
} from "@/router/consts";
import { useRoute } from "vue-router";
const route = useRoute();
const { t } = useI18n();
const page = reactive<{
new: {
Type: string;
Name: string;
};
domain: string;
loading: boolean;
}>({
loading: false,
new: {
Type: "",
Name: "",
},
domain: "",
});
const labelCol = { span: 8 };
const wrapperCol = { span: 16 };
onMounted(() => {
page.domain = route.params.domain as string;
});
async function save() {
const res = await apiFetch(`/admin/domains/${page.domain}/userdb/add`, {
method: "POST",
body: page.new,
});
if (res.error) {
notifyError(res.error);
return;
}
notifySuccess(t("components.admin.domains.userdb.add.success"));
router.push({
path: RouteAdminDomainsDomainUserDBProviderSettings.replace(
DomainPlaceholder,
page.domain
).replace(UserdbPlaceholder, res.data),
hash: "#refresh",
});
return;
}
</script>
<style scoped>
.panel-content {
width: 500px;
max-width: 50%;
margin: auto;
}
.save-button {
display: block;
margin: auto;
}
</style>

View File

@ -0,0 +1,434 @@
<template>
<a-table
class="panel-content"
:columns="columns"
:data-source="page.data"
:loading="page.loading"
bordered
size="small"
:pagination="false"
>
<template #bodyCell="{ column, record }">
<template v-if="column.key === 'action'">
<a-space>
<a-button type="primary" @click="showMembersModal(record)">{{
t("components.admin.domains.userdb.groups.members")
}}</a-button>
<a-button @click="showAddModal(record)">{{
t("components.admin.domains.userdb.groups.edit")
}}</a-button>
<a-popconfirm
:title="t('components.admin.domains.userdb.groups.delete') + '?'"
:ok-text="t('components.admin.domains.userdb.groups.ok')"
:cancel-text="t('components.admin.domains.userdb.groups.cancel')"
@confirm="deleteGroup(record.Gid)"
>
<a-button danger>{{
t("components.admin.domains.userdb.groups.delete")
}}</a-button>
</a-popconfirm>
</a-space>
</template>
<template v-if="column.key === 'users_count'">
{{ record.MemberUsers ? (record.MemberUsers as Array<any>).length : 0 }}
</template>
<template v-if="column.key === 'groups_count'">
{{ record.MemberGroups ? (record.MemberGroups as Array<any>).length : 0 }}
</template>
</template>
<template #title>
<div style="display: flex; justify-content: end">
<a-button type="primary" @click="showAddModal(undefined)">{{
$t("components.admin.domains.userdb.groups.add")
}}</a-button>
</div>
</template>
</a-table>
<a-modal
v-model:open="page.add.show"
:title="
page.add.Gid
? t(`components.admin.domains.userdb.groups.edit`) + ' ' + page.add.Name
: t(`components.admin.domains.userdb.groups.add`)
"
>
<a-divider></a-divider>
<a-form :label-col="labelCol" :wrapper-col="wrapperCol" :labelWrap="true">
<a-form-item
:label="$t('components.admin.domains.userdb.groups.add_modal.name')
"
:rules="[{ required: true }]"
>
<a-input v-model:value="page.add.Name"> </a-input>
</a-form-item>
<a-form-item
:label="$t('components.admin.domains.userdb.groups.add_modal.email')"
:rules="[{ required: true }]"
>
<a-input v-model:value="page.add.Email"> </a-input>
</a-form-item>
<a-form-item
:label="
$t('components.admin.domains.userdb.groups.add_modal.alt_emails')
"
>
<a-textarea v-model:value="page.add.AlternateEmailsStr"> </a-textarea>
</a-form-item>
</a-form>
<template #footer>
<a-space>
<a-button @click="page.add.show = false">
{{ $t("components.admin.domains.userdb.groups.add_modal.cancel") }}
</a-button>
<a-button
:disabled="!page.add.Name || !page.add.Email"
type="primary"
@click="add"
>
{{ $t("components.admin.domains.userdb.groups.add_modal.save") }}
</a-button>
</a-space>
</template>
</a-modal>
<a-modal v-model:open="page.members.show" style="width: 70%" @cancel="get">
<a-transfer
:disabled="page.members.disabled"
v-model:target-keys="page.members.users_ids"
:data-source="page.all_users"
:render="(item: any) => item.name"
show-search
:titles="[' (all users)', ' (users in group)']"
:list-style="{
width: '100%',
height: '320px',
}"
@change="changeUserMember"
/>
<br />
<a-transfer
:disabled="page.members.disabled"
v-model:target-keys="page.members.groups_ids"
:data-source="page.all_groups"
:render="(item: any) => item.name"
show-search
:titles="[' (all groups)', ' (groups in group)']"
:list-style="{
width: '100%',
height: '320px',
}"
@change="changeGroupMember"
/>
<template #footer> </template>
</a-modal>
</template>
<script setup lang="ts">
import { notifyError } from "@/composables/alert";
import { apiFetch } from "@/composables/apiFetch";
import router from "@/router";
import Export from "@/components/admin/domains/mailstorage/Export.vue";
import {
DomainPlaceholder,
MailboxPlaceholder,
RouteAdminDomainsDomainMailStorageMailboxesExport,
} from "@/router/consts";
import {
DeleteOutlined,
DownOutlined,
ExclamationCircleOutlined,
} from "@ant-design/icons-vue";
import { Modal } from "ant-design-vue";
import type { ColumnType } from "ant-design-vue/es/table";
import { createVNode, onMounted, reactive } from "vue";
import { useI18n } from "vue-i18n";
import { useRoute } from "vue-router";
const route = useRoute();
const { t } = useI18n();
const labelCol = { span: 8, };
const wrapperCol = { span: 16 };
const columns: ColumnType<any>[] = [
{
title: t("components.admin.domains.userdb.groups.col_name"),
dataIndex: "Name",
},
{
title: t("components.admin.domains.userdb.groups.col_email"),
dataIndex: "Email",
},
{
title: t("components.admin.domains.userdb.groups.col_users_count"),
key: "users_count",
},
{
title: t("components.admin.domains.userdb.groups.col_group_count"),
key: "groups_count",
},
{
key: "action",
align: "right",
},
];
interface TransferElem {
key: string;
name: string;
}
const page = reactive<{
domain: string;
sid: string;
loading: boolean;
data: any;
all_users: TransferElem[];
all_groups: TransferElem[];
members: {
show: boolean;
Name: string;
Gid: string;
users_ids: string[];
groups_ids: string[];
disabled: boolean;
};
add: {
show: boolean;
Gid: string;
Name: string;
Email: string;
AlternateEmails: string[];
AlternateEmailsStr: string;
};
}>({
domain: "",
sid: "",
loading: false,
data: [],
add: {
show: false,
Name: "",
Email: "",
AlternateEmails: [],
AlternateEmailsStr: "",
Gid: "",
},
members: {
show: false,
Name: "",
Gid: "",
users_ids: [],
groups_ids: [],
disabled: false,
},
all_users: [],
all_groups: [],
});
onMounted(() => {
page.sid = route.params.sid as string;
page.domain = route.params.domain as string;
get();
});
async function get() {
page.loading = true;
const res = await apiFetch(
`/admin/domains/${page.domain}/userdb/${page.sid}/groups`
);
page.loading = false;
if (res.error) {
notifyError(res.error);
return;
}
page.data = res.data;
return;
}
async function getUsers() {
page.loading = true;
const res = await apiFetch(
`/admin/domains/${page.domain}/userdb/${page.sid}/users`
);
page.loading = false;
if (res.error) {
notifyError(res.error);
return;
}
page.all_users = [];
page.all_users = (res.data as Array<any>).map((e) => {
return {
key: e.Uid,
name: e.DisplayName + " (" + e.Email + ")",
};
});
return;
}
async function getGroups() {
page.loading = true;
const res = await apiFetch(
`/admin/domains/${page.domain}/userdb/${page.sid}/groups`
);
page.loading = false;
if (res.error) {
notifyError(res.error);
return;
}
page.all_groups = [];
page.all_groups = (res.data as Array<any>).map((e) => {
return {
key: e.Gid,
name: e.Name + " (" + e.Email + ")",
};
});
page.all_groups = page.all_groups.filter((e) => e.key !== page.members.Gid);
return;
}
async function showAddModal(record: any) {
if (record) {
page.add.Gid = record.Gid;
page.add.Email = record.Email;
page.add.Name = record.Name;
page.add.AlternateEmails = record.AlternateEmails;
page.add.AlternateEmailsStr = page.add.AlternateEmails.join("\n");
} else {
page.add.Gid = "";
page.add.Email = "";
page.add.Name = "";
page.add.AlternateEmails = [];
page.add.AlternateEmailsStr = "";
}
page.add.show = true;
}
async function showMembersModal(record: any) {
page.members.Gid = record.Gid;
page.members.Name = record.Name;
page.members.show = true;
page.members.groups_ids = record.MemberGroups;
if (!page.members.groups_ids) {
page.members.groups_ids = [];
}
page.members.users_ids = record.MemberUsers;
if (!page.members.users_ids) {
page.members.users_ids = [];
}
await getUsers();
await getGroups();
}
async function add() {
page.add.AlternateEmails = page.add.AlternateEmailsStr.split("\n");
const res = await apiFetch(
`/admin/domains/${page.domain}/userdb/${page.sid}/groups`,
{
method: "POST",
body: page.add,
}
);
if (res.error) {
notifyError(res.error);
return;
}
page.add.show = false;
return get();
}
async function deleteGroup(gid: string) {
const res = await apiFetch(
`/admin/domains/${page.domain}/userdb/${page.sid}/groups`,
{
method: "DELETE",
body: {
Gid: gid,
},
}
);
if (res.error) {
notifyError(res.error);
return;
}
return get();
}
async function changeUserMember(
nextTargetKeys: string[],
direction: string,
moveKeys: string[]
) {
page.members.disabled = true;
const res = await apiFetch(
`/admin/domains/${page.domain}/userdb/${page.sid}/groups/${page.members.Gid}/user`,
{
method: "POST",
body: {
Action: direction === "right" ? "add" : "delete",
Ids: moveKeys,
},
}
);
page.members.disabled = false;
if (res.error) {
notifyError(res.error);
return;
}
return;
}
async function changeGroupMember(
nextTargetKeys: string[],
direction: string,
moveKeys: string[]
) {
page.members.disabled = true;
const res = await apiFetch(
`/admin/domains/${page.domain}/userdb/${page.sid}/groups/${page.members.Gid}/group`,
{
method: "POST",
body: {
Action: direction === "right" ? "add" : "delete",
Ids: moveKeys,
},
}
);
page.members.disabled = false;
if (res.error) {
notifyError(res.error);
return;
}
return;
}
</script>
<style scoped>
.panel-content {
min-width: 700px;
max-width: 70%;
margin: auto;
}
</style>

View File

@ -0,0 +1,245 @@
<template>
<a-table
class="panel-content"
:columns="columns"
:data-source="page.data"
:loading="page.loading"
bordered
size="small"
:pagination="false"
>
<template #bodyCell="{ column, record }">
<template v-if="column.key === 'action'">
<a-space>
<a-button @click="showAddModal(record)">{{
t("components.admin.domains.userdb.redirects.edit")
}}</a-button>
<a-popconfirm
:title="t('components.admin.domains.userdb.redirects.delete') + '?'"
:ok-text="t('components.admin.domains.userdb.redirects.ok')"
:cancel-text="t('components.admin.domains.userdb.redirects.cancel')"
@confirm="deleteRedirect(record.Rid)"
>
<a-button danger>{{
t("components.admin.domains.userdb.redirects.delete")
}}</a-button>
</a-popconfirm>
</a-space>
</template>
</template>
<template #title>
<div style="display: flex; justify-content: end">
<a-button type="primary" @click="showAddModal(undefined)">{{
$t("components.admin.domains.userdb.redirects.add")
}}</a-button>
</div>
</template>
</a-table>
<a-modal
v-model:open="page.add.show"
:title="
page.add.Rid
? t(`components.admin.domains.userdb.redirects.edit`) +
' ' +
page.add.Name
: t(`components.admin.domains.userdb.redirects.add`)
"
>
<a-divider></a-divider>
<a-form :label-col="labelCol" :wrapper-col="wrapperCol" :labelWrap="true">
<a-form-item
:label="$t('components.admin.domains.userdb.redirects.add_modal.name')"
:rules="[{ required: true }]"
>
<a-input v-model:value="page.add.Name"> </a-input>
</a-form-item>
<a-form-item
:label="$t('components.admin.domains.userdb.redirects.add_modal.email')"
:rules="[{ required: true }]"
>
<a-input v-model:value="page.add.Email"> </a-input>
</a-form-item>
<a-form-item
:label="
$t('components.admin.domains.userdb.redirects.add_modal.destinations')
"
:rules="[{ required: true }]"
>
<a-textarea v-model:value="page.add.DestinationsStr"> </a-textarea>
</a-form-item>
</a-form>
<template #footer>
<a-space>
<a-button @click="page.add.show = false">
{{ $t("components.admin.domains.userdb.redirects.add_modal.cancel") }}
</a-button>
<a-button
:disabled="
!page.add.Name || !page.add.Email || !page.add.DestinationsStr
"
type="primary"
@click="add"
>
{{ $t("components.admin.domains.userdb.redirects.add_modal.save") }}
</a-button>
</a-space>
</template>
</a-modal>
</template>
<script setup lang="ts">
import { notifyError } from "@/composables/alert";
import { apiFetch } from "@/composables/apiFetch";
import router from "@/router";
import Export from "@/components/admin/domains/mailstorage/Export.vue";
import {
DomainPlaceholder,
MailboxPlaceholder,
RouteAdminDomainsDomainMailStorageMailboxesExport,
} from "@/router/consts";
import {
DeleteOutlined,
DownOutlined,
ExclamationCircleOutlined,
} from "@ant-design/icons-vue";
import { Modal } from "ant-design-vue";
import type { ColumnType } from "ant-design-vue/es/table";
import { createVNode, onMounted, reactive } from "vue";
import { useI18n } from "vue-i18n";
import { useRoute } from "vue-router";
const route = useRoute();
const { t } = useI18n();
const labelCol = { span: 8 };
const wrapperCol = { span: 16 };
const columns: ColumnType<any>[] = [
{
title: t("components.admin.domains.userdb.redirects.col_name"),
dataIndex: "Name",
},
{
title: t("components.admin.domains.userdb.redirects.col_email"),
dataIndex: "Email",
},
{
key: "action",
align: "right",
},
];
const page = reactive<{
domain: string;
sid: string;
loading: boolean;
data: any;
add: {
show: boolean;
Rid: string;
Name: string;
Email: string;
Destinations: string[];
DestinationsStr: string;
};
}>({
domain: "",
sid: "",
loading: false,
data: [],
add: {
show: false,
Name: "",
Email: "",
Destinations: [],
DestinationsStr: "",
Rid: "",
},
});
onMounted(() => {
page.sid = route.params.sid as string;
page.domain = route.params.domain as string;
get();
});
async function get() {
page.loading = true;
const res = await apiFetch(
`/admin/domains/${page.domain}/userdb/${page.sid}/redirects`
);
page.loading = false;
if (res.error) {
notifyError(res.error);
return;
}
page.data = res.data;
return;
}
async function showAddModal(record: any) {
if (record) {
page.add.Rid = record.Rid;
page.add.Email = record.Email;
page.add.Name = record.Name;
page.add.Destinations = record.Destinations;
page.add.DestinationsStr = page.add.Destinations.join("\n");
} else {
page.add.Rid = "";
page.add.Email = "";
page.add.Name = "";
page.add.Destinations = [];
page.add.DestinationsStr = "";
}
page.add.show = true;
}
async function add() {
page.add.Destinations = page.add.DestinationsStr.split("\n");
const res = await apiFetch(
`/admin/domains/${page.domain}/userdb/${page.sid}/redirects`,
{
method: "POST",
body: page.add,
}
);
if (res.error) {
notifyError(res.error);
return;
}
page.add.show = false;
return get();
}
async function deleteRedirect(rid: string) {
const res = await apiFetch(
`/admin/domains/${page.domain}/userdb/${page.sid}/redirects`,
{
method: "DELETE",
body: {
Rid: rid,
},
}
);
if (res.error) {
notifyError(res.error);
return;
}
return get();
}
</script>
<style scoped>
.panel-content {
min-width: 600px;
max-width: 60%;
margin: auto;
}
</style>

View File

@ -0,0 +1,812 @@
<template>
<div>
<a-card class="panel-content">
<a-form :label-col="labelCol" :wrapper-col="wrapperCol" :labelWrap="true">
<a-form-item
:label="$t('components.admin.domains.userdb.settings.Name')"
>
<a-input v-model:value="page.settings.Name"> </a-input>
</a-form-item>
<a-form-item
:label="$t('components.admin.domains.userdb.settings.UseInGal')"
>
<a-switch v-model:checked="page.settings.UseInGal"> </a-switch>
</a-form-item>
<template v-if="page.type === 'jsonFileUserDB'">
<a-form-item
:label="$t('components.admin.domains.userdb.settings.Dir')"
>
<a-input v-model:value="page.settings.Params.Dir"> </a-input>
</a-form-item>
<a-form-item
:label="
$t('components.admin.domains.userdb.settings.MasterUsersGroup')
"
>
<a-input v-model:value="page.settings.Params.MasterUsersGroup">
</a-input>
</a-form-item>
</template>
<template v-else>
<a-form-item
:label="$t('components.admin.domains.userdb.settings.CacheTTL')"
>
<a-input-number
class="form-input-number"
:addon-after="$t('common.suffixes.sec')"
v-model:value="page.settings.Params.CacheTTL"
>
</a-input-number>
</a-form-item>
<a-collapse>
<a-collapse-panel
:header="
$t('components.admin.domains.userdb.settings.ConnectionGroup')
"
>
<a-form-item
:label="
$t('components.admin.domains.userdb.settings.ConnectURIs')
"
>
<a-textarea
v-model:value="page.settings.Params.ConnectURIsString"
>
</a-textarea>
</a-form-item>
<a-form-item
:label="
$t('components.admin.domains.userdb.settings.ConnectBindDN')
"
>
<a-input v-model:value="page.settings.Params.ConnectBindDN">
</a-input>
</a-form-item>
<a-form-item
:label="
$t('components.admin.domains.userdb.settings.ConnectPassword')
"
>
<a-input-password
v-model:value="page.settings.Params.ConnectPassword"
autocomplete="off"
>
</a-input-password>
</a-form-item>
<a-form-item
:label="
$t('components.admin.domains.userdb.settings.ConnectBaseDN')
"
>
<a-input v-model:value="page.settings.Params.ConnectBaseDN">
</a-input>
</a-form-item>
<a-form-item
:label="
$t(
'components.admin.domains.userdb.settings.ConnectTLSMinVer'
)
"
>
<a-select v-model:value="page.settings.Params.ConnectTLSMinVer">
<a-select-option value="1.3"></a-select-option>
<a-select-option value="1.2"></a-select-option>
<a-select-option value="1.0"></a-select-option>
</a-select>
</a-form-item>
</a-collapse-panel>
<a-collapse-panel
:header="$t('components.admin.domains.userdb.settings.UserGroup')"
>
<a-form-item
:label="
$t('components.admin.domains.userdb.settings.UserObjectclass')
"
>
<a-input v-model:value="page.settings.Params.UserObjectclass">
</a-input>
</a-form-item>
<a-form-item
:label="
$t('components.admin.domains.userdb.settings.UserMailAttr')
"
>
<a-input v-model:value="page.settings.Params.UserMailAttr">
</a-input>
</a-form-item>
<a-form-item
:label="
$t('components.admin.domains.userdb.settings.UserQuotaAttr')
"
>
<a-input v-model:value="page.settings.Params.UserQuotaAttr">
</a-input>
</a-form-item>
<a-form-item
:label="
$t('components.admin.domains.userdb.settings.MemberofAttr')
"
>
<a-input v-model:value="page.settings.Params.MemberofAttr">
</a-input>
</a-form-item>
<a-form-item
:label="
$t(
'components.admin.domains.userdb.settings.UserAddSearchFilter'
)
"
>
<a-input
v-model:value="page.settings.Params.UserAddSearchFilter"
>
</a-input>
</a-form-item>
</a-collapse-panel>
<a-collapse-panel
:header="
$t('components.admin.domains.userdb.settings.GroupsGroup')
"
>
<a-form-item
:label="
$t('components.admin.domains.userdb.settings.EmailGroups')
"
>
<a-switch v-model:checked="page.settings.Params.EmailGroups">
</a-switch>
</a-form-item>
<a-form-item
:label="
$t(
'components.admin.domains.userdb.settings.GroupObjectclass'
)
"
>
<a-input v-model:value="page.settings.Params.GroupObjectclass">
</a-input>
</a-form-item>
<a-form-item
:label="
$t('components.admin.domains.userdb.settings.GroupNameAttr')
"
>
<a-input v-model:value="page.settings.Params.GroupNameAttr">
</a-input>
</a-form-item>
<a-form-item
:label="
$t('components.admin.domains.userdb.settings.GroupMailAttr')
"
>
<a-input v-model:value="page.settings.Params.GroupMailAttr">
</a-input>
</a-form-item>
<a-form-item
:label="
$t('components.admin.domains.userdb.settings.GroupMemberAttr')
"
>
<a-input v-model:value="page.settings.Params.GroupMemberAttr">
</a-input>
</a-form-item>
<a-form-item
:label="
$t('components.admin.domains.userdb.settings.GroupMemberDn')
"
>
<a-switch v-model:checked="page.settings.Params.GroupMemberDn">
</a-switch>
</a-form-item>
<a-form-item
:label="
$t('components.admin.domains.userdb.settings.UserRDNAttr')
"
>
<a-input v-model:value="page.settings.Params.UserRDNAttr">
</a-input>
</a-form-item>
<a-form-item
:label="
$t(
'components.admin.domains.userdb.settings.GroupAddSearchFilter'
)
"
>
<a-input
v-model:value="page.settings.Params.GroupAddSearchFilter"
>
</a-input>
</a-form-item>
</a-collapse-panel>
<a-collapse-panel
:header="
$t('components.admin.domains.userdb.settings.RedirectGroup')
"
>
<a-form-item
:label="
$t('components.admin.domains.userdb.settings.EmailRedirect')
"
>
<a-switch v-model:checked="page.settings.Params.EmailRedirect">
</a-switch>
</a-form-item>
<a-form-item
:label="
$t(
'components.admin.domains.userdb.settings.AliasObjectclass'
)
"
>
<a-input v-model:value="page.settings.Params.AliasObjectclass">
</a-input>
</a-form-item>
<a-form-item
:label="
$t('components.admin.domains.userdb.settings.AliasMailAttr')
"
>
<a-input v-model:value="page.settings.Params.AliasMailAttr">
</a-input>
</a-form-item>
<a-form-item
:label="
$t(
'components.admin.domains.userdb.settings.AliasRedirectAttr'
)
"
>
<a-input v-model:value="page.settings.Params.AliasRedirectAttr">
</a-input>
</a-form-item>
</a-collapse-panel>
<a-collapse-panel
:header="
$t('components.admin.domains.userdb.settings.AltEmailGroup')
"
>
<a-form-item
:label="
$t('components.admin.domains.userdb.settings.EmailAlternate')
"
>
<a-switch v-model:checked="page.settings.Params.EmailAlternate">
</a-switch>
</a-form-item>
<a-form-item
:label="
$t(
'components.admin.domains.userdb.settings.AlternateMailAttr'
)
"
>
<a-input v-model:value="page.settings.Params.AlternateMailAttr">
</a-input>
</a-form-item>
</a-collapse-panel>
<a-collapse-panel
:header="
$t('components.admin.domains.userdb.settings.MasterGroup')
"
>
<a-form-item
:label="
$t(
'components.admin.domains.userdb.settings.MasterUserGroupNameAttr'
)
"
>
<a-input
v-model:value="page.settings.Params.MasterUserGroupNameAttr"
>
</a-input>
</a-form-item>
<a-form-item
:label="
$t('components.admin.domains.userdb.settings.MasterUserGroup')
"
>
<a-input v-model:value="page.settings.Params.MasterUserGroup">
</a-input>
</a-form-item>
</a-collapse-panel>
<a-collapse-panel
:header="$t('components.admin.domains.userdb.settings.GALGroup')"
>
<a-form-item
:label="
$t('components.admin.domains.userdb.settings.GALCacheTTL')
"
>
<a-input-number
class="form-input-number"
:addon-after="$t('common.suffixes.sec')"
v-model:value="page.settings.Params.GALCacheTTL"
>
</a-input-number>
</a-form-item>
<a-form-item
:label="
$t(
'components.admin.domains.userdb.settings.GALObjectsFilter'
)
"
>
<a-input v-model:value="page.settings.Params.GALObjectsFilter">
</a-input>
</a-form-item>
<a-collapse>
<a-collapse-panel
:header="
$t('components.admin.domains.userdb.settings.GALUserGroup')
"
>
<a-form-item
:label="
$t(
'components.admin.domains.userdb.settings.GALUserNameAttr'
)
"
>
<a-input
v-model:value="page.settings.Params.GALUserNameAttr"
>
</a-input>
</a-form-item>
<a-form-item
:label="
$t(
'components.admin.domains.userdb.settings.GALUserEmailAttr'
)
"
>
<a-input
v-model:value="page.settings.Params.GALUserEmailAttr"
>
</a-input>
</a-form-item>
<a-form-item
:label="
$t(
'components.admin.domains.userdb.settings.GALUserAltEmailAttr'
)
"
>
<a-input
v-model:value="page.settings.Params.GALUserAltEmailAttr"
>
</a-input>
</a-form-item>
<a-form-item
:label="
$t(
'components.admin.domains.userdb.settings.GALUserPhoneAttr'
)
"
>
<a-input
v-model:value="page.settings.Params.GALUserPhoneAttr"
>
</a-input>
</a-form-item>
<a-form-item
:label="
$t(
'components.admin.domains.userdb.settings.GALUserPhotoAttr'
)
"
>
<a-input
v-model:value="page.settings.Params.GALUserPhotoAttr"
>
</a-input>
</a-form-item>
<a-form-item
:label="
$t(
'components.admin.domains.userdb.settings.GALUserBirthdayAttr'
)
"
>
<a-input
v-model:value="page.settings.Params.GALUserBirthdayAttr"
>
</a-input>
</a-form-item>
<a-form-item
:label="
$t(
'components.admin.domains.userdb.settings.GALUserAddressAttr'
)
"
>
<a-input
v-model:value="page.settings.Params.GALUserAddressAttr"
>
</a-input>
</a-form-item>
<a-form-item
:label="
$t(
'components.admin.domains.userdb.settings.GALUserOrganizationAttr'
)
"
>
<a-input
v-model:value="
page.settings.Params.GALUserOrganizationAttr
"
>
</a-input>
</a-form-item>
<a-form-item
:label="
$t(
'components.admin.domains.userdb.settings.GALUserRoleAttr'
)
"
>
<a-input
v-model:value="page.settings.Params.GALUserRoleAttr"
>
</a-input>
</a-form-item>
</a-collapse-panel>
<a-collapse-panel
:header="
$t(
'components.admin.domains.userdb.settings.GALGroupsGroup'
)
"
>
<a-form-item
:label="
$t(
'components.admin.domains.userdb.settings.GALGroupNameAttr'
)
"
>
<a-input
v-model:value="page.settings.Params.GALGroupNameAttr"
>
</a-input>
</a-form-item>
<a-form-item
:label="
$t(
'components.admin.domains.userdb.settings.GALGroupEmailAttr'
)
"
>
<a-input
v-model:value="page.settings.Params.GALGroupEmailAttr"
>
</a-input>
</a-form-item>
</a-collapse-panel>
<a-collapse-panel
:header="
$t(
'components.admin.domains.userdb.settings.GALAliasesGroup'
)
"
>
<a-form-item
:label="
$t(
'components.admin.domains.userdb.settings.GALAliasNameAttr'
)
"
>
<a-input
v-model:value="page.settings.Params.GALAliasNameAttr"
>
</a-input>
</a-form-item>
<a-form-item
:label="
$t(
'components.admin.domains.userdb.settings.GALAliasEmailAttr'
)
"
>
<a-input
v-model:value="page.settings.Params.GALAliasEmailAttr"
>
</a-input>
</a-form-item>
</a-collapse-panel>
</a-collapse>
</a-collapse-panel>
</a-collapse>
</template>
<br />
<a-space style="display: flex; justify-content: center">
<a-input-search
v-model:value="page.settings.Email"
:placeholder="
t('components.admin.domains.userdb.settings.check_email')
"
style="width: 370px"
size="large"
allowClear
@search="check"
>
<template #enterButton>
<a-button
type="primary"
:disabled="!page.settings.Email"
:loading="page.checkLoading"
>{{
$t("components.admin.domains.userdb.settings.check")
}}</a-button
>
</template>
</a-input-search>
<a-button
:disabled="!page.contentLoaded"
type="primary"
size="large"
class="save-button"
style="width: fit-content"
@click="save"
>
{{ $t("components.admin.domains.userdb.settings.save") }}
</a-button>
</a-space>
</a-form>
</a-card>
</div>
</template>
<script setup lang="ts">
import { notifyError, notifySuccess } from "@/composables/alert";
import { apiFetch } from "@/composables/apiFetch";
import { ExclamationCircleOutlined } from "@ant-design/icons-vue";
import { Modal } from "ant-design-vue";
import { createVNode, onMounted, reactive } from "vue";
import { saveAndRestart } from "@/composables/restart";
import { message } from "ant-design-vue";
import { useI18n } from "vue-i18n";
import { useRoute } from "vue-router";
import router from "@/router";
import { RouteAdminDashboard } from "@/router/consts";
const route = useRoute();
const { t } = useI18n();
const page = reactive<{
sid: string;
domain: string;
contentLoaded: boolean;
checkLoading: boolean;
type: string;
settings: {
Name: string;
UseInGal: boolean;
Email: string;
Params: {
//Maildir
Dir: string;
MasterUsersGroup: string;
//Ldap
CacheTTL: number;
ConnectURIs: string[];
ConnectURIsString: string;
ConnectBindDN: string;
ConnectPassword: string;
ConnectBaseDN: string;
ConnectTLSMinVer: string;
UserObjectclass: string;
UserMailAttr: string;
UserQuotaAttr: string;
MemberofAttr: string;
UserAddSearchFilter: string;
EmailGroups: boolean;
GroupObjectclass: string;
GroupNameAttr: string;
GroupMailAttr: string;
GroupMemberAttr: string;
GroupMemberDn: boolean;
UserRDNAttr: string;
GroupAddSearchFilter: string;
EmailRedirect: boolean;
AliasObjectclass: string;
AliasMailAttr: string;
AliasRedirectAttr: string;
EmailAlternate: boolean;
AlternateMailAttr: string;
MasterUserGroupNameAttr: string;
MasterUserGroup: string;
GALCacheTTL: number;
GALObjectsFilter: string;
GALUserNameAttr: string;
GALUserEmailAttr: string;
GALUserAltEmailAttr: string;
GALUserPhoneAttr: string;
GALUserPhotoAttr: string;
GALUserBirthdayAttr: string;
GALUserAddressAttr: string;
GALUserOrganizationAttr: string;
GALUserRoleAttr: string;
GALGroupNameAttr: string;
GALGroupEmailAttr: string;
GALAliasNameAttr: string;
GALAliasEmailAttr: string;
};
};
}>({
domain: "",
sid: "",
contentLoaded: false,
checkLoading: false,
type: "",
settings: {
Name: "",
UseInGal: false,
Email: "",
Params: {
Dir: "",
MasterUsersGroup: "",
CacheTTL: 0,
ConnectURIs: [],
ConnectURIsString: "",
ConnectBindDN: "",
ConnectPassword: "",
ConnectBaseDN: "",
ConnectTLSMinVer: "",
UserObjectclass: "",
UserMailAttr: "",
UserQuotaAttr: "",
MemberofAttr: "",
UserAddSearchFilter: "",
EmailGroups: false,
GroupObjectclass: "",
GroupNameAttr: "",
GroupMailAttr: "",
GroupMemberAttr: "",
GroupMemberDn: false,
UserRDNAttr: "",
GroupAddSearchFilter: "",
EmailRedirect: false,
AliasObjectclass: "",
AliasMailAttr: "",
AliasRedirectAttr: "",
EmailAlternate: false,
AlternateMailAttr: "",
MasterUserGroupNameAttr: "",
MasterUserGroup: "",
GALCacheTTL: 0,
GALObjectsFilter: "",
GALUserNameAttr: "",
GALUserEmailAttr: "",
GALUserAltEmailAttr: "",
GALUserPhoneAttr: "",
GALUserPhotoAttr: "",
GALUserBirthdayAttr: "",
GALUserAddressAttr: "",
GALUserOrganizationAttr: "",
GALUserRoleAttr: "",
GALGroupNameAttr: "",
GALGroupEmailAttr: "",
GALAliasNameAttr: "",
GALAliasEmailAttr: "",
},
},
});
const labelCol = { span: 16, style: { "text-align": "left" } };
const wrapperCol = { span: 8 };
onMounted(() => {
page.domain = route.params.domain as string;
page.sid = route.params.sid as string;
get();
});
async function get() {
const res = await apiFetch(
`/admin/domains/${page.domain}/userdb/${page.sid}/settings`
);
if (res.error) {
notifyError(res.error);
return;
}
page.contentLoaded = true;
page.type = res.data.Type;
page.settings = res.data;
loadTextArea();
return;
}
async function save() {
saveTextArea();
const res = await apiFetch(
`/admin/domains/${page.domain}/userdb/${page.sid}/settings`,
{
method: "POST",
body: page.settings,
}
);
if (res.error) {
notifyError(res.error);
return;
}
notifySuccess(t("components.admin.domains.userdb.settings.save_success"));
router.go(0);
return;
}
async function check() {
if (!page.settings.Email) {
return;
}
saveTextArea();
page.checkLoading = true;
const res = await apiFetch(
`/admin/domains/${page.domain}/userdb/${page.sid}/settings/check`,
{
method: "POST",
body: page.settings,
}
);
page.checkLoading = false;
if (res.error) {
notifyError(
t("components.admin.domains.userdb.settings.check_error") + res.error
);
return;
}
notifySuccess(t("components.admin.domains.userdb.settings.check_success"));
return;
}
function loadTextArea() {
if (page.settings.Params.ConnectURIs) {
page.settings.Params.ConnectURIsString =
page.settings.Params.ConnectURIs.join("\n");
}
}
function saveTextArea() {
if (page.settings.Params.ConnectURIsString) {
page.settings.Params.ConnectURIs =
page.settings.Params.ConnectURIsString.split("\n");
}
}
</script>
<style scoped>
.panel-content {
min-width: 600px;
max-width: 60%;
margin: auto;
}
</style>

View File

@ -0,0 +1,294 @@
<template>
<a-table
class="panel-content"
:columns="columns"
:data-source="page.data"
:loading="page.loading"
bordered
size="small"
:pagination="false"
>
<template #bodyCell="{ column, record }">
<template v-if="column.key === 'action'">
<a-space>
<a-button @click="showAddModal(record)">{{
t("components.admin.domains.userdb.users.edit")
}}</a-button>
<a-popconfirm
:title="t('components.admin.domains.userdb.users.delete') + '?'"
:ok-text="t('components.admin.domains.userdb.users.ok')"
:cancel-text="t('components.admin.domains.userdb.users.cancel')"
@confirm="deleteUser(record.Uid)"
>
<a-button danger>{{
t("components.admin.domains.userdb.users.delete")
}}</a-button>
</a-popconfirm>
</a-space>
</template>
</template>
<template #title>
<div style="display: flex; justify-content: end">
<a-button type="primary" @click="showAddModal(undefined)">{{
$t("components.admin.domains.userdb.users.add")
}}</a-button>
</div>
</template>
</a-table>
<a-modal
v-model:open="page.add.show"
:title="
page.add.Uid
? t(`components.admin.domains.userdb.users.edit`) +
' ' +
page.add.DisplayName
: t(`components.admin.domains.userdb.users.add`)
"
>
<a-divider></a-divider>
<a-form :label-col="labelCol" :wrapper-col="wrapperCol" :labelWrap="true">
<a-form-item
:label="
$t('components.admin.domains.userdb.users.add_modal.display_name')
"
:rules="[{ required: true }]"
>
<a-input v-model:value="page.add.DisplayName"> </a-input>
</a-form-item>
<a-form-item
:label="$t('components.admin.domains.userdb.users.add_modal.email')"
:rules="[{ required: true }]"
>
<a-input v-model:value="page.add.Email"> </a-input>
</a-form-item>
<a-form-item
:label="
$t('components.admin.domains.userdb.users.add_modal.alt_emails')
"
>
<a-textarea v-model:value="page.add.AlternateEmailsStr"> </a-textarea>
</a-form-item>
<a-form-item
:label="$t('components.admin.domains.userdb.users.add_modal.quota')"
>
<a-input-number
class="form-input-number"
v-model:value="page.add.Quota"
>
</a-input-number>
</a-form-item>
<a-form-item
:label="$t('components.admin.domains.userdb.users.add_modal.password')"
:rules="[{ required: true }]"
>
<a-input-password autocomplete="off" v-model:value="page.add.Password">
</a-input-password>
</a-form-item>
<a-form-item
:label="
$t('components.admin.domains.userdb.users.add_modal.password_again')
"
:rules="[{ required: true }]"
>
<a-input-password
autocomplete="off"
v-model:value="page.add.PasswordAgain"
>
</a-input-password>
</a-form-item>
</a-form>
<template #footer>
<a-space>
<a-button @click="page.add.show = false">
{{ $t("components.admin.domains.userdb.users.add_modal.cancel") }}
</a-button>
<a-button
:disabled="
!page.add.DisplayName ||
!page.add.Email ||
(!page.add.Uid && (!page.add.Password || !page.add.PasswordAgain))
"
type="primary"
@click="add"
>
{{ $t("components.admin.domains.userdb.users.add_modal.save") }}
</a-button>
</a-space>
</template>
</a-modal>
</template>
<script setup lang="ts">
import { notifyError } from "@/composables/alert";
import { apiFetch } from "@/composables/apiFetch";
import router from "@/router";
import Export from "@/components/admin/domains/mailstorage/Export.vue";
import {
DomainPlaceholder,
MailboxPlaceholder,
RouteAdminDomainsDomainMailStorageMailboxesExport,
} from "@/router/consts";
import {
DeleteOutlined,
DownOutlined,
ExclamationCircleOutlined,
} from "@ant-design/icons-vue";
import { Modal } from "ant-design-vue";
import type { ColumnType } from "ant-design-vue/es/table";
import { createVNode, onMounted, reactive } from "vue";
import { useI18n } from "vue-i18n";
import { useRoute } from "vue-router";
const route = useRoute();
const { t } = useI18n();
const labelCol = { span: 8 };
const wrapperCol = { span: 16 };
const columns: ColumnType<any>[] = [
{
title: t("components.admin.domains.userdb.users.col_name"),
dataIndex: "DisplayName",
},
{
title: t("components.admin.domains.userdb.users.col_email"),
dataIndex: "Email",
},
{
key: "action",
align: "right",
},
];
const page = reactive<{
domain: string;
sid: string;
loading: boolean;
data: any;
add: {
show: boolean;
Uid: string;
DisplayName: string;
Email: string;
AlternateEmails: string[];
AlternateEmailsStr: string;
Quota: number;
Password: string;
PasswordAgain: string;
};
}>({
domain: "",
sid: "",
loading: false,
data: [],
add: {
show: false,
DisplayName: "",
Email: "",
AlternateEmails: [],
AlternateEmailsStr: "",
Quota: -1,
Password: "",
PasswordAgain: "",
Uid: "",
},
});
onMounted(() => {
page.sid = route.params.sid as string;
page.domain = route.params.domain as string;
get();
});
async function get() {
page.loading = true;
const res = await apiFetch(
`/admin/domains/${page.domain}/userdb/${page.sid}/users`
);
page.loading = false;
if (res.error) {
notifyError(res.error);
return;
}
page.data = res.data;
return;
}
async function showAddModal(record: any) {
if (record) {
page.add.Uid = record.Uid;
page.add.Email = record.Email;
page.add.DisplayName = record.DisplayName;
page.add.AlternateEmails = record.AlternateEmails;
page.add.AlternateEmailsStr = page.add.AlternateEmails.join("\n");
page.add.Quota = record.Quota;
page.add.Password = record.Password;
page.add.PasswordAgain = record.Password;
} else {
page.add.Uid = "";
page.add.Email = "";
page.add.DisplayName = "";
page.add.AlternateEmails = [];
page.add.AlternateEmailsStr = "";
page.add.Quota = -1;
}
page.add.Password = "";
page.add.PasswordAgain = "";
page.add.show = true;
}
async function add() {
if (page.add.Password !== page.add.PasswordAgain) {
notifyError("Passwords mismatch");
return;
}
page.add.AlternateEmails = page.add.AlternateEmailsStr.split("\n");
const res = await apiFetch(
`/admin/domains/${page.domain}/userdb/${page.sid}/users`,
{
method: "POST",
body: page.add,
}
);
if (res.error) {
notifyError(res.error);
return;
}
page.add.show = false;
return get();
}
async function deleteUser(uid: string) {
const res = await apiFetch(
`/admin/domains/${page.domain}/userdb/${page.sid}/users`,
{
method: "DELETE",
body: {
Uid: uid,
},
}
);
if (res.error) {
notifyError(res.error);
return;
}
return get();
}
</script>
<style scoped>
.panel-content {
min-width: 600px;
max-width: 60%;
margin: auto;
}
</style>

View File

@ -0,0 +1,164 @@
<template>
<div>
<a-table
class="panel-content"
:columns="columns"
:data-source="page.data"
:loading="page.loading"
bordered
size="small"
:pagination="false"
>
<template #bodyCell="{ column, record }">
<template v-if="column.key === 'action'">
<a-button @click="unban(record.ip)">
{{ t("components.admin.security.blocked_ips.unblock") }}
</a-button>
</template>
</template>
<template #title>
<a-row>
<a-col style="display: flex; align-items: flex-end" :span="8">
<a-input-search
v-model:value="page.pagination.search"
:placeholder="t('components.admin.security.blocked_ips.search')"
enter-button
allow-clear
@search="get"
/>
</a-col>
<a-col :span="16">
<a-space style="display: flex; justify-content: end">
<a-button @click="unban('')">{{
page.withSearch
? $t("components.admin.security.blocked_ips.unblock_found")
: $t("components.admin.security.blocked_ips.unblock_all")
}}</a-button>
</a-space>
</a-col>
</a-row>
</template>
<template #footer>
<a-flex justify="flex-end">
<a-pagination
v-model:value="page.pagination.current"
:defaultPageSize="page.pagination.size"
:total="page.pagination.total"
@change="pageChange"
:pageSizeOptions="['2', '10', '20', '50', '100']"
v-model:pageSize="page.pagination.size"
:showSizeChanger="true"
/>
</a-flex>
</template>
</a-table>
</div>
</template>
<script setup lang="ts">
import { notifyError } from "@/composables/alert";
import { apiFetch } from "@/composables/apiFetch";
import type { ColumnType } from "ant-design-vue/es/table";
import { onMounted, reactive } from "vue";
import { useI18n } from "vue-i18n";
const { t } = useI18n();
const columns: ColumnType<any>[] = [
{
title: t("components.admin.security.blocked_ips.ip"),
dataIndex: "ip",
},
{
key: "action",
align: "right",
},
];
const page = reactive<{
withSearch: boolean;
loading: boolean;
data: any;
pagination: {
current: number;
total: number;
size: number;
search: string;
};
}>({
withSearch: false,
pagination: {
current: 1,
total: 0,
size: 50,
search: "",
},
loading: false,
data: [],
});
onMounted(() => {
get();
});
function pageChange(current: number) {
page.pagination.current = current;
return get();
}
async function get() {
page.withSearch = page.pagination.search !== "";
page.loading = true;
const params = new URLSearchParams();
params.append("page", String(page.pagination.current));
params.append("size", String(page.pagination.size));
params.append("search", page.pagination.search);
const res = await apiFetch(
"/admin/security/blocked_ips?" + params.toString()
);
page.loading = false;
if (res.error) {
notifyError(res.error);
return;
}
page.data = (res.data as Array<any>).map((i, idx) => {
return {
ip: i,
};
});
page.pagination.total = res.total;
return;
}
async function unban(ip: string) {
const params = new URLSearchParams();
params.append("search", page.pagination.search);
params.append("ip", ip);
const res = await apiFetch(
"/admin/security/blocked_ips?" + params.toString(),
{
method: "DELETE",
}
);
if (res.error) {
notifyError(res.error);
return;
}
return get();
}
</script>
<style scoped>
.panel-content {
min-width: 600px;
max-width: 60%;
margin: auto;
}
</style>

View File

@ -0,0 +1,215 @@
<template>
<a-table
class="bw-list-panel-content"
:columns="columns"
:data-source="page.data"
:loading="page.loading"
bordered
size="small"
:pagination="false"
>
<template #bodyCell="{ column, record }">
<template v-if="column.key === 'action'">
<a-button @click="del(record.email)">
{{ t("components.admin.security.bw_list_email.delete") }}
</a-button>
</template>
</template>
<template #title>
<a-row>
<a-col style="display: flex; align-items: flex-end" :span="8">
<a-input-search
v-model:value="page.pagination.search"
:placeholder="t('components.admin.security.bw_list_email.search')"
enter-button
allow-clear
@search="get"
/>
</a-col>
<a-col :span="16">
<a-space style="display: flex; justify-content: end">
<a-button @click="del('')">{{
page.withSearch
? $t("components.admin.security.bw_list_email.delete_found")
: $t("components.admin.security.bw_list_email.delete_all")
}}</a-button>
<a-button type="primary" @click="page.showAddModal = true">
<PlusOutlined />
{{
$t("components.admin.security.bw_list_email.add_email")
}}</a-button
>
</a-space>
</a-col>
</a-row>
</template>
<template #footer>
<a-flex justify="flex-end">
<a-pagination
v-model:value="page.pagination.current"
:defaultPageSize="page.pagination.size"
:total="page.pagination.total"
@change="pageChange"
:pageSizeOptions="['10', '20', '50', '100']"
v-model:pageSize="page.pagination.size"
:showSizeChanger="true"
/>
</a-flex>
</template>
</a-table>
<a-modal
v-model:open="page.showAddModal"
:title="$t(`components.admin.security.bw_list_email.add_email`)"
>
<a-input
v-model:value="page.toAdd"
:placeholder="
$t(`components.admin.security.bw_list_email.add_email_placeholder`)
"
>
</a-input>
<template #footer>
<a-button
key="submit"
type="primary"
:loading="page.addLoading"
@click="add"
>{{ $t(`components.admin.security.bw_list_email.add`) }}</a-button
>
</template>
</a-modal>
</template>
<script setup lang="ts">
import { notifyError } from "@/composables/alert";
import { apiFetch } from "@/composables/apiFetch";
import type { ColumnType } from "ant-design-vue/es/table";
import { onMounted, reactive } from "vue";
import { PlusOutlined } from "@ant-design/icons-vue";
import { useI18n } from "vue-i18n";
const { t } = useI18n();
const columns: ColumnType<any>[] = [
{
title: t("components.admin.security.bw_list_email.email"),
dataIndex: "email",
},
{
key: "action",
align: "right",
},
];
const page = reactive<{
withSearch: boolean;
loading: boolean;
data: any;
pagination: {
current: number;
total: number;
size: number;
search: string;
};
showAddModal: boolean;
addLoading: boolean;
toAdd: string;
}>({
withSearch: false,
pagination: {
current: 1,
total: 0,
size: 50,
search: "",
},
loading: false,
data: [],
showAddModal: false,
addLoading: false,
toAdd: "",
});
onMounted(() => {
get();
});
function pageChange(current: number) {
page.pagination.current = current;
return get();
}
async function get() {
page.withSearch = page.pagination.search !== "";
page.loading = true;
const params = new URLSearchParams();
params.append("page", String(page.pagination.current));
params.append("size", String(page.pagination.size));
params.append("search", page.pagination.search);
const res = await apiFetch(
"/admin/security/black_list/email?" + params.toString()
);
page.loading = false;
if (res.error) {
notifyError(res.error);
return;
}
page.data = (res.data as Array<any>).map((i, idx) => {
return {
email: i,
};
});
page.pagination.total = res.total;
return;
}
async function del(email: string) {
const params = new URLSearchParams();
params.append("search", page.pagination.search);
params.append("email", email);
const res = await apiFetch(
"/admin/security/black_list/email?" + params.toString(),
{
method: "DELETE",
}
);
if (res.error) {
notifyError(res.error);
return;
}
return get();
}
async function add() {
const params = new URLSearchParams();
params.append("email", page.toAdd);
page.addLoading = true;
const res = await apiFetch(
"/admin/security/black_list/email?" + params.toString(),
{
method: "POST",
}
);
page.addLoading = false;
if (res.error) {
notifyError(res.error);
return;
}
page.showAddModal = false;
return get();
}
</script>
<style scoped></style>

View File

@ -0,0 +1,213 @@
<template>
<a-table
class="bw-list-panel-content"
:columns="columns"
:data-source="page.data"
:loading="page.loading"
bordered
size="small"
:pagination="false"
>
<template #bodyCell="{ column, record }">
<template v-if="column.key === 'action'">
<a-button @click="del(record.ip)">
{{ t("components.admin.security.bw_list_ip.delete") }}
</a-button>
</template>
</template>
<template #title>
<a-row>
<a-col style="display: flex; align-items: flex-end" :span="8">
<a-input-search
v-model:value="page.pagination.search"
:placeholder="t('components.admin.security.bw_list_ip.search')"
enter-button
allow-clear
@search="get"
/>
</a-col>
<a-col :span="16">
<a-space style="display: flex; justify-content: end">
<a-button @click="del('')">{{
page.withSearch
? $t("components.admin.security.bw_list_ip.delete_found")
: $t("components.admin.security.bw_list_ip.delete_all")
}}</a-button>
<a-button type="primary" @click="page.showAddModal = true">
<PlusOutlined />
{{ $t("components.admin.security.bw_list_ip.add_ip") }}</a-button
>
</a-space>
</a-col>
</a-row>
</template>
<template #footer>
<a-flex justify="flex-end">
<a-pagination
v-model:value="page.pagination.current"
:defaultPageSize="page.pagination.size"
:total="page.pagination.total"
@change="pageChange"
:pageSizeOptions="['10', '20', '50', '100']"
v-model:pageSize="page.pagination.size"
:showSizeChanger="true"
/>
</a-flex>
</template>
</a-table>
<a-modal
v-model:open="page.showAddModal"
:title="$t(`components.admin.security.bw_list_ip.add_ip`)"
>
<a-input
v-model:value="page.toAdd"
:placeholder="
$t(`components.admin.security.bw_list_ip.add_ip_placeholder`)
"
>
</a-input>
<template #footer>
<a-button
key="submit"
type="primary"
:loading="page.addLoading"
@click="add"
>{{ $t(`components.admin.security.bw_list_ip.add`) }}</a-button
>
</template>
</a-modal>
</template>
<script setup lang="ts">
import { notifyError } from "@/composables/alert";
import { apiFetch } from "@/composables/apiFetch";
import type { ColumnType } from "ant-design-vue/es/table";
import { onMounted, reactive } from "vue";
import { useI18n } from "vue-i18n";
import { PlusOutlined } from "@ant-design/icons-vue";
const { t } = useI18n();
const columns: ColumnType<any>[] = [
{
title: t("components.admin.security.bw_list_ip.ip"),
dataIndex: "ip",
},
{
key: "action",
align: "right",
},
];
const page = reactive<{
withSearch: boolean;
loading: boolean;
data: any;
pagination: {
current: number;
total: number;
size: number;
search: string;
};
showAddModal: boolean;
addLoading: boolean;
toAdd: string;
}>({
withSearch: false,
pagination: {
current: 1,
total: 0,
size: 50,
search: "",
},
loading: false,
data: [],
showAddModal: false,
addLoading: false,
toAdd: "",
});
onMounted(() => {
get();
});
function pageChange(current: number) {
page.pagination.current = current;
return get();
}
async function get() {
page.withSearch = page.pagination.search !== "";
page.loading = true;
const params = new URLSearchParams();
params.append("page", String(page.pagination.current));
params.append("size", String(page.pagination.size));
params.append("search", page.pagination.search);
const res = await apiFetch(
"/admin/security/black_list/ip?" + params.toString()
);
page.loading = false;
if (res.error) {
notifyError(res.error);
return;
}
page.data = (res.data as Array<any>).map((i, idx) => {
return {
ip: i,
};
});
page.pagination.total = res.total;
return;
}
async function del(ip: string) {
const params = new URLSearchParams();
params.append("search", page.pagination.search);
params.append("ip", ip);
const res = await apiFetch(
"/admin/security/black_list/ip?" + params.toString(),
{
method: "DELETE",
}
);
if (res.error) {
notifyError(res.error);
return;
}
return get();
}
async function add() {
const params = new URLSearchParams();
params.append("ip", page.toAdd);
page.addLoading = true;
const res = await apiFetch(
"/admin/security/black_list/ip?" + params.toString(),
{
method: "POST",
}
);
page.addLoading = false;
if (res.error) {
notifyError(res.error);
return;
}
page.showAddModal = false;
return get();
}
</script>
<style scoped></style>

View File

@ -0,0 +1,215 @@
<template>
<a-table
class="bw-list-panel-content"
:columns="columns"
:data-source="page.data"
:loading="page.loading"
bordered
size="small"
:pagination="false"
>
<template #bodyCell="{ column, record }">
<template v-if="column.key === 'action'">
<a-button @click="del(record.email)">
{{ t("components.admin.security.bw_list_email.delete") }}
</a-button>
</template>
</template>
<template #title>
<a-row>
<a-col style="display: flex; align-items: flex-end" :span="8">
<a-input-search
v-model:value="page.pagination.search"
:placeholder="t('components.admin.security.bw_list_email.search')"
enter-button
allow-clear
@search="get"
/>
</a-col>
<a-col :span="16">
<a-space style="display: flex; justify-content: end">
<a-button @click="del('')">{{
page.withSearch
? $t("components.admin.security.bw_list_email.delete_found")
: $t("components.admin.security.bw_list_email.delete_all")
}}</a-button>
<a-button type="primary" @click="page.showAddModal = true">
<PlusOutlined />
{{
$t("components.admin.security.bw_list_email.add_email")
}}</a-button
>
</a-space>
</a-col>
</a-row>
</template>
<template #footer>
<a-flex justify="flex-end">
<a-pagination
v-model:value="page.pagination.current"
:defaultPageSize="page.pagination.size"
:total="page.pagination.total"
@change="pageChange"
:pageSizeOptions="['10', '20', '50', '100']"
v-model:pageSize="page.pagination.size"
:showSizeChanger="true"
/>
</a-flex>
</template>
</a-table>
<a-modal
v-model:open="page.showAddModal"
:title="$t(`components.admin.security.bw_list_email.add_email`)"
>
<a-input
v-model:value="page.toAdd"
:placeholder="
$t(`components.admin.security.bw_list_email.add_email_placeholder`)
"
>
</a-input>
<template #footer>
<a-button
key="submit"
type="primary"
:loading="page.addLoading"
@click="add"
>{{ $t(`components.admin.security.bw_list_email.add`) }}</a-button
>
</template>
</a-modal>
</template>
<script setup lang="ts">
import { notifyError } from "@/composables/alert";
import { apiFetch } from "@/composables/apiFetch";
import type { ColumnType } from "ant-design-vue/es/table";
import { onMounted, reactive } from "vue";
import { PlusOutlined } from "@ant-design/icons-vue";
import { useI18n } from "vue-i18n";
const { t } = useI18n();
const columns: ColumnType<any>[] = [
{
title: t("components.admin.security.bw_list_email.email"),
dataIndex: "email",
},
{
key: "action",
align: "right",
},
];
const page = reactive<{
withSearch: boolean;
loading: boolean;
data: any;
pagination: {
current: number;
total: number;
size: number;
search: string;
};
showAddModal: boolean;
addLoading: boolean;
toAdd: string;
}>({
withSearch: false,
pagination: {
current: 1,
total: 0,
size: 50,
search: "",
},
loading: false,
data: [],
showAddModal: false,
addLoading: false,
toAdd: "",
});
onMounted(() => {
get();
});
function pageChange(current: number) {
page.pagination.current = current;
return get();
}
async function get() {
page.withSearch = page.pagination.search !== "";
page.loading = true;
const params = new URLSearchParams();
params.append("page", String(page.pagination.current));
params.append("size", String(page.pagination.size));
params.append("search", page.pagination.search);
const res = await apiFetch(
"/admin/security/white_list/email?" + params.toString()
);
page.loading = false;
if (res.error) {
notifyError(res.error);
return;
}
page.data = (res.data as Array<any>).map((i, idx) => {
return {
email: i,
};
});
page.pagination.total = res.total;
return;
}
async function del(email: string) {
const params = new URLSearchParams();
params.append("search", page.pagination.search);
params.append("email", email);
const res = await apiFetch(
"/admin/security/white_list/email?" + params.toString(),
{
method: "DELETE",
}
);
if (res.error) {
notifyError(res.error);
return;
}
return get();
}
async function add() {
const params = new URLSearchParams();
params.append("email", page.toAdd);
page.addLoading = true;
const res = await apiFetch(
"/admin/security/white_list/email?" + params.toString(),
{
method: "POST",
}
);
page.addLoading = false;
if (res.error) {
notifyError(res.error);
return;
}
page.showAddModal = false;
return get();
}
</script>
<style scoped></style>

View File

@ -0,0 +1,213 @@
<template>
<a-table
class="bw-list-panel-content"
:columns="columns"
:data-source="page.data"
:loading="page.loading"
bordered
size="small"
:pagination="false"
>
<template #bodyCell="{ column, record }">
<template v-if="column.key === 'action'">
<a-button @click="del(record.ip)">
{{ t("components.admin.security.bw_list_ip.delete") }}
</a-button>
</template>
</template>
<template #title>
<a-row>
<a-col style="display: flex; align-items: flex-end" :span="8">
<a-input-search
v-model:value="page.pagination.search"
:placeholder="t('components.admin.security.bw_list_ip.search')"
enter-button
allow-clear
@search="get"
/>
</a-col>
<a-col :span="16">
<a-space style="display: flex; justify-content: end">
<a-button @click="del('')">{{
page.withSearch
? $t("components.admin.security.bw_list_ip.delete_found")
: $t("components.admin.security.bw_list_ip.delete_all")
}}</a-button>
<a-button type="primary" @click="page.showAddModal = true">
<PlusOutlined />
{{ $t("components.admin.security.bw_list_ip.add_ip") }}</a-button
>
</a-space>
</a-col>
</a-row>
</template>
<template #footer>
<a-flex justify="flex-end">
<a-pagination
v-model:value="page.pagination.current"
:defaultPageSize="page.pagination.size"
:total="page.pagination.total"
@change="pageChange"
:pageSizeOptions="['10', '20', '50', '100']"
v-model:pageSize="page.pagination.size"
:showSizeChanger="true"
/>
</a-flex>
</template>
</a-table>
<a-modal
v-model:open="page.showAddModal"
:title="$t(`components.admin.security.bw_list_ip.add_ip`)"
>
<a-input
v-model:value="page.toAdd"
:placeholder="
$t(`components.admin.security.bw_list_ip.add_ip_placeholder`)
"
>
</a-input>
<template #footer>
<a-button
key="submit"
type="primary"
:loading="page.addLoading"
@click="add"
>{{ $t(`components.admin.security.bw_list_ip.add`) }}</a-button
>
</template>
</a-modal>
</template>
<script setup lang="ts">
import { notifyError } from "@/composables/alert";
import { apiFetch } from "@/composables/apiFetch";
import type { ColumnType } from "ant-design-vue/es/table";
import { onMounted, reactive } from "vue";
import { useI18n } from "vue-i18n";
import { PlusOutlined } from "@ant-design/icons-vue";
const { t } = useI18n();
const columns: ColumnType<any>[] = [
{
title: t("components.admin.security.bw_list_ip.ip"),
dataIndex: "ip",
},
{
key: "action",
align: "right",
},
];
const page = reactive<{
withSearch: boolean;
loading: boolean;
data: any;
pagination: {
current: number;
total: number;
size: number;
search: string;
};
showAddModal: boolean;
addLoading: boolean;
toAdd: string;
}>({
withSearch: false,
pagination: {
current: 1,
total: 0,
size: 50,
search: "",
},
loading: false,
data: [],
showAddModal: false,
addLoading: false,
toAdd: "",
});
onMounted(() => {
get();
});
function pageChange(current: number) {
page.pagination.current = current;
return get();
}
async function get() {
page.withSearch = page.pagination.search !== "";
page.loading = true;
const params = new URLSearchParams();
params.append("page", String(page.pagination.current));
params.append("size", String(page.pagination.size));
params.append("search", page.pagination.search);
const res = await apiFetch(
"/admin/security/white_list/ip?" + params.toString()
);
page.loading = false;
if (res.error) {
notifyError(res.error);
return;
}
page.data = (res.data as Array<any>).map((i, idx) => {
return {
ip: i,
};
});
page.pagination.total = res.total;
return;
}
async function del(ip: string) {
const params = new URLSearchParams();
params.append("search", page.pagination.search);
params.append("ip", ip);
const res = await apiFetch(
"/admin/security/white_list/ip?" + params.toString(),
{
method: "DELETE",
}
);
if (res.error) {
notifyError(res.error);
return;
}
return get();
}
async function add() {
const params = new URLSearchParams();
params.append("ip", page.toAdd);
page.addLoading = true;
const res = await apiFetch(
"/admin/security/white_list/ip?" + params.toString(),
{
method: "POST",
}
);
page.addLoading = false;
if (res.error) {
notifyError(res.error);
return;
}
page.showAddModal = false;
return get();
}
</script>
<style scoped></style>

View File

@ -0,0 +1,50 @@
<template>
<div>
<Rules
:get-get-url="getUrl"
:get-delete-url="getUrl"
:get-save-url="getUrl"
:get-move-url="moveUrl"
:get-enable-url="enableUrl"
:conditions="conditionsList"
:actions="actionsList"
>
<template #condition-value="valueProps">
<ConditionValue
:change="valueProps.change"
:value="valueProps.value"
:type="valueProps.type"
>
</ConditionValue>
</template>
<template #action-value="valueProps">
<ActionValue
:change="valueProps.change"
:value="valueProps.value"
:type="valueProps.type"
>
</ActionValue>
</template>
</Rules>
</div>
</template>
<script setup lang="ts">
import ConditionValue from "@/components/common/rules/address/Conditions.vue";
import ActionValue from "@/components/common/rules/address/Actions.vue";
import { conditionsList } from "@/components/common/rules/address/Conditions.vue";
import { actionsList } from "@/components/common/rules/address/Actions.vue";
import Rules from "@/components/common/rules/Rules.vue";
function getUrl(): string {
return `/admin/settings/address_rules`;
}
function moveUrl(): string {
return `/admin/settings/address_rules/move`;
}
function enableUrl(): string {
return `/admin/settings/address_rules/enable`;
}
</script>

View File

@ -0,0 +1,110 @@
<template>
<div>
<a-card class="panel-content">
<a-form :label-col="labelCol" :wrapper-col="wrapperCol" :labelWrap="true">
<a-form-item :label="t('components.admin.settings.calendars.freebusy')">
<a-switch v-model:checked="page.AllowFreebusy"> </a-switch>
</a-form-item>
<a-form-item :label="t('components.admin.settings.calendars.showfrom')">
<a-input-number
class="form-input-number"
:min="0"
v-model:value="page.ShowFromNWeeks"
>
</a-input-number>
</a-form-item>
<a-form-item :label="t('components.admin.settings.calendars.showto')">
<a-input-number
class="form-input-number"
:min="0"
v-model:value="page.ShowToNWeeks"
>
</a-input-number>
</a-form-item>
<a-form-item
:label="t('components.admin.settings.calendars.localpart')"
>
<a-input v-model:value="page.SenderLP"> </a-input>
</a-form-item>
</a-form>
<a-button
:disabled="!page.contentLoaded"
class="save-button"
type="primary"
size="large"
@click="update"
>
{{ $t("components.admin.settings.calendars.save") }}
</a-button>
</a-card>
</div>
</template>
<script setup lang="ts">
import { notifyError, notifySuccess } from "@/composables/alert";
import { apiFetch } from "@/composables/apiFetch";
import { onMounted, reactive } from "vue";
import { useI18n } from "vue-i18n";
const { t } = useI18n();
const labelCol = { span: 16, style: { "text-align": "left" } };
const wrapperCol = { span: 8, style: { "text-align": "left" } };
const page = reactive<{
contentLoaded: boolean;
AllowFreebusy: boolean;
ShowFromNWeeks: number;
ShowToNWeeks: number;
SenderLP: string;
}>({
contentLoaded: false,
AllowFreebusy: false,
ShowFromNWeeks: 0,
ShowToNWeeks: 0,
SenderLP: "",
});
onMounted(() => {
get();
});
async function get() {
const res = await apiFetch("/admin/settings/calendars");
if (res.error) {
notifyError(res.error);
return;
}
page.contentLoaded = true;
page.AllowFreebusy = res.data.AllowFreebusy;
page.ShowFromNWeeks = res.data.ShowFromNWeeks;
page.ShowToNWeeks = res.data.ShowToNWeeks;
page.SenderLP = res.data.SenderLP;
}
async function update() {
const res = await apiFetch("/admin/settings/calendars", {
method: "POST",
body: page,
});
if (res.error) {
notifyError(res.error);
return;
}
notifySuccess(t("components.admin.settings.calendars.success"));
}
</script>
<style scoped>
.panel-content {
width: 600px;
max-width: 60%;
margin: auto;
}
.save-button {
display: block;
margin: auto;
}
</style>

View File

@ -0,0 +1,272 @@
<template>
<a-card class="panel-content" v-if="page.contentLoaded">
<div v-if="page.accepted">
<a-descriptions
v-if="page.license"
:column="1"
:title="t(`components.admin.settings.license.title`)"
>
<a-descriptions-item :label="t(`components.admin.settings.license.id`)"
><div style="font-weight: bold">{{ page.license.ID }}</div>
</a-descriptions-item>
<a-descriptions-item
:label="t(`components.admin.settings.license.edition`)"
>
<div style="font-weight: bold">{{ page.license.Edition }}</div>
</a-descriptions-item>
<a-descriptions-item
:label="t(`components.admin.settings.license.valid_till`)"
>
<div style="font-weight: bold">
{{
page.license.ExpiresUnix > 0
? timeToFullDate(page.license.ExpiresUnix * 1000)
: $t("components.admin.settings.license.valid_forever")
}}
</div>
</a-descriptions-item>
<a-descriptions-item
:label="t(`components.admin.settings.license.max_mailbox_count`)"
>
<div style="font-weight: bold">
{{ page.license.MailboxCount }}
</div></a-descriptions-item
>
<a-descriptions-item
:label="t(`components.admin.settings.license.send_report`)"
>
<div style="font-weight: bold">
{{
page.license.SendReport && page.license.SendReportEmail !== ""
? page.license.SendReportEmail
: $t("components.admin.settings.license.send_report_no")
}}
</div>
</a-descriptions-item>
<a-descriptions-item
:label="t(`components.admin.settings.license.customer_name`)"
>
<div style="font-weight: bold">
{{ page.license.CustomerName }}
</div>
</a-descriptions-item>
<a-descriptions-item
:label="t(`components.admin.settings.license.customer_type`)"
>
<div style="font-weight: bold">
{{ page.license.CustomerTypeStr }}
</div>
</a-descriptions-item>
<a-descriptions-item
:label="t(`components.admin.settings.license.customer_data`)"
>
<a-descriptions :column="1">
<a-descriptions-item
v-for="{ Title, Value } in page.license.CustomerReqs"
:label="Title"
>
<div style="font-weight: bold">
{{ Value }}
</div>
</a-descriptions-item>
</a-descriptions>
</a-descriptions-item>
</a-descriptions>
<a-alert
style="margin-bottom: 16px"
v-else
type="error"
showIcon
:message="t(`components.admin.settings.license.no_install`)"
/>
<a-space style="display: flex; justify-content: center">
<a-button size="large" @click="page.showEula = true">
{{ $t("components.admin.settings.license.show_eula") }}
</a-button>
<a-upload
:file-list="page.files"
:before-upload="
(file: any) => {
upload(file)
return false
}
"
>
<a-button size="large" :loading="page.uploading" type="primary">
<UploadOutlined />
{{ $t("components.admin.settings.license.load") }}
</a-button>
</a-upload>
</a-space>
</div>
<div v-else>
<pre
style="
white-space: -moz-pre-wrap;
white-space: -pre-wrap;
white-space: -o-pre-wrap;
white-space: pre-wrap;
"
>
{{ $t("eula") }}
</pre>
<br />
<a-button
class="save-button"
type="primary"
size="large"
style="width: fit-content"
@click="accept"
>
{{ $t("components.admin.settings.license.accept") }}
</a-button>
</div>
</a-card>
<a-modal v-model:open="page.showEula" width="80%">
<template #footer></template>
<pre
style="
white-space: -moz-pre-wrap;
white-space: -pre-wrap;
white-space: -o-pre-wrap;
white-space: pre-wrap;
"
>
{{ $t("eula") }}
</pre
>
</a-modal>
</template>
<script setup lang="ts">
import { notifyError, notifySuccess } from "@/composables/alert";
import { apiFetch } from "@/composables/apiFetch";
import { ExclamationCircleOutlined } from "@ant-design/icons-vue";
import { Modal, type UploadChangeParam } from "ant-design-vue";
import { createVNode, onMounted, reactive } from "vue";
import { saveAndRestart } from "@/composables/restart";
import { message } from "ant-design-vue";
import { UploadOutlined } from "@ant-design/icons-vue";
import { useI18n } from "vue-i18n";
import { timeToDate, timeToDateTime, timeToFullDate } from "@/composables/misc";
import { uploadListProps } from "ant-design-vue/es/upload/interface";
const { t } = useI18n();
const page = reactive<{
uploading: boolean;
contentLoaded: boolean;
accepted: boolean;
showEula: boolean;
files: any;
license: {
ID: string;
Edition: string;
ExpiresUnix: number;
MailboxCount: number;
SendReport: boolean;
SendReportEmail: string;
CustomerName: string;
CustomerType: number;
CustomerTypeStr: string;
CustomerReqs: {
Title: string;
Value: string;
}[];
};
}>({
uploading: false,
files: [],
showEula: false,
contentLoaded: false,
accepted: false,
license: {
ID: "",
Edition: "",
ExpiresUnix: 0,
MailboxCount: 0,
SendReport: false,
SendReportEmail: "",
CustomerName: "",
CustomerType: 0,
CustomerTypeStr: "",
CustomerReqs: [],
},
});
onMounted(() => {
get();
});
async function get() {
const res = await apiFetch("/admin/settings/license");
if (res.error) {
notifyError(res.error);
return;
}
page.accepted = res.data.EulaAccepted;
page.license = res.data.License;
if (page.license)
if (page.license.CustomerType === 1) {
page.license.CustomerTypeStr = t(
"components.admin.settings.license.fiz_type"
);
} else if (page.license.CustomerType === 2) {
page.license.CustomerTypeStr = t(
"components.admin.settings.license.ur_type"
);
}
page.contentLoaded = true;
return;
}
async function accept() {
const res = await apiFetch("/admin/settings/license/accept", {
method: "POST",
});
if (res.error) {
notifyError(res.error);
return;
}
get();
}
async function upload(file: any) {
page.uploading = true;
const formData = new FormData();
formData.append("file", file);
const res = await apiFetch("/admin/settings/license", {
method: "POST",
body: formData,
isFormData: true,
});
page.uploading = false;
if (res.error) {
notifyError(res.error);
return;
}
get();
}
</script>
<style scoped>
.panel-content {
min-width: 600px;
max-width: 60%;
margin: auto;
}
.save-button {
width: 30%;
min-width: 150px;
display: block;
margin: auto;
}
</style>

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,257 @@
<template>
<div>
<a-card class="panel-content">
<a-form :label-col="labelCol" :wrapper-col="wrapperCol" :labelWrap="true">
<a-form-item :label="$t('components.admin.settings.settings_db.Type')">
<a-select v-model:value="page.new.Type" @change="getNew">
<a-select-option value="pgsqlSettingsDB"
>PostgreSQL</a-select-option
>
<a-select-option value="sqliteSettingsDB">SQLite</a-select-option>
</a-select>
</a-form-item>
<template v-if="page.new.Type === 'pgsqlSettingsDB'">
<a-form-item
:label="$t('components.admin.settings.settings_db.ConnParams.Host')"
>
<a-input v-model:value="page.new.ConnParams.Host"> </a-input>
</a-form-item>
<a-form-item
:label="$t('components.admin.settings.settings_db.ConnParams.Port')"
>
<a-input-number
class="form-input-number"
v-model:value="page.new.ConnParams.Port"
>
</a-input-number>
</a-form-item>
<a-form-item
:label="$t('components.admin.settings.settings_db.ConnParams.Db')"
>
<a-input v-model:value="page.new.ConnParams.Db"> </a-input>
</a-form-item>
<a-form-item
:label="$t('components.admin.settings.settings_db.ConnParams.User')"
>
<a-input v-model:value="page.new.ConnParams.User"> </a-input>
</a-form-item>
<a-form-item
:label="$t('components.admin.settings.settings_db.ConnParams.Pass')"
>
<a-input-password
autocomplete="off"
v-model:value="page.new.ConnParams.Pass"
>
</a-input-password>
</a-form-item>
<a-form-item
:label="
$t('components.admin.settings.settings_db.ConnParams.MaxConn')
"
>
<a-input-number
class="form-input-number"
:min="1"
v-model:value="page.new.ConnParams.MaxConn"
>
</a-input-number>
</a-form-item>
</template>
<template v-if="page.new.Type === 'sqliteSettingsDB'">
<a-form-item
:label="
$t('components.admin.settings.settings_db.PathParams.DatabaseDir')
"
>
<a-input v-model:value="page.new.PathParams.DatabaseDir"> </a-input>
</a-form-item>
</template>
<a-space style="display: flex; justify-content: center">
<a-button
:loading="page.checkLoading"
:disabled="!page.new.Type"
class="save-button"
size="large"
@click="check"
>
{{ $t("components.admin.settings.settings_db.check") }}
</a-button>
<a-button
:disabled="!page.new.Type || !page.contentLoaded"
class="save-button"
type="primary"
size="large"
style="width: fit-content"
@click="saveAndRestart(save)"
>
{{
page.old.Type != page.new.Type
? $t("components.admin.settings.settings_db.change")
: $t("components.admin.settings.settings_db.save")
}}
</a-button>
</a-space>
</a-form>
</a-card>
</div>
</template>
<script setup lang="ts">
import { notifyError, notifySuccess } from "@/composables/alert";
import { apiFetch } from "@/composables/apiFetch";
import { ExclamationCircleOutlined } from "@ant-design/icons-vue";
import { Modal } from "ant-design-vue";
import { createVNode, onMounted, reactive } from "vue";
import { saveAndRestart } from "@/composables/restart";
import { message } from "ant-design-vue";
import { useI18n } from "vue-i18n";
const { t } = useI18n();
interface ConnParams {
Host: string;
Port: number;
Db: string;
User: string;
Pass: string;
MaxConn: number;
}
interface PathParams {
DatabaseDir: string;
}
const page = reactive<{
contentLoaded: boolean;
old: {
Type: string;
ConnParams: ConnParams;
PathParams: PathParams;
};
new: {
Type: string;
ConnParams: ConnParams;
PathParams: PathParams;
};
checkLoading: boolean;
}>({
contentLoaded: false,
new: {
Type: "",
ConnParams: {
Host: "",
Port: 0,
Db: "",
User: "",
Pass: "",
MaxConn: 0,
},
PathParams: {
DatabaseDir: "",
},
},
checkLoading: false,
old: {
Type: "",
ConnParams: {
Host: "",
Port: 0,
Db: "",
User: "",
Pass: "",
MaxConn: 0,
},
PathParams: {
DatabaseDir: "",
},
},
});
const labelCol = { span: 16, style: { "text-align": "left" } };
const wrapperCol = { span: 8 };
onMounted(() => {
get();
});
async function get() {
const res = await apiFetch("/admin/settings/settings_db");
if (res.error) {
notifyError(res.error);
return;
}
page.new = { ...res.data };
page.old = { ...res.data };
page.contentLoaded = true;
return;
}
async function getNew() {
if (page.new.Type === page.old.Type) {
page.new = { ...page.old };
return;
}
const res = await apiFetch(
`/admin/settings/settings_db/new?type=${page.new.Type}`
);
if (res.error) {
notifyError(res.error);
return;
}
page.new = res.data;
return;
}
async function save(): Promise<boolean> {
const res = await apiFetch("/admin/settings/settings_db", {
method: "POST",
body: page.new,
});
if (res.error) {
notifyError(res.error);
return false;
}
return true;
}
async function check() {
page.checkLoading = true;
const res = await apiFetch("/admin/settings/settings_db/check", {
method: "POST",
body: page.new,
});
page.checkLoading = false;
if (res.error) {
notifyError(
t("components.admin.settings.settings_db.check_error") + res.error
);
return;
}
notifySuccess(t("components.admin.settings.settings_db.check_success"));
return;
}
</script>
<style scoped>
.panel-content {
width: 600px;
max-width: 60%;
margin: auto;
}
.input-number {
width: 50%;
}
.save-button {
}
</style>

View File

@ -0,0 +1,388 @@
<template>
<a-flex wrap justify="center" gap="large">
<a-card>
<a-statistic
:title="t('components.admin.settings.smtp_queue.manage.total')"
:value="page.total"
style="margin-right: 50px"
/>
</a-card>
<a-card>
<a-statistic
:title="t('components.admin.settings.smtp_queue.manage.new')"
:value="page.new"
style="margin-right: 50px"
/>
</a-card>
<a-card>
<a-statistic
:title="t('components.admin.settings.smtp_queue.manage.resend')"
:value="page.awaited"
style="margin-right: 50px"
/>
</a-card>
</a-flex>
<br />
<a-table
:columns="columns"
:data-source="page.data"
:loading="page.loading"
bordered
size="small"
:pagination="false"
>
<template #bodyCell="{ column, record }">
<template v-if="column.key === 'action'">
<a-space>
<a-button @click="processMsg(record.ID)">{{
t("components.admin.settings.smtp_queue.manage.process_now")
}}</a-button>
<a-popconfirm
:title="
t('components.admin.settings.smtp_queue.manage.delete') + '?'
"
:ok-text="t('components.admin.settings.smtp_queue.manage.ok')"
:cancel-text="
t('components.admin.settings.smtp_queue.manage.cancel')
"
@confirm="deleteMsg(record.ID)"
>
<a-button danger>
{{ t("components.admin.settings.smtp_queue.manage.delete") }}
</a-button>
</a-popconfirm>
</a-space>
</template>
<template v-if="column.key === 'WillResend'">
{{
record.WillResend === ""
? t(
"components.admin.settings.smtp_queue.manage.column_will_resend_now"
)
: record.WillResend
}}
</template>
</template>
<template #title>
<a-row>
<a-col style="display: flex; align-items: flex-end" :span="6">
<a-input-search
v-model:value="page.pagination.search"
:placeholder="
t('components.admin.settings.smtp_queue.manage.search')
"
enter-button
allow-clear
@search="get"
/>
</a-col>
<a-col :span="18">
<a-space
v-if="page.withSearch"
wrap
style="display: flex; justify-content: end"
>
<a-button @click="processAwaiting">{{
$t("components.admin.settings.smtp_queue.manage.process_filtered")
}}</a-button>
<a-popconfirm
:title="
t(
'components.admin.settings.smtp_queue.manage.clear_filtered'
) + '?'
"
:ok-text="t('components.admin.settings.smtp_queue.manage.ok')"
:cancel-text="
t('components.admin.settings.smtp_queue.manage.cancel')
"
@confirm="clearAll"
>
<a-button danger>{{
$t("components.admin.settings.smtp_queue.manage.clear_filtered")
}}</a-button>
</a-popconfirm>
</a-space>
<a-space v-else wrap style="display: flex; justify-content: end">
<a-button @click="processAwaiting">{{
$t(
"components.admin.settings.smtp_queue.manage.process_awaiting_resend"
)
}}</a-button>
<a-popconfirm
:title="
t(
'components.admin.settings.smtp_queue.manage.delete_awaiting_resend'
) + '?'
"
:ok-text="t('components.admin.settings.smtp_queue.manage.ok')"
:cancel-text="
t('components.admin.settings.smtp_queue.manage.cancel')
"
@confirm="deleteAwaiting"
>
<a-button danger>{{
$t(
"components.admin.settings.smtp_queue.manage.delete_awaiting_resend"
)
}}</a-button>
</a-popconfirm>
<a-popconfirm
:title="
t('components.admin.settings.smtp_queue.manage.clear_all') + '?'
"
:ok-text="t('components.admin.settings.smtp_queue.manage.ok')"
:cancel-text="
t('components.admin.settings.smtp_queue.manage.cancel')
"
@confirm="clearAll"
>
<a-button danger>{{
$t("components.admin.settings.smtp_queue.manage.clear_all")
}}</a-button>
</a-popconfirm>
</a-space>
</a-col>
</a-row>
</template>
<template #footer>
<a-flex justify="flex-end">
<!-- <a-space>
<a-popconfirm
:title="
t('components.admin.settings.smtp_queue.manage.clear_all') + '?'
"
:ok-text="t('components.admin.settings.smtp_queue.manage.ok')"
:cancel-text="
t('components.admin.settings.smtp_queue.manage.cancel')
"
@confirm="clearAll"
>
<a-button danger>{{
$t("components.admin.settings.smtp_queue.manage.clear_all")
}}</a-button>
</a-popconfirm>
<a-popconfirm
:title="
t(
'components.admin.settings.smtp_queue.manage.delete_awaiting_resend'
) + '?'
"
:ok-text="t('components.admin.settings.smtp_queue.manage.ok')"
:cancel-text="
t('components.admin.settings.smtp_queue.manage.cancel')
"
@confirm="deleteAwaiting"
>
<a-button danger>{{
$t(
"components.admin.settings.smtp_queue.manage.delete_awaiting_resend"
)
}}</a-button>
</a-popconfirm>
<a-button @click="processAwaiting">{{
$t(
"components.admin.settings.smtp_queue.manage.process_awaiting_resend"
)
}}</a-button>
</a-space> -->
<a-pagination
v-model:value="page.pagination.current"
:defaultPageSize="page.pagination.size"
:total="page.pagination.total"
@change="pageChange"
:pageSizeOptions="['10', '20', '50', '100']"
v-model:pageSize="page.pagination.size"
:showSizeChanger="true"
/>
</a-flex>
</template>
</a-table>
</template>
<script setup lang="ts">
import { notifyError } from "@/composables/alert";
import { apiFetch } from "@/composables/apiFetch";
import { DeleteOutlined } from "@ant-design/icons-vue";
import type { ColumnType } from "ant-design-vue/es/table";
import { onMounted, reactive } from "vue";
import { useI18n } from "vue-i18n";
const { t } = useI18n();
const columns: ColumnType<any>[] = [
{
title: t("components.admin.settings.smtp_queue.manage.column_sender"),
dataIndex: "Sender",
},
{
title: t("components.admin.settings.smtp_queue.manage.column_receiver"),
dataIndex: "Receiver",
},
{
title: t("components.admin.settings.smtp_queue.manage.column_added"),
dataIndex: "Added",
},
{
title: t("components.admin.settings.smtp_queue.manage.column_will_resend"),
dataIndex: "WillResend",
key: "WillResend",
},
{
key: "action",
align: "right",
},
];
const page = reactive<{
withSearch: boolean;
loading: boolean;
total: number;
new: number;
awaited: number;
data: any;
pagination: {
current: number;
total: number;
size: number;
search: string;
};
}>({
withSearch: false,
pagination: {
current: 1,
total: 0,
size: 50,
search: "",
},
loading: false,
total: 0,
new: 0,
awaited: 0,
data: [],
});
onMounted(() => {
get();
});
async function get() {
page.withSearch = page.pagination.search !== "";
page.loading = true;
const params = new URLSearchParams();
params.append("page", String(page.pagination.current));
params.append("size", String(page.pagination.size));
params.append("search", page.pagination.search);
const res = await apiFetch(
"/admin/settings/smtp_queue/messages?" + params.toString()
);
page.loading = false;
if (res.error) {
notifyError(res.error);
return;
}
page.total = res.data.Total;
page.new = res.data.New;
page.awaited = res.data.Awaited;
page.data = res.data.Messages;
page.pagination.total = res.total;
return;
}
function pageChange(current: number) {
page.pagination.current = current;
return get();
}
async function clearAll() {
const params = new URLSearchParams();
params.append("search", page.pagination.search);
const res = await apiFetch(
"/admin/settings/smtp_queue/messages/delete?" + params.toString(),
{
method: "POST",
}
);
if (res.error) {
notifyError(res.error);
return;
}
return get();
}
async function deleteMsg(id: string) {
const res = await apiFetch(
`/admin/settings/smtp_queue/messages/delete/${id}`,
{
method: "POST",
}
);
if (res.error) {
notifyError(res.error);
return;
}
return get();
}
async function processMsg(id: string) {
const res = await apiFetch(
`/admin/settings/smtp_queue/messages/retry/${id}`,
{
method: "POST",
}
);
if (res.error) {
notifyError(res.error);
return;
}
return get();
}
async function deleteAwaiting() {
const params = new URLSearchParams();
params.append("search", page.pagination.search);
const res = await apiFetch(
"/admin/settings/smtp_queue/messages/delete_awaiting?" + params.toString(),
{
method: "POST",
}
);
if (res.error) {
notifyError(res.error);
return;
}
return get();
}
async function processAwaiting() {
const params = new URLSearchParams();
params.append("search", page.pagination.search);
const res = await apiFetch(
"/admin/settings/smtp_queue/messages/retry?" + params.toString(),
{
method: "POST",
}
);
if (res.error) {
notifyError(res.error);
return;
}
return get();
}
</script>

View File

@ -0,0 +1,304 @@
<template>
<div>
<a-card class="panel-content">
<a-form :label-col="labelCol" :wrapper-col="wrapperCol" :labelWrap="true">
<a-form-item
:label="$t('components.admin.settings.smtp_queue.settings.Type')"
>
<a-select v-model:value="page.new.Type" @change="getNew">
<a-select-option value="pgsqlSmtpQueue">PostgreSQL</a-select-option>
<a-select-option value="sqliteFilesSmtpQueue"
>SQLite</a-select-option
>
</a-select>
</a-form-item>
<template
v-if="
page.new.Type === 'pgsqlSmtpQueue'
"
>
<a-form-item
:label="
$t(
'components.admin.settings.smtp_queue.settings.ConnParams.Host'
)
"
>
<a-input v-model:value="page.new.ConnParams.Host"> </a-input>
</a-form-item>
<a-form-item
:label="
$t(
'components.admin.settings.smtp_queue.settings.ConnParams.Port'
)
"
>
<a-input-number
class="form-input-number"
v-model:value="page.new.ConnParams.Port"
>
</a-input-number>
</a-form-item>
<a-form-item
:label="
$t('components.admin.settings.smtp_queue.settings.ConnParams.Db')
"
>
<a-input v-model:value="page.new.ConnParams.Db"> </a-input>
</a-form-item>
<a-form-item
:label="
$t(
'components.admin.settings.smtp_queue.settings.ConnParams.User'
)
"
>
<a-input v-model:value="page.new.ConnParams.User"> </a-input>
</a-form-item>
<a-form-item
:label="
$t(
'components.admin.settings.smtp_queue.settings.ConnParams.Pass'
)
"
>
<a-input-password
autocomplete="off"
v-model:value="page.new.ConnParams.Pass"
>
</a-input-password>
</a-form-item>
<a-form-item
:label="
$t(
'components.admin.settings.smtp_queue.settings.ConnParams.MaxConn'
)
"
>
<a-input-number
:min="1"
class="form-input-number"
v-model:value="page.new.ConnParams.MaxConn"
>
</a-input-number>
</a-form-item>
<a-form-item
:label="
$t(
'components.admin.settings.smtp_queue.settings.ConnParams.MaxQueueItems'
)
"
>
<a-input-number
:min="20"
class="form-input-number"
v-model:value="page.new.ConnParams.MaxQueueItems"
>
</a-input-number>
</a-form-item>
</template>
<template v-if="page.new.Type === 'sqliteFilesSmtpQueue'">
<a-form-item
:label="
$t(
'components.admin.settings.smtp_queue.settings.PathParams.DatabaseDir'
)
"
>
<a-input v-model:value="page.new.PathParams.SMTPQueueDir">
</a-input>
</a-form-item>
</template>
<a-space style="display: flex; justify-content: center">
<a-button
:loading="page.checkLoading"
:disabled="!page.new.Type"
class="save-button"
size="large"
@click="check"
>
{{ $t("components.admin.settings.smtp_queue.settings.check") }}
</a-button>
<a-button
:disabled="!page.new.Type || !page.contentLoaded"
class="save-button"
type="primary"
size="large"
style="width: fit-content"
@click="saveAndRestart(save)"
>
{{
page.old.Type != page.new.Type
? $t("components.admin.settings.smtp_queue.settings.change")
: $t("components.admin.settings.smtp_queue.settings.save")
}}
</a-button>
</a-space>
</a-form>
</a-card>
</div>
</template>
<script setup lang="ts">
import { notifyError, notifySuccess } from "@/composables/alert";
import { apiFetch } from "@/composables/apiFetch";
import { ExclamationCircleOutlined } from "@ant-design/icons-vue";
import { Modal } from "ant-design-vue";
import { createVNode, onMounted, reactive } from "vue";
import { saveAndRestart } from "@/composables/restart";
import { message } from "ant-design-vue";
import { useI18n } from "vue-i18n";
const { t } = useI18n();
interface ConnParams {
Host: string;
Port: number;
Db: string;
User: string;
Pass: string;
MaxConn: number;
MaxQueueItems: number;
}
interface PathParams {
SMTPQueueDir: string;
}
const page = reactive<{
contentLoaded: boolean;
old: {
Type: string;
ConnParams: ConnParams;
PathParams: PathParams;
};
new: {
Type: string;
ConnParams: ConnParams;
PathParams: PathParams;
};
checkLoading: boolean;
}>({
contentLoaded: false,
new: {
Type: "",
ConnParams: {
Host: "",
Port: 0,
Db: "",
User: "",
Pass: "",
MaxConn: 0,
MaxQueueItems: 0,
},
PathParams: {
SMTPQueueDir: "",
},
},
checkLoading: false,
old: {
Type: "",
ConnParams: {
Host: "",
Port: 0,
Db: "",
User: "",
Pass: "",
MaxConn: 0,
MaxQueueItems: 0,
},
PathParams: {
SMTPQueueDir: "",
},
},
});
const labelCol = { span: 16, style: { "text-align": "left" } };
const wrapperCol = { span: 8 };
onMounted(() => {
get();
});
async function get() {
const res = await apiFetch("/admin/settings/smtp_queue/settings");
if (res.error) {
notifyError(res.error);
return;
}
page.new = { ...res.data };
page.old = { ...res.data };
page.contentLoaded = true;
return;
}
async function getNew() {
if (page.new.Type === page.old.Type) {
page.new = { ...page.old };
return;
}
const res = await apiFetch(
`/admin/settings/smtp_queue/settings/new?type=${page.new.Type}`
);
if (res.error) {
notifyError(res.error);
return;
}
page.new = res.data;
return;
}
async function save(): Promise<boolean> {
const res = await apiFetch("/admin/settings/smtp_queue/settings", {
method: "POST",
body: page.new,
});
if (res.error) {
notifyError(res.error);
return false;
}
return true;
}
async function check() {
page.checkLoading = true;
const res = await apiFetch("/admin/settings/smtp_queue/settings/check", {
method: "POST",
body: page.new,
});
page.checkLoading = false;
if (res.error) {
notifyError(
t("components.admin.settings.smtp_queue.settings.check_error") + res.error
);
return;
}
notifySuccess(
t("components.admin.settings.smtp_queue.settings.check_success")
);
return;
}
</script>
<style scoped>
.panel-content {
min-width: 600px;
max-width: 60%;
margin: auto;
}
.save-button {
}
</style>

View File

@ -0,0 +1,815 @@
<template>
<div>
<a-table
class="panel-content"
:columns="columns"
:data-source="page.data"
:loading="page.loading"
bordered
:pagination="false"
>
<template #title>
<a-row>
<a-col style="display: flex; align-items: flex-end" :span="18">
<a-space>
<a-input-search
v-model:value="page.pagination.search"
:placeholder="
t('components.common.shared_address_books.search')
"
enter-button
allow-clear
@search="get"
/>
</a-space>
</a-col>
<a-col :span="6">
<a-space style="display: flex; justify-content: end">
<a-button type="primary" @click="showAddModal">
<PlusOutlined />
{{ t("components.common.shared_address_books.add_book") }}
</a-button>
<a-popconfirm
v-if="page.withSearch"
:title="
t('components.common.shared_address_books.delete_found') + '?'
"
:ok-text="t('components.common.shared_address_books.ok')"
:cancel-text="
t('components.common.shared_address_books.cancel')
"
@confirm="deleteBook(undefined)"
>
<a-button danger>{{
$t("components.common.shared_address_books.delete_found")
}}</a-button>
</a-popconfirm>
<a-popconfirm
v-else
:title="
t('components.common.shared_address_books.delete_all') + '?'
"
:ok-text="t('components.common.shared_address_books.ok')"
:cancel-text="
t('components.common.shared_address_books.cancel')
"
@confirm="deleteBook(undefined)"
>
<a-button danger>{{
$t("components.common.shared_address_books.delete_all")
}}</a-button>
</a-popconfirm>
</a-space>
</a-col>
</a-row>
</template>
<template #bodyCell="{ column, record }">
<template v-if="column.key === 'action'">
<a-space>
<a-button type="primary" @click="showAccessModal(record.Id)">{{
t("components.common.shared_address_books.edit_access")
}}</a-button>
<a-button @click="showPropModal(record.Id, record.Name)">{{
t("components.common.shared_address_books.properties")
}}</a-button>
<a-popconfirm
:title="t('components.common.shared_address_books.delete') + '?'"
:ok-text="t('components.common.shared_address_books.ok')"
:cancel-text="t('components.common.shared_address_books.cancel')"
@confirm="deleteBook(record.Id)"
>
<a-button danger>{{
t("components.common.shared_address_books.delete")
}}</a-button>
</a-popconfirm>
</a-space>
</template>
</template>
<template #footer>
<a-flex justify="flex-end">
<a-pagination
v-model:value="page.pagination.current"
:defaultPageSize="page.pagination.size"
:total="page.pagination.total"
@change="pageChange"
:pageSizeOptions="['10', '20', '50', '100']"
v-model:pageSize="page.pagination.size"
:showSizeChanger="true"
/>
</a-flex>
</template>
</a-table>
</div>
<a-modal
style="width: 300px"
v-model:open="page.add.show"
:title="t('components.common.shared_address_books.add_book')"
>
<a-input
style="margin-top: 8px"
:placeholder="$t('components.common.shared_address_books.add_name')"
v-model:value="page.add.Name"
>
</a-input>
<template #footer>
<a-button type="primary" :disabled="!page.add.Name" @click="addBook">
{{ t("components.common.shared_address_books.add_book") }}
</a-button>
</template>
</a-modal>
<a-modal
style="width: 300px"
v-model:open="page.props.show"
:title="t('components.common.shared_address_books.properties')"
>
<a-input
:placeholder="$t('components.common.shared_address_books.add_name')"
v-model:value="page.props.Name"
>
</a-input>
<template #footer>
<a-button type="primary" :disabled="!page.props.Name" @click="updateBook">
{{ t("components.common.shared_address_books.update_book") }}
</a-button>
</template>
</a-modal>
<a-modal
v-model:open="page.access.show"
style="width: 50%; min-width: 720px"
@cancel="get"
>
<a-table
:columns="accessColumns"
:data-source="page.access.data"
:loading="page.access.loading"
bordered
size="small"
:pagination="false"
>
<template #title>
<a-row>
<a-col style="display: flex; align-items: flex-end" :span="18">
<a-space>
<a-radio-group
@change="getAccess()"
v-model:value="page.access.type"
>
<a-radio-button value="email">{{
t("components.common.shared_address_books.access_type_email")
}}</a-radio-button>
<a-radio-button value="group">{{
t("components.common.shared_address_books.access_type_group")
}}</a-radio-button>
</a-radio-group>
<a-input-search
v-model:value="page.access.pagination.search"
:placeholder="
t('components.common.shared_address_books.access_search')
"
enter-button
allow-clear
@search="getAccess"
/>
</a-space>
</a-col>
<a-col
style="display: flex; align-items: flex-end; justify-content: end"
:span="6"
>
<a-space>
<a-button
v-if="page.access.type === 'email'"
type="primary"
@click="page.access.showAdd = true"
>
{{
t(
"components.common.shared_address_books.access_type_email_add"
)
}}
</a-button>
<a-button
v-if="page.access.type === 'group'"
type="primary"
@click="page.access.showAdd = true"
>
{{
t(
"components.common.shared_address_books.access_type_group_add"
)
}}
</a-button>
<a-popconfirm
v-if="page.access.withSearch"
:title="
t('components.common.shared_address_books.delete_found') + '?'
"
:ok-text="t('components.common.shared_address_books.ok')"
:cancel-text="
t('components.common.shared_address_books.cancel')
"
@confirm="deleteAccess('')"
>
<a-button danger>{{
$t("components.common.shared_address_books.delete_found")
}}</a-button>
</a-popconfirm>
<a-popconfirm
v-else
:title="
t('components.common.shared_address_books.delete_all') + '?'
"
:ok-text="t('components.common.shared_address_books.ok')"
:cancel-text="
t('components.common.shared_address_books.cancel')
"
@confirm="deleteAccess('')"
>
<a-button danger>{{
$t("components.common.shared_address_books.delete_all")
}}</a-button>
</a-popconfirm>
</a-space>
</a-col>
</a-row>
</template>
<template #bodyCell="{ column, record }">
<template v-if="column.key === 'action'">
<a-popconfirm
:title="t('components.common.shared_address_books.delete') + '?'"
:ok-text="t('components.common.shared_address_books.ok')"
:cancel-text="t('components.common.shared_address_books.cancel')"
@confirm="deleteAccess(record.Name)"
>
<a-button danger>{{
t("components.common.shared_address_books.delete")
}}</a-button>
</a-popconfirm>
</template>
<template v-if="column.key === 'perm'">
<a-select
v-model:value="record.Perm"
style="min-width: 150px"
@change="updateAccess(record.Name, record.Perm)"
>
<a-select-option value="read">
{{
t("components.common.shared_address_books.access_perm_read")
}}</a-select-option
>
<a-select-option value="all">
{{
t("components.common.shared_address_books.access_perm_all")
}}</a-select-option
>
</a-select>
</template>
</template>
<template #footer>
<a-flex justify="flex-end">
<a-pagination
v-model:value="page.access.pagination.current"
:defaultPageSize="page.access.pagination.size"
:total="page.access.pagination.total"
@change="accessPageChange"
:pageSizeOptions="['10', '20', '50', '100']"
v-model:pageSize="page.access.pagination.size"
:showSizeChanger="true"
/>
</a-flex>
</template>
</a-table>
<template #footer> </template>
</a-modal>
<a-modal
style="width: 300px"
v-model:open="page.access.showAdd"
:title="
page.access.type === 'email'
? t('components.common.shared_address_books.access_type_email_add')
: t('components.common.shared_address_books.access_type_group_add')
"
>
<br />
<a-form>
<a-form-item>
<template v-if="page.access.type === 'email'">
<a-input
v-model:value="page.access.inputValue"
:placeholder="
t(
'components.common.shared_address_books.access_type_email_placeholder'
)
"
>
</a-input>
</template>
<template v-if="page.access.type === 'group'">
<a-select
style="width: 100%"
:disabled="page.access.groups.length === 0"
v-model:value="page.access.selectValue"
:options="page.access.groups"
>
</a-select>
</template>
</a-form-item>
<a-form-item>
<a-select v-model:value="page.access.perm">
<a-select-option value="read">
{{
t("components.common.shared_address_books.access_perm_read")
}}</a-select-option
>
<a-select-option value="all">
{{
t("components.common.shared_address_books.access_perm_all")
}}</a-select-option
>
</a-select>
</a-form-item>
</a-form>
<template #footer>
<a-button
:disabled="
(page.access.type === 'email' && page.access.inputValue === '') ||
(page.access.type === 'group' && page.access.selectValue === '')
"
type="primary"
@click="
addAccess(
page.access.type === 'email'
? page.access.inputValue
: page.access.selectValue
)
"
>
{{ t("components.common.shared_address_books.access_add") }}
</a-button>
</template>
</a-modal>
</template>
<script setup lang="ts">
import { notifyError } from "@/composables/alert";
import { apiFetch } from "@/composables/apiFetch";
import router from "@/router";
import Export from "@/components/admin/domains/mailstorage/Export.vue";
import {
DomainPlaceholder,
MailboxPlaceholder,
RouteAdminDomainsDomainMailStorageMailboxesExport,
} from "@/router/consts";
import {
ArrowLeftOutlined,
DeleteOutlined,
DownOutlined,
ExclamationCircleOutlined,
HomeOutlined,
PlusOutlined,
} from "@ant-design/icons-vue";
import { Modal } from "ant-design-vue";
import type { ColumnType } from "ant-design-vue/es/table";
import { createVNode, onMounted, reactive } from "vue";
import { useI18n } from "vue-i18n";
import { useRoute } from "vue-router";
const route = useRoute();
const { t } = useI18n();
const columns: ColumnType<any>[] = [
{
title: t("components.common.shared_address_books.col_name"),
dataIndex: "Name",
},
{
key: "action",
align: "right",
},
];
const accessColumns: ColumnType<any>[] = [
{
title: t("components.common.shared_address_books.access_col_name"),
dataIndex: "Name",
},
{
title: t("components.common.shared_address_books.access_col_permissions"),
dataIndex: "Perm",
key: "perm",
},
{
key: "action",
align: "right",
},
];
const props = defineProps<{
domain?: string;
user: string;
}>();
const page = reactive<{
all_groups: string[];
withSearch: boolean;
loading: boolean;
data: any;
pagination: {
current: number;
total: number;
size: number;
search: string;
};
add: {
show: boolean;
Name: string;
};
props: {
show: boolean;
Name: string;
Id: number;
};
access: {
inputValue: string;
selectValue: string;
perm: string;
type: string;
data: any[];
loading: boolean;
show: boolean;
Id: number;
groups: any[];
pagination: {
current: number;
total: number;
size: number;
search: string;
};
showAdd: boolean;
withSearch: boolean;
};
}>({
withSearch: false,
pagination: {
current: 1,
total: 0,
size: 50,
search: "",
},
loading: false,
data: [],
add: {
show: false,
Name: "",
},
all_groups: [],
access: {
withSearch: false,
inputValue: "",
selectValue: "",
perm: "read",
type: "users",
data: [],
loading: false,
show: false,
Id: 0,
groups: [],
pagination: {
current: 1,
total: 0,
size: 50,
search: "",
},
showAdd: false,
},
props: {
show: false,
Name: "",
Id: 0,
},
});
onMounted(() => {
get();
getGroups();
});
async function getGroups() {
let url = "";
if (props.domain) {
url = `/admin/domains/${props.domain}/shared_address_books/groups`;
} else {
url = `/user/address_books/groups`;
}
const res = await apiFetch(url);
if (res.error) {
notifyError(res.error);
return;
}
page.all_groups = res.data;
return;
}
async function get() {
page.withSearch = page.pagination.search !== "";
page.loading = true;
const params = new URLSearchParams();
params.append("page", String(page.pagination.current));
params.append("size", String(page.pagination.size));
params.append("search", page.pagination.search);
let url = "";
if (props.domain) {
url = `/admin/domains/${props.domain}/shared_address_books?`;
} else {
url = `/user/address_books?`;
}
const res = await apiFetch(url + params.toString());
page.loading = false;
if (res.error) {
notifyError(res.error);
return;
}
page.data = res.data;
page.pagination.total = res.total;
return;
}
function pageChange(current: number) {
page.pagination.current = current;
return get();
}
function accessPageChange(current: number) {
page.access.pagination.current = current;
return getAccess();
}
async function addBook() {
let url = "";
if (props.domain) {
url = `/admin/domains/${props.domain}/shared_address_books`;
} else {
url = `/user/address_books`;
}
const res = await apiFetch(url, {
method: "POST",
body: {
Name: page.add.Name,
},
});
if (res.error) {
notifyError(res.error);
return;
}
page.add.show = false;
return get();
}
async function updateBook() {
let url = "";
if (props.domain) {
url = `/admin/domains/${props.domain}/shared_address_books`;
} else {
url = `/user/address_books`;
}
const res = await apiFetch(url, {
method: "PATCH",
body: {
Name: page.props.Name,
Id: page.props.Id,
},
});
if (res.error) {
notifyError(res.error);
return;
}
page.props.show = false;
return get();
}
async function showAccessModal(id: number) {
page.access.Id = id;
page.access.show = true;
page.access.type = "email";
page.access.inputValue = "";
getAccess();
}
async function showPropModal(id: number, name: string) {
page.props.Name = name;
page.props.show = true;
page.props.Id = id;
}
async function showAddModal() {
page.add.show = true;
page.add.Name = "";
}
async function deleteBook(id: number | undefined) {
const params = new URLSearchParams();
params.append("search", page.pagination.search);
let url = "";
if (props.domain) {
url = `/admin/domains/${props.domain}/shared_address_books?`;
} else {
url = `/user/address_books?`;
}
const res = await apiFetch(url + params.toString(), {
method: "DELETE",
body: {
Id: id,
},
});
if (res.error) {
notifyError(res.error);
return;
}
return get();
}
async function getAccess() {
page.access.withSearch = page.access.pagination.search !== "";
page.access.loading = true;
const params = new URLSearchParams();
params.append("id", page.access.Id.toString());
params.append("type", page.access.type);
params.append("page", String(page.access.pagination.current));
params.append("size", String(page.access.pagination.size));
params.append("search", page.access.pagination.search);
let url = "";
if (props.domain) {
url = `/admin/domains/${props.domain}/shared_address_books/access?`;
} else {
url = `/user/address_books/access?`;
}
const res = await apiFetch(url + params.toString());
page.access.loading = false;
if (res.error) {
notifyError(res.error);
return;
}
page.access.data = [];
if (res.data) {
for (let index = 0; index < res.data.length; index++) {
const element = res.data[index];
page.access.data.push({
Name: element.AccessTo,
Perm: element.Permissions,
});
}
}
page.access.pagination.total = res.total;
page.access.groups = [];
page.access.selectValue = "";
if (page.access.type === "group") {
for (let j = 0; j < page.all_groups.length; j++) {
const group = page.all_groups[j];
let skip = false;
for (let i = 0; i < page.access.data.length; i++) {
const alreadyHas = page.access.data[i];
if (alreadyHas.Name === group) {
skip = true;
break;
}
}
if (!skip) {
page.access.groups.push({ value: group });
}
}
}
if (page.access.groups.length) {
page.access.selectValue = page.access.groups[0].value;
}
return;
}
async function deleteAccess(access: string) {
const params = new URLSearchParams();
params.append("id", page.access.Id.toString());
params.append("type", page.access.type);
params.append("page", String(page.access.pagination.current));
params.append("size", String(page.access.pagination.size));
params.append("search", page.access.pagination.search);
let url = "";
if (props.domain) {
url = `/admin/domains/${props.domain}/shared_address_books/access?`;
} else {
url = `/user/address_books/access?`;
}
const res = await apiFetch(url + params.toString(), {
method: "DELETE",
body: {
Access: access,
},
});
if (res.error) {
notifyError(res.error);
return;
}
return getAccess();
}
async function updateAccess(access: string, perm: string) {
const params = new URLSearchParams();
params.append("id", page.access.Id.toString());
params.append("type", page.access.type);
let url = "";
if (props.domain) {
url = `/admin/domains/${props.domain}/shared_address_books/access?`;
} else {
url = `/user/address_books/access?`;
}
const res = await apiFetch(url + params.toString(), {
method: "PATCH",
body: {
Access: access,
Perm: perm,
},
});
if (res.error) {
notifyError(res.error);
return;
}
return getAccess();
}
async function addAccess(access: string) {
const params = new URLSearchParams();
params.append("id", page.access.Id.toString());
params.append("type", page.access.type);
let url = "";
if (props.domain) {
url = `/admin/domains/${props.domain}/shared_address_books/access?`;
} else {
url = `/user/address_books/access?`;
}
const res = await apiFetch(url + params.toString(), {
method: "POST",
body: {
Access: access,
Perm: page.access.perm,
},
});
if (res.error) {
notifyError(res.error);
return;
}
page.access.showAdd = false;
return getAccess();
}
</script>
<style scoped>
.panel-content {
min-width: 700px;
max-width: 70%;
margin: auto;
}
</style>

View File

@ -0,0 +1,475 @@
<template>
<div >
<a-table class="panel-content"
:columns="columns"
:data-source="page.data"
:loading="page.loading"
bordered
:pagination="false"
>
<template #title>
<a-row>
<a-col style="display: flex; align-items: flex-end" :span="18">
<a-space>
<a-input-search
v-model:value="page.pagination.search"
:placeholder="t('components.common.shared_calendars.search')"
enter-button
allow-clear
@search="get"
/>
</a-space>
</a-col>
<a-col :span="6">
<a-space style="display: flex; justify-content: end">
<a-button type="primary" @click="showAddModal">
<PlusOutlined />
{{ t("components.common.shared_calendars.add_calendar") }}
</a-button>
<a-popconfirm
v-if="page.withSearch"
:title="
t('components.common.shared_calendars.delete_found') + '?'
"
:ok-text="t('components.common.shared_calendars.ok')"
:cancel-text="t('components.common.shared_calendars.cancel')"
@confirm="deleteCalendar(undefined)"
>
<a-button danger>{{
$t("components.common.shared_calendars.delete_found")
}}</a-button>
</a-popconfirm>
<a-popconfirm
v-else
:title="
t('components.common.shared_calendars.delete_all') + '?'
"
:ok-text="t('components.common.shared_calendars.ok')"
:cancel-text="t('components.common.shared_calendars.cancel')"
@confirm="deleteCalendar(undefined)"
>
<a-button danger>{{
$t("components.common.shared_calendars.delete_all")
}}</a-button>
</a-popconfirm>
</a-space>
</a-col>
</a-row>
</template>
<template #bodyCell="{ column, record }">
<template v-if="column.key === 'action'">
<a-space>
<a-button type="primary" @click="showAccessModal(record.Id)">{{
t("components.common.shared_calendars.edit_access")
}}</a-button>
<a-button @click="showPropModal(record)">{{
t("components.common.shared_calendars.properties")
}}</a-button>
<a-popconfirm
:title="t('components.common.shared_calendars.delete') + '?'"
:ok-text="t('components.common.shared_calendars.ok')"
:cancel-text="t('components.common.shared_calendars.cancel')"
@confirm="deleteCalendar(record.Id)"
>
<a-button danger>{{
t("components.common.shared_calendars.delete")
}}</a-button>
</a-popconfirm>
</a-space>
</template>
</template>
<template #footer>
<a-flex justify="flex-end">
<a-pagination
v-model:value="page.pagination.current"
:defaultPageSize="page.pagination.size"
:total="page.pagination.total"
@change="pageChange"
:pageSizeOptions="['10', '20', '50', '100']"
v-model:pageSize="page.pagination.size"
:showSizeChanger="true"
/>
</a-flex>
</template>
</a-table>
</div>
<a-modal
style="width: 600px"
v-model:open="page.add.show"
:title="t('components.common.shared_calendars.add_calendar')"
>
<a-divider></a-divider>
<a-form :label-col="labelCol" :wrapper-col="wrapperCol" labelWrap>
<a-form-item :label="$t('components.common.shared_calendars.add_name')"
:rules="[{ required: true }]"
>
<a-input v-model:value="page.add.cal.Name"> </a-input>
</a-form-item>
<a-form-item
:label="$t('components.common.shared_calendars.add_freebusy')"
>
<a-switch v-model:checked="page.add.cal.AllowFreebusy"> </a-switch>
</a-form-item>
<a-form-item
:label="$t('components.common.shared_calendars.add_showfrom')"
>
<a-input-number :min="0" v-model:value="page.add.cal.ShowFromNWeeks">
</a-input-number>
</a-form-item>
<a-form-item :label="$t('components.common.shared_calendars.add_showto')">
<a-input-number :min="0" v-model:value="page.add.cal.ShowToNWeeks">
</a-input-number>
</a-form-item>
<a-form-item :label="$t('components.common.shared_calendars.add_color')">
<input type="color" :value="page.add.cal.Color" @input="(e: any) => (page.add.cal.Color = e.target.value)">
</input>
</a-form-item>
</a-form>
<template #footer>
<a-button
type="primary"
:disabled="!page.add.cal.Name"
@click="addCalendar"
>
{{ t("components.common.shared_calendars.add_calendar") }}
</a-button>
</template>
</a-modal>
<a-modal
style="width: 600px"
v-model:open="page.props.show"
:title="t('components.common.shared_calendars.properties')"
>
<a-divider></a-divider>
<a-form :label-col="labelCol" :wrapper-col="wrapperCol" labelWrap>
<a-form-item :label="$t('components.common.shared_calendars.add_name')"
:rules="[{ required: true }]"
>
<a-input v-model:value="page.props.cal.Name"> </a-input>
</a-form-item>
<a-form-item
:label="$t('components.common.shared_calendars.add_freebusy')"
>
<a-switch v-model:checked="page.props.cal.AllowFreebusy"> </a-switch>
</a-form-item>
<a-form-item
:label="$t('components.common.shared_calendars.add_showfrom')"
>
<a-input-number :min="0" v-model:value="page.props.cal.ShowFromNWeeks">
</a-input-number>
</a-form-item>
<a-form-item :label="$t('components.common.shared_calendars.add_showto')">
<a-input-number :min="0" v-model:value="page.props.cal.ShowToNWeeks">
</a-input-number>
</a-form-item>
<a-form-item :label="$t('components.common.shared_calendars.add_color')">
<input type="color" :value="page.props.cal.Color" @input="(e: any) => (page.props.cal.Color = e.target.value)">
</input>
</a-form-item>
</a-form>
<template #footer>
<a-button
type="primary"
:disabled="!page.props.cal.Name"
@click="updateCalendar"
>
{{ t("components.common.shared_calendars.update_calendar") }}
</a-button>
</template>
</a-modal>
<CalendarsAccess v-if="page.access.show"
v-model:open="page.access.show"
:type="page.access.type"
:id="page.access.Id"
:domain="props.domain"
:resources="false"
>
</CalendarsAccess>
</template>
<script setup lang="ts">
import { notifyError } from "@/composables/alert";
import { apiFetch } from "@/composables/apiFetch";
import router from "@/router";
import Export from "@/components/admin/domains/mailstorage/Export.vue";
import {
DomainPlaceholder,
MailboxPlaceholder,
RouteAdminDomainsDomainMailStorageMailboxesExport,
} from "@/router/consts";
import {
ArrowLeftOutlined,
DeleteOutlined,
DownOutlined,
ExclamationCircleOutlined,
HomeOutlined,
PlusOutlined,
} from "@ant-design/icons-vue";
import { Modal } from "ant-design-vue";
import type { ColumnType } from "ant-design-vue/es/table";
import { createVNode, onMounted, reactive } from "vue";
import { useI18n } from "vue-i18n";
import { useRoute } from "vue-router";
import CalendarsAccess from "./CalendarsAccess.vue";
const route = useRoute();
const { t } = useI18n();
const labelCol = { span: 16, style: { "text-align": "left" } };
const wrapperCol = { span: 8 };
const columns: ColumnType<any>[] = [
{
title: t("components.common.shared_calendars.col_name"),
dataIndex: "Name",
},
{
key: "action",
align: "right",
},
];
const props = defineProps<{
domain?: string;
user: string;
}>();
interface Calendar {
Id: number;
Name: string;
ShowFromNWeeks: string;
ShowToNWeeks: string;
AllowFreebusy: boolean;
Color: string;
}
const page = reactive<{
all_groups: string[];
withSearch: boolean;
loading: boolean;
data: any;
pagination: {
current: number;
total: number;
size: number;
search: string;
};
add: {
show: boolean;
cal: Calendar;
};
props: {
show: boolean;
cal: Calendar;
};
access: {
perm: string;
type: string;
show: boolean;
Id: number;
};
}>({
withSearch: false,
pagination: {
current: 1,
total: 0,
size: 50,
search: "",
},
loading: false,
data: [],
add: {
show: false,
cal: {
Id: 0,
Name: "",
ShowFromNWeeks: "",
ShowToNWeeks: "",
AllowFreebusy: false,
Color: "",
},
},
all_groups: [],
access: {
perm: "read",
type: "users",
show: false,
Id: 0,
},
props: {
show: false,
cal: {
Id: 0,
Name: "",
ShowFromNWeeks: "",
ShowToNWeeks: "",
AllowFreebusy: false,
Color: "",
},
},
});
onMounted(() => {
get();
});
async function get() {
page.withSearch = page.pagination.search !== "";
page.loading = true;
const params = new URLSearchParams();
params.append("page", String(page.pagination.current));
params.append("size", String(page.pagination.size));
params.append("search", page.pagination.search);
let url = "";
if (props.domain) {
url = `/admin/domains/${props.domain}/shared_calendars?`;
} else {
url = `/user/calendars?`;
}
const res = await apiFetch(url + params.toString());
page.loading = false;
if (res.error) {
notifyError(res.error);
return;
}
page.data = res.data;
page.pagination.total = res.total;
return;
}
function pageChange(current: number) {
page.pagination.current = current;
return get();
}
async function addCalendar() {
let url = "";
if (props.domain) {
url = `/admin/domains/${props.domain}/shared_calendars`;
} else {
url = `/user/calendars`;
}
const res = await apiFetch(url, {
method: "POST",
body: page.add.cal,
});
if (res.error) {
notifyError(res.error);
return;
}
page.add.show = false;
return get();
}
async function updateCalendar() {
let url = "";
if (props.domain) {
url = `/admin/domains/${props.domain}/shared_calendars`;
} else {
url = `/user/calendars`;
}
const res = await apiFetch(url, {
method: "PATCH",
body: page.props.cal,
});
if (res.error) {
notifyError(res.error);
return;
}
page.props.show = false;
return get();
}
async function showAccessModal(id: number) {
page.access.Id = id;
page.access.show = true;
page.access.type = "email";
}
async function showPropModal(record: any) {
page.props.cal = {...record};
page.props.show = true;
}
async function showAddModal() {
let url = "";
if (props.domain) {
url = `/admin/domains/${props.domain}/shared_calendars/default`;
} else {
url = `/user/calendars/default`;
}
const res = await apiFetch(url);
if (res.error) {
notifyError(res.error);
return;
}
page.add.show = true;
page.add.cal.Name = "";
page.add.cal.ShowFromNWeeks = res.data.ShowFromNWeeks;
page.add.cal.ShowToNWeeks = res.data.ShowToNWeeks;
page.add.cal.AllowFreebusy = res.data.AllowFreebusy;
page.add.cal.Color = res.data.Color;
}
async function deleteCalendar(id: number | undefined) {
const params = new URLSearchParams();
params.append("search", page.pagination.search);
let url = "";
if (props.domain) {
url = `/admin/domains/${props.domain}/shared_calendars?`;
} else {
url = `/user/calendars?`;
}
const res = await apiFetch(url + params.toString(), {
method: "DELETE",
body: {
Id: id,
},
});
if (res.error) {
notifyError(res.error);
return;
}
return get();
}
</script>
<style scoped>
.panel-content {
min-width: 700px;
max-width: 70%;
margin: auto;
}
</style>

View File

@ -0,0 +1,504 @@
<template>
<a-modal style="width: 50%; min-width: 720px" v-model:open="openModel">
<a-table
:columns="accessColumns"
:data-source="page.access.data"
:loading="page.access.loading"
bordered
size="small"
:pagination="false"
>
<template #title>
<a-row>
<a-col style="display: flex; align-items: flex-end" :span="18">
<a-space>
<a-radio-group
v-if="!props.resources"
@change="getAccess()"
v-model:value="page.access.type"
>
<a-radio-button value="email">{{
t("components.common.shared_calendars.access_type_email")
}}</a-radio-button>
<a-radio-button value="group">{{
t("components.common.shared_calendars.access_type_group")
}}</a-radio-button>
</a-radio-group>
<a-input-search
v-model:value="page.access.pagination.search"
:placeholder="
t('components.common.shared_calendars.access_search')
"
enter-button
allow-clear
@search="getAccess"
/>
</a-space>
</a-col>
<a-col
style="display: flex; align-items: flex-end; justify-content: end"
:span="6"
>
<a-space>
<a-button
v-if="page.access.type === 'email'"
type="primary"
@click="page.access.showAdd = true"
>
{{
t("components.common.shared_calendars.access_type_email_add")
}}
</a-button>
<a-button
v-if="page.access.type === 'group'"
type="primary"
@click="page.access.showAdd = true"
>
{{
t("components.common.shared_calendars.access_type_group_add")
}}
</a-button>
<a-popconfirm
v-if="page.access.withSearch"
:title="
t('components.common.shared_calendars.delete_found') + '?'
"
:ok-text="t('components.common.shared_calendars.ok')"
:cancel-text="t('components.common.shared_calendars.cancel')"
@confirm="deleteAccess('')"
>
<a-button danger>{{
$t("components.common.shared_calendars.delete_found")
}}</a-button>
</a-popconfirm>
<a-popconfirm
v-else
:title="
t('components.common.shared_calendars.delete_all') + '?'
"
:ok-text="t('components.common.shared_calendars.ok')"
:cancel-text="t('components.common.shared_calendars.cancel')"
@confirm="deleteAccess('')"
>
<a-button danger>{{
$t("components.common.shared_calendars.delete_all")
}}</a-button>
</a-popconfirm>
</a-space>
</a-col>
</a-row>
</template>
<template #bodyCell="{ column, record }">
<template v-if="column.key === 'action'">
<a-popconfirm
:title="t('components.common.shared_calendars.delete') + '?'"
:ok-text="t('components.common.shared_calendars.ok')"
:cancel-text="t('components.common.shared_calendars.cancel')"
@confirm="deleteAccess(record.Name)"
>
<a-button danger>{{
t("components.common.shared_calendars.delete")
}}</a-button>
</a-popconfirm>
</template>
<template v-if="column.key === 'perm'">
<a-select
v-if="!props.resources"
v-model:value="record.Perm"
style="min-width: 150px"
@change="updateAccess(record.Name, record.Perm)"
>
<a-select-option value="read">
{{
t("components.common.shared_calendars.access_perm_read")
}}</a-select-option
>
<a-select-option value="all">
{{
t("components.common.shared_calendars.access_perm_all")
}}</a-select-option
>
</a-select>
<div v-else>
{{
record.Perm === "read"
? t("components.common.shared_calendars.access_perm_read")
: t("components.common.shared_calendars.access_perm_all")
}}
</div>
</template>
</template>
<template #footer>
<a-flex justify="flex-end">
<a-pagination
v-model:value="page.access.pagination.current"
:defaultPageSize="page.access.pagination.size"
:total="page.access.pagination.total"
@change="accessPageChange"
:pageSizeOptions="['10', '20', '50', '100']"
v-model:pageSize="page.access.pagination.size"
:showSizeChanger="true"
/>
</a-flex>
</template>
</a-table>
<template #footer> </template>
</a-modal>
<a-modal
style="width: 300px"
v-model:open="page.access.showAdd"
:title="
page.access.type === 'email'
? t('components.common.shared_calendars.access_type_email_add')
: t('components.common.shared_calendars.access_type_group_add')
"
>
<br />
<a-form>
<a-form-item>
<template v-if="page.access.type === 'email'">
<a-input
v-model:value="page.access.inputValue"
:placeholder="
t(
'components.common.shared_calendars.access_type_email_placeholder'
)
"
>
</a-input>
</template>
<template v-if="page.access.type === 'group'">
<a-select
style="width: 100%"
:disabled="page.access.groups.length === 0"
v-model:value="page.access.selectValue"
:options="page.access.groups"
>
</a-select>
</template>
</a-form-item>
<a-form-item v-if="!props.resources">
<a-select v-model:value="page.access.perm">
<a-select-option value="read">
{{
t("components.common.shared_calendars.access_perm_read")
}}</a-select-option
>
<a-select-option value="all">
{{
t("components.common.shared_calendars.access_perm_all")
}}</a-select-option
>
</a-select>
</a-form-item>
</a-form>
<template #footer>
<a-button
:disabled="
(page.access.type === 'email' && page.access.inputValue === '') ||
(page.access.type === 'group' && page.access.selectValue === '')
"
type="primary"
@click="
addAccess(
page.access.type === 'email'
? page.access.inputValue
: page.access.selectValue
)
"
>
{{ t("components.common.shared_calendars.access_add") }}
</a-button>
</template>
</a-modal>
</template>
<script setup lang="ts">
import { useI18n } from "vue-i18n";
import { useRoute } from "vue-router";
import type { ColumnType } from "ant-design-vue/es/table";
import { onMounted, reactive } from "vue";
import { apiFetch } from "@/composables/apiFetch";
import { notifyError, notifySuccess } from "@/composables/alert";
const route = useRoute();
const { t } = useI18n();
const openModel = defineModel<boolean>("open");
const props = defineProps<{
type: string;
id: number;
domain?: string;
resources: boolean;
}>();
const accessColumns: ColumnType<any>[] = [
{
title: t("components.common.shared_calendars.access_col_name"),
dataIndex: "Name",
},
{
title: t("components.common.shared_calendars.access_col_permissions"),
dataIndex: "Perm",
key: "perm",
},
{
key: "action",
align: "right",
},
];
const page = reactive<{
all_groups: string[];
access: {
inputValue: string;
selectValue: string;
perm: string;
type: string;
data: any[];
loading: boolean;
show: boolean;
groups: any[];
pagination: {
current: number;
total: number;
size: number;
search: string;
};
showAdd: boolean;
withSearch: boolean;
};
}>({
all_groups: [],
access: {
withSearch: false,
inputValue: "",
selectValue: "",
perm: "read",
type: "users",
data: [],
loading: false,
show: false,
groups: [],
pagination: {
current: 1,
total: 0,
size: 50,
search: "",
},
showAdd: false,
},
});
onMounted(() => {
page.access.inputValue = "";
page.access.type = props.type;
init();
});
async function init() {
await getGroups();
getAccess();
}
async function getGroups() {
let url = "";
if (props.domain) {
url = `/admin/domains/${props.domain}/${
props.resources ? `resources/resources` : `shared_calendars`
}/groups`;
} else {
url = `/user/calendars/groups`;
}
const res = await apiFetch(url);
if (res.error) {
notifyError(res.error);
return;
}
page.all_groups = res.data;
return;
}
async function getAccess() {
page.access.withSearch = page.access.pagination.search !== "";
page.access.loading = true;
const params = new URLSearchParams();
params.append("id", props.id.toString());
params.append("type", page.access.type);
params.append("page", String(page.access.pagination.current));
params.append("size", String(page.access.pagination.size));
params.append("search", page.access.pagination.search);
let url = "";
if (props.domain) {
url = `/admin/domains/${props.domain}/${
props.resources ? `resources/resources` : `shared_calendars`
}/access?`;
} else {
url = `/user/calendars/access?`;
}
const res = await apiFetch(url + params.toString());
page.access.loading = false;
if (res.error) {
notifyError(res.error);
return;
}
page.access.data = [];
if (res.data) {
for (let index = 0; index < res.data.length; index++) {
const element = res.data[index];
page.access.data.push({
Name: element.AccessTo,
Perm: element.Permissions,
});
}
}
page.access.pagination.total = res.total;
page.access.groups = [];
page.access.selectValue = "";
if (page.access.type === "group") {
for (let j = 0; j < page.all_groups.length; j++) {
const group = page.all_groups[j];
let skip = false;
for (let i = 0; i < page.access.data.length; i++) {
const alreadyHas = page.access.data[i];
if (alreadyHas.Name === group) {
skip = true;
break;
}
}
if (!skip) {
page.access.groups.push({ value: group });
}
}
}
if (page.access.groups.length) {
page.access.selectValue = page.access.groups[0].value;
}
return;
}
async function deleteAccess(access: string) {
const params = new URLSearchParams();
params.append("id", props.id.toString());
params.append("type", page.access.type);
params.append("page", String(page.access.pagination.current));
params.append("size", String(page.access.pagination.size));
params.append("search", page.access.pagination.search);
let url = "";
if (props.domain) {
url = `/admin/domains/${props.domain}/${
props.resources ? `resources/resources` : `shared_calendars`
}/access?`;
} else {
url = `/user/calendars/access?`;
}
const res = await apiFetch(url + params.toString(), {
method: "DELETE",
body: {
Access: access,
},
});
if (res.error) {
notifyError(res.error);
return;
}
notifySuccess(t("components.common.shared_calendars.remove_success"));
return getAccess();
}
async function updateAccess(access: string, perm: string) {
const params = new URLSearchParams();
params.append("id", props.id.toString());
params.append("type", page.access.type);
let url = "";
if (props.domain) {
url = `/admin/domains/${props.domain}/${
props.resources ? `resources/resources` : `shared_calendars`
}/access?`;
} else {
url = `/user/calendars/access?`;
}
const res = await apiFetch(url + params.toString(), {
method: "PATCH",
body: {
Access: access,
Perm: perm,
},
});
if (res.error) {
notifyError(res.error);
return;
}
notifySuccess(t("components.common.shared_calendars.update_success"));
return getAccess();
}
async function addAccess(access: string) {
const params = new URLSearchParams();
params.append("id", props.id.toString());
params.append("type", page.access.type);
let url = "";
if (props.domain) {
url = `/admin/domains/${props.domain}/${
props.resources ? `resources/resources` : `shared_calendars`
}/access?`;
} else {
url = `/user/calendars/access?`;
}
const res = await apiFetch(url + params.toString(), {
method: "POST",
body: {
Access: access,
Perm: page.access.perm,
},
});
if (res.error) {
notifyError(res.error);
return;
}
notifySuccess(t("components.common.shared_calendars.add_success"));
page.access.showAdd = false;
return getAccess();
}
function accessPageChange(current: number) {
page.access.pagination.current = current;
return getAccess();
}
</script>

View File

@ -0,0 +1,755 @@
<template>
<div class="panel-content">
<a-table
:columns="columns"
:data-source="page.data"
:loading="page.loading"
bordered
:pagination="false"
>
<template #title>
<a-row>
<a-col style="display: flex; align-items: flex-end" :span="18">
<a-space>
<a-button
@click="
page.currBaseDir = rootBaseDir;
page.lastBaseDir = [];
get();
"
>
<HomeOutlined />
</a-button>
<a-button
:disabled="page.lastBaseDir.length === 0"
@click="
page.currBaseDir = page.lastBaseDir.pop() as BaseDir;
get();
"
>
<ArrowLeftOutlined />
</a-button>
<a-input-search
style="width: 250px"
v-model:value="page.pagination.search"
:placeholder="
t('components.common.shared_folders.search') +
page.currBaseDir.CName
"
enter-button
allow-clear
@search="get"
/>
</a-space>
</a-col>
<a-col :span="6">
<a-space style="display: flex; justify-content: end">
<a-button type="primary" @click="showAddModal">
<PlusOutlined />
{{ t("components.common.shared_folders.add_folder") }}
</a-button>
<a-popconfirm
v-if="page.withSearch"
:title="
t('components.common.shared_folders.delete_found') + '?'
"
:ok-text="t('components.common.shared_folders.ok')"
:cancel-text="t('components.common.shared_folders.cancel')"
@confirm="deleteFolder('')"
>
<a-button danger>{{
$t("components.common.shared_folders.delete_found")
}}</a-button>
</a-popconfirm>
<a-popconfirm
v-else
:title="t('components.common.shared_folders.delete_all') + '?'"
:ok-text="t('components.common.shared_folders.ok')"
:cancel-text="t('components.common.shared_folders.cancel')"
@confirm="deleteFolder('')"
>
<a-button danger>{{
$t("components.common.shared_folders.delete_all")
}}</a-button>
</a-popconfirm>
</a-space>
</a-col>
</a-row>
</template>
<template #bodyCell="{ column, record }">
<template v-if="column.key === 'action'">
<a-space>
<a-button
:disabled="
props.user !== 'admin' && props.user !== record.Creator
"
type="primary"
@click="showAccessModal(record.Name)"
>{{ t("components.common.shared_folders.edit_access") }}</a-button
>
<a-popconfirm
:title="t('components.common.shared_folders.delete') + '?'"
:ok-text="t('components.common.shared_folders.ok')"
:cancel-text="t('components.common.shared_folders.cancel')"
@confirm="deleteFolder(record.Name)"
>
<a-button
:disabled="
props.user !== 'admin' && props.user !== record.Creator
"
danger
>{{ t("components.common.shared_folders.delete") }}</a-button
>
</a-popconfirm>
</a-space>
</template>
<template v-if="column.key === 'cname'">
<a
@click="
page.lastBaseDir = [...page.lastBaseDir, page.currBaseDir];
page.currBaseDir = {
Name: record.Name,
CName: record.CName,
};
get();
"
>
{{ record.CName }}
</a>
</template>
<template v-if="column.key === 'creator'">
<a-space>
<template v-if="record.CreatorStatus === 'archived'">
<a-tooltip
:title="
t(`components.admin.domains.mailstorage.mailboxes.in_archive`)
"
>
<LockOutlined />
</a-tooltip>
</template>
<template v-if="record.CreatorStatus === 'deleted'">
<a-tooltip
:title="
t(`components.admin.domains.mailstorage.mailboxes.deleted`)
"
>
<DeleteOutlined />
</a-tooltip>
</template>
{{ record.Creator }}
</a-space>
</template>
</template>
<template #footer>
<a-flex justify="flex-end">
<a-pagination
v-model:value="page.pagination.current"
:defaultPageSize="page.pagination.size"
:total="page.pagination.total"
@change="pageChange"
:pageSizeOptions="['10', '20', '50', '100']"
v-model:pageSize="page.pagination.size"
:showSizeChanger="true"
/>
</a-flex>
</template>
</a-table>
</div>
<a-modal
style="width: 300px"
v-model:open="page.add.show"
:title="t('components.common.shared_folders.add_folder')"
>
<a-input
style="margin-top: 8px"
:placeholder="$t('components.common.shared_folders.add_name')"
v-model:value="page.add.Name"
>
</a-input>
<template #footer>
<a-button type="primary" :disabled="!page.add.Name" @click="addFolder">
{{ t("components.common.shared_folders.add_folder") }}
</a-button>
</template>
</a-modal>
<a-modal
v-model:open="page.access.show"
style="width: 50%; min-width: 700px"
@cancel="get"
>
<a-table
:columns="accessColumns"
:data-source="page.access.data"
:loading="page.access.loading"
bordered
size="small"
:pagination="false"
>
<template #title>
<a-row>
<a-col style="display: flex; align-items: flex-end" :span="18">
<a-space>
<a-radio-group
@change="getAccess()"
v-model:value="page.access.type"
>
<a-radio-button value="email">{{
t("components.common.shared_folders.access_type_email")
}}</a-radio-button>
<a-radio-button value="group">{{
t("components.common.shared_folders.access_type_group")
}}</a-radio-button>
</a-radio-group>
<a-input-search
v-model:value="page.access.pagination.search"
:placeholder="
t('components.common.shared_folders.access_search')
"
enter-button
allow-clear
@search="getAccess"
/>
</a-space>
</a-col>
<a-col
style="display: flex; align-items: flex-end; justify-content: end"
:span="6"
>
<a-space>
<a-button
v-if="page.access.type === 'email'"
type="primary"
@click="page.access.showAdd = true"
>
{{
t("components.common.shared_folders.access_type_email_add")
}}
</a-button>
<a-button
v-if="page.access.type === 'group'"
type="primary"
@click="page.access.showAdd = true"
>
{{
t("components.common.shared_folders.access_type_group_add")
}}
</a-button>
<a-popconfirm
v-if="page.access.withSearch"
:title="
t('components.common.shared_folders.delete_found') + '?'
"
:ok-text="t('components.common.shared_folders.ok')"
:cancel-text="t('components.common.shared_folders.cancel')"
@confirm="deleteAccess('')"
>
<a-button danger>{{
$t("components.common.shared_folders.delete_found")
}}</a-button>
</a-popconfirm>
<a-popconfirm
v-else
:title="t('components.common.shared_folders.delete_all') + '?'"
:ok-text="t('components.common.shared_folders.ok')"
:cancel-text="t('components.common.shared_folders.cancel')"
@confirm="deleteAccess('')"
>
<a-button danger>{{
$t("components.common.shared_folders.delete_all")
}}</a-button>
</a-popconfirm>
</a-space>
</a-col>
</a-row>
</template>
<template #bodyCell="{ column, record }">
<template v-if="column.key === 'action'">
<a-popconfirm
:title="t('components.common.shared_folders.delete') + '?'"
:ok-text="t('components.common.shared_folders.ok')"
:cancel-text="t('components.common.shared_folders.cancel')"
@confirm="deleteAccess(record.Name)"
>
<a-button danger>{{
t("components.common.shared_folders.delete")
}}</a-button>
</a-popconfirm>
</template>
</template>
<template #footer>
<a-flex justify="flex-end">
<a-pagination
v-model:value="page.access.pagination.current"
:defaultPageSize="page.access.pagination.size"
:total="page.access.pagination.total"
@change="accessPageChange"
:pageSizeOptions="['10', '20', '50', '100']"
v-model:pageSize="page.access.pagination.size"
:showSizeChanger="true"
/>
</a-flex>
</template>
</a-table>
<template #footer> </template>
</a-modal>
<a-modal
style="width: 300px"
v-model:open="page.access.showAdd"
:title="
page.access.type === 'email'
? t('components.common.shared_folders.access_type_email_add')
: t('components.common.shared_folders.access_type_group_add')
"
>
<template v-if="page.access.type === 'email'">
<a-input
v-model:value="page.access.inputValue"
:placeholder="
t('components.common.shared_folders.access_type_email_placeholder')
"
>
</a-input>
</template>
<template v-if="page.access.type === 'group'">
<a-select
style="width: 100%"
:disabled="page.access.groups.length === 0"
v-model:value="page.access.selectValue"
:options="page.access.groups"
>
</a-select>
</template>
<template #footer>
<a-button
:disabled="
(page.access.type === 'email' && page.access.inputValue === '') ||
(page.access.type === 'group' && page.access.selectValue === '')
"
type="primary"
@click="
addAccess(
page.access.type === 'email'
? page.access.inputValue
: page.access.selectValue
)
"
>
{{ t("components.common.shared_folders.access_add") }}
</a-button>
</template>
</a-modal>
</template>
<script setup lang="ts">
import { notifyError } from "@/composables/alert";
import { apiFetch } from "@/composables/apiFetch";
import router from "@/router";
import Export from "@/components/admin/domains/mailstorage/Export.vue";
import {
DomainPlaceholder,
MailboxPlaceholder,
RouteAdminDomainsDomainMailStorageMailboxesExport,
} from "@/router/consts";
import {
ArrowLeftOutlined,
DeleteOutlined,
DownOutlined,
ExclamationCircleOutlined,
HddOutlined,
HomeOutlined,
LockOutlined,
PlusOutlined,
} from "@ant-design/icons-vue";
import { Modal } from "ant-design-vue";
import type { ColumnType } from "ant-design-vue/es/table";
import { createVNode, onMounted, reactive } from "vue";
import { useI18n } from "vue-i18n";
import { useRoute } from "vue-router";
const route = useRoute();
const { t } = useI18n();
const columns: ColumnType<any>[] = [
{
title: t("components.common.shared_folders.col_folder"),
dataIndex: "CName",
key: "cname",
},
{
title: t("components.common.shared_folders.col_owner"),
dataIndex: "Creator",
key: "creator",
},
{
key: "action",
align: "right",
},
];
const accessColumns: ColumnType<any>[] = [
{
title: t("components.common.shared_folders.access_col_name"),
dataIndex: "Name",
},
{
key: "action",
align: "right",
},
];
const props = defineProps<{
domain?: string;
user: string;
}>();
interface BaseDir {
Name: string;
CName: string;
}
const rootBaseDir: BaseDir = {
Name: "",
CName: t("components.common.shared_folders.root_folder"),
};
const page = reactive<{
all_groups: string[];
currBaseDir: BaseDir;
lastBaseDir: BaseDir[];
withSearch: boolean;
loading: boolean;
data: any;
pagination: {
current: number;
total: number;
size: number;
search: string;
};
add: {
show: boolean;
Name: string;
};
access: {
inputValue: string;
selectValue: string;
type: string;
data: any[];
loading: boolean;
show: boolean;
Name: string;
groups: any[];
pagination: {
current: number;
total: number;
size: number;
search: string;
};
showAdd: boolean;
withSearch: boolean;
};
}>({
currBaseDir: rootBaseDir,
lastBaseDir: [],
withSearch: false,
pagination: {
current: 1,
total: 0,
size: 50,
search: "",
},
loading: false,
data: [],
add: {
show: false,
Name: "",
},
all_groups: [],
access: {
withSearch: false,
inputValue: "",
selectValue: "",
type: "email",
data: [],
loading: false,
show: false,
Name: "",
groups: [],
pagination: {
current: 1,
total: 0,
size: 50,
search: "",
},
showAdd: false,
},
});
onMounted(() => {
get();
getGroups();
});
async function getGroups() {
let url = "";
if (props.domain) {
url = `/admin/domains/${props.domain}/shared_folders/groups`;
} else {
url = `/user/shared_folders/groups`;
}
const res = await apiFetch(url);
if (res.error) {
notifyError(res.error);
return;
}
page.all_groups = res.data;
return;
}
async function get() {
page.withSearch = page.pagination.search !== "";
page.loading = true;
const params = new URLSearchParams();
params.append("page", String(page.pagination.current));
params.append("size", String(page.pagination.size));
params.append("search", page.pagination.search);
params.append("base_dir", page.currBaseDir.Name);
let url = "";
if (props.domain) {
url = `/admin/domains/${props.domain}/shared_folders?`;
} else {
url = `/user/shared_folders?`;
}
const res = await apiFetch(url + params.toString());
page.loading = false;
if (res.error) {
notifyError(res.error);
return;
}
page.data = res.data;
page.pagination.total = res.total;
return;
}
function pageChange(current: number) {
page.pagination.current = current;
return get();
}
function accessPageChange(current: number) {
page.access.pagination.current = current;
return getAccess();
}
async function addFolder() {
const params = new URLSearchParams();
params.append("base_dir", page.currBaseDir.Name);
let url = "";
if (props.domain) {
url = `/admin/domains/${props.domain}/shared_folders?`;
} else {
url = `/user/shared_folders?`;
}
const res = await apiFetch(url + params.toString(), {
method: "POST",
body: {
Name: page.add.Name,
},
});
if (res.error) {
notifyError(res.error);
return;
}
page.add.show = false;
return get();
}
async function showAccessModal(name: string) {
page.access.Name = name;
page.access.show = true;
page.access.type = "email";
page.access.inputValue = "";
getAccess();
}
async function showAddModal() {
page.add.show = true;
page.add.Name = "";
}
async function deleteFolder(name: string) {
const params = new URLSearchParams();
params.append("search", page.pagination.search);
params.append("base_dir", page.currBaseDir.Name);
let url = "";
if (props.domain) {
url = `/admin/domains/${props.domain}/shared_folders?`;
} else {
url = `/user/shared_folders?`;
}
const res = await apiFetch(url + params.toString(), {
method: "DELETE",
body: {
Name: name,
},
});
if (res.error) {
notifyError(res.error);
return;
}
return get();
}
async function getAccess() {
page.access.withSearch = page.access.pagination.search !== "";
page.access.loading = true;
const params = new URLSearchParams();
params.append("name", page.access.Name);
params.append("type", page.access.type);
params.append("page", String(page.access.pagination.current));
params.append("size", String(page.access.pagination.size));
params.append("search", page.access.pagination.search);
params.append("base_dir", page.currBaseDir.Name);
let url = "";
if (props.domain) {
url = `/admin/domains/${props.domain}/shared_folders/access?`;
} else {
url = `/user/shared_folders/access?`;
}
const res = await apiFetch(url + params.toString());
page.access.loading = false;
if (res.error) {
notifyError(res.error);
return;
}
page.access.data = [];
if (res.data) {
for (let index = 0; index < res.data.length; index++) {
const element = res.data[index];
page.access.data.push({ Name: element });
}
}
page.access.pagination.total = res.total;
page.access.groups = [];
page.access.selectValue = "";
if (page.access.type === "group") {
for (let j = 0; j < page.all_groups.length; j++) {
const group = page.all_groups[j];
let skip = false;
for (let i = 0; i < page.access.data.length; i++) {
const alreadyHas = page.access.data[i];
if (alreadyHas.Name === group) {
skip = true;
break;
}
}
if (!skip) {
page.access.groups.push({ value: group });
}
}
}
if (page.access.groups.length) {
page.access.selectValue = page.access.groups[0].value;
}
return;
}
async function deleteAccess(access: string) {
const params = new URLSearchParams();
params.append("name", page.access.Name);
params.append("type", page.access.type);
params.append("page", String(page.access.pagination.current));
params.append("size", String(page.access.pagination.size));
params.append("search", page.access.pagination.search);
params.append("base_dir", page.currBaseDir.Name);
let url = "";
if (props.domain) {
url = `/admin/domains/${props.domain}/shared_folders/access?`;
} else {
url = `/user/shared_folders/access?`;
}
const res = await apiFetch(url + params.toString(), {
method: "DELETE",
body: {
Access: access,
},
});
if (res.error) {
notifyError(res.error);
return;
}
return getAccess();
}
async function addAccess(access: string) {
const params = new URLSearchParams();
params.append("name", page.access.Name);
params.append("type", page.access.type);
params.append("base_dir", page.currBaseDir.Name);
let url = "";
if (props.domain) {
url = `/admin/domains/${props.domain}/shared_folders/access?`;
} else {
url = `/user/shared_folders/access?`;
}
const res = await apiFetch(url + params.toString(), {
method: "POST",
body: {
Access: access,
},
});
if (res.error) {
notifyError(res.error);
return;
}
page.access.showAdd = false;
return getAccess();
}
</script>
<style scoped>
.panel-content {
min-width: 700px;
max-width: 70%;
margin: auto;
}
</style>

View File

@ -0,0 +1,40 @@
<template>
<a-select v-model="model">
<a-select-option v-for="tz in timezones" :value="tz"
>UTC{{ tz }}</a-select-option
>
</a-select>
</template>
<script setup lang="ts">
const model = defineModel();
const timezones = [
"-12",
"-11",
"-10",
"-09",
"-08",
"-07",
"-06",
"-05",
"-04",
"-03",
"-02",
"-01",
"+00",
"+01",
"+02",
"+03",
"+04",
"+05",
"+06",
"+07",
"+08",
"+09",
"+10",
"+11",
"+12",
"+13",
"+14",
];
</script>

View File

@ -0,0 +1,36 @@
<template>
<a-select
v-model="model"
mode="multiple"
style="width: 100%"
max-tag-count="responsive"
>
<a-select-option value="1">{{
t("components.user.calendars.share_free_time.work_days_names.monday")
}}</a-select-option>
<a-select-option value="2">{{
t("components.user.calendars.share_free_time.work_days_names.tuesday")
}}</a-select-option>
<a-select-option value="3">{{
t("components.user.calendars.share_free_time.work_days_names.wednesday")
}}</a-select-option>
<a-select-option value="4">{{
t("components.user.calendars.share_free_time.work_days_names.thursday")
}}</a-select-option>
<a-select-option value="5">{{
t("components.user.calendars.share_free_time.work_days_names.friday")
}}</a-select-option>
<a-select-option value="6">{{
t("components.user.calendars.share_free_time.work_days_names.saturday")
}}</a-select-option>
<a-select-option value="0">{{
t("components.user.calendars.share_free_time.work_days_names.sunday")
}}</a-select-option>
</a-select>
</template>
<script setup lang="ts">
import { useI18n } from "vue-i18n";
const { t } = useI18n();
const model = defineModel();
</script>

View File

@ -0,0 +1,6 @@
<template>
<a-time-range-picker v-model="model" format="HH:mm" />
</template>
<script setup lang="ts">
const model = defineModel();
</script>

View File

@ -0,0 +1,118 @@
<template>
<div>
<Card size="small" style="min-width: 100px; width: min-content">
<template #title>
{{ timeToDate(props.day) }}
<br />
{{ timeToDayName(new Date(props.day)) }}
</template>
<a-card-grid
v-for="interval in page.intervals"
style="width: 100%; text-align: center; padding: 1px"
>
<Interval
:interval="interval"
:checked="page.checked.get(interval)"
:add="add"
:remove="remove"
></Interval>
</a-card-grid>
</Card>
</div>
</template>
<script setup lang="ts">
import { timeToDate } from "@/composables/misc";
import { onMounted, reactive } from "vue";
import { useI18n } from "vue-i18n";
import Interval from "@/components/common/approval/Interval.vue";
import Card from "ant-design-vue/es/card/Card";
const { t } = useI18n();
const props = defineProps<{
day: string;
add: Function;
remove: Function;
checked?: string[];
}>();
const page = reactive<{
intervals: string[];
checked: Map<string, boolean>;
}>({
intervals: [],
checked: new Map<string, boolean>(),
});
onMounted(() => {
if (props.checked) {
props.checked.forEach((element) => {
page.checked.set(element, true);
});
}
page.intervals = formIntervals();
});
function formIntervals(): string[] {
let res: string[] = [];
for (let hour = 6; hour < 22; hour++) {
let hourStr = String(hour);
if (hour < 10) {
hourStr = "0" + hourStr;
}
res.push(hourStr + ":00" + " - " + hourStr + ":30");
let nextHour = hour + 1;
let nextHourStr = String(nextHour);
if (nextHour < 10) {
nextHourStr = "0" + nextHourStr;
}
res.push(hourStr + ":30" + " - " + nextHourStr + ":00");
}
return res;
}
function add(interval: string) {
return props.add(props.day + "|" + interval);
}
function remove(interval: string) {
return props.remove(props.day + "|" + interval);
}
function timeToDayName(date: Date): string {
switch (date.getDay()) {
case 1:
return t(
"components.user.calendars.share_free_time.work_days_names.monday"
);
case 2:
return t(
"components.user.calendars.share_free_time.work_days_names.tuesday"
);
case 3:
return t(
"components.user.calendars.share_free_time.work_days_names.wednesday"
);
case 4:
return t(
"components.user.calendars.share_free_time.work_days_names.thursday"
);
case 5:
return t(
"components.user.calendars.share_free_time.work_days_names.friday"
);
case 6:
return t(
"components.user.calendars.share_free_time.work_days_names.saturday"
);
case 0:
return t(
"components.user.calendars.share_free_time.work_days_names.sunday"
);
}
return "";
}
</script>

View File

@ -0,0 +1,66 @@
<template>
<div
:class="page.checked ? 'checked' : 'unchecked'"
@pointerdown="onHover"
@pointerenter="onHover"
@click="click"
>
{{ props.interval }}
</div>
</template>
<script setup lang="ts">
import { onMounted, reactive } from "vue";
const props = defineProps<{
interval: string;
add: Function;
remove: Function;
checked?: boolean;
}>();
const page = reactive<{
checked: boolean;
}>({
checked: false,
});
onMounted(() => {
if (props.checked) {
change();
}
});
function onHover(payload: PointerEvent) {
if (payload.buttons && payload.pointerType !== "touch") {
change();
}
}
function click(payload: MouseEvent) {
const pointerPayload = payload as PointerEvent;
if (pointerPayload.pointerType === "mouse") {
return;
}
change();
}
function change() {
page.checked = !page.checked;
if (page.checked) {
props.add(props.interval);
} else {
props.remove(props.interval);
}
}
</script>
<style scoped>
.checked {
background-color: #bae7ff;
user-select: none;
}
.unchecked {
background-color: white;
user-select: none;
}
</style>

View File

@ -0,0 +1,74 @@
<template>
<a-flex gap="middle">
<a-select :value="page.currentAct.Label" @change="changeAct">
<a-select-option v-for="(act, i) in actions" :value="act.Label">
{{ act.Label }}
</a-select-option>
</a-select>
<slot
v-if="
page.currentAct.Args.length > 1 &&
!page.currentAct.NoVal &&
page.renderValue
"
name="action-value"
:change="(v: string) => {page.currentAct.Args[1] = v; update()}"
:type="page.currentAct.Value"
:value="page.currentAct.Args[1]"
>
</slot>
</a-flex>
</template>
<script setup lang="ts">
import { nextTick, onMounted, reactive } from "vue";
import { useI18n } from "vue-i18n";
import { useRoute } from "vue-router";
const props = defineProps<{
actions: Action[];
value: string[];
update: Function;
}>();
const page = reactive<{
renderValue: boolean;
currentAct: Action;
}>({
renderValue: false,
currentAct: {
...(props.actions.find(
(c: Action) => c.Value === props.value[0]
) as Action),
},
});
onMounted(() => {
page.currentAct.Args = [...props.value];
page.renderValue = true;
});
async function changeAct(newLabel: string) {
page.currentAct = {
...(props.actions.find((c: Action) => c.Label === newLabel) as Action),
};
page.currentAct.Args = [...page.currentAct.Args];
update();
page.renderValue = false;
nextTick(() => {
page.renderValue = true;
});
}
async function update() {
return props.update(page.currentAct.Args);
}
</script>
<script lang="ts">
export interface Action {
Value: string;
Label: string;
NextAllowedActions: string[];
Args: string[];
NoVal: boolean;
}
</script>

View File

@ -0,0 +1,91 @@
<template>
<a-flex gap="middle">
<a-select :value="page.currentCond.Label" @change="changeCond">
<a-select-option v-for="(cond, i) in conditions" :value="cond.Label">
{{ cond.Label }}
</a-select-option>
</a-select>
<a-select
v-if="page.currentCond.OpIdx !== -1"
v-model:value="page.currentCond.Args[page.currentCond.OpIdx]"
@change="update()"
>
<a-select-option
v-for="(op, i) in page.currentCond.OpList"
:value="op.Val"
>
{{ op.Cname }}
</a-select-option>
</a-select>
<slot
v-if="page.currentCond.ValIdx !== -1 && page.renderValue"
name="condition-value"
:change="(v: string) => {page.currentCond.Args[page.currentCond.ValIdx] = String(v); update()}"
:type="page.currentCond.Value"
:value="page.currentCond.Args[page.currentCond.ValIdx]"
>
</slot>
</a-flex>
</template>
<script setup lang="ts">
import { nextTick, onMounted, reactive } from "vue";
import { useI18n } from "vue-i18n";
import { useRoute } from "vue-router";
const props = defineProps<{
conditions: Condition[];
value: string[];
update: Function;
}>();
const page = reactive<{
renderValue: boolean;
currentCond: Condition;
}>({
renderValue: false,
currentCond: {
...(props.conditions.find(
(c: Condition) => c.Args[c.KeyIdx] === props.value[c.KeyIdx]
) as Condition),
},
});
onMounted(() => {
page.currentCond.Args = [...props.value];
page.renderValue = true;
});
async function changeCond(newLabel: string) {
page.currentCond = {
...(props.conditions.find(
(c: Condition) => c.Label === newLabel
) as Condition),
};
page.currentCond.Args = [...page.currentCond.Args];
update();
page.renderValue = false;
nextTick(() => {
page.renderValue = true;
});
}
async function update() {
return props.update(page.currentCond.Args);
}
</script>
<script lang="ts">
export interface OpList {
Val: string;
Cname: string;
}
export interface Condition {
Value: string;
Label: string;
KeyIdx: number;
OpIdx: number;
ValIdx: number;
OpList: OpList[];
Args: string[];
}
</script>

View File

@ -0,0 +1,588 @@
<template>
<div class="panel-content">
<a-table
:columns="columns"
:data-source="page.data"
:loading="page.loading"
bordered
:pagination="false"
>
<template #title>
<div style="display: flex; justify-content: end">
<a-button type="primary" @click="showRule(newRule())">
<PlusOutlined />
{{ t("components.common.rules.add_rule") }}
</a-button>
</div>
</template>
<template #bodyCell="{ column, record }">
<template v-if="column.key === 'enabled'">
<a-switch
v-model:checked="record.Enabled"
@change="enable(record.Idx, record.Enabled)"
></a-switch>
</template>
<template v-if="column.key === 'name'">
<a @click="showRule(record)">{{ record.Name }}</a>
</template>
<template v-if="column.key === 'action'">
<a-space>
<a-button
@click="move(record.Idx, 'up')"
:disabled="record.Idx === 0"
>
<UpOutlined></UpOutlined>
</a-button>
<a-button
@click="move(record.Idx, 'down')"
:disabled="record.Idx === page.data.length - 1"
>
<DownOutlined></DownOutlined>
</a-button>
</a-space>
</template>
</template>
</a-table>
</div>
<a-modal
style="width: 90%"
v-model:open="page.edit.show"
:title="
page.edit.rule.Idx === -1
? t('components.common.rules.add_rule')
: t('components.common.rules.edit_rule')
"
>
<a-divider></a-divider>
<a-form :label-col="labelCol" :wrapper-col="wrapperCol">
<a-form-item :label="t('components.common.rules.edit_name')">
<a-row>
<a-col flex="auto">
<a-input style="width: 200px" v-model:value="page.edit.rule.Name">
</a-input>
</a-col>
<a-col flex="100px"> </a-col>
</a-row>
</a-form-item>
<a-divider></a-divider>
<a-form-item :label="t('components.common.rules.edit_conditions')">
<a-flex gap="middle" vertical>
<a-row>
<a-col flex="auto">
<a-radio-group v-model:value="page.edit.rule.Condition.Op">
<a-radio-button value="and">{{
t("components.common.rules.edit_conditions_all")
}}</a-radio-button>
<a-radio-button value="or">{{
t("components.common.rules.edit_conditions_any")
}}</a-radio-button>
</a-radio-group>
</a-col>
<a-col flex="100px"> </a-col>
</a-row>
<template v-if="page.renderConditions && page.edit.show">
<div v-for="(condition, i) in page.edit.rule.Condition.Conditions">
<a-row>
<a-col flex="auto">
<Conditions
:value="condition"
:update="(newVal: string[]) => updateCondition(i, newVal)"
:conditions="props.conditions"
>
<template #condition-value="valueProps">
<slot
name="condition-value"
:change="valueProps.change"
:value="valueProps.value"
:type="valueProps.type"
>
</slot>
</template>
</Conditions>
</a-col>
<a-col flex="100px">
<a-button
:disabled="i === 0"
danger
@click="deleteCondition(i)"
>
{{ t("components.common.rules.delete") }}
</a-button>
</a-col>
</a-row>
</div>
</template>
<a-row>
<a-col flex="auto">
<a-button
style="width: fit-content; margin: auto"
@click="addCondition"
>
<PlusOutlined> </PlusOutlined>
{{ t("components.common.rules.add_condition") }}
</a-button>
</a-col>
<a-col flex="100px"> </a-col>
</a-row>
</a-flex>
</a-form-item>
<a-divider></a-divider>
<a-form-item :label="t('components.common.rules.edit_actions')">
<a-flex gap="middle" vertical>
<template v-if="page.renderActions && page.edit.show">
<div v-for="(action, i) in page.edit.rule.Action.Actions">
<a-row>
<a-col flex="auto">
<Actions
:value="action"
:update="(newVal: string[]) => updateAction(i,newVal)"
:actions="page.allowedActions[i]"
>
<template #action-value="valueProps">
<slot
name="action-value"
:change="valueProps.change"
:value="valueProps.value"
:type="valueProps.type"
>
</slot>
</template>
</Actions>
</a-col>
<a-col flex="100px">
<a-button :disabled="i === 0" danger @click="deleteAction(i)">
{{ t("components.common.rules.delete") }}
</a-button>
</a-col>
</a-row>
</div>
</template>
<a-row>
<a-col flex="auto">
<a-button
:disabled="page.newActionOptions.length === 0"
style="width: fit-content; margin: auto"
@click="addAction"
>
<PlusOutlined> </PlusOutlined>
{{ t("components.common.rules.add_action") }}
</a-button>
</a-col>
<a-col flex="100px"> </a-col>
</a-row>
</a-flex>
</a-form-item>
</a-form>
<a-divider></a-divider>
<template #footer>
<a-space>
<a-popconfirm
v-if="page.edit.rule.Idx !== -1"
:title="t('components.common.rules.delete_rule') + '?'"
:ok-text="t('components.common.rules.ok')"
:cancel-text="t('components.common.rules.cancel')"
@confirm="deleteRule(page.edit.rule.Idx)"
>
<a-button danger>{{
$t("components.common.rules.delete_rule")
}}</a-button>
</a-popconfirm>
<a-button
:disabled="!page.edit.rule.Name || !page.edit.valid"
@click="saveRule(page.edit.rule)"
type="primary"
>
{{ t("components.common.rules.save_rule") }}
</a-button>
</a-space>
</template>
</a-modal>
</template>
<script setup lang="ts">
import { notifyError } from "@/composables/alert";
import { apiFetch } from "@/composables/apiFetch";
import router from "@/router";
import Export from "@/components/admin/domains/mailstorage/Export.vue";
import {
DomainPlaceholder,
MailboxPlaceholder,
RouteAdminDomainsDomainMailStorageMailboxesExport,
} from "@/router/consts";
import {
ArrowLeftOutlined,
DeleteOutlined,
DownOutlined,
ExclamationCircleOutlined,
HomeOutlined,
PlusOutlined,
UpOutlined,
} from "@ant-design/icons-vue";
import { Modal } from "ant-design-vue";
import type { ColumnType } from "ant-design-vue/es/table";
import { createVNode, onMounted, reactive } from "vue";
import { useI18n } from "vue-i18n";
import { useRoute } from "vue-router";
import type { Condition } from "./Conditions.vue";
import Conditions from "./Conditions.vue";
import type { Action } from "./Actions.vue";
import { nextTick } from "vue";
import Actions from "./Actions.vue";
const route = useRoute();
const { t } = useI18n();
const columns: ColumnType<any>[] = [
{
title: t("components.common.rules.col_enabled"),
key: "enabled",
align: "center",
width: 100,
},
{
title: t("components.common.rules.col_name"),
key: "name",
align: "center",
},
{
title: t("components.common.rules.col_action"),
key: "action",
align: "center",
width: 120,
},
];
const labelCol = { span: 3, style: { "text-align": "left" } };
const wrapperCol = { span: 21, style: { "text-align": "center" } };
interface ConditionStruct {
Op: string;
Conditions: string[][];
}
interface ActionStruct {
Actions: string[][];
}
interface Rule {
Idx: number;
Name: string;
Condition: ConditionStruct;
Action: ActionStruct;
}
function newRule(): Rule {
return {
Idx: -1,
Name: "",
Condition: {
Op: "and",
Conditions: [[...props.conditions[0].Args]],
},
Action: {
Actions: [[...props.actions[0].Args]],
},
};
}
function copyRule(old: Rule): Rule {
return {
Idx: old.Idx,
Name: old.Name,
Condition: {
Op: old.Condition.Op,
Conditions: [...old.Condition.Conditions],
},
Action: {
Actions: [...old.Action.Actions],
},
};
}
const props = defineProps<{
getGetUrl: Function;
getMoveUrl: Function;
getEnableUrl: Function;
getDeleteUrl: Function;
getSaveUrl: Function;
conditions: Condition[];
actions: Action[];
}>();
const page = reactive<{
loading: boolean;
data: any;
edit: {
show: boolean;
rule: Rule;
valid: boolean;
};
renderConditions: boolean;
renderActions: boolean;
allowedActions: Action[][];
newActionOptions: Action[];
}>({
loading: false,
data: undefined,
edit: {
show: false,
rule: newRule(),
valid: true,
},
renderConditions: true,
renderActions: true,
allowedActions: [],
newActionOptions: [],
});
onMounted(() => {
get();
});
function computeAllowedActions() {
page.allowedActions = [];
page.newActionOptions = [];
for (let i = 0; i < page.edit.rule.Action.Actions.length + 1; i++) {
if (i === 0) {
page.allowedActions.push(props.actions);
continue;
}
let action = props.actions.find(
(a: Action) => a.Value === page.edit.rule.Action.Actions[i - 1][0]
) as Action;
let newAllowedActionsValue = [...action.NextAllowedActions];
for (let j = 0; j < page.allowedActions.length; j++) {
const prevAllowedActions = page.allowedActions[j];
newAllowedActionsValue = newAllowedActionsValue.filter((act: string) =>
prevAllowedActions.map((a: Action) => a.Value).includes(act)
);
}
page.allowedActions.push(
props.actions.filter((a: Action) =>
newAllowedActionsValue.includes(a.Value)
)
);
}
page.newActionOptions = page.allowedActions.pop() as Action[];
for (let i = page.edit.rule.Action.Actions.length - 1; i >= 0; i--) {
if (
!page.allowedActions[i]
.map((a: Action) => a.Value)
.includes(page.edit.rule.Action.Actions[i][0])
) {
page.edit.rule.Action.Actions.splice(i, 1);
page.allowedActions.splice(i, 1);
}
}
}
function updateAction(i: number, newVal: string[]) {
page.edit.rule.Action.Actions[i] = newVal;
computeAllowedActions();
validate();
}
function updateCondition(i: number, newVal: string[]) {
page.edit.rule.Condition.Conditions[i] = newVal;
validate();
}
function validate() {
let ok = true;
page.edit.rule.Action.Actions.forEach((args) => {
let action = props.actions.find(
(a: Action) => a.Value === args[0]
) as Action;
if (!action.NoVal && args[1] === "") {
ok = false;
}
});
page.edit.rule.Condition.Conditions.forEach((args) => {
let cond = props.conditions.find(
(a: Condition) => a.Value === args[0]
) as Condition;
if (cond.ValIdx != -1 && args[cond.ValIdx] === "") {
ok = false;
}
});
page.edit.valid = ok;
}
function addCondition() {
page.edit.rule.Condition.Conditions.push(props.conditions[0].Args);
validate();
}
function deleteCondition(i: number) {
page.edit.rule.Condition.Conditions.splice(i, 1);
page.renderConditions = false;
validate();
nextTick(() => {
page.renderConditions = true;
});
}
function addAction() {
if (page.newActionOptions.length > 0) {
page.edit.rule.Action.Actions.push(page.newActionOptions[0].Args);
}
computeAllowedActions();
validate();
}
function deleteAction(i: number) {
page.edit.rule.Action.Actions.splice(i, 1);
computeAllowedActions();
validate();
page.renderActions = false;
nextTick(() => {
page.renderActions = true;
});
// console.log(page.edit.rule.Action.Actions);
}
async function showRule(rule: any) {
page.edit.rule = copyRule(rule);
page.edit.show = true;
computeAllowedActions();
// for (let i = 0; i < page.edit.rule.Action.Actions.length; i++) {
// const action = page.edit.rule.Action.Actions[i];
// page.allowedActions.push(getAllowedActions(i));
// }
}
// function getAllowedActions(to: number): Action[] {
// if (page.edit.rule.Action.Actions.length === 1) {
// return props.actions;
// }
// let avaliableActions = props.actions.map((a: Action) => a.Value);
// for (let i = 0; i < to; i++) {
// const element = page.edit.rule.Action.Actions[i];
// let action = props.actions.find(
// (a: Action) => a.Value === element[0]
// ) as Action;
// avaliableActions = avaliableActions.filter((act: string) =>
// action.NextAllowedActions.includes(act)
// );
// }
// let res = props.actions.filter((a: Action) =>
// avaliableActions.includes(a.Value)
// );
// console.log(res);
// return res;
// return nil;
// }
async function get() {
page.loading = true;
let url = props.getGetUrl();
const res = await apiFetch(url);
page.loading = false;
if (res.error) {
notifyError(res.error);
return;
}
page.data = res.data;
for (let i = 0; i < page.data.length; i++) {
const element = page.data[i];
element.Enabled = !element.Disabled;
element.Idx = i;
}
return;
}
async function move(idx: number, direction: string) {
const params = new URLSearchParams();
params.append("idx", String(idx));
params.append("direction", direction);
let url = props.getMoveUrl();
const res = await apiFetch(url + "?" + params.toString(), {
method: "POST",
});
if (res.error) {
notifyError(res.error);
return;
}
return get();
}
async function enable(idx: number, enabled: boolean) {
const params = new URLSearchParams();
params.append("idx", String(idx));
params.append("enabled", String(enabled));
let url = props.getEnableUrl();
const res = await apiFetch(url + "?" + params.toString(), {
method: "POST",
});
if (res.error) {
notifyError(res.error);
return;
}
return get();
}
async function deleteRule(idx: number) {
const params = new URLSearchParams();
params.append("idx", String(idx));
let url = props.getDeleteUrl();
const res = await apiFetch(url + "?" + params.toString(), {
method: "DELETE",
});
if (res.error) {
notifyError(res.error);
return;
}
page.edit.show = false;
return get();
}
async function saveRule(rule: any) {
let url = props.getSaveUrl();
const res = await apiFetch(url, {
method: "POST",
body: rule,
});
if (res.error) {
notifyError(res.error);
return;
}
page.edit.show = false;
return get();
}
</script>
<style scoped>
.panel-content {
min-width: 500px;
max-width: 50%;
margin: auto;
}
</style>

View File

@ -0,0 +1,61 @@
<template>
<div style="width: 100%">
<a-input
v-if="
type === ActKwTransformFromAddress || type === ActKwTransformToAddress
"
v-model:value="page.value"
@change="props.change(page.value)"
:status="page.value === '' ? 'error' : ''"
>
</a-input>
</div>
</template>
<script setup lang="ts">
import { useRoute } from "vue-router";
const route = useRoute();
const props = defineProps<{
change: Function;
value: string;
type: string;
}>();
const page = reactive<{
value: string;
}>({
value: props.value,
});
</script>
<script lang="ts">
import { onMounted, reactive } from "vue";
import type { Action } from "../Actions.vue";
import { apiFetch } from "@/composables/apiFetch";
import { notifyError } from "@/composables/alert";
import i18n from "@/locale";
const ActKwTransformFromAddress = `transform_from_address`;
const ActKwTransformToAddress = `transform_to_address`;
export const actionsList: Action[] = [
{
Value: ActKwTransformFromAddress,
Label: i18n.global.t(
"components.common.rules.address.actions.transform_from_address"
),
NextAllowedActions: [ActKwTransformFromAddress, ActKwTransformToAddress],
Args: [ActKwTransformFromAddress, "*@(old_host.ru) => new_host.ru"],
NoVal: false,
},
{
Value: ActKwTransformToAddress,
Label: i18n.global.t(
"components.common.rules.address.actions.transform_to_address"
),
NextAllowedActions: [ActKwTransformFromAddress, ActKwTransformToAddress],
Args: [ActKwTransformToAddress, "*@(old_host.ru) => new_host.ru"],
NoVal: false,
},
];
</script>

View File

@ -0,0 +1,192 @@
<template>
<div style="width: 100%">
<a-input
v-if="
props.type === CondKwFromSMTP ||
props.type === CondKwToSMTP ||
props.type === CondKwFromGroup ||
props.type === CondKwToGroup
"
v-model:value="page.value"
@change="props.change(page.value)"
:status="page.value === '' ? 'error' : ''"
>
</a-input>
</div>
</template>
<script setup lang="ts">
import dayjs from "dayjs";
const props = defineProps<{
change: Function;
value: string;
type: string;
}>();
const page = reactive<{
value: string;
}>({
value: props.value,
});
</script>
<script lang="ts">
import { onMounted, reactive } from "vue";
import type { Condition } from "../Conditions.vue";
import i18n from "@/locale";
const CondKwFromSMTP = `from`;
const CondKwToSMTP = `to`;
const CondKwFromGroup = `from_group`;
const CondKwToGroup = `to_group`;
const CondKwAll = `all`;
export const conditionsList: Condition[] = [
{
Value: CondKwFromSMTP,
Label: i18n.global.t("components.common.rules.address.conditions.from"),
KeyIdx: 0,
OpIdx: 1,
ValIdx: 2,
OpList: [
{
Val: `=`,
Cname: i18n.global.t(
"components.common.rules.address.conditions.op_contains"
),
},
{
Val: `!`,
Cname: i18n.global.t(
"components.common.rules.address.conditions.op_not_contains"
),
},
{
Val: `==`,
Cname: i18n.global.t(
"components.common.rules.address.conditions.op_equal"
),
},
{
Val: `!=`,
Cname: i18n.global.t(
"components.common.rules.address.conditions.op_not_equal"
),
},
{
Val: `^`,
Cname: i18n.global.t(
"components.common.rules.address.conditions.op_starts"
),
},
{
Val: `$`,
Cname: i18n.global.t(
"components.common.rules.address.conditions.op_ends"
),
},
],
Args: [CondKwFromSMTP, "=", "test@example.com"],
},
{
Value: CondKwToSMTP,
Label: i18n.global.t("components.common.rules.address.conditions.to"),
KeyIdx: 0,
OpIdx: 1,
ValIdx: 2,
OpList: [
{
Val: `=`,
Cname: i18n.global.t(
"components.common.rules.address.conditions.op_contains"
),
},
{
Val: `!`,
Cname: i18n.global.t(
"components.common.rules.address.conditions.op_not_contains"
),
},
{
Val: `==`,
Cname: i18n.global.t(
"components.common.rules.address.conditions.op_equal"
),
},
{
Val: `!=`,
Cname: i18n.global.t(
"components.common.rules.address.conditions.op_not_equal"
),
},
{
Val: `^`,
Cname: i18n.global.t(
"components.common.rules.address.conditions.op_starts"
),
},
{
Val: `$`,
Cname: i18n.global.t(
"components.common.rules.address.conditions.op_ends"
),
},
],
Args: [CondKwToSMTP, "=", "test@example.com"],
},
{
Value: CondKwFromGroup,
Label: i18n.global.t(
"components.common.rules.address.conditions.from_group"
),
KeyIdx: 0,
OpIdx: 1,
ValIdx: 2,
OpList: [
{
Val: `==`,
Cname: i18n.global.t(
"components.common.rules.address.conditions.op_equal"
),
},
{
Val: `!=`,
Cname: i18n.global.t(
"components.common.rules.address.conditions.op_not_equal"
),
},
],
Args: [CondKwFromGroup, "==", "group"],
},
{
Value: CondKwToGroup,
Label: i18n.global.t("components.common.rules.address.conditions.to_group"),
KeyIdx: 0,
OpIdx: 1,
ValIdx: 2,
OpList: [
{
Val: `==`,
Cname: i18n.global.t(
"components.common.rules.address.conditions.op_equal"
),
},
{
Val: `!=`,
Cname: i18n.global.t(
"components.common.rules.address.conditions.op_not_equal"
),
},
],
Args: [CondKwToGroup, "==", "group"],
},
{
Value: CondKwAll,
Label: i18n.global.t("components.common.rules.address.conditions.all"),
KeyIdx: 0,
OpIdx: -1,
ValIdx: -1,
OpList: [],
Args: [CondKwAll],
},
];
</script>

View File

@ -0,0 +1,210 @@
<template>
<div style="width: 100%">
<a-select
v-if="
(type === ActKwToFolder || type === ActKwCopyToFolder) &&
page.folders.length !== 0
"
v-model:value="page.value"
@change="(newVal: string) => props.change(newVal)"
>
<a-select-option
v-for="foldersOpt in page.folders"
:value="foldersOpt[1]"
>
{{ foldersOpt[0] }}
</a-select-option>
</a-select>
<a-input
v-if="type === ActKwRedirectTo || type === ActKwCopyTo"
v-model:value="page.value"
@change="props.change(page.value)"
:status="page.value === '' ? 'error' : ''"
>
</a-input>
<a-textarea
v-if="type === ActKwReplyMsg"
v-model:value="page.value"
@change="props.change(page.value)"
:status="page.value === '' ? 'error' : ''"
>
</a-textarea>
</div>
</template>
<script setup lang="ts">
import { useRoute } from "vue-router";
const route = useRoute();
const props = defineProps<{
change: Function;
value: string;
type: string;
getFoldersUrl: Function;
}>();
const page = reactive<{
value: string;
folders: string[][];
}>({
value: props.value,
folders: [],
});
onMounted(() => {
if (props.type === ActKwToFolder || props.type === ActKwCopyToFolder) {
getFolders();
}
});
async function getFolders() {
let url = props.getFoldersUrl();
const res = await apiFetch(url);
if (res.error) {
notifyError(res.error);
return;
}
page.folders = [];
page.folders = res.data;
}
</script>
<script lang="ts">
import { onMounted, reactive } from "vue";
import type { Action } from "../Actions.vue";
import { apiFetch } from "@/composables/apiFetch";
import { notifyError } from "@/composables/alert";
import i18n from "@/locale";
const ActKwToFolder = `to_folder`;
const ActKwCopyToFolder = `copy_to_folder`;
const ActKwMarkAsSeen = `mark_as_seen`;
const ActKwMarkAsFlagged = `mark_as_flagged`;
const ActKwRedirectTo = `redirect_to`;
const ActKwCopyTo = `copy_to`;
const ActKwReplyMsg = `reply_msg`;
const ActKwReject = `reject`;
const ActKwStop = `stop`;
export const actionsList: Action[] = [
{
Value: ActKwToFolder,
Label: i18n.global.t("components.common.rules.incoming.actions.to_folder"),
NextAllowedActions: [
ActKwCopyTo,
ActKwReplyMsg,
ActKwStop,
ActKwCopyToFolder,
ActKwMarkAsSeen,
ActKwMarkAsFlagged,
],
Args: [ActKwToFolder, "INBOX"],
NoVal: false,
},
{
Value: ActKwCopyToFolder,
Label: i18n.global.t(
"components.common.rules.incoming.actions.copy_to_folder"
),
NextAllowedActions: [
ActKwToFolder,
ActKwCopyToFolder,
ActKwMarkAsSeen,
ActKwMarkAsFlagged,
ActKwCopyTo,
ActKwReplyMsg,
ActKwStop,
],
Args: [ActKwCopyToFolder, "INBOX"],
NoVal: false,
},
{
Value: ActKwMarkAsSeen,
Label: i18n.global.t(
"components.common.rules.incoming.actions.mark_as_seen"
),
NextAllowedActions: [
ActKwToFolder,
ActKwCopyTo,
ActKwReplyMsg,
ActKwStop,
ActKwCopyToFolder,
ActKwMarkAsFlagged,
],
Args: [ActKwMarkAsSeen, ""],
NoVal: true,
},
{
Value: ActKwMarkAsFlagged,
Label: i18n.global.t(
"components.common.rules.incoming.actions.mark_as_flagged"
),
NextAllowedActions: [
ActKwToFolder,
ActKwCopyTo,
ActKwReplyMsg,
ActKwStop,
ActKwCopyToFolder,
ActKwMarkAsSeen,
],
Args: [ActKwMarkAsFlagged, ""],
NoVal: true,
},
{
Value: ActKwRedirectTo,
Label: i18n.global.t(
"components.common.rules.incoming.actions.redirect_to"
),
NextAllowedActions: [ActKwCopyTo, ActKwReplyMsg],
Args: [ActKwRedirectTo, ""],
NoVal: false,
},
{
Value: ActKwCopyTo,
Label: i18n.global.t("components.common.rules.incoming.actions.copy_to"),
NextAllowedActions: [
ActKwRedirectTo,
ActKwToFolder,
ActKwCopyToFolder,
ActKwMarkAsSeen,
ActKwMarkAsFlagged,
ActKwReplyMsg,
ActKwCopyTo,
ActKwStop,
],
Args: [ActKwCopyTo, ""],
NoVal: false,
},
{
Value: ActKwReplyMsg,
Label: i18n.global.t("components.common.rules.incoming.actions.reply_msg"),
NextAllowedActions: [
ActKwRedirectTo,
ActKwToFolder,
ActKwCopyToFolder,
ActKwMarkAsSeen,
ActKwMarkAsFlagged,
ActKwCopyTo,
ActKwReplyMsg,
ActKwReject,
ActKwStop,
],
Args: [ActKwReplyMsg, ""],
NoVal: false,
},
{
Value: ActKwReject,
Label: i18n.global.t("components.common.rules.incoming.actions.reject"),
NextAllowedActions: [],
Args: [ActKwReject, ""],
NoVal: true,
},
{
Value: ActKwStop,
Label: i18n.global.t("components.common.rules.incoming.actions.stop"),
NextAllowedActions: [],
Args: [ActKwStop, ""],
NoVal: true,
},
];
</script>

View File

@ -0,0 +1,503 @@
<template>
<div style="width: 100%">
<a-input
v-if="
props.type === CondKwToSMTP ||
props.type === CondKwFromSMTP ||
props.type === CondKwHeader ||
props.type === CondKwBody
"
v-model:value="page.value"
@change="props.change(page.value)"
:status="page.value === '' ? 'error' : ''"
>
</a-input>
<a-input-number
v-if="props.type === CondKwSpamScore"
style="width: 100%"
v-model:value="page.value"
@change="props.change(page.value)"
:status="!page.value ? 'error' : ''"
>
</a-input-number>
<a-tooltip title="1048576, 1024K or 1M">
<a-input
v-if="props.type === CondKwSize"
v-model:value="page.value"
@change="props.change(page.value)"
:status="page.value === '' ? 'error' : ''"
>
</a-input>
</a-tooltip>
<a-date-picker
v-if="props.type === CondKwDate"
style="width: 100%"
v-model:value="page.dateValue"
@change="props.change(page.dateValue.format('YYYY-MM-DD'))"
:allowClear="false"
/>
</div>
</template>
<script setup lang="ts">
import dayjs from "dayjs";
const props = defineProps<{
change: Function;
value: string;
type: string;
}>();
const page = reactive<{
value: string;
dateValue: any;
}>({
value: props.value,
dateValue: undefined,
});
onMounted(() => {
if (props.type === "date") {
page.dateValue = dayjs(Date.parse(page.value));
}
});
</script>
<script lang="ts">
import { onMounted, reactive } from "vue";
import type { Condition, OpList } from "../Conditions.vue";
import i18n from "@/locale";
const CondKwDate = `date`;
const CondKwHeader = `header`;
const CondKwSize = `size`;
const CondKwSpamScore = `spam_score`;
const CondKwBody = `body`;
const CondKwAll = `all`;
const CondKwFromSMTP = `from`;
const CondKwToSMTP = `to`;
export const conditionsList: Condition[] = [
{
Value: CondKwDate,
Label: i18n.global.t("components.common.rules.incoming.conditions.date"),
KeyIdx: 0,
OpIdx: 1,
ValIdx: 2,
OpList: [
{
Val: `=`,
Cname: i18n.global.t(
"components.common.rules.incoming.conditions.op_equal"
),
},
{
Val: `>=`,
Cname: i18n.global.t(
"components.common.rules.incoming.conditions.op_starts"
),
},
{
Val: `<=`,
Cname: i18n.global.t(
"components.common.rules.incoming.conditions.op_ends"
),
},
],
Args: [CondKwDate, "=", new Date().toISOString().split("T")[0]],
},
{
Value: CondKwToSMTP,
Label: i18n.global.t("components.common.rules.incoming.conditions.to"),
KeyIdx: 0,
OpIdx: 1,
ValIdx: 2,
OpList: [
{
Val: `=`,
Cname: i18n.global.t(
"components.common.rules.incoming.conditions.op_contains"
),
},
{
Val: `!`,
Cname: i18n.global.t(
"components.common.rules.incoming.conditions.op_not_contains"
),
},
{
Val: `==`,
Cname: i18n.global.t(
"components.common.rules.incoming.conditions.op_equal"
),
},
{
Val: `!=`,
Cname: i18n.global.t(
"components.common.rules.incoming.conditions.op_not_equal"
),
},
{
Val: `^`,
Cname: i18n.global.t(
"components.common.rules.incoming.conditions.op_starts"
),
},
{
Val: `$`,
Cname: i18n.global.t(
"components.common.rules.incoming.conditions.op_ends"
),
},
],
Args: [CondKwToSMTP, "=", "test@example.com"],
},
{
Value: CondKwFromSMTP,
Label: i18n.global.t("components.common.rules.incoming.conditions.from"),
KeyIdx: 0,
OpIdx: 1,
ValIdx: 2,
OpList: [
{
Val: `=`,
Cname: i18n.global.t(
"components.common.rules.incoming.conditions.op_contains"
),
},
{
Val: `!`,
Cname: i18n.global.t(
"components.common.rules.incoming.conditions.op_not_contains"
),
},
{
Val: `==`,
Cname: i18n.global.t(
"components.common.rules.incoming.conditions.op_equal"
),
},
{
Val: `!=`,
Cname: i18n.global.t(
"components.common.rules.incoming.conditions.op_not_equal"
),
},
{
Val: `^`,
Cname: i18n.global.t(
"components.common.rules.incoming.conditions.op_starts"
),
},
{
Val: `$`,
Cname: i18n.global.t(
"components.common.rules.incoming.conditions.op_ends"
),
},
],
Args: [CondKwFromSMTP, "=", "test@example.com"],
},
{
Value: CondKwHeader,
Label: i18n.global.t(
"components.common.rules.incoming.conditions.header_to"
),
KeyIdx: 1,
OpIdx: 2,
ValIdx: 3,
OpList: [
{
Val: `=`,
Cname: i18n.global.t(
"components.common.rules.incoming.conditions.op_contains"
),
},
{
Val: `!`,
Cname: i18n.global.t(
"components.common.rules.incoming.conditions.op_not_contains"
),
},
{
Val: `==`,
Cname: i18n.global.t(
"components.common.rules.incoming.conditions.op_equal"
),
},
{
Val: `!=`,
Cname: i18n.global.t(
"components.common.rules.incoming.conditions.op_not_equal"
),
},
{
Val: `^`,
Cname: i18n.global.t(
"components.common.rules.incoming.conditions.op_starts"
),
},
{
Val: `$`,
Cname: i18n.global.t(
"components.common.rules.incoming.conditions.op_ends"
),
},
],
Args: [CondKwHeader, "To", "=", "test@example.com"],
},
{
Value: CondKwHeader,
Label: i18n.global.t(
"components.common.rules.incoming.conditions.header_from"
),
KeyIdx: 1,
OpIdx: 2,
ValIdx: 3,
OpList: [
{
Val: `=`,
Cname: i18n.global.t(
"components.common.rules.incoming.conditions.op_contains"
),
},
{
Val: `!`,
Cname: i18n.global.t(
"components.common.rules.incoming.conditions.op_not_contains"
),
},
{
Val: `==`,
Cname: i18n.global.t(
"components.common.rules.incoming.conditions.op_equal"
),
},
{
Val: `!=`,
Cname: i18n.global.t(
"components.common.rules.incoming.conditions.op_not_equal"
),
},
{
Val: `^`,
Cname: i18n.global.t(
"components.common.rules.incoming.conditions.op_starts"
),
},
{
Val: `$`,
Cname: i18n.global.t(
"components.common.rules.incoming.conditions.op_ends"
),
},
],
Args: [CondKwHeader, "From", "=", "test@example.com"],
},
{
Value: CondKwHeader,
Label: i18n.global.t("components.common.rules.incoming.conditions.subject"),
KeyIdx: 1,
OpIdx: 2,
ValIdx: 3,
OpList: [
{
Val: `=`,
Cname: i18n.global.t(
"components.common.rules.incoming.conditions.op_contains"
),
},
{
Val: `!`,
Cname: i18n.global.t(
"components.common.rules.incoming.conditions.op_not_contains"
),
},
{
Val: `==`,
Cname: i18n.global.t(
"components.common.rules.incoming.conditions.op_equal"
),
},
{
Val: `!=`,
Cname: i18n.global.t(
"components.common.rules.incoming.conditions.op_not_equal"
),
},
{
Val: `^`,
Cname: i18n.global.t(
"components.common.rules.incoming.conditions.op_starts"
),
},
{
Val: `$`,
Cname: i18n.global.t(
"components.common.rules.incoming.conditions.op_ends"
),
},
],
Args: [CondKwHeader, "Subject", "=", "some subject"],
},
{
Value: CondKwHeader,
Label: i18n.global.t(
"components.common.rules.incoming.conditions.copy_recipient"
),
KeyIdx: 1,
OpIdx: 2,
ValIdx: 3,
OpList: [
{
Val: `=`,
Cname: i18n.global.t(
"components.common.rules.incoming.conditions.op_contains"
),
},
{
Val: `!`,
Cname: i18n.global.t(
"components.common.rules.incoming.conditions.op_not_contains"
),
},
{
Val: `==`,
Cname: i18n.global.t(
"components.common.rules.incoming.conditions.op_equal"
),
},
{
Val: `!=`,
Cname: i18n.global.t(
"components.common.rules.incoming.conditions.op_not_equal"
),
},
{
Val: `^`,
Cname: i18n.global.t(
"components.common.rules.incoming.conditions.op_starts"
),
},
{
Val: `$`,
Cname: i18n.global.t(
"components.common.rules.incoming.conditions.op_ends"
),
},
],
Args: [CondKwHeader, "Cc", "=", "test@example.com"],
},
{
Value: CondKwSize,
Label: i18n.global.t("components.common.rules.incoming.conditions.size"),
KeyIdx: 0,
OpIdx: 1,
ValIdx: 2,
OpList: [
{
Val: `<`,
Cname: i18n.global.t(
"components.common.rules.incoming.conditions.op_less"
),
},
{
Val: `>`,
Cname: i18n.global.t(
"components.common.rules.incoming.conditions.op_greater"
),
},
{
Val: `=`,
Cname: i18n.global.t(
"components.common.rules.incoming.conditions.op_equal"
),
},
],
Args: [CondKwSize, "=", "100K"],
},
{
Value: CondKwSpamScore,
Label: i18n.global.t(
"components.common.rules.incoming.conditions.spam_score"
),
KeyIdx: 0,
OpIdx: 1,
ValIdx: 2,
OpList: [
{
Val: `<=`,
Cname: i18n.global.t(
"components.common.rules.incoming.conditions.op_less_or_equal"
),
},
{
Val: `>=`,
Cname: i18n.global.t(
"components.common.rules.incoming.conditions.op_greater_or_equal"
),
},
{
Val: `=`,
Cname: i18n.global.t(
"components.common.rules.incoming.conditions.op_equal"
),
},
],
Args: [CondKwSpamScore, "=", "5"],
},
{
Value: CondKwBody,
Label: i18n.global.t("components.common.rules.incoming.conditions.body"),
KeyIdx: 0,
OpIdx: 1,
ValIdx: 2,
OpList: [
{
Val: `=`,
Cname: i18n.global.t(
"components.common.rules.incoming.conditions.op_contains"
),
},
{
Val: `!`,
Cname: i18n.global.t(
"components.common.rules.incoming.conditions.op_not_contains"
),
},
],
Args: [CondKwBody, "=", "hello"],
},
{
Value: CondKwAll,
Label: i18n.global.t("components.common.rules.incoming.conditions.all"),
KeyIdx: 0,
OpIdx: -1,
ValIdx: -1,
OpList: [],
Args: [CondKwAll],
},
];
function filterCondsForMailDeletion(list: Condition[]): Condition[] {
for (let i = 0; i < list.length; i++) {
const element = list[i];
if (element.Value === CondKwHeader) {
element.OpList = element.OpList.filter((c: OpList) => {
return c.Val === "=" || c.Val === "!";
});
}
}
return list.filter((c: Condition) => {
return (
c.Value === CondKwDate ||
c.Value === CondKwHeader ||
c.Value === CondKwSize ||
c.Value === CondKwAll ||
c.Value === CondKwBody ||
c.Value === CondKwSpamScore
);
});
}
export const conditionsListForMailDeletion: Condition[] =
filterCondsForMailDeletion(conditionsList);
</script>

View File

@ -0,0 +1,381 @@
<template>
<div style="width: 100%">
<a-input
v-if="
type === ActKwTransformFromAddress ||
type === ActKwTransformToAddress ||
type === ActKwTransformFromAddressAndHeader ||
type === ActKwTransformToAddressAndHeader ||
type === ActKwCopyTo ||
type === ActKwPremoderation ||
type === ActKwSendViaSmartHost
"
v-model:value="page.value"
@change="props.change(page.value)"
:status="page.value === '' ? 'error' : ''"
>
</a-input>
<a-input-number
v-if="
type === ActKwRejectExternalCount || type === ActKwRejectInternalCount
"
style="width: 100%"
v-model:value="page.value"
@change="props.change(page.value)"
:status="!page.value ? 'error' : ''"
:min="0"
>
</a-input-number>
<a-select
v-if="
(type === ActKwRejectGroups || type === ActKwAllowGroups) &&
page.gEmails.length !== 0
"
v-model:value="page.value"
@change="(newVal: string) => props.change(newVal)"
:status="page.value === '' ? 'error' : ''"
>
<a-select-option v-for="v in page.gEmails" :value="v">
{{ v }}
</a-select-option>
</a-select>
<a-select
v-if="
(type === ActKwRejectInGroup || type === ActKwAllowInGroup) &&
page.groups.length !== 0
"
v-model:value="page.value"
@change="(newVal: string) => props.change(newVal)"
:status="page.value === '' ? 'error' : ''"
>
<a-select-option v-for="v in page.groups" :value="v">
{{ v }}
</a-select-option>
</a-select>
<a-textarea
v-if="type === ActKwSendResponse"
v-model:value="page.value"
@change="props.change(page.value)"
:status="page.value === '' ? 'error' : ''"
>
</a-textarea>
<a-textarea
v-if="type === ActKwAddTextToStart || type === ActKwAddTextToEnd"
v-model:value="page.value"
@change="props.change(page.value)"
:status="page.value === '' ? 'error' : ''"
>
</a-textarea>
</div>
</template>
<script setup lang="ts">
import { useRoute } from "vue-router";
const route = useRoute();
const props = defineProps<{
change: Function;
value: string;
type: string;
getGroupsUrl: Function;
getGroupEmailsUrl: Function;
}>();
const page = reactive<{
value: string;
gEmails: [];
groups: [];
}>({
value: props.value,
gEmails: [],
groups: [],
});
onMounted(() => {
if (props.type === ActKwAllowInGroup || props.type === ActKwRejectInGroup) {
getGroups();
}
if (props.type === ActKwAllowGroups || props.type === ActKwRejectGroups) {
getGroupEmails();
}
});
async function getGroups() {
let url = props.getGroupsUrl();
const res = await apiFetch(url);
if (res.error) {
notifyError(res.error);
return;
}
page.groups = [];
page.groups = res.data;
}
async function getGroupEmails() {
let url = props.getGroupEmailsUrl();
const res = await apiFetch(url);
if (res.error) {
notifyError(res.error);
return;
}
page.gEmails = [];
page.gEmails = res.data;
}
</script>
<script lang="ts">
import { onMounted, reactive } from "vue";
import type { Action } from "../Actions.vue";
import { apiFetch } from "@/composables/apiFetch";
import { notifyError } from "@/composables/alert";
import i18n from "@/locale";
const ActKwTransformFromAddress = `transform_from_address`;
const ActKwTransformToAddress = `transform_to_address`;
const ActKwTransformFromAddressAndHeader = `transform_from_address_and_header`;
const ActKwTransformToAddressAndHeader = `transform_to_address_and_header`;
const ActKwCopyTo = `copy_to`;
const ActKwPremoderation = `premoderation`;
const ActKwSendViaSmartHost = `send_via_smart-host`;
const ActKwRejectExternal = `reject_external`;
const ActKwRejectExternalCount = `reject_external_count`;
const ActKwRejectInternalCount = `reject_internal_count`;
const ActKwRejectGroups = `reject_groups`;
const ActKwRejectInGroup = `reject_in_group`;
const ActRejectAny = `reject_any`;
const ActKwAllowExternal = `allow_external`;
const ActKwAllowGroups = `allow_groups`;
const ActKwAllowInGroup = `allow_in_group`;
const ActKwAllowAny = `allow_any`;
const ActKwSendResponse = `send_response`;
const ActKwAddTextToStart = `add_text_to_start`;
const ActKwAddTextToEnd = `add_text_to_end`;
const allActions = [
ActKwTransformFromAddress,
ActKwTransformToAddress,
ActKwTransformFromAddressAndHeader,
ActKwTransformToAddressAndHeader,
ActKwCopyTo,
ActKwPremoderation,
ActKwSendViaSmartHost,
ActKwRejectExternal,
ActKwRejectExternalCount,
ActKwRejectInternalCount,
ActKwRejectGroups,
ActKwRejectInGroup,
ActRejectAny,
ActKwAllowExternal,
ActKwAllowGroups,
ActKwAllowInGroup,
ActKwAllowAny,
ActKwSendResponse,
];
const onlyAllowActions = [
ActKwAllowExternal,
ActKwAllowGroups,
ActKwAllowInGroup,
ActKwAllowAny,
];
export const actionsList: Action[] = [
{
Value: ActKwTransformFromAddress,
Label: i18n.global.t(
"components.common.rules.outgoing.actions.transform_from_address"
),
NextAllowedActions: allActions,
Args: [ActKwTransformFromAddress, "*@(old_host.ru) => new_host.ru"],
NoVal: false,
},
{
Value: ActKwTransformToAddress,
Label: i18n.global.t(
"components.common.rules.outgoing.actions.transform_to_address"
),
NextAllowedActions: allActions,
Args: [ActKwTransformToAddress, "*@(old_host.ru) => new_host.ru"],
NoVal: false,
},
{
Value: ActKwTransformFromAddressAndHeader,
Label: i18n.global.t(
"components.common.rules.outgoing.actions.transform_from_address_and_header"
),
NextAllowedActions: allActions,
Args: [
ActKwTransformFromAddressAndHeader,
"*@(old_host.ru) => new_host.ru",
],
NoVal: false,
},
{
Value: ActKwTransformToAddressAndHeader,
Label: i18n.global.t(
"components.common.rules.outgoing.actions.transform_to_address_and_header"
),
NextAllowedActions: allActions,
Args: [ActKwTransformToAddressAndHeader, "*@(old_host.ru) => new_host.ru"],
NoVal: false,
},
{
Value: ActKwCopyTo,
Label: i18n.global.t("components.common.rules.outgoing.actions.copy_to"),
NextAllowedActions: allActions,
Args: [ActKwCopyTo, "copy_to@example.ru"],
NoVal: false,
},
{
Value: ActKwPremoderation,
Label: i18n.global.t(
"components.common.rules.outgoing.actions.premoderation"
),
NextAllowedActions: allActions,
Args: [ActKwPremoderation, "moderator@example.ru"],
NoVal: false,
},
{
Value: ActKwSendViaSmartHost,
Label: i18n.global.t(
"components.common.rules.outgoing.actions.send_via_smart_host"
),
NextAllowedActions: allActions,
Args: [ActKwSendViaSmartHost, "[user:pass@]host[:port]"],
NoVal: false,
},
{
Value: ActKwRejectExternal,
Label: i18n.global.t(
"components.common.rules.outgoing.actions.reject_external"
),
NextAllowedActions: allActions,
Args: [ActKwRejectExternal, ""],
NoVal: true,
},
{
Value: ActKwRejectExternalCount,
Label: i18n.global.t(
"components.common.rules.outgoing.actions.reject_external_count"
),
NextAllowedActions: allActions,
Args: [ActKwRejectExternalCount, "10"],
NoVal: false,
},
{
Value: ActKwRejectInternalCount,
Label: i18n.global.t(
"components.common.rules.outgoing.actions.reject_internal_count"
),
NextAllowedActions: allActions,
Args: [ActKwRejectInternalCount, "10"],
NoVal: false,
},
{
Value: ActKwRejectGroups,
Label: i18n.global.t(
"components.common.rules.outgoing.actions.reject_groups"
),
NextAllowedActions: allActions,
Args: [ActKwRejectGroups, ""],
NoVal: false,
},
{
Value: ActKwRejectInGroup,
Label: i18n.global.t(
"components.common.rules.outgoing.actions.reject_in_group"
),
NextAllowedActions: allActions,
Args: [ActKwRejectInGroup, ""],
NoVal: false,
},
{
Value: ActRejectAny,
Label: i18n.global.t("components.common.rules.outgoing.actions.reject_any"),
NextAllowedActions: allActions,
Args: [ActRejectAny, ""],
NoVal: true,
},
{
Value: ActKwAllowExternal,
Label: i18n.global.t(
"components.common.rules.outgoing.actions.allow_external"
),
NextAllowedActions: onlyAllowActions,
Args: [ActKwAllowExternal, ""],
NoVal: true,
},
{
Value: ActKwAllowGroups,
Label: i18n.global.t(
"components.common.rules.outgoing.actions.allow_groups"
),
NextAllowedActions: onlyAllowActions,
Args: [ActKwAllowGroups, ""],
NoVal: false,
},
{
Value: ActKwAllowInGroup,
Label: i18n.global.t(
"components.common.rules.outgoing.actions.allow_in_group"
),
NextAllowedActions: onlyAllowActions,
Args: [ActKwAllowInGroup, ""],
NoVal: false,
},
{
Value: ActKwAllowAny,
Label: i18n.global.t("components.common.rules.outgoing.actions.allow_any"),
NextAllowedActions: onlyAllowActions,
Args: [ActKwAllowAny, ""],
NoVal: true,
},
{
Value: ActKwSendResponse,
Label: i18n.global.t(
"components.common.rules.outgoing.actions.send_response"
),
NextAllowedActions: allActions,
Args: [
ActKwSendResponse,
i18n.global.t(
"components.common.rules.outgoing.actions.send_response_default"
),
],
NoVal: false,
},
{
Value: ActKwAddTextToStart,
Label: i18n.global.t(
"components.common.rules.outgoing.actions.add_text_to_start"
),
NextAllowedActions: allActions,
Args: [
ActKwAddTextToStart,
i18n.global.t(
"components.common.rules.outgoing.actions.add_text_default"
),
],
NoVal: false,
},
{
Value: ActKwAddTextToEnd,
Label: i18n.global.t(
"components.common.rules.outgoing.actions.add_text_to_end"
),
NextAllowedActions: allActions,
Args: [
ActKwAddTextToEnd,
i18n.global.t(
"components.common.rules.outgoing.actions.add_text_default"
),
],
NoVal: false,
},
];
</script>

View File

@ -0,0 +1,300 @@
<template>
<div style="width: 100%">
<a-select
v-if="
(type === CondKwFromGroup || type === CondKwToGroup) &&
page.groups.length !== 0
"
v-model:value="page.value"
@change="(newVal: string) => props.change(newVal)"
:status="page.value === '' ? 'error' : ''"
>
<a-select-option v-for="g in page.groups" :value="g">
{{ g }}
</a-select-option>
</a-select>
<a-input
v-if="
props.type === CondKwToSMTP ||
props.type === CondKwFromSMTP ||
props.type === CondKwHeader ||
props.type === CondKwBody
"
v-model:value="page.value"
@change="props.change(page.value)"
:status="page.value === '' ? 'error' : ''"
>
</a-input>
</div>
</template>
<script setup lang="ts">
import dayjs from "dayjs";
const props = defineProps<{
change: Function;
value: string;
type: string;
getGroupsUrl: Function;
}>();
const page = reactive<{
value: string;
groups: string[];
}>({
value: props.value,
groups: [],
});
onMounted(() => {
if (props.type === CondKwFromGroup || props.type === CondKwToGroup) {
getGroups();
}
});
async function getGroups() {
let url = props.getGroupsUrl();
const res = await apiFetch(url);
if (res.error) {
notifyError(res.error);
return;
}
page.groups = [];
page.groups = res.data;
}
</script>
<script lang="ts">
import { onMounted, reactive } from "vue";
import type { Condition } from "../Conditions.vue";
import i18n from "@/locale";
import { apiFetch } from "@/composables/apiFetch";
import { notifyError } from "@/composables/alert";
const CondKwToSMTP = `to`;
const CondKwFromSMTP = `from`;
const CondKwFromGroup = `from_group`;
const CondKwToGroup = `to_group`;
const CondKwHeader = `header`;
const CondKwBody = `body`;
const CondKwAll = `all`;
export const conditionsList: Condition[] = [
{
Value: CondKwToSMTP,
Label: i18n.global.t("components.common.rules.outgoing.conditions.to"),
KeyIdx: 0,
OpIdx: 1,
ValIdx: 2,
OpList: [
{
Val: `=`,
Cname: i18n.global.t(
"components.common.rules.outgoing.conditions.op_contains"
),
},
{
Val: `!`,
Cname: i18n.global.t(
"components.common.rules.outgoing.conditions.op_not_contains"
),
},
{
Val: `==`,
Cname: i18n.global.t(
"components.common.rules.outgoing.conditions.op_equal"
),
},
{
Val: `!=`,
Cname: i18n.global.t(
"components.common.rules.outgoing.conditions.op_not_equal"
),
},
{
Val: `^`,
Cname: i18n.global.t(
"components.common.rules.outgoing.conditions.op_starts"
),
},
{
Val: `$`,
Cname: i18n.global.t(
"components.common.rules.outgoing.conditions.op_ends"
),
},
],
Args: [CondKwToSMTP, "=", "test@example.com"],
},
{
Value: CondKwFromSMTP,
Label: i18n.global.t("components.common.rules.outgoing.conditions.from"),
KeyIdx: 0,
OpIdx: 1,
ValIdx: 2,
OpList: [
{
Val: `=`,
Cname: i18n.global.t(
"components.common.rules.outgoing.conditions.op_contains"
),
},
{
Val: `!`,
Cname: i18n.global.t(
"components.common.rules.outgoing.conditions.op_not_contains"
),
},
{
Val: `==`,
Cname: i18n.global.t(
"components.common.rules.outgoing.conditions.op_equal"
),
},
{
Val: `!=`,
Cname: i18n.global.t(
"components.common.rules.outgoing.conditions.op_not_equal"
),
},
{
Val: `^`,
Cname: i18n.global.t(
"components.common.rules.outgoing.conditions.op_starts"
),
},
{
Val: `$`,
Cname: i18n.global.t(
"components.common.rules.outgoing.conditions.op_ends"
),
},
],
Args: [CondKwFromSMTP, "=", "test@example.com"],
},
{
Value: CondKwHeader,
Label: i18n.global.t("components.common.rules.outgoing.conditions.subject"),
KeyIdx: 1,
OpIdx: 2,
ValIdx: 3,
OpList: [
{
Val: `=`,
Cname: i18n.global.t(
"components.common.rules.outgoing.conditions.op_contains"
),
},
{
Val: `!`,
Cname: i18n.global.t(
"components.common.rules.outgoing.conditions.op_not_contains"
),
},
{
Val: `==`,
Cname: i18n.global.t(
"components.common.rules.outgoing.conditions.op_equal"
),
},
{
Val: `!=`,
Cname: i18n.global.t(
"components.common.rules.outgoing.conditions.op_not_equal"
),
},
{
Val: `^`,
Cname: i18n.global.t(
"components.common.rules.outgoing.conditions.op_starts"
),
},
{
Val: `$`,
Cname: i18n.global.t(
"components.common.rules.outgoing.conditions.op_ends"
),
},
],
Args: [CondKwHeader, "Subject", "=", "some subject"],
},
{
Value: CondKwBody,
Label: i18n.global.t("components.common.rules.outgoing.conditions.body"),
KeyIdx: 0,
OpIdx: 1,
ValIdx: 2,
OpList: [
{
Val: `=`,
Cname: i18n.global.t(
"components.common.rules.outgoing.conditions.op_contains"
),
},
{
Val: `!`,
Cname: i18n.global.t(
"components.common.rules.outgoing.conditions.op_not_contains"
),
},
],
Args: [CondKwBody, "=", "hello"],
},
{
Value: CondKwFromGroup,
Label: i18n.global.t(
"components.common.rules.outgoing.conditions.from_group"
),
KeyIdx: 0,
OpIdx: 1,
ValIdx: 2,
OpList: [
{
Val: `==`,
Cname: i18n.global.t(
"components.common.rules.outgoing.conditions.op_equal"
),
},
{
Val: `!=`,
Cname: i18n.global.t(
"components.common.rules.outgoing.conditions.op_not_equal"
),
},
],
Args: [CondKwFromGroup, "==", ""],
},
{
Value: CondKwToGroup,
Label: i18n.global.t(
"components.common.rules.outgoing.conditions.to_group"
),
KeyIdx: 0,
OpIdx: 1,
ValIdx: 2,
OpList: [
{
Val: `==`,
Cname: i18n.global.t(
"components.common.rules.outgoing.conditions.op_equal"
),
},
{
Val: `!=`,
Cname: i18n.global.t(
"components.common.rules.outgoing.conditions.op_not_equal"
),
},
],
Args: [CondKwToGroup, "==", ""],
},
{
Value: CondKwAll,
Label: i18n.global.t("components.common.rules.outgoing.conditions.all"),
KeyIdx: 0,
OpIdx: -1,
ValIdx: -1,
OpList: [],
Args: [CondKwAll],
},
];
</script>

View File

@ -0,0 +1,176 @@
<template>
<div>
<a-table
class="panel-content"
:columns="columns"
:data-source="page.data"
:loading="page.loading"
bordered
size="small"
:pagination="false"
>
<template #title>
<a-input-search
style="width: 300px"
v-model:value="page.pagination.search"
:placeholder="t('components.user.address_books.with_access.search')"
enter-button
allow-clear
@search="get"
/>
</template>
<template #bodyCell="{ column, record }">
<template v-if="column.key === 'perm'">
<div v-if="record.Permissions === 'all'">
{{ t("components.common.shared_address_books.access_perm_all") }}
</div>
<div v-if="record.Permissions === 'read'">
{{ t("components.common.shared_address_books.access_perm_read") }}
</div>
</template>
<template v-if="column.key === 'owner'">
<a-space>
<template v-if="record.OwnerStatus === 'archived'">
<a-tooltip
:title="
t(`components.admin.domains.mailstorage.mailboxes.in_archive`)
"
>
<LockOutlined />
</a-tooltip>
</template>
<template v-if="record.OwnerStatus === 'deleted'">
<a-tooltip
:title="
t(`components.admin.domains.mailstorage.mailboxes.deleted`)
"
>
<DeleteOutlined />
</a-tooltip>
</template>
{{ record.Owner }}
</a-space>
</template>
</template>
<template #footer>
<a-flex justify="flex-end">
<a-pagination
v-model:value="page.pagination.current"
:defaultPageSize="page.pagination.size"
:total="page.pagination.total"
@change="pageChange"
:pageSizeOptions="['10', '20', '50', '100']"
v-model:pageSize="page.pagination.size"
:showSizeChanger="true"
/>
</a-flex>
</template>
</a-table>
</div>
</template>
<script setup lang="ts">
import { notifyError } from "@/composables/alert";
import { apiFetch } from "@/composables/apiFetch";
import router from "@/router";
import Export from "@/components/admin/domains/mailstorage/Export.vue";
import {
DomainPlaceholder,
MailboxPlaceholder,
RouteAdminDomainsDomainMailStorageMailboxesExport,
} from "@/router/consts";
import {
ArrowLeftOutlined,
DeleteOutlined,
DownOutlined,
ExclamationCircleOutlined,
HddOutlined,
HomeOutlined,
LockOutlined,
PlusOutlined,
} from "@ant-design/icons-vue";
import { Modal } from "ant-design-vue";
import type { ColumnType } from "ant-design-vue/es/table";
import { createVNode, onMounted, reactive } from "vue";
import { useI18n } from "vue-i18n";
import { useRoute } from "vue-router";
const route = useRoute();
const { t } = useI18n();
const columns: ColumnType<any>[] = [
{
title: t("components.user.address_books.with_access.col_name"),
dataIndex: "Name",
},
{
title: t("components.user.address_books.with_access.col_owner"),
dataIndex: "Owner",
key: "owner",
},
{
title: t("components.user.address_books.with_access.col_permission"),
dataIndex: "Permissions",
key: "perm",
},
];
const page = reactive<{
loading: boolean;
data: any;
pagination: {
current: number;
total: number;
size: number;
search: string;
};
}>({
pagination: {
current: 1,
total: 0,
size: 50,
search: "",
},
loading: false,
data: [],
});
onMounted(() => {
get();
});
async function get() {
page.loading = true;
const params = new URLSearchParams();
params.append("page", String(page.pagination.current));
params.append("size", String(page.pagination.size));
params.append("search", page.pagination.search);
const res = await apiFetch(
`/user/address_books/with_access?` + params.toString()
);
page.loading = false;
if (res.error) {
notifyError(res.error);
return;
}
page.data = res.data;
page.pagination.total = res.total;
return;
}
function pageChange(current: number) {
page.pagination.current = current;
return get();
}
</script>
<style scoped>
.panel-content {
min-width: 700px;
max-width: 70%;
margin: auto;
}
</style>

View File

@ -0,0 +1,23 @@
<template>
<AddressBooks v-if="page.loaded" :user="page.user"></AddressBooks>
</template>
<script setup lang="ts">
import AddressBooks from "@/components/common/AddressBooks.vue";
import { onMounted, reactive } from "vue";
import { useRoute } from "vue-router";
import { useAuthStore } from "@/stores/auth";
const route = useRoute();
const page = reactive<{
user: string;
loaded: boolean;
}>({
user: "",
loaded: false,
});
onMounted(() => {
page.user = useAuthStore().username;
page.loaded = true;
});
</script>

View File

@ -0,0 +1,176 @@
<template>
<div>
<a-table
class="panel-content"
:columns="columns"
:data-source="page.data"
:loading="page.loading"
bordered
size="small"
:pagination="false"
>
<template #title>
<a-input-search
style="width: 300px"
v-model:value="page.pagination.search"
:placeholder="t('components.user.calendars.with_access.search')"
enter-button
allow-clear
@search="get"
/>
</template>
<template #bodyCell="{ column, record }">
<template v-if="column.key === 'perm'">
<div v-if="record.Permissions === 'all'">
{{ t("components.common.shared_calendars.access_perm_all") }}
</div>
<div v-if="record.Permissions === 'read'">
{{ t("components.common.shared_calendars.access_perm_read") }}
</div>
</template>
<template v-if="column.key === 'owner'">
<a-space>
<template v-if="record.OwnerStatus === 'archived'">
<a-tooltip
:title="
t(`components.admin.domains.mailstorage.mailboxes.in_archive`)
"
>
<LockOutlined />
</a-tooltip>
</template>
<template v-if="record.OwnerStatus === 'deleted'">
<a-tooltip
:title="
t(`components.admin.domains.mailstorage.mailboxes.deleted`)
"
>
<DeleteOutlined />
</a-tooltip>
</template>
{{ record.Owner }}
</a-space>
</template>
</template>
<template #footer>
<a-flex justify="flex-end">
<a-pagination
v-model:value="page.pagination.current"
:defaultPageSize="page.pagination.size"
:total="page.pagination.total"
@change="pageChange"
:pageSizeOptions="['10', '20', '50', '100']"
v-model:pageSize="page.pagination.size"
:showSizeChanger="true"
/>
</a-flex>
</template>
</a-table>
</div>
</template>
<script setup lang="ts">
import { notifyError } from "@/composables/alert";
import { apiFetch } from "@/composables/apiFetch";
import router from "@/router";
import Export from "@/components/admin/domains/mailstorage/Export.vue";
import {
DomainPlaceholder,
MailboxPlaceholder,
RouteAdminDomainsDomainMailStorageMailboxesExport,
} from "@/router/consts";
import {
ArrowLeftOutlined,
DeleteOutlined,
DownOutlined,
ExclamationCircleOutlined,
HddOutlined,
HomeOutlined,
LockOutlined,
PlusOutlined,
} from "@ant-design/icons-vue";
import { Modal } from "ant-design-vue";
import type { ColumnType } from "ant-design-vue/es/table";
import { createVNode, onMounted, reactive } from "vue";
import { useI18n } from "vue-i18n";
import { useRoute } from "vue-router";
const route = useRoute();
const { t } = useI18n();
const columns: ColumnType<any>[] = [
{
title: t("components.user.calendars.with_access.col_name"),
dataIndex: "Name",
},
{
title: t("components.user.calendars.with_access.col_owner"),
dataIndex: "Owner",
key: "owner",
},
{
title: t("components.user.calendars.with_access.col_permission"),
dataIndex: "Permissions",
key: "perm",
},
];
const page = reactive<{
loading: boolean;
data: any;
pagination: {
current: number;
total: number;
size: number;
search: string;
};
}>({
pagination: {
current: 1,
total: 0,
size: 50,
search: "",
},
loading: false,
data: [],
});
onMounted(() => {
get();
});
async function get() {
page.loading = true;
const params = new URLSearchParams();
params.append("page", String(page.pagination.current));
params.append("size", String(page.pagination.size));
params.append("search", page.pagination.search);
const res = await apiFetch(
`/user/calendars/with_access?` + params.toString()
);
page.loading = false;
if (res.error) {
notifyError(res.error);
return;
}
page.data = res.data;
page.pagination.total = res.total;
return;
}
function pageChange(current: number) {
page.pagination.current = current;
return get();
}
</script>
<style scoped>
.panel-content {
min-width: 700px;
max-width: 70%;
margin: auto;
}
</style>

View File

@ -0,0 +1,360 @@
<template>
<div>
<a-table
class="panel-content"
:columns="columns"
:data-source="page.data"
:loading="page.loading"
bordered
size="small"
:pagination="false"
>
<template #title>
<a-row>
<a-col style="display: flex; align-items: flex-end" :span="18">
<a-space>
<a-input-search
style="width: 300px"
v-model:value="page.pagination.search"
:placeholder="
t('components.user.calendars.events_planner.my_events.search')
"
enter-button
allow-clear
@search="get"
/>
<a-range-picker
style="width: 230px"
@change="get"
v-model:value="page.range"
>
</a-range-picker>
</a-space>
</a-col>
<a-col :span="6">
<a-space style="display: flex; justify-content: end">
<a-button type="primary" @click="newEvent">
<PlusOutlined />
{{
t(
"components.user.calendars.events_planner.my_events.new_event"
)
}}
</a-button>
</a-space>
</a-col>
</a-row>
</template>
<template #bodyCell="{ column, record }">
<template v-if="column.key === 'start'">
<template v-if="record.Start">
{{ record.Start }}
</template>
<template v-else>
<a-space>
{{
record.ApprovalCreatedAt
? t(
"components.user.calendars.events_planner.my_events.status_under_approval"
)
: t(
"components.user.calendars.events_planner.my_events.status_approval_done"
)
}}
<a-button shape="circle" @click="showApprovalStatus(record.Id)">
<EyeOutlined />
</a-button>
</a-space>
</template>
</template>
<template v-if="column.key === 'action'">
<a-space>
<a-button @click="editEvent(record.Id)">{{
t("common.misc.edit")
}}</a-button>
<a-popconfirm
:title="t('common.misc.delete') + '?'"
:ok-text="t('common.misc.ok')"
:cancel-text="t('common.misc.cancel')"
@confirm="deleteEvent(record.Id)"
>
<a-button danger>{{ t("common.misc.delete") }}</a-button>
</a-popconfirm>
</a-space>
</template>
</template>
<template #footer>
<a-flex justify="flex-end">
<a-pagination
v-model:value="page.pagination.current"
:defaultPageSize="page.pagination.size"
:total="page.pagination.total"
@change="pageChange"
:pageSizeOptions="['10', '20', '50', '100']"
v-model:pageSize="page.pagination.size"
:showSizeChanger="true"
/>
</a-flex>
</template>
</a-table>
</div>
<a-modal v-model:open="page.approvalStatus.show">
<template v-if="page.approvalStatus.approved.length !== 0">
<a-divider orientation="left">{{
$t(
"components.user.calendars.events_planner.my_events.approval_with_ft"
)
}}</a-divider>
<a-list size="small" bordered :data-source="page.approvalStatus.approved">
<template #renderItem="{ item }">
<a-list-item>{{ item }}</a-list-item>
</template>
</a-list>
</template>
<template v-if="page.approvalStatus.waiting.length !== 0">
<a-divider orientation="left">{{
$t(
"components.user.calendars.events_planner.my_events.approval_without_ft"
)
}}</a-divider>
<a-list size="small" bordered :data-source="page.approvalStatus.waiting">
<template #renderItem="{ item }">
<a-list-item>{{ item }}</a-list-item>
</template>
</a-list>
</template>
<template #footer> </template>
</a-modal>
<ScriptErrorNotify
v-model:open="page.scriptErrorNotify.show"
:data="page.scriptErrorNotify.data"
></ScriptErrorNotify>
</template>
<script setup lang="ts">
import { notifyError } from "@/composables/alert";
import { apiFetch } from "@/composables/apiFetch";
import router from "@/router";
import dayjs, { Dayjs } from "dayjs";
import Export from "@/components/admin/domains/mailstorage/Export.vue";
import {
DomainPlaceholder,
EventIdPlaceholder,
MailboxPlaceholder,
RouteAdminDomainsDomainMailStorageMailboxesExport,
RouteUserCalendarsEventsPlannerEdit,
RouteUserCalendarsEventsPlannerNew,
} from "@/router/consts";
import {
ArrowLeftOutlined,
DeleteOutlined,
DownOutlined,
ExclamationCircleOutlined,
EyeOutlined,
HddOutlined,
HomeOutlined,
PlusOutlined,
QuestionOutlined,
} from "@ant-design/icons-vue";
import { Modal } from "ant-design-vue";
import type { ColumnType } from "ant-design-vue/es/table";
import { createVNode, onMounted, reactive } from "vue";
import { useI18n } from "vue-i18n";
import { useRoute } from "vue-router";
import { timeToDateTime } from "@/composables/misc";
const route = useRoute();
const { t } = useI18n();
const columns: ColumnType<any>[] = [
{
title: t("components.user.calendars.events_planner.my_events.col_name"),
dataIndex: "Subject",
},
{
title: t("components.user.calendars.events_planner.my_events.col_start"),
dataIndex: "Start",
key: "start",
},
{
title: t("components.user.calendars.events_planner.my_events.col_finish"),
dataIndex: "Finish",
},
{
title: t(
"components.user.calendars.events_planner.my_events.col_participants"
),
dataIndex: "Participants",
},
// {
// title: t("components.user.calendars.events_planner.my_events.col_status"),
// dataIndex: "Status",
// key: "status",
// },
{
title: "",
key: "action",
},
];
const defaultFrom = new Date();
defaultFrom.setHours(0, 0, 0, 0);
const defaultTo = new Date().setDate(defaultFrom.getDate() + 7);
const page = reactive<{
loading: boolean;
data: any[];
range: [any, any];
pagination: {
current: number;
total: number;
size: number;
search: string;
};
approvalStatus: {
show: boolean;
waiting: string[];
approved: string[];
};
scriptErrorNotify: {
show: boolean;
data: any;
};
}>({
pagination: {
current: 1,
total: 0,
size: 50,
search: "",
},
loading: false,
data: [],
range: [dayjs(defaultFrom), dayjs(defaultTo)],
approvalStatus: {
show: false,
waiting: [],
approved: [],
},
scriptErrorNotify: {
show: false,
data: undefined,
},
});
onMounted(() => {
get();
});
async function get() {
page.loading = true;
const params = new URLSearchParams();
params.append("page", String(page.pagination.current));
params.append("size", String(page.pagination.size));
params.append("from", (page.range[0] as Dayjs).toISOString());
params.append("to", (page.range[1] as Dayjs).toISOString());
params.append("search", page.pagination.search);
const res = await apiFetch(`/user/calendars/events?` + params.toString());
page.loading = false;
if (res.error) {
notifyError(res.error);
return;
}
page.data = [];
if (res.data) {
(res.data as Array<any>).forEach((el: any) => {
el.Participants =
(el.MandatoryParticipants as Array<any>).length +
(el.OptionalParticipants as Array<any>).length;
if (el.IncludeCreator) {
el.Participants += 1;
}
if (el.Start) {
el.Finish = timeToDateTime(
addMinutes(new Date(el.Start), el.DurationMins)
);
el.Start = timeToDateTime(el.Start);
}
page.data.push(el);
});
}
page.pagination.total = res.total;
return;
}
function addMinutes(date: Date, minutes: number): Date {
return new Date(date.getTime() + minutes * 60000);
}
function pageChange(current: number) {
page.pagination.current = current;
return get();
}
async function deleteEvent(id: number) {
const res = await apiFetch(`/user/calendars/events/${id}`, {
method: "DELETE",
});
if (res.error) {
notifyError(res.error);
return;
}
if (res.data) {
page.scriptErrorNotify.data = res.data;
page.scriptErrorNotify.show = true;
return;
}
return get();
}
async function editEvent(id: number) {
router.push({
path: RouteUserCalendarsEventsPlannerEdit.replace(
EventIdPlaceholder,
id.toString()
),
});
}
async function newEvent() {
router.push({
path: RouteUserCalendarsEventsPlannerNew,
});
}
async function showApprovalStatus(eventId: number) {
page.approvalStatus.approved = [];
page.approvalStatus.waiting = [];
const res = await apiFetch(
`/user/calendars/events/${eventId}/approval_status`
);
page.loading = false;
if (res.error) {
notifyError(res.error);
return;
}
page.approvalStatus.approved = res.data.approved;
page.approvalStatus.waiting = res.data.waiting;
page.approvalStatus.show = true;
}
</script>
<style scoped>
.panel-content {
min-width: 700px;
max-width: 70%;
margin: auto;
}
</style>

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,90 @@
<template>
<a-modal v-model:open="open" width="800px">
<a-descriptions
:column="1"
:labelStyle="{ fontWeight: '600' }"
bordered
size="small"
>
<template #title>
<a-space>
<ExclamationCircleTwoTone
two-tone-color="orangered"
style="font-size: x-large"
/>
<div style="font-size: large">
{{
$t(
"components.user.calendars.events_planner.script_error_notify.title"
)
}}
</div>
</a-space>
</template>
<a-descriptions-item
:label="
$t(
`components.user.calendars.events_planner.script_error_notify.resource_name`
)
"
>{{ props.data.ResourceName }}</a-descriptions-item
>
<a-descriptions-item
:label="
$t(
`components.user.calendars.events_planner.script_error_notify.error`
)
"
>
<div style="color: orangered">{{ props.data.Error }}</div>
</a-descriptions-item>
<a-descriptions-item
:label="
$t(
`components.user.calendars.events_planner.script_error_notify.script`
)
"
>
<a-textarea v-model:value="props.data.Script" :rows="13"></a-textarea>
</a-descriptions-item>
<a-descriptions-item
:label="
$t(
`components.user.calendars.events_planner.script_error_notify.output`
)
"
>
<a-textarea v-model:value="props.data.Output" :rows="3"></a-textarea>
</a-descriptions-item>
</a-descriptions>
<template #footer></template>
</a-modal>
</template>
<script setup lang="ts">
import { Modal } from "ant-design-vue";
import {
CloseCircleFilled,
ExclamationCircleFilled,
ExclamationCircleTwoTone,
} from "@ant-design/icons-vue";
import { onMounted, onUnmounted, reactive } from "vue";
interface ScriptErrorMeta {
ResourceName: string;
Error: string;
Script: string;
Output: string;
}
const open = defineModel("open");
const props = defineProps<{
data: ScriptErrorMeta;
}>();
const page = reactive<{
meta?: ScriptErrorMeta;
}>({
meta: undefined,
});
</script>

View File

@ -0,0 +1,23 @@
<template>
<Calendars v-if="page.loaded" :user="page.user"></Calendars>
</template>
<script setup lang="ts">
import Calendars from "@/components/common/Calendars.vue";
import { onMounted, reactive } from "vue";
import { useRoute } from "vue-router";
import { useAuthStore } from "@/stores/auth";
const route = useRoute();
const page = reactive<{
user: string;
loaded: boolean;
}>({
user: "",
loaded: false,
});
onMounted(() => {
page.user = useAuthStore().username;
page.loaded = true;
});
</script>

View File

@ -0,0 +1,255 @@
<template>
<div>
<a-card class="panel-content">
<div v-if="page.contentLoaded">
<a-form
v-if="page.contentLoaded"
:label-col="labelCol"
:wrapper-col="wrapperCol"
:labelWrap="true"
>
<a-form-item
:label="t('components.user.calendars.share_free_time.timezone')"
>
<TimezoneSelect
v-model:value="page.settings.Timezone"
@change="updateSettings"
>
</TimezoneSelect>
</a-form-item>
<a-form-item
:label="t('components.user.calendars.share_free_time.work_days')"
>
<WorkDaysSelect
@change="updateSettings"
v-model:value="page.settings.WorkDays"
>
</WorkDaysSelect>
</a-form-item>
<a-form-item
:label="t('components.user.calendars.share_free_time.work_hours')"
>
<WorkHoursRangePicker
@change="updateSettings"
v-model:value="page.settings.workTimeValues"
>
</WorkHoursRangePicker>
</a-form-item>
<a-form-item
:label="
t('components.user.calendars.share_free_time.time_between_events')
"
>
<a-select
@change="updateSettings"
v-model:value="page.settings.timeBetweenEventsMinsValue"
>
<a-select-option value="0"
>0 {{ t("common.suffixes.min") }}</a-select-option
>
<a-select-option value="5"
>5 {{ t("common.suffixes.min") }}</a-select-option
>
<a-select-option value="10"
>10 {{ t("common.suffixes.min") }}</a-select-option
>
<a-select-option value="15"
>15 {{ t("common.suffixes.min") }}</a-select-option
>
<a-select-option value="30"
>30 {{ t("common.suffixes.min") }}</a-select-option
>
<a-select-option value="60"
>60 {{ t("common.suffixes.min") }}</a-select-option
>
</a-select>
</a-form-item>
<a-form-item
:label="
t(
'components.user.calendars.share_free_time.two_month_restriction'
)
"
>
<a-switch
@change="updateSettings"
v-model:checked="page.settings.TwoMonthRestriction"
>
</a-switch>
</a-form-item>
</a-form>
<a-divider></a-divider>
<div>
<a-row :gutter="16">
<a-col flex="auto">
<a-input-search :disabled="!page.link" v-model:value="page.link">
<template #enterButton>
<a-button
:disabled="!page.link"
@click="copyToClipboard(page.link)"
>
<CopyOutlined />
</a-button>
</template> </a-input-search
></a-col>
<a-col flex="100px">
<a-button
type="primary"
@click="getToken"
:loading="page.linkLoading"
>
{{ $t("components.user.calendars.share_free_time.gen_link") }}
</a-button></a-col
>
</a-row>
</div>
</div>
<a-spin
v-else
size="large"
style="display: flex; justify-content: center"
></a-spin>
</a-card>
</div>
</template>
<script setup lang="ts">
import { notifyError, notifySuccess } from "@/composables/alert";
import { apiFetch } from "@/composables/apiFetch";
import { onMounted, reactive } from "vue";
import { useI18n } from "vue-i18n";
import dayjs from "dayjs";
import { copyToClipboard } from "@/composables/misc";
import {
CopyOutlined,
LockOutlined,
RetweetOutlined,
} from "@ant-design/icons-vue";
import { RouteSharedFreeTime, TokenPlaceholder } from "@/router/consts";
import TimezoneSelect from "../../common/TimezoneSelect.vue";
import WorkDaysSelect from "../../common/WorkDaysSelect.vue";
import WorkHoursRangePicker from "../../common/WorkHoursRangePicker.vue";
const { t } = useI18n();
const labelCol = { span: 14, style: { "text-align": "left" } };
const wrapperCol = { span: 10, style: {} };
interface Settings {
Timezone: string;
WorkDays: string[];
WorkTime: string[];
workTimeValues: any[];
TimeBetweenEventsMins: number;
timeBetweenEventsMinsValue: string;
TwoMonthRestriction: boolean;
Token: string;
Hostname: string;
}
const page = reactive<{
contentLoaded: boolean;
settings: Settings;
link: string;
linkLoading: boolean;
}>({
contentLoaded: false,
settings: {
TwoMonthRestriction: true,
Timezone: "+03",
WorkTime: [],
workTimeValues: [
dayjs(Date.parse("1970-01-01 10:00")),
dayjs(Date.parse("1970-01-01 19:00")),
],
WorkDays: ["1", "2", "3", "4", "5"],
TimeBetweenEventsMins: 0,
timeBetweenEventsMinsValue: "0",
Token: "",
Hostname: "",
},
link: "",
linkLoading: false,
});
onMounted(() => {
getSettings();
});
async function getSettings() {
const params = new URLSearchParams();
params.append("timezone", String(new Date().getTimezoneOffset()));
const res = await apiFetch("/user/share_free_time?" + params.toString());
if (res.error) {
notifyError(res.error);
return;
}
page.contentLoaded = true;
page.settings = res.data;
page.settings.timeBetweenEventsMinsValue = String(
page.settings.TimeBetweenEventsMins
);
page.settings.workTimeValues = [
dayjs(Date.parse("1970-01-01 " + page.settings.WorkTime[0])),
dayjs(Date.parse("1970-01-01 " + page.settings.WorkTime[1])),
];
page.link = generateLink(page.settings.Hostname, page.settings.Token);
}
async function updateSettings() {
page.settings.TimeBetweenEventsMins = Number(
page.settings.timeBetweenEventsMinsValue
);
page.settings.WorkTime[0] = page.settings.workTimeValues[0].format("HH:mm");
page.settings.WorkTime[1] = page.settings.workTimeValues[1].format("HH:mm");
const res = await apiFetch("/user/share_free_time", {
method: "POST",
body: page.settings,
});
if (res.error) {
notifyError(res.error);
return;
}
}
async function getToken() {
page.linkLoading = true;
const res = await apiFetch("/user/share_free_time/refresh", {
method: "POST",
});
page.linkLoading = false;
if (res.error) {
notifyError(res.error);
return;
}
page.link = generateLink(res.data.Hostname, res.data.Token);
notifySuccess(
t("components.user.calendars.share_free_time.gen_link_success")
);
}
function generateLink(hostname: string, token: string): string {
// let schema = location.href.substring(0, location.href.indexOf(location.host));
return (
// schema +
// location.host +
hostname + RouteSharedFreeTime.replace(TokenPlaceholder, token)
);
}
</script>
<style scoped>
.panel-content {
min-width: 700px;
max-width: 25%;
margin: auto;
}
</style>

View File

@ -0,0 +1,55 @@
<template>
<div>
<Rules
:get-get-url="getUrl"
:get-delete-url="getUrl"
:get-save-url="getUrl"
:get-move-url="moveUrl"
:get-enable-url="enableUrl"
:conditions="conditionsList"
:actions="actionsList"
>
<template #condition-value="valueProps">
<ConditionValue
:change="valueProps.change"
:value="valueProps.value"
:type="valueProps.type"
>
</ConditionValue>
</template>
<template #action-value="valueProps">
<ActionValue
:change="valueProps.change"
:value="valueProps.value"
:type="valueProps.type"
:get-folders-url="foldersUrl"
>
</ActionValue>
</template>
</Rules>
</div>
</template>
<script setup lang="ts">
import ConditionValue from "@/components/common/rules/incoming/Conditions.vue";
import ActionValue from "@/components/common/rules/incoming/Actions.vue";
import { conditionsList } from "@/components/common/rules/incoming/Conditions.vue";
import { actionsList } from "@/components/common/rules/incoming/Actions.vue";
import Rules from "@/components/common/rules/Rules.vue";
function getUrl(): string {
return `/user/incoming_rules`;
}
function moveUrl(): string {
return `/user/incoming_rules/move`;
}
function foldersUrl(): string {
return `/user/incoming_rules/folders`;
}
function enableUrl(): string {
return `/user/incoming_rules/enable`;
}
</script>

View File

@ -0,0 +1,345 @@
<template>
<div class="panel-content">
<a-table
:columns="columns"
:data-source="page.data"
:loading="page.loading"
bordered
size="small"
:pagination="false"
>
<template #title>
<a-row>
<a-col style="display: flex; align-items: flex-end" :span="18">
<a-space>
<a-radio-group @change="get()" v-model:value="page.type">
<a-radio-button value="email">{{
t("components.user.mailbox_shared_access.type_email")
}}</a-radio-button>
<a-radio-button value="group">{{
t("components.user.mailbox_shared_access.type_group")
}}</a-radio-button>
</a-radio-group>
<a-input-search
v-model:value="page.pagination.search"
:placeholder="t('components.user.mailbox_shared_access.search')"
enter-button
allow-clear
@search="get"
/>
</a-space>
</a-col>
<a-col
style="display: flex; align-items: flex-end; justify-content: end"
:span="6"
>
<a-space>
<a-button
v-if="page.type === 'email'"
type="primary"
@click="page.showAdd = true"
>
{{ t("components.user.mailbox_shared_access.type_email_add") }}
</a-button>
<a-button
v-if="page.type === 'group'"
type="primary"
@click="page.showAdd = true"
>
{{ t("components.user.mailbox_shared_access.type_group_add") }}
</a-button>
<a-popconfirm
v-if="page.withSearch"
:title="
t('components.user.mailbox_shared_access.delete_found') + '?'
"
:ok-text="t('components.user.mailbox_shared_access.ok')"
:cancel-text="t('components.user.mailbox_shared_access.cancel')"
@confirm="deleteAccess('')"
>
<a-button danger>{{
$t("components.user.mailbox_shared_access.delete_found")
}}</a-button>
</a-popconfirm>
<a-popconfirm
v-else
:title="
t('components.user.mailbox_shared_access.delete_all') + '?'
"
:ok-text="t('components.user.mailbox_shared_access.ok')"
:cancel-text="t('components.user.mailbox_shared_access.cancel')"
@confirm="deleteAccess('')"
>
<a-button danger>{{
$t("components.user.mailbox_shared_access.delete_all")
}}</a-button>
</a-popconfirm>
</a-space>
</a-col>
</a-row>
</template>
<template #bodyCell="{ column, record }">
<template v-if="column.key === 'action'">
<a-popconfirm
:title="t('components.user.mailbox_shared_access.delete') + '?'"
:ok-text="t('components.user.mailbox_shared_access.ok')"
:cancel-text="t('components.user.mailbox_shared_access.cancel')"
@confirm="deleteAccess(record.Name)"
>
<a-button danger>{{
t("components.user.mailbox_shared_access.delete")
}}</a-button>
</a-popconfirm>
</template>
</template>
<template #footer>
<a-flex justify="flex-end">
<a-pagination
v-model:value="page.pagination.current"
:defaultPageSize="page.pagination.size"
:total="page.pagination.total"
@change="pageChange"
:pageSizeOptions="['10', '20', '50', '100']"
v-model:pageSize="page.pagination.size"
:showSizeChanger="true"
/>
</a-flex>
</template>
</a-table>
</div>
<a-modal
style="width: 300px"
v-model:open="page.showAdd"
:title="
page.type === 'email'
? t('components.user.mailbox_shared_access.type_email_add')
: t('components.user.mailbox_shared_access.type_group_add')
"
>
<div style="margin-top: 16px">
<template v-if="page.type === 'email'">
<a-input
v-model:value="page.inputValue"
:placeholder="
t('components.user.mailbox_shared_access.type_email_placeholder')
"
>
</a-input>
</template>
<template v-if="page.type === 'group'">
<a-select
style="width: 100%"
:disabled="page.groups.length === 0"
v-model:value="page.selectValue"
:options="page.groups"
>
</a-select>
</template>
</div>
<template #footer>
<a-button
:disabled="
(page.type === 'email' && page.inputValue === '') ||
(page.type === 'group' && page.selectValue === '')
"
type="primary"
@click="add(page.type === 'email' ? page.inputValue : page.selectValue)"
>
{{ t("components.user.mailbox_shared_access.add") }}
</a-button>
</template>
</a-modal>
</template>
<script setup lang="ts">
import { apiFetch } from "@/composables/apiFetch";
import { onMounted, reactive } from "vue";
import { useI18n } from "vue-i18n";
import { notifyError } from "@/composables/alert";
import type { ColumnType } from "ant-design-vue/es/table";
const { t } = useI18n();
const columns: ColumnType<any>[] = [
{
title: t("components.user.mailbox_shared_access.col_name"),
dataIndex: "Name",
},
{
key: "action",
align: "right",
},
];
const page = reactive<{
inputValue: string;
selectValue: string;
type: string;
data: any[];
loading: boolean;
show: boolean;
Name: string;
groups: any[];
pagination: {
current: number;
total: number;
size: number;
search: string;
};
showAdd: boolean;
withSearch: boolean;
all_groups: string[];
}>({
inputValue: "",
selectValue: "",
type: "email",
data: [],
loading: false,
show: false,
Name: "",
groups: [],
pagination: {
current: 1,
total: 0,
size: 50,
search: "",
},
showAdd: false,
withSearch: false,
all_groups: [],
});
onMounted(() => {
getGroups();
get();
});
async function getGroups() {
const res = await apiFetch(`/user/mailbox_shared_access/groups`);
if (res.error) {
notifyError(res.error);
return;
}
page.all_groups = res.data;
return;
}
function pageChange(current: number) {
page.pagination.current = current;
return get();
}
async function get() {
page.withSearch = page.pagination.search !== "";
page.loading = true;
const params = new URLSearchParams();
params.append("type", page.type);
params.append("page", String(page.pagination.current));
params.append("size", String(page.pagination.size));
params.append("search", page.pagination.search);
const res = await apiFetch(
`/user/mailbox_shared_access?` + params.toString()
);
page.loading = false;
if (res.error) {
notifyError(res.error);
return;
}
page.data = [];
if (res.data) {
for (let index = 0; index < res.data.length; index++) {
const element = res.data[index];
page.data.push({ Name: element });
}
}
page.pagination.total = res.total;
page.groups = [];
page.selectValue = "";
if (page.type === "group") {
for (let j = 0; j < page.all_groups.length; j++) {
const group = page.all_groups[j];
let skip = false;
for (let i = 0; i < page.data.length; i++) {
const alreadyHas = page.data[i];
if (alreadyHas.Name === group) {
skip = true;
break;
}
}
if (!skip) {
page.groups.push({ value: group });
}
}
}
if (page.groups.length) {
page.selectValue = page.groups[0].value;
}
return;
}
async function deleteAccess(access: string) {
const params = new URLSearchParams();
params.append("search", page.pagination.search);
const res = await apiFetch(
`/user/mailbox_shared_access?` + params.toString(),
{
method: "DELETE",
body: {
AccessTo: access,
Type: page.type,
},
}
);
if (res.error) {
notifyError(res.error);
return;
}
return get();
}
async function add(access: string) {
const res = await apiFetch(`/user/mailbox_shared_access`, {
method: "POST",
body: {
AccessTo: access,
Type: page.type,
},
});
if (res.error) {
notifyError(res.error);
return;
}
page.showAdd = false;
return get();
}
</script>
<style scoped>
.panel-content {
min-width: 800px;
max-width: 60%;
margin: auto;
}
</style>

View File

@ -0,0 +1,277 @@
<template>
<div>
<a-row :gutter="[16, 16]" justify="space-around">
<a-col :xs="24" :sm="24" :md="24" :lg="12" :xl="12">
<a-card hoverable style="height: 100%">
<template #title>
{{ t("components.user.profile.mailbox.title") }}
</template>
<a-descriptions :column="1" :labelStyle="{ fontWeight: '600' }">
<a-descriptions-item
:label="t('components.user.profile.mailbox.user')"
>{{ page.mailboxBlock.User }}</a-descriptions-item
>
<a-descriptions-item
:label="t('components.user.profile.mailbox.size')"
>{{ page.mailboxBlock.Size }}</a-descriptions-item
>
<a-descriptions-item
:label="t('components.user.profile.mailbox.count')"
>{{ page.mailboxBlock.Count }}</a-descriptions-item
>
</a-descriptions>
</a-card>
</a-col>
<a-col :xs="24" :sm="24" :md="24" :lg="12" :xl="12">
<a-card hoverable style="height: 100%">
<template #title>
{{ t("components.user.profile.dav.title") }}
</template>
<a-descriptions :column="1" :labelStyle="{ fontWeight: '600' }">
<a-descriptions-item
:label="t('components.user.profile.dav.books')"
>
<a-space>
{{ page.davBlock.BooksUrl }}
<a-button
v-if="page.davBlock.BooksUrl.startsWith('http')"
@click="copyToClipboard(page.davBlock.BooksUrl)"
size="small"
>
<CopyOutlined />
</a-button>
</a-space>
</a-descriptions-item>
<a-descriptions-item
:label="t('components.user.profile.dav.calendars')"
>
<a-space>
{{ page.davBlock.CalsUrl }}
<a-button
v-if="page.davBlock.CalsUrl.startsWith('http')"
@click="copyToClipboard(page.davBlock.CalsUrl)"
size="small"
>
<CopyOutlined />
</a-button>
</a-space>
</a-descriptions-item>
<a-descriptions-item
:label="t('components.user.profile.dav.login')"
>
<a-space>
{{ page.davBlock.Login }}
<a-button
@click="copyToClipboard(page.davBlock.Login)"
size="small"
>
<CopyOutlined />
</a-button>
</a-space>
</a-descriptions-item>
</a-descriptions>
</a-card>
</a-col>
</a-row>
<br />
<a-row :gutter="[16, 16]" justify="space-around">
<a-col :xs="24" :sm="24" :md="24" :lg="12" :xl="12">
<a-card hoverable style="height: 100%">
<template #title>
{{ t("components.user.profile.imap.title") }}
</template>
<a-descriptions :column="1" :labelStyle="{ fontWeight: '600' }">
<a-descriptions-item
:label="t('components.user.profile.imap.server')"
>
<a-space>
{{ page.imapBlock.Server }}
<a-button
@click="copyToClipboard(page.imapBlock.Server)"
size="small"
>
<CopyOutlined />
</a-button>
</a-space>
</a-descriptions-item>
<a-descriptions-item
:label="t('components.user.profile.imap.port')"
>
<a-space>
{{ page.imapBlock.Port }}
<a-button
@click="copyToClipboard(page.imapBlock.Port.toString())"
size="small"
>
<CopyOutlined />
</a-button>
</a-space>
</a-descriptions-item>
<a-descriptions-item
:label="t('components.user.profile.imap.encryption')"
>{{ page.imapBlock.SSL }}</a-descriptions-item
>
<a-descriptions-item
:label="t('components.user.profile.imap.auth')"
>{{
t("components.user.profile.imap.auth_text")
}}</a-descriptions-item
>
<a-descriptions-item
:label="t('components.user.profile.imap.auth_login')"
>
<a-space>
{{ page.imapBlock.Login }}
<a-button
@click="copyToClipboard(page.imapBlock.Login)"
size="small"
>
<CopyOutlined />
</a-button>
</a-space>
</a-descriptions-item>
</a-descriptions>
</a-card>
</a-col>
<a-col :xs="24" :sm="24" :md="24" :lg="12" :xl="12">
<a-card hoverable style="height: 100%">
<template #title>
{{ t("components.user.profile.smtp.title") }}
</template>
<a-descriptions :column="1" :labelStyle="{ fontWeight: '600' }">
<a-descriptions-item
:label="t('components.user.profile.smtp.server')"
>
<a-space>
{{ page.smtpBlock.Server }}
<a-button
@click="copyToClipboard(page.smtpBlock.Server)"
size="small"
>
<CopyOutlined />
</a-button> </a-space
></a-descriptions-item>
<a-descriptions-item
:label="t('components.user.profile.smtp.port')"
>
<a-space>
{{ page.smtpBlock.Port }}
<a-button
@click="copyToClipboard(page.smtpBlock.Port.toString())"
size="small"
>
<CopyOutlined />
</a-button> </a-space
></a-descriptions-item>
<a-descriptions-item
:label="t('components.user.profile.smtp.encryption')"
>{{ page.smtpBlock.SSL }}</a-descriptions-item
>
<a-descriptions-item
:label="t('components.user.profile.smtp.auth')"
>{{
t("components.user.profile.smtp.auth_text")
}}</a-descriptions-item
>
<a-descriptions-item
:label="t('components.user.profile.smtp.auth_login')"
>
<a-space>
{{ page.smtpBlock.Login }}
<a-button
@click="copyToClipboard(page.smtpBlock.Login)"
size="small"
>
<CopyOutlined />
</a-button> </a-space
></a-descriptions-item>
</a-descriptions>
</a-card>
</a-col>
</a-row>
<br />
</div>
</template>
<script setup lang="ts">
import { apiFetch } from "@/composables/apiFetch";
import { onMounted, reactive } from "vue";
import { useI18n } from "vue-i18n";
import { copyToClipboard } from "@/composables/misc";
import { CopyOutlined } from "@ant-design/icons-vue";
const { t } = useI18n();
const page = reactive<{
contentLoaded: boolean;
mailboxBlock: {
Count: number;
Size: string;
User: string;
};
davBlock: {
BooksUrl: string;
CalsUrl: string;
Login: string;
};
imapBlock: {
Server: string;
Port: number;
SSL: string;
Login: string;
};
smtpBlock: {
Server: string;
Port: number;
SSL: string;
Login: string;
};
}>({
contentLoaded: false,
mailboxBlock: {
Count: 0,
Size: "",
User: "",
},
davBlock: {
BooksUrl: "",
CalsUrl: "",
Login: "",
},
imapBlock: {
Server: "",
Port: 0,
SSL: "",
Login: "",
},
smtpBlock: {
Server: "",
Port: 0,
SSL: "",
Login: "",
},
});
onMounted(() => {
get();
});
async function get() {
const res = await apiFetch("/user/profile");
if (res.error) {
return;
}
page.contentLoaded = true;
page.mailboxBlock = res.data.MailboxBlock;
page.davBlock = res.data.DavBlock;
page.smtpBlock = res.data.SmtpBlock;
page.imapBlock = res.data.ImapBlock;
}
</script>

View File

@ -0,0 +1,84 @@
<template>
<div>
<a-card class="panel-content">
<a-form :label-col="labelCol" :wrapper-col="wrapperCol" :labelWrap="true">
<a-form-item :label="t('components.user.recovery.show')">
<a-switch v-model:checked="page.showFolder"> </a-switch>
</a-form-item>
</a-form>
<a-button
:disabled="!page.contentLoaded"
class="save-button"
type="primary"
size="large"
@click="update"
>
{{ $t("components.user.recovery.save") }}
</a-button>
</a-card>
</div>
</template>
<script setup lang="ts">
import { notifyError, notifySuccess } from "@/composables/alert";
import { apiFetch } from "@/composables/apiFetch";
import { onMounted, reactive } from "vue";
import { useI18n } from "vue-i18n";
const { t } = useI18n();
const labelCol = { span: 16, style: { "text-align": "left" } };
const wrapperCol = { span: 8, style: { "text-align": "right" } };
const page = reactive<{
contentLoaded: boolean;
showFolder: boolean;
}>({
contentLoaded: false,
showFolder: false,
});
onMounted(() => {
get();
});
async function get() {
const res = await apiFetch("/user/recovery_folder");
if (res.error) {
notifyError(res.error);
return;
}
page.contentLoaded = true;
page.showFolder = res.data;
}
async function update() {
const res = await apiFetch("/user/recovery_folder", {
method: "POST",
body: {
Show: page.showFolder,
},
});
if (res.error) {
notifyError(res.error);
return;
}
notifySuccess(t("components.user.recovery.success"));
}
</script>
<style scoped>
.panel-content {
min-width: 500px;
max-width: 25%;
margin: auto;
}
.save-button {
width: 30%;
min-width: 200px;
display: block;
margin: auto;
}
</style>

View File

@ -0,0 +1,23 @@
<template>
<SharedFolders v-if="page.loaded" :user="page.user"></SharedFolders>
</template>
<script setup lang="ts">
import SharedFolders from "@/components/common/SharedFolders.vue";
import { onMounted, reactive } from "vue";
import { useRoute } from "vue-router";
import { useAuthStore } from "@/stores/auth";
const route = useRoute();
const page = reactive<{
user: string;
loaded: boolean;
}>({
user: "",
loaded: false,
});
onMounted(() => {
page.user = useAuthStore().username;
page.loaded = true;
});
</script>

View File

@ -0,0 +1,27 @@
import { notification } from "ant-design-vue";
import i18n from "@/locale";
notification.config({
maxCount: 1,
});
export function notifyError(msg: string) {
notification["error"]({
message: i18n.global.t("common.notify.error"),
description: msg,
});
}
export function notifySuccess(msg: string) {
notification["success"]({
message: i18n.global.t("common.notify.success"),
description: msg,
});
}
export function notifyWarning(msg: string) {
notification["warning"]({
message: i18n.global.t("common.notify.warning"),
description: msg,
});
}

View File

@ -0,0 +1,47 @@
import { useAuthStore } from "@/stores/auth";
import router from "@/router";
import { RouteLogin } from "@/router/consts";
import { useReturnToPageStore } from "@/stores/returnToPage";
import { useRoute } from "vue-router";
export const apiFetch = async (request: any, opts?: any): Promise<Response> => {
if (opts && opts.body && !opts.isFormData) {
opts.body = JSON.stringify(opts.body);
}
const headers = new Headers();
//headers.set("Authorization", "Bearer " + useAuthStore().token);
if (opts && !opts.isFormData) {
headers.set("Content-Type", "application/json");
}
const resp = await fetch(
import.meta.env.VITE_API_URL + "/backend" + request,
{
headers,
credentials: "include",
...opts,
}
);
const { result, total, error } = await resp.json();
if (resp.status != 200) {
if (resp.status == 401) {
useAuthStore().resetAuth();
useReturnToPageStore().setPath(
router.currentRoute.value.path,
useRoute().query
);
router.push(RouteLogin);
}
}
return { data: result, total, error };
};
export interface Response {
data: any;
total: number;
error: string;
}

View File

@ -0,0 +1,79 @@
import { message } from "ant-design-vue";
import i18n from "@/locale";
export const timeToDateTime = (time: any) => {
if (time == undefined) {
return;
}
var d = new Date(time);
return (
("0" + d.getHours()).slice(-2) +
":" +
("0" + d.getMinutes()).slice(-2) +
" " +
("0" + d.getDate()).slice(-2) +
"/" +
("0" + (d.getMonth() + 1)).slice(-2) +
"/" +
d.getFullYear()
);
};
export const timeToDate = (time: any) => {
if (time == undefined) {
return;
}
var d = new Date(time);
return (
("0" + d.getDate()).slice(-2) + "/" + ("0" + (d.getMonth() + 1)).slice(-2)
);
};
export const timeToFullDate = (time: any) => {
if (time == undefined) {
return;
}
var d = new Date(time);
return (
("0" + d.getDate()).slice(-2) +
"." +
("0" + (d.getMonth() + 1)).slice(-2) +
"." +
d.getFullYear()
);
};
export const copyToClipboard = (text: string) => {
navigator.clipboard.writeText(text);
message.info(i18n.global.t("common.misc.copy"));
};
export const genRandomString = (length: number) => {
let result = "";
const characters = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz";
const charactersLength = characters.length;
let counter = 0;
while (counter < length) {
result += characters.charAt(Math.floor(Math.random() * charactersLength));
counter += 1;
}
return result;
};
export const getTimezoneString = () => {
let offset = new Date().getTimezoneOffset();
offset = offset / -60;
let resStr = "UTC";
if (offset < 0) {
resStr += "-";
offset *= -1;
} else {
resStr += "+";
}
if (offset < 10) {
resStr += "0";
}
resStr += offset;
resStr += ":00";
return resStr;
};

View File

@ -0,0 +1,31 @@
import { ExclamationCircleOutlined } from "@ant-design/icons-vue";
import { Modal } from "ant-design-vue";
import { createVNode } from "vue";
import router from "@/router";
import { RouteLogin } from "@/router/consts";
import i18n from "@/locale";
import { apiFetch } from "./apiFetch";
import { notifyError } from "./alert";
export function saveAndRestart(callback: () => Promise<boolean>) {
Modal.confirm({
title: i18n.global.t("common.save_and_restart.confirm_title"),
icon: createVNode(ExclamationCircleOutlined),
content: i18n.global.t("common.save_and_restart.confirm_content"),
okText: i18n.global.t("common.save_and_restart.ok"),
cancelText: i18n.global.t("common.save_and_restart.cancel"),
async onOk() {
if (await callback()) {
const res = await apiFetch("/admin/restart", {
method: "POST",
});
if (res.error) {
notifyError(res.error);
return;
}
router.push(RouteLogin);
}
},
});
}

View File

@ -0,0 +1,32 @@
import { createI18n } from "vue-i18n";
import { translations } from "./translations";
const defaultLocale = "eng";
export function getLocale() {
const locale = localStorage.getItem("locale");
if (!locale) {
return defaultLocale;
}
return locale;
}
export function setLocale(locale: any) {
localStorage.setItem("locale", locale);
i18n.global.locale.value = locale;
}
var locale = localStorage.getItem("locale");
if (!locale) {
locale = defaultLocale;
localStorage.setItem("locale", locale);
}
const i18n = createI18n({
legacy: false,
locale: locale,
fallbackLocale: defaultLocale,
messages: translations,
});
export default i18n;

File diff suppressed because it is too large Load Diff

View File

@ -1,5 +1,49 @@
import {createApp} from 'vue' import "./assets/main.css";
import App from './App.vue'
import './style.css';
createApp(App).mount('#app') import { createApp, nextTick } from "vue";
import { createPinia } from "pinia";
import i18n from "@/locale";
import piniaPluginPersistedState from "pinia-plugin-persistedstate";
import App from "./App.vue";
import router from "@/router";
const app = createApp(App);
const pinia = createPinia();
pinia.use(piniaPluginPersistedState);
app.use(pinia);
app.use(i18n);
app.use(router);
app.use({
install(Vue) {
Vue.mixin({
mounted() {
this.disableAutoComplete();
},
methods: {
disableAutoComplete() {
let elements = document.querySelectorAll('[autocomplete="off"]');
if (!elements) {
return;
}
elements.forEach((element) => {
element.setAttribute("readonly", "readonly");
nextTick(() => {
setTimeout(() => {
element.removeAttribute("readonly");
}, 100);
});
});
},
},
});
},
});
app.mount("#app");

View File

@ -0,0 +1,98 @@
export const RouteLogin = "/";
export const RouteShared = "/sh";
export const TokenPlaceholder = ":token";
export const RouteSharedFreeTime = `/sh/ft/${TokenPlaceholder}`;
export const RouteEventsExternalApproval = `/sh/ev/ap/${TokenPlaceholder}`;
export const RouteAdmin = "/admin";
export const RouteAdminDashboard = "/admin/dashboard";
export const RouteAdminSettings = "/admin/settings";
export const RouteAdminSettingsMain = "/admin/settings/main";
export const RouteAdminSettingsSMTPQueue = "/admin/settings/smtp_queue";
export const RouteAdminSettingsSMTPQueueSettings =
"/admin/settings/smtp_queue/settings";
export const RouteAdminSettingsSMTPQueueManage =
"/admin/settings/smtp_queue/manage";
export const RouteAdminSettingsSettingsDB = "/admin/settings/settings_db";
export const RouteAdminSettingsAddressChange = "/admin/settings/address_change";
export const RouteAdminSettingsCalendars = "/admin/settings/calendars";
export const RouteAdminSettingsLicense = "/admin/settings/license";
export const RouteAdminSecurity = "/admin/security";
export const RouteAdminSecurityBlockedIps = "/admin/security/blocked_ips";
export const RouteAdminSecurityWhiteList = "/admin/security/white_list";
export const RouteAdminSecurityWhiteListIp = "/admin/security/white_list/ip";
export const RouteAdminSecurityWhiteListEmail =
"/admin/security/white_list/email";
export const RouteAdminSecurityBlackList = "/admin/security/black_list";
export const RouteAdminSecurityBlackListIp = "/admin/security/black_list/ip";
export const RouteAdminSecurityBlackListEmail =
"/admin/security/black_list/email";
export const RouteAdminDomains = "/admin/domains";
export const DomainPlaceholder = ":domain";
export const UserdbPlaceholder = ":sid";
export const RouteAdminDomainsAdd = `/admin/domains/add`;
export const RouteAdminDomainsDomain = `/admin/domains/${DomainPlaceholder}`;
export const RouteAdminDomainsDomainMailStorage = `/admin/domains/${DomainPlaceholder}/mailstorage`;
export const RouteAdminDomainsDomainMailStorageMailboxes = `/admin/domains/${DomainPlaceholder}/mailstorage/mailboxes`;
export const MailboxPlaceholder = ":mailbox";
export const RouteAdminDomainsDomainMailStorageMailboxesExport = `/admin/domains/${DomainPlaceholder}/mailstorage/mailboxes/${MailboxPlaceholder}/export`;
export const RouteAdminDomainsDomainMailStorageSettings = `/admin/domains/${DomainPlaceholder}/mailstorage/settings`;
export const RouteAdminDomainsDomainUserDB = `/admin/domains/${DomainPlaceholder}/userdb`;
export const RouteAdminDomainsDomainUserDBAdd = `/admin/domains/${DomainPlaceholder}/userdb/add`;
export const RouteAdminDomainsDomainUserDBProvider = `/admin/domains/${DomainPlaceholder}/userdb/${UserdbPlaceholder}`;
export const RouteAdminDomainsDomainUserDBProviderUsers = `/admin/domains/${DomainPlaceholder}/userdb/${UserdbPlaceholder}/users`;
export const RouteAdminDomainsDomainUserDBProviderGroups = `/admin/domains/${DomainPlaceholder}/userdb/${UserdbPlaceholder}/groups`;
export const RouteAdminDomainsDomainUserDBProviderRedirects = `/admin/domains/${DomainPlaceholder}/userdb/${UserdbPlaceholder}/redirects`;
export const RouteAdminDomainsDomainUserDBProviderSettings = `/admin/domains/${DomainPlaceholder}/userdb/${UserdbPlaceholder}/settings`;
export const RouteAdminDomainsDomainUserDBProviderOther = `/admin/domains/${DomainPlaceholder}/userdb/${UserdbPlaceholder}/other`;
export const RouteAdminDomainsDomainSharedFolders = `/admin/domains/${DomainPlaceholder}/shared_folders`;
export const RouteAdminDomainsDomainAddressBooks = `/admin/domains/${DomainPlaceholder}/address_books`;
export const RouteAdminDomainsDomainCalendars = `/admin/domains/${DomainPlaceholder}/calendars`;
export const RouteAdminDomainsDomainResources = `/admin/domains/${DomainPlaceholder}/resources`;
export const RouteAdminDomainsDomainResourcesList = `/admin/domains/${DomainPlaceholder}/resources/list`;
export const RouteAdminDomainsDomainResourcesOffices = `/admin/domains/${DomainPlaceholder}/resources/offices`;
export const RouteAdminDomainsDomainResourcesCategories = `/admin/domains/${DomainPlaceholder}/resources/categories`;
export const RouteAdminDomainsDomainIncRules = `/admin/domains/${DomainPlaceholder}/incoming_rules`;
export const RouteAdminDomainsDomainOutRules = `/admin/domains/${DomainPlaceholder}/outgoing_rules`;
export const RouteAdminDomainsDomainDKIM = `/admin/domains/${DomainPlaceholder}/dkim`;
export const RouteAdminDomainsDomainMigration = `/admin/domains/${DomainPlaceholder}/migration`;
export const RouteAdminDomainsDomainCIDRAccess = `/admin/domains/${DomainPlaceholder}/cidr_access`;
export const RouteAdminDomainsDomainCIDRAccessPools = `/admin/domains/${DomainPlaceholder}/cidr_access/pools`;
export const RouteAdminDomainsDomainCIDRAccessPolicy = `/admin/domains/${DomainPlaceholder}/cidr_access/policy`;
export const RouteAdminDomainsDomainOther = `/admin/domains/${DomainPlaceholder}/other`;
export const RouteAdminDomainsDomainOtherMailsPermDeletion = `/admin/domains/${DomainPlaceholder}/other/mails_perm_deletion`;
export const RouteUser = "/user";
export const RouteUserProfile = "/user/profile";
export const RouteUserRules = "/user/rules";
export const RouteUserRecovery = "/user/recovery";
export const RouteUserMailboxSharedAccess = "/user/mailbox_shared_access";
export const RouteUserSharedFolders = "/user/shared_folders";
export const RouteUserAddressBooks = "/user/address_books";
export const RouteUserAddressBooksMy = "/user/address_books/my_books";
export const RouteUserAddressBooksAvaliableToMe =
"/user/address_books/avaliable_to_me";
export const RouteUserCalendars = "/user/calendars";
export const RouteUserCalendarsMy = "/user/calendars/my_calendars";
export const RouteUserCalendarsAvaliableToMe =
"/user/calendars/avaliable_to_me";
export const RouteUserCalendarsShareFreeTime =
"/user/calendars/share_free_time";
export const RouteUserCalendarsEventsPlanner = "/user/calendars/events_planner";
export const RouteUserCalendarsEventsPlannerList =
"/user/calendars/events_planner/my_events";
export const RouteUserCalendarsEventsPlannerNew =
"/user/calendars/events_planner/new";
export const EventIdPlaceholder = ":event_id";
export const RouteUserCalendarsEventsPlannerEdit = `/user/calendars/events_planner/${EventIdPlaceholder}/edit`;

View File

@ -0,0 +1,105 @@
import {
createRouter,
createWebHistory,
type RouteRecordRaw,
} from "vue-router";
import { pipeline } from "@/router/middleware/pipeline";
import { useAuthStore } from "@/stores/auth";
import {
RouteAdmin,
RouteAdminDashboard,
RouteLogin,
RouteShared,
RouteUser,
RouteUserProfile,
} from "./consts";
import routes from "@/router/routes";
import { nextTick } from "vue";
import i18n from "@/locale";
import { useReturnToPageStore } from "@/stores/returnToPage";
const router = createRouter({
history: createWebHistory(import.meta.env.BASE_URL),
routes: routes as RouteRecordRaw[],
});
router.beforeEach((to, from, next) => {
let path = to.path;
if (!to) {
path = location.pathname;
}
if (!path.startsWith(RouteShared)) {
if (to.name !== RouteLogin) {
if (!useAuthStore().authed) {
useReturnToPageStore().setPath(path, to.query);
return next({ name: RouteLogin });
}
if (useAuthStore().isAdmin && !to.path.startsWith(RouteAdmin)) {
return next({ name: RouteAdminDashboard });
}
if (!useAuthStore().isAdmin && !to.path.startsWith(RouteUser)) {
return next({ name: RouteUserProfile });
}
}
}
if (!to.meta.middleware) {
return next();
}
const middleware = Array.isArray(to.meta.middleware)
? to.meta.middleware
: [to.meta.middleware];
const context = {
to,
from,
next,
};
return middleware[0](pipeline(context, middleware, 1), context);
});
router.afterEach((to, from) => {
// Use next tick to handle router history correctly
// see: https://github.com/vuejs/vue-router/issues/914#issuecomment-384477609
nextTick(() => {
let title = to.meta.title as string;
if (!title) {
if (to.path.startsWith("/admin")) {
title = i18n.global.t("admin.title");
} else if (to.path.startsWith("/user")) {
if (useAuthStore().username) {
title = useAuthStore().username;
} else {
title = i18n.global.t("user.title");
}
}
}
if (title) {
title = title + " - " + i18n.global.t("tegu");
}
document.title = title || "Tegu";
});
if (to.hash === "#refresh") {
router.replace({ hash: "" }).then(() => {
router.go(0);
});
return;
}
if (to.query.redirect) {
let redirect = to.query.redirect as string;
let parts = redirect.split("?");
router.push(parts[0]);
useReturnToPageStore().setPath(
parts[0],
Object.fromEntries(new URLSearchParams(parts[1]))
);
}
});
export default router;

Some files were not shown because too many files have changed in this diff Show More