Base Url Support (#642)

* Added base url config

* UI side is not working

* Working base url more

* Attempt to get UI to work with base url

* Implemented the ability to set the Base URL for the app

* Hooked in Base URL as a managed setting

* Ensure we always start with / for base url

* Removed default base href from debug builds. Cleaned up an issue with base url migration.

* Fixed an issue with our BaseURL migration
This commit is contained in:
Joseph Milazzo 2021-10-06 05:42:02 -07:00 committed by GitHub
parent 56239ee0a7
commit 27aaa040c4
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
14 changed files with 126 additions and 10 deletions

View File

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

View File

@ -26,5 +26,9 @@
/// Enables Authentication on the server. Defaults to true.
/// </summary>
public bool EnableAuthentication { get; set; }
/// <summary>
/// Base Url for the kavita. Defaults to "/". Managed in appsettings.json.Requires restart to take effect.
/// </summary>
public string BaseUrl { get; set; } = "/";
}
}

View File

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

View File

@ -21,7 +21,9 @@ namespace API.Entities.Enums
[Description("EnableOpds")]
EnableOpds = 7,
[Description("EnableAuthentication")]
EnableAuthentication = 8
EnableAuthentication = 8,
[Description("BaseUrl")]
BaseUrl = 9
}
}

View File

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

View File

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

View File

@ -18,5 +18,7 @@
"MaxRollingFiles": 5
}
},
"Port": 5000
"Port": 5000,
"BaseUrl": "/"
}

View File

@ -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<IOsVersionAdapter>()).IsDocker)
{
return "/";
}
try
{
var json = File.ReadAllText(filePath);
var jsonObj = JsonSerializer.Deserialize<dynamic>(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<IOsVersionAdapter>()).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)

View File

@ -7,4 +7,5 @@ export interface ServerSettings {
allowStatCollection: boolean;
enableOpds: boolean;
enableAuthentication: boolean;
baseUrl: string;
}

View File

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

View File

@ -1,6 +1,6 @@
<div class="container-fluid">
<form [formGroup]="settingsForm" *ngIf="serverSettings !== undefined">
<p class="text-warning pt-2">Port and Logging Level require a manual restart of Kavita to take effect.</p>
<p class="text-warning pt-2">Port, Base Url, and Logging Level require a manual restart of Kavita to take effect.</p>
<div class="form-group">
<label for="settings-cachedir">Cache Directory</label>&nbsp;<i class="fa fa-info-circle" placement="right" [ngbTooltip]="cacheDirectoryTooltip" role="button" tabindex="0"></i>
<ng-template #cacheDirectoryTooltip>Where the server place temporary files when reading. This will be cleaned up on a regular basis.</ng-template>
@ -8,6 +8,13 @@
<input readonly id="settings-cachedir" aria-describedby="settings-cachedir-help" class="form-control" formControlName="cacheDirectory" type="text">
</div>
<div class="form-group">
<label for="settings-baseurl">Base Url</label>&nbsp;<i class="fa fa-info-circle" placement="right" [ngbTooltip]="baseUrlTooltip" role="button" tabindex="0"></i>
<ng-template #baseUrlTooltip>Use this if you want to host Kavita on a base url ie) yourdomain.com/kavita</ng-template>
<span class="sr-only" id="settings-baseurl-help">Use this if you want to host Kavita on a base url ie) yourdomain.com/kavita</span>
<input id="settings-baseurl" aria-describedby="settings-baseurl-help" class="form-control" formControlName="baseUrl" type="text">
</div>
<div class="form-group">
<label for="settings-port">Port</label>&nbsp;<i class="fa fa-info-circle" placement="right" [ngbTooltip]="portTooltip" role="button" tabindex="0"></i>
<ng-template #portTooltip>Port the server listens on. This is fixed if you are running on Docker. Requires restart to take effect.</ng-template>

View File

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

View File

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

View File

@ -40,4 +40,9 @@
<app-root></app-root>
<noscript>Please enable JavaScript to continue using this application.</noscript>
</body>
<script>
(function() {
window['_app_base'] = '/' + window.location.pathname.split('/')[1];
})();
</script>
</html>