
* Moved the Server Settings out into a button on nav header * Refactored Mange Users page to the new design (skeleton). Implemented skeleton code for Invite User. * Hashed out more of the code, but need to move all the email code to a Kavita controlled API server due to password credentials. * Cleaned up some warnings * When no user exists for an api key in Plugin controller, throw 401. * Hooked in the ability to check if the Kavita instance can be accessed externally so we can determine if the user can invite or not. * Hooked up some logic if the user's server isn't accessible, then default to old flow * Basic flow is working for confirm email. Needs validation, error handling, etc. * Refactored Password validation to account service * Cleaned up the code in confirm-email to work much better. * Refactored the login page to have a container functionality, so we can reuse the styles on multiple pages (registration pages). Hooked up the code for confirm email. * Messy code, but making progress. Refactored Register to be used only for first time user registration. Added a new register component to handle first time flow only. * Invite works much better, still needs a bit of work for non-accessible server setup. Started work on underlying manage users page to meet new design. * Changed (you) to a star to indicate who you're logged in as. * Inviting a user is now working and tested fully. * Removed the register member component as we now have invite and confirm components. * Editing a user is now working. Username change and Role/Library access from within one screen. Email changing is on hold. * Cleaned up code for edit user and disabled email field for now. * Cleaned up the code to indicate changing a user's email is not possible. * Implemented a migration for existing accounts so they can validate their emails and still login. * Change url for email server * Implemented the ability to resend an email confirmation code (or regenerate for non accessible servers). Fixed an overflow on the confirm dialog. * Took care of some code cleanup * Removed 3 db calls from cover refresh and some misc cleanup * Fixed a broken test
216 lines
7 KiB
TypeScript
216 lines
7 KiB
TypeScript
import { HttpClient } from '@angular/common/http';
|
|
import { Injectable, OnDestroy } from '@angular/core';
|
|
import { Observable, of, ReplaySubject, Subject } from 'rxjs';
|
|
import { map, takeUntil } from 'rxjs/operators';
|
|
import { environment } from 'src/environments/environment';
|
|
import { Preferences } from '../_models/preferences/preferences';
|
|
import { User } from '../_models/user';
|
|
import { Router } from '@angular/router';
|
|
import { MessageHubService } from './message-hub.service';
|
|
|
|
@Injectable({
|
|
providedIn: 'root'
|
|
})
|
|
export class AccountService implements OnDestroy {
|
|
|
|
baseUrl = environment.apiUrl;
|
|
userKey = 'kavita-user';
|
|
public lastLoginKey = 'kavita-lastlogin';
|
|
currentUser: User | undefined;
|
|
|
|
// Stores values, when someone subscribes gives (1) of last values seen.
|
|
private currentUserSource = new ReplaySubject<User>(1);
|
|
currentUser$ = this.currentUserSource.asObservable();
|
|
|
|
/**
|
|
* SetTimeout handler for keeping track of refresh token call
|
|
*/
|
|
private refreshTokenTimeout: ReturnType<typeof setTimeout> | undefined;
|
|
|
|
private readonly onDestroy = new Subject<void>();
|
|
|
|
constructor(private httpClient: HttpClient, private router: Router,
|
|
private messageHub: MessageHubService) {}
|
|
|
|
ngOnDestroy(): void {
|
|
this.onDestroy.next();
|
|
this.onDestroy.complete();
|
|
}
|
|
|
|
hasAdminRole(user: User) {
|
|
return user && user.roles.includes('Admin');
|
|
}
|
|
|
|
hasDownloadRole(user: User) {
|
|
return user && user.roles.includes('Download');
|
|
}
|
|
|
|
getRoles() {
|
|
return this.httpClient.get<string[]>(this.baseUrl + 'account/roles');
|
|
}
|
|
|
|
login(model: {username: string, password: string}): Observable<any> {
|
|
return this.httpClient.post<User>(this.baseUrl + 'account/login', model).pipe(
|
|
map((response: User) => {
|
|
const user = response;
|
|
if (user) {
|
|
this.setCurrentUser(user);
|
|
this.messageHub.createHubConnection(user, this.hasAdminRole(user));
|
|
}
|
|
}),
|
|
takeUntil(this.onDestroy)
|
|
);
|
|
}
|
|
|
|
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.lastLoginKey, user.username);
|
|
}
|
|
|
|
this.currentUserSource.next(user);
|
|
this.currentUser = user;
|
|
if (this.currentUser !== undefined) {
|
|
this.startRefreshTokenTimer();
|
|
} else {
|
|
this.stopRefreshTokenTimer();
|
|
}
|
|
}
|
|
|
|
logout() {
|
|
localStorage.removeItem(this.userKey);
|
|
this.currentUserSource.next(undefined);
|
|
this.currentUser = undefined;
|
|
this.stopRefreshTokenTimer();
|
|
// Upon logout, perform redirection
|
|
this.router.navigateByUrl('/login');
|
|
this.messageHub.stopHubConnection();
|
|
}
|
|
|
|
|
|
/**
|
|
* Registers the first admin on the account. Only used for that. All other registrations must occur through invite
|
|
* @param model
|
|
* @returns
|
|
*/
|
|
register(model: {username: string, password: string, email: string}) {
|
|
return this.httpClient.post<User>(this.baseUrl + 'account/register', model).pipe(
|
|
map((user: User) => {
|
|
return user;
|
|
}),
|
|
takeUntil(this.onDestroy)
|
|
);
|
|
}
|
|
|
|
migrateUser(model: {email: string, username: string, password: string, sendEmail: boolean}) {
|
|
return this.httpClient.post<string>(this.baseUrl + 'account/migrate-email', model, {responseType: 'text' as 'json'});
|
|
}
|
|
|
|
confirmMigrationEmail(model: {email: string, token: string}) {
|
|
return this.httpClient.post<User>(this.baseUrl + 'account/confirm-migration-email', model);
|
|
}
|
|
|
|
resendConfirmationEmail(userId: number) {
|
|
return this.httpClient.post<string>(this.baseUrl + 'account/resend-confirmation-email?userId=' + userId, {}, {responseType: 'text' as 'json'});
|
|
}
|
|
|
|
inviteUser(model: {email: string, roles: Array<string>, libraries: Array<number>, sendEmail: boolean}) {
|
|
return this.httpClient.post<string>(this.baseUrl + 'account/invite', model, {responseType: 'text' as 'json'});
|
|
}
|
|
|
|
confirmEmail(model: {email: string, username: string, password: string, token: string}) {
|
|
return this.httpClient.post<User>(this.baseUrl + 'account/confirm-email', model);
|
|
}
|
|
|
|
getDecodedToken(token: string) {
|
|
return JSON.parse(atob(token.split('.')[1]));
|
|
}
|
|
|
|
resetPassword(username: string, password: string) {
|
|
return this.httpClient.post(this.baseUrl + 'account/reset-password', {username, password}, {responseType: 'json' as 'text'});
|
|
}
|
|
|
|
update(model: {email: string, roles: Array<string>, libraries: Array<number>, userId: number}) {
|
|
return this.httpClient.post(this.baseUrl + 'account/update', model);
|
|
}
|
|
|
|
updatePreferences(userPreferences: Preferences) {
|
|
return this.httpClient.post<Preferences>(this.baseUrl + 'users/update-preferences', userPreferences).pipe(map(settings => {
|
|
if (this.currentUser !== undefined || this.currentUser != null) {
|
|
this.currentUser.preferences = settings;
|
|
this.setCurrentUser(this.currentUser);
|
|
}
|
|
return settings;
|
|
}), takeUntil(this.onDestroy));
|
|
}
|
|
|
|
getUserFromLocalStorage(): User | undefined {
|
|
|
|
const userString = localStorage.getItem(this.userKey);
|
|
|
|
if (userString) {
|
|
return JSON.parse(userString)
|
|
};
|
|
|
|
return undefined;
|
|
}
|
|
|
|
resetApiKey() {
|
|
return this.httpClient.post<string>(this.baseUrl + 'account/reset-api-key', {}, {responseType: 'text' as 'json'}).pipe(map(key => {
|
|
const user = this.getUserFromLocalStorage();
|
|
if (user) {
|
|
user.apiKey = key;
|
|
|
|
localStorage.setItem(this.userKey, JSON.stringify(user));
|
|
|
|
this.currentUserSource.next(user);
|
|
this.currentUser = user;
|
|
}
|
|
return key;
|
|
}));
|
|
}
|
|
|
|
private refreshToken() {
|
|
if (this.currentUser === null || this.currentUser === undefined) return of();
|
|
|
|
return this.httpClient.post<{token: string, refreshToken: string}>(this.baseUrl + 'account/refresh-token', {token: this.currentUser.token, refreshToken: this.currentUser.refreshToken}).pipe(map(user => {
|
|
if (this.currentUser) {
|
|
this.currentUser.token = user.token;
|
|
this.currentUser.refreshToken = user.refreshToken;
|
|
}
|
|
|
|
this.currentUserSource.next(this.currentUser);
|
|
this.startRefreshTokenTimer();
|
|
return user;
|
|
}));
|
|
}
|
|
|
|
private startRefreshTokenTimer() {
|
|
if (this.currentUser === null || this.currentUser === undefined) return;
|
|
|
|
if (this.refreshTokenTimeout !== undefined) {
|
|
this.stopRefreshTokenTimer();
|
|
}
|
|
|
|
const jwtToken = JSON.parse(atob(this.currentUser.token.split('.')[1]));
|
|
// set a timeout to refresh the token a minute before it expires
|
|
const expires = new Date(jwtToken.exp * 1000);
|
|
const timeout = expires.getTime() - Date.now() - (60 * 1000);
|
|
this.refreshTokenTimeout = setTimeout(() => this.refreshToken().subscribe(() => {
|
|
console.log('Token Refreshed');
|
|
}), timeout);
|
|
}
|
|
|
|
private stopRefreshTokenTimer() {
|
|
if (this.refreshTokenTimeout !== undefined) {
|
|
clearTimeout(this.refreshTokenTimeout);
|
|
}
|
|
}
|
|
|
|
|
|
|
|
}
|