From 7da18e4420c4b71a8ecfda07f39324fbfec081c3 Mon Sep 17 00:00:00 2001 From: Chocobozzz Date: Tue, 9 Aug 2016 21:45:21 +0200 Subject: [PATCH] Client: add user management --- client/src/app/admin/admin.component.ts | 10 ++++ client/src/app/admin/admin.routes.ts | 14 ++++++ client/src/app/admin/index.ts | 3 ++ client/src/app/admin/users/index.ts | 5 ++ client/src/app/admin/users/shared/index.ts | 1 + .../app/admin/users/shared/user.service.ts | 49 +++++++++++++++++++ client/src/app/admin/users/user-add/index.ts | 1 + .../users/user-add/user-add.component.html | 29 +++++++++++ .../users/user-add/user-add.component.ts | 33 +++++++++++++ client/src/app/admin/users/user-list/index.ts | 1 + .../users/user-list/user-list.component.html | 24 +++++++++ .../users/user-list/user-list.component.scss | 7 +++ .../users/user-list/user-list.component.ts | 44 +++++++++++++++++ client/src/app/admin/users/users.component.ts | 13 +++++ client/src/app/admin/users/users.routes.ts | 27 ++++++++++ client/src/app/app.component.html | 7 ++- client/src/app/app.component.ts | 4 ++ client/src/app/app.routes.ts | 3 +- .../{user.model.ts => auth-user.model.ts} | 28 ++++++----- client/src/app/shared/auth/auth.service.ts | 20 +++++--- client/src/app/shared/auth/index.ts | 2 +- client/src/app/shared/index.ts | 1 + client/src/app/shared/users/index.ts | 1 + client/src/app/shared/users/user.model.ts | 15 ++++++ .../videos/video-list/video-list.component.ts | 6 +-- client/src/index.html | 1 + client/tsconfig.json | 16 +++++- 27 files changed, 338 insertions(+), 27 deletions(-) create mode 100644 client/src/app/admin/admin.component.ts create mode 100644 client/src/app/admin/admin.routes.ts create mode 100644 client/src/app/admin/index.ts create mode 100644 client/src/app/admin/users/index.ts create mode 100644 client/src/app/admin/users/shared/index.ts create mode 100644 client/src/app/admin/users/shared/user.service.ts create mode 100644 client/src/app/admin/users/user-add/index.ts create mode 100644 client/src/app/admin/users/user-add/user-add.component.html create mode 100644 client/src/app/admin/users/user-add/user-add.component.ts create mode 100644 client/src/app/admin/users/user-list/index.ts create mode 100644 client/src/app/admin/users/user-list/user-list.component.html create mode 100644 client/src/app/admin/users/user-list/user-list.component.scss create mode 100644 client/src/app/admin/users/user-list/user-list.component.ts create mode 100644 client/src/app/admin/users/users.component.ts create mode 100644 client/src/app/admin/users/users.routes.ts rename client/src/app/shared/auth/{user.model.ts => auth-user.model.ts} (79%) create mode 100644 client/src/app/shared/users/index.ts create mode 100644 client/src/app/shared/users/user.model.ts diff --git a/client/src/app/admin/admin.component.ts b/client/src/app/admin/admin.component.ts new file mode 100644 index 000000000..82f2529ec --- /dev/null +++ b/client/src/app/admin/admin.component.ts @@ -0,0 +1,10 @@ +import { Component } from '@angular/core'; +import { ROUTER_DIRECTIVES } from '@angular/router'; + +@Component({ + template: '', + 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 index 000000000..d375a86af --- /dev/null +++ b/client/src/app/admin/admin.routes.ts @@ -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 index 000000000..3b0540818 --- /dev/null +++ b/client/src/app/admin/index.ts @@ -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 index 000000000..e98a81f62 --- /dev/null +++ b/client/src/app/admin/users/index.ts @@ -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 index 000000000..e17ee5c7a --- /dev/null +++ b/client/src/app/admin/users/shared/index.ts @@ -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 index 000000000..be433f0a1 --- /dev/null +++ b/client/src/app/admin/users/shared/user.service.ts @@ -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 index 000000000..66d5ca04f --- /dev/null +++ b/client/src/app/admin/users/user-add/index.ts @@ -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 index 000000000..aa102358a --- /dev/null +++ b/client/src/app/admin/users/user-add/user-add.component.html @@ -0,0 +1,29 @@ +

Add user

+ +
{{ error }}
+ +
+
+ + +
+ Username is required with a length >= 3 and <= 20 +
+
+ +
+ + +
+ Password is required with a length >= 6 +
+
+ + +
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 index 000000000..30ca947a0 --- /dev/null +++ b/client/src/app/admin/users/user-add/user-add.component.ts @@ -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 index 000000000..51fbefa80 --- /dev/null +++ b/client/src/app/admin/users/user-list/index.ts @@ -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 index 000000000..2aca05f2b --- /dev/null +++ b/client/src/app/admin/users/user-list/user-list.component.html @@ -0,0 +1,24 @@ + + + + + + + + + + + + + + + + +
IdUsernameRemove
{{ user.id }}{{ user.username }} + +
+ + + + Add user + 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 index 000000000..e9f61e900 --- /dev/null +++ b/client/src/app/admin/users/user-list/user-list.component.scss @@ -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 index 000000000..598daa42a --- /dev/null +++ b/client/src/app/admin/users/user-list/user-list.component.ts @@ -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 index 000000000..46aa0862f --- /dev/null +++ b/client/src/app/admin/users/users.component.ts @@ -0,0 +1,13 @@ +import { Component } from '@angular/core'; +import { ROUTER_DIRECTIVES } from '@angular/router'; + +import { UserService } from './shared'; + +@Component({ + template: '', + 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 index 000000000..0457c3843 --- /dev/null +++ b/client/src/app/admin/users/users.routes.ts @@ -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 + } + ] + } +]; diff --git a/client/src/app/app.component.html b/client/src/app/app.component.html index ea4b31421..58967abca 100644 --- a/client/src/app/app.component.html +++ b/client/src/app/app.component.html @@ -47,7 +47,12 @@ -
+
+
+ + List users +
+
Make friends diff --git a/client/src/app/app.component.ts b/client/src/app/app.component.ts index 5764f24ca..444b6b3b4 100644 --- a/client/src/app/app.component.ts +++ b/client/src/app/app.component.ts @@ -45,6 +45,10 @@ export class AppComponent { ); } + isUserAdmin() { + return this.authService.isAdmin(); + } + logout() { this.authService.logout(); // Redirect to home page diff --git a/client/src/app/app.routes.ts b/client/src/app/app.routes.ts index 1c414038d..d7194cb4f 100644 --- a/client/src/app/app.routes.ts +++ b/client/src/app/app.routes.ts @@ -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 diff --git a/client/src/app/shared/auth/user.model.ts b/client/src/app/shared/auth/auth-user.model.ts 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 e486873ab..bdd5ea5a9 100644 --- a/client/src/app/shared/auth/user.model.ts +++ b/client/src/app/shared/auth/auth-user.model.ts @@ -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(); } } diff --git a/client/src/app/shared/auth/auth.service.ts b/client/src/app/shared/auth/auth.service.ts index 24d1a4fa2..8eea0c4bf 100644 --- a/client/src/app/shared/auth/auth.service.ts +++ b/client/src/app/shared/auth/auth.service.ts @@ -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; - private user: User = null; + private user: AuthUser = null; constructor(private http: Http) { this.loginChanged = new Subject(); @@ -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); diff --git a/client/src/app/shared/auth/index.ts b/client/src/app/shared/auth/index.ts index aafaacbf1..ebd9e14cd 100644 --- a/client/src/app/shared/auth/index.ts +++ b/client/src/app/shared/auth/index.ts @@ -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'; diff --git a/client/src/app/shared/index.ts b/client/src/app/shared/index.ts index dfea4c67c..c05e8d253 100644 --- a/client/src/app/shared/index.ts +++ b/client/src/app/shared/index.ts @@ -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 index 000000000..5a670ce8f --- /dev/null +++ b/client/src/app/shared/users/index.ts @@ -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 index 000000000..0f34d4480 --- /dev/null +++ b/client/src/app/shared/users/user.model.ts @@ -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'; + } +} diff --git a/client/src/app/videos/video-list/video-list.component.ts b/client/src/app/videos/video-list/video-list.component.ts index 5691d684e..062340ec5 100644 --- a/client/src/app/videos/video-list/video-list.component.ts +++ b/client/src/app/videos/video-list/video-list.component.ts @@ -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 diff --git a/client/src/index.html b/client/src/index.html index 5cf491221..f39d8d2cf 100644 --- a/client/src/index.html +++ b/client/src/index.html @@ -1,3 +1,4 @@ + diff --git a/client/tsconfig.json b/client/tsconfig.json index b10231b7b..5c5f008c3 100644 --- a/client/tsconfig.json +++ b/client/tsconfig.json @@ -31,6 +31,18 @@ "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", @@ -40,15 +52,17 @@ "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", -- 2.41.0