feat: add a return of the data that has been configured

This commit is contained in:
Polianin Nikita 2024-12-18 07:39:17 +03:00
parent fd578aa61e
commit e9ff1cabe8
13 changed files with 362 additions and 48 deletions

View File

@ -0,0 +1,17 @@
namespace Mirea.Api.Dto.Common;
/// <summary>
/// Specifies the types of caching mechanisms available.
/// </summary>
public enum CacheType
{
/// <summary>
/// Memcached caching type.
/// </summary>
Memcached,
/// <summary>
/// Redis caching type.
/// </summary>
Redis
}

View File

@ -0,0 +1,22 @@
namespace Mirea.Api.Dto.Common;
/// <summary>
/// Specifies the types of databases supported.
/// </summary>
public enum DatabaseType
{
/// <summary>
/// MySQL database type.
/// </summary>
Mysql,
/// <summary>
/// SQLite database type.
/// </summary>
Sqlite,
/// <summary>
/// PostgreSQL database type.
/// </summary>
PostgresSql
}

View File

@ -10,27 +10,18 @@ public class CreateUserRequest
/// <summary>
/// Gets or sets the email address of the user.
/// </summary>
/// <remarks>
/// The email address is a required field.
/// </remarks>
[Required]
public required string Email { get; set; }
/// <summary>
/// Gets or sets the username of the user.
/// </summary>
/// <remarks>
/// The username is a required field.
/// </remarks>
[Required]
public required string Username { get; set; }
/// <summary>
/// Gets or sets the password of the user.
/// </summary>
/// <remarks>
/// The password is a required field.
/// </remarks>
[Required]
public required string Password { get; set; }
}

View File

@ -1,16 +1,17 @@
using Mirea.Api.Dto.Common;
using System;
using System.ComponentModel.DataAnnotations;
namespace Mirea.Api.Dto.Responses;
/// <summary>
/// Represents the response containing information about available OAuth providers.
/// </summary>
public class AvailableProvidersResponse
public class AvailableOAuthProvidersResponse
{
/// <summary>
/// Gets or sets the name of the OAuth provider.
/// </summary>
[Required]
public required string ProviderName { get; set; }
/// <summary>
@ -19,7 +20,8 @@ public class AvailableProvidersResponse
public OAuthProvider Provider { get; set; }
/// <summary>
/// Gets or sets the redirect URI for the OAuth provider.
/// Gets or sets the redirect URL for the OAuth provider's authorization process.
/// </summary>
public required Uri Redirect { get; set; }
[Required]
public required string Redirect { get; set; }
}

View File

@ -0,0 +1,29 @@
using Mirea.Api.Dto.Common;
namespace Mirea.Api.Dto.Responses.Configuration;
/// <summary>
/// Represents a response containing cache configuration details.
/// </summary>
public class CacheResponse
{
/// <summary>
/// Gets or sets the type of cache database.
/// </summary>
public CacheType Type { get; set; }
/// <summary>
/// Gets or sets the server address.
/// </summary>
public string? Server { get; set; }
/// <summary>
/// Gets or sets the port number.
/// </summary>
public int Port { get; set; }
/// <summary>
/// Gets or sets the password.
/// </summary>
public string? Password { get; set; }
}

View File

@ -0,0 +1,49 @@
using Mirea.Api.Dto.Common;
namespace Mirea.Api.Dto.Responses.Configuration;
/// <summary>
/// Represents a response containing database configuration details.
/// </summary>
public class DatabaseResponse
{
/// <summary>
/// Gets or sets the type of database.
/// </summary>
public DatabaseType Type { get; set; }
/// <summary>
/// Gets or sets the server address.
/// </summary>
public string? Server { get; set; }
/// <summary>
/// Gets or sets the port number.
/// </summary>
public int Port { get; set; }
/// <summary>
/// Gets or sets the database name.
/// </summary>
public string? Database { get; set; }
/// <summary>
/// Gets or sets the username.
/// </summary>
public string? User { get; set; }
/// <summary>
/// Gets or sets a value indicating whether SSL is enabled.
/// </summary>
public bool Ssl { get; set; }
/// <summary>
/// Gets or sets the password.
/// </summary>
public string? Password { get; set; }
/// <summary>
/// Gets or sets the path to database. Only for Sqlite
/// </summary>
public string? PathToDatabase { get; set; }
}

View File

@ -0,0 +1,17 @@
namespace Mirea.Api.Dto.Responses;
/// <summary>
/// Represents the response containing the TOTP (Time-Based One-Time Password) key details.
/// </summary>
public class TotpKeyResponse
{
/// <summary>
/// Gets or sets the secret key used for TOTP generation.
/// </summary>
public required string Secret { get; set; }
/// <summary>
/// Gets or sets the image (QR code) representing the TOTP key.
/// </summary>
public required string Image { get; set; }
}

View File

@ -0,0 +1,35 @@
using Mirea.Api.Dto.Common;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
namespace Mirea.Api.Dto.Responses;
/// <summary>
/// Represents a response containing user information.
/// </summary>
public class UserResponse
{
/// <summary>
/// Gets or sets the email address of the user.
/// </summary>
[Required]
public required string Email { get; set; }
/// <summary>
/// Gets or sets the username of the user.
/// </summary>
[Required]
public required string Username { get; set; }
/// <summary>
/// Gets or sets a value indicating whether the user has two-factor authentication enabled.
/// </summary>
[Required]
public bool TwoFactorAuthenticatorEnabled { get; set; }
/// <summary>
/// Gets or sets a collection of OAuth providers used by the user.
/// </summary>
[Required]
public required IEnumerable<OAuthProvider> UsedOAuthProviders { get; set; }
}

View File

@ -1,7 +1,7 @@
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Mirea.Api.Dto.Common;
using Mirea.Api.Endpoint.Configuration.Model;
using Mirea.Api.Endpoint.Configuration.Model.GeneralSettings;
namespace Mirea.Api.Endpoint.Configuration.Core.Startup;
@ -10,7 +10,7 @@ public static class CacheConfiguration
public static IServiceCollection AddCustomRedis(this IServiceCollection services, IConfiguration configuration, IHealthChecksBuilder? healthChecksBuilder = null)
{
var cache = configuration.Get<GeneralConfig>()?.CacheSettings;
if (cache?.TypeDatabase != CacheSettings.CacheEnum.Redis)
if (cache?.TypeDatabase != CacheType.Redis)
return services;
services.AddStackExchangeRedisCache(options =>

View File

@ -1,8 +1,8 @@
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Mirea.Api.Dto.Common;
using Mirea.Api.Endpoint.Common.Services.Security;
using Mirea.Api.Endpoint.Configuration.Model;
using Mirea.Api.Endpoint.Configuration.Model.GeneralSettings;
using Mirea.Api.Security;
using Mirea.Api.Security.Common.Interfaces;
@ -16,7 +16,7 @@ public static class SecureConfiguration
services.AddSingleton<IRevokedToken, MemoryRevokedTokenService>();
if (configuration.Get<GeneralConfig>()?.CacheSettings?.TypeDatabase == CacheSettings.CacheEnum.Redis)
if (configuration.Get<GeneralConfig>()?.CacheSettings?.TypeDatabase == CacheType.Redis)
services.AddSingleton<ICacheService, DistributedCacheService>();
else
services.AddSingleton<ICacheService, MemoryCacheService>();

View File

@ -1,4 +1,5 @@
using Mirea.Api.Endpoint.Configuration.Validation.Attributes;
using Mirea.Api.Dto.Common;
using Mirea.Api.Endpoint.Configuration.Validation.Attributes;
using Mirea.Api.Endpoint.Configuration.Validation.Interfaces;
namespace Mirea.Api.Endpoint.Configuration.Model.GeneralSettings;
@ -6,18 +7,12 @@ namespace Mirea.Api.Endpoint.Configuration.Model.GeneralSettings;
[RequiredSettings]
public class CacheSettings : IIsConfigured
{
public enum CacheEnum
{
Memcached,
Redis
}
public CacheEnum TypeDatabase { get; set; }
public CacheType TypeDatabase { get; set; }
public string? ConnectionString { get; set; }
public bool IsConfigured()
{
return TypeDatabase == CacheEnum.Memcached ||
return TypeDatabase == CacheType.Memcached ||
!string.IsNullOrEmpty(ConnectionString);
}
}

View File

@ -1,4 +1,5 @@
using Mirea.Api.DataAccess.Persistence.Common;
using Mirea.Api.Dto.Common;
using Mirea.Api.Endpoint.Configuration.Validation.Attributes;
using Mirea.Api.Endpoint.Configuration.Validation.Interfaces;
using System;
@ -9,22 +10,16 @@ namespace Mirea.Api.Endpoint.Configuration.Model.GeneralSettings;
[RequiredSettings]
public class DbSettings : IIsConfigured
{
public enum DatabaseEnum
{
Mysql,
Sqlite,
PostgresSql
}
public DatabaseEnum TypeDatabase { get; set; }
public DatabaseType TypeDatabase { get; set; }
public required string ConnectionStringSql { get; set; }
[JsonIgnore]
public DatabaseProvider DatabaseProvider =>
TypeDatabase switch
{
DatabaseEnum.PostgresSql => DatabaseProvider.Postgresql,
DatabaseEnum.Mysql => DatabaseProvider.Mysql,
DatabaseEnum.Sqlite => DatabaseProvider.Sqlite,
DatabaseType.PostgresSql => DatabaseProvider.Postgresql,
DatabaseType.Mysql => DatabaseProvider.Mysql,
DatabaseType.Sqlite => DatabaseProvider.Sqlite,
_ => throw new ArgumentOutOfRangeException()
};

View File

@ -4,15 +4,21 @@ using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Data.Sqlite;
using Microsoft.Extensions.Caching.Memory;
using Microsoft.Extensions.Options;
using Mirea.Api.Dto.Common;
using Mirea.Api.Dto.Requests;
using Mirea.Api.Dto.Requests.Configuration;
using Mirea.Api.Dto.Responses;
using Mirea.Api.Dto.Responses.Configuration;
using Mirea.Api.Endpoint.Common.Attributes;
using Mirea.Api.Endpoint.Common.Exceptions;
using Mirea.Api.Endpoint.Common.Interfaces;
using Mirea.Api.Endpoint.Common.MapperDto;
using Mirea.Api.Endpoint.Common.Services;
using Mirea.Api.Endpoint.Configuration.Model;
using Mirea.Api.Endpoint.Configuration.Model.GeneralSettings;
using Mirea.Api.Endpoint.Configuration.Validation.Validators;
using Mirea.Api.Security.Common.Domain;
using Mirea.Api.Security.Services;
using MySqlConnector;
using Npgsql;
@ -36,7 +42,8 @@ public class SetupController(
ISetupToken setupToken,
IMaintenanceModeNotConfigureService notConfigureService,
IMemoryCache cache,
PasswordHashService passwordHashService) : BaseController
PasswordHashService passwordHashService,
IOptionsSnapshot<Admin> user) : BaseController
{
private const string CacheGeneralKey = "config_general";
private const string CacheAdminKey = "config_admin";
@ -95,7 +102,12 @@ public class SetupController(
return Ok(true);
}
private ActionResult<bool> SetDatabase<TConnection, TException>(string connectionString, DbSettings.DatabaseEnum databaseType)
[HttpGet("IsConfiguredToken")]
[TokenAuthentication]
public ActionResult<bool> IsConfiguredToken() =>
Ok(true);
private void SetDatabase<TConnection, TException>(string connectionString, DatabaseType databaseType)
where TConnection : class, IDbConnection, new()
where TException : Exception
{
@ -118,8 +130,6 @@ public class SetupController(
TypeDatabase = databaseType
};
GeneralConfig = general;
return Ok(true);
}
catch (TException ex)
{
@ -138,7 +148,20 @@ public class SetupController(
if (request.Ssl)
connectionString += ";SSL Mode=Require;";
return SetDatabase<NpgsqlConnection, NpgsqlException>(connectionString, DbSettings.DatabaseEnum.PostgresSql);
SetDatabase<NpgsqlConnection, NpgsqlException>(connectionString, DatabaseType.PostgresSql);
cache.Set("database", new DatabaseResponse
{
Type = DatabaseType.PostgresSql,
Database = request.Database,
Password = request.Password,
Ssl = request.Ssl,
Port = request.Port,
Server = request.Server,
User = request.User
});
return Ok(true);
}
[HttpPost("SetMysql")]
@ -152,7 +175,19 @@ public class SetupController(
if (request.Ssl)
connectionString += "SslMode=Require;";
return SetDatabase<MySqlConnection, MySqlException>(connectionString, DbSettings.DatabaseEnum.Mysql);
SetDatabase<MySqlConnection, MySqlException>(connectionString, DatabaseType.Mysql);
cache.Set("database", new DatabaseResponse
{
Type = DatabaseType.Mysql,
Database = request.Database,
Password = request.Password,
Ssl = request.Ssl,
Port = request.Port,
Server = request.Server,
User = request.User
});
return Ok(true);
}
[HttpPost("SetSqlite")]
@ -179,14 +214,26 @@ public class SetupController(
var filePath = Path.Combine(path, "database.db3");
var connectionString = $"Data Source={filePath}";
var result = SetDatabase<SqliteConnection, SqliteException>(connectionString, DbSettings.DatabaseEnum.Sqlite);
SetDatabase<SqliteConnection, SqliteException>(connectionString, DatabaseType.Sqlite);
foreach (var file in Directory.GetFiles(path))
System.IO.File.Delete(file);
return result;
cache.Set("database", new DatabaseResponse
{
Type = DatabaseType.Sqlite,
PathToDatabase = path
});
return Ok(true);
}
[HttpGet("DatabaseConfiguration")]
[TokenAuthentication]
[ProducesResponseType(StatusCodes.Status204NoContent)]
public ActionResult<DatabaseRequest> DatabaseConfiguration() =>
cache.TryGetValue<DatabaseResponse>("database", out var response) ? Ok(response) : NoContent();
[HttpPost("SetRedis")]
[TokenAuthentication]
[BadRequestResponse]
@ -205,10 +252,17 @@ public class SetupController(
general.CacheSettings = new CacheSettings
{
ConnectionString = connectionString,
TypeDatabase = CacheSettings.CacheEnum.Redis
TypeDatabase = CacheType.Redis
};
GeneralConfig = general;
cache.Set("cache", new CacheResponse
{
Type = CacheType.Redis,
Server = request.Server,
Password = request.Password,
Port = request.Port
});
return Ok(true);
}
catch (Exception ex)
@ -226,20 +280,29 @@ public class SetupController(
general.CacheSettings = new CacheSettings
{
ConnectionString = null,
TypeDatabase = CacheSettings.CacheEnum.Memcached
TypeDatabase = CacheType.Memcached
};
GeneralConfig = general;
cache.Set("cache", new CacheResponse
{
Type = CacheType.Memcached
});
return Ok(true);
}
[HttpGet("CacheConfiguration")]
[TokenAuthentication]
[ProducesResponseType(StatusCodes.Status204NoContent)]
public ActionResult<CacheResponse> CacheConfiguration() =>
cache.TryGetValue<CacheResponse>("cache", out var response) ? Ok(response) : NoContent();
[HttpPost("CreateAdmin")]
[TokenAuthentication]
[BadRequestResponse]
public ActionResult<string> 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.");
new PasswordPolicyService(GeneralConfig.PasswordPolicy).ValidatePasswordOrThrow(user.Password);
if (!MailAddress.TryCreate(user.Email, out _))
throw new ControllerArgumentException("The email address is incorrect.");
@ -258,6 +321,73 @@ public class SetupController(
return Ok(true);
}
[HttpGet("UpdateAdminConfiguration")]
[TokenAuthentication]
public ActionResult UpdateAdminConfiguration()
{
if (string.IsNullOrEmpty(user.Value.Email))
return Ok();
if (!cache.TryGetValue<Admin>(CacheAdminKey, out var admin))
{
admin = user.Value;
cache.Set(CacheAdminKey, admin);
return Ok();
}
admin!.OAuthProviders = user.Value.OAuthProviders;
if (string.IsNullOrEmpty(admin.Email))
admin.Email = user.Value.Email;
if (string.IsNullOrEmpty(admin.Username))
admin.Username = user.Value.Username;
cache.Set(CacheAdminKey, admin);
return Ok();
}
[HttpGet("AdminConfiguration")]
[TokenAuthentication]
[ProducesResponseType(StatusCodes.Status204NoContent)]
public ActionResult<UserResponse> AdminConfiguration() =>
cache.TryGetValue<Admin>(CacheAdminKey, out var admin) ? Ok(new UserResponse()
{
Email = admin!.Email,
Username = admin.Username,
TwoFactorAuthenticatorEnabled = admin.TwoFactorAuthenticator != TwoFactorAuthenticator.None,
UsedOAuthProviders = admin.OAuthProviders == null ? [] : admin.OAuthProviders.Keys.Select(x => x.ConvertToDto())
}) : NoContent();
[HttpGet("GenerateTotpKey")]
[TokenAuthentication]
public ActionResult<string> GenerateTotpKey()
{
if (cache.TryGetValue<string>("totpSecret", out var secret))
return secret!;
secret = GeneratorKey.GenerateAlphaNumericBase32Compatible(16);
cache.Set("totpSecret", secret);
return secret;
}
[HttpGet("VerifyTotp")]
[TokenAuthentication]
public ActionResult<bool> VerifyTotp([FromQuery] string code)
{
var isCorrect = cache.TryGetValue<string>("totpSecret", out var secret) &&
new TotpService(secret!).VerifyToken(code);
if (!isCorrect || !cache.TryGetValue<Admin>(CacheAdminKey, out var admin))
return false;
admin!.Secret = secret;
admin.TwoFactorAuthenticator = TwoFactorAuthenticator.Totp;
cache.Set(CacheAdminKey, admin);
return true;
}
[HttpPost("SetLogging")]
[TokenAuthentication]
[BadRequestResponse]
@ -292,9 +422,22 @@ public class SetupController(
general.LogSettings = settings;
GeneralConfig = general;
cache.Set("logging", new LoggingRequest
{
EnableLogToFile = settings.EnableLogToFile,
LogFileName = settings.LogFileName,
LogFilePath = settings.LogFilePath
});
return true;
}
[HttpGet("LoggingConfiguration")]
[TokenAuthentication]
[ProducesResponseType(StatusCodes.Status204NoContent)]
public ActionResult<LoggingRequest> LoggingConfiguration() =>
cache.TryGetValue<LoggingRequest>("logging", out var data) ? Ok(data) : NoContent();
[HttpPost("SetEmail")]
[TokenAuthentication]
[BadRequestResponse]
@ -318,9 +461,16 @@ public class SetupController(
general.EmailSettings = settings;
GeneralConfig = general;
cache.Set("email", settings);
return true;
}
[HttpGet("EmailConfiguration")]
[TokenAuthentication]
[ProducesResponseType(StatusCodes.Status204NoContent)]
public ActionResult<EmailRequest> EmailConfiguration() =>
cache.TryGetValue<EmailRequest>("email", out var data) ? Ok(data) : NoContent();
[HttpPost("SetSchedule")]
[TokenAuthentication]
[BadRequestResponse]
@ -349,8 +499,20 @@ public class SetupController(
GeneralConfig = general;
cache.Set("schedule", new ScheduleConfigurationRequest()
{
StartTerm = general.ScheduleSettings.StartTerm,
CronUpdateSchedule = general.ScheduleSettings.CronUpdateSchedule
});
return true;
}
[HttpGet("ScheduleConfiguration")]
[TokenAuthentication]
[ProducesResponseType(StatusCodes.Status204NoContent)]
public ActionResult<ScheduleConfigurationRequest> ScheduleConfiguration() =>
cache.TryGetValue<ScheduleConfigurationRequest>("schedule", out var data) ? Ok(data) : NoContent();
[HttpPost("SetPasswordPolicy")]
[TokenAuthentication]
public ActionResult<bool> SetPasswordPolicy([FromBody] PasswordPolicy? policy = null)