mirror of
https://github.com/Kareadita/Kavita.git
synced 2025-07-09 03:04:19 -04:00
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:
parent
56239ee0a7
commit
27aaa040c4
@ -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;
|
||||
|
@ -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; } = "/";
|
||||
}
|
||||
}
|
||||
|
@ -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();
|
||||
|
||||
|
@ -21,7 +21,9 @@ namespace API.Entities.Enums
|
||||
[Description("EnableOpds")]
|
||||
EnableOpds = 7,
|
||||
[Description("EnableAuthentication")]
|
||||
EnableAuthentication = 8
|
||||
EnableAuthentication = 8,
|
||||
[Description("BaseUrl")]
|
||||
BaseUrl = 9
|
||||
|
||||
}
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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 =
|
||||
|
@ -18,5 +18,7 @@
|
||||
"MaxRollingFiles": 5
|
||||
}
|
||||
},
|
||||
"Port": 5000
|
||||
"Port": 5000,
|
||||
"BaseUrl": "/"
|
||||
|
||||
}
|
||||
|
@ -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)
|
||||
|
@ -7,4 +7,5 @@ export interface ServerSettings {
|
||||
allowStatCollection: boolean;
|
||||
enableOpds: boolean;
|
||||
enableAuthentication: boolean;
|
||||
baseUrl: string;
|
||||
}
|
||||
|
@ -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';
|
||||
|
@ -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> <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> <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> <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>
|
||||
|
@ -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() {
|
||||
|
@ -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]
|
||||
|
@ -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>
|
||||
|
Loading…
x
Reference in New Issue
Block a user