Refactored isAdmin to use JWT RBS instead.
This commit is contained in:
parent
b47d2acac8
commit
530e73fbea
11 changed files with 63 additions and 38 deletions
|
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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) {
|
||||||
|
|
|
||||||
|
|
@ -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[];
|
||||||
}
|
}
|
||||||
|
|
@ -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[];
|
||||||
}
|
}
|
||||||
|
|
@ -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]));
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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">
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
|
||||||
|
|
@ -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">
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue