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});
|
||||||
alexm marked this conversation as resolved
Outdated
|
|||||||
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">
|
||||||
alexm marked this conversation as resolved
Outdated
juan
commented
No estas fent us de les rutes anidades No estas fent us de les rutes anidades
|
|||||||
</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;
|
||||||
alexm marked this conversation as resolved
Outdated
juan
commented
Açò funciona en tots els casos? Que passa si un estat conte login? Açò funciona en **tots** els casos? Que passa si un estat conte login?
|
|||||||
|
}
|
||||||
|
|
||||||
|
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'};
|
||||||
alexm
commented
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 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
|
|||||||
|
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 dejado esta ruta(recoverPassword) porque si se llama directamente resetPassword y el correo que se le pasa no pertenece a un usuario, devuelve un error al frontend.
Usando una ruta con try catch, hacemos que no devuelva nunca error y asi no pueden saber si ese correo es de un usuario nuestro o no.
Nomes deuria de ignorar el error de tipo "usuario no existe", tots els demes deuria de rellançarlos