using System; using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; using AutoMapper; using Kavita.API.Database; using Kavita.API.Services; using Kavita.Common; using Kavita.Common.Extensions; using Kavita.Common.Helpers; using Kavita.Models; using Kavita.Models.Constants; using Kavita.Models.DTOs; using Kavita.Models.DTOs.Email; using Kavita.Models.DTOs.KavitaPlus.Metadata; using Kavita.Models.DTOs.Settings; using Kavita.Models.Entities.Enums; using Kavita.Server.Extensions; using Kavita.Services; using Microsoft.AspNetCore.Authentication; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; using Microsoft.Extensions.Logging; namespace Kavita.Server.Controllers; public class SettingsController( ILogger logger, IUnitOfWork unitOfWork, IMapper mapper, IEmailService emailService, ILocalizationService localizationService, ISettingsService settingsService, IAuthenticationSchemeProvider authenticationSchemeProvider, IOidcService oidcService) : BaseApiController { /// /// Returns the base url for this instance (if set) /// /// [HttpGet("base-url")] public async Task> GetBaseUrl() { var settingsDto = await unitOfWork.SettingsRepository.GetSettingsDtoAsync(); return Ok(settingsDto.BaseUrl); } /// /// Returns the server settings /// /// [HttpGet] [Authorize(Policy = PolicyGroups.AdminPolicy)] public async Task> GetSettings() { var settingsDto = await unitOfWork.SettingsRepository.GetSettingsDtoAsync(); // Do not send OIDC secret to user settingsDto.OidcConfig.Secret = "*".Repeat(settingsDto.OidcConfig.Secret.Length); return Ok(settingsDto); } [HttpPost("reset")] [Authorize(Policy = PolicyGroups.AdminPolicy)] public async Task> ResetSettings() { logger.LogInformation("{UserName} is resetting Server Settings", Username!); return await UpdateSettings(mapper.Map(Defaults.DefaultSettings)); } /// /// Resets the IP Addresses /// /// [HttpPost("reset-ip-addresses")] [Authorize(Policy = PolicyGroups.AdminPolicy)] public async Task> ResetIpAddressesSettings() { logger.LogInformation("{UserName} is resetting IP Addresses Setting", Username!); var ipAddresses = await unitOfWork.SettingsRepository.GetSettingAsync(ServerSettingKey.IpAddresses); ipAddresses.Value = Configuration.DefaultIpAddresses; unitOfWork.SettingsRepository.Update(ipAddresses); if (!await unitOfWork.CommitAsync()) { await unitOfWork.RollbackAsync(); } return Ok(await unitOfWork.SettingsRepository.GetSettingsDtoAsync()); } /// /// Resets the Base url /// /// [HttpPost("reset-base-url")] [Authorize(Policy = PolicyGroups.AdminPolicy)] public async Task> ResetBaseUrlSettings() { logger.LogInformation("{UserName} is resetting Base Url Setting", Username!); var baseUrl = await unitOfWork.SettingsRepository.GetSettingAsync(ServerSettingKey.BaseUrl); baseUrl.Value = Configuration.DefaultBaseUrl; unitOfWork.SettingsRepository.Update(baseUrl); if (!await unitOfWork.CommitAsync()) { await unitOfWork.RollbackAsync(); } Configuration.BaseUrl = baseUrl.Value; return Ok(await unitOfWork.SettingsRepository.GetSettingsDtoAsync()); } /// /// Is the minimum information setup for Email to work /// /// [HttpGet("is-email-setup")] public async Task> IsEmailSetup() { var settings = await unitOfWork.SettingsRepository.GetSettingsDtoAsync(); return Ok(settings.IsEmailSetup()); } /// /// Update Server settings /// /// /// [HttpPost] [Authorize(Policy = PolicyGroups.AdminPolicy)] public async Task> UpdateSettings(ServerSettingDto updateSettingsDto) { logger.LogInformation("{UserName} is updating Server Settings", Username!); try { var d = await settingsService.UpdateSettings(updateSettingsDto); return Ok(d); } catch (KavitaException ex) { return BadRequest(await localizationService.Translate(UserId, ex.Message)); } catch (Exception ex) { logger.LogError(ex, "There was an exception when updating server settings"); return BadRequest(await localizationService.Translate(UserId, "generic-error")); } } /// /// All values allowed for Task Scheduling APIs. A custom cron job is not included. Disabled is not applicable for Cleanup. /// /// [HttpGet("task-frequencies")] [Authorize(Policy = PolicyGroups.AdminPolicy)] public ActionResult> GetTaskFrequencies() { return Ok(CronConverter.Options); } [HttpGet("library-types")] [Authorize(Policy = PolicyGroups.AdminPolicy)] public ActionResult> GetLibraryTypes() { return Ok(Enum.GetValues().Select(t => t.ToDescription())); } [HttpGet("log-levels")] [Authorize(Policy = PolicyGroups.AdminPolicy)] public ActionResult> GetLogLevels() { return Ok(new[] {"Trace", "Debug", "Information", "Warning", "Critical"}); } [HttpGet("opds-enabled")] public async Task> GetOpdsEnabled() { var settingsDto = await unitOfWork.SettingsRepository.GetSettingsDtoAsync(); return Ok(settingsDto.EnableOpds); } /// /// Is the cron expression valid for Kavita's scheduler /// /// /// [HttpGet("is-valid-cron")] public ActionResult IsValidCron(string cronExpression) { // NOTE: This must match Hangfire's underlying cron system. Hangfire is unique return Ok(CronHelper.IsValidCron(cronExpression)); } /// /// Sends a test email to see if email settings are hooked up correctly /// /// [HttpPost("test-email-url")] [Authorize(Policy = PolicyGroups.AdminPolicy)] public async Task> TestEmailServiceUrl() { var user = await unitOfWork.UserRepository.GetUserByIdAsync(UserId); if (string.IsNullOrEmpty(user?.Email)) return BadRequest("Your account has no email on record. Cannot email."); return Ok(await emailService.SendTestEmail(user!.Email)); } /// /// Get the metadata settings for Kavita+ users. /// /// [HttpGet("metadata-settings")] [Authorize(Policy = PolicyGroups.AdminPolicy)] public async Task> GetMetadataSettings() { return Ok(await unitOfWork.SettingsRepository.GetMetadataSettingDto()); } /// /// Update the metadata settings for Kavita+ Metadata feature /// /// /// [HttpPost("metadata-settings")] [Authorize(Policy = PolicyGroups.AdminPolicy)] public async Task> UpdateMetadataSettings(MetadataSettingsDto dto) { try { return Ok(await settingsService.UpdateMetadataSettings(dto)); } catch (Exception ex) { logger.LogError(ex, "There was an issue when updating metadata settings"); return BadRequest(ex.Message); } } /// /// Import field mappings /// /// [HttpPost("import-field-mappings")] [Authorize(Policy = PolicyGroups.AdminPolicy)] public async Task> ImportFieldMappings([FromBody] ImportFieldMappingsDto dto) { try { return Ok(await settingsService.ImportFieldMappings(dto.Data, dto.Settings)); } catch (Exception ex) { logger.LogError(ex, "There was an issue importing field mappings"); return BadRequest(ex.Message); } } /// /// Retrieve publicly required configuration regarding Oidc /// /// [AllowAnonymous] [HttpGet("oidc")] public async Task> GetOidcConfig() { var oidcScheme = await authenticationSchemeProvider.GetSchemeAsync(IdentityServiceExtensions.OpenIdConnect); var settings = (await unitOfWork.SettingsRepository.GetSettingsDtoAsync()).OidcConfig; var publicConfig = mapper.Map(settings); publicConfig.Enabled = oidcScheme != null && !string.IsNullOrEmpty(settings.Authority) && !string.IsNullOrEmpty(settings.ClientId) && !string.IsNullOrEmpty(settings.Secret); return Ok(publicConfig); } [Authorize(PolicyGroups.AdminPolicy)] [HttpPost("reset-external-ids")] public async Task ResetExternalIds() { await oidcService.ClearOidcIds(); return Ok(); } /// /// Validate if the given authority is reachable from the server /// /// /// [Authorize(PolicyGroups.AdminPolicy)] [HttpPost("is-valid-authority")] public async Task> IsValidAuthority([FromBody] AuthorityValidationDto authority) { return Ok(await settingsService.IsValidAuthority(authority.Authority)); } }