Repository: ignite Updated Branches: refs/heads/master dfef77e59 -> cdf2d867f
IGNITE-10839 Web Console: Added optional email confirmation. Co-authored-by: Andrey Novikov <anovi...@apache.org> Co-authored-by: Ilya Borisov <klast...@gmail.com> Project: http://git-wip-us.apache.org/repos/asf/ignite/repo Commit: http://git-wip-us.apache.org/repos/asf/ignite/commit/cdf2d867 Tree: http://git-wip-us.apache.org/repos/asf/ignite/tree/cdf2d867 Diff: http://git-wip-us.apache.org/repos/asf/ignite/diff/cdf2d867 Branch: refs/heads/master Commit: cdf2d867fecef4459edb0f128c017cbb7b21ba07 Parents: dfef77e Author: Andrey Novikov <anovi...@apache.org> Authored: Fri Dec 28 13:52:50 2018 +0700 Committer: Alexey Kuznetsov <akuznet...@apache.org> Committed: Fri Dec 28 13:52:50 2018 +0700 ---------------------------------------------------------------------- modules/web-console/assembly/README.txt | 25 +++--- modules/web-console/backend/app/mongo.js | 20 ++++- modules/web-console/backend/app/schemas.js | 5 +- modules/web-console/backend/app/settings.js | 14 ++++ .../backend/config/settings.json.sample | 5 ++ .../MissingConfirmRegistrationException.js | 34 ++++++++ modules/web-console/backend/errors/index.js | 2 + modules/web-console/backend/middlewares/api.js | 3 + modules/web-console/backend/routes/public.js | 53 +++++++++---- modules/web-console/backend/services/auth.js | 83 ++++++++++++++++++-- modules/web-console/backend/services/mails.js | 18 +++++ modules/web-console/backend/services/users.js | 32 ++++++-- modules/web-console/frontend/app/app.js | 5 +- .../list-of-registered-users/categories.js | 1 + .../list-of-registered-users/column-defs.js | 1 + .../app/components/page-signin/component.ts | 5 +- .../app/components/page-signin/controller.ts | 10 ++- .../frontend/app/components/page-signin/run.ts | 11 ++- .../app/components/page-signin/template.pug | 4 +- .../page-signup-confirmation/component.ts | 28 +++++++ .../page-signup-confirmation/controller.ts | 42 ++++++++++ .../page-signup-confirmation/index.ts | 23 ++++++ .../page-signup-confirmation/state.ts | 48 +++++++++++ .../page-signup-confirmation/style.scss | 22 ++++++ .../page-signup-confirmation/template.tpl.pug | 24 ++++++ .../app/components/page-signup/controller.ts | 7 ++ .../frontend/app/modules/user/Auth.service.ts | 14 +++- .../user/emailConfirmationInterceptor.ts | 37 +++++++++ .../frontend/app/modules/user/user.module.js | 27 ++++--- 29 files changed, 540 insertions(+), 63 deletions(-) ---------------------------------------------------------------------- http://git-wip-us.apache.org/repos/asf/ignite/blob/cdf2d867/modules/web-console/assembly/README.txt ---------------------------------------------------------------------- diff --git a/modules/web-console/assembly/README.txt b/modules/web-console/assembly/README.txt index 2656aca..e88e345 100644 --- a/modules/web-console/assembly/README.txt +++ b/modules/web-console/assembly/README.txt @@ -41,21 +41,24 @@ Technical details On Windows: `ignite-web-console-win.exe --server:port 3000` All available parameters with defaults: - Web Console host: --server:host 0.0.0.0 - Web Console port: --server:port 80 + Web Console host: --server:host 0.0.0.0 + Web Console port: --server:port 80 - Enable HTTPS: --server:ssl false + Enable HTTPS: --server:ssl false + Disable self registration: --server:disable:signup false - Disable self registration: --server:disable:signup false + MongoDB URL: --mongodb:url mongodb://localhost/console - MongoDB URL: --mongodb:url mongodb://localhost/console + Enable account activation: --activation:enabled false + Activation timeout(milliseconds): --activation:timeout 1800000 + Activation send email throttle (milliseconds): --activation:sendTimeout 180000 - Mail service: --mail:service "gmail" - Signature text: --mail:sign "Kind regards, Apache Ignite Team" - Greeting text: --mail:greeting "Apache Ignite Web Console" - Mail FROM: --mail:from "Apache Ignite Web Console <someusername@somecompany.somedomain>" - User to send e-mail: --mail:auth:user "someusername@somecompany.somedomain" - E-mail service password: --mail:auth:pass "" + Mail service: --mail:service "gmail" + Signature text: --mail:sign "Kind regards, Apache Ignite Team" + Greeting text: --mail:greeting "Apache Ignite Web Console" + Mail FROM: --mail:from "Apache Ignite Web Console <someusername@somecompany.somedomain>" + User to send e-mail: --mail:auth:user "someusername@somecompany.somedomain" + E-mail service password: --mail:auth:pass "" SSL options has no default values: --server:key "path to file with server.key" http://git-wip-us.apache.org/repos/asf/ignite/blob/cdf2d867/modules/web-console/backend/app/mongo.js ---------------------------------------------------------------------- diff --git a/modules/web-console/backend/app/mongo.js b/modules/web-console/backend/app/mongo.js index 6a843db..57171ac 100644 --- a/modules/web-console/backend/app/mongo.js +++ b/modules/web-console/backend/app/mongo.js @@ -128,7 +128,8 @@ module.exports.factory = function(settings, mongoose, schemas) { admin: true, token: 'ruQvlWff09zqoVYyh6WJ', attempts: 0, - resetPasswordToken: 'O2GWgOkKkhqpDcxjYnSP' + resetPasswordToken: 'O2GWgOkKkhqpDcxjYnSP', + activated: true }), mongo.Space.create({ _id: '59fc0c26e145c32be0f83b34', @@ -146,5 +147,22 @@ module.exports.factory = function(settings, mongoose, schemas) { return mongo; }); + }) + .then((mongo) => { + if (settings.activation.enabled) { + return mongo.Account.find({ + $or: [{activated: false}, {activated: {$exists: false}}], + activationToken: {$exists: false} + }, '_id').lean().exec() + .then((accounts) => { + const conditions = _.map(accounts, (account) => ({session: {$regex: `"${account._id}"`}})); + + return mongoose.connection.db.collection('sessions').deleteMany({$or: conditions}); + }) + .then(() => mongo) + .catch(() => mongo); + } + + return mongo; }); }; http://git-wip-us.apache.org/repos/asf/ignite/blob/cdf2d867/modules/web-console/backend/app/schemas.js ---------------------------------------------------------------------- diff --git a/modules/web-console/backend/app/schemas.js b/modules/web-console/backend/app/schemas.js index 14ce111..2f6498f 100644 --- a/modules/web-console/backend/app/schemas.js +++ b/modules/web-console/backend/app/schemas.js @@ -46,7 +46,10 @@ module.exports.factory = function(mongoose) { lastActivity: Date, admin: Boolean, token: String, - resetPasswordToken: String + resetPasswordToken: String, + activated: {type: Boolean, default: false}, + activationSentAt: Date, + activationToken: String }); // Install passport plugin. http://git-wip-us.apache.org/repos/asf/ignite/blob/cdf2d867/modules/web-console/backend/app/settings.js ---------------------------------------------------------------------- diff --git a/modules/web-console/backend/app/settings.js b/modules/web-console/backend/app/settings.js index 9c4d3c3..0079788 100644 --- a/modules/web-console/backend/app/settings.js +++ b/modules/web-console/backend/app/settings.js @@ -18,6 +18,7 @@ 'use strict'; const fs = require('fs'); +const _ = require('lodash'); // Fire me up! @@ -61,6 +62,14 @@ module.exports = { return v === 'true' || v === true; }; + let activationEnabled = _isTrue('activation:enabled'); + + if (activationEnabled && _.isEmpty(mail)) { + activationEnabled = false; + + console.warn('Mail server settings are required for account confirmation!'); + } + const settings = { agent: { dists: nconf.get('agent:dists') || dfltAgentDists @@ -72,6 +81,11 @@ module.exports = { disableSignup: _isTrue('server:disable:signup') }, mail, + activation: { + enabled: activationEnabled, + timeout: nconf.get('activation:timeout') || 1800000, + sendTimeout: nconf.get('activation:sendTimeout') || 180000 + }, mongoUrl: nconf.get('mongodb:url') || 'mongodb://127.0.0.1/console', cookieTTL: 3600000 * 24 * 30, sessionSecret: nconf.get('server:sessionSecret') || 'keyboard cat', http://git-wip-us.apache.org/repos/asf/ignite/blob/cdf2d867/modules/web-console/backend/config/settings.json.sample ---------------------------------------------------------------------- diff --git a/modules/web-console/backend/config/settings.json.sample b/modules/web-console/backend/config/settings.json.sample index c16ba26..02bc327 100644 --- a/modules/web-console/backend/config/settings.json.sample +++ b/modules/web-console/backend/config/settings.json.sample @@ -18,6 +18,11 @@ "mongodb": { "url": "mongodb://localhost/console" }, + "activation": { + "enabled": false, + "timeout": 1800000, + "sendTimeout": 180000 + }, "mail": { "service": "gmail", "from": "Some Company Web Console <some_username@some_company.com>", http://git-wip-us.apache.org/repos/asf/ignite/blob/cdf2d867/modules/web-console/backend/errors/MissingConfirmRegistrationException.js ---------------------------------------------------------------------- diff --git a/modules/web-console/backend/errors/MissingConfirmRegistrationException.js b/modules/web-console/backend/errors/MissingConfirmRegistrationException.js new file mode 100644 index 0000000..a094a67 --- /dev/null +++ b/modules/web-console/backend/errors/MissingConfirmRegistrationException.js @@ -0,0 +1,34 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +'use strict'; + +const IllegalAccessError = require('./IllegalAccessError'); + +class MissingConfirmRegistrationException extends IllegalAccessError { + constructor(email) { + super('User account email not activated'); + + this.data = { + errorCode: 10104, + message: this.message, + email + }; + } +} + +module.exports = MissingConfirmRegistrationException; http://git-wip-us.apache.org/repos/asf/ignite/blob/cdf2d867/modules/web-console/backend/errors/index.js ---------------------------------------------------------------------- diff --git a/modules/web-console/backend/errors/index.js b/modules/web-console/backend/errors/index.js index 2fadc12..cb8f043 100644 --- a/modules/web-console/backend/errors/index.js +++ b/modules/web-console/backend/errors/index.js @@ -24,6 +24,7 @@ const IllegalArgumentException = require('./IllegalArgumentException'); const IllegalAccessError = require('./IllegalAccessError'); const DuplicateKeyException = require('./DuplicateKeyException'); const ServerErrorException = require('./ServerErrorException'); +const MissingConfirmRegistrationException = require('./MissingConfirmRegistrationException'); const MissingResourceException = require('./MissingResourceException'); const AuthFailedException = require('./AuthFailedException'); @@ -35,6 +36,7 @@ module.exports = { IllegalArgumentException, DuplicateKeyException, ServerErrorException, + MissingConfirmRegistrationException, MissingResourceException, AuthFailedException }) http://git-wip-us.apache.org/repos/asf/ignite/blob/cdf2d867/modules/web-console/backend/middlewares/api.js ---------------------------------------------------------------------- diff --git a/modules/web-console/backend/middlewares/api.js b/modules/web-console/backend/middlewares/api.js index 27e130d..d4d832b 100644 --- a/modules/web-console/backend/middlewares/api.js +++ b/modules/web-console/backend/middlewares/api.js @@ -37,6 +37,9 @@ module.exports.factory = () => { if (_.includes(['MongoError', 'MongooseError'], err.name)) return res.status(500).send(err.message); + if (_.isObject(err.data)) + return res.status(err.httpCode || err.code || 500).json(err.data); + res.status(err.httpCode || err.code || 500).send(err.message); }, http://git-wip-us.apache.org/repos/asf/ignite/blob/cdf2d867/modules/web-console/backend/routes/public.js ---------------------------------------------------------------------- diff --git a/modules/web-console/backend/routes/public.js b/modules/web-console/backend/routes/public.js index 8aa3b43..d290b53 100644 --- a/modules/web-console/backend/routes/public.js +++ b/modules/web-console/backend/routes/public.js @@ -25,17 +25,18 @@ const passport = require('passport'); module.exports = { implements: 'routes/public', - inject: ['mongo', 'services/mails', 'services/users', 'services/auth'] + inject: ['mongo', 'settings', 'services/users', 'services/auth', 'errors'] }; -/** +/** * @param mongo - * @param mailsService + * @param settings * @param {UsersService} usersService * @param {AuthService} authService + * @param errors * @returns {Promise} */ -module.exports.factory = function(mongo, mailsService, usersService, authService) { +module.exports.factory = function(mongo, settings, usersService, authService, errors) { return new Promise((factoryResolve) => { const router = new express.Router(); @@ -76,14 +77,34 @@ module.exports.factory = function(mongo, mailsService, usersService, authService router.post('/signin', (req, res, next) => { passport.authenticate('local', (errAuth, user) => { if (errAuth) - return res.status(401).send(errAuth.message); + return res.api.error(new errors.AuthFailedException(errAuth.message)); if (!user) - return res.status(401).send('Invalid email or password'); + return res.api.error(new errors.AuthFailedException('Invalid email or password')); - req.logIn(user, {}, (errLogIn) => { + if (settings.activation.enabled) { + const activationToken = req.body.activationToken; + + const errToken = authService.validateActivationToken(user, activationToken); + + if (errToken) + return res.api.error(errToken); + + if (authService.isActivationTokenExpired(user, activationToken)) { + authService.resetActivationToken(req.origin(), user.email) + .catch((ignored) => { + // No-op. + }); + + return res.api.error(new errors.AuthFailedException('This activation link was expired. We resend a new one. Please open the most recent email and click on the activation link.')); + } + + user.activated = true; + } + + return req.logIn(user, {}, (errLogIn) => { if (errLogIn) - return res.status(401).send(errLogIn.message); + return res.api.error(new errors.AuthFailedException(errLogIn.message)); return res.sendStatus(200); }); @@ -103,10 +124,8 @@ module.exports.factory = function(mongo, mailsService, usersService, authService * Send e-mail to user with reset token. */ router.post('/password/forgot', (req, res) => { - authService.resetPasswordToken(req.body.email) - .then((user) => mailsService.emailUserResetLink(req.origin(), user)) - .then(() => 'An email has been sent with further instructions.') - .then(res.api.ok) + authService.resetPasswordToken(req.origin(), req.body.email) + .then(() => res.api.ok('An email has been sent with further instructions.')) .catch(res.api.error); }); @@ -116,8 +135,7 @@ module.exports.factory = function(mongo, mailsService, usersService, authService router.post('/password/reset', (req, res) => { const {token, password} = req.body; - authService.resetPasswordByToken(token, password) - .then((user) => mailsService.emailPasswordChanged(req.origin(), user)) + authService.resetPasswordByToken(req.origin(), token, password) .then(res.api.ok) .catch(res.api.error); }); @@ -131,6 +149,13 @@ module.exports.factory = function(mongo, mailsService, usersService, authService .catch(res.api.error); }); + /* Send e-mail to user with account confirmation token. */ + router.post('/activation/resend', (req, res) => { + authService.resetActivationToken(req.origin(), req.body.email) + .then(() => res.api.ok('An email has been sent with further instructions.')) + .catch(res.api.error); + }); + factoryResolve(router); }); }; http://git-wip-us.apache.org/repos/asf/ignite/blob/cdf2d867/modules/web-console/backend/services/auth.js ---------------------------------------------------------------------- diff --git a/modules/web-console/backend/services/auth.js b/modules/web-console/backend/services/auth.js index 986ed95..4679957 100644 --- a/modules/web-console/backend/services/auth.js +++ b/modules/web-console/backend/services/auth.js @@ -19,9 +19,11 @@ // Fire me up! +const _ = require('lodash'); + module.exports = { implements: 'services/auth', - inject: ['mongo', 'settings', 'errors', 'services/utils'] + inject: ['mongo', 'settings', 'errors', 'services/utils', 'services/mails'] }; /** @@ -29,37 +31,44 @@ module.exports = { * @param settings * @param errors * @param {UtilsService} utilsService + * @param {MailsService} mailsService * @returns {AuthService} */ -module.exports.factory = (mongo, settings, errors, utilsService) => { +module.exports.factory = (mongo, settings, errors, utilsService, mailsService) => { class AuthService { /** * Reset password reset token for user. * + * @param host Web Console host. * @param email - user email * @returns {Promise.<mongo.Account>} - that resolves account found by email with new reset password token. */ - static resetPasswordToken(email) { + static resetPasswordToken(host, email) { return mongo.Account.findOne({email}).exec() .then((user) => { if (!user) throw new errors.MissingResourceException('Account with that email address does not exists!'); + if (settings.activation.enabled && !user.activated) + throw new errors.MissingConfirmRegistrationException(user.email); + user.resetPasswordToken = utilsService.randomString(settings.tokenLength); return user.save(); - }); + }) + .then((user) => mailsService.emailUserResetLink(host, user)); } /** * Reset password by reset token. * + * @param host Web Console host. * @param {string} token - reset token * @param {string} newPassword - new password * @returns {Promise.<mongo.Account>} - that resolves account with new password */ - static resetPasswordByToken(token, newPassword) { + static resetPasswordByToken(host, token, newPassword) { return mongo.Account.findOne({resetPasswordToken: token}).exec() .then((user) => { if (!user) @@ -75,7 +84,8 @@ module.exports.factory = (mongo, settings, errors, utilsService) => { resolve(_user.save()); }); }); - }); + }) + .then((user) => mailsService.emailPasswordChanged(host, user)); } /** @@ -93,6 +103,67 @@ module.exports.factory = (mongo, settings, errors, utilsService) => { return {token, email: user.email}; }); } + + /** + * Validate activationToken token. + * + * @param {mongo.Account} user - User object. + * @param {string} activationToken - activate account token + * @return {Error} If token is invalid. + */ + static validateActivationToken(user, activationToken) { + if (user.activated) { + if (user.activationToken !== activationToken) + return new errors.AuthFailedException('Invalid email or password!'); + } + else { + if (_.isEmpty(activationToken)) + return new errors.MissingConfirmRegistrationException(user.email); + + if (user.activationToken !== activationToken) + return new errors.AuthFailedException('This activation token isn\'t valid.'); + } + } + + /** + * Check if activation token expired. + * + * @param {mongo.Account} user - User object. + * @param {string} activationToken - activate account token + * @return {boolean} If token was already expired. + */ + static isActivationTokenExpired(user, activationToken) { + return !user.activated && + new Date().getTime() - user.activationSentAt.getTime() >= settings.activation.timeout; + } + + /** + * Reset password reset token for user. + * + * @param host Web Console host. + * @param email - user email. + * @returns {Promise}. + */ + static resetActivationToken(host, email) { + return mongo.Account.findOne({email}).exec() + .then((user) => { + if (!user) + throw new errors.MissingResourceException('Account with that email address does not exists!'); + + if (!settings.activation.enabled) + throw new errors.IllegalAccessError('Activation was not enabled!'); + + if (user.activationSentAt && + new Date().getTime() - user.activationSentAt.getTime() < settings.activation.sendTimeout) + throw new errors.IllegalAccessError('Too Many Activation Attempts!'); + + user.activationToken = utilsService.randomString(settings.tokenLength); + user.activationSentAt = new Date(); + + return user.save(); + }) + .then((user) => mailsService.emailUserActivation(host, user)); + } } return AuthService; http://git-wip-us.apache.org/repos/asf/ignite/blob/cdf2d867/modules/web-console/backend/services/mails.js ---------------------------------------------------------------------- diff --git a/modules/web-console/backend/services/mails.js b/modules/web-console/backend/services/mails.js index 75da128..183fbe1 100644 --- a/modules/web-console/backend/services/mails.js +++ b/modules/web-console/backend/services/mails.js @@ -108,6 +108,24 @@ module.exports.factory = (settings) => { /** * Send email to user for password reset. + * + * @param host + * @param user + */ + emailUserActivation(host, user) { + const activationLink = `${host}/signin?activationToken=${user.activationToken}`; + + return this.send(user, `Confirm your account on ${settings.mail.greeting}`, + `Hello ${user.firstName} ${user.lastName}!<br><br>` + + `You are receiving this email because you have signed up to use <a href="${host}">${settings.mail.greeting}</a>.<br><br>` + + 'Please click on the following link, or paste this into your browser to activate your account:<br><br>' + + `<a href="${activationLink}">${activationLink}</a>`, + 'Failed to send email with confirm account link!'); + } + + /** + * Send email to user for password reset. + * * @param host * @param user */ http://git-wip-us.apache.org/repos/asf/ignite/blob/cdf2d867/modules/web-console/backend/services/users.js ---------------------------------------------------------------------- diff --git a/modules/web-console/backend/services/users.js b/modules/web-console/backend/services/users.js index 79578c5..ed844db 100644 --- a/modules/web-console/backend/services/users.js +++ b/modules/web-console/backend/services/users.js @@ -53,6 +53,8 @@ module.exports.factory = (errors, settings, mongo, spacesService, mailsService, user.admin = cnt === 0; user.registered = new Date(); user.token = utilsService.randomString(settings.tokenLength); + user.resetPasswordToken = utilsService.randomString(settings.tokenLength); + user.activated = false; if (settings.server.disableSignup && !user.admin && !createdByAdmin) throw new errors.ServerErrorException('Sign-up is not allowed. Ask your Web Console administrator to create account for you.'); @@ -73,15 +75,27 @@ module.exports.factory = (errors, settings, mongo, spacesService, mailsService, }); }) .then((registered) => { - registered.resetPasswordToken = utilsService.randomString(settings.tokenLength); + return mongo.Space.create({name: 'Personal space', owner: registered._id}) + .then(() => registered) + }) + .then((registered) => { + if (settings.activation.enabled) { + registered.activationToken = utilsService.randomString(settings.tokenLength); + registered.activationSentAt = new Date(); - return registered.save() - .then(() => mongo.Space.create({name: 'Personal space', owner: registered._id})) - .then(() => { - mailsService.emailUserSignUp(host, registered, createdByAdmin); + if (!createdByAdmin) { + return registered.save() + .then(() => { + mailsService.emailUserActivation(host, registered); - return registered; - }); + throw new errors.MissingConfirmRegistrationException(registered.email); + }); + } + } + + mailsService.emailUserSignUp(host, registered, createdByAdmin); + + return registered; }); } @@ -93,6 +107,9 @@ module.exports.factory = (errors, settings, mongo, spacesService, mailsService, */ static save(changed) { delete changed.admin; + delete changed.activated; + delete changed.activationSentAt; + delete changed.activationToken; return mongo.Account.findById(changed._id).exec() .then((user) => { @@ -157,6 +174,7 @@ module.exports.factory = (errors, settings, mongo, spacesService, mailsService, country: 1, lastLogin: 1, lastActivity: 1, + activated: 1, spaces: { $filter: { input: '$spaces', http://git-wip-us.apache.org/repos/asf/ignite/blob/cdf2d867/modules/web-console/frontend/app/app.js ---------------------------------------------------------------------- diff --git a/modules/web-console/frontend/app/app.js b/modules/web-console/frontend/app/app.js index fd75ade..94f9704 100644 --- a/modules/web-console/frontend/app/app.js +++ b/modules/web-console/frontend/app/app.js @@ -161,6 +161,7 @@ import pageForgotPassword from './components/page-forgot-password'; import formSignup from './components/form-signup'; import sidebar from './components/web-console-sidebar'; import permanentNotifications from './components/permanent-notifications'; +import signupConfirmation from './components/page-signup-confirmation'; import igniteServices from './services'; @@ -268,7 +269,9 @@ export default angular.module('ignite-console', [ formSignup.name, timedRedirection.name, sidebar.name, - permanentNotifications.name + permanentNotifications.name, + timedRedirection.name, + signupConfirmation.name ]) .service('$exceptionHandler', $exceptionHandler) // Directives. http://git-wip-us.apache.org/repos/asf/ignite/blob/cdf2d867/modules/web-console/frontend/app/components/list-of-registered-users/categories.js ---------------------------------------------------------------------- diff --git a/modules/web-console/frontend/app/components/list-of-registered-users/categories.js b/modules/web-console/frontend/app/components/list-of-registered-users/categories.js index e73dd17..aa70863 100644 --- a/modules/web-console/frontend/app/components/list-of-registered-users/categories.js +++ b/modules/web-console/frontend/app/components/list-of-registered-users/categories.js @@ -19,6 +19,7 @@ export default [ {name: 'Actions', visible: false, enableHiding: false}, {name: 'User', visible: true, enableHiding: false}, {name: 'Email', visible: true, enableHiding: true}, + {name: 'Activated', visible: false, enableHiding: true}, {name: 'Company', visible: true, enableHiding: true}, {name: 'Country', visible: true, enableHiding: true}, {name: 'Last login', visible: false, enableHiding: true}, http://git-wip-us.apache.org/repos/asf/ignite/blob/cdf2d867/modules/web-console/frontend/app/components/list-of-registered-users/column-defs.js ---------------------------------------------------------------------- diff --git a/modules/web-console/frontend/app/components/list-of-registered-users/column-defs.js b/modules/web-console/frontend/app/components/list-of-registered-users/column-defs.js index 2094f0c..b1c71f7 100644 --- a/modules/web-console/frontend/app/components/list-of-registered-users/column-defs.js +++ b/modules/web-console/frontend/app/components/list-of-registered-users/column-defs.js @@ -32,6 +32,7 @@ const VALUE_WITH_TITLE = '<div class="ui-grid-cell-contents"><label bs-tooltip d export default [ {name: 'user', enableHiding: false, displayName: 'User', categoryDisplayName: 'User', field: 'userName', cellTemplate: USER_TEMPLATE, minWidth: 160, enableFiltering: true, pinnedLeft: true, filter: { placeholder: 'Filter by name...' }}, {name: 'email', displayName: 'Email', categoryDisplayName: 'Email', field: 'email', cellTemplate: EMAIL_TEMPLATE, minWidth: 160, width: 220, enableFiltering: true, filter: { placeholder: 'Filter by email...' }}, + {name: 'activated', displayName: 'Activated', categoryDisplayName: 'Activated', field: 'activated', width: 220, enableFiltering: true, filter: { placeholder: 'Filter by activation...' }, visible: false}, {name: 'company', displayName: 'Company', categoryDisplayName: 'Company', field: 'company', cellTemplate: VALUE_WITH_TITLE, minWidth: 180, enableFiltering: true, filter: { placeholder: 'Filter by company...' }}, {name: 'country', displayName: 'Country', categoryDisplayName: 'Country', field: 'countryCode', cellTemplate: VALUE_WITH_TITLE, minWidth: 160, enableFiltering: true, filter: { placeholder: 'Filter by country...' }}, {name: 'lastlogin', displayName: 'Last login', categoryDisplayName: 'Last login', field: 'lastLogin', cellTemplate: DATE_WITH_TITLE, minWidth: 135, width: 135, enableFiltering: false, visible: false}, http://git-wip-us.apache.org/repos/asf/ignite/blob/cdf2d867/modules/web-console/frontend/app/components/page-signin/component.ts ---------------------------------------------------------------------- diff --git a/modules/web-console/frontend/app/components/page-signin/component.ts b/modules/web-console/frontend/app/components/page-signin/component.ts index 968ff39..6c5f461 100644 --- a/modules/web-console/frontend/app/components/page-signin/component.ts +++ b/modules/web-console/frontend/app/components/page-signin/component.ts @@ -22,5 +22,8 @@ import './style.scss'; /** @type {ng.IComponentOptions} */ export default { controller, - template + template, + bindings: { + activationToken: '@?' + } }; http://git-wip-us.apache.org/repos/asf/ignite/blob/cdf2d867/modules/web-console/frontend/app/components/page-signin/controller.ts ---------------------------------------------------------------------- diff --git a/modules/web-console/frontend/app/components/page-signin/controller.ts b/modules/web-console/frontend/app/components/page-signin/controller.ts index 18fee0b..71caa0e 100644 --- a/modules/web-console/frontend/app/components/page-signin/controller.ts +++ b/modules/web-console/frontend/app/components/page-signin/controller.ts @@ -17,6 +17,8 @@ import AuthService from 'app/modules/user/Auth.service'; +import {PageSigninStateParams} from './run'; + interface ISiginData { email: string, password: string @@ -27,7 +29,9 @@ interface ISigninFormController extends ng.IFormController { password: ng.INgModelController } -export default class implements ng.IPostLink { +export default class PageSignIn implements ng.IPostLink { + activationToken?: PageSigninStateParams['activationToken']; + data: ISiginData = { email: null, password: null @@ -65,8 +69,8 @@ export default class implements ng.IPostLink { if (!this.canSubmitForm(this.form)) return; - return this.Auth.signin(this.data.email, this.data.password).catch((res) => { - this.IgniteMessages.showError(null, res.data); + return this.Auth.signin(this.data.email, this.data.password, this.activationToken).catch((res) => { + this.IgniteMessages.showError(null, res.data.errorMessage ? res.data.errorMessage : res.data); this.setServerError(res.data); http://git-wip-us.apache.org/repos/asf/ignite/blob/cdf2d867/modules/web-console/frontend/app/components/page-signin/run.ts ---------------------------------------------------------------------- diff --git a/modules/web-console/frontend/app/components/page-signin/run.ts b/modules/web-console/frontend/app/components/page-signin/run.ts index 4c0a1e6..1d13397 100644 --- a/modules/web-console/frontend/app/components/page-signin/run.ts +++ b/modules/web-console/frontend/app/components/page-signin/run.ts @@ -16,12 +16,14 @@ */ import publicTemplate from '../../../views/public.pug'; -import {UIRouter} from '@uirouter/angularjs'; +import {UIRouter, StateParams} from '@uirouter/angularjs'; import {IIgniteNg1StateDeclaration} from 'app/types'; +export type PageSigninStateParams = StateParams & {activationToken?: string}; + export function registerState($uiRouter: UIRouter) { const state: IIgniteNg1StateDeclaration = { - url: '/signin', + url: '/signin?{activationToken:string}', name: 'signin', views: { '': { @@ -54,6 +56,11 @@ export function registerState($uiRouter: UIRouter) { }, tfMetaTags: { title: 'Sign In' + }, + resolve: { + activationToken() { + return $uiRouter.stateService.transition.params<PageSigninStateParams>().activationToken; + } } }; http://git-wip-us.apache.org/repos/asf/ignite/blob/cdf2d867/modules/web-console/frontend/app/components/page-signin/template.pug ---------------------------------------------------------------------- diff --git a/modules/web-console/frontend/app/components/page-signin/template.pug b/modules/web-console/frontend/app/components/page-signin/template.pug index 2a128a0..c85976f 100644 --- a/modules/web-console/frontend/app/components/page-signin/template.pug +++ b/modules/web-console/frontend/app/components/page-signin/template.pug @@ -17,6 +17,8 @@ include /app/helpers/jade/mixins h3.public-page__title Sign In +p(ng-if='$ctrl.activationToken') + | Please sign in to confirm your registration form(name='$ctrl.form' novalidate ng-submit='$ctrl.signin()') +form-field__email({ label: 'Email:', @@ -45,6 +47,6 @@ form(name='$ctrl.form' novalidate ng-submit='$ctrl.signin()') a(ui-sref='forgotPassword({email: $ctrl.data.email})') Forgot password? button.btn-ignite.btn-ignite--primary( type='submit' - ) Sign In + ) {{ ::$ctrl.activationToken ? "Activate" : "Sign In" }} footer.page-signin__no-account-message | Don't have an account? #[a(ui-sref='signup') Get started] http://git-wip-us.apache.org/repos/asf/ignite/blob/cdf2d867/modules/web-console/frontend/app/components/page-signup-confirmation/component.ts ---------------------------------------------------------------------- diff --git a/modules/web-console/frontend/app/components/page-signup-confirmation/component.ts b/modules/web-console/frontend/app/components/page-signup-confirmation/component.ts new file mode 100644 index 0000000..3a1cc81 --- /dev/null +++ b/modules/web-console/frontend/app/components/page-signup-confirmation/component.ts @@ -0,0 +1,28 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import controller from './controller'; +import templateUrl from './template.tpl.pug'; +import './style.scss'; + +export const component: ng.IComponentOptions = { + controller, + templateUrl, + bindings: { + email: '@' + } +}; http://git-wip-us.apache.org/repos/asf/ignite/blob/cdf2d867/modules/web-console/frontend/app/components/page-signup-confirmation/controller.ts ---------------------------------------------------------------------- diff --git a/modules/web-console/frontend/app/components/page-signup-confirmation/controller.ts b/modules/web-console/frontend/app/components/page-signup-confirmation/controller.ts new file mode 100644 index 0000000..e5f5c89 --- /dev/null +++ b/modules/web-console/frontend/app/components/page-signup-confirmation/controller.ts @@ -0,0 +1,42 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import {default as Auth} from '../../modules/user/Auth.service'; +import {default as MessagesFactory} from '../../services/Messages.service'; + +export default class PageSignupConfirmation { + email: string; + + static $inject = ['Auth', 'IgniteMessages', '$element']; + + constructor(private auth: Auth, private messages: ReturnType<typeof MessagesFactory>, private el: JQLite) { + } + + $postLink() { + this.el.addClass('public-page'); + } + + async resendConfirmation() { + try { + await this.auth.resendSignupConfirmation(this.email); + this.messages.showInfo('Signup confirmation sent, check your email'); + } + catch (e) { + this.messages.showError(e); + } + } +} http://git-wip-us.apache.org/repos/asf/ignite/blob/cdf2d867/modules/web-console/frontend/app/components/page-signup-confirmation/index.ts ---------------------------------------------------------------------- diff --git a/modules/web-console/frontend/app/components/page-signup-confirmation/index.ts b/modules/web-console/frontend/app/components/page-signup-confirmation/index.ts new file mode 100644 index 0000000..8df7532 --- /dev/null +++ b/modules/web-console/frontend/app/components/page-signup-confirmation/index.ts @@ -0,0 +1,23 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import {component} from './component'; +import {state} from './state'; + +export default angular.module('ignite-console.page-signup-confirmation', []) + .run(state) + .component('pageSignupConfirmation', component); http://git-wip-us.apache.org/repos/asf/ignite/blob/cdf2d867/modules/web-console/frontend/app/components/page-signup-confirmation/state.ts ---------------------------------------------------------------------- diff --git a/modules/web-console/frontend/app/components/page-signup-confirmation/state.ts b/modules/web-console/frontend/app/components/page-signup-confirmation/state.ts new file mode 100644 index 0000000..fb1b6a4 --- /dev/null +++ b/modules/web-console/frontend/app/components/page-signup-confirmation/state.ts @@ -0,0 +1,48 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import {UIRouter, StateParams} from '@uirouter/angularjs'; +import {IIgniteNg1StateDeclaration} from '../../types'; +import publicTemplate from '../../../views/public.pug'; + +export type PageSignupConfirmationStateParams = StateParams & {email: string}; + +state.$inject = ['$uiRouter']; + +export function state(router: UIRouter) { + router.stateRegistry.register({ + name: 'signup-confirmation', + url: '/signup-confirmation?{email:string}', + views: { + '': { + template: publicTemplate + }, + 'page@signup-confirmation': { + component: 'pageSignupConfirmation' + } + }, + unsaved: true, + tfMetaTags: { + title: 'Sign Up Confirmation' + }, + resolve: { + email() { + return router.stateService.transition.params<PageSignupConfirmationStateParams>().email; + } + } + } as IIgniteNg1StateDeclaration); +} http://git-wip-us.apache.org/repos/asf/ignite/blob/cdf2d867/modules/web-console/frontend/app/components/page-signup-confirmation/style.scss ---------------------------------------------------------------------- diff --git a/modules/web-console/frontend/app/components/page-signup-confirmation/style.scss b/modules/web-console/frontend/app/components/page-signup-confirmation/style.scss new file mode 100644 index 0000000..d3c062b --- /dev/null +++ b/modules/web-console/frontend/app/components/page-signup-confirmation/style.scss @@ -0,0 +1,22 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +page-signup-confirmation { + display: flex; + flex-direction: column; + flex: 1 0 auto; +} http://git-wip-us.apache.org/repos/asf/ignite/blob/cdf2d867/modules/web-console/frontend/app/components/page-signup-confirmation/template.tpl.pug ---------------------------------------------------------------------- diff --git a/modules/web-console/frontend/app/components/page-signup-confirmation/template.tpl.pug b/modules/web-console/frontend/app/components/page-signup-confirmation/template.tpl.pug new file mode 100644 index 0000000..a1d183c --- /dev/null +++ b/modules/web-console/frontend/app/components/page-signup-confirmation/template.tpl.pug @@ -0,0 +1,24 @@ +//- + Licensed to the Apache Software Foundation (ASF) under one or more + contributor license agreements. See the NOTICE file distributed with + this work for additional information regarding copyright ownership. + The ASF licenses this file to You under the Apache License, Version 2.0 + (the "License"); you may not use this file except in compliance with + the License. You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + + +h3.public-page__title Confirm your email +p + | Thanks For Signing Up! + br + | Please check your email and click link in the message we just sent to <b>{{::$ctrl.email}}</b>. + br + | If you donât receive email try to <a ng-click='$ctrl.resendConfirmation()'>resend confirmation</a> once more. http://git-wip-us.apache.org/repos/asf/ignite/blob/cdf2d867/modules/web-console/frontend/app/components/page-signup/controller.ts ---------------------------------------------------------------------- diff --git a/modules/web-console/frontend/app/components/page-signup/controller.ts b/modules/web-console/frontend/app/components/page-signup/controller.ts index 93d9987..748bf7f 100644 --- a/modules/web-console/frontend/app/components/page-signup/controller.ts +++ b/modules/web-console/frontend/app/components/page-signup/controller.ts @@ -19,6 +19,10 @@ import Auth from '../../modules/user/Auth.service'; import MessagesFactory from '../../services/Messages.service'; import FormUtilsFactoryFactory from '../../services/FormUtils.service'; import {ISignupData} from '../form-signup'; +import {get, eq, pipe} from 'lodash/fp'; + +const EMAIL_NOT_CONFIRMED_ERROR_CODE = 10104; +const isEmailConfirmationError = pipe(get('data.errorCode'), eq(EMAIL_NOT_CONFIRMED_ERROR_CODE)); export default class PageSignup implements ng.IPostLink { form: ng.IFormController; @@ -64,6 +68,9 @@ export default class PageSignup implements ng.IPostLink { return; return this.Auth.signup(this.data).catch((res) => { + if (isEmailConfirmationError(res)) + return; + this.IgniteMessages.showError(null, res.data); this.setServerError(res.data); }); http://git-wip-us.apache.org/repos/asf/ignite/blob/cdf2d867/modules/web-console/frontend/app/modules/user/Auth.service.ts ---------------------------------------------------------------------- diff --git a/modules/web-console/frontend/app/modules/user/Auth.service.ts b/modules/web-console/frontend/app/modules/user/Auth.service.ts index 55956ad..08cf263 100644 --- a/modules/web-console/frontend/app/modules/user/Auth.service.ts +++ b/modules/web-console/frontend/app/modules/user/Auth.service.ts @@ -30,7 +30,7 @@ type SignupUserInfo = { }; type AuthActions = 'signin' | 'signup' | 'password/forgot'; -type AuthOptions = {email:string, password:string}|SignupUserInfo|{email:string}; +type AuthOptions = {email:string, password:string, activationToken?: string}|SignupUserInfo|{email:string}; export default class AuthService { static $inject = ['$http', '$rootScope', '$state', '$window', 'IgniteMessages', 'gettingStarted', 'User']; @@ -49,8 +49,8 @@ export default class AuthService { return this._auth('signup', userInfo, loginAfterSignup); } - signin(email: string, password: string) { - return this._auth('signin', {email, password}); + signin(email: string, password: string, activationToken?: string) { + return this._auth('signin', {email, password, activationToken}); } remindPassword(email: string) { @@ -87,4 +87,12 @@ export default class AuthService { }) .catch((e) => this.Messages.showError(e)); } + + async resendSignupConfirmation(email: string) { + try { + return await this.$http.post('/api/v1/activation/resend/', {email}); + } catch (res) { + throw res.data; + } + } } http://git-wip-us.apache.org/repos/asf/ignite/blob/cdf2d867/modules/web-console/frontend/app/modules/user/emailConfirmationInterceptor.ts ---------------------------------------------------------------------- diff --git a/modules/web-console/frontend/app/modules/user/emailConfirmationInterceptor.ts b/modules/web-console/frontend/app/modules/user/emailConfirmationInterceptor.ts new file mode 100644 index 0000000..ce377b1 --- /dev/null +++ b/modules/web-console/frontend/app/modules/user/emailConfirmationInterceptor.ts @@ -0,0 +1,37 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import {UIRouter} from '@uirouter/angularjs'; + +registerInterceptor.$inject = ['$httpProvider']; + +export function registerInterceptor(http: ng.IHttpProvider) { + emailConfirmationInterceptor.$inject = ['$q', '$injector']; + + function emailConfirmationInterceptor($q: ng.IQService, $injector: ng.auto.IInjectorService): ng.IHttpInterceptor { + return { + responseError(res) { + if (res.status === 403 && res.data && res.data.errorCode === 10104) + $injector.get<UIRouter>('$uiRouter').stateService.go('signup-confirmation', {email: res.data.email}); + + return $q.reject(res); + } + }; + } + + http.interceptors.push(emailConfirmationInterceptor as ng.IHttpInterceptorFactory); +} http://git-wip-us.apache.org/repos/asf/ignite/blob/cdf2d867/modules/web-console/frontend/app/modules/user/user.module.js ---------------------------------------------------------------------- diff --git a/modules/web-console/frontend/app/modules/user/user.module.js b/modules/web-console/frontend/app/modules/user/user.module.js index e11a2d4..c78cc8c 100644 --- a/modules/web-console/frontend/app/modules/user/user.module.js +++ b/modules/web-console/frontend/app/modules/user/user.module.js @@ -20,6 +20,7 @@ import aclData from './permissions'; import Auth from './Auth.service'; import User from './User.service'; +import {registerInterceptor} from './emailConfirmationInterceptor'; /** * @param {ng.auto.IInjectorService} $injector @@ -94,15 +95,17 @@ function run($root, $transitions, AclService, User, Activities) { run.$inject = ['$rootScope', '$transitions', 'AclService', 'User', 'IgniteActivitiesData']; -angular.module('ignite-console.user', [ - 'mm.acl', - 'ignite-console.config', - 'ignite-console.core' -]) -.factory('sessionRecoverer', sessionRecoverer) -.config(['$httpProvider', ($httpProvider) => { - $httpProvider.interceptors.push('sessionRecoverer'); -}]) -.service('Auth', Auth) -.service('User', User) -.run(run); +angular + .module('ignite-console.user', [ + 'mm.acl', + 'ignite-console.config', + 'ignite-console.core' + ]) + .factory('sessionRecoverer', sessionRecoverer) + .config(registerInterceptor) + .config(['$httpProvider', ($httpProvider) => { + $httpProvider.interceptors.push('sessionRecoverer'); + }]) + .service('Auth', Auth) + .service('User', User) + .run(run);