From 0fb544d88b5b13113bb733c0d6f20c40d1e93ee3 Mon Sep 17 00:00:00 2001 From: Alice Gaudon Date: Tue, 10 Nov 2020 14:53:58 +0100 Subject: [PATCH 1/8] Add users list backend and allow admins to change a user's password --- src/App.ts | 6 +- .../backend/AccountBackendController.ts | 65 +++++++++++++++++++ .../{ => backend}/MailboxBackendController.ts | 8 +-- src/models/UserPasswordComponent.ts | 4 +- views/backend/users-change-password.njk | 27 ++++++++ views/backend/users.njk | 43 ++++++++++++ 6 files changed, 145 insertions(+), 8 deletions(-) create mode 100644 src/controllers/backend/AccountBackendController.ts rename src/controllers/{ => backend}/MailboxBackendController.ts (97%) create mode 100644 views/backend/users-change-password.njk create mode 100644 views/backend/users.njk diff --git a/src/App.ts b/src/App.ts index f71cd99..712eef7 100644 --- a/src/App.ts +++ b/src/App.ts @@ -19,7 +19,6 @@ import AuthGuard from "wms-core/auth/AuthGuard"; import {PasswordAuthProof} from "./models/UserPasswordComponent"; import LDAPServerComponent from "./LDAPServerComponent"; import AutoUpdateComponent from "wms-core/components/AutoUpdateComponent"; -import packageJson = require('../package.json'); import DummyMigration from "wms-core/migrations/DummyMigration"; import DropLegacyLogsTable from "wms-core/migrations/DropLegacyLogsTable"; import AccountController from "./controllers/AccountController"; @@ -37,9 +36,11 @@ 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/MailboxBackendController"; +import MailboxBackendController from "./controllers/backend/MailboxBackendController"; import RedirectBackComponent from "wms-core/components/RedirectBackComponent"; import MailAutoConfigController from "./controllers/MailAutoConfigController"; +import AccountBackendController from "./controllers/backend/AccountBackendController"; +import packageJson = require('../package.json'); export default class App extends Application { private magicLinkWebSocketListener?: MagicLinkWebSocketListener; @@ -136,6 +137,7 @@ export default class App extends Application { this.use(new MagicLinkController(this.as(MagicLinkWebSocketListener))); this.use(new BackendController()); this.use(new MailboxBackendController()); + this.use(new AccountBackendController()); this.use(new AuthController()); this.use(new MailAutoConfigController()); // Needs to override MailController diff --git a/src/controllers/backend/AccountBackendController.ts b/src/controllers/backend/AccountBackendController.ts new file mode 100644 index 0000000..c347568 --- /dev/null +++ b/src/controllers/backend/AccountBackendController.ts @@ -0,0 +1,65 @@ +import Controller from "wms-core/Controller"; +import BackendController from "wms-core/helpers/BackendController"; +import {RequireAdminMiddleware, RequireAuthMiddleware} from "wms-core/auth/AuthComponent"; +import {Request, Response} from "express"; +import User from "wms-core/auth/models/User"; +import {NotFoundHttpError} from "wms-core/HttpError"; +import Validator from "wms-core/db/Validator"; +import UserPasswordComponent from "../../models/UserPasswordComponent"; +import UserNameComponent from "../../models/UserNameComponent"; + +export default class AccountBackendController extends Controller { + + public constructor() { + super(); + + BackendController.registerMenuElement({ + getLink: async () => Controller.route('backend-list-users'), + getDisplayString: async () => 'Users', + getDisplayIcon: async () => 'user', + }); + } + + public getRoutesPrefix(): string { + return '/backend/users'; + } + + public routes(): void { + this.get('/', this.getListUsers, 'backend-list-users', RequireAuthMiddleware, RequireAdminMiddleware); + this.get('/:user_id/change-password', this.getChangeUserPassword, 'backend-change-user-password', RequireAuthMiddleware, RequireAdminMiddleware); + this.post('/:user_id/change-password', this.postChangeUserPassword, 'backend-change-user-password', RequireAuthMiddleware, RequireAdminMiddleware); + } + + protected async getListUsers(req: Request, res: Response): Promise { + res.render('backend/users', { + accounts: await User.select() + .with('mainEmail') + .get(), + }); + } + + protected async getChangeUserPassword(req: Request, res: Response): Promise { + const user = await User.getById(req.params.user_id); + if (!user) throw new NotFoundHttpError('user', req.url); + + res.render('backend/users-change-password', { + user: user, + }); + } + + protected async postChangeUserPassword(req: Request, res: Response): Promise { + const user = await User.getById(req.params.user_id); + if (!user) throw new NotFoundHttpError('user', req.url); + + await this.validate({ + 'new_password': new Validator().defined(), + 'new_password_confirmation': new Validator().sameAs('new_password', req.body.new_password), + }, req.body); + + await user.as(UserPasswordComponent).setPassword(req.body.new_password, 'new_password'); + await user.save(); + + req.flash('success', `New password set for ${user.as(UserNameComponent).name}`); + res.redirect(Controller.route('backend-list-users')); + } +} diff --git a/src/controllers/MailboxBackendController.ts b/src/controllers/backend/MailboxBackendController.ts similarity index 97% rename from src/controllers/MailboxBackendController.ts rename to src/controllers/backend/MailboxBackendController.ts index 4d3b035..49ab92b 100644 --- a/src/controllers/MailboxBackendController.ts +++ b/src/controllers/backend/MailboxBackendController.ts @@ -2,12 +2,12 @@ import Controller from "wms-core/Controller"; import {Request, Response} from "express"; import User from "wms-core/auth/models/User"; import {WhereTest} from "wms-core/db/ModelQuery"; -import UserNameComponent from "../models/UserNameComponent"; -import UserMailIdentityComponent from "../models/UserMailIdentityComponent"; +import UserNameComponent from "../../models/UserNameComponent"; +import UserMailIdentityComponent from "../../models/UserMailIdentityComponent"; import {NotFoundHttpError} from "wms-core/HttpError"; -import MailDomain from "../models/MailDomain"; +import MailDomain from "../../models/MailDomain"; import BackendController from "wms-core/helpers/BackendController"; -import MailIdentity from "../models/MailIdentity"; +import MailIdentity from "../../models/MailIdentity"; import {RequireAdminMiddleware, RequireAuthMiddleware} from "wms-core/auth/AuthComponent"; export default class MailboxBackendController extends Controller { diff --git a/src/models/UserPasswordComponent.ts b/src/models/UserPasswordComponent.ts index 810ca9e..a667a69 100644 --- a/src/models/UserPasswordComponent.ts +++ b/src/models/UserPasswordComponent.ts @@ -11,9 +11,9 @@ export default class UserPasswordComponent extends ModelComponent { this.setValidation('password').acceptUndefined().maxLength(128); } - public async setPassword(rawPassword: string): Promise { + public async setPassword(rawPassword: string, fieldName: string = 'password'): Promise { await new Validator().defined().minLength(8).maxLength(512) - .execute('password', rawPassword, true); + .execute(fieldName, rawPassword, true); this.password = await argon2.hash(rawPassword, { timeCost: 10, memoryCost: 65536, diff --git a/views/backend/users-change-password.njk b/views/backend/users-change-password.njk new file mode 100644 index 0000000..21c61f3 --- /dev/null +++ b/views/backend/users-change-password.njk @@ -0,0 +1,27 @@ +{% extends 'layouts/base.njk' %} + +{% set title = app.name + ' - Backend' %} + +{% block body %} +
+ {{ macros.breadcrumb('Change ' + user.name + '\'s password', [ + {title: 'Backend', link: route('backend')}, + {title: 'Users', link: route('backend-list-users')} + ]) }} + +

Accounts manager

+ +
+

Change {{ user.name }}'s password

+ +
+ {{ macros.field(_locals, 'password', 'new_password', null, 'New password') }} + {{ macros.field(_locals, 'password', 'new_password_confirmation', null, 'New password confirmation') }} + + + + {{ macros.csrf(getCsrfToken) }} +
+
+
+{% endblock %} diff --git a/views/backend/users.njk b/views/backend/users.njk new file mode 100644 index 0000000..77deed3 --- /dev/null +++ b/views/backend/users.njk @@ -0,0 +1,43 @@ +{% extends 'layouts/base.njk' %} + +{% set title = app.name + ' - Backend' %} + +{% block body %} +
+ {{ macros.breadcrumb('Users', [ + {title: 'Backend', link: route('backend')} + ]) }} + +

Accounts manager

+ +
+

Accounts

+ + + + + + + + + + + + + {% for account in accounts %} + + + + + + + {% endfor %} + +
#NameEmailActions
{{ account.id }}{{ account.name }}{{ account.mainEmail.getOrFail().email | default('-') }} + + Change password + +
+
+
+{% endblock %} From a9f56cd0cfba97424047c9588b6e1d3c6d241a59 Mon Sep 17 00:00:00 2001 From: Alice Gaudon Date: Tue, 10 Nov 2020 15:05:43 +0100 Subject: [PATCH 2/8] Allow users to change their password --- README.md | 12 ++++++------ src/controllers/AccountController.ts | 25 +++++++++++++++++++++++++ views/account.njk | 14 ++++++++++++++ 3 files changed, 45 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index 1fc5b30..b1fa3de 100644 --- a/README.md +++ b/README.md @@ -8,12 +8,12 @@ Please feel free to contribute by making issues, bug reports and pull requests. ## /!\ THIS PROJECT STILL LACKS ESSENTIAL FEATURES SUCH AS: /!\ -- Change password -- Password recovery (recovery emails are unused yet) -- Quota management -- Editable terms of service -- Complex permissions system -- Redirections (can be manually setup with sql queries) +- [x] ~~Change password~~ +- [ ] Password recovery (recovery emails are unused yet) +- [ ] Quota management +- [ ] Editable terms of service +- [ ] Complex permissions system +- [ ] Redirections (can be manually setup with sql queries) - Probably many others, please make an issue so I can add them to this list ## How does it work? diff --git a/src/controllers/AccountController.ts b/src/controllers/AccountController.ts index 5101c76..802a406 100644 --- a/src/controllers/AccountController.ts +++ b/src/controllers/AccountController.ts @@ -12,6 +12,7 @@ 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 { @@ -20,6 +21,9 @@ export default class AccountController extends Controller { 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); @@ -51,6 +55,27 @@ export default class AccountController extends Controller { }); } + protected async postChangePassword(req: Request, res: Response): Promise { + 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 { await this.validate({ email: new Validator().defined().regexp(EMAIL_REGEX), diff --git a/views/account.njk b/views/account.njk index 163095b..8caa2e6 100644 --- a/views/account.njk +++ b/views/account.njk @@ -11,6 +11,20 @@

Name: {{ user.name }}

+
+

Change password

+ +
+ {{ 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') }} + + + + {{ macros.csrf(getCsrfToken) }} +
+
+

Recovery email addresses

From a2e6dfbeaf9c7642c43d119e58e63708d9262a01 Mon Sep 17 00:00:00 2001 From: Alice Gaudon Date: Tue, 10 Nov 2020 15:12:32 +0100 Subject: [PATCH 3/8] Add missing recovery email address warning and improve info layout --- src/controllers/AccountController.ts | 1 + views/account.njk | 15 +++++++++++---- 2 files changed, 12 insertions(+), 4 deletions(-) diff --git a/src/controllers/AccountController.ts b/src/controllers/AccountController.ts index 802a406..53abebe 100644 --- a/src/controllers/AccountController.ts +++ b/src/controllers/AccountController.ts @@ -37,6 +37,7 @@ export default class AccountController extends Controller { 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 => ({ diff --git a/views/account.njk b/views/account.njk index 8caa2e6..1fc58f7 100644 --- a/views/account.njk +++ b/views/account.njk @@ -4,12 +4,19 @@ {% block body %}
-
- -

My account

+

My account

+ + {% if emails | length <= 0 %} + {{ macros.message('warning', 'To avoid losing access to your account, please add a recovery email address.') }} + {% endif %} + +
+

Personnal info

Name: {{ user.name }}

-
+ +

Contact email: {{ main_email.email | default('-') }}

+

Change password

From d588bc8ce314fcff4df505ce544a03badc15e347 Mon Sep 17 00:00:00 2001 From: Alice Gaudon Date: Tue, 10 Nov 2020 15:21:44 +0100 Subject: [PATCH 4/8] backend: add link to user's mailbox from user list --- views/backend/users.njk | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/views/backend/users.njk b/views/backend/users.njk index 77deed3..4ff5f2b 100644 --- a/views/backend/users.njk +++ b/views/backend/users.njk @@ -33,6 +33,10 @@ Change password + + + Edit mailbox + {% endfor %} From 5ce5e60af4605258efb09c72f9e9529d0752a236 Mon Sep 17 00:00:00 2001 From: Alice Gaudon Date: Tue, 10 Nov 2020 15:22:06 +0100 Subject: [PATCH 5/8] backend: improve user mailbox layout and display --- src/controllers/backend/MailboxBackendController.ts | 1 + views/backend/mailbox.njk | 10 +++++----- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/src/controllers/backend/MailboxBackendController.ts b/src/controllers/backend/MailboxBackendController.ts index 49ab92b..0394322 100644 --- a/src/controllers/backend/MailboxBackendController.ts +++ b/src/controllers/backend/MailboxBackendController.ts @@ -86,6 +86,7 @@ export default class MailboxBackendController extends Controller { res.render('backend/mailbox', { mailbox: { id: user.id, + userName: user.as(UserNameComponent).name, name: await mainMailIdentity?.toEmail() || 'Not created.', }, domains: mailDomains.map(d => ({ diff --git a/views/backend/mailbox.njk b/views/backend/mailbox.njk index 771326f..37e31b8 100644 --- a/views/backend/mailbox.njk +++ b/views/backend/mailbox.njk @@ -4,14 +4,14 @@ {% block body %}
- {{ macros.breadcrumb(mailbox.name, [ + {{ macros.breadcrumb(mailbox.userName + ' - ' + mailbox.name, [ {title: 'Backend', link: route('backend')}, {title: 'Mailboxes', link: route('backend-mailboxes')} ]) }} +

Mailbox: {{ mailbox.userName }} - {{ mailbox.name }}

-
- -

Mailbox: {{ mailbox.name }}

+
+

Identities

@@ -57,6 +57,6 @@ {{ macros.csrf(getCsrfToken) }} - + {% endblock %} From 2406485ae7b453bce2ac211f8ad018503b30b662 Mon Sep 17 00:00:00 2001 From: Alice Gaudon Date: Tue, 10 Nov 2020 15:27:14 +0100 Subject: [PATCH 6/8] frontend: fix css priority on td.actions child margin --- assets/sass/app.scss | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/assets/sass/app.scss b/assets/sass/app.scss index 2b59433..8e783e9 100644 --- a/assets/sass/app.scss +++ b/assets/sass/app.scss @@ -12,10 +12,6 @@ td.actions { justify-content: center; align-items: center; - > *:not(:first-child) { - margin-left: 8px; - } - form { padding: 0; display: inline; @@ -29,6 +25,10 @@ td.actions { margin-right: 0; } } + + > *:not(:first-child) { + margin-left: 8px; + } } body > header { From 0ce7fec35a95d221cacdb741849f45c30756f8d9 Mon Sep 17 00:00:00 2001 From: Alice Gaudon Date: Tue, 10 Nov 2020 15:31:29 +0100 Subject: [PATCH 7/8] README.md: update redis' requirement purpose --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index b1fa3de..ca5f604 100644 --- a/README.md +++ b/README.md @@ -31,7 +31,7 @@ Note that a user only has one mailbox once they create their first address and c ### Requirements - mariadb (for the mysql database that will be operated) -- redis (cache) (this may go as I think I'm not using it at all, but it won't start without it yet) +- redis (session, cache) - A working mail smtp server (to send emails) - git and yarn for updates From c0144fe70240d5c1fb1a14ce68d8180efb679a5f Mon Sep 17 00:00:00 2001 From: Alice Gaudon Date: Tue, 10 Nov 2020 15:36:03 +0100 Subject: [PATCH 8/8] Version 2.2.0 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 011aeb3..0906e98 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "rainbox.email", - "version": "2.1.2", + "version": "2.2.0", "description": "ISP mail provider manager with mysql and integrated LDAP server", "repository": "https://gitlab.com/ArisuOngaku/rainbox.email", "author": "Alice Gaudon ",