Compare commits
62 Commits
Author | SHA1 | Date | |
---|---|---|---|
bce1796115 | |||
97c87a96f4 | |||
e7d28000a4 | |||
87dbcf7e3c | |||
679e8ab769 | |||
827e7644fa | |||
efbc895ba3 | |||
2cb5b6f8f9 | |||
f5d135d331 | |||
82439366ba | |||
162f884103 | |||
00bb24cf2e | |||
911008af8b | |||
6399573432 | |||
14edb62823 | |||
84081ccd8c | |||
2bf70ca9e1 | |||
f90208c74e | |||
4bcee79e8a | |||
d223c9c77f | |||
faa728aacb | |||
00b1f25804 | |||
12f82f0f3d | |||
ceaa0769bc | |||
0f83dd7ad6 | |||
2bd1e9ac9f | |||
3c702de71b | |||
530f6c6043 | |||
b9949cdf5e | |||
272e506281 | |||
6478d77733 | |||
74f7d92437 | |||
0a0d284ecc | |||
959e5ca276 | |||
1cdd118806 | |||
145d8df70a | |||
01a1216c99 | |||
2726fd16cc | |||
6ee13b59ec | |||
2ccebc4295 | |||
8612b0f33a | |||
bda464ab16 | |||
44add2852d | |||
b6aa913006 | |||
f80d70da13 | |||
e7165edafd | |||
16394c0d4c | |||
d2a67a12ee | |||
1043690a5d | |||
5c5b273da6 | |||
9d1a9c2e3e | |||
1718736a27 | |||
1f064b5ea1 | |||
5564bf7991 | |||
46168c0e90 | |||
5eaebd5d12 | |||
9f3a541c3e | |||
ba5b90a4f9 | |||
f16d63c74f | |||
04f19f5a28 | |||
a4df579937 | |||
9e07d4bff4 |
@ -88,6 +88,7 @@
|
|||||||
},
|
},
|
||||||
"ignorePatterns": [
|
"ignorePatterns": [
|
||||||
"jest.config.js",
|
"jest.config.js",
|
||||||
|
"scripts/**/*",
|
||||||
"webpack.config.js",
|
"webpack.config.js",
|
||||||
"dist/**/*",
|
"dist/**/*",
|
||||||
"public/**/*",
|
"public/**/*",
|
||||||
|
2
.gitignore
vendored
2
.gitignore
vendored
@ -3,3 +3,5 @@ node_modules
|
|||||||
public
|
public
|
||||||
dist
|
dist
|
||||||
yarn-error.log
|
yarn-error.log
|
||||||
|
|
||||||
|
src/package.json
|
||||||
|
@ -9,7 +9,7 @@ Please feel free to contribute by making issues, bug reports and pull requests.
|
|||||||
## /!\ THIS PROJECT STILL LACKS ESSENTIAL FEATURES SUCH AS: /!\
|
## /!\ THIS PROJECT STILL LACKS ESSENTIAL FEATURES SUCH AS: /!\
|
||||||
|
|
||||||
- [x] ~~Change password~~
|
- [x] ~~Change password~~
|
||||||
- [ ] Password recovery (recovery emails are unused yet)
|
- [ ] Password reset (a magic link to remove your password, see [this issue](https://eternae.ink/arisu/swaf/issues/23))
|
||||||
- [ ] Quota management
|
- [ ] Quota management
|
||||||
- [ ] Editable terms of service
|
- [ ] Editable terms of service
|
||||||
- [ ] Complex permissions system
|
- [ ] Complex permissions system
|
||||||
@ -56,7 +56,7 @@ Note that a user only has one mailbox once they create their first address and c
|
|||||||
|
|
||||||
### Configuration
|
### Configuration
|
||||||
|
|
||||||
Create a `config/local.json5` file and override all wanted parameters. See `config/{default,prouction}.json5` and `node_modules/wms-core/config/{default,production}.json5` for defaults.
|
Create a `config/local.json5` file and override all wanted parameters. See `config/{default,prouction}.json5` and `node_modules/swaf/config/{default,production}.json5` for defaults.
|
||||||
|
|
||||||
## Postfix and dovecot mysql queries
|
## Postfix and dovecot mysql queries
|
||||||
|
|
||||||
|
@ -1,3 +1,6 @@
|
|||||||
|
# Please customize values i.e. paths, user, group, WorkingDirectory based on your environment. Do not use the same
|
||||||
|
# user and group for different applications.
|
||||||
|
|
||||||
[Unit]
|
[Unit]
|
||||||
Description=Rainbox Email website
|
Description=Rainbox Email website
|
||||||
After=network-online.target
|
After=network-online.target
|
||||||
|
@ -6,9 +6,10 @@ $secondaryForeground: $primaryForeground;
|
|||||||
$backgroundColor: darken($primary, 4%);
|
$backgroundColor: darken($primary, 4%);
|
||||||
$defaultTextColor: #ffffff;
|
$defaultTextColor: #ffffff;
|
||||||
|
|
||||||
$headerBackground: darken($primary, 7.5%);
|
$headerBackground: transparent;
|
||||||
$footerBackground: lighten($headerBackground, 1%);
|
$headerContainer: true;
|
||||||
$panelBackground: lighten($headerBackground, 1%);
|
$footerBackground: transparent;
|
||||||
|
$panelBackground: darken($backgroundColor, 3.2%);
|
||||||
$inputBackground: darken($panelBackground, 4%);
|
$inputBackground: darken($panelBackground, 4%);
|
||||||
|
|
||||||
$info: #4499ff;
|
$info: #4499ff;
|
||||||
@ -29,3 +30,4 @@ $errorColor: desaturate($errorText, 50%);
|
|||||||
|
|
||||||
// Responsivity
|
// Responsivity
|
||||||
$mobileThreshold: 632px;
|
$mobileThreshold: 632px;
|
||||||
|
$desktopThreshold: 940px;
|
||||||
|
@ -1,110 +1,13 @@
|
|||||||
@import "layout";
|
@import "layout";
|
||||||
|
|
||||||
a {
|
|
||||||
&:hover {
|
|
||||||
color: lighten($secondary, 30%);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
td.actions {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: row;
|
|
||||||
justify-content: center;
|
|
||||||
align-items: center;
|
|
||||||
|
|
||||||
form {
|
|
||||||
padding: 0;
|
|
||||||
display: inline;
|
|
||||||
}
|
|
||||||
|
|
||||||
button, .button {
|
|
||||||
margin: 0;
|
|
||||||
padding: 8px;
|
|
||||||
|
|
||||||
.feather {
|
|
||||||
margin-right: 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
> *:not(:first-child) {
|
|
||||||
margin-left: 8px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
body > header {
|
body > header {
|
||||||
background: transparent;
|
|
||||||
line-height: 16px;
|
|
||||||
|
|
||||||
@include container;
|
|
||||||
padding-top: 8px;
|
|
||||||
padding-bottom: 8px;
|
|
||||||
align-items: center;
|
|
||||||
|
|
||||||
@media (max-width: $mobileThreshold) {
|
|
||||||
padding: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.logo {
|
.logo {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
align-items: center;
|
|
||||||
max-width: 148px;
|
max-width: 148px;
|
||||||
padding: 8px;
|
padding: 8px 0;
|
||||||
|
|
||||||
font-size: 18px;
|
font-size: 18px;
|
||||||
line-height: 18px;
|
line-height: 18px;
|
||||||
|
|
||||||
> img {
|
|
||||||
margin: 0 8px 0 0;
|
|
||||||
width: initial;
|
|
||||||
height: 48px;
|
|
||||||
flex-shrink: 0;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
nav {
|
|
||||||
ul {
|
|
||||||
li {
|
|
||||||
margin-left: 8px;
|
|
||||||
|
|
||||||
a, button {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: row;
|
|
||||||
align-items: center;
|
|
||||||
height: auto;
|
|
||||||
padding: 8px;
|
|
||||||
|
|
||||||
border-radius: 3px;
|
|
||||||
|
|
||||||
.feather {
|
|
||||||
--icon-size: 16px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
button {
|
|
||||||
margin: 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
body > footer {
|
|
||||||
background: transparent;
|
|
||||||
}
|
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
@import "vars";
|
@import "vars";
|
||||||
@import 'fonts';
|
@import 'fonts';
|
||||||
|
@import "responsivity_tools";
|
||||||
|
|
||||||
* {
|
* {
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
@ -69,21 +70,31 @@ body {
|
|||||||
body > header {
|
body > header {
|
||||||
z-index: 50;
|
z-index: 50;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: row;
|
flex-direction: row-reverse;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
|
||||||
$headerHeight: 64px;
|
$headerHeight: 64px;
|
||||||
height: $headerHeight;
|
height: $headerHeight;
|
||||||
line-height: $headerHeight;
|
line-height: $headerHeight;
|
||||||
|
|
||||||
background-color: $headerBackground;
|
background: $headerBackground;
|
||||||
|
|
||||||
|
@if $headerContainer {
|
||||||
|
@include container;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: $mobileThreshold) {
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
.logo {
|
.logo {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
|
align-items: center;
|
||||||
|
|
||||||
padding: 0 24px 0 16px;
|
padding: 0 16px 0 8px;
|
||||||
font-size: 32px;
|
font-size: 24px;
|
||||||
color: $defaultTextColor;
|
color: $defaultTextColor;
|
||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
@ -91,34 +102,48 @@ body > header {
|
|||||||
}
|
}
|
||||||
|
|
||||||
img {
|
img {
|
||||||
width: $headerHeight;
|
width: initial;
|
||||||
height: $headerHeight;
|
height: calc(#{$headerHeight} - 16px);
|
||||||
margin-right: 16px;
|
margin-right: 8px;
|
||||||
|
flex-shrink: 0;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
nav {
|
nav {
|
||||||
ul {
|
> ul {
|
||||||
|
position: fixed;
|
||||||
|
z-index: -1;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
height: 100%;
|
||||||
|
transform: translateX(-100%);
|
||||||
|
transition: transform ease-out 150ms;
|
||||||
|
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: row;
|
flex-direction: column;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
padding: 0;
|
padding: $headerHeight 8px 8px;
|
||||||
|
|
||||||
font-size: 20px;
|
font-size: 20px;
|
||||||
|
|
||||||
|
background: $panelBackground;
|
||||||
|
|
||||||
li {
|
li {
|
||||||
position: relative;
|
position: relative;
|
||||||
list-style: none;
|
list-style: none;
|
||||||
|
margin-top: 8px;
|
||||||
|
|
||||||
a, button {
|
a, button {
|
||||||
position: relative;
|
position: relative;
|
||||||
height: 64px;
|
|
||||||
margin: 0;
|
margin: 0;
|
||||||
padding: 0 24px;
|
|
||||||
|
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
height: auto;
|
||||||
|
padding: 8px;
|
||||||
|
|
||||||
|
border-radius: 3px;
|
||||||
|
|
||||||
&:hover, &:active {
|
&:hover, &:active {
|
||||||
&:not(button) {
|
&:not(button) {
|
||||||
@ -127,13 +152,40 @@ body > header {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.feather {
|
.feather {
|
||||||
--icon-size: 24px;
|
--icon-size: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tip {
|
||||||
|
position: static;
|
||||||
|
visibility: visible;
|
||||||
|
opacity: 1;
|
||||||
|
display: block;
|
||||||
|
height: auto;
|
||||||
|
margin-left: 8px;
|
||||||
|
padding: 0 0 0 4px;
|
||||||
|
transform: none;
|
||||||
|
|
||||||
|
font-size: 16px;
|
||||||
|
line-height: 16px;
|
||||||
|
|
||||||
|
color: inherit;
|
||||||
|
text-transform: uppercase;
|
||||||
|
font-weight: inherit;
|
||||||
|
background: transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
.tip {
|
||||||
|
visibility: visible;
|
||||||
|
opacity: 1;
|
||||||
|
transition: opacity ease-out 100ms;
|
||||||
|
transition-delay: 150ms;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
button {
|
button {
|
||||||
margin: 8px;
|
margin: 0;
|
||||||
padding: 24px;
|
|
||||||
height: 32px;
|
height: 32px;
|
||||||
|
|
||||||
.feather {
|
.feather {
|
||||||
@ -158,45 +210,27 @@ body > header {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.dropdown {
|
.dropdown {
|
||||||
position: absolute;
|
position: initial;
|
||||||
z-index: -1;
|
|
||||||
top: 100%;
|
|
||||||
right: 0;
|
|
||||||
|
|
||||||
white-space: nowrap;
|
|
||||||
background: $headerBackground;
|
|
||||||
border-radius: 0 0 3px 3px;
|
|
||||||
|
|
||||||
a {
|
|
||||||
padding: 0 8px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
&:hover .dropdown {
|
|
||||||
display: block;
|
display: block;
|
||||||
|
padding-left: 0;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
> li:not(:first-child) {
|
||||||
|
border-top: 1px solid transparentize($defaultTextColor, 0.8);
|
||||||
|
padding-top: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.open {
|
||||||
|
transform: translateX(0%);
|
||||||
|
box-shadow: 0 0 5px darken($panelBackground, 20%);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#menu-button {
|
#menu-button {
|
||||||
display: none;
|
position: fixed;
|
||||||
}
|
top: 0;
|
||||||
}
|
left: 0;
|
||||||
|
|
||||||
@media (max-width: $mobileThreshold) {
|
|
||||||
flex-direction: row-reverse;
|
|
||||||
|
|
||||||
.logo {
|
|
||||||
padding: 0 16px 0 8px;
|
|
||||||
font-size: 24px;
|
|
||||||
|
|
||||||
img {
|
|
||||||
margin-right: 8px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
nav {
|
|
||||||
#menu-button {
|
|
||||||
display: block;
|
display: block;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
padding: 0 16px;
|
padding: 0 16px;
|
||||||
@ -212,46 +246,32 @@ body > header {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
> ul {
|
hr {
|
||||||
flex-direction: column;
|
border: 0;
|
||||||
position: absolute;
|
border-bottom: 1px solid $defaultTextColor;
|
||||||
z-index: 10;
|
opacity: 0.2;
|
||||||
left: 0;
|
|
||||||
transform: translateX(-100%);
|
|
||||||
transition: transform ease-out 150ms;
|
|
||||||
|
|
||||||
background-color: $headerBackground;
|
|
||||||
|
|
||||||
&.open {
|
|
||||||
transform: translateX(0%);
|
|
||||||
}
|
|
||||||
|
|
||||||
li {
|
|
||||||
a, button {
|
|
||||||
.tip {
|
|
||||||
display: block;
|
|
||||||
margin-left: 8px;
|
|
||||||
text-transform: inherit;
|
|
||||||
font-weight: inherit;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.dropdown {
|
|
||||||
position: initial;
|
|
||||||
display: block;
|
|
||||||
padding-left: 32px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (min-width: $mobileThreshold) {
|
@media (min-width: $mobileThreshold) {
|
||||||
nav ul li {
|
flex-direction: row;
|
||||||
a, button, .button {
|
|
||||||
@include tip;
|
nav {
|
||||||
|
#menu-button {
|
||||||
|
display: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
ul {
|
||||||
|
position: static;
|
||||||
|
flex-direction: row;
|
||||||
|
transform: none;
|
||||||
|
padding: 0;
|
||||||
|
background: transparent;
|
||||||
|
|
||||||
|
li {
|
||||||
|
margin-top: 0;
|
||||||
|
margin-left: 8px;
|
||||||
|
|
||||||
&:last-child {
|
&:last-child {
|
||||||
a, button, .button {
|
a, button, .button {
|
||||||
.tip {
|
.tip {
|
||||||
@ -261,11 +281,46 @@ body > header {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.dropdown {
|
||||||
|
position: absolute;
|
||||||
|
z-index: -1;
|
||||||
|
top: 100%;
|
||||||
|
right: 0;
|
||||||
|
display: none;
|
||||||
|
padding: 8px;
|
||||||
|
|
||||||
|
white-space: nowrap;
|
||||||
|
background: $panelBackground;
|
||||||
|
border-radius: 0 0 3px 3px;
|
||||||
|
|
||||||
|
box-shadow: 0 2px 2px transparentize(darken($panelBackground, 20%), 0.75);
|
||||||
|
border-top: 4px solid lighten($panelBackground, 5%);
|
||||||
|
|
||||||
|
li {
|
||||||
|
margin-left: 0;
|
||||||
|
|
||||||
|
&:not(:first-child) {
|
||||||
|
margin-top: 8px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
footer {
|
&:hover .dropdown {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
> li:not(:first-child) {
|
||||||
|
border-top: 0;
|
||||||
|
padding-top: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
body > footer {
|
||||||
padding: 8px;
|
padding: 8px;
|
||||||
margin-top: 8px;
|
margin-top: 8px;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
@ -338,7 +393,7 @@ a {
|
|||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
color: lighten($secondary, 10%);
|
color: lighten($secondary, 30%);
|
||||||
}
|
}
|
||||||
|
|
||||||
.feather.feather-external-link {
|
.feather.feather-external-link {
|
||||||
@ -352,7 +407,7 @@ form {
|
|||||||
padding: 8px 16px;
|
padding: 8px 16px;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
|
|
||||||
.form-field {
|
.form-field:not(.hidden) {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
margin: 16px auto;
|
margin: 16px auto;
|
||||||
@ -607,6 +662,35 @@ button, .button {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// ---
|
||||||
|
// --- Tables
|
||||||
|
// ---
|
||||||
|
td.actions {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
|
||||||
|
form {
|
||||||
|
padding: 0;
|
||||||
|
display: inline;
|
||||||
|
}
|
||||||
|
|
||||||
|
button, .button {
|
||||||
|
margin: 0;
|
||||||
|
padding: 8px;
|
||||||
|
|
||||||
|
.feather {
|
||||||
|
margin-right: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
> *:not(:first-child) {
|
||||||
|
margin-left: 8px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.data-table {
|
.data-table {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
text-align: left;
|
text-align: left;
|
||||||
@ -618,6 +702,7 @@ button, .button {
|
|||||||
|
|
||||||
th {
|
th {
|
||||||
border-bottom: 1px solid #39434a;
|
border-bottom: 1px solid #39434a;
|
||||||
|
white-space: nowrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
tr:nth-child(even) {
|
tr:nth-child(even) {
|
||||||
@ -633,6 +718,10 @@ button, .button {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// ---
|
||||||
|
// --- Breadcrumb widget
|
||||||
|
// ---
|
||||||
.breadcrumb {
|
.breadcrumb {
|
||||||
list-style: none;
|
list-style: none;
|
||||||
display: flex;
|
display: flex;
|
||||||
@ -646,6 +735,7 @@ button, .button {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
// ---
|
// ---
|
||||||
// --- Layout helpers
|
// --- Layout helpers
|
||||||
// ---
|
// ---
|
||||||
@ -653,24 +743,6 @@ button, .button {
|
|||||||
text-align: center;
|
text-align: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
@mixin container {
|
|
||||||
width: $mobileThreshold;
|
|
||||||
padding: 0 16px;
|
|
||||||
|
|
||||||
@media (min-width: $mobileThreshold) {
|
|
||||||
margin: 0 auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (max-width: $mobileThreshold) {
|
|
||||||
width: 100%;
|
|
||||||
padding: 0 8px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.container {
|
|
||||||
@include container;
|
|
||||||
}
|
|
||||||
|
|
||||||
.panel {
|
.panel {
|
||||||
position: relative;
|
position: relative;
|
||||||
margin: 16px 0 48px;
|
margin: 16px 0 48px;
|
||||||
@ -797,37 +869,7 @@ button, .button {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.content {
|
.content {
|
||||||
overflow: hidden;
|
width: 0;
|
||||||
white-space: nowrap;
|
|
||||||
padding: 8px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.copy-button {
|
|
||||||
margin: 0;
|
|
||||||
padding: 0;
|
|
||||||
border-radius: 0;
|
|
||||||
|
|
||||||
.feather {
|
|
||||||
--icon-size: 20px;
|
|
||||||
margin: 8px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.copyable-text {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: row;
|
|
||||||
margin: 8px;
|
|
||||||
|
|
||||||
background-color: darken($backgroundColor, 2%);
|
|
||||||
border-radius: 5px;
|
|
||||||
overflow: hidden;
|
|
||||||
|
|
||||||
.title {
|
|
||||||
padding: 8px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.content {
|
|
||||||
flex-grow: 1;
|
flex-grow: 1;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
@ -877,3 +919,34 @@ button, .button {
|
|||||||
background: $secondary;
|
background: $secondary;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.table-col-grow {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pagination {
|
||||||
|
ul {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
list-style: none;
|
||||||
|
padding: 8px;
|
||||||
|
|
||||||
|
justify-content: center;
|
||||||
|
|
||||||
|
li {
|
||||||
|
a, &.active, &.ellipsis {
|
||||||
|
display: block;
|
||||||
|
min-width: 40px;
|
||||||
|
height: 40px;
|
||||||
|
padding: 4px;
|
||||||
|
|
||||||
|
line-height: 32px;
|
||||||
|
text-align:center;
|
||||||
|
|
||||||
|
&:hover:not(.active):not(.ellipsis) {
|
||||||
|
background-color: #fff5;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
19
assets/sass/responsivity_tools.scss
Normal file
19
assets/sass/responsivity_tools.scss
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
@import "vars";
|
||||||
|
|
||||||
|
@mixin container {
|
||||||
|
width: 100%;
|
||||||
|
padding: 0 8px;
|
||||||
|
|
||||||
|
@media (min-width: $mobileThreshold) {
|
||||||
|
margin: 0 auto;
|
||||||
|
padding: 0 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (min-width: $desktopThreshold) {
|
||||||
|
width: $desktopThreshold;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.container {
|
||||||
|
@include container;
|
||||||
|
}
|
@ -1,7 +1,7 @@
|
|||||||
/*
|
/*
|
||||||
* For labels to update their state (css selectors based on the value attribute)
|
* For labels to update their state (css selectors based on the value attribute)
|
||||||
*/
|
*/
|
||||||
import {ValidationError} from "wms-core/db/Validator";
|
import {ValidationError} from "swaf/db/Validator";
|
||||||
|
|
||||||
export function updateInputs(): void {
|
export function updateInputs(): void {
|
||||||
document.querySelectorAll<HTMLInputElement | HTMLTextAreaElement>('input, textarea').forEach(el => {
|
document.querySelectorAll<HTMLInputElement | HTMLTextAreaElement>('input, textarea').forEach(el => {
|
||||||
|
@ -36,11 +36,17 @@
|
|||||||
allow_invalid_tls: true,
|
allow_invalid_tls: true,
|
||||||
from: 'contact@rainbox.email',
|
from: 'contact@rainbox.email',
|
||||||
from_name: 'Rainbox Email',
|
from_name: 'Rainbox Email',
|
||||||
|
// Unlimited
|
||||||
|
max_public_identities_per_user: -1,
|
||||||
},
|
},
|
||||||
view: {
|
view: {
|
||||||
cache: false,
|
cache: false,
|
||||||
},
|
},
|
||||||
|
auth: {
|
||||||
approval_mode: false,
|
approval_mode: false,
|
||||||
|
// 30 days
|
||||||
|
name_change_wait_period: 2592000000,
|
||||||
|
},
|
||||||
magic_link: {
|
magic_link: {
|
||||||
validity_period: 20,
|
validity_period: 20,
|
||||||
},
|
},
|
||||||
|
@ -5,14 +5,16 @@
|
|||||||
public_websocket_url: "wss://rainbox.email",
|
public_websocket_url: "wss://rainbox.email",
|
||||||
session: {
|
session: {
|
||||||
cookie: {
|
cookie: {
|
||||||
secure: true
|
secure: true,
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
mail: {
|
mail: {
|
||||||
secure: true,
|
secure: true,
|
||||||
allow_invalid_tls: false
|
allow_invalid_tls: false,
|
||||||
},
|
},
|
||||||
|
auth: {
|
||||||
approval_mode: true,
|
approval_mode: true,
|
||||||
|
},
|
||||||
magic_link: {
|
magic_link: {
|
||||||
validity_period: 900,
|
validity_period: 900,
|
||||||
},
|
},
|
||||||
|
@ -6,4 +6,7 @@
|
|||||||
database: "rainbox_email_test",
|
database: "rainbox_email_test",
|
||||||
create_database_automatically: true
|
create_database_automatically: true
|
||||||
},
|
},
|
||||||
|
mail: {
|
||||||
|
max_public_identities_per_user: 1,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
40
package.json
40
package.json
@ -1,27 +1,26 @@
|
|||||||
{
|
{
|
||||||
"name": "rainbox.email",
|
"name": "rainbox.email",
|
||||||
"version": "2.2.0",
|
"version": "2.4.2",
|
||||||
"description": "ISP mail provider manager with mysql and integrated LDAP server",
|
"description": "ISP mail provider manager with mysql and integrated LDAP server",
|
||||||
"repository": "https://gitlab.com/ArisuOngaku/rainbox.email",
|
"repository": "https://eternae.ink/ashpie/rainbox.email",
|
||||||
"author": "Alice Gaudon <alice@gaudon.pro>",
|
"author": "Alice Gaudon <alice@gaudon.pro>",
|
||||||
"main": "dist/src/main.js",
|
"main": "dist/main.js",
|
||||||
"license": "AGPL-3.0-only",
|
"license": "AGPL-3.0-only",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dist-webpack": "webpack --mode production",
|
"test": "jest --verbose --runInBand",
|
||||||
"clean": "(test ! -d dist || rm -r dist)",
|
"clean": "node scripts/clean.js",
|
||||||
|
"prepare-sources": "node scripts/prepare-sources.js",
|
||||||
"compile": "yarn clean && tsc",
|
"compile": "yarn clean && tsc",
|
||||||
"build": "yarn compile && yarn dist-webpack",
|
"build": "yarn prepare-sources && yarn compile && webpack --mode production",
|
||||||
"lint": "eslint . --ext .js,.jsx,.ts,.tsx",
|
"dev": "yarn prepare-sources && concurrently -k -n \"Typescript,Node,Webpack,Maildev\" -p \"[{name}]\" -c \"blue,green,red,yellow\" \"tsc --watch\" \"nodemon\" \"webpack --watch --mode development\" \"maildev\"",
|
||||||
"dev": "concurrently -k -n \"Typescript,Node,Webpack,Maildev\" -p \"[{name}]\" -c \"blue,green,red,yellow\" \"tsc --watch\" \"nodemon\" \"webpack --watch --mode development\" \"maildev\"",
|
"start": "yarn build && node",
|
||||||
"start": "yarn build && node dist/src/main.js",
|
"lint": "eslint ."
|
||||||
"test": "jest --verbose --runInBand"
|
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@babel/core": "^7.9.0",
|
"@babel/core": "^7.9.0",
|
||||||
"@babel/preset-env": "^7.9.5",
|
"@babel/preset-env": "^7.9.5",
|
||||||
"@fortawesome/fontawesome-free": "^5.14.0",
|
"@fortawesome/fontawesome-free": "^5.14.0",
|
||||||
"@types/argon2": "^0.15.0",
|
"@types/config": "^0.0.38",
|
||||||
"@types/config": "^0.0.36",
|
|
||||||
"@types/express": "^4.17.6",
|
"@types/express": "^4.17.6",
|
||||||
"@types/express-session": "^1.17.0",
|
"@types/express-session": "^1.17.0",
|
||||||
"@types/feather-icons": "^4.7.0",
|
"@types/feather-icons": "^4.7.0",
|
||||||
@ -29,32 +28,33 @@
|
|||||||
"@types/jest": "^26.0.4",
|
"@types/jest": "^26.0.4",
|
||||||
"@types/ldapjs": "^1.0.7",
|
"@types/ldapjs": "^1.0.7",
|
||||||
"@types/mysql": "^2.15.15",
|
"@types/mysql": "^2.15.15",
|
||||||
"@types/node": "^14.6.3",
|
"@types/node": "^15.0.1",
|
||||||
"@types/nodemailer": "^6.4.0",
|
"@types/nodemailer": "^6.4.0",
|
||||||
"@types/nunjucks": "^3.1.3",
|
"@types/nunjucks": "^3.1.3",
|
||||||
"@types/ws": "^7.2.6",
|
"@types/ws": "^7.2.6",
|
||||||
"@typescript-eslint/eslint-plugin": "^4.3.0",
|
"@typescript-eslint/eslint-plugin": "^4.3.0",
|
||||||
"@typescript-eslint/parser": "^4.3.0",
|
"@typescript-eslint/parser": "^4.3.0",
|
||||||
"babel-loader": "^8.1.0",
|
"babel-loader": "^8.1.0",
|
||||||
"concurrently": "^5.1.0",
|
"concurrently": "^6.0.0",
|
||||||
"css-loader": "^5.0.0",
|
"css-loader": "^5.0.0",
|
||||||
"eslint": "^7.10.0",
|
"eslint": "^7.10.0",
|
||||||
"feather-icons": "^4.28.0",
|
"feather-icons": "^4.28.0",
|
||||||
"file-loader": "^6.0.0",
|
"file-loader": "^6.0.0",
|
||||||
"imagemin": "^7.0.1",
|
"imagemin": "^7.0.0",
|
||||||
"imagemin-gifsicle": "^7.0.0",
|
"imagemin-gifsicle": "^7.0.0",
|
||||||
"imagemin-mozjpeg": "^9.0.0",
|
"imagemin-mozjpeg": "^9.0.0",
|
||||||
"imagemin-pngquant": "^9.0.0",
|
"imagemin-pngquant": "^9.0.0",
|
||||||
"imagemin-svgo": "^8.0.0",
|
"imagemin-svgo": "^9.0.0",
|
||||||
"img-loader": "^3.0.1",
|
"img-loader": "^3.0.1",
|
||||||
"jest": "^26.1.0",
|
"jest": "^26.1.0",
|
||||||
|
"maildev": "^1.1.0",
|
||||||
"mini-css-extract-plugin": "^1.2.1",
|
"mini-css-extract-plugin": "^1.2.1",
|
||||||
"node-sass": "^5.0.0",
|
|
||||||
"nodemon": "^2.0.3",
|
"nodemon": "^2.0.3",
|
||||||
"sass-loader": "^10.0.1",
|
"sass": "^1.32.12",
|
||||||
|
"sass-loader": "^11.0.1",
|
||||||
"terser-webpack-plugin": "^5.0.3",
|
"terser-webpack-plugin": "^5.0.3",
|
||||||
"ts-jest": "^26.1.1",
|
"ts-jest": "^26.1.1",
|
||||||
"ts-loader": "^8.0.4",
|
"ts-loader": "^9.1.0",
|
||||||
"typescript": "^4.0.2",
|
"typescript": "^4.0.2",
|
||||||
"webpack": "^5.3.2",
|
"webpack": "^5.3.2",
|
||||||
"webpack-cli": "^4.1.0"
|
"webpack-cli": "^4.1.0"
|
||||||
@ -64,6 +64,6 @@
|
|||||||
"config": "^3.3.1",
|
"config": "^3.3.1",
|
||||||
"express": "^4.17.1",
|
"express": "^4.17.1",
|
||||||
"ldapjs": "^2.0.0",
|
"ldapjs": "^2.0.0",
|
||||||
"wms-core": "^0.22.0"
|
"swaf": "^0.23.0"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
10
scripts/clean.js
Normal file
10
scripts/clean.js
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
const fs = require('fs');
|
||||||
|
|
||||||
|
[
|
||||||
|
'dist',
|
||||||
|
].forEach(file => {
|
||||||
|
if (fs.existsSync(file)) {
|
||||||
|
console.log('Cleaning', file, '...');
|
||||||
|
fs.rmSync(file, {recursive: true});
|
||||||
|
}
|
||||||
|
});
|
4
scripts/prepare-sources.js
Normal file
4
scripts/prepare-sources.js
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
const fs = require('fs');
|
||||||
|
const path = require('path');
|
||||||
|
|
||||||
|
fs.copyFileSync('package.json', path.join('src', 'package.json'));
|
141
src/App.ts
141
src/App.ts
@ -1,50 +1,50 @@
|
|||||||
import Application from "wms-core/Application";
|
import Application from "swaf/Application";
|
||||||
import Migration, {MigrationType} from "wms-core/db/Migration";
|
import Migration, {MigrationType} from "swaf/db/Migration";
|
||||||
import ExpressAppComponent from "wms-core/components/ExpressAppComponent";
|
import ExpressAppComponent from "swaf/components/ExpressAppComponent";
|
||||||
import NunjucksComponent from "wms-core/components/NunjucksComponent";
|
import NunjucksComponent from "swaf/components/NunjucksComponent";
|
||||||
import MysqlComponent from "wms-core/components/MysqlComponent";
|
import MysqlComponent from "swaf/components/MysqlComponent";
|
||||||
import LogRequestsComponent from "wms-core/components/LogRequestsComponent";
|
import LogRequestsComponent from "swaf/components/LogRequestsComponent";
|
||||||
import RedisComponent from "wms-core/components/RedisComponent";
|
import RedisComponent from "swaf/components/RedisComponent";
|
||||||
import ServeStaticDirectoryComponent from "wms-core/components/ServeStaticDirectoryComponent";
|
import ServeStaticDirectoryComponent from "swaf/components/ServeStaticDirectoryComponent";
|
||||||
import MaintenanceComponent from "wms-core/components/MaintenanceComponent";
|
import MaintenanceComponent from "swaf/components/MaintenanceComponent";
|
||||||
import MailComponent from "wms-core/components/MailComponent";
|
import MailComponent from "swaf/components/MailComponent";
|
||||||
import SessionComponent from "wms-core/components/SessionComponent";
|
import SessionComponent from "swaf/components/SessionComponent";
|
||||||
import FormHelperComponent from "wms-core/components/FormHelperComponent";
|
import FormHelperComponent from "swaf/components/FormHelperComponent";
|
||||||
import CsrfProtectionComponent from "wms-core/components/CsrfProtectionComponent";
|
import CsrfProtectionComponent from "swaf/components/CsrfProtectionComponent";
|
||||||
import WebSocketServerComponent from "wms-core/components/WebSocketServerComponent";
|
import WebSocketServerComponent from "swaf/components/WebSocketServerComponent";
|
||||||
import HomeController from "./controllers/HomeController";
|
import HomeController from "./controllers/HomeController";
|
||||||
import AuthController from "./controllers/AuthController";
|
import AuthComponent from "swaf/auth/AuthComponent";
|
||||||
import AuthComponent from "wms-core/auth/AuthComponent";
|
|
||||||
import AuthGuard from "wms-core/auth/AuthGuard";
|
|
||||||
import {PasswordAuthProof} from "./models/UserPasswordComponent";
|
|
||||||
import LDAPServerComponent from "./LDAPServerComponent";
|
import LDAPServerComponent from "./LDAPServerComponent";
|
||||||
import AutoUpdateComponent from "wms-core/components/AutoUpdateComponent";
|
import AutoUpdateComponent from "swaf/components/AutoUpdateComponent";
|
||||||
import DummyMigration from "wms-core/migrations/DummyMigration";
|
import DummyMigration from "swaf/migrations/DummyMigration";
|
||||||
import DropLegacyLogsTable from "wms-core/migrations/DropLegacyLogsTable";
|
import DropLegacyLogsTable from "swaf/migrations/DropLegacyLogsTable";
|
||||||
import AccountController from "./controllers/AccountController";
|
import AccountMailboxController from "./controllers/AccountMailboxController";
|
||||||
import CreateMigrationsTable from "wms-core/migrations/CreateMigrationsTable";
|
import CreateMigrationsTable from "swaf/migrations/CreateMigrationsTable";
|
||||||
import CreateUsersAndUserEmailsTable from "wms-core/auth/migrations/CreateUsersAndUserEmailsTable";
|
import MagicLinkWebSocketListener from "swaf/auth/magic_link/MagicLinkWebSocketListener";
|
||||||
import AddPasswordToUsers from "./migrations/AddPasswordToUsers";
|
import BackendController from "swaf/helpers/BackendController";
|
||||||
import CreateMagicLinksTable from "wms-core/auth/migrations/CreateMagicLinksTable";
|
import CreateMailTablesMigration from "./migrations/CreateMailTablesMigration";
|
||||||
import MailController from "wms-core/auth/MailController";
|
|
||||||
import MagicLinkController from "./controllers/MagicLinkController";
|
|
||||||
import MagicLinkWebSocketListener from "wms-core/auth/magic_link/MagicLinkWebSocketListener";
|
|
||||||
import BackendController from "wms-core/helpers/BackendController";
|
|
||||||
import AddApprovedFieldToUsersTable from "wms-core/auth/migrations/AddApprovedFieldToUsersTable";
|
|
||||||
import FixUserMainEmailRelation from "wms-core/auth/migrations/FixUserMainEmailRelation";
|
|
||||||
import DropNameFromUsers from "wms-core/auth/migrations/DropNameFromUsers";
|
|
||||||
import MagicLink from "wms-core/auth/models/MagicLink";
|
|
||||||
import AddNameToUsers from "./migrations/AddNameToUsers";
|
|
||||||
import CreateMailTables from "./migrations/CreateMailTables";
|
|
||||||
import MailboxBackendController from "./controllers/backend/MailboxBackendController";
|
import MailboxBackendController from "./controllers/backend/MailboxBackendController";
|
||||||
import RedirectBackComponent from "wms-core/components/RedirectBackComponent";
|
|
||||||
import MailAutoConfigController from "./controllers/MailAutoConfigController";
|
import MailAutoConfigController from "./controllers/MailAutoConfigController";
|
||||||
import AccountBackendController from "./controllers/backend/AccountBackendController";
|
import AccountBackendController from "./controllers/backend/AccountBackendController";
|
||||||
import packageJson = require('../package.json');
|
import CreateUsersAndUserEmailsTableMigration from "swaf/auth/migrations/CreateUsersAndUserEmailsTableMigration";
|
||||||
|
import AddPasswordToUsersMigration from "swaf/auth/password/AddPasswordToUsersMigration";
|
||||||
|
import CreateMagicLinksTableMigration from "swaf/auth/magic_link/CreateMagicLinksTableMigration";
|
||||||
|
import AddApprovedFieldToUsersTableMigration from "swaf/auth/migrations/AddApprovedFieldToUsersTableMigration";
|
||||||
|
import AddNameToUsersMigration from "swaf/auth/migrations/AddNameToUsersMigration";
|
||||||
|
import PreviousUrlComponent from "swaf/components/PreviousUrlComponent";
|
||||||
|
import MagicLinkAuthMethod from "swaf/auth/magic_link/MagicLinkAuthMethod";
|
||||||
|
import {MAGIC_LINK_MAIL} from "swaf/Mails";
|
||||||
|
import PasswordAuthMethod from "swaf/auth/password/PasswordAuthMethod";
|
||||||
|
import MagicLinkController from "swaf/auth/magic_link/MagicLinkController";
|
||||||
|
import AuthController from "swaf/auth/AuthController";
|
||||||
|
import MailController from "swaf/mail/MailController";
|
||||||
|
import AccountController from "swaf/auth/AccountController";
|
||||||
|
import packageJson = require('./package.json');
|
||||||
|
import AddUsedToMagicLinksMigration from "swaf/auth/magic_link/AddUsedToMagicLinksMigration";
|
||||||
|
import MakeMagicLinksSessionNotUniqueMigration from "swaf/auth/magic_link/MakeMagicLinksSessionNotUniqueMigration";
|
||||||
|
import AddNameChangedAtToUsersMigration from "swaf/auth/migrations/AddNameChangedAtToUsersMigration";
|
||||||
|
|
||||||
export default class App extends Application {
|
export default class App extends Application {
|
||||||
private magicLinkWebSocketListener?: MagicLinkWebSocketListener<this>;
|
|
||||||
|
|
||||||
public constructor(
|
public constructor(
|
||||||
private readonly addr: string,
|
private readonly addr: string,
|
||||||
private readonly port: number,
|
private readonly port: number,
|
||||||
@ -56,15 +56,18 @@ export default class App extends Application {
|
|||||||
return [
|
return [
|
||||||
CreateMigrationsTable,
|
CreateMigrationsTable,
|
||||||
DummyMigration,
|
DummyMigration,
|
||||||
CreateUsersAndUserEmailsTable,
|
CreateUsersAndUserEmailsTableMigration,
|
||||||
AddPasswordToUsers,
|
AddPasswordToUsersMigration,
|
||||||
CreateMagicLinksTable,
|
CreateMagicLinksTableMigration,
|
||||||
AddApprovedFieldToUsersTable,
|
AddApprovedFieldToUsersTableMigration,
|
||||||
FixUserMainEmailRelation,
|
DummyMigration,
|
||||||
DropNameFromUsers,
|
DummyMigration,
|
||||||
AddNameToUsers,
|
AddNameToUsersMigration,
|
||||||
CreateMailTables,
|
CreateMailTablesMigration,
|
||||||
DropLegacyLogsTable,
|
DropLegacyLogsTable,
|
||||||
|
AddUsedToMagicLinksMigration,
|
||||||
|
MakeMagicLinksSessionNotUniqueMigration,
|
||||||
|
AddNameChangedAtToUsersMigration,
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -75,12 +78,8 @@ export default class App extends Application {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private registerComponents() {
|
private registerComponents() {
|
||||||
const redisComponent = new RedisComponent();
|
|
||||||
const mysqlComponent = new MysqlComponent();
|
|
||||||
|
|
||||||
// Base
|
// Base
|
||||||
const expressAppComponent = new ExpressAppComponent(this.addr, this.port);
|
this.use(new ExpressAppComponent(this.addr, this.port));
|
||||||
this.use(expressAppComponent);
|
|
||||||
this.use(new LogRequestsComponent());
|
this.use(new LogRequestsComponent());
|
||||||
|
|
||||||
// Static files
|
// Static files
|
||||||
@ -89,21 +88,21 @@ export default class App extends Application {
|
|||||||
|
|
||||||
// Dynamic views and routes
|
// Dynamic views and routes
|
||||||
this.use(new NunjucksComponent());
|
this.use(new NunjucksComponent());
|
||||||
this.use(new RedirectBackComponent());
|
this.use(new PreviousUrlComponent());
|
||||||
|
|
||||||
// Maintenance
|
// Maintenance
|
||||||
this.use(new MaintenanceComponent(this, () => {
|
this.use(new MaintenanceComponent(this, () => {
|
||||||
return redisComponent.canServe() && mysqlComponent.canServe();
|
return this.as(RedisComponent).canServe() && this.as(MysqlComponent).canServe();
|
||||||
}));
|
}));
|
||||||
this.use(new AutoUpdateComponent());
|
this.use(new AutoUpdateComponent());
|
||||||
|
|
||||||
// Services
|
// Services
|
||||||
this.use(mysqlComponent);
|
this.use(new MysqlComponent());
|
||||||
this.use(new MailComponent());
|
this.use(new MailComponent());
|
||||||
|
|
||||||
// Session
|
// Session
|
||||||
this.use(redisComponent);
|
this.use(new RedisComponent());
|
||||||
this.use(new SessionComponent(redisComponent));
|
this.use(new SessionComponent(this.as(RedisComponent)));
|
||||||
|
|
||||||
// Utils
|
// Utils
|
||||||
this.use(new FormHelperComponent());
|
this.use(new FormHelperComponent());
|
||||||
@ -112,37 +111,33 @@ export default class App extends Application {
|
|||||||
this.use(new CsrfProtectionComponent());
|
this.use(new CsrfProtectionComponent());
|
||||||
|
|
||||||
// Auth
|
// Auth
|
||||||
this.use(new AuthComponent(new class extends AuthGuard<PasswordAuthProof | MagicLink> {
|
this.use(new AuthComponent(this, new MagicLinkAuthMethod(this, MAGIC_LINK_MAIL), new PasswordAuthMethod(this)));
|
||||||
public async getProofForSession(session: Express.Session): Promise<PasswordAuthProof | MagicLink | null> {
|
|
||||||
return PasswordAuthProof.getProofForSession(session) ||
|
|
||||||
await MagicLink.bySessionId(session.id);
|
|
||||||
}
|
|
||||||
}(this)));
|
|
||||||
|
|
||||||
// WebSocket server
|
// WebSocket server
|
||||||
this.use(new WebSocketServerComponent(this, expressAppComponent, redisComponent));
|
this.use(new WebSocketServerComponent(this, this.as(ExpressAppComponent), this.as(RedisComponent)));
|
||||||
|
|
||||||
// LDAP server
|
// LDAP server
|
||||||
this.use(new LDAPServerComponent());
|
this.use(new LDAPServerComponent());
|
||||||
}
|
}
|
||||||
|
|
||||||
private registerWebSocketListeners() {
|
private registerWebSocketListeners() {
|
||||||
this.magicLinkWebSocketListener = new MagicLinkWebSocketListener();
|
this.use(new MagicLinkWebSocketListener());
|
||||||
this.use(this.magicLinkWebSocketListener);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private registerControllers() {
|
private registerControllers() {
|
||||||
// Priority routes / interrupting middlewares
|
// Priority routes / interrupting middlewares
|
||||||
|
this.use(new MailAutoConfigController()); // Needs to override MailController
|
||||||
|
this.use(new MailController());
|
||||||
|
this.use(new AuthController());
|
||||||
this.use(new AccountController());
|
this.use(new AccountController());
|
||||||
this.use(new MagicLinkController(this.as(MagicLinkWebSocketListener)));
|
this.use(new AccountMailboxController());
|
||||||
|
|
||||||
|
this.use(new MagicLinkController(this.as<MagicLinkWebSocketListener<this>>(MagicLinkWebSocketListener)));
|
||||||
|
|
||||||
|
// Core functionality
|
||||||
this.use(new BackendController());
|
this.use(new BackendController());
|
||||||
this.use(new MailboxBackendController());
|
this.use(new MailboxBackendController());
|
||||||
this.use(new AccountBackendController());
|
this.use(new AccountBackendController());
|
||||||
this.use(new AuthController());
|
|
||||||
this.use(new MailAutoConfigController()); // Needs to override MailController
|
|
||||||
|
|
||||||
// Core functionality
|
|
||||||
this.use(new MailController());
|
|
||||||
|
|
||||||
// Other functionnality
|
// Other functionnality
|
||||||
|
|
||||||
|
@ -1,9 +1,9 @@
|
|||||||
import ApplicationComponent from "wms-core/ApplicationComponent";
|
import ApplicationComponent from "swaf/ApplicationComponent";
|
||||||
import ldap, {InvalidCredentialsError, Server} from "ldapjs";
|
import ldap, {InvalidCredentialsError, Server} from "ldapjs";
|
||||||
import {log} from "wms-core/Logger";
|
import {logger} from "swaf/Logger";
|
||||||
import UserPasswordComponent from "./models/UserPasswordComponent";
|
import Throttler from "swaf/Throttler";
|
||||||
import Throttler from "wms-core/Throttler";
|
import User from "swaf/auth/models/User";
|
||||||
import User from "wms-core/auth/models/User";
|
import UserPasswordComponent from "swaf/auth/password/UserPasswordComponent";
|
||||||
|
|
||||||
export default class LDAPServerComponent extends ApplicationComponent {
|
export default class LDAPServerComponent extends ApplicationComponent {
|
||||||
private server?: Server;
|
private server?: Server;
|
||||||
@ -24,12 +24,12 @@ export default class LDAPServerComponent extends ApplicationComponent {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
log.debug('Matrix authentication attempt:', username, email);
|
logger.debug('Matrix authentication attempt:', username, email);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
Throttler.throttle('ldap_auth', 3, 30 * 1000, username);
|
Throttler.throttle('ldap_auth', 3, 30 * 1000, username);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
log.debug('Too many auth requests');
|
logger.debug('Too many auth requests');
|
||||||
next(new InvalidCredentialsError());
|
next(new InvalidCredentialsError());
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -39,31 +39,31 @@ export default class LDAPServerComponent extends ApplicationComponent {
|
|||||||
const email = await user.mainEmail.get();
|
const email = await user.mainEmail.get();
|
||||||
if (email) {
|
if (email) {
|
||||||
if (await user.as(UserPasswordComponent).verifyPassword(req.credentials)) {
|
if (await user.as(UserPasswordComponent).verifyPassword(req.credentials)) {
|
||||||
log.debug('Success');
|
logger.debug('Success');
|
||||||
res.end();
|
res.end();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
log.debug('Fail');
|
logger.debug('Fail');
|
||||||
next(new InvalidCredentialsError());
|
next(new InvalidCredentialsError());
|
||||||
});
|
});
|
||||||
this.server.unbind((req: unknown, res: unknown, next: () => void) => {
|
this.server.unbind((req: unknown, res: unknown, next: () => void) => {
|
||||||
log.debug('Unbind', req);
|
logger.debug('Unbind', req);
|
||||||
next();
|
next();
|
||||||
});
|
});
|
||||||
this.server.listen(8389, '127.0.0.1', () => {
|
this.server.listen(8389, '127.0.0.1', () => {
|
||||||
log.info(`LDAP server listening on ${this.server?.url}`);
|
logger.info(`LDAP server listening on ${this.server?.url}`);
|
||||||
});
|
});
|
||||||
this.server.on('close', () => {
|
this.server.on('close', () => {
|
||||||
log.info('LDAP server closed.');
|
logger.info('LDAP server closed.');
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
public async stop(): Promise<void> {
|
public async stop(): Promise<void> {
|
||||||
await new Promise(resolve => {
|
await new Promise<void>(resolve => {
|
||||||
if (this.server) {
|
if (this.server) {
|
||||||
this.server.close(() => {
|
this.server.close(() => {
|
||||||
resolve();
|
resolve();
|
||||||
|
@ -1,6 +0,0 @@
|
|||||||
import {MailTemplate} from "wms-core/Mail";
|
|
||||||
|
|
||||||
export const ADD_RECOVERY_EMAIL_MAIL_TEMPLATE: MailTemplate = new MailTemplate(
|
|
||||||
'add_recovery_email',
|
|
||||||
(data) => 'Add ' + data.email + ' as you recovery email.',
|
|
||||||
);
|
|
@ -1,215 +0,0 @@
|
|||||||
import Controller from "wms-core/Controller";
|
|
||||||
import {RequireAuthMiddleware} from "wms-core/auth/AuthComponent";
|
|
||||||
import {Request, Response} from "express";
|
|
||||||
import {ADD_RECOVERY_EMAIL_MAIL_TEMPLATE} from "../Mails";
|
|
||||||
import Validator, {EMAIL_REGEX, InvalidFormatValidationError, ValidationBag} from "wms-core/db/Validator";
|
|
||||||
import MagicLinkController from "./MagicLinkController";
|
|
||||||
import {MagicLinkActionType} from "./MagicLinkActionType";
|
|
||||||
import UserEmail from "wms-core/auth/models/UserEmail";
|
|
||||||
import {BadRequestError, ForbiddenHttpError, NotFoundHttpError, ServerError} from "wms-core/HttpError";
|
|
||||||
import MailDomain from "../models/MailDomain";
|
|
||||||
import UserMailIdentityComponent from "../models/UserMailIdentityComponent";
|
|
||||||
import MailIdentity from "../models/MailIdentity";
|
|
||||||
import UserNameComponent from "../models/UserNameComponent";
|
|
||||||
import {WhereOperator, WhereTest} from "wms-core/db/ModelQuery";
|
|
||||||
import UserPasswordComponent from "../models/UserPasswordComponent";
|
|
||||||
|
|
||||||
export default class AccountController extends Controller {
|
|
||||||
public getRoutesPrefix(): string {
|
|
||||||
return '/account';
|
|
||||||
}
|
|
||||||
|
|
||||||
public routes(): void {
|
|
||||||
this.get('/', this.getAccount, 'account', RequireAuthMiddleware);
|
|
||||||
|
|
||||||
this.post('/change-password', this.postChangePassword, 'change-password', RequireAuthMiddleware);
|
|
||||||
|
|
||||||
this.post('/add-recovery-email', this.addRecoveryEmail, 'add-recovery-email', RequireAuthMiddleware);
|
|
||||||
this.post('/set-main-email', this.postSetMainRecoveryEmail, 'set-main-recovery-email', RequireAuthMiddleware);
|
|
||||||
this.post('/remove-email', this.postRemoveRecoveryEmail, 'remove-recovery-email', RequireAuthMiddleware);
|
|
||||||
|
|
||||||
this.post('/create-mail-identity', this.postCreateMailIdentity, 'create-mail-identity', RequireAuthMiddleware);
|
|
||||||
this.post('/delete-mail-identity', this.postDeleteMailIdentity, 'delete-mail-identity', RequireAuthMiddleware);
|
|
||||||
}
|
|
||||||
|
|
||||||
protected async getAccount(req: Request, res: Response): Promise<void> {
|
|
||||||
const user = req.as(RequireAuthMiddleware).getUser();
|
|
||||||
const userMailIdentity = user.as(UserMailIdentityComponent);
|
|
||||||
|
|
||||||
res.render('account', {
|
|
||||||
main_email: await user.mainEmail.get(),
|
|
||||||
emails: await user.emails.get(),
|
|
||||||
mailboxIdentity: await (await userMailIdentity.mainMailIdentity.get())?.toEmail(),
|
|
||||||
identities: await Promise.all((await userMailIdentity.mailIdentities.get()).map(async identity => ({
|
|
||||||
id: identity.id,
|
|
||||||
email: await identity.toEmail(),
|
|
||||||
}))),
|
|
||||||
domains: (await MailDomain.select()
|
|
||||||
.where('user_id', user.id)
|
|
||||||
.where('user_id', null, WhereTest.EQ, WhereOperator.OR)
|
|
||||||
.sortBy('user_id', 'DESC')
|
|
||||||
.get())
|
|
||||||
.map(d => ({
|
|
||||||
value: d.id,
|
|
||||||
display: d.name,
|
|
||||||
})),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
protected async postChangePassword(req: Request, res: Response): Promise<void> {
|
|
||||||
await this.validate({
|
|
||||||
'current_password': new Validator().defined(),
|
|
||||||
'new_password': new Validator().defined(),
|
|
||||||
'new_password_confirmation': new Validator().sameAs('new_password', req.body.new_password),
|
|
||||||
}, req.body);
|
|
||||||
|
|
||||||
const user = req.as(RequireAuthMiddleware).getUser();
|
|
||||||
if (!await user.as(UserPasswordComponent).verifyPassword(req.body.current_password)) {
|
|
||||||
req.flash('error', 'Invalid current password.');
|
|
||||||
res.redirectBack(Controller.route('account'));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
await user.as(UserPasswordComponent).setPassword(req.body.new_password, 'new_password');
|
|
||||||
await user.save();
|
|
||||||
|
|
||||||
req.flash('success', 'Password change successfully.');
|
|
||||||
res.redirectBack(Controller.route('account'));
|
|
||||||
}
|
|
||||||
|
|
||||||
protected async addRecoveryEmail(req: Request, res: Response): Promise<void> {
|
|
||||||
await this.validate({
|
|
||||||
email: new Validator().defined().regexp(EMAIL_REGEX),
|
|
||||||
}, req.body);
|
|
||||||
|
|
||||||
const email = req.body.email;
|
|
||||||
|
|
||||||
// Existing email
|
|
||||||
if (await UserEmail.select().where('email', email).first()) {
|
|
||||||
const bag = new ValidationBag();
|
|
||||||
const error = new InvalidFormatValidationError('You already have this email.');
|
|
||||||
error.thingName = 'email';
|
|
||||||
bag.addMessage(error);
|
|
||||||
throw bag;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!req.sessionID) throw new ServerError('Session not initialized.');
|
|
||||||
|
|
||||||
await MagicLinkController.sendMagicLink(
|
|
||||||
this.getApp(),
|
|
||||||
req.sessionID,
|
|
||||||
MagicLinkActionType.ADD_RECOVERY_EMAIL,
|
|
||||||
Controller.route('account'),
|
|
||||||
email,
|
|
||||||
ADD_RECOVERY_EMAIL_MAIL_TEMPLATE,
|
|
||||||
{},
|
|
||||||
);
|
|
||||||
|
|
||||||
res.redirect(Controller.route('magic_link_lobby', undefined, {
|
|
||||||
redirect_uri: Controller.route('account'),
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
|
|
||||||
protected async postSetMainRecoveryEmail(req: Request, res: Response): Promise<void> {
|
|
||||||
if (!req.body.id)
|
|
||||||
throw new BadRequestError('Missing id field', 'Check form parameters.', req.url);
|
|
||||||
|
|
||||||
const user = req.as(RequireAuthMiddleware).getUser();
|
|
||||||
|
|
||||||
const userEmail = await UserEmail.getById(req.body.id);
|
|
||||||
if (!userEmail)
|
|
||||||
throw new NotFoundHttpError('email', req.url);
|
|
||||||
if (userEmail.user_id !== user.id)
|
|
||||||
throw new ForbiddenHttpError('email', req.url);
|
|
||||||
if (userEmail.id === user.main_email_id)
|
|
||||||
throw new BadRequestError('This address is already your main address', 'Try refreshing the account page.', req.url);
|
|
||||||
|
|
||||||
user.main_email_id = userEmail.id;
|
|
||||||
await user.save();
|
|
||||||
|
|
||||||
req.flash('success', 'This email was successfully set as your main address.');
|
|
||||||
res.redirectBack();
|
|
||||||
}
|
|
||||||
|
|
||||||
protected async postRemoveRecoveryEmail(req: Request, res: Response): Promise<void> {
|
|
||||||
if (!req.body.id)
|
|
||||||
throw new BadRequestError('Missing id field', 'Check form parameters.', req.url);
|
|
||||||
|
|
||||||
const user = req.as(RequireAuthMiddleware).getUser();
|
|
||||||
|
|
||||||
const userEmail = await UserEmail.getById(req.body.id);
|
|
||||||
if (!userEmail)
|
|
||||||
throw new NotFoundHttpError('email', req.url);
|
|
||||||
if (userEmail.user_id !== user.id)
|
|
||||||
throw new ForbiddenHttpError('email', req.url);
|
|
||||||
if (userEmail.id === user.main_email_id)
|
|
||||||
throw new BadRequestError('Cannot remove main email address', 'Try refreshing the account page.', req.url);
|
|
||||||
|
|
||||||
await userEmail.delete();
|
|
||||||
|
|
||||||
req.flash('success', 'This email was successfully removed from your account.');
|
|
||||||
res.redirectBack();
|
|
||||||
}
|
|
||||||
|
|
||||||
protected async postCreateMailIdentity(req: Request, res: Response): Promise<void> {
|
|
||||||
const domain = await MailDomain.getById(req.body.mail_domain_id);
|
|
||||||
if (!domain) throw new NotFoundHttpError('domain', req.url);
|
|
||||||
|
|
||||||
const user = req.as(RequireAuthMiddleware).getUser();
|
|
||||||
const mailIdentityComponent = user.as(UserMailIdentityComponent);
|
|
||||||
|
|
||||||
const identity = MailIdentity.create({
|
|
||||||
user_id: user.id,
|
|
||||||
name: req.body.name,
|
|
||||||
mail_domain_id: req.body.mail_domain_id,
|
|
||||||
});
|
|
||||||
|
|
||||||
// Check whether this identity can be created by this user
|
|
||||||
if (domain.isPublic()) {
|
|
||||||
await this.validate({
|
|
||||||
name: new Validator<string>().defined().equals(user.as(UserNameComponent).name),
|
|
||||||
}, req.body);
|
|
||||||
const actualPublicAddressesCount = await mailIdentityComponent.getPublicAddressesCount();
|
|
||||||
const maxPublicAddressesCount = mailIdentityComponent.getMaxPublicAddressesCount();
|
|
||||||
if (actualPublicAddressesCount >= maxPublicAddressesCount) {
|
|
||||||
req.flash('error', 'You have reached maximum public email addresses.');
|
|
||||||
res.redirectBack();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
if (!domain.canCreateAddresses(user)) {
|
|
||||||
throw new ForbiddenHttpError('domain', req.url);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Save identity
|
|
||||||
await identity.save();
|
|
||||||
|
|
||||||
// Set main mail identity if not already set
|
|
||||||
if (!mailIdentityComponent.main_mail_identity_id) {
|
|
||||||
mailIdentityComponent.main_mail_identity_id = identity.id;
|
|
||||||
await user.save();
|
|
||||||
req.flash('info', 'Congratulations! You just created your mailbox.');
|
|
||||||
}
|
|
||||||
|
|
||||||
req.flash('success', 'Mail identity ' + await identity.toEmail() + ' successfully created.');
|
|
||||||
res.redirectBack();
|
|
||||||
}
|
|
||||||
|
|
||||||
protected async postDeleteMailIdentity(req: Request, res: Response): Promise<void> {
|
|
||||||
const user = req.as(RequireAuthMiddleware).getUser();
|
|
||||||
|
|
||||||
const identity = await MailIdentity.getById(req.body.id);
|
|
||||||
if (!identity) throw new NotFoundHttpError('Mail identity', req.url);
|
|
||||||
if (identity.user_id !== user.id) throw new ForbiddenHttpError('mail identity', req.url);
|
|
||||||
if (user.as(UserMailIdentityComponent).main_mail_identity_id === identity.id) {
|
|
||||||
req.flash('error', 'Cannot delete your mailbox identity.');
|
|
||||||
res.redirectBack();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
await identity.delete();
|
|
||||||
req.flash('success', 'Identity ' + await identity.toEmail() + ' successfully deleted.');
|
|
||||||
res.redirectBack();
|
|
||||||
}
|
|
||||||
}
|
|
106
src/controllers/AccountMailboxController.ts
Normal file
106
src/controllers/AccountMailboxController.ts
Normal file
@ -0,0 +1,106 @@
|
|||||||
|
import Controller from "swaf/Controller";
|
||||||
|
import {RequireAuthMiddleware} from "swaf/auth/AuthComponent";
|
||||||
|
import {Request, Response} from "express";
|
||||||
|
import Validator from "swaf/db/Validator";
|
||||||
|
import {ForbiddenHttpError, NotFoundHttpError} from "swaf/HttpError";
|
||||||
|
import MailDomain from "../models/MailDomain";
|
||||||
|
import UserMailIdentityComponent from "../models/UserMailIdentityComponent";
|
||||||
|
import MailIdentity from "../models/MailIdentity";
|
||||||
|
import {WhereOperator, WhereTest} from "swaf/db/ModelQuery";
|
||||||
|
import UserNameComponent from "swaf/auth/models/UserNameComponent";
|
||||||
|
|
||||||
|
export default class AccountMailboxController extends Controller {
|
||||||
|
public getRoutesPrefix(): string {
|
||||||
|
return '/account/mailbox';
|
||||||
|
}
|
||||||
|
|
||||||
|
public routes(): void {
|
||||||
|
this.get('/', this.getAccountMailbox, 'account-mailbox', RequireAuthMiddleware);
|
||||||
|
|
||||||
|
this.post('/create-mail-identity', this.postCreateMailIdentity, 'create-mail-identity', RequireAuthMiddleware);
|
||||||
|
this.post('/delete-mail-identity', this.postDeleteMailIdentity, 'delete-mail-identity', RequireAuthMiddleware);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected async getAccountMailbox(req: Request, res: Response): Promise<void> {
|
||||||
|
const user = req.as(RequireAuthMiddleware).getUser();
|
||||||
|
const userMailIdentity = user.as(UserMailIdentityComponent);
|
||||||
|
|
||||||
|
res.render('account-mailbox', {
|
||||||
|
mailboxIdentity: await (await userMailIdentity.mainMailIdentity.get())?.toEmail(),
|
||||||
|
identities: await Promise.all((await userMailIdentity.mailIdentities.get()).map(async identity => ({
|
||||||
|
id: identity.id,
|
||||||
|
email: await identity.toEmail(),
|
||||||
|
}))),
|
||||||
|
domains: (await MailDomain.select()
|
||||||
|
.where('user_id', user.id)
|
||||||
|
.where('user_id', null, WhereTest.EQ, WhereOperator.OR)
|
||||||
|
.sortBy('user_id', 'DESC')
|
||||||
|
.get())
|
||||||
|
.map(d => ({
|
||||||
|
value: d.id,
|
||||||
|
display: d.name,
|
||||||
|
})),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
protected async postCreateMailIdentity(req: Request, res: Response): Promise<void> {
|
||||||
|
const domain = await MailDomain.getById(req.body.mail_domain_id);
|
||||||
|
if (!domain) throw new NotFoundHttpError('domain', req.url);
|
||||||
|
|
||||||
|
const user = req.as(RequireAuthMiddleware).getUser();
|
||||||
|
const mailIdentityComponent = user.as(UserMailIdentityComponent);
|
||||||
|
|
||||||
|
const identity = MailIdentity.create({
|
||||||
|
user_id: user.id,
|
||||||
|
name: req.body.name,
|
||||||
|
mail_domain_id: req.body.mail_domain_id,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Check whether this identity can be created by this user
|
||||||
|
if (!domain.canCreateAddresses(user)) {
|
||||||
|
throw new ForbiddenHttpError('domain', req.url);
|
||||||
|
}
|
||||||
|
if (domain.isPublic()) {
|
||||||
|
await Validator.validate({
|
||||||
|
name: new Validator<string>().defined().equals(user.as(UserNameComponent).getName()),
|
||||||
|
}, req.body);
|
||||||
|
const actualPublicAddressesCount = await mailIdentityComponent.getPublicAddressesCount();
|
||||||
|
const maxPublicAddressesCount = mailIdentityComponent.getMaxPublicAddressesCount();
|
||||||
|
if (maxPublicAddressesCount >= 0 && actualPublicAddressesCount >= maxPublicAddressesCount) {
|
||||||
|
req.flash('error', 'You have reached maximum public email addresses.');
|
||||||
|
res.redirect(Controller.route('account-mailbox'));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Save identity
|
||||||
|
await identity.save();
|
||||||
|
|
||||||
|
// Set main mail identity if not already set
|
||||||
|
if (!mailIdentityComponent.main_mail_identity_id) {
|
||||||
|
mailIdentityComponent.main_mail_identity_id = identity.id;
|
||||||
|
await user.save();
|
||||||
|
req.flash('info', 'Congratulations! You just created your mailbox.');
|
||||||
|
}
|
||||||
|
|
||||||
|
req.flash('success', 'Mail identity ' + await identity.toEmail() + ' successfully created.');
|
||||||
|
res.redirect(Controller.route('account-mailbox'));
|
||||||
|
}
|
||||||
|
|
||||||
|
protected async postDeleteMailIdentity(req: Request, res: Response): Promise<void> {
|
||||||
|
const user = req.as(RequireAuthMiddleware).getUser();
|
||||||
|
|
||||||
|
const identity = await MailIdentity.getById(req.body.id);
|
||||||
|
if (!identity) throw new NotFoundHttpError('Mail identity', req.url);
|
||||||
|
if (identity.user_id !== user.id) throw new ForbiddenHttpError('mail identity', req.url);
|
||||||
|
if (user.as(UserMailIdentityComponent).main_mail_identity_id === identity.id) {
|
||||||
|
req.flash('error', 'Cannot delete your mailbox identity.');
|
||||||
|
res.redirect(Controller.route('account-mailbox'));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
await identity.delete();
|
||||||
|
req.flash('success', 'Identity ' + await identity.toEmail() + ' successfully deleted.');
|
||||||
|
res.redirect(Controller.route('account-mailbox'));
|
||||||
|
}
|
||||||
|
}
|
@ -1,127 +0,0 @@
|
|||||||
import Controller from "wms-core/Controller";
|
|
||||||
import AuthComponent, {RequireAuthMiddleware, RequireGuestMiddleware} from "wms-core/auth/AuthComponent";
|
|
||||||
import {Request, Response} from "express";
|
|
||||||
import Validator, {InvalidFormatValidationError, ValidationBag} from "wms-core/db/Validator";
|
|
||||||
import UserPasswordComponent, {PasswordAuthProof} from "../models/UserPasswordComponent";
|
|
||||||
import UserNameComponent, {USERNAME_REGEXP} from "../models/UserNameComponent";
|
|
||||||
import _AuthController from "wms-core/auth/AuthController";
|
|
||||||
import {NotFoundHttpError, ServerError} from "wms-core/HttpError";
|
|
||||||
import {AuthError, PendingApprovalAuthError, RegisterCallback} from "wms-core/auth/AuthGuard";
|
|
||||||
import User from "wms-core/auth/models/User";
|
|
||||||
import Throttler from "wms-core/Throttler";
|
|
||||||
|
|
||||||
export default class AuthController extends _AuthController {
|
|
||||||
public routes(): void {
|
|
||||||
this.get('/login', this.getLogin, 'auth', RequireGuestMiddleware);
|
|
||||||
this.post('/login', this.postLogin, 'auth', RequireGuestMiddleware);
|
|
||||||
this.get('/register', this.getRegister, 'register', RequireGuestMiddleware);
|
|
||||||
this.post('/register', this.postRegister, 'register', RequireGuestMiddleware);
|
|
||||||
this.post('/logout', this.postLogout, 'logout', RequireAuthMiddleware);
|
|
||||||
}
|
|
||||||
|
|
||||||
protected async getLogin(req: Request, res: Response): Promise<void> {
|
|
||||||
res.render('login');
|
|
||||||
}
|
|
||||||
|
|
||||||
protected async postLogin(req: Request, res: Response): Promise<void> {
|
|
||||||
await this.validate({
|
|
||||||
username: new Validator().defined().exists(User, 'name'),
|
|
||||||
password: new Validator().acceptUndefined(),
|
|
||||||
}, req.body);
|
|
||||||
|
|
||||||
const user = await User.select()
|
|
||||||
.where('name', req.body.username)
|
|
||||||
.first();
|
|
||||||
|
|
||||||
if (!user) throw new NotFoundHttpError(`Couldn't find a user with name ${req.body.username}`, req.url);
|
|
||||||
|
|
||||||
if (!req.session) throw new ServerError('Session not initialized.');
|
|
||||||
|
|
||||||
const passwordAuthProof = PasswordAuthProof.createProofForLogin(req.session);
|
|
||||||
passwordAuthProof.setResource(user);
|
|
||||||
|
|
||||||
await passwordAuthProof.authorize(req.body.password);
|
|
||||||
try {
|
|
||||||
await this.getApp().as(AuthComponent).getAuthGuard().authenticateOrRegister(req.session, passwordAuthProof);
|
|
||||||
} catch (e) {
|
|
||||||
if (e instanceof AuthError) {
|
|
||||||
Throttler.throttle('login_failed_attempts_user', 3, 180000, <string>user.getOrFail('name'), 1000, 60000);
|
|
||||||
Throttler.throttle('login_failed_attempts_ip', 5, 60000, req.ip, 1000, 60000);
|
|
||||||
|
|
||||||
if (e instanceof PendingApprovalAuthError) {
|
|
||||||
req.flash('error', 'Your account is still being reviewed.');
|
|
||||||
res.redirectBack();
|
|
||||||
return;
|
|
||||||
} else {
|
|
||||||
const bag = new ValidationBag();
|
|
||||||
const err = new InvalidFormatValidationError('Invalid password.');
|
|
||||||
err.thingName = 'password';
|
|
||||||
bag.addMessage(err);
|
|
||||||
throw bag;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
throw e;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
req.flash('success', `Welcome, ${user.name}.`);
|
|
||||||
res.redirect(Controller.route('home'));
|
|
||||||
}
|
|
||||||
|
|
||||||
protected async getRegister(req: Request, res: Response): Promise<void> {
|
|
||||||
res.render('register');
|
|
||||||
}
|
|
||||||
|
|
||||||
protected async postRegister(req: Request, res: Response): Promise<void> {
|
|
||||||
Throttler.throttle('register_password', 10, 30000, req.ip);
|
|
||||||
|
|
||||||
await this.validate({
|
|
||||||
username: new Validator().defined().between(3, 64).regexp(USERNAME_REGEXP).unique(User, 'name'),
|
|
||||||
password: new Validator().defined().minLength(8),
|
|
||||||
password_confirmation: new Validator().defined().sameAs('password', req.body.password),
|
|
||||||
terms: new Validator().defined(),
|
|
||||||
}, req.body);
|
|
||||||
|
|
||||||
if(!req.session) throw new ServerError('Session not initialized.');
|
|
||||||
|
|
||||||
const passwordAuthProof = PasswordAuthProof.createAuthorizedProofForRegistration(req.session);
|
|
||||||
try {
|
|
||||||
await this.getApp().as(AuthComponent).getAuthGuard().authenticateOrRegister(req.session, passwordAuthProof,
|
|
||||||
undefined, async (connection, user) => {
|
|
||||||
const callbacks: RegisterCallback[] = [];
|
|
||||||
|
|
||||||
// Password
|
|
||||||
await user.as(UserPasswordComponent).setPassword(req.body.password);
|
|
||||||
|
|
||||||
// Username
|
|
||||||
user.as(UserNameComponent).name = req.body.username;
|
|
||||||
|
|
||||||
return callbacks;
|
|
||||||
}, async (connection, user) => {
|
|
||||||
passwordAuthProof.setResource(user);
|
|
||||||
return [];
|
|
||||||
});
|
|
||||||
} catch (e) {
|
|
||||||
if (e instanceof PendingApprovalAuthError) {
|
|
||||||
req.flash('info', `Your account was successfully created and is pending review from an administrator.`);
|
|
||||||
res.redirect(Controller.route('home'));
|
|
||||||
return;
|
|
||||||
} else {
|
|
||||||
throw e;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const user = await passwordAuthProof.getResource();
|
|
||||||
|
|
||||||
req.flash('success', `Your account was successfully created! Welcome, ${user?.as(UserNameComponent).name}.`);
|
|
||||||
res.redirect(Controller.route('home'));
|
|
||||||
}
|
|
||||||
|
|
||||||
protected async getCheckAuth(): Promise<void> {
|
|
||||||
throw new ServerError('Not implemented.');
|
|
||||||
}
|
|
||||||
|
|
||||||
protected async postAuth(): Promise<void> {
|
|
||||||
throw new ServerError('Not implemented.');
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,4 +1,4 @@
|
|||||||
import Controller from "wms-core/Controller";
|
import Controller from "swaf/Controller";
|
||||||
import {Request, Response} from "express";
|
import {Request, Response} from "express";
|
||||||
import config from "config";
|
import config from "config";
|
||||||
|
|
||||||
@ -20,9 +20,9 @@ export default class HomeController extends Controller {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* This is to test and assert that wms-core extended types are available
|
* This is to test and assert that swaf extended types are available
|
||||||
*/
|
*/
|
||||||
protected async goBack(req: Request, res: Response): Promise<void> {
|
protected async goBack(req: Request, res: Response): Promise<void> {
|
||||||
res.redirectBack();
|
res.redirect(req.getPreviousUrl() || Controller.route('home'));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,3 +0,0 @@
|
|||||||
export enum MagicLinkActionType {
|
|
||||||
ADD_RECOVERY_EMAIL = 'Add a recovery email',
|
|
||||||
}
|
|
@ -1,51 +0,0 @@
|
|||||||
import _MagicLinkController from "wms-core/auth/magic_link/MagicLinkController";
|
|
||||||
import MagicLink from "wms-core/auth/models/MagicLink";
|
|
||||||
import {Request, Response} from "express";
|
|
||||||
import MagicLinkWebSocketListener from "wms-core/auth/magic_link/MagicLinkWebSocketListener";
|
|
||||||
import {MagicLinkActionType} from "./MagicLinkActionType";
|
|
||||||
import Controller from "wms-core/Controller";
|
|
||||||
import {BadOwnerMagicLink} from "wms-core/auth/magic_link/MagicLinkAuthController";
|
|
||||||
import UserEmail from "wms-core/auth/models/UserEmail";
|
|
||||||
import App from "../App";
|
|
||||||
import AuthComponent from "wms-core/auth/AuthComponent";
|
|
||||||
|
|
||||||
export default class MagicLinkController extends _MagicLinkController<App> {
|
|
||||||
public constructor(magicLinkWebSocketListener: MagicLinkWebSocketListener<App>) {
|
|
||||||
super(magicLinkWebSocketListener);
|
|
||||||
}
|
|
||||||
|
|
||||||
protected async performAction(magicLink: MagicLink, req: Request, res: Response): Promise<void> {
|
|
||||||
if (magicLink.action_type === MagicLinkActionType.ADD_RECOVERY_EMAIL) {
|
|
||||||
if (!req.session || !req.sessionID || magicLink.session_id !== req.sessionID) throw new BadOwnerMagicLink();
|
|
||||||
|
|
||||||
await magicLink.delete();
|
|
||||||
|
|
||||||
const authGuard = this.getApp().as(AuthComponent).getAuthGuard();
|
|
||||||
const proof = await authGuard.isAuthenticated(req.session);
|
|
||||||
const user = await proof?.getResource();
|
|
||||||
if (!user) return;
|
|
||||||
|
|
||||||
const email = await magicLink.getOrFail('email');
|
|
||||||
if (await UserEmail.select().with('user').where('email', email).first()) {
|
|
||||||
req.flash('error', 'An account already exists with this email address. Please first remove it there before adding it here.');
|
|
||||||
res.redirect(Controller.route('account'));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const userEmail = UserEmail.create({
|
|
||||||
user_id: user.id,
|
|
||||||
email: email,
|
|
||||||
main: false,
|
|
||||||
});
|
|
||||||
await userEmail.save();
|
|
||||||
|
|
||||||
if (!user.main_email_id) {
|
|
||||||
user.main_email_id = userEmail.id;
|
|
||||||
await user.save();
|
|
||||||
}
|
|
||||||
|
|
||||||
req.flash('success', `Recovery email ${userEmail.email} successfully added.`);
|
|
||||||
res.redirect(Controller.route('account'));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,4 +1,4 @@
|
|||||||
import Controller from "wms-core/Controller";
|
import Controller from "swaf/Controller";
|
||||||
import {Request, Response} from "express";
|
import {Request, Response} from "express";
|
||||||
import MailDomain from "../models/MailDomain";
|
import MailDomain from "../models/MailDomain";
|
||||||
import config from "config";
|
import config from "config";
|
||||||
|
@ -1,12 +1,12 @@
|
|||||||
import Controller from "wms-core/Controller";
|
import Controller from "swaf/Controller";
|
||||||
import BackendController from "wms-core/helpers/BackendController";
|
import BackendController from "swaf/helpers/BackendController";
|
||||||
import {RequireAdminMiddleware, RequireAuthMiddleware} from "wms-core/auth/AuthComponent";
|
import {RequireAdminMiddleware, RequireAuthMiddleware} from "swaf/auth/AuthComponent";
|
||||||
import {Request, Response} from "express";
|
import {Request, Response} from "express";
|
||||||
import User from "wms-core/auth/models/User";
|
import User from "swaf/auth/models/User";
|
||||||
import {NotFoundHttpError} from "wms-core/HttpError";
|
import {NotFoundHttpError} from "swaf/HttpError";
|
||||||
import Validator from "wms-core/db/Validator";
|
import Validator from "swaf/db/Validator";
|
||||||
import UserPasswordComponent from "../../models/UserPasswordComponent";
|
import UserPasswordComponent from "swaf/auth/password/UserPasswordComponent";
|
||||||
import UserNameComponent from "../../models/UserNameComponent";
|
import UserNameComponent from "swaf/auth/models/UserNameComponent";
|
||||||
|
|
||||||
export default class AccountBackendController extends Controller {
|
export default class AccountBackendController extends Controller {
|
||||||
|
|
||||||
@ -51,7 +51,7 @@ export default class AccountBackendController extends Controller {
|
|||||||
const user = await User.getById(req.params.user_id);
|
const user = await User.getById(req.params.user_id);
|
||||||
if (!user) throw new NotFoundHttpError('user', req.url);
|
if (!user) throw new NotFoundHttpError('user', req.url);
|
||||||
|
|
||||||
await this.validate({
|
await Validator.validate({
|
||||||
'new_password': new Validator().defined(),
|
'new_password': new Validator().defined(),
|
||||||
'new_password_confirmation': new Validator().sameAs('new_password', req.body.new_password),
|
'new_password_confirmation': new Validator().sameAs('new_password', req.body.new_password),
|
||||||
}, req.body);
|
}, req.body);
|
||||||
@ -59,7 +59,7 @@ export default class AccountBackendController extends Controller {
|
|||||||
await user.as(UserPasswordComponent).setPassword(req.body.new_password, 'new_password');
|
await user.as(UserPasswordComponent).setPassword(req.body.new_password, 'new_password');
|
||||||
await user.save();
|
await user.save();
|
||||||
|
|
||||||
req.flash('success', `New password set for ${user.as(UserNameComponent).name}`);
|
req.flash('success', `New password set for ${user.as(UserNameComponent).getName()}`);
|
||||||
res.redirect(Controller.route('backend-list-users'));
|
res.redirect(Controller.route('backend-list-users'));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,18 +1,23 @@
|
|||||||
import Controller from "wms-core/Controller";
|
import Controller from "swaf/Controller";
|
||||||
import {Request, Response} from "express";
|
import {Request, Response} from "express";
|
||||||
import User from "wms-core/auth/models/User";
|
import User from "swaf/auth/models/User";
|
||||||
import {WhereTest} from "wms-core/db/ModelQuery";
|
import {WhereTest} from "swaf/db/ModelQuery";
|
||||||
import UserNameComponent from "../../models/UserNameComponent";
|
|
||||||
import UserMailIdentityComponent from "../../models/UserMailIdentityComponent";
|
import UserMailIdentityComponent from "../../models/UserMailIdentityComponent";
|
||||||
import {NotFoundHttpError} from "wms-core/HttpError";
|
import {NotFoundHttpError, ServerError} from "swaf/HttpError";
|
||||||
import MailDomain from "../../models/MailDomain";
|
import MailDomain from "../../models/MailDomain";
|
||||||
import BackendController from "wms-core/helpers/BackendController";
|
import BackendController from "swaf/helpers/BackendController";
|
||||||
import MailIdentity from "../../models/MailIdentity";
|
import MailIdentity from "../../models/MailIdentity";
|
||||||
import {RequireAdminMiddleware, RequireAuthMiddleware} from "wms-core/auth/AuthComponent";
|
import {RequireAdminMiddleware, RequireAuthMiddleware} from "swaf/auth/AuthComponent";
|
||||||
|
import UserNameComponent from "swaf/auth/models/UserNameComponent";
|
||||||
|
|
||||||
export default class MailboxBackendController extends Controller {
|
export default class MailboxBackendController extends Controller {
|
||||||
public constructor() {
|
public constructor() {
|
||||||
super();
|
super();
|
||||||
|
BackendController.registerMenuElement({
|
||||||
|
getLink: async () => Controller.route('backend-mail-domains'),
|
||||||
|
getDisplayString: async () => 'Mail domains',
|
||||||
|
getDisplayIcon: async () => 'globe',
|
||||||
|
});
|
||||||
BackendController.registerMenuElement({
|
BackendController.registerMenuElement({
|
||||||
getLink: async () => Controller.route('backend-mailboxes'),
|
getLink: async () => Controller.route('backend-mailboxes'),
|
||||||
getDisplayString: async () => 'Mailboxes',
|
getDisplayString: async () => 'Mailboxes',
|
||||||
@ -26,23 +31,20 @@ export default class MailboxBackendController extends Controller {
|
|||||||
|
|
||||||
public routes(): void {
|
public routes(): void {
|
||||||
this.get('/', this.getMailboxesBackend, 'backend-mailboxes', RequireAuthMiddleware, RequireAdminMiddleware);
|
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.post('/add-domain', this.postAddDomain, 'backend-add-domain', RequireAuthMiddleware, RequireAdminMiddleware);
|
||||||
this.get('/edit-domain/:id', this.getEditDomain, 'backend-edit-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('/edit-domain/:id', this.postEditDomain, 'backend-edit-domain', RequireAuthMiddleware, RequireAdminMiddleware);
|
||||||
this.post('/remove-domain', this.postRemoveDomain, 'backend-remove-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('/: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);
|
this.post('/delete-mail-identity', this.postDeleteMailIdentity, 'backend-delete-mail-identity', RequireAuthMiddleware, RequireAdminMiddleware);
|
||||||
}
|
}
|
||||||
|
|
||||||
protected async getMailboxesBackend(req: Request, res: Response): Promise<void> {
|
protected async getMailboxesBackend(req: Request, res: Response): Promise<void> {
|
||||||
const mailDomains = await MailDomain.select()
|
|
||||||
.with('owner')
|
|
||||||
.with('identities')
|
|
||||||
.get();
|
|
||||||
|
|
||||||
const users = await User.select()
|
const users = await User.select()
|
||||||
.where('main_mail_identity_id', null, WhereTest.NE)
|
.where('main_mail_identity_id', null, WhereTest.NE)
|
||||||
.with('mainMailIdentity')
|
.with('mainMailIdentity')
|
||||||
@ -50,22 +52,9 @@ export default class MailboxBackendController extends Controller {
|
|||||||
.get();
|
.get();
|
||||||
|
|
||||||
res.render('backend/mailboxes', {
|
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',
|
|
||||||
}, ...(await User.select().get()).map(u => ({
|
|
||||||
value: u.id,
|
|
||||||
display: u.name,
|
|
||||||
}))],
|
|
||||||
mailboxes: await Promise.all(users.map(async user => ({
|
mailboxes: await Promise.all(users.map(async user => ({
|
||||||
id: user.id,
|
id: user.id,
|
||||||
username: user.as(UserNameComponent).name,
|
username: user.as(UserNameComponent).getName(),
|
||||||
name: await (await user.as(UserMailIdentityComponent).mainMailIdentity.get())?.toEmail(),
|
name: await (await user.as(UserMailIdentityComponent).mainMailIdentity.get())?.toEmail(),
|
||||||
identity_count: (await user.as(UserMailIdentityComponent).mailIdentities.get()).length,
|
identity_count: (await user.as(UserMailIdentityComponent).mailIdentities.get()).length,
|
||||||
domain_count: (await user.as(UserMailIdentityComponent).mailDomains.get()).length,
|
domain_count: (await user.as(UserMailIdentityComponent).mailDomains.get()).length,
|
||||||
@ -86,8 +75,9 @@ export default class MailboxBackendController extends Controller {
|
|||||||
res.render('backend/mailbox', {
|
res.render('backend/mailbox', {
|
||||||
mailbox: {
|
mailbox: {
|
||||||
id: user.id,
|
id: user.id,
|
||||||
userName: user.as(UserNameComponent).name,
|
userName: user.as(UserNameComponent).getName(),
|
||||||
name: await mainMailIdentity?.toEmail() || 'Not created.',
|
name: await mainMailIdentity?.toEmail() || 'Not created.',
|
||||||
|
exists: !!mainMailIdentity,
|
||||||
},
|
},
|
||||||
domains: mailDomains.map(d => ({
|
domains: mailDomains.map(d => ({
|
||||||
display: d.name,
|
display: d.name,
|
||||||
@ -100,11 +90,34 @@ 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).getName(),
|
||||||
|
identity_count: (await domain.identities.get()).length,
|
||||||
|
}))),
|
||||||
|
users: [{
|
||||||
|
value: 0,
|
||||||
|
display: 'Public',
|
||||||
|
}, ...(await User.select().get()).map(u => ({
|
||||||
|
value: u.id,
|
||||||
|
display: u.name,
|
||||||
|
}))],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
protected async postAddDomain(req: Request, res: Response): Promise<void> {
|
protected async postAddDomain(req: Request, res: Response): Promise<void> {
|
||||||
const domain = MailDomain.create(req.body);
|
const domain = MailDomain.create(req.body);
|
||||||
await domain.save();
|
await domain.save();
|
||||||
req.flash('success', `Domain ${domain.name} successfully added with owner ${(await domain.owner.get())?.name}`);
|
req.flash('success', `Domain ${domain.name} successfully added with owner ${(await domain.owner.get())?.name}`);
|
||||||
res.redirectBack();
|
res.redirect(Controller.route('backend-edit-domain', domain.id));
|
||||||
}
|
}
|
||||||
|
|
||||||
protected async getEditDomain(req: Request, res: Response): Promise<void> {
|
protected async getEditDomain(req: Request, res: Response): Promise<void> {
|
||||||
@ -133,7 +146,7 @@ export default class MailboxBackendController extends Controller {
|
|||||||
await domain.save();
|
await domain.save();
|
||||||
|
|
||||||
req.flash('success', `Domain ${domain.name} updated successfully.`);
|
req.flash('success', `Domain ${domain.name} updated successfully.`);
|
||||||
res.redirectBack();
|
res.redirect(Controller.route('backend-edit-domain', domain.id));
|
||||||
}
|
}
|
||||||
|
|
||||||
protected async postRemoveDomain(req: Request, res: Response): Promise<void> {
|
protected async postRemoveDomain(req: Request, res: Response): Promise<void> {
|
||||||
@ -146,14 +159,14 @@ export default class MailboxBackendController extends Controller {
|
|||||||
// Don't delete that domain if it still has identities
|
// Don't delete that domain if it still has identities
|
||||||
if ((await domain.identities.get()).length > 0) {
|
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).`);
|
req.flash('error', `This domain still has identities. Please remove all of these first (don't forget to rename mailboxes).`);
|
||||||
res.redirectBack();
|
res.redirect(Controller.route('backend-edit-domain', domain.id));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
await domain.delete();
|
await domain.delete();
|
||||||
|
|
||||||
req.flash('success', `Domain ${domain.name} successfully deleted.`);
|
req.flash('success', `Domain ${domain.name} successfully deleted.`);
|
||||||
res.redirectBack();
|
res.redirect(Controller.route('backend-mailboxes'));
|
||||||
}
|
}
|
||||||
|
|
||||||
protected async postCreateMailIdentity(req: Request, res: Response): Promise<void> {
|
protected async postCreateMailIdentity(req: Request, res: Response): Promise<void> {
|
||||||
@ -184,7 +197,7 @@ export default class MailboxBackendController extends Controller {
|
|||||||
}
|
}
|
||||||
|
|
||||||
req.flash('success', 'Mail identity ' + await identity.toEmail() + ' successfully created.');
|
req.flash('success', 'Mail identity ' + await identity.toEmail() + ' successfully created.');
|
||||||
res.redirectBack();
|
res.redirect(Controller.route('backend-mailbox', user.id));
|
||||||
}
|
}
|
||||||
|
|
||||||
protected async postDeleteMailIdentity(req: Request, res: Response): Promise<void> {
|
protected async postDeleteMailIdentity(req: Request, res: Response): Promise<void> {
|
||||||
@ -194,15 +207,38 @@ export default class MailboxBackendController extends Controller {
|
|||||||
.first();
|
.first();
|
||||||
if (!identity) throw new NotFoundHttpError('Mail identity', req.url);
|
if (!identity) throw new NotFoundHttpError('Mail identity', req.url);
|
||||||
|
|
||||||
const user = await identity.user.get();
|
const user = identity.user.getOrFail();
|
||||||
if (user?.as(UserMailIdentityComponent).main_mail_identity_id === identity.id) {
|
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.`);
|
req.flash('error', `Cannot delete this user's mailbox identity.`);
|
||||||
res.redirectBack();
|
res.redirect(Controller.route('backend-mailbox', user.id));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
await identity.delete();
|
await identity.delete();
|
||||||
req.flash('success', 'Identity ' + await identity.toEmail() + ' successfully deleted.');
|
req.flash('success', 'Identity ' + await identity.toEmail() + ' successfully deleted.');
|
||||||
res.redirectBack();
|
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));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
12
src/main.ts
12
src/main.ts
@ -1,20 +1,20 @@
|
|||||||
import {delimiter} from "path";
|
import {delimiter} from "path";
|
||||||
|
|
||||||
// Load config from specified path or default + wms-core/config (default defaults)
|
// Load config from specified path or default + swaf/config (default defaults)
|
||||||
process.env['NODE_CONFIG_DIR'] =
|
process.env['NODE_CONFIG_DIR'] =
|
||||||
__dirname + '/../../node_modules/wms-core/config/'
|
__dirname + '/../node_modules/swaf/config/'
|
||||||
+ delimiter
|
+ delimiter
|
||||||
+ (process.env['NODE_CONFIG_DIR'] || __dirname + '/../../config/');
|
+ (process.env['NODE_CONFIG_DIR'] || __dirname + '/../config/');
|
||||||
|
|
||||||
import {log} from "wms-core/Logger";
|
import {logger} from "swaf/Logger";
|
||||||
import App from "./App";
|
import App from "./App";
|
||||||
import config from "config";
|
import config from "config";
|
||||||
|
|
||||||
(async () => {
|
(async () => {
|
||||||
log.debug('Config path:', process.env['NODE_CONFIG_DIR']);
|
logger.debug('Config path:', process.env['NODE_CONFIG_DIR']);
|
||||||
|
|
||||||
const app = new App(config.get<string>('listen_addr'), config.get<number>('port'));
|
const app = new App(config.get<string>('listen_addr'), config.get<number>('port'));
|
||||||
await app.start();
|
await app.start();
|
||||||
})().catch(err => {
|
})().catch(err => {
|
||||||
log.error(err);
|
logger.error(err);
|
||||||
});
|
});
|
||||||
|
@ -1,20 +0,0 @@
|
|||||||
import Migration from "wms-core/db/Migration";
|
|
||||||
import ModelFactory from "wms-core/db/ModelFactory";
|
|
||||||
import User from "wms-core/auth/models/User";
|
|
||||||
import UserNameComponent from "../models/UserNameComponent";
|
|
||||||
import {Connection} from "mysql";
|
|
||||||
|
|
||||||
export default class AddNameToUsers extends Migration {
|
|
||||||
public async install(connection: Connection): Promise<void> {
|
|
||||||
await this.query(`ALTER TABLE users
|
|
||||||
ADD COLUMN name VARCHAR(64) UNIQUE NOT NULL`, connection);
|
|
||||||
}
|
|
||||||
|
|
||||||
public async rollback(connection: Connection): Promise<void> {
|
|
||||||
await this.query('ALTER TABLE users DROP COLUMN name', connection);
|
|
||||||
}
|
|
||||||
|
|
||||||
public registerModels(): void {
|
|
||||||
ModelFactory.get(User).addComponent(UserNameComponent);
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,21 +0,0 @@
|
|||||||
import Migration from "wms-core/db/Migration";
|
|
||||||
import {Connection} from "mysql";
|
|
||||||
import ModelFactory from "wms-core/db/ModelFactory";
|
|
||||||
import User from "wms-core/auth/models/User";
|
|
||||||
import UserPasswordComponent from "../models/UserPasswordComponent";
|
|
||||||
|
|
||||||
export default class AddPasswordToUsers extends Migration {
|
|
||||||
public async install(connection: Connection): Promise<void> {
|
|
||||||
await this.query(`ALTER TABLE users
|
|
||||||
ADD COLUMN password VARCHAR(128) NOT NULL`, connection);
|
|
||||||
}
|
|
||||||
|
|
||||||
public async rollback(connection: Connection): Promise<void> {
|
|
||||||
await this.query(`ALTER TABLE users
|
|
||||||
DROP COLUMN password`, connection);
|
|
||||||
}
|
|
||||||
|
|
||||||
public registerModels(): void {
|
|
||||||
ModelFactory.get(User).addComponent(UserPasswordComponent);
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,13 +1,12 @@
|
|||||||
import Migration from "wms-core/db/Migration";
|
import Migration from "swaf/db/Migration";
|
||||||
import {Connection} from "mysql";
|
import ModelFactory from "swaf/db/ModelFactory";
|
||||||
import ModelFactory from "wms-core/db/ModelFactory";
|
|
||||||
import MailDomain from "../models/MailDomain";
|
import MailDomain from "../models/MailDomain";
|
||||||
import MailIdentity from "../models/MailIdentity";
|
import MailIdentity from "../models/MailIdentity";
|
||||||
import User from "wms-core/auth/models/User";
|
import User from "swaf/auth/models/User";
|
||||||
import UserMailIdentityComponent from "../models/UserMailIdentityComponent";
|
import UserMailIdentityComponent from "../models/UserMailIdentityComponent";
|
||||||
|
|
||||||
export default class CreateMailTables extends Migration {
|
export default class CreateMailTablesMigration extends Migration {
|
||||||
public async install(connection: Connection): Promise<void> {
|
public async install(): Promise<void> {
|
||||||
await this.query(`CREATE TABLE mail_domains
|
await this.query(`CREATE TABLE mail_domains
|
||||||
(
|
(
|
||||||
id INT NOT NULL AUTO_INCREMENT,
|
id INT NOT NULL AUTO_INCREMENT,
|
||||||
@ -15,7 +14,7 @@ export default class CreateMailTables extends Migration {
|
|||||||
user_id INT,
|
user_id INT,
|
||||||
PRIMARY KEY (id),
|
PRIMARY KEY (id),
|
||||||
FOREIGN KEY (user_id) REFERENCES users (id) ON DELETE SET NULL
|
FOREIGN KEY (user_id) REFERENCES users (id) ON DELETE SET NULL
|
||||||
)`, connection);
|
)`);
|
||||||
await this.query(`CREATE TABLE mail_identities
|
await this.query(`CREATE TABLE mail_identities
|
||||||
(
|
(
|
||||||
id INT NOT NULL AUTO_INCREMENT,
|
id INT NOT NULL AUTO_INCREMENT,
|
||||||
@ -27,17 +26,17 @@ export default class CreateMailTables extends Migration {
|
|||||||
UNIQUE (mail_domain_id, name),
|
UNIQUE (mail_domain_id, name),
|
||||||
FOREIGN KEY (user_id) REFERENCES users (id) ON DELETE CASCADE,
|
FOREIGN KEY (user_id) REFERENCES users (id) ON DELETE CASCADE,
|
||||||
FOREIGN KEY (mail_domain_id) REFERENCES mail_domains (id) ON DELETE CASCADE
|
FOREIGN KEY (mail_domain_id) REFERENCES mail_domains (id) ON DELETE CASCADE
|
||||||
)`, connection);
|
)`);
|
||||||
await this.query(`ALTER TABLE users
|
await this.query(`ALTER TABLE users
|
||||||
ADD COLUMN main_mail_identity_id INT,
|
ADD COLUMN main_mail_identity_id INT,
|
||||||
ADD FOREIGN KEY main_mail_identity_fk (main_mail_identity_id) REFERENCES mail_identities (id)`, connection);
|
ADD FOREIGN KEY main_mail_identity_fk (main_mail_identity_id) REFERENCES mail_identities (id)`);
|
||||||
}
|
}
|
||||||
|
|
||||||
public async rollback(connection: Connection): Promise<void> {
|
public async rollback(): Promise<void> {
|
||||||
await this.query(`ALTER TABLE users
|
await this.query(`ALTER TABLE users
|
||||||
DROP FOREIGN KEY main_mail_identity_fk,
|
DROP FOREIGN KEY main_mail_identity_fk,
|
||||||
DROP COLUMN main_mail_identity_id`, connection);
|
DROP COLUMN main_mail_identity_id`);
|
||||||
await this.query(`DROP TABLE IF EXISTS mail_identities, mail_domains`, connection);
|
await this.query(`DROP TABLE IF EXISTS mail_identities, mail_domains`);
|
||||||
}
|
}
|
||||||
|
|
||||||
public registerModels(): void {
|
public registerModels(): void {
|
@ -1,7 +1,8 @@
|
|||||||
import Model from "wms-core/db/Model";
|
import Model from "swaf/db/Model";
|
||||||
import User from "wms-core/auth/models/User";
|
import User from "swaf/auth/models/User";
|
||||||
import {ManyModelRelation, OneModelRelation} from "wms-core/db/ModelRelation";
|
import {ManyModelRelation, OneModelRelation} from "swaf/db/ModelRelation";
|
||||||
import MailIdentity from "./MailIdentity";
|
import MailIdentity from "./MailIdentity";
|
||||||
|
import UserNameComponent from "swaf/auth/models/UserNameComponent";
|
||||||
|
|
||||||
export default class MailDomain extends Model {
|
export default class MailDomain extends Model {
|
||||||
public id?: number = undefined;
|
public id?: number = undefined;
|
||||||
@ -38,6 +39,6 @@ export default class MailDomain extends Model {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public canCreateAddresses(user: User): boolean {
|
public canCreateAddresses(user: User): boolean {
|
||||||
return this.user_id === user.id || this.isPublic();
|
return this.user_id === user.id || this.isPublic() && user.as(UserNameComponent).hasName();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,8 +1,8 @@
|
|||||||
import Model from "wms-core/db/Model";
|
import Model from "swaf/db/Model";
|
||||||
import User from "wms-core/auth/models/User";
|
import User from "swaf/auth/models/User";
|
||||||
import MailDomain from "./MailDomain";
|
import MailDomain from "./MailDomain";
|
||||||
import {OneModelRelation} from "wms-core/db/ModelRelation";
|
import {OneModelRelation} from "swaf/db/ModelRelation";
|
||||||
import {EMAIL_REGEX} from "wms-core/db/Validator";
|
import {EMAIL_REGEX} from "swaf/db/Validator";
|
||||||
|
|
||||||
export default class MailIdentity extends Model {
|
export default class MailIdentity extends Model {
|
||||||
public static get table(): string {
|
public static get table(): string {
|
||||||
|
@ -1,8 +1,9 @@
|
|||||||
import ModelComponent from "wms-core/db/ModelComponent";
|
import ModelComponent from "swaf/db/ModelComponent";
|
||||||
import User from "wms-core/auth/models/User";
|
import User from "swaf/auth/models/User";
|
||||||
import {ManyModelRelation, OneModelRelation} from "wms-core/db/ModelRelation";
|
import {ManyModelRelation, OneModelRelation} from "swaf/db/ModelRelation";
|
||||||
import MailIdentity from "./MailIdentity";
|
import MailIdentity from "./MailIdentity";
|
||||||
import MailDomain from "./MailDomain";
|
import MailDomain from "./MailDomain";
|
||||||
|
import config from "config";
|
||||||
|
|
||||||
export default class UserMailIdentityComponent extends ModelComponent<User> {
|
export default class UserMailIdentityComponent extends ModelComponent<User> {
|
||||||
public main_mail_identity_id?: number = undefined;
|
public main_mail_identity_id?: number = undefined;
|
||||||
@ -27,7 +28,7 @@ export default class UserMailIdentityComponent extends ModelComponent<User> {
|
|||||||
});
|
});
|
||||||
|
|
||||||
public getMaxPublicAddressesCount(): number {
|
public getMaxPublicAddressesCount(): number {
|
||||||
return 1;
|
return config.get<number>('mail.max_public_identities_per_user');
|
||||||
}
|
}
|
||||||
|
|
||||||
public async getPublicAddressesCount(): Promise<number> {
|
public async getPublicAddressesCount(): Promise<number> {
|
||||||
|
@ -1,13 +0,0 @@
|
|||||||
import User from "wms-core/auth/models/User";
|
|
||||||
import ModelComponent from "wms-core/db/ModelComponent";
|
|
||||||
|
|
||||||
export const USERNAME_REGEXP = /^[0-9a-z_-]+$/;
|
|
||||||
|
|
||||||
export default class UserNameComponent extends ModelComponent<User> {
|
|
||||||
public name?: string = undefined;
|
|
||||||
|
|
||||||
public init(): void {
|
|
||||||
this.setValidation('name').defined().between(3, 64).regexp(USERNAME_REGEXP).unique(this._model);
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
@ -1,109 +0,0 @@
|
|||||||
import Validator from "wms-core/db/Validator";
|
|
||||||
import User from "wms-core/auth/models/User";
|
|
||||||
import argon2, {argon2id} from "argon2";
|
|
||||||
import AuthProof from "wms-core/auth/AuthProof";
|
|
||||||
import ModelComponent from "wms-core/db/ModelComponent";
|
|
||||||
|
|
||||||
export default class UserPasswordComponent extends ModelComponent<User> {
|
|
||||||
private password?: string = undefined;
|
|
||||||
|
|
||||||
public init(): void {
|
|
||||||
this.setValidation('password').acceptUndefined().maxLength(128);
|
|
||||||
}
|
|
||||||
|
|
||||||
public async setPassword(rawPassword: string, fieldName: string = 'password'): Promise<void> {
|
|
||||||
await new Validator<string>().defined().minLength(8).maxLength(512)
|
|
||||||
.execute(fieldName, rawPassword, true);
|
|
||||||
this.password = await argon2.hash(rawPassword, {
|
|
||||||
timeCost: 10,
|
|
||||||
memoryCost: 65536,
|
|
||||||
parallelism: 4,
|
|
||||||
type: argon2id,
|
|
||||||
hashLength: 32,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
public async verifyPassword(passwordGuess: string): Promise<boolean> {
|
|
||||||
if (!this.password) return false;
|
|
||||||
|
|
||||||
return await argon2.verify(this.password, passwordGuess);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export class PasswordAuthProof implements AuthProof<User> {
|
|
||||||
public static getProofForSession(session: Express.Session): PasswordAuthProof | null {
|
|
||||||
return session.auth_password_proof ? new PasswordAuthProof(session) : null;
|
|
||||||
}
|
|
||||||
|
|
||||||
public static createAuthorizedProofForRegistration(session: Express.Session): PasswordAuthProof {
|
|
||||||
const proofForSession = new PasswordAuthProof(session);
|
|
||||||
proofForSession.authorized = true;
|
|
||||||
proofForSession.forRegistration = true;
|
|
||||||
proofForSession.save();
|
|
||||||
return proofForSession;
|
|
||||||
}
|
|
||||||
|
|
||||||
public static createProofForLogin(session: Express.Session): PasswordAuthProof {
|
|
||||||
return new PasswordAuthProof(session);
|
|
||||||
}
|
|
||||||
|
|
||||||
private readonly session: Express.Session;
|
|
||||||
private authorized: boolean;
|
|
||||||
private forRegistration: boolean = false;
|
|
||||||
private userId: number | null;
|
|
||||||
private userPassword: UserPasswordComponent | null = null;
|
|
||||||
|
|
||||||
private constructor(session: Express.Session) {
|
|
||||||
this.session = session;
|
|
||||||
this.authorized = session.auth_password_proof?.authorized || false;
|
|
||||||
this.forRegistration = session.auth_password_proof?.forRegistration || false;
|
|
||||||
this.userId = session.auth_password_proof?.userId || null;
|
|
||||||
}
|
|
||||||
|
|
||||||
public async getResource(): Promise<User | null> {
|
|
||||||
if (typeof this.userId !== 'number') return null;
|
|
||||||
return await User.getById(this.userId);
|
|
||||||
}
|
|
||||||
|
|
||||||
public setResource(user: User): void {
|
|
||||||
this.userId = user.getOrFail('id');
|
|
||||||
this.save();
|
|
||||||
}
|
|
||||||
|
|
||||||
public async isAuthorized(): Promise<boolean> {
|
|
||||||
return this.authorized;
|
|
||||||
}
|
|
||||||
|
|
||||||
public async isValid(): Promise<boolean> {
|
|
||||||
return (this.forRegistration || Boolean(await this.getResource())) &&
|
|
||||||
await this.isAuthorized();
|
|
||||||
}
|
|
||||||
|
|
||||||
public async revoke(): Promise<void> {
|
|
||||||
this.session.auth_password_proof = undefined;
|
|
||||||
}
|
|
||||||
|
|
||||||
private async getUserPassword(): Promise<UserPasswordComponent | null> {
|
|
||||||
if (!this.userPassword) {
|
|
||||||
this.userPassword = (await User.getById(this.userId))?.as(UserPasswordComponent) || null;
|
|
||||||
}
|
|
||||||
return this.userPassword;
|
|
||||||
}
|
|
||||||
|
|
||||||
public async authorize(passwordGuess: string): Promise<boolean> {
|
|
||||||
const password = await this.getUserPassword();
|
|
||||||
if (!password || !await password.verifyPassword(passwordGuess)) return false;
|
|
||||||
|
|
||||||
this.authorized = true;
|
|
||||||
this.save();
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
private save() {
|
|
||||||
this.session.auth_password_proof = {
|
|
||||||
authorized: this.authorized,
|
|
||||||
forRegistration: this.forRegistration,
|
|
||||||
userId: this.userId,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
@ -2,6 +2,7 @@
|
|||||||
"extends": "./tsconfig.json",
|
"extends": "./tsconfig.json",
|
||||||
"compilerOptions": {
|
"compilerOptions": {
|
||||||
"outDir": "public/js",
|
"outDir": "public/js",
|
||||||
|
"rootDir": "./assets",
|
||||||
"target": "ES6",
|
"target": "ES6",
|
||||||
"strict": true,
|
"strict": true,
|
||||||
"lib": [
|
"lib": [
|
||||||
|
@ -3,6 +3,7 @@
|
|||||||
"module": "CommonJS",
|
"module": "CommonJS",
|
||||||
"esModuleInterop": true,
|
"esModuleInterop": true,
|
||||||
"outDir": "dist",
|
"outDir": "dist",
|
||||||
|
"rootDir": "./src",
|
||||||
"target": "ES6",
|
"target": "ES6",
|
||||||
"strict": true,
|
"strict": true,
|
||||||
"lib": [
|
"lib": [
|
||||||
@ -16,6 +17,6 @@
|
|||||||
},
|
},
|
||||||
"include": [
|
"include": [
|
||||||
"src/**/*",
|
"src/**/*",
|
||||||
"node_modules/wms-core/types"
|
"node_modules/swaf/types"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
@ -11,7 +11,7 @@
|
|||||||
<p>Rainbox Email is a <abbr title="Free and Open Source Software">FOSS</abbr> service that allows anyone to
|
<p>Rainbox Email is a <abbr title="Free and Open Source Software">FOSS</abbr> service that allows anyone to
|
||||||
manage an <abbr title="Internet Service Provider">ISP</abbr>-like mail database.</p>
|
manage an <abbr title="Internet Service Provider">ISP</abbr>-like mail database.</p>
|
||||||
<p>You can find the source code on
|
<p>You can find the source code on
|
||||||
<a href="https://gitlab.com/ArisuOngaku/rainbox.email" target="_blank">GitLab</a>.</p>
|
<a href="https://eternae.ink/arisu/rainbox.email" target="_blank">eternae.ink</a>.</p>
|
||||||
|
|
||||||
{{ macros.message('info', '<a href="' + instance_url + '">' + instance_url + '</a> is just one instance of ' + app.name, true, true) }}
|
{{ macros.message('info', '<a href="' + instance_url + '">' + instance_url + '</a> is just one instance of ' + app.name, true, true) }}
|
||||||
</section>
|
</section>
|
||||||
|
68
views/account-mailbox.njk
Normal file
68
views/account-mailbox.njk
Normal file
@ -0,0 +1,68 @@
|
|||||||
|
{% extends 'layouts/base.njk' %}
|
||||||
|
|
||||||
|
{% set title = app.name + ' - Account' %}
|
||||||
|
|
||||||
|
{% block body %}
|
||||||
|
<main class="container">
|
||||||
|
<h1>My mailbox</h1>
|
||||||
|
|
||||||
|
<section class="panel">
|
||||||
|
<h2><i data-feather="mail"></i> {{ mailboxIdentity | default('Mailbox') }}</h2>
|
||||||
|
|
||||||
|
<p class="center">{% if mailboxIdentity == null %}(Not created yet){% else %}{{ mailboxIdentity }}{% endif %}</p>
|
||||||
|
|
||||||
|
<table class="data-table">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>#</th>
|
||||||
|
<th>Email</th>
|
||||||
|
<th>Actions</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
|
||||||
|
<tbody>
|
||||||
|
{% for identity in identities %}
|
||||||
|
<tr>
|
||||||
|
<td>{{ identity.id }}</td>
|
||||||
|
<td>{{ identity.email }}</td>
|
||||||
|
<td class="actions">
|
||||||
|
<form action="{{ route('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>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
{% else %}
|
||||||
|
<tr>
|
||||||
|
<td colspan="3">No identity (yet).</td>
|
||||||
|
</tr>
|
||||||
|
{% endfor %}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
|
||||||
|
<form action="{{ route('create-mail-identity') }}" method="POST" class="sub-panel">
|
||||||
|
<h3>{% if mailboxIdentity == null %}Create your mailbox{% else %}Create a new mail identity{% endif %}</h3>
|
||||||
|
|
||||||
|
<div class="inline-fields">
|
||||||
|
{{ macros.field(_locals, 'text', 'name', user.name, 'Email name', null, 'required') }}
|
||||||
|
<span>@</span>
|
||||||
|
{{ macros.field(_locals, 'select', 'mail_domain_id', null, 'Choose the email domain', null, 'required', domains) }}
|
||||||
|
</div>
|
||||||
|
<div class="hint">
|
||||||
|
<i data-feather="info"></i>
|
||||||
|
If using a "public" domain, can only be set to your username.
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button><i data-feather="plus"></i> Create</button>
|
||||||
|
|
||||||
|
{{ macros.csrf(getCsrfToken) }}
|
||||||
|
</form>
|
||||||
|
</section>
|
||||||
|
</main>
|
||||||
|
{% endblock %}
|
@ -1,159 +0,0 @@
|
|||||||
{% extends 'layouts/base.njk' %}
|
|
||||||
|
|
||||||
{% set title = app.name + ' - Account' %}
|
|
||||||
|
|
||||||
{% block body %}
|
|
||||||
<main class="container">
|
|
||||||
<h1>My account</h1>
|
|
||||||
|
|
||||||
{% if emails | length <= 0 %}
|
|
||||||
{{ macros.message('warning', 'To avoid losing access to your account, please add a recovery email address.') }}
|
|
||||||
{% endif %}
|
|
||||||
|
|
||||||
<section class="panel">
|
|
||||||
<h2><i data-feather="user"></i> Personnal info</h2>
|
|
||||||
|
|
||||||
<p>Name: {{ user.name }}</p>
|
|
||||||
|
|
||||||
<p>Contact email: {{ main_email.email | default('-') }}</p>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<section class="panel">
|
|
||||||
<h2><i data-feather="key"></i> Change password</h2>
|
|
||||||
|
|
||||||
<form action="{{ route('change-password') }}" method="POST">
|
|
||||||
{{ macros.field(_locals, 'password', 'current_password', null, 'Current password') }}
|
|
||||||
{{ macros.field(_locals, 'password', 'new_password', null, 'New password') }}
|
|
||||||
{{ macros.field(_locals, 'password', 'new_password_confirmation', null, 'New password confirmation') }}
|
|
||||||
|
|
||||||
<button type="submit"><i data-feather="save"></i> Save</button>
|
|
||||||
|
|
||||||
{{ macros.csrf(getCsrfToken) }}
|
|
||||||
</form>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<section class="panel">
|
|
||||||
<h2><i data-feather="shield"></i> Recovery email addresses</h2>
|
|
||||||
|
|
||||||
<table class="data-table">
|
|
||||||
<thead>
|
|
||||||
<tr>
|
|
||||||
<th>Type</th>
|
|
||||||
<th>Address</th>
|
|
||||||
<th>Actions</th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
|
|
||||||
<tbody>
|
|
||||||
{% for email in emails %}
|
|
||||||
{% if email.id == user.main_email_id %}
|
|
||||||
<tr>
|
|
||||||
<td>Main</td>
|
|
||||||
<td>{{ email.email }}</td>
|
|
||||||
<td></td>
|
|
||||||
</tr>
|
|
||||||
{% endif %}
|
|
||||||
{% endfor %}
|
|
||||||
{% for email in emails %}
|
|
||||||
{% if email.id != user.main_email_id %}
|
|
||||||
<tr>
|
|
||||||
<td>Secondary</td>
|
|
||||||
<td>{{ email.email }}</td>
|
|
||||||
<td class="actions">
|
|
||||||
<form action="{{ route('set-main-recovery-email') }}" method="POST">
|
|
||||||
<input type="hidden" name="id" value="{{ email.id }}">
|
|
||||||
|
|
||||||
<button class="warning"
|
|
||||||
onclick="return confirm('Are you sure you want to set {{ email.email }} as your main address?');">
|
|
||||||
<i data-feather="refresh-ccw"></i> <span class="tip">Set as main address</span>
|
|
||||||
</button>
|
|
||||||
{{ macros.csrf(getCsrfToken) }}
|
|
||||||
</form>
|
|
||||||
|
|
||||||
<form action="{{ route('remove-recovery-email') }}" method="POST">
|
|
||||||
<input type="hidden" name="id" value="{{ email.id }}">
|
|
||||||
|
|
||||||
<button class="danger"
|
|
||||||
onclick="return confirm('Are you sure you want to delete {{ email.email }}?');">
|
|
||||||
<i data-feather="trash"></i> <span class="tip">Remove</span>
|
|
||||||
</button>
|
|
||||||
{{ macros.csrf(getCsrfToken) }}
|
|
||||||
</form>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
{% endif %}
|
|
||||||
{% endfor %}
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
|
|
||||||
<form action="{{ route('add-recovery-email') }}" method="POST" class="sub-panel">
|
|
||||||
<h3>Add a recovery email address:</h3>
|
|
||||||
|
|
||||||
{{ macros.field(_locals, 'email', 'email', null, 'Choose a safe email address', 'An email we can use to identify you in case you lose access to your account', 'required') }}
|
|
||||||
|
|
||||||
<button><i data-feather="plus"></i> Add recovery email</button>
|
|
||||||
|
|
||||||
{{ macros.csrf(getCsrfToken) }}
|
|
||||||
</form>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<section class="panel">
|
|
||||||
<h2><i data-feather="mail"></i> Mailbox</h2>
|
|
||||||
|
|
||||||
<p class="center">{% if mailboxIdentity == null %}(Not created yet){% else %}{{ mailboxIdentity }}{% endif %}</p>
|
|
||||||
|
|
||||||
<table class="data-table">
|
|
||||||
<thead>
|
|
||||||
<tr>
|
|
||||||
<th>#</th>
|
|
||||||
<th>Email</th>
|
|
||||||
<th>Actions</th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
|
|
||||||
<tbody>
|
|
||||||
{% for identity in identities %}
|
|
||||||
<tr>
|
|
||||||
<td>{{ identity.id }}</td>
|
|
||||||
<td>{{ identity.email }}</td>
|
|
||||||
<td class="actions">
|
|
||||||
<form action="{{ route('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>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
{% else %}
|
|
||||||
<tr>
|
|
||||||
<td colspan="3">No identity (yet).</td>
|
|
||||||
</tr>
|
|
||||||
{% endfor %}
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
|
|
||||||
<form action="{{ route('create-mail-identity') }}" method="POST" class="sub-panel">
|
|
||||||
<h3>{% if mailboxIdentity == null %}Create your mailbox{% else %}Create a new mail identity{% endif %}</h3>
|
|
||||||
|
|
||||||
<div class="inline-fields">
|
|
||||||
{{ macros.field(_locals, 'text', 'name', user.name, 'Email name', null, 'required') }}
|
|
||||||
<span>@</span>
|
|
||||||
{{ macros.field(_locals, 'select', 'mail_domain_id', null, 'Choose the email domain', null, 'required', domains) }}
|
|
||||||
</div>
|
|
||||||
<div class="hint">
|
|
||||||
<i data-feather="info"></i>
|
|
||||||
If using a "public" domain, can only be set to your username.
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<button><i data-feather="plus"></i> Create</button>
|
|
||||||
|
|
||||||
{{ macros.csrf(getCsrfToken) }}
|
|
||||||
</form>
|
|
||||||
</section>
|
|
||||||
</main>
|
|
||||||
{% endblock %}
|
|
@ -4,9 +4,9 @@
|
|||||||
|
|
||||||
{% block body %}
|
{% block body %}
|
||||||
<div class="container">
|
<div class="container">
|
||||||
{{ macros.breadcrumb('Mail domain: ' + domain.name, [
|
{{ macros.breadcrumb(domain.name, [
|
||||||
{title: 'Backend', link: route('backend')},
|
{title: 'Backend', link: route('backend')},
|
||||||
{title: 'Mailboxes', link: route('backend-mailboxes')}
|
{title: 'Mail domains', link: route('backend-mail-domains')}
|
||||||
]) }}
|
]) }}
|
||||||
|
|
||||||
<div class="panel">
|
<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,6 +28,18 @@
|
|||||||
<td>{{ identity.id }}</td>
|
<td>{{ identity.id }}</td>
|
||||||
<td>{{ identity.email }}</td>
|
<td>{{ identity.email }}</td>
|
||||||
<td class="actions">
|
<td class="actions">
|
||||||
|
{% 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=""
|
||||||
|
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>
|
||||||
|
|
||||||
<form action="{{ route('backend-delete-mail-identity') }}" method="POST">
|
<form action="{{ route('backend-delete-mail-identity') }}" method="POST">
|
||||||
<input type="hidden" name="id" value="{{ identity.id }}">
|
<input type="hidden" name="id" value="{{ identity.id }}">
|
||||||
|
|
||||||
@ -38,6 +50,7 @@
|
|||||||
|
|
||||||
{{ macros.csrf(getCsrfToken) }}
|
{{ macros.csrf(getCsrfToken) }}
|
||||||
</form>
|
</form>
|
||||||
|
{% endif %}
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
@ -45,7 +58,7 @@
|
|||||||
</table>
|
</table>
|
||||||
|
|
||||||
<form action="{{ route('backend-create-mail-identity', mailbox.id) }}" method="POST" class="sub-panel">
|
<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">
|
<div class="inline-fields">
|
||||||
{{ macros.field(_locals, 'text', 'name', user.name, 'Email name', null, 'required') }}
|
{{ macros.field(_locals, 'text', 'name', user.name, 'Email name', null, 'required') }}
|
||||||
|
@ -10,60 +10,6 @@
|
|||||||
|
|
||||||
<h1>Mailbox manager</h1>
|
<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">
|
<section class="panel">
|
||||||
<h2><i data-feather="mail"></i> Mailboxes</h2>
|
<h2><i data-feather="mail"></i> Mailboxes</h2>
|
||||||
|
|
||||||
|
@ -17,24 +17,26 @@
|
|||||||
<button id="menu-button"><i data-feather="menu"></i></button>
|
<button id="menu-button"><i data-feather="menu"></i></button>
|
||||||
<ul id="main-menu">
|
<ul id="main-menu">
|
||||||
<li><a href="{{ app.webmail_url }}" target="_blank"><i data-feather="mail"></i> <span class="tip">Webmail</span></a></li>
|
<li><a href="{{ app.webmail_url }}" target="_blank"><i data-feather="mail"></i> <span class="tip">Webmail</span></a></li>
|
||||||
{# <li><a href="{{ route('about') }}"><i data-feather="info"></i> <span class="tip">About</span></a></li>#}
|
|
||||||
{% if user %}
|
{% if user %}
|
||||||
{% if user.is_admin %}
|
{% if user.is_admin %}
|
||||||
<li><a href="{{ route('backend') }}"><i data-feather="settings"></i> <span class="tip">Backend</span></a></li>
|
<li><a href="{{ route('backend') }}"><i data-feather="settings"></i> <span class="tip">Backend</span></a></li>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
<li><a href="{{ route('account') }}"><i data-feather="user"></i>
|
<li><a href="{{ route('account') }}"><i data-feather="user"></i><span class="tip">{{ user.name }}</span></a>
|
||||||
<span class="tip">{{ user.name }}</span></a></li>
|
|
||||||
|
<ul class="dropdown">
|
||||||
|
<li><a href="{{ route('account-mailbox') }}"><i data-feather="settings"></i> <span class="tip">Mailbox settings</span></a></li>
|
||||||
<li>
|
<li>
|
||||||
|
<hr>
|
||||||
<form action="{{ route('logout') }}" method="POST">
|
<form action="{{ route('logout') }}" method="POST">
|
||||||
<button><i data-feather="log-out"></i> <span class="tip">Logout</span></button>
|
<button><i data-feather="log-out"></i> <span class="tip">Logout</span></button>
|
||||||
{{ macros.csrf(getCsrfToken) }}
|
{{ macros.csrf(getCsrfToken) }}
|
||||||
</form>
|
</form>
|
||||||
</li>
|
</li>
|
||||||
|
</ul>
|
||||||
|
</li>
|
||||||
{% else %}
|
{% else %}
|
||||||
<li><a href="{{ route('auth') }}"><i data-feather="log-in"></i> <span class="tip">Login</span></a></li>
|
<li><a href="{{ route('auth') }}"><i data-feather="log-in"></i> <span class="tip">Login / Register</span></a></li>
|
||||||
<li><a href="{{ route('register') }}"><i data-feather="user-plus"></i>
|
|
||||||
<span class="tip">Register</span></a></li>
|
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</ul>
|
</ul>
|
||||||
</nav>
|
</nav>
|
||||||
|
@ -1,27 +0,0 @@
|
|||||||
{% extends 'mails/base_layout.mjml.njk' %}
|
|
||||||
|
|
||||||
{% block body %}
|
|
||||||
<mj-section>
|
|
||||||
<mj-column>
|
|
||||||
<mj-text mj-class="title">
|
|
||||||
Add this email as recovery for {{ app.name }}
|
|
||||||
</mj-text>
|
|
||||||
<mj-text>
|
|
||||||
Someone wants to add <strong>{{ mail_to }}</strong> as a recovery email to their account.
|
|
||||||
<br><br>
|
|
||||||
<strong>Do not click on this if this is not you!</strong>
|
|
||||||
</mj-text>
|
|
||||||
|
|
||||||
<mj-button href="{{ link | safe }}">
|
|
||||||
Add as recovery email
|
|
||||||
</mj-button>
|
|
||||||
</mj-column>
|
|
||||||
</mj-section>
|
|
||||||
{% endblock %}
|
|
||||||
|
|
||||||
{% block text %}
|
|
||||||
Hi!
|
|
||||||
Someone wants to add {{ mail_to }} as a recovery email to their account.
|
|
||||||
|
|
||||||
To add it as a recovery email, please follow this link: {{ link|safe }}
|
|
||||||
{% endblock %}
|
|
Loading…
Reference in New Issue
Block a user