Merge branch 'develop'
This commit is contained in:
commit
2bd1e9ac9f
@ -10,28 +10,4 @@ body > header {
|
||||
font-size: 18px;
|
||||
line-height: 18px;
|
||||
}
|
||||
|
||||
nav {
|
||||
ul {
|
||||
li {
|
||||
a, button {
|
||||
.tip {
|
||||
position: static;
|
||||
display: block;
|
||||
visibility: visible;
|
||||
height: auto;
|
||||
padding: 0 0 0 4px;
|
||||
transform: none;
|
||||
color: inherit;
|
||||
|
||||
opacity: 1;
|
||||
background: transparent;
|
||||
text-transform: uppercase;
|
||||
font-size: 16px;
|
||||
line-height: 16px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -156,10 +156,22 @@ body > header {
|
||||
}
|
||||
|
||||
.tip {
|
||||
position: static;
|
||||
visibility: visible;
|
||||
opacity: 1;
|
||||
display: block;
|
||||
height: auto;
|
||||
margin-left: 8px;
|
||||
text-transform: inherit;
|
||||
padding: 0 0 0 4px;
|
||||
transform: none;
|
||||
|
||||
font-size: 16px;
|
||||
line-height: 16px;
|
||||
|
||||
color: inherit;
|
||||
text-transform: uppercase;
|
||||
font-weight: inherit;
|
||||
background: transparent;
|
||||
}
|
||||
}
|
||||
|
||||
@ -251,10 +263,6 @@ body > header {
|
||||
margin-top: 0;
|
||||
margin-left: 8px;
|
||||
|
||||
a, button, .button {
|
||||
@include tip;
|
||||
}
|
||||
|
||||
&:last-child {
|
||||
a, button, .button {
|
||||
.tip {
|
||||
|
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "rainbox.email",
|
||||
"version": "2.3.0",
|
||||
"version": "2.4.0",
|
||||
"description": "ISP mail provider manager with mysql and integrated LDAP server",
|
||||
"repository": "https://gitlab.com/ArisuOngaku/rainbox.email",
|
||||
"author": "Alice Gaudon <alice@gaudon.pro>",
|
||||
@ -20,7 +20,6 @@
|
||||
"@babel/core": "^7.9.0",
|
||||
"@babel/preset-env": "^7.9.5",
|
||||
"@fortawesome/fontawesome-free": "^5.14.0",
|
||||
"@types/argon2": "^0.15.0",
|
||||
"@types/config": "^0.0.38",
|
||||
"@types/express": "^4.17.6",
|
||||
"@types/express-session": "^1.17.0",
|
||||
@ -36,7 +35,7 @@
|
||||
"@typescript-eslint/eslint-plugin": "^4.3.0",
|
||||
"@typescript-eslint/parser": "^4.3.0",
|
||||
"babel-loader": "^8.1.0",
|
||||
"concurrently": "^5.1.0",
|
||||
"concurrently": "^6.0.0",
|
||||
"css-loader": "^5.0.0",
|
||||
"eslint": "^7.10.0",
|
||||
"feather-icons": "^4.28.0",
|
||||
@ -52,7 +51,7 @@
|
||||
"mini-css-extract-plugin": "^1.2.1",
|
||||
"node-sass": "^5.0.0",
|
||||
"nodemon": "^2.0.3",
|
||||
"sass-loader": "^10.0.1",
|
||||
"sass-loader": "^11.0.1",
|
||||
"terser-webpack-plugin": "^5.0.3",
|
||||
"ts-jest": "^26.1.1",
|
||||
"ts-loader": "^8.0.4",
|
||||
|
@ -3,7 +3,7 @@ import {Request, Response} from "express";
|
||||
import User from "swaf/auth/models/User";
|
||||
import {WhereTest} from "swaf/db/ModelQuery";
|
||||
import UserMailIdentityComponent from "../../models/UserMailIdentityComponent";
|
||||
import {NotFoundHttpError} from "swaf/HttpError";
|
||||
import {NotFoundHttpError, ServerError} from "swaf/HttpError";
|
||||
import MailDomain from "../../models/MailDomain";
|
||||
import BackendController from "swaf/helpers/BackendController";
|
||||
import MailIdentity from "../../models/MailIdentity";
|
||||
@ -13,6 +13,11 @@ import UserNameComponent from "swaf/auth/models/UserNameComponent";
|
||||
export default class MailboxBackendController extends Controller {
|
||||
public constructor() {
|
||||
super();
|
||||
BackendController.registerMenuElement({
|
||||
getLink: async () => Controller.route('backend-mail-domains'),
|
||||
getDisplayString: async () => 'Mail domains',
|
||||
getDisplayIcon: async () => 'globe',
|
||||
});
|
||||
BackendController.registerMenuElement({
|
||||
getLink: async () => Controller.route('backend-mailboxes'),
|
||||
getDisplayString: async () => 'Mailboxes',
|
||||
@ -26,23 +31,20 @@ export default class MailboxBackendController extends Controller {
|
||||
|
||||
public routes(): void {
|
||||
this.get('/', this.getMailboxesBackend, 'backend-mailboxes', RequireAuthMiddleware, RequireAdminMiddleware);
|
||||
this.get('/:id', this.getMailboxBackend, 'backend-mailbox', RequireAuthMiddleware, RequireAdminMiddleware);
|
||||
this.get('/mailbox/:id', this.getMailboxBackend, 'backend-mailbox', RequireAuthMiddleware, RequireAdminMiddleware);
|
||||
|
||||
this.get('/domains', this.getDomainsBackend, 'backend-mail-domains', RequireAuthMiddleware, RequireAdminMiddleware);
|
||||
this.post('/add-domain', this.postAddDomain, 'backend-add-domain', RequireAuthMiddleware, RequireAdminMiddleware);
|
||||
this.get('/edit-domain/:id', this.getEditDomain, 'backend-edit-domain', RequireAuthMiddleware, RequireAdminMiddleware);
|
||||
this.post('/edit-domain/:id', this.postEditDomain, 'backend-edit-domain', RequireAuthMiddleware, RequireAdminMiddleware);
|
||||
this.post('/remove-domain', this.postRemoveDomain, 'backend-remove-domain', RequireAuthMiddleware, RequireAdminMiddleware);
|
||||
|
||||
this.post('/:id/create-mail-identity', this.postCreateMailIdentity, 'backend-create-mail-identity', RequireAuthMiddleware, RequireAdminMiddleware);
|
||||
this.post('/set-main-mail-identity', this.postSetMainMailIdentity, 'backend-set-main-mail-identity', RequireAuthMiddleware, RequireAdminMiddleware);
|
||||
this.post('/delete-mail-identity', this.postDeleteMailIdentity, 'backend-delete-mail-identity', RequireAuthMiddleware, RequireAdminMiddleware);
|
||||
}
|
||||
|
||||
protected async getMailboxesBackend(req: Request, res: Response): Promise<void> {
|
||||
const mailDomains = await MailDomain.select()
|
||||
.with('owner')
|
||||
.with('identities')
|
||||
.get();
|
||||
|
||||
const users = await User.select()
|
||||
.where('main_mail_identity_id', null, WhereTest.NE)
|
||||
.with('mainMailIdentity')
|
||||
@ -50,12 +52,6 @@ export default class MailboxBackendController extends Controller {
|
||||
.get();
|
||||
|
||||
res.render('backend/mailboxes', {
|
||||
domains: await Promise.all(mailDomains.map(async domain => ({
|
||||
id: domain.id,
|
||||
name: domain.name,
|
||||
owner_name: (await domain.owner.get())?.as(UserNameComponent).name,
|
||||
identity_count: (await domain.identities.get()).length,
|
||||
}))),
|
||||
users: [{
|
||||
value: 0,
|
||||
display: 'Public',
|
||||
@ -88,6 +84,7 @@ export default class MailboxBackendController extends Controller {
|
||||
id: user.id,
|
||||
userName: user.as(UserNameComponent).name,
|
||||
name: await mainMailIdentity?.toEmail() || 'Not created.',
|
||||
exists: !!mainMailIdentity,
|
||||
},
|
||||
domains: mailDomains.map(d => ({
|
||||
display: d.name,
|
||||
@ -100,11 +97,27 @@ export default class MailboxBackendController extends Controller {
|
||||
});
|
||||
}
|
||||
|
||||
protected async getDomainsBackend(req: Request, res: Response): Promise<void> {
|
||||
const mailDomains = await MailDomain.select()
|
||||
.with('owner')
|
||||
.with('identities')
|
||||
.get();
|
||||
|
||||
res.render('backend/mail_domains', {
|
||||
domains: await Promise.all(mailDomains.map(async domain => ({
|
||||
id: domain.id,
|
||||
name: domain.name,
|
||||
owner_name: (await domain.owner.get())?.as(UserNameComponent).name,
|
||||
identity_count: (await domain.identities.get()).length,
|
||||
}))),
|
||||
});
|
||||
}
|
||||
|
||||
protected async postAddDomain(req: Request, res: Response): Promise<void> {
|
||||
const domain = MailDomain.create(req.body);
|
||||
await domain.save();
|
||||
req.flash('success', `Domain ${domain.name} successfully added with owner ${(await domain.owner.get())?.name}`);
|
||||
res.redirect(Controller.route('backend-mailboxes'));
|
||||
res.redirect(Controller.route('backend-edit-domain', domain.id));
|
||||
}
|
||||
|
||||
protected async getEditDomain(req: Request, res: Response): Promise<void> {
|
||||
@ -133,7 +146,7 @@ export default class MailboxBackendController extends Controller {
|
||||
await domain.save();
|
||||
|
||||
req.flash('success', `Domain ${domain.name} updated successfully.`);
|
||||
res.redirect(Controller.route('backend-mailboxes'));
|
||||
res.redirect(Controller.route('backend-edit-domain', domain.id));
|
||||
}
|
||||
|
||||
protected async postRemoveDomain(req: Request, res: Response): Promise<void> {
|
||||
@ -146,7 +159,7 @@ export default class MailboxBackendController extends Controller {
|
||||
// Don't delete that domain if it still has identities
|
||||
if ((await domain.identities.get()).length > 0) {
|
||||
req.flash('error', `This domain still has identities. Please remove all of these first (don't forget to rename mailboxes).`);
|
||||
res.redirect(Controller.route('backend-mailboxes'));
|
||||
res.redirect(Controller.route('backend-edit-domain', domain.id));
|
||||
return;
|
||||
}
|
||||
|
||||
@ -184,7 +197,7 @@ export default class MailboxBackendController extends Controller {
|
||||
}
|
||||
|
||||
req.flash('success', 'Mail identity ' + await identity.toEmail() + ' successfully created.');
|
||||
res.redirect(Controller.route('backend-mailbox', req.params.id));
|
||||
res.redirect(Controller.route('backend-mailbox', user.id));
|
||||
}
|
||||
|
||||
protected async postDeleteMailIdentity(req: Request, res: Response): Promise<void> {
|
||||
@ -194,15 +207,38 @@ export default class MailboxBackendController extends Controller {
|
||||
.first();
|
||||
if (!identity) throw new NotFoundHttpError('Mail identity', req.url);
|
||||
|
||||
const user = await identity.user.get();
|
||||
if (user?.as(UserMailIdentityComponent).main_mail_identity_id === identity.id) {
|
||||
const user = identity.user.getOrFail();
|
||||
if (!user) throw new NotFoundHttpError('Mail identity owner', req.url);
|
||||
if (user.as(UserMailIdentityComponent).main_mail_identity_id === identity.id) {
|
||||
req.flash('error', `Cannot delete this user's mailbox identity.`);
|
||||
res.redirect(Controller.route('backend-mailbox', req.params.id));
|
||||
res.redirect(Controller.route('backend-mailbox', user.id));
|
||||
return;
|
||||
}
|
||||
|
||||
await identity.delete();
|
||||
req.flash('success', 'Identity ' + await identity.toEmail() + ' successfully deleted.');
|
||||
res.redirect(Controller.route('backend-mailbox', req.params.id));
|
||||
res.redirect(Controller.route('backend-mailbox', user.id));
|
||||
}
|
||||
|
||||
protected async postSetMainMailIdentity(req: Request, res: Response): Promise<void> {
|
||||
const identity = await MailIdentity.select()
|
||||
.where('id', req.body.id)
|
||||
.with('user.mainMailIdentity')
|
||||
.first();
|
||||
if (!identity) throw new NotFoundHttpError('Mail identity', req.url);
|
||||
|
||||
const user = await identity.user.getOrFail();
|
||||
if (!user) throw new NotFoundHttpError('Mail identity owner', req.url);
|
||||
|
||||
const mailIdentityComponent = user.as(UserMailIdentityComponent);
|
||||
const mainMailIdentity = mailIdentityComponent.mainMailIdentity.getOrFail();
|
||||
if (!mainMailIdentity) throw new ServerError('Could not find this users main mail identity.');
|
||||
|
||||
mailIdentityComponent.main_mail_identity_id = identity.id;
|
||||
await user.save();
|
||||
|
||||
req.flash('success', 'User ' + user.id + ' main mail identity set to ' + await identity.toEmail());
|
||||
req.flash('warning', 'Please rename user\'s mailbox folder to correspond to changing from ' + await mainMailIdentity.toEmail() + ' to ' + await identity.toEmail());
|
||||
res.redirect(Controller.route('backend-mailbox', user.id));
|
||||
}
|
||||
}
|
||||
|
@ -4,9 +4,9 @@
|
||||
|
||||
{% block body %}
|
||||
<div class="container">
|
||||
{{ macros.breadcrumb('Mail domain: ' + domain.name, [
|
||||
{{ macros.breadcrumb(domain.name, [
|
||||
{title: 'Backend', link: route('backend')},
|
||||
{title: 'Mailboxes', link: route('backend-mailboxes')}
|
||||
{title: 'Mail domains', link: route('backend-mail-domains')}
|
||||
]) }}
|
||||
|
||||
<div class="panel">
|
||||
|
67
views/backend/mail_domains.njk
Normal file
67
views/backend/mail_domains.njk
Normal file
@ -0,0 +1,67 @@
|
||||
{% extends 'layouts/base.njk' %}
|
||||
|
||||
{% set title = app.name + ' - Backend' %}
|
||||
|
||||
{% block body %}
|
||||
<div class="container">
|
||||
{{ macros.breadcrumb('Mail domains', [
|
||||
{title: 'Backend', link: route('backend')}
|
||||
]) }}
|
||||
|
||||
<h1>Domain manager</h1>
|
||||
|
||||
<section class="panel">
|
||||
<h2><i data-feather="globe"></i> Domains</h2>
|
||||
|
||||
<form action="{{ route('backend-add-domain') }}" method="POST" class="sub-panel">
|
||||
<h3>Add domain</h3>
|
||||
{{ macros.field(_locals, 'text', 'name', null, 'Domain name', null, 'required') }}
|
||||
|
||||
{{ macros.field(_locals, 'select', 'user_id', undefined, 'Owner', null, 'required', users) }}
|
||||
|
||||
<button><i data-feather="plus"></i> Add domain</button>
|
||||
|
||||
{{ macros.csrf(getCsrfToken) }}
|
||||
</form>
|
||||
|
||||
<table class="data-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>#</th>
|
||||
<th>Name</th>
|
||||
<th>Owner</th>
|
||||
<th>Identities</th>
|
||||
<th>Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
|
||||
<tbody>
|
||||
{% for domain in domains %}
|
||||
<tr>
|
||||
<td>{{ domain.id }}</td>
|
||||
<td>{{ domain.name }}</td>
|
||||
<td>{{ domain.owner_name | default('Public') }}</td>
|
||||
<td>{{ domain.identity_count }}</td>
|
||||
<td class="actions">
|
||||
<a href="{{ route('backend-edit-domain', domain.id) }}" class="button">
|
||||
<i data-feather="edit-2"></i> <span class="tip">Edit</span>
|
||||
</a>
|
||||
|
||||
<form action="{{ route('backend-remove-domain') }}" method="POST">
|
||||
<input type="hidden" name="id" value="{{ domain.id }}">
|
||||
|
||||
<button class="danger"
|
||||
onclick="return confirm('Are you sure you want to delete {{ domain.name }}?')">
|
||||
<i data-feather="trash"></i> <span class="tip">Remove</span>
|
||||
</button>
|
||||
|
||||
{{ macros.csrf(getCsrfToken) }}
|
||||
</form>
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</section>
|
||||
</div>
|
||||
{% endblock %}
|
@ -28,16 +28,29 @@
|
||||
<td>{{ identity.id }}</td>
|
||||
<td>{{ identity.email }}</td>
|
||||
<td class="actions">
|
||||
<form action="{{ route('backend-delete-mail-identity') }}" method="POST">
|
||||
<input type="hidden" name="id" value="{{ identity.id }}">
|
||||
{% if mailbox.name != identity.email %}
|
||||
<form action="{{ route('backend-set-main-mail-identity') }}" method="POST">
|
||||
<input type="hidden" name="id" value="{{ identity.id }}">
|
||||
|
||||
<button class="danger"
|
||||
onclick="return confirm('Are you sure you want to delete {{ identity.email }}?')">
|
||||
<i data-feather="trash"></i> <span class="tip">Delete</span>
|
||||
</button>
|
||||
<button class=""
|
||||
onclick="return confirm('Are you sure you want to set {{ identity.email }} as this mailbox\'s identity? This requires moving existing emails on the mail server afterwards.')">
|
||||
<i data-feather="tag"></i> <span class="tip">Set as main identity</span>
|
||||
</button>
|
||||
|
||||
{{ macros.csrf(getCsrfToken) }}
|
||||
</form>
|
||||
{{ macros.csrf(getCsrfToken) }}
|
||||
</form>
|
||||
|
||||
<form action="{{ route('backend-delete-mail-identity') }}" method="POST">
|
||||
<input type="hidden" name="id" value="{{ identity.id }}">
|
||||
|
||||
<button class="danger"
|
||||
onclick="return confirm('Are you sure you want to delete {{ identity.email }}?')">
|
||||
<i data-feather="trash"></i> <span class="tip">Delete</span>
|
||||
</button>
|
||||
|
||||
{{ macros.csrf(getCsrfToken) }}
|
||||
</form>
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
@ -45,7 +58,7 @@
|
||||
</table>
|
||||
|
||||
<form action="{{ route('backend-create-mail-identity', mailbox.id) }}" method="POST" class="sub-panel">
|
||||
<h3>{% if mailboxIdentity == null %}Create a mailbox{% else %}Create a new mail identity{% endif %}</h3>
|
||||
<h3>{% if not mailbox.exists %}Create a mailbox{% else %}Create a new mail identity{% endif %}</h3>
|
||||
|
||||
<div class="inline-fields">
|
||||
{{ macros.field(_locals, 'text', 'name', user.name, 'Email name', null, 'required') }}
|
||||
|
@ -10,60 +10,6 @@
|
||||
|
||||
<h1>Mailbox manager</h1>
|
||||
|
||||
<section class="panel">
|
||||
<h2><i data-feather="globe"></i> Domains</h2>
|
||||
|
||||
<form action="{{ route('backend-add-domain') }}" method="POST" class="sub-panel">
|
||||
<h3>Add domain</h3>
|
||||
{{ macros.field(_locals, 'text', 'name', null, 'Domain name', null, 'required') }}
|
||||
|
||||
{{ macros.field(_locals, 'select', 'user_id', undefined, 'Owner', null, 'required', users) }}
|
||||
|
||||
<button><i data-feather="plus"></i> Add domain</button>
|
||||
|
||||
{{ macros.csrf(getCsrfToken) }}
|
||||
</form>
|
||||
|
||||
<table class="data-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>#</th>
|
||||
<th>Name</th>
|
||||
<th>Owner</th>
|
||||
<th>Identities</th>
|
||||
<th>Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
|
||||
<tbody>
|
||||
{% for domain in domains %}
|
||||
<tr>
|
||||
<td>{{ domain.id }}</td>
|
||||
<td>{{ domain.name }}</td>
|
||||
<td>{{ domain.owner_name | default('Public') }}</td>
|
||||
<td>{{ domain.identity_count }}</td>
|
||||
<td class="actions">
|
||||
<a href="{{ route('backend-edit-domain', domain.id) }}" class="button">
|
||||
<i data-feather="edit-2"></i> <span class="tip">Edit</span>
|
||||
</a>
|
||||
|
||||
<form action="{{ route('backend-remove-domain') }}" method="POST">
|
||||
<input type="hidden" name="id" value="{{ domain.id }}">
|
||||
|
||||
<button class="danger"
|
||||
onclick="return confirm('Are you sure you want to delete {{ domain.name }}?')">
|
||||
<i data-feather="trash"></i> <span class="tip">Remove</span>
|
||||
</button>
|
||||
|
||||
{{ macros.csrf(getCsrfToken) }}
|
||||
</form>
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</section>
|
||||
|
||||
<section class="panel">
|
||||
<h2><i data-feather="mail"></i> Mailboxes</h2>
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user