feat(recoverPassword): use loopback

This commit is contained in:
Alex Moreno 2022-11-09 14:51:30 +01:00
parent cace29f75c
commit 5339bc1143
16 changed files with 149 additions and 113 deletions

View File

@ -1,7 +1,5 @@
const {Email} = require('vn-print');
module.exports = Self => {
Self.remoteMethodCtx('recoverPassword', {
Self.remoteMethod('recoverPassword', {
description: 'Send email to the user',
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 origin = ctx.req.headers.origin;
const ttl = 1209600;
const user = await models.Account.findOne({
fields: ['id', 'name', 'password'],
where: {
email: email
}
});
if (!user)
try {
await models.user.resetPassword({email});
} catch (e) {
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();
}
};
};

View File

@ -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();
});
});

View File

@ -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();
});
});

View File

@ -9,12 +9,15 @@ module.exports = function(Self) {
const headers = httpRequest.headers;
const origin = headers.origin;
const user = await Self.app.models.Account.findById(info.user.id);
const params = {
recipient: info.email,
lang: user.lang,
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();
});
};

View File

@ -29,6 +29,11 @@ export default {
firstModulePinIcon: 'vn-home a:nth-child(1) vn-icon[icon="push_pin"]',
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: {
addAccount: 'vn-user-index button vn-icon[icon="add"]',
newName: 'vn-user-create vn-textfield[ng-model="$ctrl.user.name"]',

View File

@ -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!');
});
});

View File

@ -3,15 +3,15 @@
</vn-layout>
<ui-view
name="login"
ng-if="!$ctrl.showLayout && !$ctrl.isRecover && !$ctrl.isRecover2">
ng-if="$ctrl.isLogin">
</ui-view>
<ui-view
name="recover-password"
ng-if="!$ctrl.showLayout && $ctrl.isRecover">
ng-if="$ctrl.isRecover">
</ui-view>
<ui-view
name="reset-password"
ng-if="!$ctrl.showLayout && $ctrl.isReset">
ng-if="$ctrl.isReset">
</ui-view>
<vn-snackbar vn-id="snackbar"></vn-snackbar>
<vn-debug-info></vn-debug-info>

View File

@ -15,7 +15,11 @@ export default class App extends Component {
get showLayout() {
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() {

View File

@ -1,4 +1,8 @@
User: User
Password: Password
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: $%&.)

View File

@ -7,3 +7,10 @@ I do not remember my password: No recuerdo mi 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
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: $%&.)

View File

@ -13,15 +13,19 @@ export default class Controller {
});
}
goToLogin() {
this.vnApp.showSuccess(this.$translate.instant('Notification sent!'));
this.$state.go('login');
}
submit() {
const params = {
email: this.email
};
this.$http.post('users/reset', params)
this.$http.post('Accounts/recoverPassword', params)
.then(() => {
this.vnApp.showSuccess(this.$translate.instant('Notification sent!'));
this.$state.go('login');
this.goToLogin();
});
}
}

View File

@ -14,7 +14,7 @@
type="password">
</vn-textfield>
<div class="footer">
<vn-submit label="Change password"></vn-submit>
<vn-submit label="Reset password"></vn-submit>
<div class="spinner-wrapper">
<vn-spinner enable="$ctrl.loading"></vn-spinner>
</div>

View File

@ -3,11 +3,6 @@ import Descriptor from 'salix/components/descriptor';
import UserError from 'core/lib/user-error';
class Controller extends Descriptor {
constructor($element, $scope, $location) {
super($element, $scope);
this.$location = $location;
}
get user() {
return this.entity;
}
@ -29,11 +24,6 @@ class Controller extends Descriptor {
.then(res => this.hasAccount = res.data.exists);
}
$onInit() {
if (this.$params.access_token)
this.onChangePassClick(false);
}
onDelete() {
return this.$http.delete(`Accounts/${this.id}`)
.then(() => this.$state.go('account.index'))
@ -110,8 +100,6 @@ class Controller extends Descriptor {
}
}
Controller.$inject = ['$element', '$scope', '$location'];
ngModule.component('vnUserDescriptor', {
template: require('./index.html'),
controller: Controller,

View File

@ -94,15 +94,4 @@ describe('component vnUserDescriptor', () => {
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();
});
});
});

View File

@ -74,7 +74,7 @@
}
},
{
"url": "/basic-data?access_token&emailConfirmed",
"url": "/basic-data?emailConfirmed",
"state": "account.card.basicData",
"component": "vn-user-basic-data",
"description": "Basic data",

View File

@ -8,10 +8,12 @@ module.exports = {
this.transporter = nodemailer.createTransport(config.smtp);
},
send(options) {
async send(options) {
options.from = `${config.app.senderName} <${config.app.senderEmail}>`;
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)
return Promise.resolve(true);
@ -24,29 +26,35 @@ module.exports = {
throw err;
}).finally(async() => {
const attachments = [];
if (options.attachments) {
for (let attachment of options.attachments) {
const fileName = attachment.filename;
const filePath = attachment.path;
if (fileName.includes('.png')) continue;
if (fileName || filePath)
attachments.push(filePath ? filePath : fileName);
}
}
const fileNames = attachments.join(',\n');
await db.rawSql(`
INSERT INTO vn.mail (receiver, replyTo, sent, subject, body, attachment, status)
VALUES (?, ?, 1, ?, ?, ?, ?)`, [
options.to,
options.replyTo,
options.subject,
options.text || options.html,
fileNames,
error && error.message || 'Sent'
]);
await this.mailLog(options, error);
});
},
async mailLog(options, error) {
const attachments = [];
if (options.attachments) {
for (let attachment of options.attachments) {
const fileName = attachment.filename;
const filePath = attachment.path;
if (fileName.includes('.png')) continue;
if (fileName || filePath)
attachments.push(filePath ? filePath : fileName);
}
}
const fileNames = attachments.join(',\n');
await db.rawSql(`
INSERT INTO vn.mail (receiver, replyTo, sent, subject, body, attachment, status)
VALUES (?, ?, 1, ?, ?, ?, ?)`, [
options.to,
options.replyTo,
options.subject,
options.text || options.html,
fileNames,
error && error.message || 'Sent'
]);
}
};