Compare commits

..

2 Commits

Author SHA1 Message Date
William Nolin deee043c66 #30 Start group token dialog
continuous-integration/drone/push Build is passing Details
2022-05-13 22:53:26 -04:00
William Nolin 3ff897fc85 #30 Update login to be compatible with backend 2022-05-12 07:45:18 -04:00
20 changed files with 254 additions and 184 deletions

View File

@ -5,18 +5,23 @@ import {SharedModule} from '../shared/shared.module'
import {Login, Logout} from './accounts'
import {CreInputsModule} from '../shared/components/inputs/inputs.module'
import {CreButtonsModule} from '../shared/components/buttons/buttons.module'
import {GroupTokenAdd} from "./group-tokens";
@NgModule({
declarations: [
Login,
Logout
Logout,
GroupTokenAdd
],
exports: [
GroupTokenAdd
],
imports: [
SharedModule,
AccountsRoutingModule,
CreInputsModule,
CreButtonsModule,
CreButtonsModule
]
})
export class AccountsModule {

View File

@ -47,9 +47,9 @@ export class Login extends ErrorHandlingComponent {
}
submit() {
this.subscribe(
this.accountService.login(this.userIdControl.value, this.passwordControl.value),
next => {}
this.subscribeAndNavigate(
this.accountService.loginAsUser(this.userIdControl.value, this.passwordControl.value),
'/color/list'
)
}
@ -79,13 +79,10 @@ export class Logout extends SubscribingComponent {
}
ngOnInit(): void {
if (!this.appState.isAuthenticated) {
this.urlUtils.navigateTo('/account/login')
if (this.appState.isAuthenticated) {
this.accountService.logout()
}
this.subscribeAndNavigate(
this.accountService.logout(),
'/account/login'
)
this.urlUtils.navigateTo('/account/login')
}
}

View File

@ -0,0 +1,8 @@
<cre-prompt-dialog
title="Définir le groupe par défaut de cet ordinateur">
<cre-dialog-body>
<div>
<cre-input [control]="controls.name" label="Name" icon="form-textbox"></cre-input>
</div>
</cre-dialog-body>
</cre-prompt-dialog>

View File

@ -0,0 +1,18 @@
import {Component, ViewChild} from "@angular/core";
import {CrePromptDialog} from "../shared/components/dialogs/dialogs";
import {Group} from "../shared/model/account.model";
import {FormControl, Validators} from "@angular/forms";
@Component({
selector: 'cre-group-token-add',
templateUrl: 'group-token-add.html'
})
export class GroupTokenAdd {
@ViewChild(CrePromptDialog) dialog: CrePromptDialog
controls = {name: new FormControl(null, Validators.required)}
show(group: Group) {
this.dialog.show()
}
}

View File

@ -2,12 +2,11 @@ import {Injectable, OnDestroy} from '@angular/core'
import {Observable, Subject} from 'rxjs'
import {take, takeUntil} from 'rxjs/operators'
import {AppState} from '../../shared/app-state'
import {HttpClient, HttpResponse} from '@angular/common/http'
import {HttpClient} from '@angular/common/http'
import {environment} from '../../../../environments/environment'
import {ApiService} from '../../shared/service/api.service'
import {Permission, AccountModel, LoginDto} from '../../shared/model/account.model'
import {LoginDto, Permission} from '../../shared/model/account.model'
import {ErrorService} from '../../shared/service/error.service'
import {AlertService} from '../../shared/service/alert.service'
import {JwtService} from "./jwt.service";
@Injectable({
@ -21,8 +20,7 @@ export class AccountService implements OnDestroy {
private api: ApiService,
private appState: AppState,
private jwtService: JwtService,
private errorService: ErrorService,
private alertService: AlertService
private errorService: ErrorService
) {
}
@ -31,112 +29,58 @@ export class AccountService implements OnDestroy {
this.destroy$.complete()
}
login(userId: number, password: string): Observable<LoginDto> {
const login$ = this.http.post<LoginDto>(
`${environment.apiUrl}/account/login`,
{id: userId, password},
{withCredentials: true})
loginAsGroupIfNotAuthenticated() {
if (this.appState.isAuthenticated) return
this.loginAsGroup()
}
loginAsUser(id: number, password: string): Observable<LoginDto> {
return this.login(false, {id, password}, error => {
if (error.status !== 403) return false
this.errorService.handleError({status: error.status, type: 'invalid-credentials', obj: error})
return true
})
}
loginAsGroup(): Observable<LoginDto> {
return this.login(true, {},
error => error.status === 403) // There is no group token, so do nothing
}
private login(isGroup: boolean, body: LoginBody, errorConsumer: (any) => boolean): Observable<LoginDto> {
let url = `${environment.apiUrl}/account/login`
if (isGroup) {
url += '/group'
}
const login$ = this.http.post<LoginDto>(url, body, {withCredentials: true})
.pipe(take(1), takeUntil(this.destroy$))
login$.subscribe({
next: result => {
console.log(result)
},
error: err => this.errorService.handleError(err)
next: result => this.appState.authenticateUser(result),
error: error => {
if (errorConsumer(error)) return;
this.errorService.handleErrorResponse(error)
}
})
return login$
}
checkAuthenticationStatus() {
if (!this.appState.isAuthenticated) {
// Try to get current default group user
// this.http.get<AccountModel>(`${environment.apiUrl}/user/group/currentuser`, {withCredentials: true})
// .pipe(
// take(1),
// takeUntil(this.destroy$),
// ).subscribe(
// {
// next: user => this.appState.authenticateGroupUser(user),
// error: err => {
// if (err.status === 404 || err.status === 403) {
// console.warn('No default user is defined on this computer')
// } else {
// this.errorService.handleError(err)
// }
// }
// })
}
}
// login(userId: number, password: string): Observable<any> {
// const subject = new Subject<void>()
//
// this.http.post<any>(`${environment.apiUrl}/login`, {id: userId, password}, {
// withCredentials: true,
// observe: 'response' as 'body'
// }).pipe(
// take(1),
// takeUntil(this.destroy$)
// ).subscribe({
// next: (response: HttpResponse<void>) => {
// this.loginUser(response)
//
// subject.next()
// subject.complete()
// },
// error: error => {
// if (error.status === 403) {
// this.alertService.pushError('Les identifiants entrés sont invalides')
// } else {
// this.errorService.handleError(error)
// }
//
// subject.next()
// subject.complete()
// }
// })
//
// return subject
// }
private loginUser(response: HttpResponse<void>) {
const authorization = response.headers.get("Authorization")
const user = this.jwtService.parseUser(authorization)
this.appState.authenticateUser(user)
}
logout(): Observable<void> {
const subject = new Subject<void>()
this.api.get<void>('/logout').pipe(
take(1),
takeUntil(this.destroy$)
).subscribe({
next: () => {
this.logoutUser()
subject.next()
subject.complete()
},
error: error => {
this.errorService.handleError(error)
subject.next()
subject.complete()
}
})
return subject
}
private logoutUser() {
logout() {
this.appState.resetAuthenticatedUser()
this.checkAuthenticationStatus()
this.loginAsGroupIfNotAuthenticated()
}
hasPermission(permission: Permission): boolean {
return this.appState.authenticatedUser && this.appState.authenticatedUser.permissions.indexOf(permission) >= 0
}
}
interface LoginBody {
id?: number
password?: string
}

View File

@ -0,0 +1,26 @@
import {Injectable} from "@angular/core";
import {ApiService} from "../../shared/service/api.service";
import {Observable} from "rxjs";
import {GroupToken} from "../../shared/model/account.model";
@Injectable({
providedIn: "root"
})
export class GroupTokenService {
constructor(
private api: ApiService
) {
}
get all(): Observable<GroupToken[]> {
return this.api.get<GroupToken[]>('/account/group/token')
}
get current(): Observable<GroupToken | null> {
return this.api.get<GroupToken | null>('/account/group/token/current')
}
save(name: string): Observable<GroupToken> {
return this.api.post<GroupToken>('/account/group/token')
}
}

View File

@ -5,13 +5,21 @@ import {ListComponent} from './pages/list/list.component';
import {SharedModule} from "../shared/shared.module";
import {AddComponent} from './pages/add/add.component';
import {EditComponent} from './pages/edit/edit.component';
import {AccountsModule} from "../accounts/accounts.module";
import {CreActionBarModule} from "../shared/components/action-bar/action-bar.module";
import {CreButtonsModule} from "../shared/components/buttons/buttons.module";
import {CreTablesModule} from "../shared/components/tables/tables.module";
@NgModule({
declarations: [ListComponent, AddComponent, EditComponent],
imports: [
GroupRoutingModule,
SharedModule
SharedModule,
AccountsModule,
CreActionBarModule,
CreButtonsModule,
CreTablesModule
]
})
export class GroupModule { }

View File

@ -1,13 +1,42 @@
<cre-entity-list
addLink="/admin/group/add"
addPermission="EDIT_USERS"
[entities$]="groups$"
[columns]="columns"
[buttons]="buttons"
[expandable]="true"
[rowDetailsTemplate]="groupDetailsTemplate">
</cre-entity-list>
<cre-action-bar [reverse]="true">
<cre-action-group>
<cre-accent-button *ngIf="hasEditPermission" routerLink="/admin/group/add">Ajouter</cre-accent-button>
</cre-action-group>
</cre-action-bar>
<ng-template #groupDetailsTemplate let-group="entity">
<cre-permissions-list [group]="group"></cre-permissions-list>
</ng-template>
<cre-warning-alert *ngIf="groupsEmpty">
<p>Il n'y a actuellement aucun groupe enregistré dans le système.</p>
<p *ngIf="hasEditPermission">Vous pouvez en créer un <b><a routerLink="/admin/group/add">ici</a></b>.</p>
</cre-warning-alert>
<cre-table *ngIf="!groupsEmpty" class="mx-auto" [data]="groups$ | async" [columns]="columns">
<ng-container matColumnDef="name">
<th mat-header-cell *matHeaderCellDef>Nom</th>
<td mat-cell *matCellDef="let group">{{group.name}}</td>
</ng-container>
<ng-container matColumnDef="permissionCount">
<th mat-header-cell *matHeaderCellDef>Nombre de permissions</th>
<td mat-cell *matCellDef="let group">{{group.permissions.length}}</td>
</ng-container>
<ng-container matColumnDef="setAsDefaultButton">
<th mat-header-cell *matHeaderCellDef></th>
<td mat-cell [class.disabled]="!hasAdminPermission" *matCellDef="let group; let i = index">
<cre-accent-button [creInteractiveCell]="i" (click)="setDefaultGroup(group)" >
Définir par défaut
</cre-accent-button>
</td>
</ng-container>
<ng-container matColumnDef="editButton">
<th mat-header-cell *matHeaderCellDef></th>
<td mat-cell [class.disabled]="!hasEditPermission" *matCellDef="let group; let i = index">
<cre-accent-button [creInteractiveCell]="i" routerLink="/admin/group/edit/{{group.id}}">
Modifier
</cre-accent-button>
</td>
</ng-container>
</cre-table>
<cre-group-token-add></cre-group-token-add>

View File

@ -1,4 +1,4 @@
import {Component} from '@angular/core'
import {Component, ViewChild} from '@angular/core'
import {GroupService} from '../../services/group.service'
import {Group, Permission} from '../../../shared/model/account.model'
import {AccountService} from '../../../accounts/services/account.service'
@ -7,6 +7,8 @@ import {ActivatedRoute, Router} from '@angular/router'
import {ErrorHandler, ErrorService} from '../../../shared/service/error.service'
import {AlertService} from '../../../shared/service/alert.service'
import {AppState} from '../../../shared/app-state'
import {GroupTokenService} from "../../../accounts/services/group-token.service";
import {GroupTokenAdd} from "../../../accounts/group-tokens";
@Component({
selector: 'cre-groups',
@ -16,27 +18,34 @@ import {AppState} from '../../../shared/app-state'
export class ListComponent extends ErrorHandlingComponent {
groups$ = this.groupService.all
defaultGroup: Group = null
columns = [
{def: 'name', title: 'Nom', valueFn: g => g.name},
{def: 'permissionCount', title: 'Nombre de permissions', valueFn: g => g.permissions.length}
]
buttons = [{
text: 'Définir par défaut',
clickFn: group => this.setDefaultGroup(group),
disabledFn: group => this.isDefaultGroup(group)
}, {
text: 'Modifier',
linkFn: group => `/admin/group/edit/${group.id}`,
permission: Permission.EDIT_USERS
}]
// columns = [
// {def: 'name', title: 'Nom', valueFn: g => g.name},
// {def: 'permissionCount', title: 'Nombre de permissions', valueFn: g => g.permissions.length}
// ]
// buttons = [{
// text: 'Définir par défaut',
// clickFn: group => this.setDefaultGroup(group),
// disabledFn: group => this.isDefaultGroup(group)
// }, {
// text: 'Modifier',
// linkFn: group => `/admin/group/edit/${group.id}`,
// permission: Permission.EDIT_USERS
// }]
groupsEmpty = false
columns = ['name', 'permissionCount', 'setAsDefaultButton', 'editButton']
errorHandlers: ErrorHandler[] = [{
filter: error => error.type === 'nodefaultgroup',
consumer: () => this.alertService.pushWarning('Aucun groupe par défaut n\'a été défini sur cet ordinateur')
}]
@ViewChild(GroupTokenAdd) groupTokenDialog: GroupTokenAdd
constructor(
private groupService: GroupService,
private groupTokenService: GroupTokenService,
private accountService: AccountService,
private alertService: AlertService,
private appState: AppState,
@ -49,22 +58,30 @@ export class ListComponent extends ErrorHandlingComponent {
}
ngOnInit(): void {
// this.subscribe(
// this.groupService.defaultGroup,
// group => this.defaultGroup = group,
// true
// )
this.subscribe(
this.groupService.defaultGroup,
group => this.defaultGroup = group,
true
this.groupTokenService.current,
token => console.info(token)
)
}
setDefaultGroup(group: Group) {
this.subscribe(
this.groupService.setDefaultGroup(group),
() => this.defaultGroup = group,
true
)
this.groupTokenDialog.show(group)
}
isDefaultGroup(group: Group): boolean {
return this.defaultGroup && this.defaultGroup.id == group.id
}
get hasEditPermission(): boolean {
return this.accountService.hasPermission(Permission.EDIT_USERS)
}
get hasAdminPermission(): boolean {
return this.accountService.hasPermission(Permission.ADMIN)
}
}

View File

@ -14,7 +14,7 @@ export class GroupService {
}
get all(): Observable<Group[]> {
return this.api.get<Group[]>('/user/group')
return this.api.get<Group[]>('/account/group')
}
get allWithDefault(): Observable<Group[]> {
@ -27,40 +27,40 @@ export class GroupService {
}
getById(id: number): Observable<Group> {
return this.api.get<Group>(`/user/group/${id}`)
return this.api.get<Group>(`/account/group/${id}`)
}
get defaultGroup(): Observable<Group> {
return this.api.get<Group>('/user/group/default')
return this.api.get<Group>('/account/group/default')
}
setDefaultGroup(value: Group): Observable<void> {
return this.api.post<void>(`/user/group/default/${value.id}`, {})
return this.api.post<void>(`/account/group/default/${value.id}`, {})
}
getUsersForGroup(id: number): Observable<AccountModel[]> {
return this.api.get<AccountModel[]>(`/user/group/${id}/users`)
return this.api.get<AccountModel[]>(`/account/group/${id}/users`)
}
addUserToGroup(id: number, user: AccountModel): Observable<void> {
return this.api.put<void>(`/user/group/${id}/${user.id}`)
return this.api.put<void>(`/account/group/${id}/${user.id}`)
}
removeUserFromGroup(user: AccountModel): Observable<void> {
return this.api.delete<void>(`/user/group/${user.group.id}/${user.id}`)
return this.api.delete<void>(`/account/group/${user.group.id}/${user.id}`)
}
save(name: string, permissions: Permission[]): Observable<Group> {
const group = {name, permissions}
return this.api.post<Group>('/user/group', group)
return this.api.post<Group>('/account/group', group)
}
update(id: number, name: string, permissions: Permission[]): Observable<Group> {
const group = {id, name, permissions}
return this.api.put<Group>('/user/group', group)
return this.api.put<Group>('/account/group', group)
}
delete(id: number): Observable<Group> {
return this.api.delete<Group>(`/user/group/${id}`)
return this.api.delete<Group>(`/account/group/${id}`)
}
}

View File

@ -175,7 +175,7 @@ export class CreRecipeExplore extends ErrorHandlingComponent {
}
get loggedInUserGroupId(): number {
return this.appState.authenticatedUser.group?.id
return this.appState.authenticatedUser.groupId
}
get selectedGroupNote(): string {

View File

@ -202,7 +202,7 @@ export class RecipeEdit extends ErrorHandlingComponent {
}
get loggedInUserGroupId(): number {
return this.appState.authenticatedUser.group?.id
return this.appState.authenticatedUser.groupId
}
private stepsPositionsAreValid(steps: Map<number, RecipeStep[]>): boolean {

View File

@ -1,5 +1,5 @@
import {Injectable} from '@angular/core'
import {AccountModel, LoginDto} from './model/account.model'
import {LoginDto} from './model/account.model'
import {Subject} from 'rxjs'
import {Title} from '@angular/platform-browser'
@ -73,7 +73,7 @@ export class AppState {
private set authenticatedUser(value: LoginDto) {
if (value === null) {
// sessionStorage.removeItem(this.KEY_LOGGED_IN_USER)
sessionStorage.removeItem(this.KEY_LOGGED_IN_USER)
} else {
sessionStorage.setItem(this.KEY_LOGGED_IN_USER, JSON.stringify(value))
}

View File

@ -35,7 +35,7 @@ export class HeaderComponent extends SubscribingComponent {
ngOnInit(): void {
super.ngOnInit()
this.accountService.checkAuthenticationStatus()
this.accountService.loginAsGroupIfNotAuthenticated()
// Gets the current route
this.subscribe(
@ -58,10 +58,8 @@ export class HeaderComponent extends SubscribingComponent {
}
ngOnDestroy(): void {
this.subscribe(
this.accountService.logout(),
() => console.info('Successfully logged out')
)
this.accountService.logout()
console.info('Successfully logged out')
super.ngOnDestroy()
}

View File

@ -1,7 +1,7 @@
import {Component, Input, OnDestroy, OnInit} from '@angular/core';
import {AccountModel, Permission} from "../../model/account.model";
import {LoginDto, Permission} from "../../model/account.model";
import {AccountService} from "../../../accounts/services/account.service";
import {ActivatedRoute, Router} from "@angular/router";
import {Router} from "@angular/router";
import {takeUntil} from "rxjs/operators";
import {AppState} from "../../app-state";
import {Subject} from "rxjs";
@ -54,7 +54,7 @@ export class NavComponent implements OnInit, OnDestroy {
return this._activeLink
}
private updateEnabledLinks(user: AccountModel) {
private updateEnabledLinks(user: LoginDto) {
this.links.forEach(l => {
if (l.permission) {
l.enabled = user && user.permissions.indexOf(l.permission) >= 0;

View File

@ -35,7 +35,7 @@ export abstract class SubscribingComponent implements OnInit, OnDestroy {
this.hideLoadingWheel(showWheel)
},
error: err => {
this.errorService.handleError(err)
this.errorService.handleErrorResponse(err)
this.hideLoadingWheel(showWheel)
}
}))
@ -61,7 +61,7 @@ export abstract class SubscribingComponent implements OnInit, OnDestroy {
},
error: err => {
this.hideLoadingWheel(showWheel)
this.errorService.handleError(err)
this.errorService.handleErrorResponse(err)
}
}))
}

View File

@ -3,7 +3,7 @@
<labeled-icon
*ngIf="authenticated"
icon="account"
label="{{user.firstName}} {{user.lastName}}">
label="{{user.fullName}}">
</labeled-icon>
<div class="d-flex flex-row">
<labeled-icon
@ -15,7 +15,7 @@
*ngIf="userInGroup"
class="user-info-group"
icon="account-multiple"
[label]="user.group.name">
[label]="user.groupName">
</labeled-icon>
</div>
</div>

View File

@ -1,6 +1,6 @@
import {Component, OnDestroy, OnInit} from '@angular/core'
import {AppState} from '../../app-state'
import {AccountModel} from '../../model/account.model'
import {LoginDto} from '../../model/account.model'
import {Subject} from 'rxjs'
import {takeUntil} from 'rxjs/operators'
import {UrlUtils} from '../../utils/url.utils'
@ -13,7 +13,7 @@ import {ActivatedRoute, Router} from '@angular/router'
})
export class UserMenuComponent implements OnInit, OnDestroy {
authenticated = false
user: AccountModel = null
user: LoginDto = null
userInGroup = false
menuEnabled = false
@ -52,11 +52,11 @@ export class UserMenuComponent implements OnInit, OnDestroy {
this.menuEnabled = false
}
private authenticationState(authenticated: boolean, user: AccountModel) {
private authenticationState(authenticated: boolean, user: LoginDto) {
this.authenticated = authenticated
this.user = user
if (this.user != null) {
this.userInGroup = this.user.group != null
this.userInGroup = this.user.groupId != null
}
}
}

View File

@ -28,6 +28,13 @@ export class Group {
}
}
export interface GroupToken {
id: string,
name: string,
enabled: boolean,
groupId: Group
}
export enum Permission {
VIEW_RECIPES = 'VIEW_RECIPES',
VIEW_USERS = 'VIEW_USERS',

View File

@ -38,19 +38,22 @@ export class ErrorService {
) {
}
handleError(response: any) {
let matchingModels
handleErrorResponse(response: any) {
if (isServerOfflineError(response)) {
this.appState.isServerOnline = false
return
}
const error = response.error
if (!error || !error.type) {
let error = response.error
if (!isHandledError(error)) {
return
}
this.handleError({status: error.status, type: error.type, obj: error})
}
handleError(error: HandledError) {
let matchingModels
if (this.activeHandler) {
matchingModels = this.activeHandler.errorHandlers.filter(m => m.filter(error)) // Find error models whose filter matches the current error
} else {
@ -71,10 +74,10 @@ export class ErrorService {
matchingModels.forEach(m => {
if (m.consumer || m.messageProducer) {
if (m.consumer) {
m.consumer(error)
m.consumer(error.obj)
}
if (m.messageProducer) {
this.alertService.pushError(m.messageProducer(error))
this.alertService.pushError(m.messageProducer(error.obj))
}
} else {
console.error('An error model has no consumer or message')
@ -106,20 +109,30 @@ export interface ErrorHandlerComponent {
/**
* An error model define how errors matching its filter will be handled.
*
* The consumer will consume matching errors when they occurs.
* The consumer will consume matching errors when they occur.
* The message producer returns a string that will be pushed to the alert system.
*
* To work correctly a model must define at least one handler (consumer or message producer).
*/
export class ErrorHandler {
constructor(
public filter: (error: any) => Boolean,
public filter: (error: HandledError) => Boolean,
public consumer?: (error: any) => void,
public messageProducer?: (error: any) => String
) {
}
}
export interface HandledError {
status: number,
type: string,
obj: any // The original object, used to access data in the error
}
function isServerOfflineError(response: any): boolean {
return response.status === 0 || response.status === 502
}
function isHandledError(error: any): error is HandledError {
return true
}