mirror of
https://github.com/adityachandelgit/BookLore.git
synced 2026-03-16 16:42:08 -05:00
Expose safe public app settings via dedicated endpoint
This commit is contained in:
committed by
Aditya Chandel
parent
f5e58080ea
commit
7cea853756
+1
-1
@@ -47,7 +47,7 @@ public class SecurityConfig {
|
||||
private static final String[] COMMON_PUBLIC_ENDPOINTS = {
|
||||
"/ws/**",
|
||||
"/api/v1/auth/**",
|
||||
"/api/v1/settings",
|
||||
"/api/v1/public-settings",
|
||||
"/api/v1/setup/**",
|
||||
"/api/v1/books/*/cover",
|
||||
"/api/v1/books/*/backup-cover",
|
||||
|
||||
+23
@@ -0,0 +1,23 @@
|
||||
package com.adityachandel.booklore.controller;
|
||||
|
||||
import com.adityachandel.booklore.model.dto.settings.PublicAppSetting;
|
||||
import com.adityachandel.booklore.service.appsettings.AppSettingService;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/api/v1/public-settings")
|
||||
@RequiredArgsConstructor
|
||||
public class PublicAppSettingController {
|
||||
|
||||
private final AppSettingService appSettingService;
|
||||
|
||||
@GetMapping
|
||||
public List<PublicAppSetting> getPublicSettings() {
|
||||
return appSettingService.getPublicSettings();
|
||||
}
|
||||
}
|
||||
+24
-22
@@ -4,35 +4,37 @@ import lombok.Getter;
|
||||
|
||||
@Getter
|
||||
public enum AppSettingKey {
|
||||
QUICK_BOOK_MATCH("quick_book_match", true),
|
||||
OIDC_PROVIDER_DETAILS("oidc_provider_details", true),
|
||||
OIDC_AUTO_PROVISION_DETAILS("oidc_auto_provision_details", true),
|
||||
SIDEBAR_LIBRARY_SORTING("sidebar_library_sorting", true),
|
||||
SIDEBAR_SHELF_SORTING("sidebar_shelf_sorting", true),
|
||||
METADATA_PROVIDER_SETTINGS("metadata_provider_settings", true),
|
||||
METADATA_MATCH_WEIGHTS("metadata_match_weights", true),
|
||||
METADATA_PERSISTENCE_SETTINGS("metadata_persistence_settings", true),
|
||||
METADATA_PUBLIC_REVIEWS_SETTINGS("metadata_public_reviews_settings", true),
|
||||
QUICK_BOOK_MATCH("quick_book_match", true, false),
|
||||
OIDC_PROVIDER_DETAILS("oidc_provider_details", true, true),
|
||||
OIDC_AUTO_PROVISION_DETAILS("oidc_auto_provision_details", true, false),
|
||||
SIDEBAR_LIBRARY_SORTING("sidebar_library_sorting", true, false),
|
||||
SIDEBAR_SHELF_SORTING("sidebar_shelf_sorting", true, false),
|
||||
METADATA_PROVIDER_SETTINGS("metadata_provider_settings", true, false),
|
||||
METADATA_MATCH_WEIGHTS("metadata_match_weights", true, false),
|
||||
METADATA_PERSISTENCE_SETTINGS("metadata_persistence_settings", true, false),
|
||||
METADATA_PUBLIC_REVIEWS_SETTINGS("metadata_public_reviews_settings", true, false),
|
||||
|
||||
AUTO_BOOK_SEARCH("auto_book_search", false),
|
||||
COVER_IMAGE_RESOLUTION("cover_image_resolution", false),
|
||||
SIMILAR_BOOK_RECOMMENDATION("similar_book_recommendation", false),
|
||||
UPLOAD_FILE_PATTERN("upload_file_pattern", false),
|
||||
MOVE_FILE_PATTERN("move_file_pattern", false),
|
||||
OPDS_SERVER_ENABLED("opds_server_enabled", false),
|
||||
OIDC_ENABLED("oidc_enabled", false),
|
||||
CBX_CACHE_SIZE_IN_MB("cbx_cache_size_in_mb", false),
|
||||
PDF_CACHE_SIZE_IN_MB("pdf_cache_size_in_mb", false),
|
||||
BOOK_DELETION_ENABLED("book_deletion_enabled", false),
|
||||
METADATA_DOWNLOAD_ON_BOOKDROP("metadata_download_on_bookdrop", false),
|
||||
MAX_FILE_UPLOAD_SIZE_IN_MB("max_file_upload_size_in_mb", false);
|
||||
AUTO_BOOK_SEARCH("auto_book_search", false, false),
|
||||
COVER_IMAGE_RESOLUTION("cover_image_resolution", false, false),
|
||||
SIMILAR_BOOK_RECOMMENDATION("similar_book_recommendation", false, false),
|
||||
UPLOAD_FILE_PATTERN("upload_file_pattern", false, false),
|
||||
MOVE_FILE_PATTERN("move_file_pattern", false, false),
|
||||
OPDS_SERVER_ENABLED("opds_server_enabled", false, false),
|
||||
OIDC_ENABLED("oidc_enabled", false, true),
|
||||
CBX_CACHE_SIZE_IN_MB("cbx_cache_size_in_mb", false, false),
|
||||
PDF_CACHE_SIZE_IN_MB("pdf_cache_size_in_mb", false, false),
|
||||
BOOK_DELETION_ENABLED("book_deletion_enabled", false, false),
|
||||
METADATA_DOWNLOAD_ON_BOOKDROP("metadata_download_on_bookdrop", false, false),
|
||||
MAX_FILE_UPLOAD_SIZE_IN_MB("max_file_upload_size_in_mb", false, false);
|
||||
|
||||
private final String dbKey;
|
||||
private final boolean isJson;
|
||||
private final boolean isPublic;
|
||||
|
||||
AppSettingKey(String dbKey, boolean isJson) {
|
||||
AppSettingKey(String dbKey, boolean isJson, boolean isPublic) {
|
||||
this.dbKey = dbKey;
|
||||
this.isJson = isJson;
|
||||
this.isPublic = isPublic;
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
+11
@@ -0,0 +1,11 @@
|
||||
package com.adityachandel.booklore.model.dto.settings;
|
||||
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Getter;
|
||||
|
||||
@Getter
|
||||
@AllArgsConstructor
|
||||
public class PublicAppSetting {
|
||||
private final String key;
|
||||
private final Object value;
|
||||
}
|
||||
+12
@@ -9,6 +9,8 @@ import jakarta.transaction.Transactional;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.locks.ReentrantLock;
|
||||
import java.util.stream.Collectors;
|
||||
@@ -49,6 +51,16 @@ public class AppSettingService {
|
||||
refreshCache();
|
||||
}
|
||||
|
||||
public List<PublicAppSetting> getPublicSettings() {
|
||||
return Arrays.stream(AppSettingKey.values())
|
||||
.filter(AppSettingKey::isPublic)
|
||||
.map(key -> new PublicAppSetting(
|
||||
key.toString(),
|
||||
settingPersistenceHelper.getOrCreateSetting(key, null)
|
||||
))
|
||||
.toList();
|
||||
}
|
||||
|
||||
private void refreshCache() {
|
||||
lock.lock();
|
||||
try {
|
||||
|
||||
@@ -1,24 +1,21 @@
|
||||
import {inject} from '@angular/core';
|
||||
import {Router} from '@angular/router';
|
||||
import {OAuthEvent, OAuthService} from 'angular-oauth2-oidc';
|
||||
import {AppSettingsService} from './core/service/app-settings.service';
|
||||
import {OAuthService} from 'angular-oauth2-oidc';
|
||||
import {AuthService, websocketInitializer} from './core/service/auth.service';
|
||||
import {filter} from 'rxjs/operators';
|
||||
import {AuthInitializationService} from './auth-initialization-service';
|
||||
import {PublicAppSettingService} from './public-app-settings.service';
|
||||
|
||||
export function initializeAuthFactory() {
|
||||
return () => {
|
||||
const oauthService = inject(OAuthService);
|
||||
const appSettingsService = inject(AppSettingsService);
|
||||
const publicAppSettingService = inject(PublicAppSettingService);
|
||||
const authService = inject(AuthService);
|
||||
const router = inject(Router);
|
||||
const authInitService = inject(AuthInitializationService);
|
||||
|
||||
return new Promise<void>((resolve) => {
|
||||
const sub = appSettingsService.appSettings$.subscribe(settings => {
|
||||
if (settings) {
|
||||
if (settings.oidcEnabled && settings.oidcProviderDetails) {
|
||||
const details = settings.oidcProviderDetails;
|
||||
const sub = publicAppSettingService.publicAppSettings$.subscribe(publicSettings => {
|
||||
if (publicSettings) {
|
||||
if (publicSettings.oidcEnabled && publicSettings.oidcProviderDetails) {
|
||||
const details = publicSettings.oidcProviderDetails;
|
||||
|
||||
oauthService.configure({
|
||||
issuer: details.issuerUri,
|
||||
|
||||
@@ -11,6 +11,7 @@ import {AppSettingsService} from '../../service/app-settings.service';
|
||||
import {AppSettings} from '../../model/app-settings.model';
|
||||
import {Observable} from 'rxjs';
|
||||
import {filter, take} from 'rxjs/operators';
|
||||
import {PublicAppSettings, PublicAppSettingService} from '../../../public-app-settings.service';
|
||||
|
||||
@Component({
|
||||
selector: 'app-login',
|
||||
@@ -33,20 +34,20 @@ export class LoginComponent implements OnInit {
|
||||
|
||||
private authService = inject(AuthService);
|
||||
private oAuthService = inject(OAuthService);
|
||||
private appSettingsService = inject(AppSettingsService);
|
||||
private appSettingsService = inject(PublicAppSettingService);
|
||||
private router = inject(Router);
|
||||
|
||||
appSettings$: Observable<AppSettings | null> = this.appSettingsService.appSettings$;
|
||||
publicAppSettings$: Observable<PublicAppSettings | null> = this.appSettingsService.publicAppSettings$;
|
||||
|
||||
ngOnInit(): void {
|
||||
this.appSettings$
|
||||
this.publicAppSettings$
|
||||
.pipe(
|
||||
filter(settings => settings != null),
|
||||
take(1)
|
||||
)
|
||||
.subscribe(settings => {
|
||||
this.oidcEnabled = settings!.oidcEnabled;
|
||||
this.oidcName = settings!.oidcProviderDetails?.providerName || 'OIDC';
|
||||
.subscribe(publicSettings => {
|
||||
this.oidcEnabled = publicSettings!.oidcEnabled;
|
||||
this.oidcName = publicSettings!.oidcProviderDetails?.providerName || 'OIDC';
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -1,44 +1,53 @@
|
||||
import {Injectable} from '@angular/core';
|
||||
import {inject, Injectable} from '@angular/core';
|
||||
import {HttpClient} from '@angular/common/http';
|
||||
import {BehaviorSubject, Observable, of} from 'rxjs';
|
||||
import {AppSettings} from '../model/app-settings.model';
|
||||
import {API_CONFIG} from '../../config/api-config';
|
||||
import {catchError, map} from 'rxjs/operators';
|
||||
import {catchError, finalize, shareReplay, tap} from 'rxjs/operators';
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root'
|
||||
})
|
||||
@Injectable({providedIn: 'root'})
|
||||
export class AppSettingsService {
|
||||
private http = inject(HttpClient);
|
||||
private readonly apiUrl = `${API_CONFIG.BASE_URL}/api/v1/settings`;
|
||||
|
||||
private loading$: Observable<AppSettings> | null = null;
|
||||
private appSettingsSubject = new BehaviorSubject<AppSettings | null>(null);
|
||||
appSettings$ = this.appSettingsSubject.asObservable();
|
||||
|
||||
constructor(private http: HttpClient) {
|
||||
this.loadAppSettings();
|
||||
}
|
||||
|
||||
loadAppSettings(): void {
|
||||
this.http.get<AppSettings>(this.apiUrl).subscribe({
|
||||
next: (settings: AppSettings) => {
|
||||
this.appSettingsSubject.next(settings);
|
||||
},
|
||||
error: (error) => {
|
||||
console.error('Error loading app settings:', error);
|
||||
this.appSettingsSubject.next(null);
|
||||
appSettings$ = this.appSettingsSubject.asObservable().pipe(
|
||||
tap(state => {
|
||||
if (!state && !this.loading$) {
|
||||
this.loading$ = this.fetchAppSettings().pipe(
|
||||
shareReplay(1),
|
||||
finalize(() => (this.loading$ = null))
|
||||
);
|
||||
this.loading$.subscribe();
|
||||
}
|
||||
});
|
||||
})
|
||||
);
|
||||
|
||||
private fetchAppSettings(): Observable<AppSettings> {
|
||||
return this.http.get<AppSettings>(this.apiUrl).pipe(
|
||||
tap(settings => this.appSettingsSubject.next(settings)),
|
||||
catchError(err => {
|
||||
console.error('Error loading app settings:', err);
|
||||
this.appSettingsSubject.next(null);
|
||||
throw err;
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
saveSettings(settings: { key: string, newValue: any }[]): Observable<void> {
|
||||
saveSettings(settings: { key: string; newValue: any }[]): Observable<void> {
|
||||
const payload = settings.map(setting => ({
|
||||
name: setting.key,
|
||||
value: setting.newValue
|
||||
}));
|
||||
|
||||
return this.http.put<void>(this.apiUrl, payload).pipe(
|
||||
map(() => {
|
||||
this.loadAppSettings();
|
||||
tap(() => {
|
||||
this.loading$ = this.fetchAppSettings().pipe(
|
||||
shareReplay(1),
|
||||
finalize(() => (this.loading$ = null))
|
||||
);
|
||||
this.loading$.subscribe();
|
||||
}),
|
||||
catchError(err => {
|
||||
console.error('Error saving settings:', err);
|
||||
|
||||
@@ -0,0 +1,42 @@
|
||||
import {inject, Injectable} from '@angular/core';
|
||||
import {API_CONFIG} from './config/api-config';
|
||||
import {BehaviorSubject, Observable} from 'rxjs';
|
||||
import {HttpClient} from '@angular/common/http';
|
||||
import {OidcProviderDetails} from './core/model/app-settings.model';
|
||||
import {catchError, finalize, shareReplay, tap} from 'rxjs/operators';
|
||||
|
||||
export interface PublicAppSettings {
|
||||
oidcEnabled: boolean;
|
||||
oidcProviderDetails: OidcProviderDetails;
|
||||
}
|
||||
|
||||
@Injectable({providedIn: 'root'})
|
||||
export class PublicAppSettingService {
|
||||
private http = inject(HttpClient);
|
||||
private readonly url = `${API_CONFIG.BASE_URL}/api/v1/public-settings`;
|
||||
|
||||
private loading$: Observable<PublicAppSettings> | null = null;
|
||||
private publicAppSettingsSubject = new BehaviorSubject<PublicAppSettings | null>(null);
|
||||
|
||||
publicAppSettings$ = this.publicAppSettingsSubject.asObservable().pipe(
|
||||
tap(state => {
|
||||
if (!state && !this.loading$) {
|
||||
this.loading$ = this.fetchPublicSettings().pipe(
|
||||
shareReplay(1),
|
||||
finalize(() => (this.loading$ = null))
|
||||
);
|
||||
this.loading$.subscribe();
|
||||
}
|
||||
})
|
||||
);
|
||||
|
||||
private fetchPublicSettings(): Observable<PublicAppSettings> {
|
||||
return this.http.get<PublicAppSettings>(this.url).pipe(
|
||||
tap(settings => this.publicAppSettingsSubject.next(settings)),
|
||||
catchError(err => {
|
||||
console.error('Failed to fetch public settings', err);
|
||||
throw err;
|
||||
})
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user