diff options
author | Chocobozzz <florian.bigard@gmail.com> | 2016-10-02 15:39:09 +0200 |
---|---|---|
committer | Chocobozzz <florian.bigard@gmail.com> | 2016-10-02 15:39:09 +0200 |
commit | a6375e69668ea42e19531c6bc68dcd37f3f7cbd7 (patch) | |
tree | 03204a408d56311692c3528bedcf95d2455e94f2 /client/src/app/shared | |
parent | 052937db8a8d282eccdbdf38d487ed8d85d3c0a7 (diff) | |
parent | c4403b29ad4db097af528a7f04eea07e0ed320d0 (diff) | |
download | PeerTube-a6375e69668ea42e19531c6bc68dcd37f3f7cbd7.tar.gz PeerTube-a6375e69668ea42e19531c6bc68dcd37f3f7cbd7.tar.zst PeerTube-a6375e69668ea42e19531c6bc68dcd37f3f7cbd7.zip |
Merge branch 'master' into webseed-merged
Diffstat (limited to 'client/src/app/shared')
19 files changed, 310 insertions, 48 deletions
diff --git a/client/src/app/shared/auth/auth-http.service.ts b/client/src/app/shared/auth/auth-http.service.ts index 9c7ef4389..2392898ca 100644 --- a/client/src/app/shared/auth/auth-http.service.ts +++ b/client/src/app/shared/auth/auth-http.service.ts | |||
@@ -28,7 +28,7 @@ export class AuthHttp extends Http { | |||
28 | return super.request(url, options) | 28 | return super.request(url, options) |
29 | .catch((err) => { | 29 | .catch((err) => { |
30 | if (err.status === 401) { | 30 | if (err.status === 401) { |
31 | return this.handleTokenExpired(err, url, options); | 31 | return this.handleTokenExpired(url, options); |
32 | } | 32 | } |
33 | 33 | ||
34 | return Observable.throw(err); | 34 | return Observable.throw(err); |
@@ -49,26 +49,29 @@ export class AuthHttp extends Http { | |||
49 | return this.request(url, options); | 49 | return this.request(url, options); |
50 | } | 50 | } |
51 | 51 | ||
52 | post(url: string, options?: RequestOptionsArgs): Observable<Response> { | 52 | post(url: string, body: any, options?: RequestOptionsArgs): Observable<Response> { |
53 | if (!options) options = {}; | 53 | if (!options) options = {}; |
54 | options.method = RequestMethod.Post; | 54 | options.method = RequestMethod.Post; |
55 | options.body = body; | ||
55 | 56 | ||
56 | return this.request(url, options); | 57 | return this.request(url, options); |
57 | } | 58 | } |
58 | 59 | ||
59 | put(url: string, options?: RequestOptionsArgs): Observable<Response> { | 60 | put(url: string, body: any, options?: RequestOptionsArgs): Observable<Response> { |
60 | if (!options) options = {}; | 61 | if (!options) options = {}; |
61 | options.method = RequestMethod.Put; | 62 | options.method = RequestMethod.Put; |
63 | options.body = body; | ||
62 | 64 | ||
63 | return this.request(url, options); | 65 | return this.request(url, options); |
64 | } | 66 | } |
65 | 67 | ||
66 | private handleTokenExpired(err: Response, url: string | Request, options: RequestOptionsArgs) { | 68 | private handleTokenExpired(url: string | Request, options: RequestOptionsArgs) { |
67 | return this.authService.refreshAccessToken().flatMap(() => { | 69 | return this.authService.refreshAccessToken() |
68 | this.setAuthorizationHeader(options.headers); | 70 | .flatMap(() => { |
71 | this.setAuthorizationHeader(options.headers); | ||
69 | 72 | ||
70 | return super.request(url, options); | 73 | return super.request(url, options); |
71 | }); | 74 | }); |
72 | } | 75 | } |
73 | 76 | ||
74 | private setAuthorizationHeader(headers: Headers) { | 77 | private setAuthorizationHeader(headers: Headers) { |
diff --git a/client/src/app/shared/auth/user.model.ts b/client/src/app/shared/auth/auth-user.model.ts index 98852f835..bdd5ea5a9 100644 --- a/client/src/app/shared/auth/user.model.ts +++ b/client/src/app/shared/auth/auth-user.model.ts | |||
@@ -1,15 +1,28 @@ | |||
1 | export class User { | 1 | import { User } from '../users'; |
2 | |||
3 | export class AuthUser extends User { | ||
2 | private static KEYS = { | 4 | private static KEYS = { |
5 | ID: 'id', | ||
6 | ROLE: 'role', | ||
3 | USERNAME: 'username' | 7 | USERNAME: 'username' |
4 | }; | 8 | }; |
5 | 9 | ||
10 | id: string; | ||
11 | role: string; | ||
6 | username: string; | 12 | username: string; |
7 | tokens: Tokens; | 13 | tokens: Tokens; |
8 | 14 | ||
9 | static load() { | 15 | static load() { |
10 | const usernameLocalStorage = localStorage.getItem(this.KEYS.USERNAME); | 16 | const usernameLocalStorage = localStorage.getItem(this.KEYS.USERNAME); |
11 | if (usernameLocalStorage) { | 17 | if (usernameLocalStorage) { |
12 | return new User(localStorage.getItem(this.KEYS.USERNAME), Tokens.load()); | 18 | return new AuthUser( |
19 | { | ||
20 | id: localStorage.getItem(this.KEYS.ID), | ||
21 | username: localStorage.getItem(this.KEYS.USERNAME), | ||
22 | role: localStorage.getItem(this.KEYS.ROLE) | ||
23 | }, | ||
24 | Tokens.load() | ||
25 | ); | ||
13 | } | 26 | } |
14 | 27 | ||
15 | return null; | 28 | return null; |
@@ -17,12 +30,14 @@ export class User { | |||
17 | 30 | ||
18 | static flush() { | 31 | static flush() { |
19 | localStorage.removeItem(this.KEYS.USERNAME); | 32 | localStorage.removeItem(this.KEYS.USERNAME); |
33 | localStorage.removeItem(this.KEYS.ID); | ||
34 | localStorage.removeItem(this.KEYS.ROLE); | ||
20 | Tokens.flush(); | 35 | Tokens.flush(); |
21 | } | 36 | } |
22 | 37 | ||
23 | constructor(username: string, hash_tokens: any) { | 38 | constructor(userHash: { id: string, username: string, role: string }, hashTokens: any) { |
24 | this.username = username; | 39 | super(userHash); |
25 | this.tokens = new Tokens(hash_tokens); | 40 | this.tokens = new Tokens(hashTokens); |
26 | } | 41 | } |
27 | 42 | ||
28 | getAccessToken() { | 43 | getAccessToken() { |
@@ -43,12 +58,14 @@ export class User { | |||
43 | } | 58 | } |
44 | 59 | ||
45 | save() { | 60 | save() { |
46 | localStorage.setItem('username', this.username); | 61 | localStorage.setItem(AuthUser.KEYS.ID, this.id); |
62 | localStorage.setItem(AuthUser.KEYS.USERNAME, this.username); | ||
63 | localStorage.setItem(AuthUser.KEYS.ROLE, this.role); | ||
47 | this.tokens.save(); | 64 | this.tokens.save(); |
48 | } | 65 | } |
49 | } | 66 | } |
50 | 67 | ||
51 | // Private class used only by User | 68 | // Private class only used by User |
52 | class Tokens { | 69 | class Tokens { |
53 | private static KEYS = { | 70 | private static KEYS = { |
54 | ACCESS_TOKEN: 'access_token', | 71 | ACCESS_TOKEN: 'access_token', |
diff --git a/client/src/app/shared/auth/auth.service.ts b/client/src/app/shared/auth/auth.service.ts index 584298fff..a30c79c86 100644 --- a/client/src/app/shared/auth/auth.service.ts +++ b/client/src/app/shared/auth/auth.service.ts | |||
@@ -1,32 +1,39 @@ | |||
1 | import { Injectable } from '@angular/core'; | 1 | import { Injectable } from '@angular/core'; |
2 | import { Headers, Http, Response, URLSearchParams } from '@angular/http'; | 2 | import { Headers, Http, Response, URLSearchParams } from '@angular/http'; |
3 | import { Router } from '@angular/router'; | ||
3 | import { Observable } from 'rxjs/Observable'; | 4 | import { Observable } from 'rxjs/Observable'; |
4 | import { Subject } from 'rxjs/Subject'; | 5 | import { Subject } from 'rxjs/Subject'; |
5 | 6 | ||
6 | import { AuthStatus } from './auth-status.model'; | 7 | import { AuthStatus } from './auth-status.model'; |
7 | import { User } from './user.model'; | 8 | import { AuthUser } from './auth-user.model'; |
9 | import { RestExtractor } from '../rest'; | ||
8 | 10 | ||
9 | @Injectable() | 11 | @Injectable() |
10 | export class AuthService { | 12 | export class AuthService { |
11 | private static BASE_CLIENT_URL = '/api/v1/users/client'; | 13 | private static BASE_CLIENT_URL = '/api/v1/clients/local'; |
12 | private static BASE_TOKEN_URL = '/api/v1/users/token'; | 14 | private static BASE_TOKEN_URL = '/api/v1/users/token'; |
15 | private static BASE_USER_INFORMATIONS_URL = '/api/v1/users/me'; | ||
13 | 16 | ||
14 | loginChangedSource: Observable<AuthStatus>; | 17 | loginChangedSource: Observable<AuthStatus>; |
15 | 18 | ||
16 | private clientId: string; | 19 | private clientId: string; |
17 | private clientSecret: string; | 20 | private clientSecret: string; |
18 | private loginChanged: Subject<AuthStatus>; | 21 | private loginChanged: Subject<AuthStatus>; |
19 | private user: User = null; | 22 | private user: AuthUser = null; |
20 | 23 | ||
21 | constructor(private http: Http) { | 24 | constructor( |
25 | private http: Http, | ||
26 | private restExtractor: RestExtractor, | ||
27 | private router: Router | ||
28 | ) { | ||
22 | this.loginChanged = new Subject<AuthStatus>(); | 29 | this.loginChanged = new Subject<AuthStatus>(); |
23 | this.loginChangedSource = this.loginChanged.asObservable(); | 30 | this.loginChangedSource = this.loginChanged.asObservable(); |
24 | 31 | ||
25 | // Fetch the client_id/client_secret | 32 | // Fetch the client_id/client_secret |
26 | // FIXME: save in local storage? | 33 | // FIXME: save in local storage? |
27 | this.http.get(AuthService.BASE_CLIENT_URL) | 34 | this.http.get(AuthService.BASE_CLIENT_URL) |
28 | .map(res => res.json()) | 35 | .map(this.restExtractor.extractDataGet) |
29 | .catch(this.handleError) | 36 | .catch((res) => this.restExtractor.handleError(res)) |
30 | .subscribe( | 37 | .subscribe( |
31 | result => { | 38 | result => { |
32 | this.clientId = result.client_id; | 39 | this.clientId = result.client_id; |
@@ -34,12 +41,15 @@ export class AuthService { | |||
34 | console.log('Client credentials loaded.'); | 41 | console.log('Client credentials loaded.'); |
35 | }, | 42 | }, |
36 | error => { | 43 | error => { |
37 | alert(error); | 44 | alert( |
45 | `Cannot retrieve OAuth Client credentials: ${error.text}. \n` + | ||
46 | 'Ensure you have correctly configured PeerTube (config/ directory), in particular the "webserver" section.' | ||
47 | ); | ||
38 | } | 48 | } |
39 | ); | 49 | ); |
40 | 50 | ||
41 | // Return null if there is nothing to load | 51 | // Return null if there is nothing to load |
42 | this.user = User.load(); | 52 | this.user = AuthUser.load(); |
43 | } | 53 | } |
44 | 54 | ||
45 | getRefreshToken() { | 55 | getRefreshToken() { |
@@ -64,10 +74,16 @@ export class AuthService { | |||
64 | return this.user.getTokenType(); | 74 | return this.user.getTokenType(); |
65 | } | 75 | } |
66 | 76 | ||
67 | getUser(): User { | 77 | getUser(): AuthUser { |
68 | return this.user; | 78 | return this.user; |
69 | } | 79 | } |
70 | 80 | ||
81 | isAdmin() { | ||
82 | if (this.user === null) return false; | ||
83 | |||
84 | return this.user.isAdmin(); | ||
85 | } | ||
86 | |||
71 | isLoggedIn() { | 87 | isLoggedIn() { |
72 | if (this.getAccessToken()) { | 88 | if (this.getAccessToken()) { |
73 | return true; | 89 | return true; |
@@ -94,21 +110,23 @@ export class AuthService { | |||
94 | }; | 110 | }; |
95 | 111 | ||
96 | return this.http.post(AuthService.BASE_TOKEN_URL, body.toString(), options) | 112 | return this.http.post(AuthService.BASE_TOKEN_URL, body.toString(), options) |
97 | .map(res => res.json()) | 113 | .map(this.restExtractor.extractDataGet) |
98 | .map(res => { | 114 | .map(res => { |
99 | res.username = username; | 115 | res.username = username; |
100 | return res; | 116 | return res; |
101 | }) | 117 | }) |
118 | .flatMap(res => this.fetchUserInformations(res)) | ||
102 | .map(res => this.handleLogin(res)) | 119 | .map(res => this.handleLogin(res)) |
103 | .catch(this.handleError); | 120 | .catch((res) => this.restExtractor.handleError(res)); |
104 | } | 121 | } |
105 | 122 | ||
106 | logout() { | 123 | logout() { |
107 | // TODO: make an HTTP request to revoke the tokens | 124 | // TODO: make an HTTP request to revoke the tokens |
108 | this.user = null; | 125 | this.user = null; |
109 | User.flush(); | ||
110 | 126 | ||
111 | this.setStatus(AuthStatus.LoggedIn); | 127 | AuthUser.flush(); |
128 | |||
129 | this.setStatus(AuthStatus.LoggedOut); | ||
112 | } | 130 | } |
113 | 131 | ||
114 | refreshAccessToken() { | 132 | refreshAccessToken() { |
@@ -131,36 +149,64 @@ export class AuthService { | |||
131 | }; | 149 | }; |
132 | 150 | ||
133 | return this.http.post(AuthService.BASE_TOKEN_URL, body.toString(), options) | 151 | return this.http.post(AuthService.BASE_TOKEN_URL, body.toString(), options) |
134 | .map(res => res.json()) | 152 | .map(this.restExtractor.extractDataGet) |
135 | .map(res => this.handleRefreshToken(res)) | 153 | .map(res => this.handleRefreshToken(res)) |
136 | .catch(this.handleError); | 154 | .catch((res: Response) => { |
155 | // The refresh token is invalid? | ||
156 | if (res.status === 400 && res.json() && res.json().error === 'invalid_grant') { | ||
157 | console.error('Cannot refresh token -> logout...'); | ||
158 | this.logout(); | ||
159 | this.router.navigate(['/login']); | ||
160 | |||
161 | return Observable.throw({ | ||
162 | json: '', | ||
163 | text: 'You need to reconnect.' | ||
164 | }); | ||
165 | } | ||
166 | |||
167 | return this.restExtractor.handleError(res); | ||
168 | }); | ||
137 | } | 169 | } |
138 | 170 | ||
139 | private setStatus(status: AuthStatus) { | 171 | private fetchUserInformations (obj: any) { |
140 | this.loginChanged.next(status); | 172 | // Do not call authHttp here to avoid circular dependencies headaches |
173 | |||
174 | const headers = new Headers(); | ||
175 | headers.set('Authorization', `Bearer ${obj.access_token}`); | ||
176 | |||
177 | return this.http.get(AuthService.BASE_USER_INFORMATIONS_URL, { headers }) | ||
178 | .map(res => res.json()) | ||
179 | .map(res => { | ||
180 | obj.id = res.id; | ||
181 | obj.role = res.role; | ||
182 | return obj; | ||
183 | } | ||
184 | ); | ||
141 | } | 185 | } |
142 | 186 | ||
143 | private handleLogin (obj: any) { | 187 | private handleLogin (obj: any) { |
188 | const id = obj.id; | ||
144 | const username = obj.username; | 189 | const username = obj.username; |
145 | const hash_tokens = { | 190 | const role = obj.role; |
191 | const hashTokens = { | ||
146 | access_token: obj.access_token, | 192 | access_token: obj.access_token, |
147 | token_type: obj.token_type, | 193 | token_type: obj.token_type, |
148 | refresh_token: obj.refresh_token | 194 | refresh_token: obj.refresh_token |
149 | }; | 195 | }; |
150 | 196 | ||
151 | this.user = new User(username, hash_tokens); | 197 | this.user = new AuthUser({ id, username, role }, hashTokens); |
152 | this.user.save(); | 198 | this.user.save(); |
153 | 199 | ||
154 | this.setStatus(AuthStatus.LoggedIn); | 200 | this.setStatus(AuthStatus.LoggedIn); |
155 | } | 201 | } |
156 | 202 | ||
157 | private handleError (error: Response) { | ||
158 | console.error(error); | ||
159 | return Observable.throw(error.json() || { error: 'Server error' }); | ||
160 | } | ||
161 | |||
162 | private handleRefreshToken (obj: any) { | 203 | private handleRefreshToken (obj: any) { |
163 | this.user.refreshTokens(obj.access_token, obj.refresh_token); | 204 | this.user.refreshTokens(obj.access_token, obj.refresh_token); |
164 | this.user.save(); | 205 | this.user.save(); |
165 | } | 206 | } |
207 | |||
208 | private setStatus(status: AuthStatus) { | ||
209 | this.loginChanged.next(status); | ||
210 | } | ||
211 | |||
166 | } | 212 | } |
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 @@ | |||
1 | export * from './auth-http.service'; | 1 | export * from './auth-http.service'; |
2 | export * from './auth-status.model'; | 2 | export * from './auth-status.model'; |
3 | export * from './auth.service'; | 3 | export * from './auth.service'; |
4 | export * from './user.model'; | 4 | export * from './auth-user.model'; |
diff --git a/client/src/app/shared/forms/form-reactive.ts b/client/src/app/shared/forms/form-reactive.ts new file mode 100644 index 000000000..1e8a69771 --- /dev/null +++ b/client/src/app/shared/forms/form-reactive.ts | |||
@@ -0,0 +1,24 @@ | |||
1 | import { FormGroup } from '@angular/forms'; | ||
2 | |||
3 | export abstract class FormReactive { | ||
4 | abstract form: FormGroup; | ||
5 | abstract formErrors: Object; | ||
6 | abstract validationMessages: Object; | ||
7 | |||
8 | abstract buildForm(): void; | ||
9 | |||
10 | protected onValueChanged(data?: any) { | ||
11 | for (const field in this.formErrors) { | ||
12 | // clear previous error message (if any) | ||
13 | this.formErrors[field] = ''; | ||
14 | const control = this.form.get(field); | ||
15 | |||
16 | if (control && control.dirty && !control.valid) { | ||
17 | const messages = this.validationMessages[field]; | ||
18 | for (const key in control.errors) { | ||
19 | this.formErrors[field] += messages[key] + ' '; | ||
20 | } | ||
21 | } | ||
22 | } | ||
23 | } | ||
24 | } | ||
diff --git a/client/src/app/shared/forms/form-validators/index.ts b/client/src/app/shared/forms/form-validators/index.ts new file mode 100644 index 000000000..1d2ae6f68 --- /dev/null +++ b/client/src/app/shared/forms/form-validators/index.ts | |||
@@ -0,0 +1,3 @@ | |||
1 | export * from './url.validator'; | ||
2 | export * from './user'; | ||
3 | export * from './video'; | ||
diff --git a/client/src/app/shared/forms/form-validators/url.validator.ts b/client/src/app/shared/forms/form-validators/url.validator.ts new file mode 100644 index 000000000..67163b4e9 --- /dev/null +++ b/client/src/app/shared/forms/form-validators/url.validator.ts | |||
@@ -0,0 +1,11 @@ | |||
1 | import { FormControl } from '@angular/forms'; | ||
2 | |||
3 | export function validateUrl(c: FormControl) { | ||
4 | let URL_REGEXP = new RegExp('^https?://(www\.)?[-a-zA-Z0-9@:%._\+~#=]{2,256}\.[a-z]{2,6}\\b([-a-zA-Z0-9@:%_\+.~#?&//=]*)$'); | ||
5 | |||
6 | return URL_REGEXP.test(c.value) ? null : { | ||
7 | validateUrl: { | ||
8 | valid: false | ||
9 | } | ||
10 | }; | ||
11 | } | ||
diff --git a/client/src/app/shared/forms/form-validators/user.ts b/client/src/app/shared/forms/form-validators/user.ts new file mode 100644 index 000000000..5b11ff265 --- /dev/null +++ b/client/src/app/shared/forms/form-validators/user.ts | |||
@@ -0,0 +1,17 @@ | |||
1 | import { Validators } from '@angular/forms'; | ||
2 | |||
3 | export const USER_USERNAME = { | ||
4 | VALIDATORS: [ Validators.required, Validators.minLength(3), Validators.maxLength(20) ], | ||
5 | MESSAGES: { | ||
6 | 'required': 'Username is required.', | ||
7 | 'minlength': 'Username must be at least 3 characters long.', | ||
8 | 'maxlength': 'Username cannot be more than 20 characters long.' | ||
9 | } | ||
10 | }; | ||
11 | export const USER_PASSWORD = { | ||
12 | VALIDATORS: [ Validators.required, Validators.minLength(6) ], | ||
13 | MESSAGES: { | ||
14 | 'required': 'Password is required.', | ||
15 | 'minlength': 'Password must be at least 6 characters long.', | ||
16 | } | ||
17 | }; | ||
diff --git a/client/src/app/shared/forms/form-validators/video.ts b/client/src/app/shared/forms/form-validators/video.ts new file mode 100644 index 000000000..3766d4018 --- /dev/null +++ b/client/src/app/shared/forms/form-validators/video.ts | |||
@@ -0,0 +1,25 @@ | |||
1 | import { Validators } from '@angular/forms'; | ||
2 | |||
3 | export const VIDEO_NAME = { | ||
4 | VALIDATORS: [ Validators.required, Validators.minLength(3), Validators.maxLength(50) ], | ||
5 | MESSAGES: { | ||
6 | 'required': 'Video name is required.', | ||
7 | 'minlength': 'Video name must be at least 3 characters long.', | ||
8 | 'maxlength': 'Video name cannot be more than 50 characters long.' | ||
9 | } | ||
10 | }; | ||
11 | export const VIDEO_DESCRIPTION = { | ||
12 | VALIDATORS: [ Validators.required, Validators.minLength(3), Validators.maxLength(250) ], | ||
13 | MESSAGES: { | ||
14 | 'required': 'Video description is required.', | ||
15 | 'minlength': 'Video description must be at least 3 characters long.', | ||
16 | 'maxlength': 'Video description cannot be more than 250 characters long.' | ||
17 | } | ||
18 | }; | ||
19 | |||
20 | export const VIDEO_TAGS = { | ||
21 | VALIDATORS: [ Validators.pattern('^[a-zA-Z0-9]{2,10}$') ], | ||
22 | MESSAGES: { | ||
23 | 'pattern': 'A tag should be between 2 and 10 alphanumeric characters long.' | ||
24 | } | ||
25 | }; | ||
diff --git a/client/src/app/shared/forms/index.ts b/client/src/app/shared/forms/index.ts new file mode 100644 index 000000000..588ebb4be --- /dev/null +++ b/client/src/app/shared/forms/index.ts | |||
@@ -0,0 +1,2 @@ | |||
1 | export * from './form-validators'; | ||
2 | export * from './form-reactive'; | ||
diff --git a/client/src/app/shared/index.ts b/client/src/app/shared/index.ts index dfea4c67c..af34b4b64 100644 --- a/client/src/app/shared/index.ts +++ b/client/src/app/shared/index.ts | |||
@@ -1,2 +1,5 @@ | |||
1 | export * from './auth'; | 1 | export * from './auth'; |
2 | export * from './forms'; | ||
3 | export * from './rest'; | ||
2 | export * from './search'; | 4 | export * from './search'; |
5 | export * from './users'; | ||
diff --git a/client/src/app/shared/rest/index.ts b/client/src/app/shared/rest/index.ts new file mode 100644 index 000000000..3c9509dc7 --- /dev/null +++ b/client/src/app/shared/rest/index.ts | |||
@@ -0,0 +1,3 @@ | |||
1 | export * from './rest-extractor.service'; | ||
2 | export * from './rest-pagination'; | ||
3 | export * from './rest.service'; | ||
diff --git a/client/src/app/shared/rest/rest-extractor.service.ts b/client/src/app/shared/rest/rest-extractor.service.ts new file mode 100644 index 000000000..fcb1598f4 --- /dev/null +++ b/client/src/app/shared/rest/rest-extractor.service.ts | |||
@@ -0,0 +1,52 @@ | |||
1 | import { Injectable } from '@angular/core'; | ||
2 | import { Response } from '@angular/http'; | ||
3 | import { Observable } from 'rxjs/Observable'; | ||
4 | |||
5 | export interface ResultList { | ||
6 | data: any[]; | ||
7 | total: number; | ||
8 | } | ||
9 | |||
10 | @Injectable() | ||
11 | export class RestExtractor { | ||
12 | |||
13 | constructor () { ; } | ||
14 | |||
15 | extractDataBool(res: Response) { | ||
16 | return true; | ||
17 | } | ||
18 | |||
19 | extractDataList(res: Response) { | ||
20 | const body = res.json(); | ||
21 | |||
22 | const ret: ResultList = { | ||
23 | data: body.data, | ||
24 | total: body.total | ||
25 | }; | ||
26 | |||
27 | return ret; | ||
28 | } | ||
29 | |||
30 | extractDataGet(res: Response) { | ||
31 | return res.json(); | ||
32 | } | ||
33 | |||
34 | handleError(res: Response) { | ||
35 | let text = 'Server error: '; | ||
36 | text += res.text(); | ||
37 | let json = ''; | ||
38 | |||
39 | try { | ||
40 | json = res.json(); | ||
41 | } catch (err) { ; } | ||
42 | |||
43 | const error = { | ||
44 | json, | ||
45 | text | ||
46 | }; | ||
47 | |||
48 | console.error(error); | ||
49 | |||
50 | return Observable.throw(error); | ||
51 | } | ||
52 | } | ||
diff --git a/client/src/app/shared/rest/rest-pagination.ts b/client/src/app/shared/rest/rest-pagination.ts new file mode 100644 index 000000000..0cfa4f468 --- /dev/null +++ b/client/src/app/shared/rest/rest-pagination.ts | |||
@@ -0,0 +1,5 @@ | |||
1 | export interface RestPagination { | ||
2 | currentPage: number; | ||
3 | itemsPerPage: number; | ||
4 | totalItems: number; | ||
5 | }; | ||
diff --git a/client/src/app/shared/rest/rest.service.ts b/client/src/app/shared/rest/rest.service.ts new file mode 100644 index 000000000..16b47e957 --- /dev/null +++ b/client/src/app/shared/rest/rest.service.ts | |||
@@ -0,0 +1,27 @@ | |||
1 | import { Injectable } from '@angular/core'; | ||
2 | import { URLSearchParams } from '@angular/http'; | ||
3 | |||
4 | import { RestPagination } from './rest-pagination'; | ||
5 | |||
6 | @Injectable() | ||
7 | export class RestService { | ||
8 | |||
9 | buildRestGetParams(pagination?: RestPagination, sort?: string) { | ||
10 | const params = new URLSearchParams(); | ||
11 | |||
12 | if (pagination) { | ||
13 | const start: number = (pagination.currentPage - 1) * pagination.itemsPerPage; | ||
14 | const count: number = pagination.itemsPerPage; | ||
15 | |||
16 | params.set('start', start.toString()); | ||
17 | params.set('count', count.toString()); | ||
18 | } | ||
19 | |||
20 | if (sort) { | ||
21 | params.set('sort', sort); | ||
22 | } | ||
23 | |||
24 | return params; | ||
25 | } | ||
26 | |||
27 | } | ||
diff --git a/client/src/app/shared/search/search.component.ts b/client/src/app/shared/search/search.component.ts index e864fbc17..b6237469b 100644 --- a/client/src/app/shared/search/search.component.ts +++ b/client/src/app/shared/search/search.component.ts | |||
@@ -1,15 +1,13 @@ | |||
1 | import { Component, OnInit } from '@angular/core'; | 1 | import { Component, OnInit } from '@angular/core'; |
2 | 2 | import { Router } from '@angular/router'; | |
3 | import { DROPDOWN_DIRECTIVES} from 'ng2-bootstrap/components/dropdown'; | ||
4 | 3 | ||
5 | import { Search } from './search.model'; | 4 | import { Search } from './search.model'; |
6 | import { SearchField } from './search-field.type'; | 5 | import { SearchField } from './search-field.type'; |
7 | import { SearchService } from './search.service'; | 6 | import { SearchService } from './search.service'; |
8 | 7 | ||
9 | @Component({ | 8 | @Component({ |
10 | selector: 'my-search', | 9 | selector: 'my-search', |
11 | template: require('./search.component.html'), | 10 | templateUrl: './search.component.html' |
12 | directives: [ DROPDOWN_DIRECTIVES ] | ||
13 | }) | 11 | }) |
14 | 12 | ||
15 | export class SearchComponent implements OnInit { | 13 | export class SearchComponent implements OnInit { |
@@ -25,10 +23,10 @@ export class SearchComponent implements OnInit { | |||
25 | value: '' | 23 | value: '' |
26 | }; | 24 | }; |
27 | 25 | ||
28 | constructor(private searchService: SearchService) {} | 26 | constructor(private searchService: SearchService, private router: Router) {} |
29 | 27 | ||
30 | ngOnInit() { | 28 | ngOnInit() { |
31 | // Subscribe is the search changed | 29 | // Subscribe if the search changed |
32 | // Usually changed by videos list component | 30 | // Usually changed by videos list component |
33 | this.searchService.updateSearch.subscribe( | 31 | this.searchService.updateSearch.subscribe( |
34 | newSearchCriterias => { | 32 | newSearchCriterias => { |
@@ -58,6 +56,10 @@ export class SearchComponent implements OnInit { | |||
58 | } | 56 | } |
59 | 57 | ||
60 | doSearch() { | 58 | doSearch() { |
59 | if (this.router.url.indexOf('/videos/list') === -1) { | ||
60 | this.router.navigate([ '/videos/list' ]); | ||
61 | } | ||
62 | |||
61 | this.searchService.searchUpdated.next(this.searchCriterias); | 63 | this.searchService.searchUpdated.next(this.searchCriterias); |
62 | } | 64 | } |
63 | 65 | ||
diff --git a/client/src/app/shared/search/search.service.ts b/client/src/app/shared/search/search.service.ts index c7993db3d..717a7fa50 100644 --- a/client/src/app/shared/search/search.service.ts +++ b/client/src/app/shared/search/search.service.ts | |||
@@ -1,5 +1,6 @@ | |||
1 | import { Injectable } from '@angular/core'; | 1 | import { Injectable } from '@angular/core'; |
2 | import { Subject } from 'rxjs/Subject'; | 2 | import { Subject } from 'rxjs/Subject'; |
3 | import { ReplaySubject } from 'rxjs/ReplaySubject'; | ||
3 | 4 | ||
4 | import { Search } from './search.model'; | 5 | import { Search } from './search.model'; |
5 | 6 | ||
@@ -12,6 +13,6 @@ export class SearchService { | |||
12 | 13 | ||
13 | constructor() { | 14 | constructor() { |
14 | this.updateSearch = new Subject<Search>(); | 15 | this.updateSearch = new Subject<Search>(); |
15 | this.searchUpdated = new Subject<Search>(); | 16 | this.searchUpdated = new ReplaySubject<Search>(1); |
16 | } | 17 | } |
17 | } | 18 | } |
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..726495d11 --- /dev/null +++ b/client/src/app/shared/users/user.model.ts | |||
@@ -0,0 +1,20 @@ | |||
1 | export class User { | ||
2 | id: string; | ||
3 | username: string; | ||
4 | role: string; | ||
5 | createdDate: Date; | ||
6 | |||
7 | constructor(hash: { id: string, username: string, role: string, createdDate?: Date }) { | ||
8 | this.id = hash.id; | ||
9 | this.username = hash.username; | ||
10 | this.role = hash.role; | ||
11 | |||
12 | if (hash.createdDate) { | ||
13 | this.createdDate = hash.createdDate; | ||
14 | } | ||
15 | } | ||
16 | |||
17 | isAdmin() { | ||
18 | return this.role === 'admin'; | ||
19 | } | ||
20 | } | ||