Kavita/API/Startup.cs
Joseph Milazzo 6ee8320c2b
Ability to restrict a user's ability to change passwords (#1018)
* Implemented a new role "Change Password". This role allows you to change your own password. By default, all users will have it. A user can have it removed arbitrarliy.

Removed components that are no longer going to be used.

* Cleaned up some code
2022-02-01 07:40:41 -08:00

304 lines
11 KiB
C#

using System;
using System.IO;
using System.IO.Compression;
using System.Linq;
using System.Net;
using System.Net.Sockets;
using System.Threading.Tasks;
using API.Data;
using API.Entities;
using API.Extensions;
using API.Middleware;
using API.Services;
using API.Services.HostedServices;
using API.Services.Tasks;
using API.SignalR;
using Hangfire;
using Hangfire.MemoryStorage;
using Kavita.Common;
using Kavita.Common.EnvironmentInfo;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.HttpOverrides;
using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.ResponseCompression;
using Microsoft.AspNetCore.StaticFiles;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
using Microsoft.OpenApi.Models;
using TaskScheduler = API.Services.TaskScheduler;
namespace API
{
public class Startup
{
private readonly IConfiguration _config;
private readonly IWebHostEnvironment _env;
public Startup(IConfiguration config, IWebHostEnvironment env)
{
_config = config;
_env = env;
}
// This method gets called by the runtime. Use this method to add services to the container.
public void ConfigureServices(IServiceCollection services)
{
services.AddApplicationServices(_config, _env);
services.AddControllers();
services.Configure<ForwardedHeadersOptions>(options =>
{
options.ForwardedHeaders =
ForwardedHeaders.XForwardedFor | ForwardedHeaders.XForwardedProto;
});
services.AddCors();
services.AddIdentityServices(_config);
services.AddSwaggerGen(c =>
{
c.SwaggerDoc("v1", new OpenApiInfo { Title = "Kavita API", Version = "v1" });
c.SwaggerDoc("Kavita API", new OpenApiInfo()
{
Description = "Kavita provides a set of APIs that are authenticated by JWT. JWT token can be copied from local storage.",
Title = "Kavita API",
Version = "v1",
});
var filePath = Path.Combine(AppContext.BaseDirectory, "API.xml");
c.IncludeXmlComments(filePath);
c.AddSecurityDefinition("Bearer", new OpenApiSecurityScheme {
In = ParameterLocation.Header,
Description = "Please insert JWT with Bearer into field",
Name = "Authorization",
Type = SecuritySchemeType.ApiKey
});
c.AddSecurityRequirement(new OpenApiSecurityRequirement {
{
new OpenApiSecurityScheme
{
Reference = new OpenApiReference
{
Type = ReferenceType.SecurityScheme,
Id = "Bearer"
}
},
Array.Empty<string>()
}
});
c.AddServer(new OpenApiServer()
{
Description = "Local Server",
Url = "http://localhost:5000/",
});
});
services.AddResponseCompression(options =>
{
options.Providers.Add<BrotliCompressionProvider>();
options.Providers.Add<GzipCompressionProvider>();
options.MimeTypes =
ResponseCompressionDefaults.MimeTypes.Concat(
new[] { "image/jpeg", "image/jpg" });
options.EnableForHttps = true;
});
services.Configure<BrotliCompressionProviderOptions>(options =>
{
options.Level = CompressionLevel.Fastest;
});
services.AddResponseCaching();
services.Configure<ForwardedHeadersOptions>(options =>
{
options.ForwardedHeaders =
ForwardedHeaders.All;
});
services.AddHangfire(configuration => configuration
.UseSimpleAssemblyNameTypeSerializer()
.UseRecommendedSerializerSettings()
.UseMemoryStorage());
// Add the processing server as IHostedService
services.AddHangfireServer();
// Add IHostedService for startup tasks
// Any services that should be bootstrapped go here
services.AddHostedService<StartupTasksHostedService>();
}
// This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
public void Configure(IApplicationBuilder app, IBackgroundJobClient backgroundJobs, IWebHostEnvironment env,
IHostApplicationLifetime applicationLifetime, IServiceProvider serviceProvider, ICacheService cacheService,
IDirectoryService directoryService, IUnitOfWork unitOfWork, IBackupService backupService, IImageService imageService)
{
// Apply Migrations
try
{
Task.Run(async () =>
{
// Apply all migrations on startup
var logger = serviceProvider.GetRequiredService<ILogger<Program>>();
var context = serviceProvider.GetRequiredService<DataContext>();
var userManager = serviceProvider.GetRequiredService<UserManager<AppUser>>();
await MigrateBookmarks.Migrate(directoryService, unitOfWork,
logger, cacheService);
await MigrateChangePasswordRoles.Migrate(unitOfWork, userManager);
var requiresCoverImageMigration = !Directory.Exists(directoryService.CoverImageDirectory);
try
{
// If this is a new install, tables wont exist yet
if (requiresCoverImageMigration)
{
MigrateCoverImages.ExtractToImages(context, directoryService, imageService);
}
}
catch (Exception)
{
requiresCoverImageMigration = false;
}
if (requiresCoverImageMigration)
{
await MigrateCoverImages.UpdateDatabaseWithImages(context, directoryService);
}
}).GetAwaiter()
.GetResult();
}
catch (Exception ex)
{
var logger = serviceProvider.GetRequiredService<ILogger<Program>>();
logger.LogCritical(ex, "An error occurred during migration");
}
app.UseMiddleware<ExceptionMiddleware>();
if (env.IsDevelopment())
{
app.UseSwagger();
app.UseSwaggerUI(c =>
{
c.SwaggerEndpoint("/swagger/v1/swagger.json", "Kavita API " + BuildInfo.Version);
});
app.UseHangfireDashboard();
}
app.UseResponseCompression();
app.UseForwardedHeaders(new ForwardedHeadersOptions
{
ForwardedHeaders = ForwardedHeaders.XForwardedFor | ForwardedHeaders.XForwardedProto | ForwardedHeaders.XForwardedHost
});
app.UseRouting();
// Ordering is important. Cors, authentication, authorization
if (env.IsDevelopment())
{
app.UseCors(policy => policy
.AllowAnyHeader()
.AllowAnyMethod()
.AllowCredentials() // For SignalR token query param
.WithOrigins("http://localhost:4200", $"http://{GetLocalIpAddress()}:4200")
.WithExposedHeaders("Content-Disposition", "Pagination"));
}
app.UseResponseCaching();
app.UseAuthentication();
app.UseAuthorization();
app.UseDefaultFiles();
// This is not implemented completely. Commenting out until implemented
// var service = serviceProvider.GetRequiredService<IUnitOfWork>();
// var settings = service.SettingsRepository.GetSettingsDto();
// if (!string.IsNullOrEmpty(settings.BaseUrl) && !settings.BaseUrl.Equals("/"))
// {
// var path = !settings.BaseUrl.StartsWith("/")
// ? $"/{settings.BaseUrl}"
// : settings.BaseUrl;
// path = !path.EndsWith("/")
// ? $"{path}/"
// : path;
// 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 =
new Microsoft.Net.Http.Headers.CacheControlHeaderValue()
{
Public = false,
MaxAge = TimeSpan.FromSeconds(10),
};
context.Response.Headers[Microsoft.Net.Http.Headers.HeaderNames.Vary] =
new[] { "Accept-Encoding" };
await next();
});
app.UseEndpoints(endpoints =>
{
endpoints.MapControllers();
endpoints.MapHub<MessageHub>("hubs/messages");
endpoints.MapHangfireDashboard();
endpoints.MapFallbackToController("Index", "Fallback");
});
applicationLifetime.ApplicationStopping.Register(OnShutdown);
applicationLifetime.ApplicationStarted.Register(() =>
{
try
{
var logger = serviceProvider.GetRequiredService<ILogger<Startup>>();
logger.LogInformation("Kavita - v{Version}", BuildInfo.Version);
}
catch (Exception)
{
/* Swallow Exception */
}
Console.WriteLine($"Kavita - v{BuildInfo.Version}");
});
}
private static void OnShutdown()
{
Console.WriteLine("Server is shutting down. Please allow a few seconds to stop any background jobs...");
TaskScheduler.Client.Dispose();
System.Threading.Thread.Sleep(1000);
Console.WriteLine("You may now close the application window.");
}
private static string GetLocalIpAddress()
{
using var socket = new Socket(AddressFamily.InterNetwork, SocketType.Dgram, 0);
socket.Connect("8.8.8.8", 65530);
if (socket.LocalEndPoint is IPEndPoint endPoint) return endPoint.Address.ToString();
throw new KavitaException("No network adapters with an IPv4 address in the system!");
}
}
}