Merge branch 'develop' into feat/auto-set-paths

This commit is contained in:
Ionuț Staicu
2025-08-13 15:06:21 +03:00
committed by GitHub
13 changed files with 101 additions and 100 deletions

View File

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

View File

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

View File

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

View File

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

View File

@@ -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"

View File

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

View File

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

View File

@@ -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]

View File

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

View File

@@ -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'

View File

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

View File

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

View File

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