Compare commits

..

No commits in common. "master" and "v2.4.0" have entirely different histories.

16 changed files with 1987 additions and 1422 deletions

View File

@ -88,7 +88,6 @@
}, },
"ignorePatterns": [ "ignorePatterns": [
"jest.config.js", "jest.config.js",
"scripts/**/*",
"webpack.config.js", "webpack.config.js",
"dist/**/*", "dist/**/*",
"public/**/*", "public/**/*",

View File

@ -30,4 +30,4 @@ $errorColor: desaturate($errorText, 50%);
// Responsivity // Responsivity
$mobileThreshold: 632px; $mobileThreshold: 632px;
$desktopThreshold: 940px; $desktopThreshold: 632px;

View File

@ -173,15 +173,6 @@ body > header {
font-weight: inherit; font-weight: inherit;
background: transparent; background: transparent;
} }
&:hover {
.tip {
visibility: visible;
opacity: 1;
transition: opacity ease-out 100ms;
transition-delay: 150ms;
}
}
} }
button { button {
@ -407,7 +398,7 @@ form {
padding: 8px 16px; padding: 8px 16px;
text-align: center; text-align: center;
.form-field:not(.hidden) { .form-field {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
margin: 16px auto; margin: 16px auto;
@ -702,7 +693,6 @@ td.actions {
th { th {
border-bottom: 1px solid #39434a; border-bottom: 1px solid #39434a;
white-space: nowrap;
} }
tr:nth-child(even) { tr:nth-child(even) {
@ -869,7 +859,37 @@ td.actions {
} }
.content { .content {
width: 0; overflow: hidden;
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;
@ -919,34 +939,3 @@ td.actions {
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;
}
}
}
}
}

View File

@ -36,17 +36,11 @@
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,
}, },

View File

@ -5,16 +5,14 @@
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,
}, },

View File

@ -6,7 +6,4 @@
database: "rainbox_email_test", database: "rainbox_email_test",
create_database_automatically: true create_database_automatically: true
}, },
mail: {
max_public_identities_per_user: 1,
},
} }

View File

@ -1,20 +1,20 @@
{ {
"name": "rainbox.email", "name": "rainbox.email",
"version": "2.4.2", "version": "2.4.0",
"description": "ISP mail provider manager with mysql and integrated LDAP server", "description": "ISP mail provider manager with mysql and integrated LDAP server",
"repository": "https://eternae.ink/ashpie/rainbox.email", "repository": "https://gitlab.com/ArisuOngaku/rainbox.email",
"author": "Alice Gaudon <alice@gaudon.pro>", "author": "Alice Gaudon <alice@gaudon.pro>",
"main": "dist/main.js", "main": "dist/main.js",
"license": "AGPL-3.0-only", "license": "AGPL-3.0-only",
"scripts": { "scripts": {
"test": "jest --verbose --runInBand", "test": "jest --verbose --runInBand",
"clean": "node scripts/clean.js", "clean": "(test ! -d dist || rm -r dist)",
"prepare-sources": "node scripts/prepare-sources.js", "prepareSources": "cp package.json src/",
"compile": "yarn clean && tsc", "compile": "yarn clean && tsc",
"build": "yarn prepare-sources && yarn compile && webpack --mode production", "build": "yarn prepareSources && yarn compile && webpack --mode production",
"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": "yarn prepareSources && 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",
"lint": "eslint ." "lint": "eslint . --ext .js,.jsx,.ts,.tsx"
}, },
"devDependencies": { "devDependencies": {
"@babel/core": "^7.9.0", "@babel/core": "^7.9.0",
@ -28,7 +28,7 @@
"@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": "^15.0.1", "@types/node": "^14.6.3",
"@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",
@ -40,21 +40,21 @@
"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.0", "imagemin": "^7.0.1",
"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": "^9.0.0", "imagemin-svgo": "^8.0.0",
"img-loader": "^3.0.1", "img-loader": "^3.0.1",
"jest": "^26.1.0", "jest": "^26.1.0",
"maildev": "^1.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": "^1.32.12",
"sass-loader": "^11.0.1", "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": "^9.1.0", "ts-loader": "^8.0.4",
"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"

View File

@ -1,10 +0,0 @@
const fs = require('fs');
[
'dist',
].forEach(file => {
if (fs.existsSync(file)) {
console.log('Cleaning', file, '...');
fs.rmSync(file, {recursive: true});
}
});

View File

@ -1,4 +0,0 @@
const fs = require('fs');
const path = require('path');
fs.copyFileSync('package.json', path.join('src', 'package.json'));

View File

@ -42,7 +42,6 @@ import AccountController from "swaf/auth/AccountController";
import packageJson = require('./package.json'); import packageJson = require('./package.json');
import AddUsedToMagicLinksMigration from "swaf/auth/magic_link/AddUsedToMagicLinksMigration"; import AddUsedToMagicLinksMigration from "swaf/auth/magic_link/AddUsedToMagicLinksMigration";
import MakeMagicLinksSessionNotUniqueMigration from "swaf/auth/magic_link/MakeMagicLinksSessionNotUniqueMigration"; 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 {
public constructor( public constructor(
@ -67,7 +66,6 @@ export default class App extends Application {
DropLegacyLogsTable, DropLegacyLogsTable,
AddUsedToMagicLinksMigration, AddUsedToMagicLinksMigration,
MakeMagicLinksSessionNotUniqueMigration, MakeMagicLinksSessionNotUniqueMigration,
AddNameChangedAtToUsersMigration,
]; ];
} }

View File

@ -57,20 +57,21 @@ export default class AccountMailboxController extends Controller {
}); });
// Check whether this identity can be created by this user // Check whether this identity can be created by this user
if (!domain.canCreateAddresses(user)) {
throw new ForbiddenHttpError('domain', req.url);
}
if (domain.isPublic()) { if (domain.isPublic()) {
await Validator.validate({ await Validator.validate({
name: new Validator<string>().defined().equals(user.as(UserNameComponent).getName()), name: new Validator<string>().defined().equals(user.as(UserNameComponent).name),
}, req.body); }, req.body);
const actualPublicAddressesCount = await mailIdentityComponent.getPublicAddressesCount(); const actualPublicAddressesCount = await mailIdentityComponent.getPublicAddressesCount();
const maxPublicAddressesCount = mailIdentityComponent.getMaxPublicAddressesCount(); const maxPublicAddressesCount = mailIdentityComponent.getMaxPublicAddressesCount();
if (maxPublicAddressesCount >= 0 && actualPublicAddressesCount >= maxPublicAddressesCount) { if (actualPublicAddressesCount >= maxPublicAddressesCount) {
req.flash('error', 'You have reached maximum public email addresses.'); req.flash('error', 'You have reached maximum public email addresses.');
res.redirect(Controller.route('account-mailbox')); res.redirect(Controller.route('account-mailbox'));
return; return;
} }
} else {
if (!domain.canCreateAddresses(user)) {
throw new ForbiddenHttpError('domain', req.url);
}
} }
// Save identity // Save identity

View File

@ -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).getName()}`); req.flash('success', `New password set for ${user.as(UserNameComponent).name}`);
res.redirect(Controller.route('backend-list-users')); res.redirect(Controller.route('backend-list-users'));
} }
} }

View File

@ -52,9 +52,16 @@ export default class MailboxBackendController extends Controller {
.get(); .get();
res.render('backend/mailboxes', { res.render('backend/mailboxes', {
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).getName(), username: user.as(UserNameComponent).name,
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,
@ -75,7 +82,7 @@ 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).getName(), userName: user.as(UserNameComponent).name,
name: await mainMailIdentity?.toEmail() || 'Not created.', name: await mainMailIdentity?.toEmail() || 'Not created.',
exists: !!mainMailIdentity, exists: !!mainMailIdentity,
}, },
@ -100,16 +107,9 @@ export default class MailboxBackendController extends Controller {
domains: await Promise.all(mailDomains.map(async domain => ({ domains: await Promise.all(mailDomains.map(async domain => ({
id: domain.id, id: domain.id,
name: domain.name, name: domain.name,
owner_name: (await domain.owner.get())?.as(UserNameComponent).getName(), owner_name: (await domain.owner.get())?.as(UserNameComponent).name,
identity_count: (await domain.identities.get()).length, identity_count: (await domain.identities.get()).length,
}))), }))),
users: [{
value: 0,
display: 'Public',
}, ...(await User.select().get()).map(u => ({
value: u.id,
display: u.name,
}))],
}); });
} }

View File

@ -2,7 +2,6 @@ import Model from "swaf/db/Model";
import User from "swaf/auth/models/User"; import User from "swaf/auth/models/User";
import {ManyModelRelation, OneModelRelation} from "swaf/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;
@ -39,6 +38,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() && user.as(UserNameComponent).hasName(); return this.user_id === user.id || this.isPublic();
} }
} }

View File

@ -3,7 +3,6 @@ import User from "swaf/auth/models/User";
import {ManyModelRelation, OneModelRelation} from "swaf/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;
@ -28,7 +27,7 @@ export default class UserMailIdentityComponent extends ModelComponent<User> {
}); });
public getMaxPublicAddressesCount(): number { public getMaxPublicAddressesCount(): number {
return config.get<number>('mail.max_public_identities_per_user'); return 1;
} }
public async getPublicAddressesCount(): Promise<number> { public async getPublicAddressesCount(): Promise<number> {

3029
yarn.lock

File diff suppressed because it is too large Load Diff