Commit a05a7c31aa91e0ccbd094fe75b09fd7cfe576b97
1 parent
33647b98
Exists in
forgot_password
Ticket #107 Implementation of new password generation
After clicking on "Forgot your password?", the user receives an email with a link to create a new password.
Showing
7 changed files
with
229 additions
and
1 deletions
Show diff stats
| @@ -0,0 +1,87 @@ | @@ -0,0 +1,87 @@ | ||
| 1 | +import { ComponentTestHelper, createClass } from "../../spec/component-test-helper"; | ||
| 2 | +import * as helpers from "../../spec/helpers"; | ||
| 3 | +import { PasswordComponent } from "./new-password.component"; | ||
| 4 | + | ||
| 5 | + | ||
| 6 | +describe("Password Component", () => { | ||
| 7 | + const htmlTemplate: string = '<new-password></new-password>'; | ||
| 8 | + | ||
| 9 | + let helper: ComponentTestHelper<PasswordComponent>; | ||
| 10 | + let passwordService = helpers.mocks.passwordService; | ||
| 11 | + let stateService = jasmine.createSpyObj("$state", ["transitionTo"]); | ||
| 12 | + let stateParams = jasmine.createSpyObj("$stateParams", ["code"]); | ||
| 13 | + let notificationService = helpers.mocks.notificationService; | ||
| 14 | + notificationService.success = jasmine.createSpy('success'); | ||
| 15 | + notificationService.error = jasmine.createSpy('error'); | ||
| 16 | + | ||
| 17 | + | ||
| 18 | + let data: any; | ||
| 19 | + | ||
| 20 | + beforeEach(() => { | ||
| 21 | + angular.mock.module('templates'); | ||
| 22 | + angular.mock.module('ngSanitize'); | ||
| 23 | + angular.mock.module('ngMessages'); | ||
| 24 | + angular.mock.module('ngPassword'); | ||
| 25 | + }); | ||
| 26 | + | ||
| 27 | + beforeEach((done) => { | ||
| 28 | + let cls = createClass({ | ||
| 29 | + template: htmlTemplate, | ||
| 30 | + directives: [PasswordComponent], | ||
| 31 | + providers: [ | ||
| 32 | + helpers.createProviderToValue('$state', stateService), | ||
| 33 | + helpers.createProviderToValue('$stateParams', stateParams), | ||
| 34 | + helpers.createProviderToValue('PasswordService', passwordService), | ||
| 35 | + helpers.createProviderToValue('NotificationService', notificationService), | ||
| 36 | + ] | ||
| 37 | + }); | ||
| 38 | + helper = new ComponentTestHelper<PasswordComponent>(cls, done); | ||
| 39 | + }); | ||
| 40 | + | ||
| 41 | + it('new password page was rendered', () => { | ||
| 42 | + expect(helper.debugElement.query('div.new-password-page').length).toEqual(1); | ||
| 43 | + }); | ||
| 44 | + | ||
| 45 | + it("changes the user password", done => { | ||
| 46 | + data = { | ||
| 47 | + code: '1234567890', | ||
| 48 | + password: 'test', | ||
| 49 | + passwordConfirmation: 'test' | ||
| 50 | + }; | ||
| 51 | + | ||
| 52 | + helper.component.code = data.code; | ||
| 53 | + helper.component.password = data.password; | ||
| 54 | + helper.component.passwordConfirmation = data.passwordConfirmation; | ||
| 55 | + | ||
| 56 | + passwordService.new_password = jasmine.createSpy("new_password").and.returnValue(Promise.resolve()); | ||
| 57 | + | ||
| 58 | + helper.component.sendNewPassword(); | ||
| 59 | + expect(passwordService.new_password).toHaveBeenCalledWith('1234567890', 'test', 'test'); | ||
| 60 | + | ||
| 61 | + expect(notificationService.success).toHaveBeenCalled(); | ||
| 62 | + | ||
| 63 | + done(); | ||
| 64 | + }); | ||
| 65 | + | ||
| 66 | + it("fails when try to change the user password", done => { | ||
| 67 | + data = { | ||
| 68 | + code: '1234567890', | ||
| 69 | + password: 'test', | ||
| 70 | + passwordConfirmation: 'test-invalid' | ||
| 71 | + }; | ||
| 72 | + | ||
| 73 | + helper.component.code = data.code; | ||
| 74 | + helper.component.password = data.password; | ||
| 75 | + helper.component.passwordConfirmation = data.passwordConfirmation; | ||
| 76 | + | ||
| 77 | + passwordService.new_password = jasmine.createSpy("new_password").and.returnValue(Promise.reject({data: {message: 'Error'}})); | ||
| 78 | + | ||
| 79 | + helper.component.sendNewPassword(); | ||
| 80 | + expect(passwordService.new_password).toHaveBeenCalledWith('1234567890', 'test', 'test-invalid'); | ||
| 81 | + | ||
| 82 | + expect(notificationService.error).toHaveBeenCalled(); | ||
| 83 | + | ||
| 84 | + done(); | ||
| 85 | + }); | ||
| 86 | + | ||
| 87 | +}); |
| @@ -0,0 +1,36 @@ | @@ -0,0 +1,36 @@ | ||
| 1 | +import { StateConfig, Component, Inject, provide } from 'ng-forward'; | ||
| 2 | + | ||
| 3 | +import { PasswordService } from "../../lib/ng-noosfero-api/http/password.service"; | ||
| 4 | +import { NotificationService } from "./../shared/services/notification.service"; | ||
| 5 | +import { AuthController } from "./auth.controller"; | ||
| 6 | + | ||
| 7 | +@Component({ | ||
| 8 | + selector: 'new-password', | ||
| 9 | + templateUrl: 'app/login/new-password.html', | ||
| 10 | + providers: [provide('passwordService', { useClass: PasswordService })] | ||
| 11 | +}) | ||
| 12 | +@Inject(PasswordService, "$state", "$stateParams", NotificationService) | ||
| 13 | +export class PasswordComponent { | ||
| 14 | + | ||
| 15 | + code: string; | ||
| 16 | + password: string; | ||
| 17 | + passwordConfirmation: string; | ||
| 18 | + | ||
| 19 | + constructor( | ||
| 20 | + public passwordService: PasswordService, | ||
| 21 | + private $state: ng.ui.IStateService, | ||
| 22 | + private $stateParams: ng.ui.IStateParamsService, | ||
| 23 | + private notificationService: NotificationService) { | ||
| 24 | + | ||
| 25 | + this.code = this.$stateParams['code']; | ||
| 26 | + } | ||
| 27 | + | ||
| 28 | + sendNewPassword() { | ||
| 29 | + this.passwordService.new_password(this.code, this.password, this.passwordConfirmation).then((response) => { | ||
| 30 | + this.notificationService.success({ title: "newPasswd.success.title", message: "newPasswd.success.message", timer: 5000 }); | ||
| 31 | + this.$state.transitionTo('main.environment'); | ||
| 32 | + }).catch((response) => { | ||
| 33 | + this.notificationService.error({ title: "newPasswd.failed.title", message: "newPasswd.failed.message" }); | ||
| 34 | + }); | ||
| 35 | + } | ||
| 36 | +} |
| @@ -0,0 +1,31 @@ | @@ -0,0 +1,31 @@ | ||
| 1 | +<div class="new-password-page col-xs-4 col-xs-offset-4"> | ||
| 2 | + <h1>{{"new_password.welcomeMessageTitle" | translate }}</h1> | ||
| 3 | + | ||
| 4 | + <form name="newPasswdForm"> | ||
| 5 | + <div class="form-group" ng-class="{'has-error': newPasswdForm.password.$invalid && newPasswdForm.password.$touched }"> | ||
| 6 | + <div class="input-group"> | ||
| 7 | + <span class="input-group-addon"><i class="fa fa-lock"></i></span> | ||
| 8 | + <input type="password" class="form-control" id="password" name="password" placeholder="{{'account.register.passwordLabel' | translate }}" ng-model="vm.password" required minlength="4"> | ||
| 9 | + </div> | ||
| 10 | + <div class="help-block" ng-show="newPasswdForm.password.$touched" ng-messages="newPasswdForm.password.$error"> | ||
| 11 | + <ul class="list-unstyled"> | ||
| 12 | + <li ng-messages-include="app/shared/messages/form-errors.html"></li> | ||
| 13 | + </ul> | ||
| 14 | + </div> | ||
| 15 | + </div> | ||
| 16 | + | ||
| 17 | + <div class="form-group" ng-class="{'has-error': newPasswdForm.passwordConfirm.$invalid && newPasswdForm.passwordConfirm.$touched }"> | ||
| 18 | + <div class="input-group"> | ||
| 19 | + <span class="input-group-addon"><i class="fa fa-unlock-alt"></i></span> | ||
| 20 | + <input type="password" class="form-control" id="passwordConfirm" name="passwordConfirm" match-password="password" placeholder="{{'account.register.passwordConfirmationLabel' | translate}}" ng-model="vm.passwordConfirmation" required> | ||
| 21 | + </div> | ||
| 22 | + <div class="help-block" ng-show="newPasswdForm.passwordConfirm.$touched" ng-messages="newPasswdForm.passwordConfirm.$error"> | ||
| 23 | + <ul class="list-unstyled"> | ||
| 24 | + <li ng-messages-include="app/shared/messages/form-errors.html"></li> | ||
| 25 | + <li ng-message="passwordMatch" translate="messages.invalid.passwordMatch"></li> | ||
| 26 | + </ul> | ||
| 27 | + </div> | ||
| 28 | + </div> | ||
| 29 | + <button type="submit" class="btn btn-default btn-block" ng-click="vm.sendNewPassword()" ng-disabled="newPasswdForm.$invalid">{{"newPasswdForm.submit" | translate}}</button> | ||
| 30 | + </form> | ||
| 31 | +</div> |
src/app/main/main.component.ts
| @@ -4,6 +4,7 @@ import { ArticleBlogComponent } from "./../article/types/blog/blog.component"; | @@ -4,6 +4,7 @@ import { ArticleBlogComponent } from "./../article/types/blog/blog.component"; | ||
| 4 | 4 | ||
| 5 | import { ArticleViewComponent } from "./../article/article-default-view.component"; | 5 | import { ArticleViewComponent } from "./../article/article-default-view.component"; |
| 6 | 6 | ||
| 7 | +import { PasswordComponent } from "../login/new-password.component"; | ||
| 7 | import { ProfileComponent } from "../profile/profile.component"; | 8 | import { ProfileComponent } from "../profile/profile.component"; |
| 8 | import { BoxesComponent } from "../layout/boxes/boxes.component"; | 9 | import { BoxesComponent } from "../layout/boxes/boxes.component"; |
| 9 | import { BlockContentComponent } from "../layout/blocks/block-content.component"; | 10 | import { BlockContentComponent } from "../layout/blocks/block-content.component"; |
| @@ -120,7 +121,8 @@ export class EnvironmentContent { | @@ -120,7 +121,8 @@ export class EnvironmentContent { | ||
| 120 | MembersBlockComponent, NoosferoTemplate, DateFormat, RawHTMLBlockComponent, StatisticsBlockComponent, | 121 | MembersBlockComponent, NoosferoTemplate, DateFormat, RawHTMLBlockComponent, StatisticsBlockComponent, |
| 121 | LoginBlockComponent, CustomContentComponent, PermissionDirective, SearchFormComponent, SearchComponent, | 122 | LoginBlockComponent, CustomContentComponent, PermissionDirective, SearchFormComponent, SearchComponent, |
| 122 | PersonTagsPluginInterestsBlockComponent, TagsBlockComponent, RecentActivitiesPluginActivitiesBlockComponent, | 123 | PersonTagsPluginInterestsBlockComponent, TagsBlockComponent, RecentActivitiesPluginActivitiesBlockComponent, |
| 123 | - ProfileImagesPluginProfileImagesBlockComponent, BlockComponent, RegisterComponent, TasksMenuComponent, TaskListComponent | 124 | + ProfileImagesPluginProfileImagesBlockComponent, BlockComponent, RegisterComponent, TasksMenuComponent, TaskListComponent, |
| 125 | + PasswordComponent | ||
| 124 | ].concat(plugins.mainComponents).concat(plugins.hotspots), | 126 | ].concat(plugins.mainComponents).concat(plugins.hotspots), |
| 125 | providers: [AuthService, SessionService, NotificationService, BodyStateClassesService, | 127 | providers: [AuthService, SessionService, NotificationService, BodyStateClassesService, |
| 126 | "ngAnimate", "ngCookies", "ngStorage", "ngTouch", | 128 | "ngAnimate", "ngCookies", "ngStorage", "ngTouch", |
| @@ -172,6 +174,18 @@ export class EnvironmentContent { | @@ -172,6 +174,18 @@ export class EnvironmentContent { | ||
| 172 | } | 174 | } |
| 173 | }, | 175 | }, |
| 174 | { | 176 | { |
| 177 | + url: "/account/new_password/:code", | ||
| 178 | + component: PasswordComponent, | ||
| 179 | + name: 'main.newPasswd', | ||
| 180 | + views: { | ||
| 181 | + "content": { | ||
| 182 | + templateUrl: "app/login/new-password.html", | ||
| 183 | + controller: PasswordComponent, | ||
| 184 | + controllerAs: "vm" | ||
| 185 | + } | ||
| 186 | + } | ||
| 187 | + }, | ||
| 188 | + { | ||
| 175 | url: "^/:profile", | 189 | url: "^/:profile", |
| 176 | abstract: true, | 190 | abstract: true, |
| 177 | component: ProfileComponent, | 191 | component: ProfileComponent, |
| @@ -0,0 +1,41 @@ | @@ -0,0 +1,41 @@ | ||
| 1 | +import { PasswordService } from "./password.service"; | ||
| 2 | + | ||
| 3 | +describe("Services", () => { | ||
| 4 | + | ||
| 5 | + describe("Password Service", () => { | ||
| 6 | + | ||
| 7 | + let $httpBackend: ng.IHttpBackendService; | ||
| 8 | + let passwordService: PasswordService; | ||
| 9 | + let $rootScope: ng.IRootScopeService; | ||
| 10 | + let data: any; | ||
| 11 | + | ||
| 12 | + beforeEach(angular.mock.module("main", ($translateProvider: angular.translate.ITranslateProvider) => { | ||
| 13 | + $translateProvider.translations('en', {}); | ||
| 14 | + })); | ||
| 15 | + | ||
| 16 | + beforeEach(inject((_$httpBackend_: ng.IHttpBackendService, | ||
| 17 | +_PasswordService_: PasswordService, _$rootScope_: ng.IRootScopeService) => { | ||
| 18 | + $httpBackend = _$httpBackend_; | ||
| 19 | + passwordService = _PasswordService_; | ||
| 20 | + $rootScope = _$rootScope_; | ||
| 21 | + })); | ||
| 22 | + | ||
| 23 | + describe("Succesfull request", () => { | ||
| 24 | + | ||
| 25 | + it("should change user password", (done) => { | ||
| 26 | + data = { | ||
| 27 | + code: '1234567890', | ||
| 28 | + password: 'test', | ||
| 29 | + password_confirmation: 'test' | ||
| 30 | + }; | ||
| 31 | + | ||
| 32 | + $httpBackend.expectPATCH(`/api/v1/new_password?code=${data.code}&password=${data.password}&password_confirmation=${data.password_confirmation}`).respond(201, [{ login: "test" }]); | ||
| 33 | + passwordService.new_password('1234567890', 'test', 'test').then((response: restangular.IResponse) => { | ||
| 34 | + expect(response.data[0].login).toEqual("test"); | ||
| 35 | + done(); | ||
| 36 | + }); | ||
| 37 | + $httpBackend.flush(); | ||
| 38 | + }); | ||
| 39 | + }); | ||
| 40 | + }); | ||
| 41 | +}); |
| @@ -0,0 +1,14 @@ | @@ -0,0 +1,14 @@ | ||
| 1 | +import { Injectable, Inject } from "ng-forward"; | ||
| 2 | +import { RestangularService } from "./restangular_service"; | ||
| 3 | + | ||
| 4 | +@Injectable() | ||
| 5 | +@Inject("Restangular") | ||
| 6 | +export class PasswordService { | ||
| 7 | + constructor(private Restangular: restangular.IService) { | ||
| 8 | + this.Restangular = Restangular; | ||
| 9 | + } | ||
| 10 | + | ||
| 11 | + new_password(code: string, password: string, password_confirmation: string): ng.IPromise<noosfero.RestResult<noosfero.User>> { | ||
| 12 | + return this.Restangular.all("").customOperation("patch", "new_password", {code: code, password: password, password_confirmation: password_confirmation }); | ||
| 13 | + } | ||
| 14 | +} |
src/spec/mocks.ts
| @@ -257,6 +257,11 @@ export var mocks: any = { | @@ -257,6 +257,11 @@ export var mocks: any = { | ||
| 257 | changeLanguage: (lang: string) => { }, | 257 | changeLanguage: (lang: string) => { }, |
| 258 | translate: (text: string) => { return text; } | 258 | translate: (text: string) => { return text; } |
| 259 | }, | 259 | }, |
| 260 | + passwordService: { | ||
| 261 | + new_password: (param: any) => { | ||
| 262 | + return Promise.resolve({ status: 201 }); | ||
| 263 | + } | ||
| 264 | + }, | ||
| 260 | notificationService: { | 265 | notificationService: { |
| 261 | success: () => { }, | 266 | success: () => { }, |
| 262 | confirmation: () => { }, | 267 | confirmation: () => { }, |