]> git.immae.eu Git - github/Chocobozzz/PeerTube.git/commitdiff
Client: add user management
authorChocobozzz <florian.bigard@gmail.com>
Tue, 9 Aug 2016 19:45:21 +0000 (21:45 +0200)
committerChocobozzz <florian.bigard@gmail.com>
Tue, 9 Aug 2016 19:45:21 +0000 (21:45 +0200)
27 files changed:
client/src/app/admin/admin.component.ts [new file with mode: 0644]
client/src/app/admin/admin.routes.ts [new file with mode: 0644]
client/src/app/admin/index.ts [new file with mode: 0644]
client/src/app/admin/users/index.ts [new file with mode: 0644]
client/src/app/admin/users/shared/index.ts [new file with mode: 0644]
client/src/app/admin/users/shared/user.service.ts [new file with mode: 0644]
client/src/app/admin/users/user-add/index.ts [new file with mode: 0644]
client/src/app/admin/users/user-add/user-add.component.html [new file with mode: 0644]
client/src/app/admin/users/user-add/user-add.component.ts [new file with mode: 0644]
client/src/app/admin/users/user-list/index.ts [new file with mode: 0644]
client/src/app/admin/users/user-list/user-list.component.html [new file with mode: 0644]
client/src/app/admin/users/user-list/user-list.component.scss [new file with mode: 0644]
client/src/app/admin/users/user-list/user-list.component.ts [new file with mode: 0644]
client/src/app/admin/users/users.component.ts [new file with mode: 0644]
client/src/app/admin/users/users.routes.ts [new file with mode: 0644]
client/src/app/app.component.html
client/src/app/app.component.ts
client/src/app/app.routes.ts
client/src/app/shared/auth/auth-user.model.ts [moved from client/src/app/shared/auth/user.model.ts with 79% similarity]
client/src/app/shared/auth/auth.service.ts
client/src/app/shared/auth/index.ts
client/src/app/shared/index.ts
client/src/app/shared/users/index.ts [new file with mode: 0644]
client/src/app/shared/users/user.model.ts [new file with mode: 0644]
client/src/app/videos/video-list/video-list.component.ts
client/src/index.html
client/tsconfig.json

diff --git a/client/src/app/admin/admin.component.ts b/client/src/app/admin/admin.component.ts
new file mode 100644 (file)
index 0000000..82f2529
--- /dev/null
@@ -0,0 +1,10 @@
+import { Component } from '@angular/core';
+import { ROUTER_DIRECTIVES } from '@angular/router';
+
+@Component({
+    template: '<router-outlet></router-outlet>',
+    directives: [ ROUTER_DIRECTIVES ]
+})
+
+export class AdminComponent {
+}
diff --git a/client/src/app/admin/admin.routes.ts b/client/src/app/admin/admin.routes.ts
new file mode 100644 (file)
index 0000000..d375a86
--- /dev/null
@@ -0,0 +1,14 @@
+import { RouterConfig } from '@angular/router';
+
+import { AdminComponent } from './admin.component';
+import { UsersRoutes } from './users';
+
+export const AdminRoutes: RouterConfig = [
+  {
+    path: 'admin',
+    component: AdminComponent,
+    children: [
+      ...UsersRoutes
+    ]
+  }
+];
diff --git a/client/src/app/admin/index.ts b/client/src/app/admin/index.ts
new file mode 100644 (file)
index 0000000..3b05408
--- /dev/null
@@ -0,0 +1,3 @@
+export * from './users';
+export * from './admin.component';
+export * from './admin.routes';
diff --git a/client/src/app/admin/users/index.ts b/client/src/app/admin/users/index.ts
new file mode 100644 (file)
index 0000000..e98a81f
--- /dev/null
@@ -0,0 +1,5 @@
+export * from './shared';
+export * from './user-add';
+export * from './user-list';
+export * from './users.component';
+export * from './users.routes';
diff --git a/client/src/app/admin/users/shared/index.ts b/client/src/app/admin/users/shared/index.ts
new file mode 100644 (file)
index 0000000..e17ee5c
--- /dev/null
@@ -0,0 +1 @@
+export * from './user.service';
diff --git a/client/src/app/admin/users/shared/user.service.ts b/client/src/app/admin/users/shared/user.service.ts
new file mode 100644 (file)
index 0000000..be433f0
--- /dev/null
@@ -0,0 +1,49 @@
+import { Injectable } from '@angular/core';
+import { Response } from '@angular/http';
+import { Observable } from 'rxjs/Observable';
+
+import { AuthHttp, User } from '../../../shared';
+
+@Injectable()
+export class UserService {
+  // TODO: merge this constant with account
+  private static BASE_USERS_URL = '/api/v1/users/';
+
+  constructor(private authHttp: AuthHttp) {}
+
+  addUser(username: string, password: string) {
+    const body = {
+      username,
+      password
+    };
+
+    return this.authHttp.post(UserService.BASE_USERS_URL, body);
+  }
+
+  getUsers() {
+    return this.authHttp.get(UserService.BASE_USERS_URL)
+                 .map(res => res.json())
+                 .map(this.extractUsers)
+                 .catch(this.handleError);
+  }
+
+  removeUser(user: User) {
+    return this.authHttp.delete(UserService.BASE_USERS_URL + user.id);
+  }
+
+  private extractUsers(body: any) {
+    const usersJson = body.data;
+    const totalUsers = body.total;
+    const users = [];
+    for (const userJson of usersJson) {
+      users.push(new User(userJson));
+    }
+
+    return { users, totalUsers };
+  }
+
+  private handleError(error: Response) {
+    console.error(error);
+    return Observable.throw(error.json().error || 'Server error');
+  }
+}
diff --git a/client/src/app/admin/users/user-add/index.ts b/client/src/app/admin/users/user-add/index.ts
new file mode 100644 (file)
index 0000000..66d5ca0
--- /dev/null
@@ -0,0 +1 @@
+export * from './user-add.component';
diff --git a/client/src/app/admin/users/user-add/user-add.component.html b/client/src/app/admin/users/user-add/user-add.component.html
new file mode 100644 (file)
index 0000000..aa10235
--- /dev/null
@@ -0,0 +1,29 @@
+<h3>Add user</h3>
+
+<div *ngIf="error" class="alert alert-danger">{{ error }}</div>
+
+<form role="form" (ngSubmit)="addUser(username.value, password.value)" #addUserForm="ngForm">
+  <div class="form-group">
+    <label for="username">Username</label>
+    <input
+      type="text" class="form-control" name="username" id="username" placeholder="Username" required
+      ngControl="username" #username="ngForm"
+    >
+    <div [hidden]="username.valid || username.pristine" class="alert alert-danger">
+      Username is required with a length >= 3 and <= 20
+    </div>
+  </div>
+
+  <div class="form-group">
+    <label for="password">Password</label>
+    <input
+      type="password" class="form-control" name="password" id="password" placeholder="Password" required
+      ngControl="password" #password="ngForm"
+    >
+    <div [hidden]="password.valid || password.pristine" class="alert alert-danger">
+      Password is required with a length >= 6
+    </div>
+  </div>
+
+  <input type="submit" value="Add user" class="btn btn-default" [disabled]="!addUserForm.form.valid">
+</form>
diff --git a/client/src/app/admin/users/user-add/user-add.component.ts b/client/src/app/admin/users/user-add/user-add.component.ts
new file mode 100644 (file)
index 0000000..30ca947
--- /dev/null
@@ -0,0 +1,33 @@
+import { Control, ControlGroup, Validators } from '@angular/common';
+import { Component, OnInit } from '@angular/core';
+import { Router } from '@angular/router';
+
+import { UserService } from '../shared';
+
+@Component({
+  selector: 'my-user-add',
+  template: require('./user-add.component.html'),
+})
+export class UserAddComponent implements OnInit {
+  userAddForm: ControlGroup;
+  error: string = null;
+
+  constructor(private router: Router, private userService: UserService) {}
+
+  ngOnInit() {
+    this.userAddForm = new ControlGroup({
+      username: new Control('', Validators.compose([ Validators.required, Validators.minLength(3), Validators.maxLength(20) ])),
+      password: new Control('', Validators.compose([ Validators.required, Validators.minLength(6) ])),
+    });
+  }
+
+  addUser(username: string, password: string) {
+    this.error = null;
+
+    this.userService.addUser(username, password).subscribe(
+      ok => this.router.navigate([ '/admin/users/list' ]),
+
+      err => this.error = err
+    );
+  }
+}
diff --git a/client/src/app/admin/users/user-list/index.ts b/client/src/app/admin/users/user-list/index.ts
new file mode 100644 (file)
index 0000000..51fbefa
--- /dev/null
@@ -0,0 +1 @@
+export * from './user-list.component';
diff --git a/client/src/app/admin/users/user-list/user-list.component.html b/client/src/app/admin/users/user-list/user-list.component.html
new file mode 100644 (file)
index 0000000..2aca05f
--- /dev/null
@@ -0,0 +1,24 @@
+<table class="table table-hover">
+  <thead>
+    <tr>
+      <th>Id</th>
+      <th>Username</th>
+      <th class="text-right">Remove</th>
+    </tr>
+  </thead>
+
+  <tbody>
+    <tr *ngFor="let user of users">
+      <td>{{ user.id }}</td>
+      <td>{{ user.username }}</td>
+      <td class="text-right">
+        <span class="glyphicon glyphicon-remove" *ngIf="!user.isAdmin()" (click)="removeUser(user)"></span>
+      </td>
+    </tr>
+  </tbody>
+</table>
+
+<a class="add-user btn btn-success pull-right" [routerLink]="['/admin/users/add']">
+  <span class="glyphicon glyphicon-plus"></span>
+  Add user
+</a>
diff --git a/client/src/app/admin/users/user-list/user-list.component.scss b/client/src/app/admin/users/user-list/user-list.component.scss
new file mode 100644 (file)
index 0000000..e9f61e9
--- /dev/null
@@ -0,0 +1,7 @@
+.glyphicon-remove {
+  cursor: pointer;
+}
+
+.add-user {
+  margin-top: 10px;
+}
diff --git a/client/src/app/admin/users/user-list/user-list.component.ts b/client/src/app/admin/users/user-list/user-list.component.ts
new file mode 100644 (file)
index 0000000..598daa4
--- /dev/null
@@ -0,0 +1,44 @@
+import { Component, OnInit } from '@angular/core';
+import { ROUTER_DIRECTIVES } from '@angular/router';
+
+import { User } from '../../../shared';
+import { UserService } from '../shared';
+
+@Component({
+  selector: 'my-user-list',
+  template: require('./user-list.component.html'),
+  styles: [ require('./user-list.component.scss') ],
+  directives: [ ROUTER_DIRECTIVES ]
+})
+export class UserListComponent implements OnInit {
+  totalUsers: number;
+  users: User[];
+
+  constructor(private userService: UserService) {}
+
+  ngOnInit() {
+    this.getUsers();
+  }
+
+  getUsers() {
+    this.userService.getUsers().subscribe(
+      ({ users, totalUsers }) => {
+        this.users = users;
+        this.totalUsers = totalUsers;
+      },
+
+      err => alert(err)
+    );
+  }
+
+
+  removeUser(user: User) {
+    if (confirm('Are you sure?')) {
+      this.userService.removeUser(user).subscribe(
+        () => this.getUsers(),
+
+        err => alert(err)
+      );
+    }
+  }
+}
diff --git a/client/src/app/admin/users/users.component.ts b/client/src/app/admin/users/users.component.ts
new file mode 100644 (file)
index 0000000..46aa086
--- /dev/null
@@ -0,0 +1,13 @@
+import { Component } from '@angular/core';
+import { ROUTER_DIRECTIVES } from '@angular/router';
+
+import { UserService } from './shared';
+
+@Component({
+    template: '<router-outlet></router-outlet>',
+    directives: [ ROUTER_DIRECTIVES ],
+    providers: [ UserService ]
+})
+
+export class UsersComponent {
+}
diff --git a/client/src/app/admin/users/users.routes.ts b/client/src/app/admin/users/users.routes.ts
new file mode 100644 (file)
index 0000000..0457c38
--- /dev/null
@@ -0,0 +1,27 @@
+import { RouterConfig } from '@angular/router';
+
+import { UsersComponent } from './users.component';
+import { UserAddComponent } from './user-add';
+import { UserListComponent } from './user-list';
+
+export const UsersRoutes: RouterConfig = [
+  {
+      path: 'users',
+      component: UsersComponent,
+      children: [
+        {
+          path: '',
+          redirectTo: 'list',
+          pathMatch: 'full'
+        },
+        {
+          path: 'list',
+          component: UserListComponent
+        },
+        {
+          path: 'add',
+          component: UserAddComponent
+        }
+      ]
+    }
+];
index ea4b31421b767a113412133bc0d5e1be4c4cdc7c..58967abca9fc6c900ac86ca2ec3154a623cacab4 100644 (file)
         </div>
       </div>
 
-      <div class="panel-block" *ngIf="isLoggedIn">
+      <div class="panel-block" *ngIf="isUserAdmin()">
+        <div id="panel-users" class="panel-button">
+          <span class="hidden-xs glyphicon glyphicon-user"></span>
+          <a [routerLink]="['/admin/users/list']">List users</a>
+        </div>
+
         <div id="panel-make-friends" class="panel-button">
           <span class="hidden-xs glyphicon glyphicon-cloud"></span>
           <a (click)='makeFriends()'>Make friends</a>
index 5764f24cad4caa4ee4d7a1de35c7227ab09cb23c..444b6b3b4a60af3adfa2a00fd83eb287e7560282 100644 (file)
@@ -45,6 +45,10 @@ export class AppComponent {
     );
   }
 
+  isUserAdmin() {
+    return this.authService.isAdmin();
+  }
+
   logout() {
     this.authService.logout();
     // Redirect to home page
index 1c414038db99582f2ee37343b263086e176899de..d7194cb4feca3cf6419da8e0e94644f65ec90400 100644 (file)
@@ -2,6 +2,7 @@ import { RouterConfig } from '@angular/router';
 
 import { AccountRoutes } from './account';
 import { LoginRoutes } from './login';
+import { AdminRoutes } from './admin';
 import { VideosRoutes } from './videos';
 
 export const routes: RouterConfig = [
@@ -10,7 +11,7 @@ export const routes: RouterConfig = [
     redirectTo: '/videos/list',
     pathMatch: 'full'
   },
-
+  ...AdminRoutes,
   ...AccountRoutes,
   ...LoginRoutes,
   ...VideosRoutes
similarity index 79%
rename from client/src/app/shared/auth/user.model.ts
rename to client/src/app/shared/auth/auth-user.model.ts
index e486873ab033fbc82d3b779952289eb35b93b20d..bdd5ea5a9e30df298a0a80319278cd9e6e59f0e4 100644 (file)
@@ -1,4 +1,6 @@
-export class User {
+import { User } from '../users';
+
+export class AuthUser extends User {
   private static KEYS = {
     ID: 'id',
     ROLE: 'role',
@@ -13,10 +15,12 @@ export class User {
   static load() {
     const usernameLocalStorage = localStorage.getItem(this.KEYS.USERNAME);
     if (usernameLocalStorage) {
-      return new User(
-        localStorage.getItem(this.KEYS.ID),
-        localStorage.getItem(this.KEYS.USERNAME),
-        localStorage.getItem(this.KEYS.ROLE),
+      return new AuthUser(
+        {
+          id: localStorage.getItem(this.KEYS.ID),
+          username: localStorage.getItem(this.KEYS.USERNAME),
+          role: localStorage.getItem(this.KEYS.ROLE)
+        },
         Tokens.load()
       );
     }
@@ -31,11 +35,9 @@ export class User {
     Tokens.flush();
   }
 
-  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);
+  constructor(userHash: { id: string, username: string, role: string }, hashTokens: any) {
+    super(userHash);
+    this.tokens = new Tokens(hashTokens);
   }
 
   getAccessToken() {
@@ -56,9 +58,9 @@ export class User {
   }
 
   save() {
-    localStorage.setItem(User.KEYS.ID, this.id);
-    localStorage.setItem(User.KEYS.USERNAME, this.username);
-    localStorage.setItem(User.KEYS.ROLE, this.role);
+    localStorage.setItem(AuthUser.KEYS.ID, this.id);
+    localStorage.setItem(AuthUser.KEYS.USERNAME, this.username);
+    localStorage.setItem(AuthUser.KEYS.ROLE, this.role);
     this.tokens.save();
   }
 }
index 24d1a4fa2345768471b3b1b18c6189e4deb10881..8eea0c4bfed4f812c9cc97e85618243c0e06d610 100644 (file)
@@ -4,7 +4,7 @@ import { Observable } from 'rxjs/Observable';
 import { Subject } from 'rxjs/Subject';
 
 import { AuthStatus } from './auth-status.model';
-import { User } from './user.model';
+import { AuthUser } from './auth-user.model';
 
 @Injectable()
 export class AuthService {
@@ -17,7 +17,7 @@ export class AuthService {
   private clientId: string;
   private clientSecret: string;
   private loginChanged: Subject<AuthStatus>;
-  private user: User = null;
+  private user: AuthUser = null;
 
   constructor(private http: Http) {
     this.loginChanged = new Subject<AuthStatus>();
@@ -40,7 +40,7 @@ export class AuthService {
       );
 
     // Return null if there is nothing to load
-    this.user = User.load();
+    this.user = AuthUser.load();
   }
 
   getRefreshToken() {
@@ -65,10 +65,16 @@ export class AuthService {
     return this.user.getTokenType();
   }
 
-  getUser(): User {
+  getUser(): AuthUser {
     return this.user;
   }
 
+  isAdmin() {
+    if (this.user === null) return false;
+
+    return this.user.isAdmin();
+  }
+
   isLoggedIn() {
     if (this.getAccessToken()) {
       return true;
@@ -108,7 +114,7 @@ export class AuthService {
   logout() {
     // TODO: make an HTTP request to revoke the tokens
     this.user = null;
-    User.flush();
+    AuthUser.flush();
 
     this.setStatus(AuthStatus.LoggedOut);
   }
@@ -163,13 +169,13 @@ export class AuthService {
     const id = obj.id;
     const username = obj.username;
     const role = obj.role;
-    const hash_tokens = {
+    const hashTokens = {
       access_token: obj.access_token,
       token_type: obj.token_type,
       refresh_token: obj.refresh_token
     };
 
-    this.user = new User(id, username, role, hash_tokens);
+    this.user = new AuthUser({ id, username, role }, hashTokens);
     this.user.save();
 
     this.setStatus(AuthStatus.LoggedIn);
index aafaacbf1ff35296c279d8c3e970ef6aee2866c8..ebd9e14cd48e20895b2361dddccab49d40213154 100644 (file)
@@ -1,4 +1,4 @@
 export * from './auth-http.service';
 export * from './auth-status.model';
 export * from './auth.service';
-export * from './user.model';
+export * from './auth-user.model';
index dfea4c67cdb959ed561a9dfc2d01774d0e1efca2..c05e8d2539edfd0838ad28e8264284a9b1318824 100644 (file)
@@ -1,2 +1,3 @@
 export * from './auth';
 export * from './search';
+export * from './users';
diff --git a/client/src/app/shared/users/index.ts b/client/src/app/shared/users/index.ts
new file mode 100644 (file)
index 0000000..5a670ce
--- /dev/null
@@ -0,0 +1 @@
+export * from './user.model';
diff --git a/client/src/app/shared/users/user.model.ts b/client/src/app/shared/users/user.model.ts
new file mode 100644 (file)
index 0000000..0f34d44
--- /dev/null
@@ -0,0 +1,15 @@
+export class User {
+  id: string;
+  username: string;
+  role: string;
+
+  constructor(hash: { id: string, username: string, role: string }) {
+    this.id = hash.id;
+    this.username = hash.username;
+    this.role = hash.role;
+  }
+
+  isAdmin() {
+    return this.role === 'admin';
+  }
+}
index 5691d684eee9cb5ab6ebaff98f5810d7ab150893..062340ec523fce4e00de666c4984a4e11c48fca5 100644 (file)
@@ -12,7 +12,7 @@ import {
   Video,
   VideoService
 } from '../shared';
-import { AuthService, Search, SearchField, User } from '../../shared';
+import { AuthService, AuthUser, Search, SearchField } from '../../shared';
 import { VideoMiniatureComponent } from './video-miniature.component';
 import { VideoSortComponent } from './video-sort.component';
 import { SearchService } from '../../shared';
@@ -33,7 +33,7 @@ export class VideoListComponent implements OnInit, OnDestroy {
     totalItems: null
   };
   sort: SortField;
-  user: User = null;
+  user: AuthUser = null;
   videos: Video[] = [];
 
   private search: Search;
@@ -51,7 +51,7 @@ export class VideoListComponent implements OnInit, OnDestroy {
 
   ngOnInit() {
     if (this.authService.isLoggedIn()) {
-      this.user = User.load();
+      this.user = AuthUser.load();
     }
 
     // Subscribe to route changes
index 5cf491221b54c5645135b2c1535a90355ad52add..f39d8d2cfc1a637247f0338b644e23d4f8959628 100644 (file)
@@ -1,3 +1,4 @@
+<!DOCTYPE html>
 <html>
   <head>
     <base href="/">
index b10231b7b16bf30ff7b68877aa1f3e9fcd24afc1..5c5f008c3c16fb5a073245e394286906b94db7b7 100644 (file)
     "src/app/account/account.routes.ts",
     "src/app/account/account.service.ts",
     "src/app/account/index.ts",
+    "src/app/admin/admin.component.ts",
+    "src/app/admin/admin.routes.ts",
+    "src/app/admin/index.ts",
+    "src/app/admin/users/index.ts",
+    "src/app/admin/users/shared/index.ts",
+    "src/app/admin/users/shared/user.service.ts",
+    "src/app/admin/users/user-add/index.ts",
+    "src/app/admin/users/user-add/user-add.component.ts",
+    "src/app/admin/users/user-list/index.ts",
+    "src/app/admin/users/user-list/user-list.component.ts",
+    "src/app/admin/users/users.component.ts",
+    "src/app/admin/users/users.routes.ts",
     "src/app/app.component.ts",
     "src/app/app.routes.ts",
     "src/app/friends/friend.service.ts",
     "src/app/login/login.routes.ts",
     "src/app/shared/auth/auth-http.service.ts",
     "src/app/shared/auth/auth-status.model.ts",
+    "src/app/shared/auth/auth-user.model.ts",
     "src/app/shared/auth/auth.service.ts",
     "src/app/shared/auth/index.ts",
-    "src/app/shared/auth/user.model.ts",
     "src/app/shared/index.ts",
     "src/app/shared/search/index.ts",
     "src/app/shared/search/search-field.type.ts",
     "src/app/shared/search/search.component.ts",
     "src/app/shared/search/search.model.ts",
     "src/app/shared/search/search.service.ts",
+    "src/app/shared/users/index.ts",
+    "src/app/shared/users/user.model.ts",
     "src/app/videos/index.ts",
     "src/app/videos/shared/index.ts",
     "src/app/videos/shared/loader/index.ts",