Move to HttpClient and PrimeNG data table

This commit is contained in:
Chocobozzz 2017-09-14 11:57:49 +02:00
parent 91f6f169b1
commit d592e0a9b2
41 changed files with 621 additions and 697 deletions

View File

@ -62,7 +62,6 @@
"json-loader": "^0.5.4", "json-loader": "^0.5.4",
"ng-router-loader": "^2.0.0", "ng-router-loader": "^2.0.0",
"ng2-file-upload": "^1.1.4-2", "ng2-file-upload": "^1.1.4-2",
"ng2-smart-table": "1.2.1",
"ngc-webpack": "3.2.2", "ngc-webpack": "3.2.2",
"ngx-bootstrap": "1.9.1", "ngx-bootstrap": "1.9.1",
"ngx-chips": "1.5.3", "ngx-chips": "1.5.3",
@ -97,6 +96,7 @@
"add-asset-html-webpack-plugin": "^2.0.1", "add-asset-html-webpack-plugin": "^2.0.1",
"codelyzer": "^3.0.0-beta.4", "codelyzer": "^3.0.0-beta.4",
"extract-text-webpack-plugin": "^3.0.0", "extract-text-webpack-plugin": "^3.0.0",
"primeng": "^4.2.0",
"purify-css": "^1.2.5", "purify-css": "^1.2.5",
"purifycss-webpack": "^0.7.0", "purifycss-webpack": "^0.7.0",
"standard": "^10.0.0", "standard": "^10.0.0",

View File

@ -93,8 +93,9 @@ export class FriendAddComponent implements OnInit {
this.friendService.makeFriends(notEmptyHosts).subscribe( this.friendService.makeFriends(notEmptyHosts).subscribe(
status => { status => {
this.notificationsService.success('Sucess', 'Make friends request sent!') this.notificationsService.success('Success', 'Make friends request sent!')
this.router.navigate([ '/admin/friends/list' ]) // Wait requests between pods
setTimeout(() => this.router.navigate([ '/admin/friends/list' ]), 1000)
}, },
err => this.notificationsService.error('Error', err.text) err => this.notificationsService.error('Error', err.text)

View File

@ -2,7 +2,18 @@
<div class="content-padding"> <div class="content-padding">
<h3>Friends list</h3> <h3>Friends list</h3>
<ng2-smart-table [settings]="tableSettings" [source]="friendsSource" (delete)="removeFriend($event)"></ng2-smart-table> <p-dataTable [value]="friends">
<p-column field="id" header="ID"></p-column>
<p-column field="host" header="Host"></p-column>
<p-column field="email" header="Email"></p-column>
<p-column field="score" header="Score"></p-column>
<p-column field="createdAt" header="Created date"></p-column>
<p-column header="Delete" styleClass="action-cell">
<ng-template pTemplate="body" let-pod="rowData">
<span (click)="removeFriend(pod)" class="glyphicon glyphicon-remove glyphicon-black" title="Remove this pod"></span>
</ng-template>
</p-column>
</p-dataTable>
<a *ngIf="hasFriends()" class="btn btn-danger pull-left" (click)="quitFriends()"> <a *ngIf="hasFriends()" class="btn btn-danger pull-left" (click)="quitFriends()">
Quit friends Quit friends

View File

@ -1,10 +1,8 @@
import { Component } from '@angular/core' import { Component, OnInit } from '@angular/core'
import { NotificationsService } from 'angular2-notifications' import { NotificationsService } from 'angular2-notifications'
import { ServerDataSource } from 'ng2-smart-table'
import { ConfirmService } from '../../../core' import { ConfirmService } from '../../../core'
import { Utils } from '../../../shared'
import { FriendService } from '../shared' import { FriendService } from '../shared'
import { Pod } from '../../../../../../shared' import { Pod } from '../../../../../../shared'
@ -13,59 +11,21 @@ import { Pod } from '../../../../../../shared'
templateUrl: './friend-list.component.html', templateUrl: './friend-list.component.html',
styleUrls: ['./friend-list.component.scss'] styleUrls: ['./friend-list.component.scss']
}) })
export class FriendListComponent { export class FriendListComponent implements OnInit {
friendsSource = null friends: Pod[] = []
tableSettings = {
mode: 'external',
attr: {
class: 'table-hover'
},
hideSubHeader: true,
actions: {
position: 'right',
add: false,
edit: false,
delete: true
},
delete: {
deleteButtonContent: Utils.getRowDeleteButton()
},
columns: {
id: {
title: 'ID',
sort: false,
sortDirection: 'asc'
},
host: {
title: 'Host',
sort: false
},
email: {
title: 'Email',
sort: false
},
score: {
title: 'Score',
sort: false
},
createdAt: {
title: 'Created Date',
sort: false,
valuePrepareFunction: Utils.dateToHuman
}
}
}
constructor ( constructor (
private notificationsService: NotificationsService, private notificationsService: NotificationsService,
private confirmService: ConfirmService, private confirmService: ConfirmService,
private friendService: FriendService private friendService: FriendService
) { ) {}
this.friendsSource = this.friendService.getDataSource()
ngOnInit () {
this.loadData()
} }
hasFriends () { hasFriends () {
return this.friendsSource.count() !== 0 return this.friends.length !== 0
} }
quitFriends () { quitFriends () {
@ -77,18 +37,17 @@ export class FriendListComponent {
this.friendService.quitFriends().subscribe( this.friendService.quitFriends().subscribe(
status => { status => {
this.notificationsService.success('Success', 'Friends left!') this.notificationsService.success('Success', 'Friends left!')
this.friendsSource.refresh() this.loadData()
}, },
err => this.notificationsService.error('Error', err.text) err => this.notificationsService.error('Error', err)
) )
} }
) )
} }
removeFriend ({ data }) { removeFriend (friend: Pod) {
const confirmMessage = 'Do you really want to remove this friend ? All its videos will be deleted.' const confirmMessage = 'Do you really want to remove this friend ? All its videos will be deleted.'
const friend: Pod = data
this.confirmService.confirm(confirmMessage, 'Remove').subscribe( this.confirmService.confirm(confirmMessage, 'Remove').subscribe(
res => { res => {
@ -97,12 +56,23 @@ export class FriendListComponent {
this.friendService.removeFriend(friend).subscribe( this.friendService.removeFriend(friend).subscribe(
status => { status => {
this.notificationsService.success('Success', 'Friend removed') this.notificationsService.success('Success', 'Friend removed')
this.friendsSource.refresh() this.loadData()
}, },
err => this.notificationsService.error('Error', err.text) err => this.notificationsService.error('Error', err)
) )
} }
) )
} }
private loadData () {
this.friendService.getFriends()
.subscribe(
resultList => {
this.friends = resultList.data
},
err => this.notificationsService.error('Error', err)
)
}
} }

View File

@ -1,24 +1,24 @@
import { Injectable } from '@angular/core' import { Injectable } from '@angular/core'
import { Observable } from 'rxjs/Observable' import { HttpClient } from '@angular/common/http'
import 'rxjs/add/operator/catch' import 'rxjs/add/operator/catch'
import 'rxjs/add/operator/map' import 'rxjs/add/operator/map'
import { ServerDataSource } from 'ng2-smart-table' import { RestExtractor, } from '../../../shared'
import { Pod, ResultList } from '../../../../../../shared'
import { AuthHttp, RestExtractor, RestDataSource, ResultList } from '../../../shared'
import { Pod } from '../../../../../../shared'
@Injectable() @Injectable()
export class FriendService { export class FriendService {
private static BASE_FRIEND_URL = API_URL + '/api/v1/pods/' private static BASE_FRIEND_URL = API_URL + '/api/v1/pods/'
constructor ( constructor (
private authHttp: AuthHttp, private authHttp: HttpClient,
private restExtractor: RestExtractor private restExtractor: RestExtractor
) {} ) {}
getDataSource () { getFriends () {
return new RestDataSource(this.authHttp, FriendService.BASE_FRIEND_URL) return this.authHttp.get<ResultList<Pod>>(FriendService.BASE_FRIEND_URL)
.map(res => this.restExtractor.convertResultListDateToHuman(res))
.catch(res => this.restExtractor.handleError(res))
} }
makeFriends (notEmptyHosts: String[]) { makeFriends (notEmptyHosts: String[]) {
@ -28,18 +28,18 @@ export class FriendService {
return this.authHttp.post(FriendService.BASE_FRIEND_URL + 'make-friends', body) return this.authHttp.post(FriendService.BASE_FRIEND_URL + 'make-friends', body)
.map(this.restExtractor.extractDataBool) .map(this.restExtractor.extractDataBool)
.catch((res) => this.restExtractor.handleError(res)) .catch(res => this.restExtractor.handleError(res))
} }
quitFriends () { quitFriends () {
return this.authHttp.get(FriendService.BASE_FRIEND_URL + 'quit-friends') return this.authHttp.get(FriendService.BASE_FRIEND_URL + 'quit-friends')
.map(res => res.status) .map(this.restExtractor.extractDataBool)
.catch((res) => this.restExtractor.handleError(res)) .catch(res => this.restExtractor.handleError(res))
} }
removeFriend (friend: Pod) { removeFriend (friend: Pod) {
return this.authHttp.delete(FriendService.BASE_FRIEND_URL + friend.id) return this.authHttp.delete(FriendService.BASE_FRIEND_URL + friend.id)
.map(this.restExtractor.extractDataBool) .map(this.restExtractor.extractDataBool)
.catch((res) => this.restExtractor.handleError(res)) .catch(res => this.restExtractor.handleError(res))
} }
} }

View File

@ -1,10 +1,11 @@
import { Injectable } from '@angular/core' import { Injectable } from '@angular/core'
import { HttpClient } from '@angular/common/http'
import { Observable } from 'rxjs/Observable' import { Observable } from 'rxjs/Observable'
import 'rxjs/add/operator/catch' import 'rxjs/add/operator/catch'
import 'rxjs/add/operator/map' import 'rxjs/add/operator/map'
import { RequestSchedulerStats } from '../../../../../../shared' import { RequestSchedulerStats } from '../../../../../../shared'
import { AuthHttp, RestExtractor } from '../../../shared' import { RestExtractor } from '../../../shared'
import { RequestSchedulerStatsAttributes } from './request-schedulers-stats-attributes.model' import { RequestSchedulerStatsAttributes } from './request-schedulers-stats-attributes.model'
@Injectable() @Injectable()
@ -12,19 +13,18 @@ export class RequestSchedulersService {
private static BASE_REQUEST_URL = API_URL + '/api/v1/request-schedulers/' private static BASE_REQUEST_URL = API_URL + '/api/v1/request-schedulers/'
constructor ( constructor (
private authHttp: AuthHttp, private authHttp: HttpClient,
private restExtractor: RestExtractor private restExtractor: RestExtractor
) {} ) {}
getStats (): Observable<RequestSchedulerStats> { getStats () {
return this.authHttp.get(RequestSchedulersService.BASE_REQUEST_URL + 'stats') return this.authHttp.get<RequestSchedulerStats>(RequestSchedulersService.BASE_REQUEST_URL + 'stats')
.map(this.restExtractor.extractDataGet) .map(res => this.buildRequestObjects(res))
.map(this.buildRequestObjects) .catch(res => this.restExtractor.handleError(res))
.catch((res) => this.restExtractor.handleError(res))
} }
private buildRequestObjects (data: RequestSchedulerStats) { private buildRequestObjects (data: RequestSchedulerStats) {
const requestSchedulers = {} const requestSchedulers: { [ id: string ]: RequestSchedulerStatsAttributes } = {}
Object.keys(data).forEach(requestSchedulerName => { Object.keys(data).forEach(requestSchedulerName => {
requestSchedulers[requestSchedulerName] = new RequestSchedulerStatsAttributes(data[requestSchedulerName]) requestSchedulers[requestSchedulerName] = new RequestSchedulerStatsAttributes(data[requestSchedulerName])

View File

@ -1,11 +1,14 @@
import { Injectable } from '@angular/core' import { Injectable } from '@angular/core'
import { HttpClient, HttpParams } from '@angular/common/http'
import { Observable } from 'rxjs/Observable'
import 'rxjs/add/operator/catch' import 'rxjs/add/operator/catch'
import 'rxjs/add/operator/map' import 'rxjs/add/operator/map'
import { SortMeta } from 'primeng/primeng'
import { BytesPipe } from 'angular-pipes/src/math/bytes.pipe' import { BytesPipe } from 'angular-pipes/src/math/bytes.pipe'
import { AuthHttp, RestExtractor, RestDataSource, User } from '../../../shared' import { RestExtractor, User, RestPagination, RestService } from '../../../shared'
import { UserCreate, UserUpdate } from '../../../../../../shared' import { UserCreate, UserUpdate, ResultList } from '../../../../../../shared'
@Injectable() @Injectable()
export class UserService { export class UserService {
@ -13,40 +16,43 @@ export class UserService {
private bytesPipe = new BytesPipe() private bytesPipe = new BytesPipe()
constructor ( constructor (
private authHttp: AuthHttp, private authHttp: HttpClient,
private restService: RestService,
private restExtractor: RestExtractor private restExtractor: RestExtractor
) {} ) {}
addUser (userCreate: UserCreate) { addUser (userCreate: UserCreate) {
return this.authHttp.post(UserService.BASE_USERS_URL, userCreate) return this.authHttp.post(UserService.BASE_USERS_URL, userCreate)
.map(this.restExtractor.extractDataBool) .map(this.restExtractor.extractDataBool)
.catch(this.restExtractor.handleError) .catch(err => this.restExtractor.handleError(err))
} }
updateUser (userId: number, userUpdate: UserUpdate) { updateUser (userId: number, userUpdate: UserUpdate) {
return this.authHttp.put(UserService.BASE_USERS_URL + userId, userUpdate) return this.authHttp.put(UserService.BASE_USERS_URL + userId, userUpdate)
.map(this.restExtractor.extractDataBool) .map(this.restExtractor.extractDataBool)
.catch(this.restExtractor.handleError) .catch(err => this.restExtractor.handleError(err))
} }
getUser (userId: number) { getUser (userId: number) {
return this.authHttp.get(UserService.BASE_USERS_URL + userId) return this.authHttp.get<User>(UserService.BASE_USERS_URL + userId)
.map(this.restExtractor.extractDataGet) .catch(err => this.restExtractor.handleError(err))
.catch(this.restExtractor.handleError)
} }
getDataSource () { getUsers (pagination: RestPagination, sort: SortMeta): Observable<ResultList<User>> {
return new RestDataSource(this.authHttp, UserService.BASE_USERS_URL, this.formatDataSource.bind(this)) let params = new HttpParams()
params = this.restService.addRestGetParams(params, pagination, sort)
return this.authHttp.get<ResultList<User>>(UserService.BASE_USERS_URL, { params })
.map(res => this.restExtractor.convertResultListDateToHuman(res))
.map(res => this.restExtractor.applyToResultListData(res, this.formatUser.bind(this)))
.catch(err => this.restExtractor.handleError(err))
} }
removeUser (user: User) { removeUser (user: User) {
return this.authHttp.delete(UserService.BASE_USERS_URL + user.id) return this.authHttp.delete(UserService.BASE_USERS_URL + user.id)
} }
private formatDataSource (users: User[]) { private formatUser (user: User) {
const newUsers = []
users.forEach(user => {
let videoQuota let videoQuota
if (user.videoQuota === -1) { if (user.videoQuota === -1) {
videoQuota = 'Unlimited' videoQuota = 'Unlimited'
@ -54,12 +60,8 @@ export class UserService {
videoQuota = this.bytesPipe.transform(user.videoQuota) videoQuota = this.bytesPipe.transform(user.videoQuota)
} }
const newUser = Object.assign(user, { return Object.assign(user, {
videoQuota videoQuota
}) })
newUsers.push(newUser)
})
return newUsers
} }
} }

View File

@ -3,10 +3,29 @@
<h3>Users list</h3> <h3>Users list</h3>
<ng2-smart-table <p-dataTable
[settings]="tableSettings" [source]="usersSource" [value]="users" [lazy]="true" [paginator]="true" [totalRecords]="totalRecords" [rows]="rowsPerPage"
(delete)="removeUser($event)" (edit)="editUser($event)" sortField="id" (onLazyLoad)="loadLazy($event)"
></ng2-smart-table> >
<p-column field="id" header="ID" [sortable]="true"></p-column>
<p-column field="username" header="Username" [sortable]="true"></p-column>
<p-column field="email" header="Email"></p-column>
<p-column field="videoQuota" header="Video quota"></p-column>
<p-column field="role" header="Role"></p-column>
<p-column field="createdAt" header="Created date" [sortable]="true"></p-column>
<p-column header="Edit" styleClass="action-cell">
<ng-template pTemplate="body" let-user="rowData">
<a [routerLink]="getRouterUserEditLink(user)" title="Edit this user">
<span class="glyphicon glyphicon-pencil glyphicon-black"></span>
</a>
</ng-template>
</p-column>
<p-column header="Delete" styleClass="action-cell">
<ng-template pTemplate="body" let-user="rowData">
<span (click)="removeUser(user)" class="glyphicon glyphicon-remove glyphicon-black" title="Remove this user"></span>
</ng-template>
</p-column>
</p-dataTable>
<a class="add-user btn btn-success pull-right" [routerLink]="['/admin/users/add']"> <a class="add-user btn btn-success pull-right" [routerLink]="['/admin/users/add']">
<span class="glyphicon glyphicon-plus"></span> <span class="glyphicon glyphicon-plus"></span>

View File

@ -1,82 +1,37 @@
import { Component } from '@angular/core' import { Component, OnInit } from '@angular/core'
import { SortMeta } from 'primeng/primeng'
import { NotificationsService } from 'angular2-notifications' import { NotificationsService } from 'angular2-notifications'
import { ConfirmService } from '../../../core' import { ConfirmService } from '../../../core'
import { RestDataSource, User, Utils } from '../../../shared' import { RestTable, RestPagination, User } from '../../../shared'
import { UserService } from '../shared' import { UserService } from '../shared'
import { Router } from '@angular/router'
@Component({ @Component({
selector: 'my-user-list', selector: 'my-user-list',
templateUrl: './user-list.component.html', templateUrl: './user-list.component.html',
styleUrls: [ './user-list.component.scss' ] styleUrls: [ './user-list.component.scss' ]
}) })
export class UserListComponent { export class UserListComponent extends RestTable implements OnInit {
usersSource: RestDataSource = null users: User[] = []
tableSettings = { totalRecords = 0
mode: 'external', rowsPerPage = 10
attr: { sort: SortMeta = { field: 'id', order: 1 }
class: 'table-hover' pagination: RestPagination = { count: this.rowsPerPage, start: 0 }
},
hideSubHeader: true,
actions: {
position: 'right',
add: false,
edit: true,
delete: true
},
delete: {
deleteButtonContent: Utils.getRowDeleteButton()
},
edit: {
editButtonContent: Utils.getRowEditButton()
},
pager: {
display: true,
perPage: 10
},
columns: {
id: {
title: 'ID',
sortDirection: 'asc'
},
username: {
title: 'Username'
},
email: {
title: 'Email'
},
videoQuota: {
title: 'Video quota'
},
role: {
title: 'Role',
sort: false
},
createdAt: {
title: 'Created Date',
valuePrepareFunction: Utils.dateToHuman
}
}
}
constructor ( constructor (
private router: Router,
private notificationsService: NotificationsService, private notificationsService: NotificationsService,
private confirmService: ConfirmService, private confirmService: ConfirmService,
private userService: UserService private userService: UserService
) { ) {
this.usersSource = this.userService.getDataSource() super()
} }
editUser ({ data }: { data: User }) { ngOnInit () {
this.router.navigate([ '/admin', 'users', data.id, 'update' ]) this.loadData()
} }
removeUser ({ data }: { data: User }) { removeUser (user: User) {
const user = data
if (user.username === 'root') { if (user.username === 'root') {
this.notificationsService.error('Error', 'You cannot delete root.') this.notificationsService.error('Error', 'You cannot delete root.')
return return
@ -89,12 +44,28 @@ export class UserListComponent {
this.userService.removeUser(user).subscribe( this.userService.removeUser(user).subscribe(
() => { () => {
this.notificationsService.success('Success', `User ${user.username} deleted.`) this.notificationsService.success('Success', `User ${user.username} deleted.`)
this.usersSource.refresh() this.loadData()
}, },
err => this.notificationsService.error('Error', err.text) err => this.notificationsService.error('Error', err)
) )
} }
) )
} }
getRouterUserEditLink (user: User) {
return [ '/admin', 'users', user.id, 'update' ]
}
protected loadData () {
this.userService.getUsers(this.pagination, this.sort)
.subscribe(
resultList => {
this.users = resultList.data
this.totalRecords = resultList.total
},
err => this.notificationsService.error('Error', err)
)
}
} }

View File

@ -3,9 +3,21 @@
<h3>Video abuses list</h3> <h3>Video abuses list</h3>
<ng2-smart-table <p-dataTable
[settings]="tableSettings" [source]="videoAbusesSource" [value]="videoAbuses" [lazy]="true" [paginator]="true" [totalRecords]="totalRecords" [rows]="rowsPerPage"
></ng2-smart-table> sortField="id" (onLazyLoad)="loadLazy($event)"
>
<p-column field="id" header="ID" [sortable]="true"></p-column>
<p-column field="reason" header="Reason"></p-column>
<p-column field="reporterPodHost" header="Reporter pod host"></p-column>
<p-column field="reporterUsername" header="Reporter username"></p-column>
<p-column header="Video" styleClass="action-cell">
<ng-template pTemplate="body" let-videoAbuse="rowData">
<a [routerLink]="getRouterVideoLink(videoAbuse.videoId)" title="Go to the video">{{ videoAbuse.videoId }}</a>
</ng-template>
</p-column>
<p-column field="createdAt" header="Created date" [sortable]="true"></p-column>
</p-dataTable>
</div> </div>
</div> </div>

View File

@ -1,72 +1,46 @@
import { Component } from '@angular/core' import { Component, OnInit } from '@angular/core'
import { NotificationsService } from 'angular2-notifications' import { NotificationsService } from 'angular2-notifications'
import { SortMeta } from 'primeng/primeng'
import { Utils, VideoAbuseService } from '../../../shared' import { RestTable, RestPagination, VideoAbuseService } from '../../../shared'
import { VideoAbuse } from '../../../../../shared' import { VideoAbuse } from '../../../../../../shared'
@Component({ @Component({
selector: 'my-video-abuse-list', selector: 'my-video-abuse-list',
templateUrl: './video-abuse-list.component.html' templateUrl: './video-abuse-list.component.html'
}) })
export class VideoAbuseListComponent { export class VideoAbuseListComponent extends RestTable implements OnInit {
videoAbusesSource = null videoAbuses: VideoAbuse[] = []
tableSettings = { totalRecords = 0
mode: 'external', rowsPerPage = 1
attr: { sort: SortMeta = { field: 'id', order: 1 }
class: 'table-hover' pagination: RestPagination = { count: this.rowsPerPage, start: 0 }
},
hideSubHeader: true,
actions: {
position: 'right',
add: false,
edit: false,
delete: false
},
pager: {
display: true,
perPage: 10
},
columns: {
id: {
title: 'ID',
sortDirection: 'asc'
},
reason: {
title: 'Reason',
sort: false
},
reporterPodHost: {
title: 'Reporter pod host',
sort: false
},
reporterUsername: {
title: 'Reporter username',
sort: false
},
videoId: {
title: 'Video',
type: 'html',
sort: false,
valuePrepareFunction: this.buildVideoLink
},
createdAt: {
title: 'Created Date',
valuePrepareFunction: Utils.dateToHuman
}
}
}
constructor ( constructor (
private notificationsService: NotificationsService, private notificationsService: NotificationsService,
private videoAbuseService: VideoAbuseService private videoAbuseService: VideoAbuseService
) { ) {
this.videoAbusesSource = this.videoAbuseService.getDataSource() super()
} }
buildVideoLink (videoId: string) { ngOnInit () {
// TODO: transform to routerLink this.loadData()
// https://github.com/akveo/ng2-smart-table/issues/57 }
return `<a href="/videos/${videoId}" title="Go to the video">${videoId}</a>`
getRouterVideoLink (videoId: number) {
return [ '/videos', videoId ]
}
protected loadData () {
return this.videoAbuseService.getVideoAbuses(this.pagination, this.sort)
.subscribe(
resultList => {
this.videoAbuses = resultList.data
this.totalRecords = resultList.total
},
err => this.notificationsService.error('Error', err)
)
} }
} }

View File

@ -59,7 +59,7 @@ export class AccountDetailsComponent extends FormReactive implements OnInit {
() => { () => {
this.notificationsService.success('Success', 'Information updated.') this.notificationsService.success('Success', 'Information updated.')
this.authService.refreshUserInformations() this.authService.refreshUserInformation()
}, },
err => this.error = err err => this.error = err

View File

@ -36,6 +36,8 @@ export class AppComponent implements OnInit {
) {} ) {}
ngOnInit () { ngOnInit () {
this.authService.loadClientCredentials()
if (this.authService.isLoggedIn()) { if (this.authService.isLoggedIn()) {
// The service will automatically redirect to the login page if the token is not valid anymore // The service will automatically redirect to the login page if the token is not valid anymore
this.userService.checkTokenValidity() this.userService.checkTokenValidity()

View File

@ -7,8 +7,6 @@ import {
} from '@angularclass/hmr' } from '@angularclass/hmr'
import { MetaModule, MetaLoader, MetaStaticLoader, PageTitlePositioning } from '@ngx-meta/core' import { MetaModule, MetaLoader, MetaStaticLoader, PageTitlePositioning } from '@ngx-meta/core'
// TODO: remove, we need this to avoid error in ng2-smart-table
import 'rxjs/add/operator/toPromise'
import 'bootstrap-loader' import 'bootstrap-loader'
import { ENV_PROVIDERS } from './environment' import { ENV_PROVIDERS } from './environment'

View File

@ -1,8 +1,8 @@
import { Injectable } from '@angular/core' import { Injectable } from '@angular/core'
import { Headers, Http, Response, URLSearchParams } from '@angular/http'
import { Router } from '@angular/router' import { Router } from '@angular/router'
import { Observable } from 'rxjs/Observable' import { Observable } from 'rxjs/Observable'
import { Subject } from 'rxjs/Subject' import { Subject } from 'rxjs/Subject'
import { HttpClient, HttpHeaders, HttpParams } from '@angular/common/http'
import 'rxjs/add/operator/map' import 'rxjs/add/operator/map'
import 'rxjs/add/operator/mergeMap' import 'rxjs/add/operator/mergeMap'
import 'rxjs/add/observable/throw' import 'rxjs/add/observable/throw'
@ -11,15 +11,35 @@ import { NotificationsService } from 'angular2-notifications'
import { AuthStatus } from './auth-status.model' import { AuthStatus } from './auth-status.model'
import { AuthUser } from './auth-user.model' import { AuthUser } from './auth-user.model'
import { OAuthClientLocal, UserRole } from '../../../../../shared' import { OAuthClientLocal, UserRole, UserRefreshToken } from '../../../../../shared'
// Do not use the barrel (dependency loop) // Do not use the barrel (dependency loop)
import { RestExtractor } from '../../shared/rest' import { RestExtractor } from '../../shared/rest'
import { UserLogin } from '../../../../../shared/models/users/user-login.model'
import { User } from '../../shared/users/user.model'
interface UserLoginWithUsername extends UserLogin {
access_token: string
refresh_token: string
token_type: string
username: string
}
interface UserLoginWithUserInformation extends UserLogin {
access_token: string
refresh_token: string
token_type: string
username: string
id: number
role: UserRole
displayNSFW: boolean
email: string
}
@Injectable() @Injectable()
export class AuthService { export class AuthService {
private static BASE_CLIENT_URL = API_URL + '/api/v1/oauth-clients/local' private static BASE_CLIENT_URL = API_URL + '/api/v1/oauth-clients/local'
private static BASE_TOKEN_URL = API_URL + '/api/v1/users/token' private static BASE_TOKEN_URL = API_URL + '/api/v1/users/token'
private static BASE_USER_INFORMATIONS_URL = API_URL + '/api/v1/users/me' private static BASE_USER_INFORMATION_URL = API_URL + '/api/v1/users/me'
loginChangedSource: Observable<AuthStatus> loginChangedSource: Observable<AuthStatus>
@ -29,7 +49,7 @@ export class AuthService {
private user: AuthUser = null private user: AuthUser = null
constructor ( constructor (
private http: Http, private http: HttpClient,
private notificationsService: NotificationsService, private notificationsService: NotificationsService,
private restExtractor: RestExtractor, private restExtractor: RestExtractor,
private router: Router private router: Router
@ -37,15 +57,19 @@ export class AuthService {
this.loginChanged = new Subject<AuthStatus>() this.loginChanged = new Subject<AuthStatus>()
this.loginChangedSource = this.loginChanged.asObservable() this.loginChangedSource = this.loginChanged.asObservable()
// Return null if there is nothing to load
this.user = AuthUser.load()
}
loadClientCredentials () {
// Fetch the client_id/client_secret // Fetch the client_id/client_secret
// FIXME: save in local storage? // FIXME: save in local storage?
this.http.get(AuthService.BASE_CLIENT_URL) this.http.get<OAuthClientLocal>(AuthService.BASE_CLIENT_URL)
.map(this.restExtractor.extractDataGet)
.catch(res => this.restExtractor.handleError(res)) .catch(res => this.restExtractor.handleError(res))
.subscribe( .subscribe(
(result: OAuthClientLocal) => { res => {
this.clientId = result.client_id this.clientId = res.client_id
this.clientSecret = result.client_secret this.clientSecret = res.client_secret
console.log('Client credentials loaded.') console.log('Client credentials loaded.')
}, },
@ -58,9 +82,6 @@ export class AuthService {
this.notificationsService.error('Error', errorMessage, { timeOut: 7000 }) this.notificationsService.error('Error', errorMessage, { timeOut: 7000 })
} }
) )
// Return null if there is nothing to load
this.user = AuthUser.load()
} }
getRefreshToken () { getRefreshToken () {
@ -70,7 +91,11 @@ export class AuthService {
} }
getRequestHeaderValue () { getRequestHeaderValue () {
return `${this.getTokenType()} ${this.getAccessToken()}` const accessToken = this.getAccessToken()
if (accessToken === null) return null
return `${this.getTokenType()} ${accessToken}`
} }
getAccessToken () { getAccessToken () {
@ -96,39 +121,26 @@ export class AuthService {
} }
isLoggedIn () { isLoggedIn () {
if (this.getAccessToken()) { return !!this.getAccessToken()
return true
} else {
return false
}
} }
login (username: string, password: string) { login (username: string, password: string) {
let body = new URLSearchParams() // Form url encoded
body.set('client_id', this.clientId) const body = new HttpParams().set('client_id', this.clientId)
body.set('client_secret', this.clientSecret) .set('client_secret', this.clientSecret)
body.set('response_type', 'code') .set('response_type', 'code')
body.set('grant_type', 'password') .set('grant_type', 'password')
body.set('scope', 'upload') .set('scope', 'upload')
body.set('username', username) .set('username', username)
body.set('password', password) .set('password', password)
let headers = new Headers() const headers = new HttpHeaders().set('Content-Type', 'application/x-www-form-urlencoded')
headers.append('Content-Type', 'application/x-www-form-urlencoded')
let options = { return this.http.post<UserLogin>(AuthService.BASE_TOKEN_URL, body, { headers })
headers: headers .map(res => Object.assign(res, { username }))
} .flatMap(res => this.mergeUserInformation(res))
return this.http.post(AuthService.BASE_TOKEN_URL, body.toString(), options)
.map(this.restExtractor.extractDataGet)
.map(res => {
res.username = username
return res
})
.flatMap(res => this.mergeUserInformations(res))
.map(res => this.handleLogin(res)) .map(res => this.handleLogin(res))
.catch((res) => this.restExtractor.handleError(res)) .catch(res => this.restExtractor.handleError(res))
} }
logout () { logout () {
@ -145,33 +157,26 @@ export class AuthService {
const refreshToken = this.getRefreshToken() const refreshToken = this.getRefreshToken()
let body = new URLSearchParams() // Form url encoded
body.set('refresh_token', refreshToken) const body = new HttpParams().set('refresh_token', refreshToken)
body.set('client_id', this.clientId) .set('client_id', this.clientId)
body.set('client_secret', this.clientSecret) .set('client_secret', this.clientSecret)
body.set('response_type', 'code') .set('response_type', 'code')
body.set('grant_type', 'refresh_token') .set('grant_type', 'refresh_token')
let headers = new Headers() const headers = new HttpHeaders().set('Content-Type', 'application/x-www-form-urlencoded')
headers.append('Content-Type', 'application/x-www-form-urlencoded')
let options = { return this.http.post<UserRefreshToken>(AuthService.BASE_TOKEN_URL, body, { headers })
headers: headers
}
return this.http.post(AuthService.BASE_TOKEN_URL, body.toString(), options)
.map(this.restExtractor.extractDataGet)
.map(res => this.handleRefreshToken(res)) .map(res => this.handleRefreshToken(res))
.catch((res: Response) => { .catch(res => {
// The refresh token is invalid? // The refresh token is invalid?
if (res.status === 400 && res.json() && res.json().error === 'invalid_grant') { if (res.status === 400 && res.error === 'invalid_grant') {
console.error('Cannot refresh token -> logout...') console.error('Cannot refresh token -> logout...')
this.logout() this.logout()
this.router.navigate(['/login']) this.router.navigate(['/login'])
return Observable.throw({ return Observable.throw({
json: () => '', error: 'You need to reconnect.'
text: () => 'You need to reconnect.'
}) })
} }
@ -179,7 +184,7 @@ export class AuthService {
}) })
} }
refreshUserInformations () { refreshUserInformation () {
const obj = { const obj = {
access_token: this.user.getAccessToken(), access_token: this.user.getAccessToken(),
refresh_token: null, refresh_token: null,
@ -187,7 +192,7 @@ export class AuthService {
username: this.user.username username: this.user.username
} }
this.mergeUserInformations (obj) this.mergeUserInformation(obj)
.subscribe( .subscribe(
res => { res => {
this.user.displayNSFW = res.displayNSFW this.user.displayNSFW = res.displayNSFW
@ -198,19 +203,11 @@ export class AuthService {
) )
} }
private mergeUserInformations (obj: { private mergeUserInformation (obj: UserLoginWithUsername): Observable<UserLoginWithUserInformation> {
access_token: string, // User is not loaded yet, set manually auth header
refresh_token: string, const headers = new HttpHeaders().set('Authorization', `${obj.token_type} ${obj.access_token}`)
token_type: string,
username: string
}) {
// Do not call authHttp here to avoid circular dependencies headaches
const headers = new Headers() return this.http.get<User>(AuthService.BASE_USER_INFORMATION_URL, { headers })
headers.set('Authorization', `Bearer ${obj.access_token}`)
return this.http.get(AuthService.BASE_USER_INFORMATIONS_URL, { headers })
.map(res => res.json())
.map(res => { .map(res => {
const newProperties = { const newProperties = {
id: res.id as number, id: res.id as number,
@ -224,16 +221,7 @@ export class AuthService {
) )
} }
private handleLogin (obj: { private handleLogin (obj: UserLoginWithUserInformation) {
access_token: string,
refresh_token: string,
token_type: string,
id: number,
username: string,
email: string,
role: UserRole,
displayNSFW: boolean
}) {
const id = obj.id const id = obj.id
const username = obj.username const username = obj.username
const role = obj.role const role = obj.role
@ -251,7 +239,7 @@ export class AuthService {
this.setStatus(AuthStatus.LoggedIn) this.setStatus(AuthStatus.LoggedIn)
} }
private handleRefreshToken (obj: { access_token: string, refresh_token: string }) { private handleRefreshToken (obj: UserRefreshToken) {
this.user.refreshTokens(obj.access_token, obj.refresh_token) this.user.refreshTokens(obj.access_token, obj.refresh_token)
this.user.save() this.user.save()
} }
@ -259,5 +247,4 @@ export class AuthService {
private setStatus (status: AuthStatus) { private setStatus (status: AuthStatus) {
this.loginChanged.next(status) this.loginChanged.next(status)
} }
} }

View File

@ -1,7 +1,6 @@
import { Injectable } from '@angular/core' import { Injectable } from '@angular/core'
import { Http } from '@angular/http' import { HttpClient } from '@angular/common/http'
import { RestExtractor } from '../../shared/rest'
import { ServerConfig } from '../../../../../shared' import { ServerConfig } from '../../../../../shared'
@Injectable() @Injectable()
@ -14,17 +13,11 @@ export class ConfigService {
} }
} }
constructor ( constructor (private http: HttpClient) {}
private http: Http,
private restExtractor: RestExtractor
) {}
loadConfig () { loadConfig () {
this.http.get(ConfigService.BASE_CONFIG_URL) this.http.get<ServerConfig>(ConfigService.BASE_CONFIG_URL)
.map(this.restExtractor.extractDataGet) .subscribe(data => this.config = data)
.subscribe(data => {
this.config = data
})
} }
getConfig () { getConfig () {

View File

@ -1,93 +0,0 @@
import { Injectable } from '@angular/core'
import {
ConnectionBackend,
Headers,
Http,
Request,
RequestMethod,
RequestOptions,
RequestOptionsArgs,
Response,
XHRBackend
} from '@angular/http'
import { Observable } from 'rxjs/Observable'
import { AuthService } from '../../core'
@Injectable()
export class AuthHttp extends Http {
constructor (backend: ConnectionBackend, defaultOptions: RequestOptions, private authService: AuthService) {
super(backend, defaultOptions)
}
request (url: string | Request, options?: RequestOptionsArgs): Observable<Response> {
if (!options) options = {}
options.headers = new Headers()
this.setAuthorizationHeader(options.headers)
return super.request(url, options)
.catch((err) => {
if (err.status === 401) {
return this.handleTokenExpired(url, options)
}
return Observable.throw(err)
})
}
delete (url: string, options?: RequestOptionsArgs): Observable<Response> {
if (!options) options = {}
options.method = RequestMethod.Delete
return this.request(url, options)
}
get (url: string, options?: RequestOptionsArgs): Observable<Response> {
if (!options) options = {}
options.method = RequestMethod.Get
return this.request(url, options)
}
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, body: any, options?: RequestOptionsArgs): Observable<Response> {
if (!options) options = {}
options.method = RequestMethod.Put
options.body = body
return this.request(url, options)
}
private handleTokenExpired (url: string | Request, options: RequestOptionsArgs) {
return this.authService.refreshAccessToken()
.flatMap(() => {
this.setAuthorizationHeader(options.headers)
return super.request(url, options)
})
}
private setAuthorizationHeader (headers: Headers) {
headers.set('Authorization', this.authService.getRequestHeaderValue())
}
}
export function useFactory (backend: XHRBackend, defaultOptions: RequestOptions, authService: AuthService) {
return new AuthHttp(backend, defaultOptions, authService)
}
export const AUTH_HTTP_PROVIDERS = [
{
provide: AuthHttp,
useFactory,
deps: [ XHRBackend, RequestOptions, AuthService ]
}
]

View File

@ -0,0 +1,62 @@
import { Injectable, Injector } from '@angular/core'
import {
HttpInterceptor,
HttpRequest,
HttpEvent,
HttpHandler, HTTP_INTERCEPTORS
} from '@angular/common/http'
import { Observable } from 'rxjs/Observable'
import { AuthService } from '../../core'
import 'rxjs/add/operator/switchMap'
@Injectable()
export class AuthInterceptor implements HttpInterceptor {
private authService: AuthService
// https://github.com/angular/angular/issues/18224#issuecomment-316957213
constructor (private injector: Injector) {}
intercept (req: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
if (this.authService === undefined) {
this.authService = this.injector.get(AuthService)
}
const authReq = this.cloneRequestWithAuth(req)
// Pass on the cloned request instead of the original request
// Catch 401 errors (refresh token expired)
return next.handle(authReq)
.catch(err => {
if (err.status === 401) {
return this.handleTokenExpired(req, next)
}
return Observable.throw(err)
})
}
private handleTokenExpired (req: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
return this.authService.refreshAccessToken()
.switchMap(() => {
const authReq = this.cloneRequestWithAuth(req)
return next.handle(authReq)
})
}
private cloneRequestWithAuth (req: HttpRequest<any>) {
const authHeaderValue = this.authService.getRequestHeaderValue()
if (authHeaderValue === null) return req
// Clone the request to add the new header
return req.clone({ headers: req.headers.set('Authorization', authHeaderValue) })
}
}
export const AUTH_INTERCEPTOR_PROVIDER = {
provide: HTTP_INTERCEPTORS,
useClass: AuthInterceptor,
multi: true
}

View File

@ -1 +1 @@
export * from './auth-http.service' export * from './auth-interceptor.service'

View File

@ -2,3 +2,4 @@ export * from './rest-data-source'
export * from './rest-extractor.service' export * from './rest-extractor.service'
export * from './rest-pagination' export * from './rest-pagination'
export * from './rest.service' export * from './rest.service'
export * from './rest-table'

View File

@ -1,68 +1,32 @@
import { Http, RequestOptionsArgs, URLSearchParams, Response } from '@angular/http' export class RestDataSource {
// protected addSortRequestOptions (requestOptions: RequestOptionsArgs) {
import { ServerDataSource } from 'ng2-smart-table' // const searchParams = requestOptions.params as URLSearchParams
//
export class RestDataSource extends ServerDataSource { // if (this.sortConf) {
private updateResponse: (input: any[]) => any[] // this.sortConf.forEach((fieldConf) => {
// const sortPrefix = fieldConf.direction === 'desc' ? '-' : ''
constructor (http: Http, endpoint: string, updateResponse?: (input: any[]) => any[]) { //
const options = { // searchParams.set(this.conf.sortFieldKey, sortPrefix + fieldConf.field)
endPoint: endpoint, // })
sortFieldKey: 'sort', // }
dataKey: 'data' //
} // return requestOptions
super(http, options) // }
//
if (updateResponse) { // protected addPagerRequestOptions (requestOptions: RequestOptionsArgs) {
this.updateResponse = updateResponse // const searchParams = requestOptions.params as URLSearchParams
} //
} // if (this.pagingConf && this.pagingConf['page'] && this.pagingConf['perPage']) {
// const perPage = this.pagingConf['perPage']
protected extractDataFromResponse (res: Response) { // const page = this.pagingConf['page']
const json = res.json() //
if (!json) return [] // const start = (page - 1) * perPage
let data = json.data // const count = perPage
//
if (this.updateResponse !== undefined) { // searchParams.set('start', start.toString())
data = this.updateResponse(data) // searchParams.set('count', count.toString())
} // }
//
return data // return requestOptions
} // }
protected extractTotalFromResponse (res: Response) {
const rawData = res.json()
return rawData ? parseInt(rawData.total, 10) : 0
}
protected addSortRequestOptions (requestOptions: RequestOptionsArgs) {
const searchParams = requestOptions.params as URLSearchParams
if (this.sortConf) {
this.sortConf.forEach((fieldConf) => {
const sortPrefix = fieldConf.direction === 'desc' ? '-' : ''
searchParams.set(this.conf.sortFieldKey, sortPrefix + fieldConf.field)
})
}
return requestOptions
}
protected addPagerRequestOptions (requestOptions: RequestOptionsArgs) {
const searchParams = requestOptions.params as URLSearchParams
if (this.pagingConf && this.pagingConf['page'] && this.pagingConf['perPage']) {
const perPage = this.pagingConf['perPage']
const page = this.pagingConf['page']
const start = (page - 1) * perPage
const count = perPage
searchParams.set('start', start.toString())
searchParams.set('count', count.toString())
}
return requestOptions
}
} }

View File

@ -1,52 +1,58 @@
import { Injectable } from '@angular/core' import { Injectable } from '@angular/core'
import { Response } from '@angular/http'
import { Observable } from 'rxjs/Observable' import { Observable } from 'rxjs/Observable'
import { HttpErrorResponse } from '@angular/common/http'
export interface ResultList { import { Utils } from '../utils'
data: any[] import { ResultList } from '../../../../../shared'
total: number
}
@Injectable() @Injectable()
export class RestExtractor { export class RestExtractor {
extractDataBool (res: Response) { extractDataBool () {
return true return true
} }
extractDataList (res: Response) { applyToResultListData <T> (result: ResultList<T>, fun: Function, additionalArgs?: any[]): ResultList<T> {
const body = res.json() const data: T[] = result.data
const newData: T[] = []
const ret: ResultList = { data.forEach(d => newData.push(fun.call(this, d, additionalArgs)))
data: body.data,
total: body.total
}
return ret return {
} total: result.total,
data: newData
extractDataGet (res: Response) { }
return res.json() }
}
convertResultListDateToHuman <T> (result: ResultList<T>, fieldsToConvert: string[] = [ 'createdAt' ]): ResultList<T> {
handleError (res: Response) { return this.applyToResultListData(result, this.convertDateToHuman, [ fieldsToConvert ])
let text = 'Server error: ' }
text += res.text()
let json = '' convertDateToHuman (target: object, fieldsToConvert: string[]) {
const source = {}
try { fieldsToConvert.forEach(field => {
json = res.json() source[field] = Utils.dateToHuman(target[field])
} catch (err) { })
console.error('Cannot get JSON from response.')
} return Object.assign(target, source)
}
const error = {
json, handleError (err: HttpErrorResponse) {
text let errorMessage
}
if (err.error instanceof Error) {
console.error(error) // A client-side or network error occurred. Handle it accordingly.
errorMessage = err.error.message
return Observable.throw(error) console.error('An error occurred:', errorMessage)
} else if (err.status !== undefined) {
// The backend returned an unsuccessful response code.
// The response body may contain clues as to what went wrong,
errorMessage = err.error
console.error(`Backend returned code ${err.status}, body was: ${errorMessage}`)
} else {
errorMessage = err
}
return Observable.throw(errorMessage)
} }
} }

View File

@ -1,5 +1,4 @@
export interface RestPagination { export interface RestPagination {
currentPage: number start: number
itemsPerPage: number count: number
totalItems: number
} }

View File

@ -0,0 +1,27 @@
import { LazyLoadEvent, SortMeta } from 'primeng/primeng'
import { RestPagination } from './rest-pagination'
export abstract class RestTable {
abstract totalRecords: number
abstract rowsPerPage: number
abstract sort: SortMeta
abstract pagination: RestPagination
protected abstract loadData (): void
loadLazy (event: LazyLoadEvent) {
this.sort = {
order: event.sortOrder,
field: event.sortField
}
this.pagination = {
start: event.first,
count: this.rowsPerPage
}
this.loadData()
}
}

View File

@ -1,27 +1,34 @@
import { Injectable } from '@angular/core' import { Injectable } from '@angular/core'
import { URLSearchParams } from '@angular/http' import { HttpParams } from '@angular/common/http'
import { SortMeta } from 'primeng/primeng'
import { RestPagination } from './rest-pagination' import { RestPagination } from './rest-pagination'
@Injectable() @Injectable()
export class RestService { export class RestService {
buildRestGetParams (pagination?: RestPagination, sort?: string) { addRestGetParams (params: HttpParams, pagination?: RestPagination, sort?: SortMeta | string) {
const params = new URLSearchParams() let newParams = params
if (pagination) { if (pagination !== undefined) {
const start: number = (pagination.currentPage - 1) * pagination.itemsPerPage newParams = newParams.set('start', pagination.start.toString())
const count: number = pagination.itemsPerPage .set('count', pagination.count.toString())
params.set('start', start.toString())
params.set('count', count.toString())
} }
if (sort) { if (sort !== undefined) {
params.set('sort', sort) let sortString = ''
if (typeof sort === 'string') {
sortString = sort
} else {
const sortPrefix = sort.order === 1 ? '' : '-'
sortString = sortPrefix + sort.field
} }
return params newParams = newParams.set('sort', sortString)
}
return newParams
} }
} }

View File

@ -1,6 +1,6 @@
import { NgModule } from '@angular/core' import { NgModule } from '@angular/core'
import { HttpClientModule } from '@angular/common/http'
import { CommonModule } from '@angular/common' import { CommonModule } from '@angular/common'
import { HttpModule } from '@angular/http'
import { FormsModule, ReactiveFormsModule } from '@angular/forms' import { FormsModule, ReactiveFormsModule } from '@angular/forms'
import { RouterModule } from '@angular/router' import { RouterModule } from '@angular/router'
@ -11,9 +11,9 @@ import { ProgressbarModule } from 'ngx-bootstrap/progressbar'
import { PaginationModule } from 'ngx-bootstrap/pagination' import { PaginationModule } from 'ngx-bootstrap/pagination'
import { ModalModule } from 'ngx-bootstrap/modal' import { ModalModule } from 'ngx-bootstrap/modal'
import { FileUploadModule } from 'ng2-file-upload/ng2-file-upload' import { FileUploadModule } from 'ng2-file-upload/ng2-file-upload'
import { Ng2SmartTableModule } from 'ng2-smart-table' import { DataTableModule, SharedModule as PrimeSharedModule } from 'primeng/primeng'
import { AUTH_HTTP_PROVIDERS } from './auth' import { AUTH_INTERCEPTOR_PROVIDER } from './auth'
import { RestExtractor, RestService } from './rest' import { RestExtractor, RestService } from './rest'
import { SearchComponent, SearchService } from './search' import { SearchComponent, SearchService } from './search'
import { UserService } from './users' import { UserService } from './users'
@ -24,8 +24,8 @@ import { VideoAbuseService } from './video-abuse'
CommonModule, CommonModule,
FormsModule, FormsModule,
ReactiveFormsModule, ReactiveFormsModule,
HttpModule,
RouterModule, RouterModule,
HttpClientModule,
BsDropdownModule.forRoot(), BsDropdownModule.forRoot(),
ModalModule.forRoot(), ModalModule.forRoot(),
@ -33,7 +33,9 @@ import { VideoAbuseService } from './video-abuse'
ProgressbarModule.forRoot(), ProgressbarModule.forRoot(),
FileUploadModule, FileUploadModule,
Ng2SmartTableModule
DataTableModule,
PrimeSharedModule
], ],
declarations: [ declarations: [
@ -46,15 +48,16 @@ import { VideoAbuseService } from './video-abuse'
CommonModule, CommonModule,
FormsModule, FormsModule,
ReactiveFormsModule, ReactiveFormsModule,
HttpModule,
RouterModule, RouterModule,
HttpClientModule,
BsDropdownModule, BsDropdownModule,
FileUploadModule, FileUploadModule,
ModalModule, ModalModule,
PaginationModule, PaginationModule,
ProgressbarModule, ProgressbarModule,
Ng2SmartTableModule, DataTableModule,
PrimeSharedModule,
BytesPipe, BytesPipe,
KeysPipe, KeysPipe,
@ -62,7 +65,7 @@ import { VideoAbuseService } from './video-abuse'
], ],
providers: [ providers: [
AUTH_HTTP_PROVIDERS, AUTH_INTERCEPTOR_PROVIDER,
RestExtractor, RestExtractor,
RestService, RestService,
SearchService, SearchService,

View File

@ -1,10 +1,8 @@
import { Injectable } from '@angular/core' import { Injectable } from '@angular/core'
import { Http } from '@angular/http' import { HttpClient } from '@angular/common/http'
import 'rxjs/add/operator/catch' import 'rxjs/add/operator/catch'
import 'rxjs/add/operator/map' import 'rxjs/add/operator/map'
import { AuthService } from '../../core'
import { AuthHttp } from '../auth'
import { RestExtractor } from '../rest' import { RestExtractor } from '../rest'
import { UserCreate, UserUpdateMe } from '../../../../../shared' import { UserCreate, UserUpdateMe } from '../../../../../shared'
@ -13,9 +11,7 @@ export class UserService {
static BASE_USERS_URL = API_URL + '/api/v1/users/' static BASE_USERS_URL = API_URL + '/api/v1/users/'
constructor ( constructor (
private http: Http, private authHttp: HttpClient,
private authHttp: AuthHttp,
private authService: AuthService,
private restExtractor: RestExtractor private restExtractor: RestExtractor
) {} ) {}
@ -34,7 +30,7 @@ export class UserService {
return this.authHttp.put(url, body) return this.authHttp.put(url, body)
.map(this.restExtractor.extractDataBool) .map(this.restExtractor.extractDataBool)
.catch((res) => this.restExtractor.handleError(res)) .catch(res => this.restExtractor.handleError(res))
} }
updateMyDetails (details: UserUpdateMe) { updateMyDetails (details: UserUpdateMe) {
@ -42,12 +38,12 @@ export class UserService {
return this.authHttp.put(url, details) return this.authHttp.put(url, details)
.map(this.restExtractor.extractDataBool) .map(this.restExtractor.extractDataBool)
.catch((res) => this.restExtractor.handleError(res)) .catch(res => this.restExtractor.handleError(res))
} }
signup (userCreate: UserCreate) { signup (userCreate: UserCreate) {
return this.http.post(UserService.BASE_USERS_URL + 'register', userCreate) return this.authHttp.post(UserService.BASE_USERS_URL + 'register', userCreate)
.map(this.restExtractor.extractDataBool) .map(this.restExtractor.extractDataBool)
.catch(this.restExtractor.handleError) .catch(res => this.restExtractor.handleError(res))
} }
} }

View File

@ -2,15 +2,7 @@ import { DatePipe } from '@angular/common'
export class Utils { export class Utils {
static dateToHuman (date: String) { static dateToHuman (date: Date) {
return new DatePipe('en').transform(date, 'medium') return new DatePipe('en').transform(date, 'medium')
} }
static getRowDeleteButton () {
return '<span class="glyphicon glyphicon-remove glyphicon-black"></span>'
}
static getRowEditButton () {
return '<span class="glyphicon glyphicon-pencil glyphicon-black"></span>'
}
} }

View File

@ -1,42 +1,53 @@
import { Injectable } from '@angular/core' import { Injectable } from '@angular/core'
import { Http } from '@angular/http' import { HttpClient, HttpParams } from '@angular/common/http'
import { Observable } from 'rxjs/Observable'
import 'rxjs/add/operator/catch' import 'rxjs/add/operator/catch'
import 'rxjs/add/operator/map' import 'rxjs/add/operator/map'
import { Observable } from 'rxjs/Observable'
import { SortMeta } from 'primeng/primeng'
import { AuthService } from '../core' import { AuthService } from '../core'
import { AuthHttp } from '../auth' import { RestExtractor, RestPagination, RestService } from '../rest'
import { RestDataSource, RestExtractor, ResultList } from '../rest' import { Utils } from '../utils'
import { VideoAbuse } from '../../../../../shared' import { ResultList, VideoAbuse } from '../../../../../shared'
@Injectable() @Injectable()
export class VideoAbuseService { export class VideoAbuseService {
private static BASE_VIDEO_ABUSE_URL = API_URL + '/api/v1/videos/' private static BASE_VIDEO_ABUSE_URL = API_URL + '/api/v1/videos/'
constructor ( constructor (
private authHttp: AuthHttp, private authHttp: HttpClient,
private restService: RestService,
private restExtractor: RestExtractor private restExtractor: RestExtractor
) {} ) {}
getDataSource () { getVideoAbuses (pagination: RestPagination, sort: SortMeta): Observable<ResultList<VideoAbuse>> {
return new RestDataSource(this.authHttp, VideoAbuseService.BASE_VIDEO_ABUSE_URL + 'abuse') const url = VideoAbuseService.BASE_VIDEO_ABUSE_URL + 'abuse'
let params = new HttpParams()
params = this.restService.addRestGetParams(params, pagination, sort)
return this.authHttp.get<ResultList<VideoAbuse>>(url, { params })
.map(res => this.restExtractor.convertResultListDateToHuman(res))
.map(res => this.restExtractor.applyToResultListData(res, this.formatVideoAbuse.bind(this)))
.catch(res => this.restExtractor.handleError(res))
} }
reportVideo (id: number, reason: string) { reportVideo (id: number, reason: string) {
const url = VideoAbuseService.BASE_VIDEO_ABUSE_URL + id + '/abuse'
const body = { const body = {
reason reason
} }
const url = VideoAbuseService.BASE_VIDEO_ABUSE_URL + id + '/abuse'
return this.authHttp.post(url, body) return this.authHttp.post(url, body)
.map(this.restExtractor.extractDataBool) .map(this.restExtractor.extractDataBool)
.catch((res) => this.restExtractor.handleError(res)) .catch(res => this.restExtractor.handleError(res))
} }
private extractVideoAbuses (result: ResultList) { private formatVideoAbuse (videoAbuse: VideoAbuse) {
const videoAbuses: VideoAbuse[] = result.data return Object.assign(videoAbuse, {
const totalVideoAbuses = result.total createdAt: Utils.dateToHuman(videoAbuse.createdAt)
})
}
return { videoAbuses, totalVideoAbuses }
}
} }

View File

@ -1,3 +1,4 @@
export * from './sort-field.type' export * from './sort-field.type'
export * from './video.model' export * from './video.model'
export * from './video.service' export * from './video.service'
export * from './video-pagination.model'

View File

@ -0,0 +1,5 @@
export interface VideoPagination {
currentPage: number
itemsPerPage: number
totalItems: number
}

View File

@ -46,7 +46,7 @@ export class Video implements VideoServerModel {
constructor (hash: { constructor (hash: {
author: string, author: string,
createdAt: string, createdAt: Date | string,
categoryLabel: string, categoryLabel: string,
category: number, category: number,
licenceLabel: string, licenceLabel: string,
@ -70,7 +70,7 @@ export class Video implements VideoServerModel {
files: VideoFile[] files: VideoFile[]
}) { }) {
this.author = hash.author this.author = hash.author
this.createdAt = new Date(hash.createdAt) this.createdAt = new Date(hash.createdAt.toString())
this.categoryLabel = hash.categoryLabel this.categoryLabel = hash.categoryLabel
this.category = hash.category this.category = hash.category
this.licenceLabel = hash.licenceLabel this.licenceLabel = hash.licenceLabel

View File

@ -1,27 +1,26 @@
import { Injectable } from '@angular/core' import { Injectable } from '@angular/core'
import { Http, Headers, RequestOptions } from '@angular/http'
import { Observable } from 'rxjs/Observable' import { Observable } from 'rxjs/Observable'
import 'rxjs/add/operator/catch' import 'rxjs/add/operator/catch'
import 'rxjs/add/operator/map' import 'rxjs/add/operator/map'
import { HttpClient, HttpParams } from '@angular/common/http'
import { Search } from '../../shared' import { Search } from '../../shared'
import { SortField } from './sort-field.type' import { SortField } from './sort-field.type'
import { AuthService } from '../../core'
import { import {
AuthHttp,
RestExtractor, RestExtractor,
RestPagination,
RestService, RestService,
ResultList,
UserService UserService
} from '../../shared' } from '../../shared'
import { Video } from './video.model' import { Video } from './video.model'
import { VideoPagination } from './video-pagination.model'
import { import {
UserVideoRate, UserVideoRate,
VideoRateType, VideoRateType,
VideoUpdate, VideoUpdate,
VideoAbuseCreate, VideoAbuseCreate,
UserVideoRateUpdate UserVideoRateUpdate,
Video as VideoServerModel,
ResultList
} from '../../../../../shared' } from '../../../../../shared'
@Injectable() @Injectable()
@ -33,9 +32,7 @@ export class VideoService {
videoLanguages: Array<{ id: number, label: string }> = [] videoLanguages: Array<{ id: number, label: string }> = []
constructor ( constructor (
private authService: AuthService, private authHttp: HttpClient,
private authHttp: AuthHttp,
private http: Http,
private restExtractor: RestExtractor, private restExtractor: RestExtractor,
private restService: RestService private restService: RestService
) {} ) {}
@ -52,9 +49,8 @@ export class VideoService {
return this.loadVideoAttributeEnum('languages', this.videoLanguages) return this.loadVideoAttributeEnum('languages', this.videoLanguages)
} }
getVideo (uuid: string): Observable<Video> { getVideo (uuid: string) {
return this.http.get(VideoService.BASE_VIDEO_URL + uuid) return this.authHttp.get<VideoServerModel>(VideoService.BASE_VIDEO_URL + uuid)
.map(this.restExtractor.extractDataGet)
.map(videoHash => new Video(videoHash)) .map(videoHash => new Video(videoHash))
.catch((res) => this.restExtractor.handleError(res)) .catch((res) => this.restExtractor.handleError(res))
} }
@ -72,19 +68,33 @@ export class VideoService {
nsfw: video.nsfw nsfw: video.nsfw
} }
const headers = new Headers({ 'Content-Type': 'application/json' }) return this.authHttp.put(`${VideoService.BASE_VIDEO_URL}/${video.id}`, body)
const options = new RequestOptions({ headers: headers })
return this.authHttp.put(`${VideoService.BASE_VIDEO_URL}/${video.id}`, body, options)
.map(this.restExtractor.extractDataBool) .map(this.restExtractor.extractDataBool)
.catch(this.restExtractor.handleError) .catch(this.restExtractor.handleError)
} }
getVideos (pagination: RestPagination, sort: SortField) { getVideos (videoPagination: VideoPagination, sort: SortField) {
const params = this.restService.buildRestGetParams(pagination, sort) const pagination = this.videoPaginationToRestPagination(videoPagination)
return this.http.get(VideoService.BASE_VIDEO_URL, { search: params }) let params = new HttpParams()
.map(res => res.json()) params = this.restService.addRestGetParams(params, pagination, sort)
return this.authHttp.get(VideoService.BASE_VIDEO_URL, { params })
.map(this.extractVideos)
.catch((res) => this.restExtractor.handleError(res))
}
searchVideos (search: Search, videoPagination: VideoPagination, sort: SortField) {
const url = VideoService.BASE_VIDEO_URL + 'search/' + encodeURIComponent(search.value)
const pagination = this.videoPaginationToRestPagination(videoPagination)
let params = new HttpParams()
params = this.restService.addRestGetParams(params, pagination, sort)
if (search.field) params.set('field', search.field)
return this.authHttp.get<ResultList<VideoServerModel>>(url, { params })
.map(this.extractVideos) .map(this.extractVideos)
.catch((res) => this.restExtractor.handleError(res)) .catch((res) => this.restExtractor.handleError(res))
} }
@ -95,17 +105,6 @@ export class VideoService {
.catch((res) => this.restExtractor.handleError(res)) .catch((res) => this.restExtractor.handleError(res))
} }
searchVideos (search: Search, pagination: RestPagination, sort: SortField) {
const params = this.restService.buildRestGetParams(pagination, sort)
if (search.field) params.set('field', search.field)
return this.http.get(VideoService.BASE_VIDEO_URL + 'search/' + encodeURIComponent(search.value), { search: params })
.map(this.restExtractor.extractDataList)
.map(this.extractVideos)
.catch((res) => this.restExtractor.handleError(res))
}
reportVideo (id: number, reason: string) { reportVideo (id: number, reason: string) {
const url = VideoService.BASE_VIDEO_URL + id + '/abuse' const url = VideoService.BASE_VIDEO_URL + id + '/abuse'
const body: VideoAbuseCreate = { const body: VideoAbuseCreate = {
@ -114,7 +113,7 @@ export class VideoService {
return this.authHttp.post(url, body) return this.authHttp.post(url, body)
.map(this.restExtractor.extractDataBool) .map(this.restExtractor.extractDataBool)
.catch((res) => this.restExtractor.handleError(res)) .catch(res => this.restExtractor.handleError(res))
} }
setVideoLike (id: number) { setVideoLike (id: number) {
@ -129,14 +128,20 @@ export class VideoService {
const url = UserService.BASE_USERS_URL + '/me/videos/' + id + '/rating' const url = UserService.BASE_USERS_URL + '/me/videos/' + id + '/rating'
return this.authHttp.get(url) return this.authHttp.get(url)
.map(this.restExtractor.extractDataGet) .catch(res => this.restExtractor.handleError(res))
.catch((res) => this.restExtractor.handleError(res))
} }
blacklistVideo (id: number) { blacklistVideo (id: number) {
return this.authHttp.post(VideoService.BASE_VIDEO_URL + id + '/blacklist', {}) return this.authHttp.post(VideoService.BASE_VIDEO_URL + id + '/blacklist', {})
.map(this.restExtractor.extractDataBool) .map(this.restExtractor.extractDataBool)
.catch((res) => this.restExtractor.handleError(res)) .catch(res => this.restExtractor.handleError(res))
}
private videoPaginationToRestPagination (videoPagination: VideoPagination) {
const start: number = (videoPagination.currentPage - 1) * videoPagination.itemsPerPage
const count: number = videoPagination.itemsPerPage
return { start, count }
} }
private setVideoRate (id: number, rateType: VideoRateType) { private setVideoRate (id: number, rateType: VideoRateType) {
@ -147,13 +152,14 @@ export class VideoService {
return this.authHttp.put(url, body) return this.authHttp.put(url, body)
.map(this.restExtractor.extractDataBool) .map(this.restExtractor.extractDataBool)
.catch((res) => this.restExtractor.handleError(res)) .catch(res => this.restExtractor.handleError(res))
} }
private extractVideos (result: ResultList) { private extractVideos (result: ResultList<VideoServerModel>) {
const videosJson = result.data const videosJson = result.data
const totalVideos = result.total const totalVideos = result.total
const videos = [] const videos = []
for (const videoJson of videosJson) { for (const videoJson of videosJson) {
videos.push(new Video(videoJson)) videos.push(new Video(videoJson))
} }
@ -162,8 +168,7 @@ export class VideoService {
} }
private loadVideoAttributeEnum (attributeName: 'categories' | 'licences' | 'languages', hashToPopulate: { id: number, label: string }[]) { private loadVideoAttributeEnum (attributeName: 'categories' | 'licences' | 'languages', hashToPopulate: { id: number, label: string }[]) {
return this.http.get(VideoService.BASE_VIDEO_URL + attributeName) return this.authHttp.get(VideoService.BASE_VIDEO_URL + attributeName)
.map(this.restExtractor.extractDataGet)
.subscribe(data => { .subscribe(data => {
Object.keys(data).forEach(dataKey => { Object.keys(data).forEach(dataKey => {
hashToPopulate.push({ hashToPopulate.push({

View File

@ -8,11 +8,12 @@ import { NotificationsService } from 'angular2-notifications'
import { import {
SortField, SortField,
Video, Video,
VideoService VideoService,
VideoPagination
} from '../shared' } from '../shared'
import { AuthService, AuthUser } from '../../core' import { AuthService, AuthUser } from '../../core'
import { RestPagination, Search, SearchField } from '../../shared' import { Search, SearchField, SearchService } from '../../shared'
import { SearchService } from '../../shared' import { } from '../../shared'
@Component({ @Component({
selector: 'my-videos-list', selector: 'my-videos-list',
@ -21,7 +22,7 @@ import { SearchService } from '../../shared'
}) })
export class VideoListComponent implements OnInit, OnDestroy { export class VideoListComponent implements OnInit, OnDestroy {
loading: BehaviorSubject<boolean> = new BehaviorSubject(false) loading: BehaviorSubject<boolean> = new BehaviorSubject(false)
pagination: RestPagination = { pagination: VideoPagination = {
currentPage: 1, currentPage: 1,
itemsPerPage: 25, itemsPerPage: 25,
totalItems: null totalItems: null

View File

@ -1,4 +1,6 @@
@import '../../node_modules/video.js/dist/video-js.css'; @import '~primeng/resources/themes/bootstrap/theme.css';
@import '~primeng/resources/primeng.css';
@import '~video.js/dist/video-js.css';
@import './video-js-custom.scss'; @import './video-js-custom.scss';
[hidden] { [hidden] {
@ -45,23 +47,13 @@ input.readonly {
} }
} }
/* some fixes for ng2-smart-table */ /* ngprime data table customizations */
ng2-smart-table { p-datatable {
thead tr { .action-cell {
border-top: 1px solid rgb(233, 235, 236) text-align: center;
}
td, th {
padding: 8px !important;
color: #333333 !important;
font-size: 14px !important;
}
.ng2-smart-pagination-nav .page-link {
font-size: 11px !important;
}
.glyphicon { .glyphicon {
font-family: 'Glyphicons Halflings' !important; cursor: pointer;
}
} }
} }

View File

@ -4436,10 +4436,6 @@ ng-router-loader@^2.0.0:
loader-utils "^0.2.16" loader-utils "^0.2.16"
recast "^0.11.20" recast "^0.11.20"
ng2-completer@^1.2.2:
version "1.6.1"
resolved "https://registry.yarnpkg.com/ng2-completer/-/ng2-completer-1.6.1.tgz#62bad1a0a1d99c62b15f6723911ee0a3a00c91bb"
ng2-file-upload@^1.1.4-2: ng2-file-upload@^1.1.4-2:
version "1.2.1" version "1.2.1"
resolved "https://registry.yarnpkg.com/ng2-file-upload/-/ng2-file-upload-1.2.1.tgz#5563c5dfd6f43fbfbe815c206e343464a0a6a197" resolved "https://registry.yarnpkg.com/ng2-file-upload/-/ng2-file-upload-1.2.1.tgz#5563c5dfd6f43fbfbe815c206e343464a0a6a197"
@ -4448,13 +4444,6 @@ ng2-material-dropdown@0.7.10:
version "0.7.10" version "0.7.10"
resolved "https://registry.yarnpkg.com/ng2-material-dropdown/-/ng2-material-dropdown-0.7.10.tgz#093471f2a9cadd726cbcb120b0ad7818a54fa5ed" resolved "https://registry.yarnpkg.com/ng2-material-dropdown/-/ng2-material-dropdown-0.7.10.tgz#093471f2a9cadd726cbcb120b0ad7818a54fa5ed"
ng2-smart-table@1.2.1:
version "1.2.1"
resolved "https://registry.yarnpkg.com/ng2-smart-table/-/ng2-smart-table-1.2.1.tgz#b25102c1a8b0588c508cf913c539ddf0f0b3341d"
dependencies:
lodash "^4.17.4"
ng2-completer "^1.2.2"
ngc-webpack@3.2.2: ngc-webpack@3.2.2:
version "3.2.2" version "3.2.2"
resolved "https://registry.yarnpkg.com/ngc-webpack/-/ngc-webpack-3.2.2.tgz#1905c40e3c7d30c86fe029c7a7fda71cb4dc59df" resolved "https://registry.yarnpkg.com/ngc-webpack/-/ngc-webpack-3.2.2.tgz#1905c40e3c7d30c86fe029c7a7fda71cb4dc59df"
@ -5340,6 +5329,10 @@ pretty-error@^2.0.2:
renderkid "^2.0.1" renderkid "^2.0.1"
utila "~0.4" utila "~0.4"
primeng@^4.2.0:
version "4.2.0"
resolved "https://registry.yarnpkg.com/primeng/-/primeng-4.2.0.tgz#49c8c99de26d254f41d3fbb8759227fe1d269772"
private@^0.1.6, private@^0.1.7, private@~0.1.5: private@^0.1.6, private@^0.1.7, private@~0.1.5:
version "0.1.7" version "0.1.7"
resolved "https://registry.yarnpkg.com/private/-/private-0.1.7.tgz#68ce5e8a1ef0a23bb570cc28537b5332aba63ef1" resolved "https://registry.yarnpkg.com/private/-/private-0.1.7.tgz#68ce5e8a1ef0a23bb570cc28537b5332aba63ef1"

View File

@ -190,6 +190,7 @@ function quitFriends () {
.catch(err => { .catch(err => {
logger.error('Some errors while quitting friends.', err) logger.error('Some errors while quitting friends.', err)
// Don't stop the process // Don't stop the process
return pods
}) })
}) })
.then(pods => { .then(pods => {

View File

@ -1,5 +1,7 @@
export * from './user.model' export * from './user.model'
export * from './user-create.model' export * from './user-create.model'
export * from './user-login.model'
export * from './user-refresh-token.model'
export * from './user-update.model' export * from './user-update.model'
export * from './user-update-me.model' export * from './user-update-me.model'
export * from './user-role.type' export * from './user-role.type'

View File

@ -0,0 +1,5 @@
export interface UserLogin {
access_token: string
refresh_token: string
token_type: string
}

View File

@ -0,0 +1,4 @@
export interface UserRefreshToken {
access_token: string
refresh_token: string
}

View File

@ -9,8 +9,8 @@ export interface Video {
id: number id: number
uuid: string uuid: string
author: string author: string
createdAt: Date createdAt: Date | string
updatedAt: Date updatedAt: Date | string
categoryLabel: string categoryLabel: string
category: number category: number
licenceLabel: string licenceLabel: string