diff --git a/API/Controllers/SettingsController.cs b/API/Controllers/SettingsController.cs index d3075de1a..a7896a568 100644 --- a/API/Controllers/SettingsController.cs +++ b/API/Controllers/SettingsController.cs @@ -85,6 +85,17 @@ namespace API.Controllers _unitOfWork.SettingsRepository.Update(setting); } + if (setting.Key == ServerSettingKey.BaseUrl && updateSettingsDto.BaseUrl + string.Empty != setting.Value) + { + var path = !updateSettingsDto.BaseUrl.StartsWith("/") + ? $"/{updateSettingsDto.BaseUrl}" + : updateSettingsDto.BaseUrl; + setting.Value = path; + // BaseUrl is managed in appSetting.json + Configuration.BaseUrl = updateSettingsDto.BaseUrl; + _unitOfWork.SettingsRepository.Update(setting); + } + if (setting.Key == ServerSettingKey.LoggingLevel && updateSettingsDto.LoggingLevel + string.Empty != setting.Value) { setting.Value = updateSettingsDto.LoggingLevel + string.Empty; diff --git a/API/DTOs/Settings/ServerSettingDTO.cs b/API/DTOs/Settings/ServerSettingDTO.cs index 0876fbfa0..957270b8b 100644 --- a/API/DTOs/Settings/ServerSettingDTO.cs +++ b/API/DTOs/Settings/ServerSettingDTO.cs @@ -26,5 +26,9 @@ /// Enables Authentication on the server. Defaults to true. /// public bool EnableAuthentication { get; set; } + /// + /// Base Url for the kavita. Defaults to "/". Managed in appsettings.json.Requires restart to take effect. + /// + public string BaseUrl { get; set; } = "/"; } } diff --git a/API/Data/Seed.cs b/API/Data/Seed.cs index 7bc438ab5..f1b9d6cc5 100644 --- a/API/Data/Seed.cs +++ b/API/Data/Seed.cs @@ -50,6 +50,7 @@ namespace API.Data new () {Key = ServerSettingKey.AllowStatCollection, Value = "true"}, new () {Key = ServerSettingKey.EnableOpds, Value = "false"}, new () {Key = ServerSettingKey.EnableAuthentication, Value = "true"}, + new () {Key = ServerSettingKey.BaseUrl, Value = ""},// Not used from DB, but DB is sync with appSettings.json }; foreach (var defaultSetting in defaultSettings) @@ -63,11 +64,20 @@ namespace API.Data await context.SaveChangesAsync(); + if (string.IsNullOrEmpty(Configuration.BaseUrl)) + { + Configuration.BaseUrl = "/"; + } + // Port and LoggingLevel are managed in appSettings.json. Update the DB values to match context.ServerSetting.First(s => s.Key == ServerSettingKey.Port).Value = Configuration.Port + string.Empty; context.ServerSetting.First(s => s.Key == ServerSettingKey.LoggingLevel).Value = Configuration.LogLevel + string.Empty; + context.ServerSetting.First(s => s.Key == ServerSettingKey.BaseUrl).Value = + Configuration.BaseUrl; + + await context.SaveChangesAsync(); diff --git a/API/Entities/Enums/ServerSettingKey.cs b/API/Entities/Enums/ServerSettingKey.cs index cbf68f013..997d0a33e 100644 --- a/API/Entities/Enums/ServerSettingKey.cs +++ b/API/Entities/Enums/ServerSettingKey.cs @@ -21,7 +21,9 @@ namespace API.Entities.Enums [Description("EnableOpds")] EnableOpds = 7, [Description("EnableAuthentication")] - EnableAuthentication = 8 + EnableAuthentication = 8, + [Description("BaseUrl")] + BaseUrl = 9 } } diff --git a/API/Helpers/Converters/ServerSettingConverter.cs b/API/Helpers/Converters/ServerSettingConverter.cs index 445e92ddb..86ed6235e 100644 --- a/API/Helpers/Converters/ServerSettingConverter.cs +++ b/API/Helpers/Converters/ServerSettingConverter.cs @@ -39,6 +39,9 @@ namespace API.Helpers.Converters case ServerSettingKey.EnableAuthentication: destination.EnableAuthentication = bool.Parse(row.Value); break; + case ServerSettingKey.BaseUrl: + destination.BaseUrl = row.Value; + break; } } diff --git a/API/Startup.cs b/API/Startup.cs index 3e6e5c659..ee309d537 100644 --- a/API/Startup.cs +++ b/API/Startup.cs @@ -160,11 +160,23 @@ namespace API app.UseDefaultFiles(); + if (!string.IsNullOrEmpty(Configuration.BaseUrl)) + { + var path = !Configuration.BaseUrl.StartsWith("/") + ? $"/{Configuration.BaseUrl}" + : Configuration.BaseUrl; + app.UsePathBase(path); + Console.WriteLine("Starting with base url as " + path); + } + app.UseStaticFiles(new StaticFileOptions { ContentTypeProvider = new FileExtensionContentTypeProvider() }); + + + app.Use(async (context, next) => { context.Response.GetTypedHeaders().CacheControl = diff --git a/API/appsettings.Development.json b/API/appsettings.Development.json index b5dc22df1..14d557be3 100644 --- a/API/appsettings.Development.json +++ b/API/appsettings.Development.json @@ -18,5 +18,7 @@ "MaxRollingFiles": 5 } }, - "Port": 5000 + "Port": 5000, + "BaseUrl": "/" + } diff --git a/Kavita.Common/Configuration.cs b/Kavita.Common/Configuration.cs index c2967c883..0c5fdfc95 100644 --- a/Kavita.Common/Configuration.cs +++ b/Kavita.Common/Configuration.cs @@ -8,7 +8,7 @@ namespace Kavita.Common { public static class Configuration { - private static readonly string AppSettingsFilename = GetAppSettingFilename(); + private static string AppSettingsFilename = GetAppSettingFilename(); public static string Branch { get => GetBranch(GetAppSettingFilename()); @@ -33,6 +33,12 @@ namespace Kavita.Common set => SetLogLevel(GetAppSettingFilename(), value); } + public static string BaseUrl + { + get => GetBaseUrl(GetAppSettingFilename()); + set => SetBaseUrl(GetAppSettingFilename(), value); + } + private static string GetAppSettingFilename() { if (!string.IsNullOrEmpty(AppSettingsFilename)) @@ -151,6 +157,55 @@ namespace Kavita.Common #endregion + #region BaseUrl + private static string GetBaseUrl(string filePath) + { + if (new OsInfo(Array.Empty()).IsDocker) + { + return "/"; + } + + try + { + var json = File.ReadAllText(filePath); + var jsonObj = JsonSerializer.Deserialize(json); + const string key = "BaseUrl"; + + if (jsonObj.TryGetProperty(key, out JsonElement tokenElement)) + { + return tokenElement.GetString(); + } + } + catch (Exception ex) + { + Console.WriteLine("Error reading app settings: " + ex.Message); + } + + return "/"; + } + + private static void SetBaseUrl(string filePath, string value) + { + if (new OsInfo(Array.Empty()).IsDocker) + { + return; + } + + var currentBaseUrl = GetBaseUrl(filePath); + var json = File.ReadAllText(filePath); + if (!json.Contains("BaseUrl")) + { + var lastBracket = json.LastIndexOf("}", StringComparison.Ordinal) - 1; + json = (json.Substring(0, lastBracket) + (",\n \"BaseUrl\": " + currentBaseUrl) + "}"); + } + else + { + json = json.Replace("\"BaseUrl\": " + currentBaseUrl, "\"BaseUrl\": " + value); + } + File.WriteAllText(filePath, json); + } + #endregion + #region LogLevel private static void SetLogLevel(string filePath, string logLevel) diff --git a/UI/Web/src/app/admin/_models/server-settings.ts b/UI/Web/src/app/admin/_models/server-settings.ts index f21b886d1..fbcb2a0f0 100644 --- a/UI/Web/src/app/admin/_models/server-settings.ts +++ b/UI/Web/src/app/admin/_models/server-settings.ts @@ -7,4 +7,5 @@ export interface ServerSettings { allowStatCollection: boolean; enableOpds: boolean; enableAuthentication: boolean; + baseUrl: string; } diff --git a/UI/Web/src/app/admin/manage-library/manage-library.component.ts b/UI/Web/src/app/admin/manage-library/manage-library.component.ts index 7160bd758..904c6f584 100644 --- a/UI/Web/src/app/admin/manage-library/manage-library.component.ts +++ b/UI/Web/src/app/admin/manage-library/manage-library.component.ts @@ -2,7 +2,7 @@ import { Component, OnDestroy, OnInit } from '@angular/core'; import { NgbModal } from '@ng-bootstrap/ng-bootstrap'; import { ToastrService } from 'ngx-toastr'; import { Subject } from 'rxjs'; -import { take, takeUntil, takeWhile } from 'rxjs/operators'; +import { take, takeUntil } from 'rxjs/operators'; import { ConfirmService } from 'src/app/shared/confirm.service'; import { ScanLibraryProgressEvent } from 'src/app/_models/events/scan-library-progress-event'; import { Library, LibraryType } from 'src/app/_models/library'; diff --git a/UI/Web/src/app/admin/manage-settings/manage-settings.component.html b/UI/Web/src/app/admin/manage-settings/manage-settings.component.html index eb7cc14ac..4467bcb6d 100644 --- a/UI/Web/src/app/admin/manage-settings/manage-settings.component.html +++ b/UI/Web/src/app/admin/manage-settings/manage-settings.component.html @@ -1,6 +1,6 @@ - Port and Logging Level require a manual restart of Kavita to take effect. + Port, Base Url, and Logging Level require a manual restart of Kavita to take effect. Cache Directory Where the server place temporary files when reading. This will be cleaned up on a regular basis. @@ -8,6 +8,13 @@ + + Base Url + Use this if you want to host Kavita on a base url ie) yourdomain.com/kavita + Use this if you want to host Kavita on a base url ie) yourdomain.com/kavita + + + Port Port the server listens on. This is fixed if you are running on Docker. Requires restart to take effect. diff --git a/UI/Web/src/app/admin/manage-settings/manage-settings.component.ts b/UI/Web/src/app/admin/manage-settings/manage-settings.component.ts index ab5a71c0c..c05464695 100644 --- a/UI/Web/src/app/admin/manage-settings/manage-settings.component.ts +++ b/UI/Web/src/app/admin/manage-settings/manage-settings.component.ts @@ -37,6 +37,7 @@ export class ManageSettingsComponent implements OnInit { this.settingsForm.addControl('allowStatCollection', new FormControl(this.serverSettings.allowStatCollection, [Validators.required])); this.settingsForm.addControl('enableOpds', new FormControl(this.serverSettings.enableOpds, [Validators.required])); this.settingsForm.addControl('enableAuthentication', new FormControl(this.serverSettings.enableAuthentication, [Validators.required])); + this.settingsForm.addControl('baseUrl', new FormControl(this.serverSettings.baseUrl, [Validators.required])); }); } @@ -49,6 +50,7 @@ export class ManageSettingsComponent implements OnInit { this.settingsForm.get('allowStatCollection')?.setValue(this.serverSettings.allowStatCollection); this.settingsForm.get('enableOpds')?.setValue(this.serverSettings.enableOpds); this.settingsForm.get('enableAuthentication')?.setValue(this.serverSettings.enableAuthentication); + this.settingsForm.get('baseUrl')?.setValue(this.serverSettings.baseUrl); } async saveSettings() { diff --git a/UI/Web/src/app/app.module.ts b/UI/Web/src/app/app.module.ts index 21a4c6ae4..76eb54f6e 100644 --- a/UI/Web/src/app/app.module.ts +++ b/UI/Web/src/app/app.module.ts @@ -1,11 +1,12 @@ import { BrowserModule, Title } from '@angular/platform-browser'; import { APP_INITIALIZER, ErrorHandler, NgModule } from '@angular/core'; +import { APP_BASE_HREF } from '@angular/common'; import { AppRoutingModule } from './app-routing.module'; import { AppComponent } from './app.component'; import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; import { FormsModule, ReactiveFormsModule } from '@angular/forms'; -import { HttpClientModule, HTTP_INTERCEPTORS } from '@angular/common/http'; +import { HttpClient, HttpClientModule, HTTP_INTERCEPTORS } from '@angular/common/http'; import { NgbCollapseModule, NgbDropdownModule, NgbNavModule, NgbPaginationModule, NgbRatingModule } from '@ng-bootstrap/ng-bootstrap'; import { NavHeaderComponent } from './nav-header/nav-header.component'; import { JwtInterceptor } from './_interceptors/jwt.interceptor'; @@ -24,12 +25,12 @@ import { CarouselModule } from './carousel/carousel.module'; import { PersonBadgeComponent } from './person-badge/person-badge.component'; import { TypeaheadModule } from './typeahead/typeahead.module'; import { RecentlyAddedComponent } from './recently-added/recently-added.component'; +import { InProgressComponent } from './in-progress/in-progress.component'; +import { DashboardComponent } from './dashboard/dashboard.component'; import { CardsModule } from './cards/cards.module'; import { CollectionsModule } from './collections/collections.module'; -import { InProgressComponent } from './in-progress/in-progress.component'; -import { SAVER, getSaver } from './shared/_providers/saver.provider'; import { ReadingListModule } from './reading-list/reading-list.module'; -import { DashboardComponent } from './dashboard/dashboard.component'; +import { SAVER, getSaver } from './shared/_providers/saver.provider'; @NgModule({ declarations: [ @@ -81,7 +82,8 @@ import { DashboardComponent } from './dashboard/dashboard.component'; {provide: HTTP_INTERCEPTORS, useClass: ErrorInterceptor, multi: true}, {provide: HTTP_INTERCEPTORS, useClass: JwtInterceptor, multi: true}, Title, - {provide: SAVER, useFactory: getSaver} + {provide: SAVER, useFactory: getSaver}, + { provide: APP_BASE_HREF, useValue: window['_app_base' as keyof Window] || '/' }, ], entryComponents: [], bootstrap: [AppComponent] diff --git a/UI/Web/src/index.html b/UI/Web/src/index.html index 0b9f60c62..834629c97 100644 --- a/UI/Web/src/index.html +++ b/UI/Web/src/index.html @@ -40,4 +40,9 @@ Please enable JavaScript to continue using this application.
Port and Logging Level require a manual restart of Kavita to take effect.
Port, Base Url, and Logging Level require a manual restart of Kavita to take effect.