mirror of
https://github.com/MAKS-IT-COM/maksit-certs-ui.git
synced 2025-12-31 04:00:03 +01:00
(feature): simple login init
This commit is contained in:
parent
1e2d4156a5
commit
c6dbf6d195
@ -8,7 +8,7 @@
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="MaksIT.Core" Version="1.5.2" />
|
||||
<PackageReference Include="MaksIT.Core" Version="1.5.3" />
|
||||
<PackageReference Include="MaksIT.Results" Version="1.1.1" />
|
||||
<PackageReference Include="Microsoft.Extensions.Caching.Abstractions" Version="9.0.10" />
|
||||
<PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="9.0.10" />
|
||||
|
||||
64
src/LetsEncryptServer/BackgroundServices/Initialization.cs
Normal file
64
src/LetsEncryptServer/BackgroundServices/Initialization.cs
Normal file
@ -0,0 +1,64 @@
|
||||
using Microsoft.Extensions.Options;
|
||||
using MaksIT.LetsEncryptServer.Domain;
|
||||
using MaksIT.LetsEncryptServer.Services;
|
||||
|
||||
|
||||
namespace MaksIT.LetsEncryptServer.BackgroundServices {
|
||||
|
||||
public class Initialization : BackgroundService {
|
||||
private readonly IServiceProvider _serviceProvider;
|
||||
private readonly Configuration _appSettings;
|
||||
private readonly ISettingsService _settingsService;
|
||||
|
||||
public Initialization(
|
||||
IOptions<Configuration> appSettings,
|
||||
IServiceProvider serviceProvider,
|
||||
ISettingsService settingsService
|
||||
) {
|
||||
_appSettings = appSettings.Value;
|
||||
_serviceProvider = serviceProvider;
|
||||
_settingsService = settingsService;
|
||||
}
|
||||
|
||||
|
||||
|
||||
protected override async Task ExecuteAsync(CancellationToken stoppingToken) {
|
||||
// using var scope = _serviceProvider.CreateScope();
|
||||
// TODO: Add your user initialization logic here.
|
||||
// Example:
|
||||
// var userService = scope.ServiceProvider.GetRequiredService<IUserService>();
|
||||
// await userService.InitializeUsersAsync();
|
||||
|
||||
var dataDir = Path.Combine(Path.DirectorySeparatorChar.ToString(), "data");
|
||||
var settingsPath = Path.Combine(dataDir, "settings.json");
|
||||
|
||||
// Ensure the data directory exists
|
||||
if (!Directory.Exists(dataDir)) {
|
||||
Directory.CreateDirectory(dataDir);
|
||||
}
|
||||
|
||||
var loadSettingsResult = await _settingsService.LoadAsync();
|
||||
if (!loadSettingsResult.IsSuccess || loadSettingsResult.Value == null) {
|
||||
throw new Exception("Failed to load settings.");
|
||||
}
|
||||
|
||||
var settings = loadSettingsResult.Value;
|
||||
|
||||
if (!settings.Init) {
|
||||
var initializeResult = settings.Initialize(_appSettings.Auth.Pepper);
|
||||
if (!initializeResult.IsSuccess || initializeResult.Value == null)
|
||||
throw new Exception(string.Join(", ", initializeResult.Messages));
|
||||
|
||||
settings = initializeResult.Value;
|
||||
|
||||
var saveResult = await _settingsService.SaveAsync(settings);
|
||||
if (!saveResult.IsSuccess) {
|
||||
throw new Exception("Failed to save initialized settings.");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
await Task.CompletedTask;
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -9,7 +9,26 @@ namespace MaksIT.LetsEncryptServer {
|
||||
public required string ServiceToReload { get; set; }
|
||||
}
|
||||
|
||||
public class Auth {
|
||||
|
||||
public required string Secret { get; set; }
|
||||
|
||||
public required string Issuer { get; set; }
|
||||
public required string Audience { get; set; }
|
||||
public required int Expiration { get; set; }
|
||||
|
||||
public required int RefreshExpiration { get; set; }
|
||||
|
||||
|
||||
public required string Pepper { get; set; }
|
||||
|
||||
}
|
||||
|
||||
public class Configuration : ILetsEncryptConfiguration {
|
||||
public required Auth Auth { get; set; }
|
||||
|
||||
public required string SettingsFile { get; set; }
|
||||
|
||||
public required string Production { get; set; }
|
||||
public required string Staging { get; set; }
|
||||
|
||||
|
||||
@ -8,7 +8,7 @@ namespace MaksIT.LetsEncryptServer.Controllers;
|
||||
[ApiController]
|
||||
[Route("api")]
|
||||
public class AccountController : ControllerBase {
|
||||
private readonly IAccountRestService _accountService;
|
||||
private readonly IAccountService _accountService;
|
||||
|
||||
public AccountController(
|
||||
IAccountService accountService
|
||||
|
||||
49
src/LetsEncryptServer/Controllers/IdentityController.cs
Normal file
49
src/LetsEncryptServer/Controllers/IdentityController.cs
Normal file
@ -0,0 +1,49 @@
|
||||
using MaksIT.LetsEncryptServer.Services;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Models.LetsEncryptServer.Identity.Login;
|
||||
using Models.LetsEncryptServer.Identity.Logout;
|
||||
|
||||
|
||||
namespace MaksIT.LetsEncryptServer.Controllers;
|
||||
|
||||
[ApiController]
|
||||
[Route("api/identity")]
|
||||
public class IdentityController(
|
||||
IIdentityService identityService
|
||||
) : ControllerBase {
|
||||
|
||||
private readonly IIdentityService _identityService = identityService;
|
||||
|
||||
#region Login/Refresh/Logout
|
||||
[HttpPost("login")]
|
||||
[ProducesResponseType(typeof(LoginResponse), StatusCodes.Status200OK)]
|
||||
public async Task<IActionResult> Login([FromBody] LoginRequest requestData) {
|
||||
var result = await _identityService.LoginAsync(requestData);
|
||||
return result.ToActionResult();
|
||||
}
|
||||
|
||||
//[HttpPost("refresh")]
|
||||
//[ProducesResponseType(typeof(LoginResponse), StatusCodes.Status200OK)]
|
||||
//public async Task<IActionResult> RefreshToken([FromBody] RefreshTokenRequest requestData) {
|
||||
// var result = await _identityService.RefreshTokenAsync(requestData);
|
||||
// return result.ToActionResult();
|
||||
//}
|
||||
|
||||
//[ServiceFilter(typeof(JwtAuthorizationFilter))]
|
||||
//[HttpPost("logout")]
|
||||
//[ProducesResponseType(StatusCodes.Status200OK)]
|
||||
//public async Task<IActionResult> Logout([FromBody] LogoutRequest requetData) {
|
||||
// var jwtTokenDataResult = HttpContext.GetJwtTokenData();
|
||||
// if (!jwtTokenDataResult.IsSuccess || jwtTokenDataResult.Value == null)
|
||||
// return jwtTokenDataResult.ToActionResult();
|
||||
|
||||
// var jwtTokenData = jwtTokenDataResult.Value;
|
||||
|
||||
// var result = await _identityService.Logout(jwtTokenData, requetData);
|
||||
// return result.ToActionResult();
|
||||
//}
|
||||
#endregion
|
||||
|
||||
|
||||
|
||||
}
|
||||
59
src/LetsEncryptServer/Domain/Settings.cs
Normal file
59
src/LetsEncryptServer/Domain/Settings.cs
Normal file
@ -0,0 +1,59 @@
|
||||
using MaksIT.Core.Abstractions.Domain;
|
||||
using MaksIT.Core.Security;
|
||||
using MaksIT.Results;
|
||||
|
||||
namespace MaksIT.LetsEncryptServer.Domain;
|
||||
|
||||
public class Settings : DomainObjectBase {
|
||||
public bool Init { get; set; }
|
||||
public List<User> Users { get; set; } = [];
|
||||
|
||||
public Settings() {}
|
||||
|
||||
public Result<Settings?> Initialize(string pepper) {
|
||||
var userResult = new User("admin")
|
||||
.SetPassword("password", pepper);
|
||||
|
||||
if (!userResult.IsSuccess || userResult.Value == null) {
|
||||
return userResult.ToResultOfType<Settings?>(_ => null);
|
||||
}
|
||||
|
||||
Init = true;
|
||||
Users = [userResult.Value];
|
||||
|
||||
return Result<Settings?>.Ok(this);
|
||||
}
|
||||
|
||||
public Result<User?> GetUserByName(string name) {
|
||||
|
||||
var user = Users.FirstOrDefault(x => x.Name == name);
|
||||
|
||||
if (user == null)
|
||||
return Result<User?>.NotFound(null, "User not found.");
|
||||
|
||||
return Result<User?>.Ok(user);
|
||||
}
|
||||
|
||||
public Result<Settings?> AddUser(string name, string password, string pepper) {
|
||||
var setPasswordResult = new User(name)
|
||||
.SetPassword(password, pepper);
|
||||
|
||||
if (!setPasswordResult.IsSuccess || setPasswordResult.Value == null)
|
||||
return setPasswordResult.ToResultOfType<Settings?>(_ => null);
|
||||
|
||||
var user = setPasswordResult.Value;
|
||||
|
||||
Users.Add(user);
|
||||
|
||||
return Result<Settings?>.Ok(this);
|
||||
}
|
||||
|
||||
public Result<Settings?> RemoveUser(string name) {
|
||||
if (Users.Any(x => x.Name == name)) {
|
||||
Users = [.. Users.Where(u => u.Name != name)];
|
||||
return Result<Settings?>.Ok(this);
|
||||
}
|
||||
|
||||
return Result<Settings?>.NotFound(null, "User not found.");
|
||||
}
|
||||
}
|
||||
64
src/LetsEncryptServer/Domain/User.cs
Normal file
64
src/LetsEncryptServer/Domain/User.cs
Normal file
@ -0,0 +1,64 @@
|
||||
using MaksIT.Core.Abstractions.Domain;
|
||||
using MaksIT.Core.Security;
|
||||
using MaksIT.Results;
|
||||
|
||||
namespace MaksIT.LetsEncryptServer.Domain;
|
||||
|
||||
public class User(
|
||||
Guid id,
|
||||
string name
|
||||
) : DomainDocumentBase<Guid>(id) {
|
||||
public string Name { get; private set; } = name;
|
||||
public string Salt { get; private set; } = string.Empty;
|
||||
public string Hash { get; private set; } = string.Empty;
|
||||
|
||||
public User(
|
||||
string name
|
||||
) : this(
|
||||
Guid.NewGuid(),
|
||||
name
|
||||
) { }
|
||||
|
||||
|
||||
// Set or change password (returns this for chaining)
|
||||
public Result<User?> SetPassword(string password, string pepper) {
|
||||
if (!PasswordHasher.TryCreateSaltedHash(password, pepper, out var saltedHash, out var errorMessage))
|
||||
return Result<User?>.InternalServerError(null, errorMessage);
|
||||
|
||||
Salt = saltedHash.Value.Salt;
|
||||
Hash = saltedHash.Value.Hash;
|
||||
|
||||
return Result<User?>.Ok(this);
|
||||
}
|
||||
|
||||
// Reset password to a new value (returns this for chaining)
|
||||
public Result<User?> ResetPassword(string newPassword, string pepper) => SetPassword(newPassword, pepper);
|
||||
|
||||
// Change user name
|
||||
public User ChangeName(string newName) {
|
||||
if (string.IsNullOrWhiteSpace(newName))
|
||||
throw new ArgumentException("Name cannot be empty.", nameof(newName));
|
||||
Name = newName;
|
||||
return this;
|
||||
}
|
||||
|
||||
// Validate password
|
||||
public Result ValidatePassword(string password, string pepper) {
|
||||
if (PasswordHasher.TryValidateHash(password, Salt, Hash, pepper, out var isValid, out var errorMessage)) {
|
||||
if (isValid)
|
||||
return Result.Ok();
|
||||
|
||||
return Result.Unauthorized("Invalid password.");
|
||||
}
|
||||
|
||||
return Result<User?>.InternalServerError(null, errorMessage);
|
||||
}
|
||||
|
||||
// For persistence
|
||||
public User SeltSaltedHash(string salt, string hash) {
|
||||
Salt = salt;
|
||||
Hash = hash;
|
||||
|
||||
return this;
|
||||
}
|
||||
}
|
||||
6
src/LetsEncryptServer/Dto/SettingsDto.cs
Normal file
6
src/LetsEncryptServer/Dto/SettingsDto.cs
Normal file
@ -0,0 +1,6 @@
|
||||
namespace MaksIT.LetsEncryptServer.Dto;
|
||||
|
||||
public class SettingsDto {
|
||||
public required bool Init { get; set; }
|
||||
public required List<UserDto> Users { get; set; } = [];
|
||||
}
|
||||
8
src/LetsEncryptServer/Dto/UserDto.cs
Normal file
8
src/LetsEncryptServer/Dto/UserDto.cs
Normal file
@ -0,0 +1,8 @@
|
||||
namespace MaksIT.LetsEncryptServer.Dto;
|
||||
|
||||
public class UserDto {
|
||||
public required string Id { get; set; }
|
||||
public required string Name { get; set; }
|
||||
public required string Salt { get; set; }
|
||||
public required string Hash { get; set; }
|
||||
}
|
||||
@ -4,6 +4,7 @@
|
||||
<TargetFramework>net8.0</TargetFramework>
|
||||
<Nullable>enable</Nullable>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<RootNamespace>MaksIT.$(MSBuildProjectName.Replace(" ", "_"))</RootNamespace>
|
||||
<DockerDefaultTargetOS>Linux</DockerDefaultTargetOS>
|
||||
<DockerComposeProjectPath>..\docker-compose.dcproj</DockerComposeProjectPath>
|
||||
</PropertyGroup>
|
||||
|
||||
@ -1,10 +1,10 @@
|
||||
using System.Text.Json.Serialization;
|
||||
using MaksIT.Core.Logging;
|
||||
using MaksIT.Core.Webapi.Middlewares;
|
||||
using MaksIT.LetsEncrypt.Extensions;
|
||||
using MaksIT.LetsEncryptServer;
|
||||
using MaksIT.LetsEncryptServer.BackgroundServices;
|
||||
using MaksIT.LetsEncryptServer.Services;
|
||||
using System.Text.Json.Serialization;
|
||||
using MaksIT.LetsEncryptServer.BackgroundServices;
|
||||
|
||||
var builder = WebApplication.CreateBuilder(args);
|
||||
|
||||
@ -48,11 +48,19 @@ builder.Services.AddMemoryCache();
|
||||
|
||||
builder.Services.RegisterLetsEncrypt(appSettings);
|
||||
|
||||
#region Work with files concurrently
|
||||
builder.Services.AddSingleton<ICacheService, CacheService>();
|
||||
builder.Services.AddSingleton<ISettingsService, SettingsService>();
|
||||
#endregion
|
||||
|
||||
builder.Services.AddHttpClient<ICertsFlowService, CertsFlowService>();
|
||||
builder.Services.AddSingleton<IAccountService, AccountService>();
|
||||
builder.Services.AddHttpClient<IAgentService, AgentService>();
|
||||
builder.Services.AddScoped<IAccountService, AccountService>();
|
||||
builder.Services.AddScoped<IIdentityService, IdentityService>();
|
||||
|
||||
// Hosted services
|
||||
builder.Services.AddHostedService<AutoRenewal>();
|
||||
builder.Services.AddHostedService<Initialization>();
|
||||
|
||||
var app = builder.Build();
|
||||
|
||||
|
||||
@ -2,22 +2,14 @@
|
||||
using LetsEncryptServer.Abstractions;
|
||||
using MaksIT.Core.Webapi.Models;
|
||||
using MaksIT.LetsEncrypt.Entities;
|
||||
using MaksIT.Models;
|
||||
using MaksIT.Models.LetsEncryptServer.Account.Requests;
|
||||
using MaksIT.Models.LetsEncryptServer.Account.Responses;
|
||||
using MaksIT.Results;
|
||||
using System;
|
||||
using static System.Collections.Specialized.BitVector32;
|
||||
|
||||
namespace MaksIT.LetsEncryptServer.Services;
|
||||
|
||||
|
||||
public interface IAccountInternalService {
|
||||
|
||||
}
|
||||
|
||||
|
||||
public interface IAccountRestService {
|
||||
public interface IAccountService {
|
||||
Task<Result<GetAccountResponse[]?>> GetAccountsAsync();
|
||||
Task<Result<GetAccountResponse?>> GetAccountAsync(Guid accountId);
|
||||
Task<Result<GetAccountResponse?>> PostAccountAsync(PostAccountRequest requestData);
|
||||
@ -25,8 +17,6 @@ public interface IAccountRestService {
|
||||
Task<Result> DeleteAccountAsync(Guid accountId);
|
||||
}
|
||||
|
||||
public interface IAccountService : IAccountInternalService, IAccountRestService { }
|
||||
|
||||
public class AccountService : ServiceBase, IAccountService {
|
||||
|
||||
private readonly ILogger<CacheService> _logger;
|
||||
|
||||
@ -195,9 +195,12 @@ public class CertsFlowService : ICertsFlowService {
|
||||
var sessionId = sessionResult.Value.Value;
|
||||
|
||||
var initResult = await InitAsync(sessionId, accountId, description, contacts);
|
||||
if (!initResult.IsSuccess)
|
||||
if (!initResult.IsSuccess || initResult.Value == null)
|
||||
return initResult.ToResultOfType<Guid?>(_ => null);
|
||||
|
||||
if (accountId == null)
|
||||
accountId = initResult.Value;
|
||||
|
||||
var challengesResult = await NewOrderAsync(sessionId, hostnames, challengeType);
|
||||
if (!challengesResult.IsSuccess)
|
||||
return challengesResult.ToResultOfType<Guid?>(_ => null);
|
||||
@ -217,7 +220,7 @@ public class CertsFlowService : ICertsFlowService {
|
||||
return certsResult.ToResultOfType<Guid?>(default);
|
||||
|
||||
if (!isStaging) {
|
||||
var applyCertsResult = await ApplyCertificatesAsync(accountId ?? Guid.Empty);
|
||||
var applyCertsResult = await ApplyCertificatesAsync(accountId.Value);
|
||||
if (!applyCertsResult.IsSuccess)
|
||||
return applyCertsResult.ToResultOfType<Guid?>(_ => null);
|
||||
}
|
||||
|
||||
108
src/LetsEncryptServer/Services/IdentityService.cs
Normal file
108
src/LetsEncryptServer/Services/IdentityService.cs
Normal file
@ -0,0 +1,108 @@
|
||||
using MaksIT.Core.Security.JWT;
|
||||
using MaksIT.LetsEncryptServer.Domain;
|
||||
using MaksIT.Results;
|
||||
using Microsoft.Extensions.Options;
|
||||
using Models.LetsEncryptServer.Identity.Login;
|
||||
using Models.LetsEncryptServer.Identity.Logout;
|
||||
using System.Security.Claims;
|
||||
|
||||
namespace MaksIT.LetsEncryptServer.Services;
|
||||
|
||||
|
||||
public interface IIdentityService {
|
||||
#region Login/Refresh/Logout
|
||||
Task<Result<LoginResponse?>> LoginAsync(LoginRequest requestData);
|
||||
//Task<Result<LoginResponse?>> RefreshTokenAsync(RefreshTokenRequest requestData);
|
||||
//Task<Result> Logout(JwtTokenData jwtTokenData, LogoutRequest requestData);
|
||||
#endregion
|
||||
}
|
||||
|
||||
public class IdentityService(
|
||||
IOptions<Configuration> appsettings,
|
||||
ISettingsService settingsService
|
||||
) : IIdentityService {
|
||||
|
||||
|
||||
private readonly Configuration _appSettings = appsettings.Value;
|
||||
private readonly ISettingsService _settingsService = settingsService;
|
||||
|
||||
#region Login/Refresh/Logout
|
||||
public async Task<Result<LoginResponse?>> LoginAsync(LoginRequest requestData) {
|
||||
|
||||
var loadSettingsResult = await _settingsService.LoadAsync();
|
||||
if (!loadSettingsResult.IsSuccess || loadSettingsResult.Value == null) {
|
||||
return loadSettingsResult.ToResultOfType<LoginResponse?>(_ => null);
|
||||
}
|
||||
|
||||
var settings = loadSettingsResult.Value;
|
||||
|
||||
var userResult = settings.GetUserByName(requestData.Username);
|
||||
if (!userResult.IsSuccess || userResult.Value == null)
|
||||
return userResult.ToResultOfType<LoginResponse?>(_ => null);
|
||||
|
||||
var user = userResult.Value;
|
||||
|
||||
var validatePasswordResult = user.ValidatePassword(requestData.Password, _appSettings.Auth.Pepper);
|
||||
if (!validatePasswordResult.IsSuccess)
|
||||
return validatePasswordResult.ToResultOfType<LoginResponse?>(default);
|
||||
|
||||
if (!JwtGenerator.TryGenerateToken(new JWTTokenGenerateRequest {
|
||||
Secret = _appSettings.Auth.Secret,
|
||||
Issuer = _appSettings.Auth.Issuer,
|
||||
Audience = _appSettings.Auth.Audience,
|
||||
Expiration = _appSettings.Auth.Expiration,
|
||||
UserId = user.Id.ToString(),
|
||||
Username = user.Name,
|
||||
}, out (string token, JWTTokenClaims claims)? tokenData, out string? errorMessage)) {
|
||||
return Result<LoginResponse?>.InternalServerError(null, errorMessage);
|
||||
}
|
||||
|
||||
var (token, claims) = tokenData.Value;
|
||||
|
||||
if (claims.IssuedAt == null || claims.ExpiresAt == null)
|
||||
return Result<LoginResponse?>.InternalServerError(null, "Token claims are missing required fields.");
|
||||
|
||||
string refreshToken = JwtGenerator.GenerateRefreshToken();
|
||||
|
||||
var response = new LoginResponse {
|
||||
TokenType = "Bearer",
|
||||
Token = token,
|
||||
ExpiresAt = claims.ExpiresAt.Value,
|
||||
RefreshToken = refreshToken,
|
||||
RefreshTokenExpiresAt = claims.IssuedAt.Value.AddDays(_appSettings.Auth.RefreshExpiration)
|
||||
};
|
||||
|
||||
return Result<LoginResponse?>.Ok(response);
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
//public async Task<Result<LoginResponse?>> RefreshTokenAsync(RefreshTokenRequest requestData) {
|
||||
// return await HandleTokenResponseAsync(() =>
|
||||
// _identityDomainService.RefreshTokenAsync(requestData.RefreshToken));
|
||||
//}
|
||||
|
||||
//private static async Task<Result<LoginResponse?>> HandleTokenResponseAsync(Func<Task<Result<JwtToken?>>> tokenOperation) {
|
||||
// var jwtTokenResult = await tokenOperation();
|
||||
// if (!jwtTokenResult.IsSuccess || jwtTokenResult.Value == null)
|
||||
// return jwtTokenResult.ToResultOfType<LoginResponse?>(_ => null);
|
||||
|
||||
// var jwtToken = jwtTokenResult.Value;
|
||||
|
||||
// return Result<LoginResponse?>.Ok(new LoginResponse {
|
||||
// TokenType = jwtToken.TokenType,
|
||||
// Token = jwtToken.Token,
|
||||
// ExpiresAt = jwtToken.ExpiresAt,
|
||||
// RefreshToken = jwtToken.RefreshToken,
|
||||
// RefreshTokenExpiresAt = jwtToken.RefreshTokenExpiresAt
|
||||
// });
|
||||
//}
|
||||
|
||||
//public async Task<Result> Logout(JwtTokenData jwtTokenData, LogoutRequest requestData) {
|
||||
// var logoutResult = await _identityDomainService.LogoutAsync(jwtTokenData.Username, jwtTokenData.Token, requestData.LogoutFromAllDevices);
|
||||
// return logoutResult;
|
||||
//}
|
||||
#endregion
|
||||
|
||||
}
|
||||
88
src/LetsEncryptServer/Services/SettingsService.cs
Normal file
88
src/LetsEncryptServer/Services/SettingsService.cs
Normal file
@ -0,0 +1,88 @@
|
||||
using MaksIT.Core.Threading;
|
||||
using MaksIT.LetsEncryptServer.Domain;
|
||||
using MaksIT.LetsEncryptServer.Dto;
|
||||
using MaksIT.Core.Extensions;
|
||||
using MaksIT.Results;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Options;
|
||||
using System.IO;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace MaksIT.LetsEncryptServer.Services;
|
||||
|
||||
public interface ISettingsService {
|
||||
Task<Result<Settings>> LoadAsync();
|
||||
Task<Result> SaveAsync(Settings settings);
|
||||
}
|
||||
|
||||
public class SettingsService : ISettingsService, IDisposable {
|
||||
private readonly ILogger<SettingsService> _logger;
|
||||
private readonly string _settingsPath;
|
||||
private readonly LockManager _lockManager;
|
||||
|
||||
public SettingsService(
|
||||
ILogger<SettingsService> logger,
|
||||
IOptions<Configuration> appSettings
|
||||
) {
|
||||
_logger = logger;
|
||||
_settingsPath = appSettings.Value.SettingsFile;
|
||||
_lockManager = new LockManager();
|
||||
}
|
||||
|
||||
#region Internal I/O
|
||||
|
||||
private async Task<Result<Settings>> LoadInternalAsync() {
|
||||
try {
|
||||
if (!File.Exists(_settingsPath))
|
||||
return Result<Settings>.Ok(new Settings());
|
||||
|
||||
var json = await File.ReadAllTextAsync(_settingsPath);
|
||||
var settingsDto = json.ToObject<SettingsDto>();
|
||||
if (settingsDto == null)
|
||||
return Result<Settings>.InternalServerError(new Settings(), "Settings file is invalid or empty.");
|
||||
|
||||
var settings = new Settings {
|
||||
Init = settingsDto.Init,
|
||||
Users = [.. settingsDto.Users.Select(userDto => new User(userDto.Id.ToGuid(), userDto.Name).SeltSaltedHash(userDto.Salt, userDto.Hash))]
|
||||
};
|
||||
return Result<Settings>.Ok(settings);
|
||||
} catch (Exception ex) {
|
||||
_logger.LogError(ex, "Error loading settings file.");
|
||||
return Result<Settings>.InternalServerError(new Settings(), ex.Message);
|
||||
}
|
||||
}
|
||||
|
||||
private async Task<Result> SaveInternalAsync(Settings settings) {
|
||||
try {
|
||||
var settingsDto = new SettingsDto {
|
||||
Init = settings.Init,
|
||||
Users = [.. settings.Users.Select(u => new UserDto {
|
||||
Id = u.Id.ToString(),
|
||||
Name = u.Name,
|
||||
Salt = u.Salt,
|
||||
Hash = u.Hash
|
||||
})]
|
||||
};
|
||||
await File.WriteAllTextAsync(_settingsPath, settingsDto.ToJson());
|
||||
_logger.LogInformation("Settings file saved.");
|
||||
return Result.Ok();
|
||||
} catch (Exception ex) {
|
||||
_logger.LogError(ex, "Error saving settings file.");
|
||||
return Result.InternalServerError(ex.Message);
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
public async Task<Result<Settings>> LoadAsync() {
|
||||
return await _lockManager.ExecuteWithLockAsync(() => LoadInternalAsync());
|
||||
}
|
||||
|
||||
public async Task<Result> SaveAsync(Settings settings) {
|
||||
return await _lockManager.ExecuteWithLockAsync(() => SaveInternalAsync(settings));
|
||||
}
|
||||
|
||||
public void Dispose() {
|
||||
_lockManager.Dispose();
|
||||
}
|
||||
}
|
||||
@ -8,6 +8,18 @@
|
||||
"AllowedHosts": "*",
|
||||
|
||||
"Configuration": {
|
||||
"SettingsFile": "/data/settings.json",
|
||||
"Auth": {
|
||||
"Secret": "",
|
||||
"Issuer": "",
|
||||
"Audience": "",
|
||||
"Expiration": 60,
|
||||
"RefreshExpiration": 120,
|
||||
|
||||
|
||||
"Pepper": ""
|
||||
},
|
||||
|
||||
"Production": "https://acme-v02.api.letsencrypt.org/directory",
|
||||
"Staging": "https://acme-staging-v02.api.letsencrypt.org/directory",
|
||||
|
||||
|
||||
17
src/MaksIT.WebUI/package-lock.json
generated
17
src/MaksIT.WebUI/package-lock.json
generated
@ -1948,6 +1948,7 @@
|
||||
"integrity": "sha512-WmdoynAX8Stew/36uTSVMcLJJ1KRh6L3IZRx1PZ7qJtBqT3dYTgyDTx8H1qoRghErydW7xw9mSJ3wS//tCRpFA==",
|
||||
"devOptional": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"csstype": "^3.0.2"
|
||||
}
|
||||
@ -2035,6 +2036,7 @@
|
||||
"integrity": "sha512-Zhy8HCvBUEfBECzIl1PKqF4p11+d0aUJS1GeUiuqK9WmOug8YCmC4h4bjyBvMyAMI9sbRczmrYL5lKg/YMbrcQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@typescript-eslint/scope-manager": "8.38.0",
|
||||
"@typescript-eslint/types": "8.38.0",
|
||||
@ -2280,6 +2282,7 @@
|
||||
"integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"bin": {
|
||||
"acorn": "bin/acorn"
|
||||
},
|
||||
@ -3164,6 +3167,7 @@
|
||||
"integrity": "sha512-LSehfdpgMeWcTZkWZVIJl+tkZ2nuSkyyB9C27MZqFWXuph7DvaowgcTvKqxvpLW1JZIk8PN7hFY3Rj9LQ7m7lg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@eslint-community/eslint-utils": "^4.2.0",
|
||||
"@eslint-community/regexpp": "^4.12.1",
|
||||
@ -5256,6 +5260,7 @@
|
||||
"resolved": "https://registry.npmjs.org/react/-/react-19.1.1.tgz",
|
||||
"integrity": "sha512-w8nqGImo45dmMIfljjMwOGtbmC/mk4CMYhWIicdSflH91J9TyCyczcPFXJzrZ/ZXcgGRFeP6BU0BEJTw6tZdfQ==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
@ -5265,6 +5270,7 @@
|
||||
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.1.1.tgz",
|
||||
"integrity": "sha512-Dlq/5LAZgF0Gaz6yiqZCf6VCcZs1ghAJyrsu84Q/GT0gV+mCxbfmKNoGRKBYMJ8IEdGPqu49YWXD02GCknEDkw==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"scheduler": "^0.26.0"
|
||||
},
|
||||
@ -5327,6 +5333,7 @@
|
||||
"resolved": "https://registry.npmjs.org/react-redux/-/react-redux-9.2.0.tgz",
|
||||
"integrity": "sha512-ROY9fvHhwOD9ySfrF0wmvu//bKCQ6AeZZq1nJNtbDC+kk5DuSuNX/n6YWYF/SYy7bSba4D4FSz8DJeKY/S/r+g==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@types/use-sync-external-store": "^0.0.6",
|
||||
"use-sync-external-store": "^1.4.0"
|
||||
@ -5405,7 +5412,8 @@
|
||||
"version": "5.0.1",
|
||||
"resolved": "https://registry.npmjs.org/redux/-/redux-5.0.1.tgz",
|
||||
"integrity": "sha512-M9/ELqF6fy8FwmkpnF0S3YKOqMyoWJ4+CS5Efg2ct3oY9daQvd/Pc71FpGZsVsbl3Cpb+IIcjBDUnnyBdQbq4w==",
|
||||
"license": "MIT"
|
||||
"license": "MIT",
|
||||
"peer": true
|
||||
},
|
||||
"node_modules/redux-thunk": {
|
||||
"version": "3.1.0",
|
||||
@ -5946,7 +5954,8 @@
|
||||
"resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.1.11.tgz",
|
||||
"integrity": "sha512-2E9TBm6MDD/xKYe+dvJZAmg3yxIEDNRc0jwlNyDg/4Fil2QcSLjFKGVff0lAf1jjeaArlG/M75Ey/EYr/OJtBA==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
"license": "MIT",
|
||||
"peer": true
|
||||
},
|
||||
"node_modules/tapable": {
|
||||
"version": "2.2.2",
|
||||
@ -6020,6 +6029,7 @@
|
||||
"integrity": "sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
},
|
||||
@ -6150,6 +6160,7 @@
|
||||
"integrity": "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"peer": true,
|
||||
"bin": {
|
||||
"tsc": "bin/tsc",
|
||||
"tsserver": "bin/tsserver"
|
||||
@ -6246,6 +6257,7 @@
|
||||
"integrity": "sha512-MHFiOENNBd+Bd9uvc8GEsIzdkn1JxMmEeYX35tI3fv0sJBUTfW5tQsoaOwuY4KhBI09A3dUJ/DXf2yxPVPUceg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"esbuild": "^0.25.0",
|
||||
"fdir": "^6.4.6",
|
||||
@ -6336,6 +6348,7 @@
|
||||
"integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
},
|
||||
|
||||
@ -10,6 +10,7 @@ import { Toast } from './components/Toast'
|
||||
import { UtilitiesPage } from './pages/UtilitiesPage'
|
||||
import { RegisterPage } from './pages/RegisterPage'
|
||||
import { LetsEncryptTermsOfServicePage } from './pages/LetsEncryptTermsOfServicePage'
|
||||
import { UserPage } from './pages/UserPage'
|
||||
|
||||
|
||||
interface LayoutWrapperProps {
|
||||
@ -45,7 +46,14 @@ const LayoutWrapper: FC<LayoutWrapperProps> = (props) => {
|
||||
}
|
||||
footer={
|
||||
{
|
||||
children: <p>© {new Date().getFullYear()} <a href={import.meta.env.VITE_COMPANY_URL}>{import.meta.env.VITE_COMPANY}</a></p>
|
||||
children: <p>
|
||||
© {new Date().getFullYear()} <a
|
||||
href={import.meta.env.VITE_COMPANY_URL}
|
||||
target={'_blank'}
|
||||
rel={'noopener noreferrer'}>
|
||||
{import.meta.env.VITE_COMPANY}
|
||||
</a>
|
||||
</p>
|
||||
}
|
||||
}
|
||||
>{children}</Layout>
|
||||
@ -90,25 +98,27 @@ const AppMap: AppMapType[] = [
|
||||
routes: ['/terms-of-service'],
|
||||
page: LetsEncryptTermsOfServicePage,
|
||||
linkArea: [LinkArea.SideMenu]
|
||||
}
|
||||
},
|
||||
|
||||
// {
|
||||
// title: 'Login',
|
||||
// routes: ['/login'],
|
||||
// page: LoginScreen,
|
||||
// useAuth: false,
|
||||
// useLayout: false
|
||||
// },
|
||||
{
|
||||
title: 'Login',
|
||||
routes: ['/login'],
|
||||
page: LoginScreen,
|
||||
useAuth: false,
|
||||
useLayout: false
|
||||
},
|
||||
|
||||
{
|
||||
title: 'User',
|
||||
routes: ['/user/:userId'],
|
||||
page: UserPage
|
||||
},
|
||||
// {
|
||||
// title: 'About',
|
||||
// routes: ['/about'],
|
||||
// page: Home
|
||||
// },
|
||||
// {
|
||||
// title: 'User',
|
||||
// routes: ['/user/:userId'],
|
||||
// page: Users
|
||||
// },
|
||||
|
||||
// {
|
||||
// title: 'Organizations',
|
||||
// routes: ['/organizations', '/organization/:organizationId'],
|
||||
@ -221,14 +231,12 @@ const GetRoutes = () => {
|
||||
<Route
|
||||
key={route}
|
||||
path={route}
|
||||
// element={useAuth
|
||||
// ? <Authorization>
|
||||
// {PageComponent}
|
||||
// <UserOffcanvas />
|
||||
// </Authorization>
|
||||
// : PageComponent}
|
||||
|
||||
element={PageComponent}
|
||||
element={useAuth
|
||||
? <Authorization>
|
||||
{PageComponent}
|
||||
<UserOffcanvas />
|
||||
</Authorization>
|
||||
: PageComponent}
|
||||
/>
|
||||
)
|
||||
})
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
import { useEffect, useState } from 'react'
|
||||
import { useNavigate } from 'react-router-dom'
|
||||
import { FC, useEffect, useMemo } from 'react'
|
||||
import { useNavigate, useLocation } from 'react-router-dom'
|
||||
import { useAppDispatch, useAppSelector } from '../redux/hooks'
|
||||
import { setIdentityFromLocalStorage } from '../redux/slices/identitySlice'
|
||||
|
||||
@ -7,44 +7,36 @@ interface AuthorizationProps {
|
||||
children: React.ReactNode;
|
||||
}
|
||||
|
||||
const Authorization = (props: AuthorizationProps) => {
|
||||
const Authorization: FC<AuthorizationProps> = (props) => {
|
||||
const { children } = props
|
||||
|
||||
const navigate = useNavigate()
|
||||
const location = useLocation()
|
||||
const dispatch = useAppDispatch()
|
||||
const { identity } = useAppSelector((state) => state.identity)
|
||||
|
||||
const [loading, setLoading] = useState(true)
|
||||
const isTokenExpired = useMemo(() => {
|
||||
if (!identity || !identity.refreshTokenExpiresAt)
|
||||
return true
|
||||
|
||||
return new Date(identity.refreshTokenExpiresAt) < new Date()
|
||||
}, [identity])
|
||||
|
||||
useEffect(() => {
|
||||
// Load identity from local storage on mount
|
||||
dispatch(setIdentityFromLocalStorage())
|
||||
setLoading(false)
|
||||
}, [dispatch])
|
||||
|
||||
useEffect(() => {
|
||||
if (!loading) {
|
||||
if (!identity || new Date(identity.refreshTokenExpiresAt) < new Date()) {
|
||||
navigate('/login', { replace: true })
|
||||
if (!identity || isTokenExpired) {
|
||||
// Optionally, pass the current location for redirect after login
|
||||
navigate('/login', { replace: true, state: { from: location } })
|
||||
}
|
||||
}
|
||||
}, [identity, navigate, loading])
|
||||
}, [identity, isTokenExpired, navigate, location])
|
||||
|
||||
// Render a simple loading spinner while loading (Tailwind v4 compatible)
|
||||
if (loading) {
|
||||
return (
|
||||
<div className={'flex items-center justify-center h-screen w-screen bg-white'}>
|
||||
<div className={'animate-spin rounded-full h-8 w-8 border-t-2 border-b-2 border-blue-500'}></div>
|
||||
</div>
|
||||
)
|
||||
return identity && !isTokenExpired
|
||||
? children
|
||||
: <></>
|
||||
}
|
||||
|
||||
if (!identity || new Date(identity.refreshTokenExpiresAt) < new Date()) {
|
||||
return null
|
||||
}
|
||||
|
||||
return <>{children}</>
|
||||
}
|
||||
|
||||
export {
|
||||
Authorization
|
||||
}
|
||||
export { Authorization }
|
||||
@ -1,4 +1,4 @@
|
||||
import React, { useEffect, useState } from 'react'
|
||||
import React, { FC, useEffect, useState } from 'react'
|
||||
import { LoginRequest, LoginRequestSchema } from '../models/identity/login/LoginRequest'
|
||||
import { useAppDispatch, useAppSelector } from '../redux/hooks'
|
||||
import { login } from '../redux/slices/identitySlice'
|
||||
@ -6,7 +6,7 @@ import { useFormState } from '../hooks/useFormState'
|
||||
import { useNavigate } from 'react-router-dom'
|
||||
import { ButtonComponent, CheckBoxComponent, TextBoxComponent } from './editors'
|
||||
|
||||
const LoginScreen: React.FC = () => {
|
||||
const LoginScreen: FC = () => {
|
||||
const [use2FA, setUse2FA] = useState(false)
|
||||
const [use2FARecovery, setUse2FARecovery] = useState(false)
|
||||
|
||||
@ -48,14 +48,6 @@ const LoginScreen: React.FC = () => {
|
||||
dispatch(login(formState))
|
||||
}
|
||||
|
||||
// Utility classes
|
||||
const inputClasses =
|
||||
'block w-full rounded-md border border-gray-300 p-2 focus:border-blue-500 focus:ring-2 focus:ring-blue-200'
|
||||
const checkboxClasses =
|
||||
'h-4 w-4 text-blue-600 border-gray-300 rounded focus:ring-2 focus:ring-blue-200'
|
||||
const buttonPrimaryClasses =
|
||||
'w-full py-2 px-4 rounded-md bg-blue-600 hover:bg-blue-700 text-white font-semibold'
|
||||
|
||||
return (
|
||||
<div className={'flex items-center justify-center min-h-screen bg-gray-100'}>
|
||||
<div className={'w-full max-w-md bg-white rounded-lg shadow-md p-8 space-y-6'}>
|
||||
@ -68,8 +60,8 @@ const LoginScreen: React.FC = () => {
|
||||
<div className={'space-y-4'}>
|
||||
<div className={'space-y-4'}>
|
||||
<TextBoxComponent
|
||||
label={'Email'}
|
||||
placeholder={'Email...'}
|
||||
label={'Username'}
|
||||
placeholder={'Username...'}
|
||||
value={formState.username}
|
||||
onChange={(e) => handleInputChange('username', e.target.value)}
|
||||
errorText={errors.username}
|
||||
|
||||
@ -3,12 +3,7 @@ import { setShowUserOffcanvas } from '../redux/slices/identitySlice'
|
||||
|
||||
const UserButton = () => {
|
||||
const dispatch = useAppDispatch()
|
||||
//const { identity } = useAppSelector(state => state.identity)
|
||||
|
||||
const identity = {
|
||||
username: 'JohnDoe',
|
||||
isGlobalAdmin: true
|
||||
}
|
||||
const { identity } = useAppSelector(state => state.identity)
|
||||
|
||||
if (!identity) return <></>
|
||||
|
||||
@ -16,9 +11,7 @@ const UserButton = () => {
|
||||
className={'bg-white text-blue-500 px-2 py-1 rounded'}
|
||||
onClick={() => {
|
||||
dispatch(setShowUserOffcanvas())
|
||||
}}>
|
||||
{`${identity.username} ${identity.isGlobalAdmin ? '(Global Admin)' : ''}`.trim()}
|
||||
</button>
|
||||
}}>{identity.username}</button>
|
||||
}
|
||||
|
||||
export {
|
||||
|
||||
8
src/MaksIT.WebUI/src/forms/EditIdentity.tsx
Normal file
8
src/MaksIT.WebUI/src/forms/EditIdentity.tsx
Normal file
@ -0,0 +1,8 @@
|
||||
const EditIdentity = () => {
|
||||
return <div>Edit Identity Form</div>
|
||||
}
|
||||
|
||||
export {
|
||||
EditIdentity
|
||||
}
|
||||
|
||||
@ -5,10 +5,11 @@ import { CacheAccount } from '../entities/CacheAccount'
|
||||
import { GetAccountResponse } from '../models/letsEncryptServer/account/responses/GetAccountResponse'
|
||||
import { deleteData, getData, postData } from '../axiosConfig'
|
||||
import { ApiRoutes, GetApiRoute } from '../AppMap'
|
||||
import { formatISODateString } from '../functions'
|
||||
import { enumToArr, formatISODateString } from '../functions'
|
||||
import { addToast } from '../components/Toast/addToast'
|
||||
import { Offcanvas } from '../components/Offcanvas'
|
||||
import { EditAccount } from './EditAccount'
|
||||
import { ChallengeType } from '../entities/ChallengeType'
|
||||
|
||||
|
||||
const Home: FC = () => {
|
||||
@ -113,14 +114,12 @@ const Home: FC = () => {
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
<RadioGroupComponent
|
||||
colspan={12}
|
||||
label={'LetsEncrypt Environment'}
|
||||
options={[
|
||||
{ value: 'staging', label: 'Staging' },
|
||||
{ value: 'production', label: 'Production' }
|
||||
]}
|
||||
value={acc.challengeType ? 'staging' : 'production'}
|
||||
<SelectBoxComponent
|
||||
label={'Challenge Type'}
|
||||
options={enumToArr(ChallengeType)
|
||||
.map(ct => ({ value: ct.value, label: ct.displayValue }))
|
||||
.filter(ct => ct.value !== ChallengeType.dns01)}
|
||||
value={acc.challengeType}
|
||||
disabled={true}
|
||||
/>
|
||||
<h3 className={'col-span-12'}>Hostnames:</h3>
|
||||
@ -148,11 +147,13 @@ const Home: FC = () => {
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
<SelectBoxComponent
|
||||
label={'Environment'}
|
||||
|
||||
<RadioGroupComponent
|
||||
colspan={12}
|
||||
label={'LetsEncrypt Environment'}
|
||||
options={[
|
||||
{ value: 'production', label: 'Production' },
|
||||
{ value: 'staging', label: 'Staging' }
|
||||
{ value: 'staging', label: 'Staging' },
|
||||
{ value: 'production', label: 'Production' }
|
||||
]}
|
||||
value={acc.isStaging ? 'staging' : 'production'}
|
||||
disabled={true}
|
||||
|
||||
@ -117,8 +117,8 @@ const Register: FC<RegisterProps> = () => {
|
||||
{formState.contacts.map((contact) => (
|
||||
<li key={contact} className={'grid grid-cols-12 gap-4 w-full pb-2'}>
|
||||
<span className={'col-span-10'}>{contact}</span>
|
||||
<FieldContainer colspan={2}>
|
||||
<ButtonComponent
|
||||
colspan={2}
|
||||
onClick={() => {
|
||||
const updatedContacts = formState.contacts.filter(c => c !== contact)
|
||||
handleInputChange('contacts', updatedContacts)
|
||||
@ -126,7 +126,6 @@ const Register: FC<RegisterProps> = () => {
|
||||
>
|
||||
<TrashIcon />
|
||||
</ButtonComponent>
|
||||
</FieldContainer>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
@ -170,10 +169,10 @@ const Register: FC<RegisterProps> = () => {
|
||||
<h3 className={'col-span-12'}>Hostnames:</h3>
|
||||
<ul className={'col-span-12'}>
|
||||
{formState.hostnames.map((hostname) => (
|
||||
<li key={hostname} className={'grid grid-cols-12 gap-4 w-full'}>
|
||||
<li key={hostname} className={'grid grid-cols-12 gap-4 w-full pb-2'}>
|
||||
<span className={'col-span-10'}>{hostname}</span>
|
||||
<FieldContainer colspan={2}>
|
||||
<ButtonComponent
|
||||
colspan={2}
|
||||
onClick={() => {
|
||||
const updatedHostnames = formState.hostnames.filter(h => h !== hostname)
|
||||
handleInputChange('hostnames', updatedHostnames)
|
||||
@ -181,7 +180,6 @@ const Register: FC<RegisterProps> = () => {
|
||||
>
|
||||
<TrashIcon />
|
||||
</ButtonComponent>
|
||||
</FieldContainer>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
|
||||
10
src/MaksIT.WebUI/src/pages/UserPage.tsx
Normal file
10
src/MaksIT.WebUI/src/pages/UserPage.tsx
Normal file
@ -0,0 +1,10 @@
|
||||
import { EditIdentity } from '../forms/EditIdentity'
|
||||
|
||||
|
||||
const UserPage = () => {
|
||||
return <EditIdentity />
|
||||
}
|
||||
|
||||
export {
|
||||
UserPage
|
||||
}
|
||||
@ -5,7 +5,7 @@ import identityReducer from './slices/identitySlice'
|
||||
export const store = configureStore({
|
||||
reducer: {
|
||||
loader: loaderReducer,
|
||||
//identity: identityReducer,
|
||||
identity: identityReducer,
|
||||
},
|
||||
})
|
||||
|
||||
|
||||
11
src/Models/LetsEncryptServer/Identity/Login/LoginRequest.cs
Normal file
11
src/Models/LetsEncryptServer/Identity/Login/LoginRequest.cs
Normal file
@ -0,0 +1,11 @@
|
||||
using MaksIT.Core.Abstractions.Webapi;
|
||||
|
||||
|
||||
namespace Models.LetsEncryptServer.Identity.Login;
|
||||
|
||||
public class LoginRequest : RequestModelBase {
|
||||
public required string Username { get; set; }
|
||||
public required string Password { get; set; }
|
||||
public string? TwoFactorCode { get; set; }
|
||||
public string? TwoFactorRecoveryCode { get; set; }
|
||||
}
|
||||
13
src/Models/LetsEncryptServer/Identity/Login/LoginResponse.cs
Normal file
13
src/Models/LetsEncryptServer/Identity/Login/LoginResponse.cs
Normal file
@ -0,0 +1,13 @@
|
||||
using MaksIT.Core.Abstractions.Webapi;
|
||||
|
||||
|
||||
namespace Models.LetsEncryptServer.Identity.Login;
|
||||
|
||||
public class LoginResponse : ResponseModelBase {
|
||||
|
||||
public required string TokenType { get; set; }
|
||||
public required string Token { get; set; }
|
||||
public required DateTime ExpiresAt { get; set; }
|
||||
public required string RefreshToken { get; set; }
|
||||
public required DateTime RefreshTokenExpiresAt { get; set; }
|
||||
}
|
||||
@ -0,0 +1,8 @@
|
||||
using MaksIT.Core.Abstractions.Webapi;
|
||||
|
||||
|
||||
namespace Models.LetsEncryptServer.Identity.Login;
|
||||
|
||||
public class RefreshTokenRequest : RequestModelBase {
|
||||
public required string RefreshToken { get; set; } // The refresh token used for renewing access
|
||||
}
|
||||
@ -0,0 +1,8 @@
|
||||
using MaksIT.Core.Abstractions.Webapi;
|
||||
|
||||
|
||||
namespace Models.LetsEncryptServer.Identity.Logout;
|
||||
|
||||
public class LogoutRequest : RequestModelBase {
|
||||
public bool LogoutFromAllDevices { get; set; }
|
||||
}
|
||||
@ -11,7 +11,7 @@
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="MaksIT.Core" Version="1.5.2" />
|
||||
<PackageReference Include="MaksIT.Core" Version="1.5.3" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
|
||||
@ -27,6 +27,7 @@ services:
|
||||
volumes:
|
||||
- D:\Compose\MaksIT.CertsUI\acme:/acme
|
||||
- D:\Compose\MaksIT.CertsUI\cache:/cache
|
||||
- D:\Compose\MaksIT.CertsUI\data:/data
|
||||
- D:\Compose\MaksIT.CertsUI\tmp:/tmp
|
||||
- D:\Compose\MaksIT.CertsUI\configMap\appsettings.json:/configMap/appsettings.json:ro
|
||||
- D:\Compose\MaksIT.CertsUI\secrets\appsecrets.json:/secrets/appsecrets.json:ro
|
||||
|
||||
Loading…
Reference in New Issue
Block a user