feat(recoverPassword): use loopback
This commit is contained in:
parent
cace29f75c
commit
5339bc1143
|
@ -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();
|
||||
}
|
||||
};
|
||||
};
|
||||
|
|
|
@ -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 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();
|
||||
});
|
||||
};
|
||||
|
|
|
@ -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"]',
|
||||
|
|
|
@ -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>
|
||||
<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>
|
||||
|
|
|
@ -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() {
|
||||
|
|
|
@ -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: $%&.)
|
||||
|
|
|
@ -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: $%&.)
|
||||
|
|
|
@ -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();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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'
|
||||
]);
|
||||
}
|
||||
|
||||
};
|
||||
|
|
Loading…
Reference in New Issue