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);
|
_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)
|
if (setting.Key == ServerSettingKey.LoggingLevel && updateSettingsDto.LoggingLevel + string.Empty != setting.Value)
|
||||||
{
|
{
|
||||||
setting.Value = updateSettingsDto.LoggingLevel + string.Empty;
|
setting.Value = updateSettingsDto.LoggingLevel + string.Empty;
|
||||||
|
@ -26,5 +26,9 @@
|
|||||||
/// Enables Authentication on the server. Defaults to true.
|
/// Enables Authentication on the server. Defaults to true.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public bool EnableAuthentication { get; set; }
|
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.AllowStatCollection, Value = "true"},
|
||||||
new () {Key = ServerSettingKey.EnableOpds, Value = "false"},
|
new () {Key = ServerSettingKey.EnableOpds, Value = "false"},
|
||||||
new () {Key = ServerSettingKey.EnableAuthentication, Value = "true"},
|
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)
|
foreach (var defaultSetting in defaultSettings)
|
||||||
@ -63,11 +64,20 @@ namespace API.Data
|
|||||||
|
|
||||||
await context.SaveChangesAsync();
|
await context.SaveChangesAsync();
|
||||||
|
|
||||||
|
if (string.IsNullOrEmpty(Configuration.BaseUrl))
|
||||||
|
{
|
||||||
|
Configuration.BaseUrl = "/";
|
||||||
|
}
|
||||||
|
|
||||||
// Port and LoggingLevel are managed in appSettings.json. Update the DB values to match
|
// Port and LoggingLevel are managed in appSettings.json. Update the DB values to match
|
||||||
context.ServerSetting.First(s => s.Key == ServerSettingKey.Port).Value =
|
context.ServerSetting.First(s => s.Key == ServerSettingKey.Port).Value =
|
||||||
Configuration.Port + string.Empty;
|
Configuration.Port + string.Empty;
|
||||||
context.ServerSetting.First(s => s.Key == ServerSettingKey.LoggingLevel).Value =
|
context.ServerSetting.First(s => s.Key == ServerSettingKey.LoggingLevel).Value =
|
||||||
Configuration.LogLevel + string.Empty;
|
Configuration.LogLevel + string.Empty;
|
||||||
|
context.ServerSetting.First(s => s.Key == ServerSettingKey.BaseUrl).Value =
|
||||||
|
Configuration.BaseUrl;
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
await context.SaveChangesAsync();
|
await context.SaveChangesAsync();
|
||||||
|
|
||||||
|
@ -21,7 +21,9 @@ namespace API.Entities.Enums
|
|||||||
[Description("EnableOpds")]
|
[Description("EnableOpds")]
|
||||||
EnableOpds = 7,
|
EnableOpds = 7,
|
||||||
[Description("EnableAuthentication")]
|
[Description("EnableAuthentication")]
|
||||||
EnableAuthentication = 8
|
EnableAuthentication = 8,
|
||||||
|
[Description("BaseUrl")]
|
||||||
|
BaseUrl = 9
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -39,6 +39,9 @@ namespace API.Helpers.Converters
|
|||||||
case ServerSettingKey.EnableAuthentication:
|
case ServerSettingKey.EnableAuthentication:
|
||||||
destination.EnableAuthentication = bool.Parse(row.Value);
|
destination.EnableAuthentication = bool.Parse(row.Value);
|
||||||
break;
|
break;
|
||||||
|
case ServerSettingKey.BaseUrl:
|
||||||
|
destination.BaseUrl = row.Value;
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -160,11 +160,23 @@ namespace API
|
|||||||
|
|
||||||
app.UseDefaultFiles();
|
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
|
app.UseStaticFiles(new StaticFileOptions
|
||||||
{
|
{
|
||||||
ContentTypeProvider = new FileExtensionContentTypeProvider()
|
ContentTypeProvider = new FileExtensionContentTypeProvider()
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
app.Use(async (context, next) =>
|
app.Use(async (context, next) =>
|
||||||
{
|
{
|
||||||
context.Response.GetTypedHeaders().CacheControl =
|
context.Response.GetTypedHeaders().CacheControl =
|
||||||
|
@ -18,5 +18,7 @@
|
|||||||
"MaxRollingFiles": 5
|
"MaxRollingFiles": 5
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"Port": 5000
|
"Port": 5000,
|
||||||
|
"BaseUrl": "/"
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -8,7 +8,7 @@ namespace Kavita.Common
|
|||||||
{
|
{
|
||||||
public static class Configuration
|
public static class Configuration
|
||||||
{
|
{
|
||||||
private static readonly string AppSettingsFilename = GetAppSettingFilename();
|
private static string AppSettingsFilename = GetAppSettingFilename();
|
||||||
public static string Branch
|
public static string Branch
|
||||||
{
|
{
|
||||||
get => GetBranch(GetAppSettingFilename());
|
get => GetBranch(GetAppSettingFilename());
|
||||||
@ -33,6 +33,12 @@ namespace Kavita.Common
|
|||||||
set => SetLogLevel(GetAppSettingFilename(), value);
|
set => SetLogLevel(GetAppSettingFilename(), value);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static string BaseUrl
|
||||||
|
{
|
||||||
|
get => GetBaseUrl(GetAppSettingFilename());
|
||||||
|
set => SetBaseUrl(GetAppSettingFilename(), value);
|
||||||
|
}
|
||||||
|
|
||||||
private static string GetAppSettingFilename()
|
private static string GetAppSettingFilename()
|
||||||
{
|
{
|
||||||
if (!string.IsNullOrEmpty(AppSettingsFilename))
|
if (!string.IsNullOrEmpty(AppSettingsFilename))
|
||||||
@ -151,6 +157,55 @@ namespace Kavita.Common
|
|||||||
|
|
||||||
#endregion
|
#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
|
#region LogLevel
|
||||||
|
|
||||||
private static void SetLogLevel(string filePath, string logLevel)
|
private static void SetLogLevel(string filePath, string logLevel)
|
||||||
|
@ -7,4 +7,5 @@ export interface ServerSettings {
|
|||||||
allowStatCollection: boolean;
|
allowStatCollection: boolean;
|
||||||
enableOpds: boolean;
|
enableOpds: boolean;
|
||||||
enableAuthentication: boolean;
|
enableAuthentication: boolean;
|
||||||
|
baseUrl: string;
|
||||||
}
|
}
|
||||||
|
@ -2,7 +2,7 @@ import { Component, OnDestroy, OnInit } from '@angular/core';
|
|||||||
import { NgbModal } from '@ng-bootstrap/ng-bootstrap';
|
import { NgbModal } from '@ng-bootstrap/ng-bootstrap';
|
||||||
import { ToastrService } from 'ngx-toastr';
|
import { ToastrService } from 'ngx-toastr';
|
||||||
import { Subject } from 'rxjs';
|
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 { ConfirmService } from 'src/app/shared/confirm.service';
|
||||||
import { ScanLibraryProgressEvent } from 'src/app/_models/events/scan-library-progress-event';
|
import { ScanLibraryProgressEvent } from 'src/app/_models/events/scan-library-progress-event';
|
||||||
import { Library, LibraryType } from 'src/app/_models/library';
|
import { Library, LibraryType } from 'src/app/_models/library';
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
<div class="container-fluid">
|
<div class="container-fluid">
|
||||||
<form [formGroup]="settingsForm" *ngIf="serverSettings !== undefined">
|
<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">
|
<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>
|
<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>
|
<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">
|
<input readonly id="settings-cachedir" aria-describedby="settings-cachedir-help" class="form-control" formControlName="cacheDirectory" type="text">
|
||||||
</div>
|
</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">
|
<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>
|
<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>
|
<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('allowStatCollection', new FormControl(this.serverSettings.allowStatCollection, [Validators.required]));
|
||||||
this.settingsForm.addControl('enableOpds', new FormControl(this.serverSettings.enableOpds, [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('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('allowStatCollection')?.setValue(this.serverSettings.allowStatCollection);
|
||||||
this.settingsForm.get('enableOpds')?.setValue(this.serverSettings.enableOpds);
|
this.settingsForm.get('enableOpds')?.setValue(this.serverSettings.enableOpds);
|
||||||
this.settingsForm.get('enableAuthentication')?.setValue(this.serverSettings.enableAuthentication);
|
this.settingsForm.get('enableAuthentication')?.setValue(this.serverSettings.enableAuthentication);
|
||||||
|
this.settingsForm.get('baseUrl')?.setValue(this.serverSettings.baseUrl);
|
||||||
}
|
}
|
||||||
|
|
||||||
async saveSettings() {
|
async saveSettings() {
|
||||||
|
@ -1,11 +1,12 @@
|
|||||||
import { BrowserModule, Title } from '@angular/platform-browser';
|
import { BrowserModule, Title } from '@angular/platform-browser';
|
||||||
import { APP_INITIALIZER, ErrorHandler, NgModule } from '@angular/core';
|
import { APP_INITIALIZER, ErrorHandler, NgModule } from '@angular/core';
|
||||||
|
import { APP_BASE_HREF } from '@angular/common';
|
||||||
|
|
||||||
import { AppRoutingModule } from './app-routing.module';
|
import { AppRoutingModule } from './app-routing.module';
|
||||||
import { AppComponent } from './app.component';
|
import { AppComponent } from './app.component';
|
||||||
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
|
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
|
||||||
import { FormsModule, ReactiveFormsModule } from '@angular/forms';
|
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 { NgbCollapseModule, NgbDropdownModule, NgbNavModule, NgbPaginationModule, NgbRatingModule } from '@ng-bootstrap/ng-bootstrap';
|
||||||
import { NavHeaderComponent } from './nav-header/nav-header.component';
|
import { NavHeaderComponent } from './nav-header/nav-header.component';
|
||||||
import { JwtInterceptor } from './_interceptors/jwt.interceptor';
|
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 { PersonBadgeComponent } from './person-badge/person-badge.component';
|
||||||
import { TypeaheadModule } from './typeahead/typeahead.module';
|
import { TypeaheadModule } from './typeahead/typeahead.module';
|
||||||
import { RecentlyAddedComponent } from './recently-added/recently-added.component';
|
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 { CardsModule } from './cards/cards.module';
|
||||||
import { CollectionsModule } from './collections/collections.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 { ReadingListModule } from './reading-list/reading-list.module';
|
||||||
import { DashboardComponent } from './dashboard/dashboard.component';
|
import { SAVER, getSaver } from './shared/_providers/saver.provider';
|
||||||
|
|
||||||
@NgModule({
|
@NgModule({
|
||||||
declarations: [
|
declarations: [
|
||||||
@ -81,7 +82,8 @@ import { DashboardComponent } from './dashboard/dashboard.component';
|
|||||||
{provide: HTTP_INTERCEPTORS, useClass: ErrorInterceptor, multi: true},
|
{provide: HTTP_INTERCEPTORS, useClass: ErrorInterceptor, multi: true},
|
||||||
{provide: HTTP_INTERCEPTORS, useClass: JwtInterceptor, multi: true},
|
{provide: HTTP_INTERCEPTORS, useClass: JwtInterceptor, multi: true},
|
||||||
Title,
|
Title,
|
||||||
{provide: SAVER, useFactory: getSaver}
|
{provide: SAVER, useFactory: getSaver},
|
||||||
|
{ provide: APP_BASE_HREF, useValue: window['_app_base' as keyof Window] || '/' },
|
||||||
],
|
],
|
||||||
entryComponents: [],
|
entryComponents: [],
|
||||||
bootstrap: [AppComponent]
|
bootstrap: [AppComponent]
|
||||||
|
@ -40,4 +40,9 @@
|
|||||||
<app-root></app-root>
|
<app-root></app-root>
|
||||||
<noscript>Please enable JavaScript to continue using this application.</noscript>
|
<noscript>Please enable JavaScript to continue using this application.</noscript>
|
||||||
</body>
|
</body>
|
||||||
|
<script>
|
||||||
|
(function() {
|
||||||
|
window['_app_base'] = '/' + window.location.pathname.split('/')[1];
|
||||||
|
})();
|
||||||
|
</script>
|
||||||
</html>
|
</html>
|
||||||
|
Loading…
x
Reference in New Issue
Block a user