using Asp.Versioning; using Cronos; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; using Microsoft.Data.Sqlite; using Microsoft.Extensions.Caching.Memory; using Mirea.Api.Dto.Requests; using Mirea.Api.Dto.Requests.Configuration; using Mirea.Api.Endpoint.Common.Attributes; using Mirea.Api.Endpoint.Common.Exceptions; using Mirea.Api.Endpoint.Common.Interfaces; using Mirea.Api.Endpoint.Common.Services; using Mirea.Api.Endpoint.Common.Settings; using Mirea.Api.Endpoint.Configuration.General.Settings; using Mirea.Api.Endpoint.Configuration.General.Validators; using Mirea.Api.Security.Services; using MySqlConnector; using Npgsql; using StackExchange.Redis; using System; using System.Collections.Generic; using System.Data; using System.IO; using System.Linq; using System.Net.Mail; using System.Runtime.InteropServices; using System.Security.Cryptography; namespace Mirea.Api.Endpoint.Controllers.Configuration; [ApiVersion("1.0")] [ApiController] [MaintenanceModeIgnore] [ApiExplorerSettings(IgnoreApi = true)] public class SetupController( ISetupToken setupToken, IMaintenanceModeNotConfigureService notConfigureService, IMemoryCache cache, PasswordHashService passwordHashService) : BaseController { private const string CacheGeneralKey = "config_general"; private const string CacheAdminKey = "config_admin"; private GeneralConfig GeneralConfig { get => cache.Get(CacheGeneralKey) ?? new GeneralConfig(); set => cache.Set(CacheGeneralKey, value); } [HttpGet("GenerateToken")] [Localhost] public ActionResult GenerateToken() { if (!notConfigureService.IsMaintenanceMode) throw new ControllerArgumentException( "The token cannot be generated because the server has been configured. " + $"If you need to restart the configuration, then delete the \"{GeneralConfig.FilePath}\" file and restart the application."); var token = new byte[32]; RandomNumberGenerator.Create().GetBytes(token); setupToken.SetToken(token); return Ok(Convert.ToBase64String(token)); } [HttpGet("IsConfigured")] public ActionResult IsConfigured() => !notConfigureService.IsMaintenanceMode; [HttpGet("CheckToken")] public ActionResult CheckToken([FromQuery] string token) { if (!setupToken.MatchToken(Convert.FromBase64String(token))) return Unauthorized("The token is not valid"); Response.Cookies.Append(TokenAuthenticationAttribute.AuthToken, token, new CookieOptions { Path = UrlHelper.GetSubPathWithoutFirstApiName + "api", Domain = HttpContext.GetCurrentDomain(), HttpOnly = true, #if !DEBUG Secure = true #endif }); return Ok(true); } private ActionResult SetDatabase(string connectionString, DbSettings.DatabaseEnum databaseType) where TConnection : class, IDbConnection, new() where TException : Exception { try { using (var connection = new TConnection()) { connection.ConnectionString = connectionString; connection.Open(); connection.Close(); if (connection is SqliteConnection) SqliteConnection.ClearAllPools(); } var general = GeneralConfig; general.DbSettings = new DbSettings { ConnectionStringSql = connectionString, TypeDatabase = databaseType }; GeneralConfig = general; return Ok(true); } catch (TException ex) { throw new ControllerArgumentException($"Error when connecting: {ex.Message}"); } } [HttpPost("SetPsql")] [TokenAuthentication] [BadRequestResponse] public ActionResult SetPsql([FromBody] DatabaseRequest request) { string connectionString = $"Host={request.Server}:{request.Port};Username={request.User};Database={request.Database}"; if (request.Password != null) connectionString += $";Password={request.Password}"; if (request.Ssl) connectionString += ";SSL Mode=Require;"; return SetDatabase(connectionString, DbSettings.DatabaseEnum.PostgresSql); } [HttpPost("SetMysql")] [TokenAuthentication] [BadRequestResponse] public ActionResult SetMysql([FromBody] DatabaseRequest request) { string connectionString = $"Server={request.Server}:{request.Port};Uid={request.User};Database={request.Database};"; if (request.Password != null) connectionString += $"Pwd={request.Password};"; if (request.Ssl) connectionString += "SslMode=Require;"; return SetDatabase(connectionString, DbSettings.DatabaseEnum.Mysql); } [HttpPost("SetSqlite")] [TokenAuthentication] public ActionResult SetSqlite([FromQuery] string? path) { if (string.IsNullOrEmpty(path)) path = "database"; path = PathBuilder.Combine(path); if (!Directory.Exists(path)) { if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) Directory.CreateDirectory(path); else Directory.CreateDirectory(path, UnixFileMode.UserRead | UnixFileMode.UserWrite); } else if (Directory.GetDirectories(path).Length != 0 || !Directory.GetFiles(path).Select(x => string.Equals(Path.GetFileName(x), "database.db3")).All(x => x)) throw new ControllerArgumentException("Such a folder exists. Enter a different name"); var filePath = Path.Combine(path, "database.db3"); var connectionString = $"Data Source={filePath}"; //System.IO.File.Create(filePath); var result = SetDatabase(connectionString, DbSettings.DatabaseEnum.Sqlite); foreach (var file in Directory.GetFiles(path)) System.IO.File.Delete(file); return result; } [HttpPost("SetRedis")] [TokenAuthentication] [BadRequestResponse] public ActionResult SetRedis([FromBody] CacheRequest request) { string connectionString = $"{request.Server}:{request.Port},ssl=false"; if (request.Password != null) connectionString += $",password={request.Password}"; try { var redis = ConnectionMultiplexer.Connect(connectionString); redis.Close(); var general = GeneralConfig; general.CacheSettings = new CacheSettings { ConnectionString = connectionString, TypeDatabase = CacheSettings.CacheEnum.Redis }; GeneralConfig = general; return Ok(true); } catch (Exception ex) { throw new ControllerArgumentException("Error when connecting to Redis: " + ex.Message); } } [HttpPost("SetMemcached")] [TokenAuthentication] [BadRequestResponse] public ActionResult SetMemcached() { var general = GeneralConfig; general.CacheSettings = new CacheSettings { ConnectionString = null, TypeDatabase = CacheSettings.CacheEnum.Memcached }; GeneralConfig = general; return Ok(true); } [HttpPost("CreateAdmin")] [TokenAuthentication] [BadRequestResponse] public ActionResult CreateAdmin([FromBody] CreateUserRequest user) { if (!PasswordHashService.HasPasswordInPolicySecurity(user.Password)) throw new ControllerArgumentException("The password must be at least 8 characters long and contain at least one uppercase letter and one special character."); if (!MailAddress.TryCreate(user.Email, out _)) throw new ControllerArgumentException("The email address is incorrect."); var (salt, hash) = passwordHashService.HashPassword(user.Password); var admin = new Admin { Username = user.Username, Email = user.Email, PasswordHash = hash, Salt = salt }; cache.Set(CacheAdminKey, admin); return Ok(true); } [HttpPost("SetLogging")] [TokenAuthentication] [BadRequestResponse] public ActionResult SetLogging([FromBody] LoggingRequest? request = null) { var settings = (request == null) switch { true => new LogSettings { EnableLogToFile = true }, false => new LogSettings { EnableLogToFile = request.EnableLogToFile, LogFileName = request.LogFileName, LogFilePath = request.LogFilePath } }; if (settings.EnableLogToFile) { if (string.IsNullOrEmpty(settings.LogFileName)) settings.LogFileName = "log-"; if (string.IsNullOrEmpty(settings.LogFilePath)) settings.LogFilePath = OperatingSystem.IsWindows() || PathBuilder.IsDefaultPath ? PathBuilder.Combine("logs") : "/var/log/mirea"; } var general = GeneralConfig; general.LogSettings = settings; GeneralConfig = general; return true; } [HttpPost("SetEmail")] [TokenAuthentication] [BadRequestResponse] public ActionResult SetEmail([FromBody] EmailRequest? request = null) { var settings = (request == null) switch { true => new EmailSettings(), false => new EmailSettings { Server = request.Server, From = request.From, Password = request.Password, Port = request.Port, Ssl = request.Ssl, User = request.User } }; var general = GeneralConfig; general.EmailSettings = settings; GeneralConfig = general; return true; } [HttpPost("SetSchedule")] [TokenAuthentication] [BadRequestResponse] public ActionResult SetSchedule([FromBody] ScheduleConfigurationRequest request) { var general = GeneralConfig; general.ScheduleSettings = new ScheduleSettings { // every 6 hours CronUpdateSchedule = request.CronUpdateSchedule ?? "0 */6 * * *", StartTerm = request.StartTerm, PairPeriod = new Dictionary { {1, new ScheduleSettings.PairPeriodTime(new TimeOnly(9, 0, 0), new TimeOnly(10, 30, 0))}, {2, new ScheduleSettings.PairPeriodTime(new TimeOnly(10, 40, 0), new TimeOnly(12, 10, 0))}, {3, new ScheduleSettings.PairPeriodTime(new TimeOnly(12, 40, 0), new TimeOnly(14, 10, 0))}, {4, new ScheduleSettings.PairPeriodTime(new TimeOnly(14, 20, 0), new TimeOnly(15, 50, 0))}, {5, new ScheduleSettings.PairPeriodTime(new TimeOnly(16, 20, 0), new TimeOnly(17, 50, 0))}, {6, new ScheduleSettings.PairPeriodTime(new TimeOnly(18, 0, 0), new TimeOnly(19, 30, 0))}, {7, new ScheduleSettings.PairPeriodTime(new TimeOnly(19, 40, 0), new TimeOnly(21, 10, 0))}, } }; if (!CronExpression.TryParse(general.ScheduleSettings.CronUpdateSchedule, CronFormat.Standard, out _)) throw new ControllerArgumentException("The Cron task could not be parsed. Check the format of the entered data."); GeneralConfig = general; return true; } [HttpPost("Submit")] [TokenAuthentication] [BadRequestResponse] public ActionResult Submit() { if (!new SettingsRequiredValidator(GeneralConfig).AreSettingsValid()) throw new ControllerArgumentException("The necessary data has not been configured."); if (!cache.TryGetValue(CacheAdminKey, out Admin? admin) || admin == null) throw new ControllerArgumentException("The administrator's data was not set."); admin.SaveSetting(); GeneralConfig.SaveSetting(); return true; } }