diff --git a/ApiDto/Common/CacheType.cs b/ApiDto/Common/CacheType.cs
new file mode 100644
index 0000000..d8a719d
--- /dev/null
+++ b/ApiDto/Common/CacheType.cs
@@ -0,0 +1,17 @@
+namespace Mirea.Api.Dto.Common;
+
+///
+/// Specifies the types of caching mechanisms available.
+///
+public enum CacheType
+{
+ ///
+ /// Memcached caching type.
+ ///
+ Memcached,
+
+ ///
+ /// Redis caching type.
+ ///
+ Redis
+}
\ No newline at end of file
diff --git a/ApiDto/Common/DatabaseType.cs b/ApiDto/Common/DatabaseType.cs
new file mode 100644
index 0000000..6e413b2
--- /dev/null
+++ b/ApiDto/Common/DatabaseType.cs
@@ -0,0 +1,22 @@
+namespace Mirea.Api.Dto.Common;
+
+///
+/// Specifies the types of databases supported.
+///
+public enum DatabaseType
+{
+ ///
+ /// MySQL database type.
+ ///
+ Mysql,
+
+ ///
+ /// SQLite database type.
+ ///
+ Sqlite,
+
+ ///
+ /// PostgreSQL database type.
+ ///
+ PostgresSql
+}
\ No newline at end of file
diff --git a/ApiDto/Requests/CreateUserRequest.cs b/ApiDto/Requests/CreateUserRequest.cs
index 3bcf70c..e1b3a4c 100644
--- a/ApiDto/Requests/CreateUserRequest.cs
+++ b/ApiDto/Requests/CreateUserRequest.cs
@@ -10,27 +10,18 @@ public class CreateUserRequest
///
/// Gets or sets the email address of the user.
///
- ///
- /// The email address is a required field.
- ///
[Required]
public required string Email { get; set; }
///
/// Gets or sets the username of the user.
///
- ///
- /// The username is a required field.
- ///
[Required]
public required string Username { get; set; }
///
/// Gets or sets the password of the user.
///
- ///
- /// The password is a required field.
- ///
[Required]
public required string Password { get; set; }
}
diff --git a/ApiDto/Responses/AvailableProvidersResponse.cs b/ApiDto/Responses/AvailableOAuthProvidersResponse.cs
similarity index 66%
rename from ApiDto/Responses/AvailableProvidersResponse.cs
rename to ApiDto/Responses/AvailableOAuthProvidersResponse.cs
index c4e9dfc..bb8e13c 100644
--- a/ApiDto/Responses/AvailableProvidersResponse.cs
+++ b/ApiDto/Responses/AvailableOAuthProvidersResponse.cs
@@ -1,16 +1,17 @@
using Mirea.Api.Dto.Common;
-using System;
+using System.ComponentModel.DataAnnotations;
namespace Mirea.Api.Dto.Responses;
///
/// Represents the response containing information about available OAuth providers.
///
-public class AvailableProvidersResponse
+public class AvailableOAuthProvidersResponse
{
///
/// Gets or sets the name of the OAuth provider.
///
+ [Required]
public required string ProviderName { get; set; }
///
@@ -19,7 +20,8 @@ public class AvailableProvidersResponse
public OAuthProvider Provider { get; set; }
///
- /// Gets or sets the redirect URI for the OAuth provider.
+ /// Gets or sets the redirect URL for the OAuth provider's authorization process.
///
- public required Uri Redirect { get; set; }
+ [Required]
+ public required string Redirect { get; set; }
}
\ No newline at end of file
diff --git a/ApiDto/Responses/Configuration/CacheResponse.cs b/ApiDto/Responses/Configuration/CacheResponse.cs
new file mode 100644
index 0000000..70671de
--- /dev/null
+++ b/ApiDto/Responses/Configuration/CacheResponse.cs
@@ -0,0 +1,29 @@
+using Mirea.Api.Dto.Common;
+
+namespace Mirea.Api.Dto.Responses.Configuration;
+
+///
+/// Represents a response containing cache configuration details.
+///
+public class CacheResponse
+{
+ ///
+ /// Gets or sets the type of cache database.
+ ///
+ public CacheType Type { get; set; }
+
+ ///
+ /// Gets or sets the server address.
+ ///
+ public string? Server { get; set; }
+
+ ///
+ /// Gets or sets the port number.
+ ///
+ public int Port { get; set; }
+
+ ///
+ /// Gets or sets the password.
+ ///
+ public string? Password { get; set; }
+}
\ No newline at end of file
diff --git a/ApiDto/Responses/Configuration/DatabaseResponse.cs b/ApiDto/Responses/Configuration/DatabaseResponse.cs
new file mode 100644
index 0000000..76baad7
--- /dev/null
+++ b/ApiDto/Responses/Configuration/DatabaseResponse.cs
@@ -0,0 +1,49 @@
+using Mirea.Api.Dto.Common;
+
+namespace Mirea.Api.Dto.Responses.Configuration;
+
+///
+/// Represents a response containing database configuration details.
+///
+public class DatabaseResponse
+{
+ ///
+ /// Gets or sets the type of database.
+ ///
+ public DatabaseType Type { get; set; }
+
+ ///
+ /// Gets or sets the server address.
+ ///
+ public string? Server { get; set; }
+
+ ///
+ /// Gets or sets the port number.
+ ///
+ public int Port { get; set; }
+
+ ///
+ /// Gets or sets the database name.
+ ///
+ public string? Database { get; set; }
+
+ ///
+ /// Gets or sets the username.
+ ///
+ public string? User { get; set; }
+
+ ///
+ /// Gets or sets a value indicating whether SSL is enabled.
+ ///
+ public bool Ssl { get; set; }
+
+ ///
+ /// Gets or sets the password.
+ ///
+ public string? Password { get; set; }
+
+ ///
+ /// Gets or sets the path to database. Only for Sqlite
+ ///
+ public string? PathToDatabase { get; set; }
+}
\ No newline at end of file
diff --git a/ApiDto/Responses/TotpKeyResponse.cs b/ApiDto/Responses/TotpKeyResponse.cs
new file mode 100644
index 0000000..70a5865
--- /dev/null
+++ b/ApiDto/Responses/TotpKeyResponse.cs
@@ -0,0 +1,17 @@
+namespace Mirea.Api.Dto.Responses;
+
+///
+/// Represents the response containing the TOTP (Time-Based One-Time Password) key details.
+///
+public class TotpKeyResponse
+{
+ ///
+ /// Gets or sets the secret key used for TOTP generation.
+ ///
+ public required string Secret { get; set; }
+
+ ///
+ /// Gets or sets the image (QR code) representing the TOTP key.
+ ///
+ public required string Image { get; set; }
+}
\ No newline at end of file
diff --git a/ApiDto/Responses/UserResponse.cs b/ApiDto/Responses/UserResponse.cs
new file mode 100644
index 0000000..16f8a9d
--- /dev/null
+++ b/ApiDto/Responses/UserResponse.cs
@@ -0,0 +1,35 @@
+using Mirea.Api.Dto.Common;
+using System.Collections.Generic;
+using System.ComponentModel.DataAnnotations;
+
+namespace Mirea.Api.Dto.Responses;
+
+///
+/// Represents a response containing user information.
+///
+public class UserResponse
+{
+ ///
+ /// Gets or sets the email address of the user.
+ ///
+ [Required]
+ public required string Email { get; set; }
+
+ ///
+ /// Gets or sets the username of the user.
+ ///
+ [Required]
+ public required string Username { get; set; }
+
+ ///
+ /// Gets or sets a value indicating whether the user has two-factor authentication enabled.
+ ///
+ [Required]
+ public bool TwoFactorAuthenticatorEnabled { get; set; }
+
+ ///
+ /// Gets or sets a collection of OAuth providers used by the user.
+ ///
+ [Required]
+ public required IEnumerable UsedOAuthProviders { get; set; }
+}
\ No newline at end of file
diff --git a/Endpoint/Configuration/Core/Startup/CacheConfiguration.cs b/Endpoint/Configuration/Core/Startup/CacheConfiguration.cs
index ca3ddb8..7cac5c9 100644
--- a/Endpoint/Configuration/Core/Startup/CacheConfiguration.cs
+++ b/Endpoint/Configuration/Core/Startup/CacheConfiguration.cs
@@ -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()?.CacheSettings;
- if (cache?.TypeDatabase != CacheSettings.CacheEnum.Redis)
+ if (cache?.TypeDatabase != CacheType.Redis)
return services;
services.AddStackExchangeRedisCache(options =>
diff --git a/Endpoint/Configuration/Core/Startup/SecureConfiguration.cs b/Endpoint/Configuration/Core/Startup/SecureConfiguration.cs
index f9f7a81..c2671d7 100644
--- a/Endpoint/Configuration/Core/Startup/SecureConfiguration.cs
+++ b/Endpoint/Configuration/Core/Startup/SecureConfiguration.cs
@@ -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();
- if (configuration.Get()?.CacheSettings?.TypeDatabase == CacheSettings.CacheEnum.Redis)
+ if (configuration.Get()?.CacheSettings?.TypeDatabase == CacheType.Redis)
services.AddSingleton();
else
services.AddSingleton();
diff --git a/Endpoint/Configuration/Model/GeneralSettings/CacheSettings.cs b/Endpoint/Configuration/Model/GeneralSettings/CacheSettings.cs
index c5e5dc9..af0acb3 100644
--- a/Endpoint/Configuration/Model/GeneralSettings/CacheSettings.cs
+++ b/Endpoint/Configuration/Model/GeneralSettings/CacheSettings.cs
@@ -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);
}
}
\ No newline at end of file
diff --git a/Endpoint/Configuration/Model/GeneralSettings/DbSettings.cs b/Endpoint/Configuration/Model/GeneralSettings/DbSettings.cs
index 28921e9..c839fd4 100644
--- a/Endpoint/Configuration/Model/GeneralSettings/DbSettings.cs
+++ b/Endpoint/Configuration/Model/GeneralSettings/DbSettings.cs
@@ -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()
};
diff --git a/Endpoint/Controllers/Configuration/SetupController.cs b/Endpoint/Controllers/Configuration/SetupController.cs
index ecc21c2..e1780f2 100644
--- a/Endpoint/Controllers/Configuration/SetupController.cs
+++ b/Endpoint/Controllers/Configuration/SetupController.cs
@@ -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 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 SetDatabase(string connectionString, DbSettings.DatabaseEnum databaseType)
+ [HttpGet("IsConfiguredToken")]
+ [TokenAuthentication]
+ public ActionResult IsConfiguredToken() =>
+ Ok(true);
+
+ private void SetDatabase(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(connectionString, DbSettings.DatabaseEnum.PostgresSql);
+
+ SetDatabase(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(connectionString, DbSettings.DatabaseEnum.Mysql);
+ SetDatabase(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(connectionString, DbSettings.DatabaseEnum.Sqlite);
+ SetDatabase(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 DatabaseConfiguration() =>
+ cache.TryGetValue("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 CacheConfiguration() =>
+ cache.TryGetValue("cache", out var response) ? Ok(response) : NoContent();
+
[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.");
+ 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(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 AdminConfiguration() =>
+ cache.TryGetValue(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 GenerateTotpKey()
+ {
+ if (cache.TryGetValue("totpSecret", out var secret))
+ return secret!;
+
+ secret = GeneratorKey.GenerateAlphaNumericBase32Compatible(16);
+ cache.Set("totpSecret", secret);
+ return secret;
+ }
+
+ [HttpGet("VerifyTotp")]
+ [TokenAuthentication]
+ public ActionResult VerifyTotp([FromQuery] string code)
+ {
+ var isCorrect = cache.TryGetValue("totpSecret", out var secret) &&
+ new TotpService(secret!).VerifyToken(code);
+
+ if (!isCorrect || !cache.TryGetValue(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 LoggingConfiguration() =>
+ cache.TryGetValue("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 EmailConfiguration() =>
+ cache.TryGetValue("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 ScheduleConfiguration() =>
+ cache.TryGetValue("schedule", out var data) ? Ok(data) : NoContent();
+
[HttpPost("SetPasswordPolicy")]
[TokenAuthentication]
public ActionResult SetPasswordPolicy([FromBody] PasswordPolicy? policy = null)