From 530e73fbeac201fe9fa59ec55f7a923401b24e38 Mon Sep 17 00:00:00 2001 From: Joseph Milazzo Date: Tue, 22 Dec 2020 17:27:51 -0600 Subject: [PATCH] Refactored isAdmin to use JWT RBS instead. --- src/app/_guards/admin.guard.ts | 2 +- src/app/_interceptors/error.interceptor.ts | 11 ++++++-- src/app/_models/member.ts | 1 + src/app/_models/user.ts | 2 +- src/app/_services/account.service.ts | 15 ++++++++++- src/app/_services/member.service.ts | 13 ++++++++++ src/app/home/home.component.html | 3 +-- src/app/home/home.component.ts | 20 +++++++-------- src/app/nav-header/nav-header.component.html | 2 +- .../register-member.component.html | 7 +++--- .../register-member.component.ts | 25 ++++++------------- 11 files changed, 63 insertions(+), 38 deletions(-) diff --git a/src/app/_guards/admin.guard.ts b/src/app/_guards/admin.guard.ts index 19fd9b447..2297e381c 100644 --- a/src/app/_guards/admin.guard.ts +++ b/src/app/_guards/admin.guard.ts @@ -16,7 +16,7 @@ export class AdminGuard implements CanActivate { // this automaticallys subs due to being router guard return this.accountService.currentUser$.pipe( map((user: User) => { - if (user && user.isAdmin) { + if (user && user.roles.includes('Admin')) { return true; } diff --git a/src/app/_interceptors/error.interceptor.ts b/src/app/_interceptors/error.interceptor.ts index 124af2783..eacd4b3f4 100644 --- a/src/app/_interceptors/error.interceptor.ts +++ b/src/app/_interceptors/error.interceptor.ts @@ -22,7 +22,14 @@ export class ErrorInterceptor implements HttpInterceptor { console.error('error:', error); switch (error.status) { case 400: - if (error.error.errors) { + // IF type of array, this comes from signin handler + if (Array.isArray(error.error)) { + const modalStateErrors: any[] = []; + error.error.forEach((issue: {code: string, description: string}) => { + modalStateErrors.push(issue.description); + }); + throw modalStateErrors.flat(); + } else if (error.error.errors) { // Validation error const modalStateErrors = []; for (const key in error.error.errors) { @@ -37,7 +44,7 @@ export class ErrorInterceptor implements HttpInterceptor { break; case 401: // if statement is due to http/2 spec issue: https://github.com/angular/angular/issues/23334 - this.toastr.error(error.statusText === 'OK' ? 'Unauthorized' : error.statusText, error.status); + this.toastr.error(error.statusText === 'OK' ? 'Unauthorized' : error.statusText, error.status); break; case 404: this.router.navigateByUrl('/not-found'); diff --git a/src/app/_models/member.ts b/src/app/_models/member.ts index aacdad561..51bb91398 100644 --- a/src/app/_models/member.ts +++ b/src/app/_models/member.ts @@ -5,5 +5,6 @@ export interface Member { lastActive: string; // datetime created: string; // datetime isAdmin: boolean; + roles: string[]; // TODO: Refactor members to use RBS libraries: Library[]; } \ No newline at end of file diff --git a/src/app/_models/user.ts b/src/app/_models/user.ts index 081f9f80b..cb18cd9f1 100644 --- a/src/app/_models/user.ts +++ b/src/app/_models/user.ts @@ -1,6 +1,6 @@ export interface User { username: string; token: string; - isAdmin: boolean; photoUrl?: string; + roles: string[]; } \ No newline at end of file diff --git a/src/app/_services/account.service.ts b/src/app/_services/account.service.ts index 33762a16d..dc6449c6e 100644 --- a/src/app/_services/account.service.ts +++ b/src/app/_services/account.service.ts @@ -12,9 +12,10 @@ export class AccountService { baseUrl = environment.apiUrl; userKey = 'kavita-user'; + currentUser: User | undefined; // Stores values, when someone subscribes gives (1) of last values seen. - private currentUserSource = new ReplaySubject(1); + private currentUserSource = new ReplaySubject(1); // TODO: Move away from ReplaySubject. It's overly complex for what it provides currentUser$ = this.currentUserSource.asObservable(); // $ at end is because this is observable constructor(private httpClient: HttpClient) { @@ -32,13 +33,21 @@ export class AccountService { } setCurrentUser(user: User) { + if (user) { + user.roles = []; + const roles = this.getDecodedToken(user.token).role; + Array.isArray(roles) ? user.roles = roles : user.roles.push(roles); + } + localStorage.setItem(this.userKey, JSON.stringify(user)); this.currentUserSource.next(user); + this.currentUser = user; } logout() { localStorage.removeItem(this.userKey); this.currentUserSource.next(undefined); + this.currentUser = undefined; } register(model: {username: string, password: string, isAdmin?: boolean}, login = false) { @@ -53,5 +62,9 @@ export class AccountService { ); } + getDecodedToken(token: string) { + return JSON.parse(atob(token.split('.')[1])); + } + } diff --git a/src/app/_services/member.service.ts b/src/app/_services/member.service.ts index 2ebb40cba..90a4a0fa4 100644 --- a/src/app/_services/member.service.ts +++ b/src/app/_services/member.service.ts @@ -15,4 +15,17 @@ export class MemberService { getMembers() { return this.httpClient.get(this.baseUrl + 'users'); } + + adminExists() { + return this.httpClient.get(this.baseUrl + 'admin/exists'); + } + + updatePassword(newPassword: string) { + // TODO: Implement update password (use JWT to assume role) + } + + deleteMember(member: string) { + // TODO: Implement delete member (admin only) + } + } diff --git a/src/app/home/home.component.html b/src/app/home/home.component.html index d9f442777..25e5235f4 100644 --- a/src/app/home/home.component.html +++ b/src/app/home/home.component.html @@ -1,9 +1,8 @@
-

Please create an admin account for yourself to start your reading journey.

- +
diff --git a/src/app/home/home.component.ts b/src/app/home/home.component.ts index 9954cc9cf..05ac2d0a9 100644 --- a/src/app/home/home.component.ts +++ b/src/app/home/home.component.ts @@ -24,15 +24,14 @@ export class HomeComponent implements OnInit { ngOnInit(): void { - // TODO: Clean up this logic - this.accountService.currentUser$.pipe(take(1)).subscribe(user => { - if (user) { - // User is logged in, redirect to libraries - this.router.navigateByUrl('/library'); - } else { - this.memberService.getMembers().subscribe(members => { - this.firstTimeFlow = members.filter(m => m.isAdmin).length === 0; - console.log('First time user flow: ', this.firstTimeFlow); + this.memberService.adminExists().subscribe(adminExists => { + this.firstTimeFlow = !adminExists; + if (!this.firstTimeFlow) { + this.accountService.currentUser$.pipe(take(1)).subscribe(user => { + if (user) { + // User is logged in, redirect to libraries + this.router.navigateByUrl('/library'); + } }); } }); @@ -41,7 +40,8 @@ export class HomeComponent implements OnInit { onAdminCreated(success: boolean) { if (success) { - this.router.navigateByUrl('/library'); + this.router.navigateByUrl('/home'); + this.firstTimeFlow = false; } } diff --git a/src/app/nav-header/nav-header.component.html b/src/app/nav-header/nav-header.component.html index f0c3c822b..431ce4a94 100644 --- a/src/app/nav-header/nav-header.component.html +++ b/src/app/nav-header/nav-header.component.html @@ -13,7 +13,7 @@ diff --git a/src/app/shared/register-member/register-member.component.html b/src/app/shared/register-member/register-member.component.html index 4a82e058d..6ffba59c3 100644 --- a/src/app/shared/register-member/register-member.component.html +++ b/src/app/shared/register-member/register-member.component.html @@ -1,7 +1,8 @@ -
- +
  • {{error}}
  • +
    diff --git a/src/app/shared/register-member/register-member.component.ts b/src/app/shared/register-member/register-member.component.ts index 640f3c1d1..9b839b3aa 100644 --- a/src/app/shared/register-member/register-member.component.ts +++ b/src/app/shared/register-member/register-member.component.ts @@ -1,7 +1,5 @@ import { Component, EventEmitter, Input, OnInit, Output } from '@angular/core'; import { FormGroup, FormControl, Validators } from '@angular/forms'; -import { MemberService } from 'src/app/_services/member.service'; -import { Member } from 'src/app/_models/member'; import { AccountService } from 'src/app/_services/account.service'; @Component({ @@ -11,38 +9,31 @@ import { AccountService } from 'src/app/_services/account.service'; }) export class RegisterMemberComponent implements OnInit { + @Input() firstTimeFlow = false; @Output() created = new EventEmitter(); adminExists = false; - model: any = {}; registerForm: FormGroup = new FormGroup({ username: new FormControl('', [Validators.required]), password: new FormControl('', [Validators.required]), isAdmin: new FormControl(false, []) }); + errors: string[] = []; - constructor(private accountService: AccountService, private memberService: MemberService) { - this.memberService.getMembers().subscribe(members => { - this.adminExists = members.filter((m: Member) => m.isAdmin).length > 0; - if (!this.adminExists) { - this.registerForm.get('isAdmin')?.setValue(true); - this.model.isAdmin = true; - } - }); + constructor(private accountService: AccountService) { } ngOnInit(): void { + if (this.firstTimeFlow) { + this.registerForm.get('isAdmin')?.setValue(true); + } } register() { - this.model.username = this.registerForm.get('username')?.value; - this.model.password = this.registerForm.get('password')?.value; - this.model.isAdmin = this.registerForm.get('isAdmin')?.value; - - this.accountService.register(this.model).subscribe(resp => { + this.accountService.register(this.registerForm.value).subscribe(resp => { this.created.emit(true); }, err => { - console.log('validation errors from interceptor', err); + this.errors = err; }); }