mirror of
https://github.com/adityachandelgit/BookLore.git
synced 2026-02-11 06:59:03 -06:00
Merge branch 'develop' into feat/auto-set-paths
This commit is contained in:
@@ -19,6 +19,6 @@ public class ReadProgressRequest {
|
||||
|
||||
@AssertTrue(message = "At least one progress field must be provided")
|
||||
public boolean isProgressValid() {
|
||||
return epubProgress != null || pdfProgress != null || cbxProgress != null;
|
||||
return epubProgress != null || pdfProgress != null || cbxProgress != null || dateFinished != null;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -312,9 +312,12 @@ public class BookService {
|
||||
userBookProgress.setCbxProgress(request.getCbxProgress().getPage());
|
||||
userBookProgress.setCbxProgressPercent(request.getCbxProgress().getPercentage());
|
||||
}
|
||||
|
||||
// Update dateFinished if provided
|
||||
if (request.getDateFinished() != null) {
|
||||
userBookProgress.setDateFinished(request.getDateFinished());
|
||||
}
|
||||
|
||||
userBookProgressRepository.save(userBookProgress);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,19 +1,12 @@
|
||||
import {Injectable} from '@angular/core';
|
||||
import {BehaviorSubject} from 'rxjs';
|
||||
|
||||
@Injectable({ providedIn: 'root' })
|
||||
@Injectable({providedIn: 'root'})
|
||||
export class AuthInitializationService {
|
||||
private initialized = new BehaviorSubject<boolean>(false);
|
||||
initialized$ = this.initialized.asObservable();
|
||||
|
||||
private oidcFailed = new BehaviorSubject<boolean>(false);
|
||||
oidcFailed$ = this.oidcFailed.asObservable();
|
||||
|
||||
markAsInitialized() {
|
||||
this.initialized.next(true);
|
||||
}
|
||||
|
||||
setOidcFailed(failed: boolean) {
|
||||
this.oidcFailed.next(failed);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,59 +16,52 @@ export function initializeAuthFactory() {
|
||||
|
||||
return new Promise<void>((resolve) => {
|
||||
const sub = appSettingsService.appSettings$.subscribe(settings => {
|
||||
if (!settings) return;
|
||||
if (settings) {
|
||||
if (settings.oidcEnabled && settings.oidcProviderDetails) {
|
||||
const details = settings.oidcProviderDetails;
|
||||
|
||||
sub.unsubscribe();
|
||||
|
||||
if (settings.oidcEnabled && settings.oidcProviderDetails) {
|
||||
const details = settings.oidcProviderDetails;
|
||||
|
||||
oauthService.configure({
|
||||
issuer: details.issuerUri,
|
||||
clientId: details.clientId,
|
||||
scope: 'openid profile email offline_access',
|
||||
redirectUri: window.location.origin + '/oauth2-callback',
|
||||
responseType: 'code',
|
||||
showDebugInformation: false,
|
||||
requireHttps: false,
|
||||
strictDiscoveryDocumentValidation: false,
|
||||
});
|
||||
|
||||
oauthService.loadDiscoveryDocumentAndTryLogin()
|
||||
.then(() => {
|
||||
if (oauthService.hasValidAccessToken()) {
|
||||
console.log('[OIDC] Valid access token found');
|
||||
oauthService.setupAutomaticSilentRefresh();
|
||||
websocketInitializer(authService);
|
||||
} else {
|
||||
console.warn('[OIDC] No valid access token. Will proceed to app and show login page.');
|
||||
}
|
||||
})
|
||||
.catch(err => {
|
||||
console.error('[OIDC] Failed to load discovery document or login:', err);
|
||||
authInitService.setOidcFailed(true);
|
||||
})
|
||||
.finally(() => {
|
||||
authInitService.markAsInitialized();
|
||||
resolve();
|
||||
oauthService.configure({
|
||||
issuer: details.issuerUri,
|
||||
clientId: details.clientId,
|
||||
scope: 'openid profile email offline_access',
|
||||
redirectUri: window.location.origin + '/oauth2-callback',
|
||||
responseType: 'code',
|
||||
showDebugInformation: false,
|
||||
requireHttps: false,
|
||||
strictDiscoveryDocumentValidation: false,
|
||||
});
|
||||
|
||||
} else if (settings.remoteAuthEnabled) {
|
||||
authService.remoteLogin().subscribe({
|
||||
next: () => {
|
||||
authInitService.markAsInitialized();
|
||||
resolve();
|
||||
},
|
||||
error: err => {
|
||||
console.error('[Remote Login] failed:', err);
|
||||
authInitService.markAsInitialized();
|
||||
resolve();
|
||||
}
|
||||
});
|
||||
|
||||
} else {
|
||||
authInitService.markAsInitialized();
|
||||
resolve();
|
||||
oauthService.loadDiscoveryDocumentAndTryLogin()
|
||||
.then(() => {
|
||||
console.log('[OIDC] Discovery document loaded and login attempted');
|
||||
if (oauthService.hasValidAccessToken()) {
|
||||
authService.tokenSubject.next(oauthService.getAccessToken())
|
||||
console.log('[OIDC] Valid access token found after tryLogin');
|
||||
router.navigate(['/dashboard']);
|
||||
oauthService.setupAutomaticSilentRefresh();
|
||||
websocketInitializer(authService);
|
||||
authInitService.markAsInitialized();
|
||||
resolve();
|
||||
} else {
|
||||
console.log('[OIDC] No valid access token found, attempting silent login with prompt=none');
|
||||
oauthService.initImplicitFlow();
|
||||
resolve();
|
||||
}
|
||||
})
|
||||
.catch(err => {
|
||||
authInitService.markAsInitialized();
|
||||
console.error(
|
||||
'OIDC initialization failed: Unable to complete OpenID Connect discovery or login. ' +
|
||||
'This may be due to an incorrect issuer URL, client ID, or network issue. ' +
|
||||
'Falling back to local login. Details:', err
|
||||
);
|
||||
resolve();
|
||||
});
|
||||
} else {
|
||||
authInitService.markAsInitialized();
|
||||
resolve();
|
||||
}
|
||||
sub.unsubscribe();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,4 +1,9 @@
|
||||
<div class="book-card"
|
||||
tooltipPosition="bottom"
|
||||
autoHide="false"
|
||||
tooltipStyleClass="text-xs text-center"
|
||||
[pTooltip]="book.metadata?.title"
|
||||
|
||||
[class.selected]="isSelected"
|
||||
(mouseover)="isHovered = true"
|
||||
(mouseout)="isHovered = false">
|
||||
@@ -31,8 +36,17 @@
|
||||
</div>
|
||||
}
|
||||
|
||||
<p-button [rounded]="true" icon="pi pi-info" class="info-btn" (click)="openBookInfo(book)"></p-button>
|
||||
<p-button [hidden]="readButtonHidden" [rounded]="true" icon="pi pi-book" class="read-btn" (click)="readBook(book)"></p-button>
|
||||
<p-button
|
||||
tooltipPosition="top"
|
||||
tooltipStyleClass="text-xs text-center"
|
||||
pTooltip="Open Book Info"
|
||||
[rounded]="true" icon="pi pi-info" class="info-btn" (click)="openBookInfo(book)"></p-button>
|
||||
|
||||
<p-button
|
||||
tooltipPosition="top"
|
||||
tooltipStyleClass="text-xs text-center"
|
||||
pTooltip="{{ koProgressPercentage || progressPercentage ? 'Continue Reading' : 'Start Reading' }}"
|
||||
[hidden]="readButtonHidden" [rounded]="true" icon="pi pi-book" class="read-btn" (click)="readBook(book)"></p-button>
|
||||
|
||||
@if (isCheckboxEnabled) {
|
||||
<p-checkbox
|
||||
@@ -66,7 +80,7 @@
|
||||
|
||||
<div [hidden]="bottomBarHidden">
|
||||
<div class="book-title-container flex items-center">
|
||||
<h4 class="book-title m-0 pl-2" [title]="book.metadata?.title">{{ book.metadata?.title }}</h4>
|
||||
<h4 class="book-title m-0 pl-2">{{ book.metadata?.title }}</h4>
|
||||
<p-tieredmenu #menu [model]="items" [popup]="true" appendTo="body"></p-tieredmenu>
|
||||
<p-button
|
||||
class="custom-button-padding"
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import {Component, ElementRef, EventEmitter, inject, Input, OnDestroy, OnInit, Output, ViewChild} from '@angular/core';
|
||||
import {TooltipModule} from "primeng/tooltip";
|
||||
import {Book, ReadStatus} from '../../../model/book.model';
|
||||
import {Button} from 'primeng/button';
|
||||
import {MenuModule} from 'primeng/menu';
|
||||
@@ -29,7 +30,7 @@ import {ResetProgressTypes} from '../../../../shared/constants/reset-progress-ty
|
||||
selector: 'app-book-card',
|
||||
templateUrl: './book-card.component.html',
|
||||
styleUrls: ['./book-card.component.scss'],
|
||||
imports: [Button, MenuModule, CheckboxModule, FormsModule, NgClass, TieredMenu, ProgressBar],
|
||||
imports: [Button, MenuModule, CheckboxModule, FormsModule, NgClass, TieredMenu, ProgressBar, TooltipModule],
|
||||
standalone: true
|
||||
})
|
||||
export class BookCardComponent implements OnInit, OnDestroy {
|
||||
|
||||
@@ -48,7 +48,7 @@
|
||||
(click)="toggleMetadataLock(metadata)">
|
||||
</p-button>
|
||||
</td>
|
||||
<td (click)="openMetadataCenter(book.id)">
|
||||
<td (click)="openMetadataCenter(book.id)" class="cursor-pointer">
|
||||
<img [attr.src]="urlHelper.getCoverUrl(metadata.bookId, metadata.coverUpdatedOn)" alt="Book Cover" class="size-7"/>
|
||||
</td>
|
||||
|
||||
@@ -68,7 +68,12 @@
|
||||
</span>
|
||||
</td>
|
||||
} @else {
|
||||
<td [title]="getCellValue(metadata, book, col.field)" class="overflow-hidden truncate text-right min-w-[6rem] max-w-[12rem]">
|
||||
<td [title]="getCellValue(metadata, book, col.field)"
|
||||
tooltipPosition="right"
|
||||
autoHide="false"
|
||||
tooltipStyleClass="text-xs text-center"
|
||||
[pTooltip]="getCellValue(metadata, book, col.field)?.toString()"
|
||||
class="overflow-hidden truncate text-right min-w-[6rem] max-w-[12rem]">
|
||||
{{ getCellValue(metadata, book, col.field) }}
|
||||
</td>
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ import {TableModule} from 'primeng/table';
|
||||
import {DatePipe} from '@angular/common';
|
||||
import {Rating} from 'primeng/rating';
|
||||
import {FormsModule} from '@angular/forms';
|
||||
import {TooltipModule} from "primeng/tooltip";
|
||||
import {Book, BookMetadata} from '../../../model/book.model';
|
||||
import {SortOption} from '../../../model/sort.model';
|
||||
import {UrlHelperService} from '../../../../utilities/service/url-helper.service';
|
||||
@@ -24,7 +25,8 @@ import {take, takeUntil} from 'rxjs/operators';
|
||||
TableModule,
|
||||
Rating,
|
||||
FormsModule,
|
||||
Button
|
||||
Button,
|
||||
TooltipModule
|
||||
],
|
||||
styleUrls: ['./book-table.component.scss'],
|
||||
providers: [DatePipe]
|
||||
|
||||
@@ -1,4 +1,8 @@
|
||||
<div class="book-cover-wrapper"
|
||||
tooltipPosition="top"
|
||||
autoHide="false"
|
||||
tooltipStyleClass="text-xs text-center"
|
||||
[pTooltip]="book.metadata?.title"
|
||||
(mouseenter)="isHovered = true"
|
||||
(mouseleave)="isHovered = false">
|
||||
<img
|
||||
|
||||
@@ -8,12 +8,14 @@ import {filter, Subject} from 'rxjs';
|
||||
import {NgClass} from '@angular/common';
|
||||
import {BookMetadataHostService} from '../../../utilities/service/book-metadata-host-service';
|
||||
import {takeUntil} from 'rxjs/operators';
|
||||
import { TooltipModule } from 'primeng/tooltip';
|
||||
|
||||
@Component({
|
||||
selector: 'app-book-card-lite-component',
|
||||
imports: [
|
||||
Button,
|
||||
NgClass
|
||||
NgClass,
|
||||
TooltipModule
|
||||
],
|
||||
templateUrl: './book-card-lite-component.html',
|
||||
styleUrl: './book-card-lite-component.scss'
|
||||
|
||||
@@ -20,11 +20,9 @@ export class AuthService {
|
||||
private oAuthService = inject(OAuthService);
|
||||
private router = inject(Router);
|
||||
|
||||
private tokenSubject = new BehaviorSubject<string | null>(this.getOidcAccessToken() || this.getInternalAccessToken());
|
||||
public tokenSubject = new BehaviorSubject<string | null>(this.getOidcAccessToken() || this.getInternalAccessToken());
|
||||
public token$ = this.tokenSubject.asObservable();
|
||||
|
||||
private logoutSubject = new Subject<void>();
|
||||
public logout$ = this.logoutSubject.asObservable();
|
||||
|
||||
internalLogin(credentials: { username: string; password: string }): Observable<{ accessToken: string; refreshToken: string, isDefaultPassword: string }> {
|
||||
return this.http.post<{ accessToken: string; refreshToken: string, isDefaultPassword: string }>(`${this.apiUrl}/login`, credentials).pipe(
|
||||
@@ -48,17 +46,6 @@ export class AuthService {
|
||||
);
|
||||
}
|
||||
|
||||
remoteLogin(): Observable<{ accessToken: string; refreshToken: string, isDefaultPassword: string }> {
|
||||
return this.http.get<{ accessToken: string; refreshToken: string, isDefaultPassword: string }>(`${this.apiUrl}/remote`).pipe(
|
||||
tap((response) => {
|
||||
if (response.accessToken && response.refreshToken) {
|
||||
this.saveInternalTokens(response.accessToken, response.refreshToken);
|
||||
this.initializeWebSocketConnection();
|
||||
}
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
saveInternalTokens(accessToken: string, refreshToken: string): void {
|
||||
localStorage.setItem('accessToken_Internal', accessToken);
|
||||
localStorage.setItem('refreshToken_Internal', refreshToken);
|
||||
@@ -81,13 +68,8 @@ export class AuthService {
|
||||
localStorage.removeItem('accessToken_Internal');
|
||||
localStorage.removeItem('refreshToken_Internal');
|
||||
this.tokenSubject.next(null);
|
||||
this.logoutSubject.next();
|
||||
this.getRxStompService().deactivate();
|
||||
if (this.oAuthService.clientId) {
|
||||
this.oAuthService.logOut();
|
||||
} else {
|
||||
this.router.navigate(['/login']);
|
||||
}
|
||||
this.router.navigate(['/login']);
|
||||
}
|
||||
|
||||
getRxStompService(): RxStompService {
|
||||
|
||||
@@ -1,26 +1,28 @@
|
||||
import { Injectable, inject } from '@angular/core';
|
||||
import { AuthService } from './auth.service';
|
||||
import { UserService } from '../../settings/user-management/user.service';
|
||||
import { filter, catchError } from 'rxjs/operators';
|
||||
import { of } from 'rxjs';
|
||||
import {Injectable, inject} from '@angular/core';
|
||||
import {AuthService} from './auth.service';
|
||||
import {UserService} from '../../settings/user-management/user.service';
|
||||
import {filter, catchError} from 'rxjs/operators';
|
||||
import {of} from 'rxjs';
|
||||
import {OAuthService} from 'angular-oauth2-oidc';
|
||||
|
||||
@Injectable({ providedIn: 'root' })
|
||||
@Injectable({providedIn: 'root'})
|
||||
export class StartupService {
|
||||
private auth = inject(AuthService);
|
||||
private userSvc = inject(UserService);
|
||||
private authService = inject(AuthService);
|
||||
private userService = inject(UserService);
|
||||
|
||||
load(): Promise<void> {
|
||||
this.auth.token$
|
||||
this.authService.token$
|
||||
.pipe(filter(t => !!t))
|
||||
.subscribe(() => {
|
||||
this.userSvc.getMyself()
|
||||
this.userService.getMyself()
|
||||
.pipe(catchError(() => of(null)))
|
||||
.subscribe(user => {
|
||||
if (user) {
|
||||
this.userSvc.setInitialUser(user);
|
||||
this.userService.setInitialUser(user);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
return Promise.resolve();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
name: booklore
|
||||
services:
|
||||
booklore:
|
||||
image: ghcr.io/adityachandelgit/booklore-app:${BOOKLORE_IMAGE_TAG}
|
||||
image: booklore/booklore:${BOOKLORE_IMAGE_TAG}
|
||||
container_name: booklore_server
|
||||
env_file:
|
||||
- .env
|
||||
@@ -30,4 +30,4 @@ services:
|
||||
test: ["CMD", "mariadb-admin", "ping", "-h", "localhost"]
|
||||
interval: 10s
|
||||
timeout: 5s
|
||||
retries: 5
|
||||
retries: 5
|
||||
|
||||
Reference in New Issue
Block a user