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
return this.accountService.currentUser$.pipe(
map((user: User) => {
if (user && user.isAdmin) {
if (user && user.roles.includes('Admin')) {
return true;
}

View file

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

View file

@ -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[];
}

View file

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

View file

@ -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<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
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]));
}
}

View file

@ -15,4 +15,17 @@ export class MemberService {
getMembers() {
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">
<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>
<app-register-member (created)="onAdminCreated($event)"></app-register-member>
<app-register-member (created)="onAdminCreated($event)" [firstTimeFlow]="firstTimeFlow"></app-register-member>
</ng-container>
<ng-container *ngIf="!firstTimeFlow && (this.accountService.currentUser$ | async) == null">

View file

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

View file

@ -13,7 +13,7 @@
<div ngbDropdown class=" nav-item dropdown" *ngIf="(accountService.currentUser$ | async) as user" dropdown>
<button class="btn btn-outline-primary" ngbDropdownToggle>{{user.username | titlecase}}</button>
<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>
</div>
</div>

View file

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

View file

@ -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<boolean>();
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;
});
}