Refactored isAdmin to use JWT RBS instead.

This commit is contained in:
Joseph Milazzo 2020-12-22 17:27:51 -06:00
parent b47d2acac8
commit 530e73fbea
11 changed files with 63 additions and 38 deletions

View file

@ -16,7 +16,7 @@ export class AdminGuard implements CanActivate {
// this automaticallys subs due to being router guard // this automaticallys subs due to being router guard
return this.accountService.currentUser$.pipe( return this.accountService.currentUser$.pipe(
map((user: User) => { map((user: User) => {
if (user && user.isAdmin) { if (user && user.roles.includes('Admin')) {
return true; return true;
} }

View file

@ -22,7 +22,14 @@ export class ErrorInterceptor implements HttpInterceptor {
console.error('error:', error); console.error('error:', error);
switch (error.status) { switch (error.status) {
case 400: 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 // Validation error
const modalStateErrors = []; const modalStateErrors = [];
for (const key in error.error.errors) { for (const key in error.error.errors) {

View file

@ -5,5 +5,6 @@ export interface Member {
lastActive: string; // datetime lastActive: string; // datetime
created: string; // datetime created: string; // datetime
isAdmin: boolean; isAdmin: boolean;
roles: string[]; // TODO: Refactor members to use RBS
libraries: Library[]; libraries: Library[];
} }

View file

@ -1,6 +1,6 @@
export interface User { export interface User {
username: string; username: string;
token: string; token: string;
isAdmin: boolean;
photoUrl?: string; photoUrl?: string;
roles: string[];
} }

View file

@ -12,9 +12,10 @@ export class AccountService {
baseUrl = environment.apiUrl; baseUrl = environment.apiUrl;
userKey = 'kavita-user'; userKey = 'kavita-user';
currentUser: User | undefined;
// Stores values, when someone subscribes gives (1) of last values seen. // Stores values, when someone subscribes gives (1) of last values seen.
private currentUserSource = new ReplaySubject<User>(1); private currentUserSource = new ReplaySubject<User>(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 currentUser$ = this.currentUserSource.asObservable(); // $ at end is because this is observable
constructor(private httpClient: HttpClient) { constructor(private httpClient: HttpClient) {
@ -32,13 +33,21 @@ export class AccountService {
} }
setCurrentUser(user: User) { 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)); localStorage.setItem(this.userKey, JSON.stringify(user));
this.currentUserSource.next(user); this.currentUserSource.next(user);
this.currentUser = user;
} }
logout() { logout() {
localStorage.removeItem(this.userKey); localStorage.removeItem(this.userKey);
this.currentUserSource.next(undefined); this.currentUserSource.next(undefined);
this.currentUser = undefined;
} }
register(model: {username: string, password: string, isAdmin?: boolean}, login = false) { 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]));
}
} }

View file

@ -15,4 +15,17 @@ export class MemberService {
getMembers() { getMembers() {
return this.httpClient.get<Member[]>(this.baseUrl + 'users'); return this.httpClient.get<Member[]>(this.baseUrl + 'users');
} }
adminExists() {
return this.httpClient.get<boolean>(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)
}
} }

View file

@ -1,9 +1,8 @@
<div class="container"> <div class="container">
<ng-container *ngIf="firstTimeFlow"> <ng-container *ngIf="firstTimeFlow">
<!-- NOTE: We don't need a first time user flow. We just need a simple component to register a new admin. Once registered, we can put them on admin/ route -->
<p>Please create an admin account for yourself to start your reading journey.</p> <p>Please create an admin account for yourself to start your reading journey.</p>
<app-register-member (created)="onAdminCreated($event)"></app-register-member> <app-register-member (created)="onAdminCreated($event)" [firstTimeFlow]="firstTimeFlow"></app-register-member>
</ng-container> </ng-container>
<ng-container *ngIf="!firstTimeFlow && (this.accountService.currentUser$ | async) == null"> <ng-container *ngIf="!firstTimeFlow && (this.accountService.currentUser$ | async) == null">

View file

@ -24,15 +24,14 @@ export class HomeComponent implements OnInit {
ngOnInit(): void { ngOnInit(): void {
// TODO: Clean up this logic this.memberService.adminExists().subscribe(adminExists => {
this.firstTimeFlow = !adminExists;
if (!this.firstTimeFlow) {
this.accountService.currentUser$.pipe(take(1)).subscribe(user => { this.accountService.currentUser$.pipe(take(1)).subscribe(user => {
if (user) { if (user) {
// User is logged in, redirect to libraries // User is logged in, redirect to libraries
this.router.navigateByUrl('/library'); 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);
}); });
} }
}); });
@ -41,7 +40,8 @@ export class HomeComponent implements OnInit {
onAdminCreated(success: boolean) { onAdminCreated(success: boolean) {
if (success) { if (success) {
this.router.navigateByUrl('/library'); this.router.navigateByUrl('/home');
this.firstTimeFlow = false;
} }
} }

View file

@ -13,7 +13,7 @@
<div ngbDropdown class=" nav-item dropdown" *ngIf="(accountService.currentUser$ | async) as user" dropdown> <div ngbDropdown class=" nav-item dropdown" *ngIf="(accountService.currentUser$ | async) as user" dropdown>
<button class="btn btn-outline-primary" ngbDropdownToggle>{{user.username | titlecase}}</button> <button class="btn btn-outline-primary" ngbDropdownToggle>{{user.username | titlecase}}</button>
<div ngbDropdownMenu > <div ngbDropdownMenu >
<button ngbDropdownItem routerLink="/admin/dashboard" *ngIf="user.isAdmin">Server Settings</button> <button ngbDropdownItem routerLink="/admin/dashboard" *ngIf="user.roles.includes('Admin')">Server Settings</button>
<button ngbDropdownItem (click)="logout()">Logout</button> <button ngbDropdownItem (click)="logout()">Logout</button>
</div> </div>
</div> </div>

View file

@ -1,7 +1,8 @@
<div class="text-warning"> <div class="text-danger" *ngIf="errors.length > 0">
<!-- <p>Error:</p> <p>Errors:</p>
<ul> <ul>
</ul> --> <li *ngFor="let error of errors">{{error}}</li>
</ul>
</div> </div>
<form [formGroup]="registerForm" (ngSubmit)="register()"> <form [formGroup]="registerForm" (ngSubmit)="register()">
<div class="form-group"> <div class="form-group">

View file

@ -1,7 +1,5 @@
import { Component, EventEmitter, Input, OnInit, Output } from '@angular/core'; import { Component, EventEmitter, Input, OnInit, Output } from '@angular/core';
import { FormGroup, FormControl, Validators } from '@angular/forms'; 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'; import { AccountService } from 'src/app/_services/account.service';
@Component({ @Component({
@ -11,38 +9,31 @@ import { AccountService } from 'src/app/_services/account.service';
}) })
export class RegisterMemberComponent implements OnInit { export class RegisterMemberComponent implements OnInit {
@Input() firstTimeFlow = false;
@Output() created = new EventEmitter<boolean>(); @Output() created = new EventEmitter<boolean>();
adminExists = false; adminExists = false;
model: any = {};
registerForm: FormGroup = new FormGroup({ registerForm: FormGroup = new FormGroup({
username: new FormControl('', [Validators.required]), username: new FormControl('', [Validators.required]),
password: new FormControl('', [Validators.required]), password: new FormControl('', [Validators.required]),
isAdmin: new FormControl(false, []) isAdmin: new FormControl(false, [])
}); });
errors: string[] = [];
constructor(private accountService: AccountService, private memberService: MemberService) { constructor(private accountService: AccountService) {
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;
}
});
} }
ngOnInit(): void { ngOnInit(): void {
if (this.firstTimeFlow) {
this.registerForm.get('isAdmin')?.setValue(true);
}
} }
register() { register() {
this.model.username = this.registerForm.get('username')?.value; this.accountService.register(this.registerForm.value).subscribe(resp => {
this.model.password = this.registerForm.get('password')?.value;
this.model.isAdmin = this.registerForm.get('isAdmin')?.value;
this.accountService.register(this.model).subscribe(resp => {
this.created.emit(true); this.created.emit(true);
}, err => { }, err => {
console.log('validation errors from interceptor', err); this.errors = err;
}); });
} }