mirror of
https://github.com/Kareadita/Kavita.git
synced 2025-07-09 03:04:19 -04:00
Base Url implementation (#1824)
* Base Url implementation * PR requested changes
This commit is contained in:
parent
74f62fd5e2
commit
2cff1bcebe
@ -191,6 +191,7 @@ public class SettingsController : BaseApiController
|
|||||||
? $"{path}/"
|
? $"{path}/"
|
||||||
: path;
|
: path;
|
||||||
setting.Value = path;
|
setting.Value = path;
|
||||||
|
Configuration.BaseUrl = updateSettingsDto.BaseUrl;
|
||||||
_unitOfWork.SettingsRepository.Update(setting);
|
_unitOfWork.SettingsRepository.Update(setting);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -19,6 +19,7 @@ using API.Services.HostedServices;
|
|||||||
using API.Services.Tasks;
|
using API.Services.Tasks;
|
||||||
using API.SignalR;
|
using API.SignalR;
|
||||||
using Hangfire;
|
using Hangfire;
|
||||||
|
using HtmlAgilityPack;
|
||||||
using Kavita.Common;
|
using Kavita.Common;
|
||||||
using Kavita.Common.EnvironmentInfo;
|
using Kavita.Common.EnvironmentInfo;
|
||||||
using Microsoft.AspNetCore.Builder;
|
using Microsoft.AspNetCore.Builder;
|
||||||
@ -277,6 +278,11 @@ public class Startup
|
|||||||
|
|
||||||
app.UseForwardedHeaders();
|
app.UseForwardedHeaders();
|
||||||
|
|
||||||
|
var basePath = Configuration.BaseUrl;
|
||||||
|
|
||||||
|
app.UsePathBase(basePath);
|
||||||
|
UpdateBaseUrlInIndex(basePath);
|
||||||
|
|
||||||
app.UseRouting();
|
app.UseRouting();
|
||||||
|
|
||||||
// Ordering is important. Cors, authentication, authorization
|
// Ordering is important. Cors, authentication, authorization
|
||||||
@ -351,6 +357,20 @@ public class Startup
|
|||||||
}
|
}
|
||||||
Console.WriteLine($"Kavita - v{BuildInfo.Version}");
|
Console.WriteLine($"Kavita - v{BuildInfo.Version}");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
var _logger = serviceProvider.GetRequiredService<ILogger<Startup>>();
|
||||||
|
_logger.LogInformation("Starting with base url as {baseUrl}", basePath);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void UpdateBaseUrlInIndex(string baseUrl)
|
||||||
|
{
|
||||||
|
var htmlDoc = new HtmlDocument();
|
||||||
|
var indexHtmlPath = Path.Combine(Directory.GetCurrentDirectory(), "wwwroot", "index.html");
|
||||||
|
htmlDoc.Load(indexHtmlPath);
|
||||||
|
|
||||||
|
var baseNode = htmlDoc.DocumentNode.SelectSingleNode("/html/head/base");
|
||||||
|
baseNode.SetAttributeValue("href", baseUrl);
|
||||||
|
htmlDoc.Save(indexHtmlPath);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void OnShutdown()
|
private static void OnShutdown()
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
{
|
{
|
||||||
"TokenKey": "super secret unguessable key",
|
"TokenKey": "super secret unguessable key",
|
||||||
"Port": 5000,
|
"Port": 5000,
|
||||||
"IpAddresses": ""
|
"IpAddresses": "",
|
||||||
|
"BaseUrl": "/"
|
||||||
}
|
}
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
{
|
{
|
||||||
"TokenKey": "super secret unguessable key",
|
"TokenKey": "super secret unguessable key",
|
||||||
"Port": 5000,
|
"Port": 5000,
|
||||||
"IpAddresses": ""
|
"IpAddresses": "",
|
||||||
|
"BaseUrl": "/"
|
||||||
}
|
}
|
||||||
|
@ -9,6 +9,7 @@ namespace Kavita.Common;
|
|||||||
public static class Configuration
|
public static class Configuration
|
||||||
{
|
{
|
||||||
public const string DefaultIpAddresses = "0.0.0.0,::";
|
public const string DefaultIpAddresses = "0.0.0.0,::";
|
||||||
|
public const string DefaultBaseUrl = "/";
|
||||||
private static readonly string AppSettingsFilename = Path.Join("config", GetAppSettingFilename());
|
private static readonly string AppSettingsFilename = Path.Join("config", GetAppSettingFilename());
|
||||||
|
|
||||||
public static int Port
|
public static int Port
|
||||||
@ -29,6 +30,12 @@ public static class Configuration
|
|||||||
set => SetJwtToken(GetAppSettingFilename(), value);
|
set => SetJwtToken(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))
|
||||||
@ -200,10 +207,81 @@ public static class Configuration
|
|||||||
}
|
}
|
||||||
#endregion
|
#endregion
|
||||||
|
|
||||||
|
#region BaseUrl
|
||||||
|
private static string GetBaseUrl(string filePath)
|
||||||
|
{
|
||||||
|
if (new OsInfo(Array.Empty<IOsVersionAdapter>()).IsDocker)
|
||||||
|
{
|
||||||
|
return DefaultBaseUrl;
|
||||||
|
}
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var json = File.ReadAllText(filePath);
|
||||||
|
var jsonObj = JsonSerializer.Deserialize<dynamic>(json);
|
||||||
|
const string key = "BaseUrl";
|
||||||
|
|
||||||
|
if (jsonObj.TryGetProperty(key, out JsonElement tokenElement))
|
||||||
|
{
|
||||||
|
var baseUrl = tokenElement.GetString();
|
||||||
|
if (!String.IsNullOrEmpty(baseUrl))
|
||||||
|
{
|
||||||
|
baseUrl = !baseUrl.StartsWith("/")
|
||||||
|
? $"/{baseUrl}"
|
||||||
|
: baseUrl;
|
||||||
|
|
||||||
|
baseUrl = !baseUrl.EndsWith("/")
|
||||||
|
? $"{baseUrl}/"
|
||||||
|
: baseUrl;
|
||||||
|
|
||||||
|
return baseUrl;
|
||||||
|
}
|
||||||
|
return DefaultBaseUrl;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
Console.WriteLine("Error reading app settings: " + ex.Message);
|
||||||
|
}
|
||||||
|
|
||||||
|
return DefaultBaseUrl;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void SetBaseUrl(string filePath, string value)
|
||||||
|
{
|
||||||
|
if (new OsInfo(Array.Empty<IOsVersionAdapter>()).IsDocker)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var baseUrl = !value.StartsWith("/")
|
||||||
|
? $"/{value}"
|
||||||
|
: value;
|
||||||
|
|
||||||
|
baseUrl = !baseUrl.EndsWith("/")
|
||||||
|
? $"{baseUrl}/"
|
||||||
|
: baseUrl;
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var json = File.ReadAllText(filePath);
|
||||||
|
var jsonObj = JsonSerializer.Deserialize<AppSettings>(json);
|
||||||
|
jsonObj.BaseUrl = baseUrl;
|
||||||
|
json = JsonSerializer.Serialize(jsonObj, new JsonSerializerOptions { WriteIndented = true });
|
||||||
|
File.WriteAllText(filePath, json);
|
||||||
|
}
|
||||||
|
catch (Exception)
|
||||||
|
{
|
||||||
|
/* Swallow exception */
|
||||||
|
}
|
||||||
|
}
|
||||||
|
#endregion
|
||||||
|
|
||||||
private class AppSettings
|
private class AppSettings
|
||||||
{
|
{
|
||||||
public string TokenKey { get; set; }
|
public string TokenKey { get; set; }
|
||||||
public int Port { get; set; }
|
public int Port { get; set; }
|
||||||
public string IpAddresses { get; set; }
|
public string IpAddresses { get; set; }
|
||||||
|
public string BaseUrl { get; set; }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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">Changing Port requires a manual restart of Kavita to take effect.</p>
|
<p class="text-warning pt-2">Changing Port or Base Url requires a manual restart of Kavita to take effect.</p>
|
||||||
<div class="mb-3">
|
<div class="mb-3">
|
||||||
<label for="settings-cachedir" class="form-label">Cache Directory</label> <i class="fa fa-info-circle" placement="right" [ngbTooltip]="cacheDirectoryTooltip" role="button" tabindex="0"></i>
|
<label for="settings-cachedir" class="form-label">Cache Directory</label> <i class="fa fa-info-circle" placement="right" [ngbTooltip]="cacheDirectoryTooltip" role="button" tabindex="0"></i>
|
||||||
<ng-template #cacheDirectoryTooltip>Where the server places temporary files when reading. This will be cleaned up on a regular basis.</ng-template>
|
<ng-template #cacheDirectoryTooltip>Where the server places temporary files when reading. This will be cleaned up on a regular basis.</ng-template>
|
||||||
@ -24,7 +24,7 @@
|
|||||||
<label for="settings-hostname" class="form-label">Host Name</label> <i class="fa fa-info-circle" placement="right" [ngbTooltip]="hostNameTooltip" role="button" tabindex="0"></i>
|
<label for="settings-hostname" class="form-label">Host Name</label> <i class="fa fa-info-circle" placement="right" [ngbTooltip]="hostNameTooltip" role="button" tabindex="0"></i>
|
||||||
<ng-template #hostNameTooltip>Domain Name (of Reverse Proxy). If set, email generation will always use this.</ng-template>
|
<ng-template #hostNameTooltip>Domain Name (of Reverse Proxy). If set, email generation will always use this.</ng-template>
|
||||||
<span class="visually-hidden" id="settings-hostname-help">Domain Name (of Reverse Proxy). If set, email generation will always use this.</span>
|
<span class="visually-hidden" id="settings-hostname-help">Domain Name (of Reverse Proxy). If set, email generation will always use this.</span>
|
||||||
<input id="settings-hostname" aria-describedby="settings-hostname-help" class="form-control" formControlName="hostName" type="text"
|
<input id="settings-hostname" aria-describedby="settings-hostname-help" class="form-control" formControlName="hostName" type="text"
|
||||||
[class.is-invalid]="settingsForm.get('hostName')?.invalid && settingsForm.get('hostName')?.touched">
|
[class.is-invalid]="settingsForm.get('hostName')?.invalid && settingsForm.get('hostName')?.touched">
|
||||||
<div id="hostname-validations" class="invalid-feedback" *ngIf="settingsForm.dirty || settingsForm.touched">
|
<div id="hostname-validations" class="invalid-feedback" *ngIf="settingsForm.dirty || settingsForm.touched">
|
||||||
<div *ngIf="settingsForm.get('hostName')?.errors?.pattern">
|
<div *ngIf="settingsForm.get('hostName')?.errors?.pattern">
|
||||||
@ -33,6 +33,19 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="settings-baseurl" class="form-label">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="visually-hidden" id="settings-cachedir-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"
|
||||||
|
[class.is-invalid]="settingsForm.get('baseUrl')?.invalid && settingsForm.get('baseUrl')?.touched">
|
||||||
|
<div id="baseurl-validations" class="invalid-feedback" *ngIf="settingsForm.dirty || settingsForm.touched">
|
||||||
|
<div *ngIf="settingsForm.get('baseUrl')?.errors?.pattern">
|
||||||
|
Base URL must start and end with /
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="row g-0 mb-2">
|
<div class="row g-0 mb-2">
|
||||||
<div class="col-md-8 col-sm-12 pe-2">
|
<div class="col-md-8 col-sm-12 pe-2">
|
||||||
<label for="settings-ipaddresses" class="form-label">IP Addresses</label> <i class="fa fa-info-circle" placement="right" [ngbTooltip]="ipAddressesTooltip" role="button" tabindex="0"></i>
|
<label for="settings-ipaddresses" class="form-label">IP Addresses</label> <i class="fa fa-info-circle" placement="right" [ngbTooltip]="ipAddressesTooltip" role="button" tabindex="0"></i>
|
||||||
|
@ -47,7 +47,7 @@ export class ManageSettingsComponent implements OnInit {
|
|||||||
this.settingsForm.addControl('loggingLevel', new FormControl(this.serverSettings.loggingLevel, [Validators.required]));
|
this.settingsForm.addControl('loggingLevel', new FormControl(this.serverSettings.loggingLevel, [Validators.required]));
|
||||||
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('baseUrl', new FormControl(this.serverSettings.baseUrl, [Validators.required]));
|
this.settingsForm.addControl('baseUrl', new FormControl(this.serverSettings.baseUrl, [Validators.pattern(/^(\/[\w-]+)*\/$/)]));
|
||||||
this.settingsForm.addControl('emailServiceUrl', new FormControl(this.serverSettings.emailServiceUrl, [Validators.required]));
|
this.settingsForm.addControl('emailServiceUrl', new FormControl(this.serverSettings.emailServiceUrl, [Validators.required]));
|
||||||
this.settingsForm.addControl('totalBackups', new FormControl(this.serverSettings.totalBackups, [Validators.required, Validators.min(1), Validators.max(30)]));
|
this.settingsForm.addControl('totalBackups', new FormControl(this.serverSettings.totalBackups, [Validators.required, Validators.min(1), Validators.max(30)]));
|
||||||
this.settingsForm.addControl('totalLogs', new FormControl(this.serverSettings.totalLogs, [Validators.required, Validators.min(1), Validators.max(30)]));
|
this.settingsForm.addControl('totalLogs', new FormControl(this.serverSettings.totalLogs, [Validators.required, Validators.min(1), Validators.max(30)]));
|
||||||
|
3
UI/Web/src/app/base-url.provider.ts
Normal file
3
UI/Web/src/app/base-url.provider.ts
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
export function getBaseUrl() : string {
|
||||||
|
return document.getElementsByTagName('base')[0]?.getAttribute('href') || '/';
|
||||||
|
}
|
@ -22,6 +22,7 @@ import { BookPageLayoutMode } from 'src/app/_models/readers/book-page-layout-mod
|
|||||||
import { forkJoin, Subject } from 'rxjs';
|
import { forkJoin, Subject } from 'rxjs';
|
||||||
import { bookColorThemes } from 'src/app/book-reader/_components/reader-settings/reader-settings.component';
|
import { bookColorThemes } from 'src/app/book-reader/_components/reader-settings/reader-settings.component';
|
||||||
import { BookService } from 'src/app/book-reader/_services/book.service';
|
import { BookService } from 'src/app/book-reader/_services/book.service';
|
||||||
|
import { environment } from 'src/environments/environment';
|
||||||
|
|
||||||
enum AccordionPanelID {
|
enum AccordionPanelID {
|
||||||
ImageReader = 'image-reader',
|
ImageReader = 'image-reader',
|
||||||
@ -252,6 +253,10 @@ export class UserPreferencesComponent implements OnInit, OnDestroy {
|
|||||||
|
|
||||||
|
|
||||||
transformKeyToOpdsUrl(key: string) {
|
transformKeyToOpdsUrl(key: string) {
|
||||||
|
if (environment.production) {
|
||||||
|
return `${location.origin}${environment.apiUrl}opds/${key}`;
|
||||||
|
}
|
||||||
|
|
||||||
return `${location.origin}/api/opds/${key}`;
|
return `${location.origin}/api/opds/${key}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,5 +1,8 @@
|
|||||||
|
import { getBaseUrl } from "src/app/base-url.provider";
|
||||||
|
const BASE_URL = getBaseUrl();
|
||||||
|
|
||||||
export const environment = {
|
export const environment = {
|
||||||
production: true,
|
production: true,
|
||||||
apiUrl: '/api/',
|
apiUrl: `${BASE_URL}api/`,
|
||||||
hubUrl: '/hubs/'
|
hubUrl:`${BASE_URL}hubs/`
|
||||||
};
|
};
|
||||||
|
@ -18,7 +18,7 @@
|
|||||||
|
|
||||||
<meta name="apple-mobile-web-app-capable" content="yes">
|
<meta name="apple-mobile-web-app-capable" content="yes">
|
||||||
<meta name="mobile-web-app-capable" content="yes">
|
<meta name="mobile-web-app-capable" content="yes">
|
||||||
|
|
||||||
</head>
|
</head>
|
||||||
<body class="mat-typography default" theme="dark">
|
<body class="mat-typography default" theme="dark">
|
||||||
<app-root></app-root>
|
<app-root></app-root>
|
||||||
|
@ -15305,4 +15305,4 @@
|
|||||||
"description": "Responsible for all things Want To Read"
|
"description": "Responsible for all things Want To Read"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user