feature/30-group-authentication #9

Merged
william merged 6 commits from feature/30-group-authentication into develop 2022-08-03 08:07:11 -04:00
11 changed files with 89 additions and 137 deletions
Showing only changes of commit 3ff897fc85 - Show all commits

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

@ -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

@ -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

@ -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
}