Client: implement password change
authorChocobozzz <florian.bigard@gmail.com>
Fri, 5 Aug 2016 16:04:08 +0000 (18:04 +0200)
committerChocobozzz <florian.bigard@gmail.com>
Fri, 5 Aug 2016 16:04:08 +0000 (18:04 +0200)
client/src/app/account/account.component.html [new file with mode: 0644]
client/src/app/account/account.component.ts [new file with mode: 0644]
client/src/app/account/account.routes.ts [new file with mode: 0644]
client/src/app/account/account.service.ts [new file with mode: 0644]
client/src/app/account/index.ts [new file with mode: 0644]
client/src/app/app.component.html
client/src/app/app.routes.ts
client/src/app/shared/auth/auth-http.service.ts
client/src/app/shared/auth/auth.service.ts
client/src/app/shared/auth/user.model.ts
client/tsconfig.json

diff --git a/client/src/app/account/account.component.html b/client/src/app/account/account.component.html
new file mode 100644 (file)
index 0000000..ad8f690
--- /dev/null
@@ -0,0 +1,27 @@
+<h3>Account</h3>
+
+<div *ngIf="information" class="alert alert-success">{{ information }}</div>
+<div *ngIf="error" class="alert alert-danger">{{ error }}</div>
+
+<form role="form" (ngSubmit)="changePassword(newPassword.value, newConfirmedPassword.value)" [ngFormModel]="changePasswordForm">
+  <div class="form-group">
+    <label for="new-password">New password</label>
+    <input
+      type="password" class="form-control" name="new-password" id="new-password"
+      ngControl="newPassword" #newPassword="ngForm"
+    >
+    <div [hidden]="newPassword.valid || newPassword.pristine" class="alert alert-warning">
+      The password should have more than 5 characters
+    </div>
+  </div>
+
+  <div class="form-group">
+    <label for="name">Confirm new password</label>
+    <input
+      type="password" class="form-control" name="new-confirmed-password" id="new-confirmed-password"
+      ngControl="newConfirmedPassword" #newConfirmedPassword="ngForm"
+    >
+  </div>
+
+  <input type="submit" value="Change password" class="btn btn-default" [disabled]="!changePasswordForm.valid">
+</form>
diff --git a/client/src/app/account/account.component.ts b/client/src/app/account/account.component.ts
new file mode 100644 (file)
index 0000000..5c42103
--- /dev/null
@@ -0,0 +1,45 @@
+import { Control, ControlGroup, Validators } from '@angular/common';
+import { Component, OnInit } from '@angular/core';
+import { Router } from '@angular/router';
+
+import { AccountService } from './account.service';
+
+@Component({
+  selector: 'my-account',
+  template: require('./account.component.html'),
+  providers: [ AccountService ]
+})
+
+export class AccountComponent implements OnInit {
+  changePasswordForm: ControlGroup;
+  information: string = null;
+  error: string = null;
+
+  constructor(
+    private accountService: AccountService,
+    private router: Router
+  ) {}
+
+  ngOnInit() {
+    this.changePasswordForm = new ControlGroup({
+      newPassword: new Control('', Validators.compose([ Validators.required, Validators.minLength(6) ])),
+      newConfirmedPassword: new Control('', Validators.compose([ Validators.required, Validators.minLength(6) ])),
+    });
+  }
+
+  changePassword(newPassword: string, newConfirmedPassword: string) {
+    this.information = null;
+    this.error = null;
+
+    if (newPassword !== newConfirmedPassword) {
+      this.error = 'The new password and the confirmed password do not correspond.';
+      return;
+    }
+
+    this.accountService.changePassword(newPassword).subscribe(
+      ok => this.information = 'Password updated.',
+
+      err => this.error = err
+    );
+  }
+}
diff --git a/client/src/app/account/account.routes.ts b/client/src/app/account/account.routes.ts
new file mode 100644 (file)
index 0000000..e348c6e
--- /dev/null
@@ -0,0 +1,5 @@
+import { AccountComponent } from './account.component';
+
+export const AccountRoutes = [
+  { path: 'account', component: AccountComponent }
+];
diff --git a/client/src/app/account/account.service.ts b/client/src/app/account/account.service.ts
new file mode 100644 (file)
index 0000000..19b4e06
--- /dev/null
@@ -0,0 +1,19 @@
+import { Injectable } from '@angular/core';
+
+import { AuthHttp, AuthService } from '../shared';
+
+@Injectable()
+export class AccountService {
+  private static BASE_USERS_URL = '/api/v1/users/';
+
+  constructor(private authHttp: AuthHttp, private authService: AuthService) {  }
+
+  changePassword(newPassword: string) {
+    const url = AccountService.BASE_USERS_URL + this.authService.getUser().id;
+    const body = {
+      password: newPassword
+    };
+
+    return this.authHttp.put(url, body);
+  }
+}
diff --git a/client/src/app/account/index.ts b/client/src/app/account/index.ts
new file mode 100644 (file)
index 0000000..7445003
--- /dev/null
@@ -0,0 +1,2 @@
+export * from './account.component';
+export * from './account.routes';
index f2acffea4a2b1520169ebe5a25ff6a0047f81d65..ea4b31421b767a113412133bc0d5e1be4c4cdc7c 100644 (file)
     <menu class="col-md-2 col-sm-3 col-xs-3">
       <div class="panel-block">
         <div id="panel-user-login" class="panel-button">
+          <span *ngIf="!isLoggedIn" >
+            <span class="hidden-xs glyphicon glyphicon-log-in"></span>
+            <a [routerLink]="['/login']">Login</a>
+          </span>
+
+          <span *ngIf="isLoggedIn">
+            <span class="hidden-xs glyphicon glyphicon-log-out"></span>
+            <a *ngIf="isLoggedIn" (click)="logout()">Logout</a>
+          </span>
+        </div>
+
+        <div *ngIf="isLoggedIn" id="panel-user-account" class="panel-button">
           <span class="hidden-xs glyphicon glyphicon-user"></span>
-          <a *ngIf="!isLoggedIn" [routerLink]="['/login']">Login</a>
-          <a *ngIf="isLoggedIn" (click)="logout()">Logout</a>
+          <a [routerLink]="['/account']">My account</a>
         </div>
       </div>
 
index 59ef4ce55ceb4e6f6cc254b749e4142cae43aff4..1c414038db99582f2ee37343b263086e176899de 100644 (file)
@@ -1,5 +1,6 @@
 import { RouterConfig } from '@angular/router';
 
+import { AccountRoutes } from './account';
 import { LoginRoutes } from './login';
 import { VideosRoutes } from './videos';
 
@@ -10,6 +11,7 @@ export const routes: RouterConfig = [
     pathMatch: 'full'
   },
 
+  ...AccountRoutes,
   ...LoginRoutes,
   ...VideosRoutes
 ];
index 9c7ef4389d7395349fd73c3bcc5bb006baf69b93..55bb501e6bbed89a2d8c1dc7da512cd1bf005ff3 100644 (file)
@@ -49,16 +49,18 @@ export class AuthHttp extends Http {
     return this.request(url, options);
   }
 
-  post(url: string, options?: RequestOptionsArgs): Observable<Response> {
+  post(url: string, body: any, options?: RequestOptionsArgs): Observable<Response> {
     if (!options) options = {};
     options.method = RequestMethod.Post;
+    options.body = body;
 
     return this.request(url, options);
   }
 
-  put(url: string, options?: RequestOptionsArgs): Observable<Response> {
+  put(url: string, body: any, options?: RequestOptionsArgs): Observable<Response> {
     if (!options) options = {};
     options.method = RequestMethod.Put;
+    options.body = body;
 
     return this.request(url, options);
   }
index 6a5b19ffeed4ddd5766388ba26f8f5f5c8efc2e7..24d1a4fa2345768471b3b1b18c6189e4deb10881 100644 (file)
@@ -10,6 +10,7 @@ import { User } from './user.model';
 export class AuthService {
   private static BASE_CLIENT_URL = '/api/v1/clients/local';
   private static BASE_TOKEN_URL = '/api/v1/users/token';
+  private static BASE_USER_INFORMATIONS_URL = '/api/v1/users/me';
 
   loginChangedSource: Observable<AuthStatus>;
 
@@ -99,6 +100,7 @@ export class AuthService {
                       res.username = username;
                       return res;
                     })
+                    .flatMap(res => this.fetchUserInformations(res))
                     .map(res => this.handleLogin(res))
                     .catch(this.handleError);
   }
@@ -136,31 +138,50 @@ export class AuthService {
                     .catch(this.handleError);
   }
 
-  private setStatus(status: AuthStatus) {
-    this.loginChanged.next(status);
+  private fetchUserInformations (obj: any) {
+    // Do not call authHttp here to avoid circular dependencies headaches
+
+    const headers = new Headers();
+    headers.set('Authorization', `Bearer ${obj.access_token}`);
+
+    return this.http.get(AuthService.BASE_USER_INFORMATIONS_URL, { headers })
+             .map(res => res.json())
+             .map(res => {
+               obj.id = res.id;
+               obj.role = res.role;
+               return obj;
+             }
+    );
+  }
+
+  private handleError (error: Response) {
+    console.error(error);
+    return Observable.throw(error.json() || { error: 'Server error' });
   }
 
   private handleLogin (obj: any) {
+    const id = obj.id;
     const username = obj.username;
+    const role = obj.role;
     const hash_tokens = {
       access_token: obj.access_token,
       token_type: obj.token_type,
       refresh_token: obj.refresh_token
     };
 
-    this.user = new User(username, hash_tokens);
+    this.user = new User(id, username, role, hash_tokens);
     this.user.save();
 
     this.setStatus(AuthStatus.LoggedIn);
   }
 
-  private handleError (error: Response) {
-    console.error(error);
-    return Observable.throw(error.json() || { error: 'Server error' });
-  }
-
   private handleRefreshToken (obj: any) {
     this.user.refreshTokens(obj.access_token, obj.refresh_token);
     this.user.save();
   }
+
+  private setStatus(status: AuthStatus) {
+    this.loginChanged.next(status);
+  }
+
 }
index 98852f8355c834dfda754ef45591afa07365038b..e486873ab033fbc82d3b779952289eb35b93b20d 100644 (file)
@@ -1,15 +1,24 @@
 export class User {
   private static KEYS = {
+    ID: 'id',
+    ROLE: 'role',
     USERNAME: 'username'
   };
 
+  id: string;
+  role: string;
   username: string;
   tokens: Tokens;
 
   static load() {
     const usernameLocalStorage = localStorage.getItem(this.KEYS.USERNAME);
     if (usernameLocalStorage) {
-      return new User(localStorage.getItem(this.KEYS.USERNAME), Tokens.load());
+      return new User(
+        localStorage.getItem(this.KEYS.ID),
+        localStorage.getItem(this.KEYS.USERNAME),
+        localStorage.getItem(this.KEYS.ROLE),
+        Tokens.load()
+      );
     }
 
     return null;
@@ -17,11 +26,15 @@ export class User {
 
   static flush() {
     localStorage.removeItem(this.KEYS.USERNAME);
+    localStorage.removeItem(this.KEYS.ID);
+    localStorage.removeItem(this.KEYS.ROLE);
     Tokens.flush();
   }
 
-  constructor(username: string, hash_tokens: any) {
+  constructor(id: string, username: string, role: string, hash_tokens: any) {
+    this.id = id;
     this.username = username;
+    this.role = role;
     this.tokens = new Tokens(hash_tokens);
   }
 
@@ -43,12 +56,14 @@ export class User {
   }
 
   save() {
-    localStorage.setItem('username', this.username);
+    localStorage.setItem(User.KEYS.ID, this.id);
+    localStorage.setItem(User.KEYS.USERNAME, this.username);
+    localStorage.setItem(User.KEYS.ROLE, this.role);
     this.tokens.save();
   }
 }
 
-// Private class used only by User
+// Private class only used by User
 class Tokens {
   private static KEYS = {
     ACCESS_TOKEN: 'access_token',
index 67d1fb4f179665382c89d2e336bf97b263f1c50e..e2d61851e6b8cf24f70314cad4897520677015b4 100644 (file)
     "typings/main.d.ts"
   ],
   "files": [
+    "src/app/account/account.component.ts",
+    "src/app/account/account.routes.ts",
+    "src/app/account/account.service.ts",
+    "src/app/account/index.ts",
     "src/app/app.component.ts",
     "src/app/app.routes.ts",
     "src/app/friends/friend.service.ts",
@@ -45,6 +49,8 @@
     "src/app/shared/search/search.component.ts",
     "src/app/shared/search/search.model.ts",
     "src/app/shared/search/search.service.ts",
+    "src/app/shared/user/index.ts",
+    "src/app/shared/user/user.service.ts",
     "src/app/videos/index.ts",
     "src/app/videos/shared/index.ts",
     "src/app/videos/shared/loader/index.ts",