4077-login_recover-password & account_verifyEmail #1063
|
@ -1,7 +1,5 @@
|
||||||
const {Email} = require('vn-print');
|
|
||||||
|
|
||||||
module.exports = Self => {
|
module.exports = Self => {
|
||||||
Self.remoteMethodCtx('recoverPassword', {
|
Self.remoteMethod('recoverPassword', {
|
||||||
description: 'Send email to the user',
|
description: 'Send email to the user',
|
||||||
accepts: [
|
accepts: [
|
||||||
{
|
{
|
||||||
|
@ -17,34 +15,13 @@ module.exports = Self => {
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
Self.recoverPassword = async function(ctx, email) {
|
Self.recoverPassword = async function(email) {
|
||||||
const models = Self.app.models;
|
const models = Self.app.models;
|
||||||
const origin = ctx.req.headers.origin;
|
|
||||||
const ttl = 1209600;
|
|
||||||
|
|
||||||
const user = await models.Account.findOne({
|
try {
|
||||||
fields: ['id', 'name', 'password'],
|
await models.user.resetPassword({email});
|
||||||
where: {
|
} catch (e) {
|
||||||
email: email
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!user)
|
|
||||||
return;
|
return;
|
||||||
|
}
|
||||||
const token = await models.AccessToken.create({
|
|
||||||
ttl: ttl,
|
|
||||||
userId: user.id
|
|
||||||
});
|
|
||||||
|
|
||||||
const url = `${origin}/#!/account/${user.id}/basic-data?access_token=${token.id}`;
|
|
||||||
const params = {
|
|
||||||
recipient: email,
|
|
||||||
url: url
|
|
||||||
};
|
|
||||||
|
|
||||||
const sendEmail = new Email('recover-password', params);
|
|
||||||
|
|
||||||
return sendEmail.send();
|
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
|
@ -1,25 +0,0 @@
|
||||||
const models = require('vn-loopback/server/server').models;
|
|
||||||
|
|
||||||
describe('account recoverPassword()', () => {
|
|
||||||
const ctx = {
|
|
||||||
req: {
|
|
||||||
headers: {origin: 'http://localhost:5000'}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
ctx.req.__ = value => {
|
|
||||||
return value;
|
|
||||||
};
|
|
||||||
|
|
||||||
it('should update password when it passes requirements', async() => {
|
|
||||||
const user = await models.Account.findById(1107);
|
|
||||||
await models.Account.recoverPassword(ctx, user.email);
|
|
||||||
|
|
||||||
const [result] = await models.AccessToken.find({
|
|
||||||
where: {
|
|
||||||
userId: user.id
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
expect(result).toBeDefined();
|
|
||||||
});
|
|
||||||
});
|
|
|
@ -0,0 +1,32 @@
|
||||||
|
const models = require('vn-loopback/server/server').models;
|
||||||
|
const LoopBackContext = require('loopback-context');
|
||||||
|
|
||||||
|
describe('account recoverPassword()', () => {
|
||||||
|
const userId = 1107;
|
||||||
|
|
||||||
|
const activeCtx = {
|
||||||
|
accessToken: {userId: userId},
|
||||||
|
http: {
|
||||||
|
req: {
|
||||||
|
headers: {origin: 'http://localhost'}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
spyOn(LoopBackContext, 'getCurrentContext').and.returnValue({
|
||||||
|
active: activeCtx
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should send email with token', async() => {
|
||||||
|
const userId = 1107;
|
||||||
|
const user = await models.Account.findById(userId);
|
||||||
|
|
||||||
|
await models.Account.recoverPassword(user.email);
|
||||||
|
|
||||||
|
const result = await models.AccessToken.findOne({where: {userId: userId}});
|
||||||
|
|
||||||
|
expect(result).toBeDefined();
|
||||||
|
});
|
||||||
|
});
|
|
@ -9,12 +9,15 @@ module.exports = function(Self) {
|
||||||
const headers = httpRequest.headers;
|
const headers = httpRequest.headers;
|
||||||
const origin = headers.origin;
|
const origin = headers.origin;
|
||||||
|
|
||||||
|
const user = await Self.app.models.Account.findById(info.user.id);
|
||||||
const params = {
|
const params = {
|
||||||
|
recipient: info.email,
|
||||||
|
lang: user.lang,
|
||||||
url: `${origin}/#!/login/reset-password?access_token=${info.accessToken.id}`
|
url: `${origin}/#!/login/reset-password?access_token=${info.accessToken.id}`
|
||||||
};
|
};
|
||||||
|
|
||||||
const sendEmail = new Email('recover-password', params);
|
const email = new Email('recover-password', params);
|
||||||
|
|
||||||
return sendEmail.send();
|
return email.send();
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
|
@ -29,6 +29,11 @@ export default {
|
||||||
firstModulePinIcon: 'vn-home a:nth-child(1) vn-icon[icon="push_pin"]',
|
firstModulePinIcon: 'vn-home a:nth-child(1) vn-icon[icon="push_pin"]',
|
||||||
firstModuleRemovePinIcon: 'vn-home a:nth-child(1) vn-icon[icon="remove_circle"]'
|
firstModuleRemovePinIcon: 'vn-home a:nth-child(1) vn-icon[icon="remove_circle"]'
|
||||||
},
|
},
|
||||||
|
recoverPassword: {
|
||||||
|
recoverPasswordButton: 'vn-login a[ui-sref="login.recover-password"]',
|
||||||
|
email: 'vn-recover-password vn-textfield[ng-model="$ctrl.email"]',
|
||||||
|
sendEmailButton: 'vn-recover-password vn-submit',
|
||||||
|
},
|
||||||
accountIndex: {
|
accountIndex: {
|
||||||
addAccount: 'vn-user-index button vn-icon[icon="add"]',
|
addAccount: 'vn-user-index button vn-icon[icon="add"]',
|
||||||
newName: 'vn-user-create vn-textfield[ng-model="$ctrl.user.name"]',
|
newName: 'vn-user-create vn-textfield[ng-model="$ctrl.user.name"]',
|
||||||
|
|
|
@ -0,0 +1,40 @@
|
||||||
|
import selectors from '../../helpers/selectors';
|
||||||
|
import getBrowser from '../../helpers/puppeteer';
|
||||||
|
|
||||||
|
fdescribe('Login path', async() => {
|
||||||
|
let browser;
|
||||||
|
let page;
|
||||||
|
|
||||||
|
beforeAll(async() => {
|
||||||
|
browser = await getBrowser();
|
||||||
|
page = browser.page;
|
||||||
|
|
||||||
|
await page.waitToClick(selectors.recoverPassword.recoverPasswordButton);
|
||||||
|
await page.waitForState('login.recover-password');
|
||||||
|
});
|
||||||
|
|
||||||
|
afterAll(async() => {
|
||||||
|
await browser.close();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should not throw error if not exist user', async() => {
|
||||||
|
await page.write(selectors.recoverPassword.email, 'fakeEmail@mydomain.com');
|
||||||
|
await page.waitToClick(selectors.recoverPassword.sendEmailButton);
|
||||||
|
|
||||||
|
const message = await page.waitForSnackbar();
|
||||||
|
|
||||||
|
expect(message.text).toContain('Notification sent!');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should send email', async() => {
|
||||||
|
await page.waitForState('login');
|
||||||
|
await page.waitToClick(selectors.recoverPassword.recoverPasswordButton);
|
||||||
|
|
||||||
|
await page.write(selectors.recoverPassword.email, 'BruceWayne@mydomain.com');
|
||||||
|
await page.waitToClick(selectors.recoverPassword.sendEmailButton);
|
||||||
|
const message = await page.waitForSnackbar();
|
||||||
|
await page.waitForState('login');
|
||||||
|
|
||||||
|
expect(message.text).toContain('Notification sent!');
|
||||||
|
});
|
||||||
|
});
|
|
@ -3,15 +3,15 @@
|
||||||
</vn-layout>
|
</vn-layout>
|
||||||
<ui-view
|
<ui-view
|
||||||
name="login"
|
name="login"
|
||||||
ng-if="!$ctrl.showLayout && !$ctrl.isRecover && !$ctrl.isRecover2">
|
ng-if="$ctrl.isLogin">
|
||||||
</ui-view>
|
</ui-view>
|
||||||
<ui-view
|
<ui-view
|
||||||
name="recover-password"
|
name="recover-password"
|
||||||
ng-if="!$ctrl.showLayout && $ctrl.isRecover">
|
ng-if="$ctrl.isRecover">
|
||||||
</ui-view>
|
</ui-view>
|
||||||
<ui-view
|
<ui-view
|
||||||
name="reset-password"
|
name="reset-password"
|
||||||
ng-if="!$ctrl.showLayout && $ctrl.isReset">
|
ng-if="$ctrl.isReset">
|
||||||
</ui-view>
|
</ui-view>
|
||||||
<vn-snackbar vn-id="snackbar"></vn-snackbar>
|
<vn-snackbar vn-id="snackbar"></vn-snackbar>
|
||||||
<vn-debug-info></vn-debug-info>
|
<vn-debug-info></vn-debug-info>
|
||||||
|
|
|
@ -15,7 +15,11 @@ export default class App extends Component {
|
||||||
|
|
||||||
get showLayout() {
|
get showLayout() {
|
||||||
let state = this.$state.current.name;
|
let state = this.$state.current.name;
|
||||||
return state && state != 'login' && !this.isRecover && !this.isReset;
|
return state && !this.isLogin && !this.isRecover && !this.isReset;
|
||||||
|
}
|
||||||
|
|
||||||
|
get isLogin() {
|
||||||
|
return this.$state.current.name == 'login';
|
||||||
}
|
}
|
||||||
|
|
||||||
get isRecover() {
|
get isRecover() {
|
||||||
|
|
|
@ -2,3 +2,7 @@ User: User
|
||||||
Password: Password
|
Password: Password
|
||||||
Do not close session: Do not close session
|
Do not close session: Do not close session
|
||||||
Enter: Enter
|
Enter: Enter
|
||||||
|
Password requirements: >
|
||||||
|
The password must have at least {{ length }} length characters,
|
||||||
|
{{nAlpha}} alphabetic characters, {{nUpper}} capital letters, {{nDigits}}
|
||||||
|
digits and {{nPunct}} symbols (Ex: $%&.)
|
||||||
|
|
|
@ -7,3 +7,10 @@ I do not remember my password: No recuerdo mi contraseña
|
||||||
Recover password: Recuperar contraseña
|
Recover password: Recuperar contraseña
|
||||||
We will sent you an email to recover your password: Te enviaremos un correo para restablecer tu contraseña
|
We will sent you an email to recover your password: Te enviaremos un correo para restablecer tu contraseña
|
||||||
Notification sent!: ¡Notificación enviada!
|
Notification sent!: ¡Notificación enviada!
|
||||||
|
Reset password: Restrablecer contraseña
|
||||||
|
New password: Nueva contraseña
|
||||||
|
Repeat password: Repetir contraseña
|
||||||
|
Password requirements: >
|
||||||
|
La contraseña debe tener al menos {{ length }} caracteres de longitud,
|
||||||
|
{{nAlpha}} caracteres alfabéticos, {{nUpper}} letras mayúsculas, {{nDigits}}
|
||||||
|
dígitos y {{nPunct}} símbolos (Ej: $%&.)
|
||||||
|
|
|
@ -13,15 +13,19 @@ export default class Controller {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
goToLogin() {
|
||||||
|
this.vnApp.showSuccess(this.$translate.instant('Notification sent!'));
|
||||||
|
this.$state.go('login');
|
||||||
|
}
|
||||||
|
|
||||||
submit() {
|
submit() {
|
||||||
const params = {
|
const params = {
|
||||||
email: this.email
|
email: this.email
|
||||||
};
|
};
|
||||||
|
|
||||||
this.$http.post('users/reset', params)
|
this.$http.post('Accounts/recoverPassword', params)
|
||||||
.then(() => {
|
.then(() => {
|
||||||
this.vnApp.showSuccess(this.$translate.instant('Notification sent!'));
|
this.goToLogin();
|
||||||
this.$state.go('login');
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -14,7 +14,7 @@
|
||||||
type="password">
|
type="password">
|
||||||
</vn-textfield>
|
</vn-textfield>
|
||||||
<div class="footer">
|
<div class="footer">
|
||||||
<vn-submit label="Change password"></vn-submit>
|
<vn-submit label="Reset password"></vn-submit>
|
||||||
<div class="spinner-wrapper">
|
<div class="spinner-wrapper">
|
||||||
<vn-spinner enable="$ctrl.loading"></vn-spinner>
|
<vn-spinner enable="$ctrl.loading"></vn-spinner>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -3,11 +3,6 @@ import Descriptor from 'salix/components/descriptor';
|
||||||
import UserError from 'core/lib/user-error';
|
import UserError from 'core/lib/user-error';
|
||||||
|
|
||||||
class Controller extends Descriptor {
|
class Controller extends Descriptor {
|
||||||
constructor($element, $scope, $location) {
|
|
||||||
super($element, $scope);
|
|
||||||
this.$location = $location;
|
|
||||||
}
|
|
||||||
|
|
||||||
get user() {
|
get user() {
|
||||||
return this.entity;
|
return this.entity;
|
||||||
}
|
}
|
||||||
|
@ -29,11 +24,6 @@ class Controller extends Descriptor {
|
||||||
.then(res => this.hasAccount = res.data.exists);
|
.then(res => this.hasAccount = res.data.exists);
|
||||||
}
|
}
|
||||||
|
|
||||||
$onInit() {
|
|
||||||
if (this.$params.access_token)
|
|
||||||
this.onChangePassClick(false);
|
|
||||||
}
|
|
||||||
|
|
||||||
onDelete() {
|
onDelete() {
|
||||||
return this.$http.delete(`Accounts/${this.id}`)
|
return this.$http.delete(`Accounts/${this.id}`)
|
||||||
.then(() => this.$state.go('account.index'))
|
.then(() => this.$state.go('account.index'))
|
||||||
|
@ -110,8 +100,6 @@ class Controller extends Descriptor {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Controller.$inject = ['$element', '$scope', '$location'];
|
|
||||||
|
|
||||||
ngModule.component('vnUserDescriptor', {
|
ngModule.component('vnUserDescriptor', {
|
||||||
template: require('./index.html'),
|
template: require('./index.html'),
|
||||||
controller: Controller,
|
controller: Controller,
|
||||||
|
|
|
@ -94,15 +94,4 @@ describe('component vnUserDescriptor', () => {
|
||||||
expect(controller.emit).toHaveBeenCalledWith('change');
|
expect(controller.emit).toHaveBeenCalledWith('change');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('onInit()', () => {
|
|
||||||
it('should open onChangePassClick popup', () => {
|
|
||||||
controller.$params = {access_token: 'RANDOM_TOKEN'};
|
|
||||||
jest.spyOn(controller, 'onChangePassClick');
|
|
||||||
|
|
||||||
controller.$onInit();
|
|
||||||
|
|
||||||
expect(controller.onChangePassClick).toHaveBeenCalled();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|
|
@ -74,7 +74,7 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"url": "/basic-data?access_token&emailConfirmed",
|
"url": "/basic-data?emailConfirmed",
|
||||||
"state": "account.card.basicData",
|
"state": "account.card.basicData",
|
||||||
"component": "vn-user-basic-data",
|
"component": "vn-user-basic-data",
|
||||||
"description": "Basic data",
|
"description": "Basic data",
|
||||||
|
|
|
@ -8,10 +8,12 @@ module.exports = {
|
||||||
this.transporter = nodemailer.createTransport(config.smtp);
|
this.transporter = nodemailer.createTransport(config.smtp);
|
||||||
},
|
},
|
||||||
|
|
||||||
send(options) {
|
async send(options) {
|
||||||
options.from = `${config.app.senderName} <${config.app.senderEmail}>`;
|
options.from = `${config.app.senderName} <${config.app.senderEmail}>`;
|
||||||
|
|
||||||
if (process.env.NODE_ENV !== 'production') {
|
if (process.env.NODE_ENV !== 'production') {
|
||||||
|
const notProductionError = {message: 'This not production, this email not sended'};
|
||||||
|
|||||||
|
await this.mailLog(options, notProductionError);
|
||||||
if (!config.smtp.auth.user)
|
if (!config.smtp.auth.user)
|
||||||
return Promise.resolve(true);
|
return Promise.resolve(true);
|
||||||
|
|
||||||
|
@ -24,6 +26,11 @@ module.exports = {
|
||||||
|
|
||||||
throw err;
|
throw err;
|
||||||
}).finally(async() => {
|
}).finally(async() => {
|
||||||
|
await this.mailLog(options, error);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
async mailLog(options, error) {
|
||||||
const attachments = [];
|
const attachments = [];
|
||||||
if (options.attachments) {
|
if (options.attachments) {
|
||||||
for (let attachment of options.attachments) {
|
for (let attachment of options.attachments) {
|
||||||
|
@ -37,6 +44,7 @@ module.exports = {
|
||||||
}
|
}
|
||||||
|
|
||||||
const fileNames = attachments.join(',\n');
|
const fileNames = attachments.join(',\n');
|
||||||
|
|
||||||
await db.rawSql(`
|
await db.rawSql(`
|
||||||
INSERT INTO vn.mail (receiver, replyTo, sent, subject, body, attachment, status)
|
INSERT INTO vn.mail (receiver, replyTo, sent, subject, body, attachment, status)
|
||||||
VALUES (?, ?, 1, ?, ?, ?, ?)`, [
|
VALUES (?, ?, 1, ?, ?, ?, ?)`, [
|
||||||
|
@ -47,6 +55,6 @@ module.exports = {
|
||||||
fileNames,
|
fileNames,
|
||||||
error && error.message || 'Sent'
|
error && error.message || 'Sent'
|
||||||
]);
|
]);
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
};
|
};
|
||||||
|
|
Loading…
Reference in New Issue
He modificado esta parte porque como esta diseñado print. Solo inserta en mail(a modo de log) cuando realmente se envia el correo (producción). Haciendo este cambio podemos ver los logs tanto en local como en test.
Aunque al final no he usado esta funcionalidad para los test creo que es util para hacer pruebas en test