mirror of
https://github.com/MAKS-IT-COM/maksit-certs-ui.git
synced 2026-02-14 01:27:19 +01:00
(feature): jwt, password change, general codebase improvements
This commit is contained in:
parent
712b880ab2
commit
7a745a30db
@ -9,11 +9,11 @@
|
|||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="Microsoft.VisualStudio.Azure.Containers.Tools.Targets" Version="1.22.1" />
|
<PackageReference Include="Microsoft.VisualStudio.Azure.Containers.Tools.Targets" Version="1.22.1" />
|
||||||
<PackageReference Include="Swashbuckle.AspNetCore" Version="9.0.6" />
|
<PackageReference Include="Swashbuckle.AspNetCore" Version="10.0.0" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<ProjectReference Include="..\Models\Models.csproj" />
|
<ProjectReference Include="..\Models\MaksIT.Models.csproj" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
|
|||||||
@ -7,13 +7,13 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "LetsEncrypt", "LetsEncrypt\
|
|||||||
EndProject
|
EndProject
|
||||||
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Tests", "Tests", "{3374FDB1-C95E-4103-8E14-5BBF0BDC4E9D}"
|
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Tests", "Tests", "{3374FDB1-C95E-4103-8E14-5BBF0BDC4E9D}"
|
||||||
EndProject
|
EndProject
|
||||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "LetsEncryptServer", "LetsEncryptServer\LetsEncryptServer.csproj", "{B5F39E04-C2E3-49BF-82C2-9DEBAA949E3D}"
|
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MaksIT.Webapi", "MaksIT.Webapi\MaksIT.Webapi.csproj", "{B5F39E04-C2E3-49BF-82C2-9DEBAA949E3D}"
|
||||||
EndProject
|
EndProject
|
||||||
Project("{E53339B2-1760-4266-BCC7-CA923CBCF16C}") = "docker-compose", "docker-compose.dcproj", "{0233E43F-435D-4309-B20C-ECD4BFBD2E63}"
|
Project("{E53339B2-1760-4266-BCC7-CA923CBCF16C}") = "docker-compose", "docker-compose.dcproj", "{0233E43F-435D-4309-B20C-ECD4BFBD2E63}"
|
||||||
EndProject
|
EndProject
|
||||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Agent", "Agent\Agent.csproj", "{871BDED3-C6AE-437D-9B45-3AA3F184D002}"
|
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Agent", "Agent\Agent.csproj", "{871BDED3-C6AE-437D-9B45-3AA3F184D002}"
|
||||||
EndProject
|
EndProject
|
||||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Models", "Models\Models.csproj", "{6814169B-D4D0-40B2-9FA9-89997DD44C30}"
|
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MaksIT.Models", "Models\MaksIT.Models.csproj", "{6814169B-D4D0-40B2-9FA9-89997DD44C30}"
|
||||||
EndProject
|
EndProject
|
||||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ReverseProxy", "ReverseProxy\ReverseProxy.csproj", "{BE051147-7AB7-4358-9C24-5CB40FAFF4FC}"
|
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ReverseProxy", "ReverseProxy\ReverseProxy.csproj", "{BE051147-7AB7-4358-9C24-5CB40FAFF4FC}"
|
||||||
EndProject
|
EndProject
|
||||||
|
|||||||
@ -8,13 +8,13 @@
|
|||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="MaksIT.Core" Version="1.5.3" />
|
<PackageReference Include="MaksIT.Core" Version="1.5.4" />
|
||||||
<PackageReference Include="MaksIT.Results" Version="1.1.1" />
|
<PackageReference Include="MaksIT.Results" Version="1.1.1" />
|
||||||
<PackageReference Include="Microsoft.Extensions.Caching.Abstractions" Version="9.0.10" />
|
<PackageReference Include="Microsoft.Extensions.Caching.Abstractions" Version="10.0.0" />
|
||||||
<PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="9.0.10" />
|
<PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="10.0.0" />
|
||||||
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="9.0.10" />
|
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="10.0.0" />
|
||||||
<PackageReference Include="Microsoft.Extensions.Hosting" Version="9.0.10" />
|
<PackageReference Include="Microsoft.Extensions.Hosting" Version="10.0.0" />
|
||||||
<PackageReference Include="Microsoft.Extensions.Http" Version="9.0.10" />
|
<PackageReference Include="Microsoft.Extensions.Http" Version="10.0.0" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
</Project>
|
</Project>
|
||||||
|
|||||||
@ -13,20 +13,13 @@ namespace MaksIT.LetsEncrypt.Services;
|
|||||||
|
|
||||||
public interface IJwsService {
|
public interface IJwsService {
|
||||||
void SetKeyId(string location);
|
void SetKeyId(string location);
|
||||||
|
|
||||||
JwsMessage Encode(JwsHeader protectedHeader);
|
JwsMessage Encode(JwsHeader protectedHeader);
|
||||||
|
|
||||||
JwsMessage Encode<TPayload>(TPayload payload, JwsHeader protectedHeader);
|
JwsMessage Encode<TPayload>(TPayload payload, JwsHeader protectedHeader);
|
||||||
|
|
||||||
string GetKeyAuthorization(string token);
|
string GetKeyAuthorization(string token);
|
||||||
|
|
||||||
|
|
||||||
string Base64UrlEncoded(string s);
|
string Base64UrlEncoded(string s);
|
||||||
|
|
||||||
string Base64UrlEncoded(byte[] arg);
|
string Base64UrlEncoded(byte[] arg);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
public class JwsService : IJwsService {
|
public class JwsService : IJwsService {
|
||||||
|
|
||||||
public Jwk _jwk;
|
public Jwk _jwk;
|
||||||
@ -89,12 +82,17 @@ public class JwsService : IJwsService {
|
|||||||
$"{token}.{GetSha256Thumbprint()}";
|
$"{token}.{GetSha256Thumbprint()}";
|
||||||
|
|
||||||
private string GetSha256Thumbprint() {
|
private string GetSha256Thumbprint() {
|
||||||
|
|
||||||
|
var thumbprint = new {
|
||||||
|
e = _jwk.Exponent,
|
||||||
|
kty = "RSA",
|
||||||
|
n = _jwk.Modulus
|
||||||
|
};
|
||||||
|
|
||||||
var json = "{\"e\":\"" + _jwk.Exponent + "\",\"kty\":\"RSA\",\"n\":\"" + _jwk.Modulus + "\"}";
|
var json = "{\"e\":\"" + _jwk.Exponent + "\",\"kty\":\"RSA\",\"n\":\"" + _jwk.Modulus + "\"}";
|
||||||
return Base64UrlEncoded(SHA256.HashData(Encoding.UTF8.GetBytes(json)));
|
return Base64UrlEncoded(SHA256.HashData(Encoding.UTF8.GetBytes(json)));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
public string Base64UrlEncoded(string s) =>
|
public string Base64UrlEncoded(string s) =>
|
||||||
Base64UrlEncoded(Encoding.UTF8.GetBytes(s));
|
Base64UrlEncoded(Encoding.UTF8.GetBytes(s));
|
||||||
|
|
||||||
|
|||||||
@ -754,16 +754,12 @@ public class LetsEncryptService : ILetsEncryptService {
|
|||||||
private static bool StatusEquals(string? status, OrderStatus expected) => status == expected.GetDisplayName();
|
private static bool StatusEquals(string? status, OrderStatus expected) => status == expected.GetDisplayName();
|
||||||
|
|
||||||
private Result HandleUnhandledException(Exception ex, string defaultMessage = "Let's Encrypt client unhandled exception") {
|
private Result HandleUnhandledException(Exception ex, string defaultMessage = "Let's Encrypt client unhandled exception") {
|
||||||
List<string> messages = new() { defaultMessage };
|
_logger.LogError(ex, defaultMessage);
|
||||||
_logger.LogError(ex, messages.FirstOrDefault());
|
return Result.InternalServerError([defaultMessage, .. ex.ExtractMessages()]);
|
||||||
ex.ExtractMessages().ForEach(m => messages.Add(m));
|
|
||||||
return Result.InternalServerError([.. messages]);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private Result<T?> HandleUnhandledException<T>(Exception ex, T? defaultValue = default, string defaultMessage = "Let's Encrypt client unhandled exception") {
|
private Result<T?> HandleUnhandledException<T>(Exception ex, T? defaultValue = default, string defaultMessage = "Let's Encrypt client unhandled exception") {
|
||||||
List<string> messages = new() { defaultMessage };
|
_logger.LogError(ex, defaultMessage);
|
||||||
_logger.LogError(ex, messages.FirstOrDefault());
|
return Result<T?>.InternalServerError(defaultValue, [.. ex.ExtractMessages()]);
|
||||||
ex.ExtractMessages().ForEach(m => messages.Add(m));
|
|
||||||
return Result<T?>.InternalServerError(defaultValue, [.. messages]);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,39 +0,0 @@
|
|||||||
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();
|
|
||||||
}
|
|
||||||
|
|
||||||
[HttpPost("logout")]
|
|
||||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
|
||||||
public async Task<IActionResult> Logout([FromBody] LogoutRequest requestData) {
|
|
||||||
var result = await _identityService.Logout(requestData);
|
|
||||||
return result.ToActionResult();
|
|
||||||
}
|
|
||||||
#endregion
|
|
||||||
}
|
|
||||||
@ -1,112 +0,0 @@
|
|||||||
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)
|
|
||||||
.SetName(userDto.Name)
|
|
||||||
.SetSaltedHash(userDto.Salt, userDto.Hash)
|
|
||||||
.SetJwtTokens([.. userDto.JwtTokens.Select(jtDto =>
|
|
||||||
new JwtToken(jtDto.Id)
|
|
||||||
.SetAccessTokenData(jtDto.Token, jtDto.IssuedAt, jtDto.ExpiresAt)
|
|
||||||
.SetRefreshTokenData(jtDto.RefreshToken, jtDto.RefreshTokenExpiresAt)
|
|
||||||
)])
|
|
||||||
.SetLastLogin(userDto.LastLogin)
|
|
||||||
)]
|
|
||||||
};
|
|
||||||
return Result<Settings?>.Ok(settings);
|
|
||||||
}
|
|
||||||
catch (Exception ex) {
|
|
||||||
var message = "Error loading settings file.";
|
|
||||||
_logger.LogError(ex, message);
|
|
||||||
return Result<Settings?>.InternalServerError(null, [message, .. ex.ExtractMessages()]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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,
|
|
||||||
Name = u.Name,
|
|
||||||
Salt = u.Salt,
|
|
||||||
Hash = u.Hash,
|
|
||||||
JwtTokens = [.. u.JwtTokens.Select(jt => new JwtTokenDto {
|
|
||||||
Id = jt.Id,
|
|
||||||
Token = jt.Token,
|
|
||||||
ExpiresAt = jt.ExpiresAt,
|
|
||||||
IssuedAt = jt.IssuedAt,
|
|
||||||
RefreshToken = jt.RefreshToken,
|
|
||||||
RefreshTokenExpiresAt = jt.RefreshTokenExpiresAt,
|
|
||||||
IsRevoked = jt.IsRevoked
|
|
||||||
})],
|
|
||||||
LastLogin = u.LastLogin,
|
|
||||||
})]
|
|
||||||
};
|
|
||||||
|
|
||||||
await File.WriteAllTextAsync(_settingsPath, settingsDto.ToJson());
|
|
||||||
_logger.LogInformation("Settings file saved.");
|
|
||||||
return Result.Ok();
|
|
||||||
}
|
|
||||||
catch (Exception ex) {
|
|
||||||
var message = "Error saving settings file.";
|
|
||||||
_logger.LogError(ex, message);
|
|
||||||
return Result.InternalServerError([message, .. ex.ExtractMessages()]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#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();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -194,6 +194,8 @@ enum ApiRoutes {
|
|||||||
generateSecret = 'GET|/secret/generatesecret',
|
generateSecret = 'GET|/secret/generatesecret',
|
||||||
|
|
||||||
// Identity
|
// Identity
|
||||||
|
identityPatch = 'PATCH|/identity/user/{userId}',
|
||||||
|
|
||||||
identityLogin = 'POST|/identity/login',
|
identityLogin = 'POST|/identity/login',
|
||||||
identityRefresh = 'POST|/identity/refresh',
|
identityRefresh = 'POST|/identity/refresh',
|
||||||
identityLogout = 'POST|/identity/logout',
|
identityLogout = 'POST|/identity/logout',
|
||||||
|
|||||||
@ -13,7 +13,6 @@ const axiosInstance = axios.create({
|
|||||||
timeout: 10000, // Set a timeout if needed
|
timeout: 10000, // Set a timeout if needed
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
||||||
let isRefreshing = false
|
let isRefreshing = false
|
||||||
let refreshPromise: Promise<unknown> | null = null
|
let refreshPromise: Promise<unknown> | null = null
|
||||||
|
|
||||||
@ -77,15 +76,14 @@ axiosInstance.interceptors.response.use(
|
|||||||
// Handle response error
|
// Handle response error
|
||||||
store.dispatch(hideLoader())
|
store.dispatch(hideLoader())
|
||||||
if (error.response) {
|
if (error.response) {
|
||||||
if (error.response.status === 401) {
|
const contentType = error.response.headers['content-type']
|
||||||
// Handle unauthorized error (e.g., redirect to login)
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
const contentType = error.response.headers['content-type']
|
|
||||||
|
|
||||||
if (contentType && contentType.includes('application/problem+json')) {
|
if (contentType && contentType.includes('application/problem+json')) {
|
||||||
const problem = error.response.data as ProblemDetails
|
const problem = error.response.data as ProblemDetails
|
||||||
addToast(`${problem.title}: ${problem.detail}`, 'error')
|
addToast(`${problem.title}: ${problem.detail}`, 'error')
|
||||||
|
|
||||||
|
if (error.response.status === 401) {
|
||||||
|
// Handle unauthorized error (e.g., redirect to login)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,14 +1,12 @@
|
|||||||
import React, { FC, useEffect, useState } from 'react'
|
import { FC, useEffect, KeyboardEvent } from 'react'
|
||||||
import { LoginRequest, LoginRequestSchema } from '../models/identity/login/LoginRequest'
|
import { LoginRequest, LoginRequestSchema } from '../models/identity/login/LoginRequest'
|
||||||
import { useAppDispatch, useAppSelector } from '../redux/hooks'
|
import { useAppDispatch, useAppSelector } from '../redux/hooks'
|
||||||
import { login } from '../redux/slices/identitySlice'
|
import { login } from '../redux/slices/identitySlice'
|
||||||
import { useFormState } from '../hooks/useFormState'
|
import { useFormState } from '../hooks/useFormState'
|
||||||
import { useNavigate } from 'react-router-dom'
|
import { useNavigate } from 'react-router-dom'
|
||||||
import { ButtonComponent, CheckBoxComponent, TextBoxComponent } from './editors'
|
import { ButtonComponent, TextBoxComponent } from './editors'
|
||||||
|
|
||||||
const LoginScreen: FC = () => {
|
const LoginScreen: FC = () => {
|
||||||
const [use2FA, setUse2FA] = useState(false)
|
|
||||||
const [use2FARecovery, setUse2FARecovery] = useState(false)
|
|
||||||
|
|
||||||
const navigate = useNavigate()
|
const navigate = useNavigate()
|
||||||
const dispatch = useAppDispatch()
|
const dispatch = useAppDispatch()
|
||||||
@ -32,13 +30,6 @@ const LoginScreen: FC = () => {
|
|||||||
}
|
}
|
||||||
}, [identity, navigate])
|
}, [identity, navigate])
|
||||||
|
|
||||||
const handleUse2FA = (e: React.ChangeEvent<HTMLInputElement>) => {
|
|
||||||
setUse2FA(e.target.checked)
|
|
||||||
if (!e.target.checked) {
|
|
||||||
setUse2FARecovery(false)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const handleLogin = () => {
|
const handleLogin = () => {
|
||||||
if (!formIsValid) return
|
if (!formIsValid) return
|
||||||
|
|
||||||
@ -48,14 +39,28 @@ const LoginScreen: FC = () => {
|
|||||||
dispatch(login(formState))
|
dispatch(login(formState))
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
const handleSubmit = (e: KeyboardEvent<HTMLDivElement>) => {
|
||||||
<div className={'flex items-center justify-center min-h-screen bg-gray-100'}>
|
if (e.key === 'Enter') handleLogin()
|
||||||
<div className={'w-full max-w-md bg-white rounded-lg shadow-md p-8 space-y-6'}>
|
}
|
||||||
{/* Logo */}
|
|
||||||
<div className={'flex justify-center'}>
|
|
||||||
<img src={'/logo.png'} alt={'Logo'} className={'h-12 w-auto'} />
|
|
||||||
</div>
|
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={'relative min-h-screen bg-gray-100 flex items-center justify-center'}>
|
||||||
|
{/* Top left logo and company name */}
|
||||||
|
<a
|
||||||
|
href={import.meta.env.VITE_COMPANY_URL}
|
||||||
|
target={'_blank'}
|
||||||
|
rel={'noopener noreferrer'}
|
||||||
|
className={'absolute top-6 left-8 flex items-center space-x-3 z-10'}
|
||||||
|
>
|
||||||
|
<img src={'/logo.png'} alt={'Logo'} className={'h-10 w-auto'} />
|
||||||
|
</a>
|
||||||
|
|
||||||
|
<div className={'w-full max-w-md bg-white rounded-lg shadow-md p-8 space-y-6'} onKeyDown={handleSubmit} tabIndex={0}>
|
||||||
|
{/* App logo and name above form */}
|
||||||
|
<div className={'flex justify-center items-center space-x-3 mb-2'}>
|
||||||
|
<img src={'/logo.png'} alt={'App Logo'} className={'h-12 w-auto'} />
|
||||||
|
<span className={'text-2xl font-bold text-gray-800 select-none'}>{import.meta.env.VITE_APP_TITLE}</span>
|
||||||
|
</div>
|
||||||
{/* Form */}
|
{/* Form */}
|
||||||
<div className={'space-y-4'}>
|
<div className={'space-y-4'}>
|
||||||
<div className={'space-y-4'}>
|
<div className={'space-y-4'}>
|
||||||
@ -66,7 +71,6 @@ const LoginScreen: FC = () => {
|
|||||||
onChange={(e) => handleInputChange('username', e.target.value)}
|
onChange={(e) => handleInputChange('username', e.target.value)}
|
||||||
errorText={errors.username}
|
errorText={errors.username}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<TextBoxComponent
|
<TextBoxComponent
|
||||||
label={'Password'}
|
label={'Password'}
|
||||||
placeholder={'Password...'}
|
placeholder={'Password...'}
|
||||||
@ -76,46 +80,6 @@ const LoginScreen: FC = () => {
|
|||||||
errorText={errors.password}
|
errorText={errors.password}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* 2FA Options */}
|
|
||||||
<div className={'flex items-center gap-4'}>
|
|
||||||
<CheckBoxComponent
|
|
||||||
label={'Use 2FA'}
|
|
||||||
value={use2FA}
|
|
||||||
onChange={handleUse2FA}
|
|
||||||
/>
|
|
||||||
{use2FA && (
|
|
||||||
<CheckBoxComponent
|
|
||||||
label={'Use 2FA Recovery'}
|
|
||||||
value={use2FARecovery}
|
|
||||||
onChange={(e) => setUse2FARecovery(e.target.checked)}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* 2FA Inputs */}
|
|
||||||
{use2FA && (
|
|
||||||
<div className={'space-y-4'}>
|
|
||||||
{use2FARecovery ? (
|
|
||||||
<TextBoxComponent
|
|
||||||
label={'2FA Recovery Code'}
|
|
||||||
placeholder={'Recovery code...'}
|
|
||||||
value={formState.twoFactorRecoveryCode}
|
|
||||||
onChange={(e) => handleInputChange('twoFactorRecoveryCode', e.target.value)}
|
|
||||||
errorText={errors.twoFactorRecoveryCode}
|
|
||||||
/>
|
|
||||||
) : (
|
|
||||||
<TextBoxComponent
|
|
||||||
label={'2FA Code'}
|
|
||||||
placeholder={'Authentication code...'}
|
|
||||||
value={formState.twoFactorCode}
|
|
||||||
onChange={(e) => handleInputChange('twoFactorCode', e.target.value)}
|
|
||||||
errorText={errors.twoFactorCode}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* Submit */}
|
{/* Submit */}
|
||||||
<ButtonComponent
|
<ButtonComponent
|
||||||
label={'Sign in'}
|
label={'Sign in'}
|
||||||
|
|||||||
@ -1,8 +0,0 @@
|
|||||||
const EditIdentity = () => {
|
|
||||||
return <div>Edit Identity Form</div>
|
|
||||||
}
|
|
||||||
|
|
||||||
export {
|
|
||||||
EditIdentity
|
|
||||||
}
|
|
||||||
|
|
||||||
107
src/MaksIT.WebUI/src/forms/Users/EditUser/ChangePassword.tsx
Normal file
107
src/MaksIT.WebUI/src/forms/Users/EditUser/ChangePassword.tsx
Normal file
@ -0,0 +1,107 @@
|
|||||||
|
import { FC } from 'react'
|
||||||
|
import { ButtonComponent, TextBoxComponent } from '../../../components/editors'
|
||||||
|
import { Offcanvas } from '../../../components/Offcanvas'
|
||||||
|
import { PatchUserChangePasswordRequest, PatchUserChangePasswordRequestSchema } from '../../../models/identity/user/PatchUserRequest'
|
||||||
|
import { useFormState } from '../../../hooks/useFormState'
|
||||||
|
import { PatchOperation } from '../../../models/PatchOperation'
|
||||||
|
import { UserResponse } from '../../../models/identity/user/UserResponse'
|
||||||
|
import { ApiRoutes, GetApiRoute } from '../../../AppMap'
|
||||||
|
import { patchData } from '../../../axiosConfig'
|
||||||
|
import { FormContainer, FormContent, FormHeader } from '../../../components/FormLayout'
|
||||||
|
|
||||||
|
interface ChangePasswordProps {
|
||||||
|
userId: string;
|
||||||
|
isOpen?: boolean;
|
||||||
|
onClose?: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
const ChangePassword: FC<ChangePasswordProps> = (props) => {
|
||||||
|
const {
|
||||||
|
userId,
|
||||||
|
isOpen = false,
|
||||||
|
onClose
|
||||||
|
} = props
|
||||||
|
|
||||||
|
const {
|
||||||
|
formState,
|
||||||
|
errors,
|
||||||
|
formIsValid,
|
||||||
|
handleInputChange,
|
||||||
|
setInitialState
|
||||||
|
} = useFormState<PatchUserChangePasswordRequest>({
|
||||||
|
initialState: {
|
||||||
|
password: '',
|
||||||
|
confirmPassword: ''
|
||||||
|
},
|
||||||
|
validationSchema: PatchUserChangePasswordRequestSchema,
|
||||||
|
})
|
||||||
|
|
||||||
|
const handleOnClose = () => {
|
||||||
|
setInitialState({
|
||||||
|
password: '',
|
||||||
|
confirmPassword: ''
|
||||||
|
})
|
||||||
|
|
||||||
|
onClose?.()
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleSave = () => {
|
||||||
|
if (formIsValid) {
|
||||||
|
const data: PatchUserChangePasswordRequest = {
|
||||||
|
password: formState.password,
|
||||||
|
operations: {
|
||||||
|
password: PatchOperation.SetField
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
patchData<PatchUserChangePasswordRequest, UserResponse>(GetApiRoute(ApiRoutes.identityPatch).route.replace('{userId}', userId), data)
|
||||||
|
.then(response => {
|
||||||
|
if (!response) return
|
||||||
|
|
||||||
|
handleOnClose()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return <Offcanvas isOpen={isOpen}>
|
||||||
|
<FormContainer>
|
||||||
|
<FormHeader>Change password</FormHeader>
|
||||||
|
<FormContent>
|
||||||
|
<div className={'grid grid-cols-12 gap-4 w-full h-full content-start'}>
|
||||||
|
<TextBoxComponent
|
||||||
|
colspan={12}
|
||||||
|
type={'password'}
|
||||||
|
label={'Password'}
|
||||||
|
value={formState.password}
|
||||||
|
errorText={errors.password}
|
||||||
|
onChange={(e) => handleInputChange('password', e.target.value)}
|
||||||
|
/>
|
||||||
|
<TextBoxComponent
|
||||||
|
colspan={12}
|
||||||
|
type={'password'}
|
||||||
|
label={'Confirm password'}
|
||||||
|
value={formState.confirmPassword}
|
||||||
|
errorText={errors.confirmPassword}
|
||||||
|
onChange={(e) => handleInputChange('confirmPassword', e.target.value)}
|
||||||
|
/>
|
||||||
|
<ButtonComponent
|
||||||
|
colspan={6}
|
||||||
|
label={'Save'}
|
||||||
|
buttonHierarchy={'primary'}
|
||||||
|
onClick={handleSave}
|
||||||
|
/>
|
||||||
|
<ButtonComponent
|
||||||
|
colspan={6}
|
||||||
|
label={'Cancel'}
|
||||||
|
buttonHierarchy={'secondary'}
|
||||||
|
onClick={handleOnClose}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</FormContent>
|
||||||
|
</FormContainer>
|
||||||
|
</Offcanvas>
|
||||||
|
}
|
||||||
|
|
||||||
|
export {
|
||||||
|
ChangePassword
|
||||||
|
}
|
||||||
53
src/MaksIT.WebUI/src/forms/Users/EditUser/index.tsx
Normal file
53
src/MaksIT.WebUI/src/forms/Users/EditUser/index.tsx
Normal file
@ -0,0 +1,53 @@
|
|||||||
|
import { FC, useState } from 'react'
|
||||||
|
import { FormContainer, FormContent, FormHeader } from '../../../components/FormLayout'
|
||||||
|
import { ButtonComponent } from '../../../components/editors'
|
||||||
|
import { UserResponse } from '../../../models/identity/user/UserResponse'
|
||||||
|
import { ChangePassword } from './ChangePassword'
|
||||||
|
|
||||||
|
|
||||||
|
interface EditUserProps {
|
||||||
|
userId: string;
|
||||||
|
onSubmitted?: (entity: UserResponse) => void
|
||||||
|
cancelEnabled?: boolean
|
||||||
|
onCancel?: () => void
|
||||||
|
}
|
||||||
|
|
||||||
|
const EditUser : FC<EditUserProps> = (props) => {
|
||||||
|
const {
|
||||||
|
userId,
|
||||||
|
onSubmitted,
|
||||||
|
cancelEnabled = false,
|
||||||
|
onCancel
|
||||||
|
} = props
|
||||||
|
|
||||||
|
const [showChangePassword, setShowChangePassword] = useState(false)
|
||||||
|
|
||||||
|
return <>
|
||||||
|
<FormContainer>
|
||||||
|
<FormHeader>Edit user</FormHeader>
|
||||||
|
<FormContent>
|
||||||
|
<div className={'grid grid-cols-12 gap-4 w-full'}>
|
||||||
|
<ButtonComponent
|
||||||
|
colspan={3}
|
||||||
|
label={'Change password'}
|
||||||
|
buttonHierarchy={'primary'}
|
||||||
|
onClick={() => setShowChangePassword(true)}
|
||||||
|
/>
|
||||||
|
<span className={'col-span-9'}></span>
|
||||||
|
</div>
|
||||||
|
</FormContent>
|
||||||
|
</FormContainer>
|
||||||
|
|
||||||
|
|
||||||
|
<ChangePassword
|
||||||
|
userId={userId}
|
||||||
|
isOpen={showChangePassword}
|
||||||
|
onClose={() => setShowChangePassword(false)}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
}
|
||||||
|
|
||||||
|
export {
|
||||||
|
EditUser
|
||||||
|
}
|
||||||
|
|
||||||
@ -1,8 +1,11 @@
|
|||||||
import { EditIdentity } from '../forms/EditIdentity'
|
import { useParams } from 'react-router-dom'
|
||||||
|
import { EditUser } from '../forms/Users/EditUser'
|
||||||
|
|
||||||
|
|
||||||
const UserPage = () => {
|
const UserPage = () => {
|
||||||
return <EditIdentity />
|
const { userId } = useParams<{ userId: string }>()
|
||||||
|
|
||||||
|
return userId ? <EditUser userId={userId} /> : <>User not found</>
|
||||||
}
|
}
|
||||||
|
|
||||||
export {
|
export {
|
||||||
|
|||||||
@ -0,0 +1,14 @@
|
|||||||
|
using Microsoft.AspNetCore.Mvc.Filters;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
|
namespace MaksIT.Webapi.Abstractions.Authorization.Filters;
|
||||||
|
|
||||||
|
public abstract class BaseAsyncAuthorizationFilter(
|
||||||
|
ILogger logger
|
||||||
|
) : IAsyncAuthorizationFilter {
|
||||||
|
|
||||||
|
protected readonly ILogger _logger = logger;
|
||||||
|
|
||||||
|
// Derived classes must implement this method.
|
||||||
|
public abstract Task OnAuthorizationAsync(AuthorizationFilterContext context);
|
||||||
|
}
|
||||||
@ -1,9 +1,14 @@
|
|||||||
using MaksIT.Results;
|
using MaksIT.Webapi;
|
||||||
|
using MaksIT.Results;
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
|
using Microsoft.Extensions.Options;
|
||||||
|
|
||||||
namespace LetsEncryptServer.Abstractions;
|
namespace MaksIT.Webapi.Abstractions.Services;
|
||||||
|
|
||||||
public abstract class ServiceBase {
|
public abstract class ServiceBase(ILogger logger, IOptions<Configuration> appSettings) {
|
||||||
|
|
||||||
|
protected readonly ILogger _logger = logger;
|
||||||
|
protected readonly Configuration _appSettings = appSettings.Value;
|
||||||
|
|
||||||
protected Result UnsupportedPatchOperationResponse() {
|
protected Result UnsupportedPatchOperationResponse() {
|
||||||
return Result.BadRequest("Unsupported operation");
|
return Result.BadRequest("Unsupported operation");
|
||||||
@ -0,0 +1,14 @@
|
|||||||
|
using MaksIT.Results;
|
||||||
|
|
||||||
|
namespace MaksIT.Webapi.Authorization.Extensions;
|
||||||
|
|
||||||
|
public static class HttpContextExtension {
|
||||||
|
public static Result<JwtTokenData?> GetJwtTokenData(this HttpContext context) {
|
||||||
|
var jwtTokenData = context.Items[HttpContextValue.JwtTokenData] as JwtTokenData;
|
||||||
|
|
||||||
|
if (jwtTokenData == null)
|
||||||
|
return Result<JwtTokenData?>.Forbidden(null, "JWT token data not found in the request");
|
||||||
|
|
||||||
|
return Result<JwtTokenData?>.Ok(jwtTokenData);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,90 @@
|
|||||||
|
using MaksIT.Core.Security.JWT;
|
||||||
|
using MaksIT.Results;
|
||||||
|
using MaksIT.Webapi.Abstractions.Authorization.Filters;
|
||||||
|
using MaksIT.Webapi.Dto;
|
||||||
|
using MaksIT.Webapi.Services;
|
||||||
|
using Microsoft.AspNetCore.Mvc.Filters;
|
||||||
|
using Microsoft.Extensions.Options;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
|
namespace MaksIT.Webapi.Authorization.Filters;
|
||||||
|
|
||||||
|
public class JwtAuthorizationFilter : BaseAsyncAuthorizationFilter {
|
||||||
|
private const string BearerTokenHeaderName = "Authorization";
|
||||||
|
private readonly Auth _jwtSettingsConfiguration;
|
||||||
|
private readonly ISettingsService _settingsService;
|
||||||
|
|
||||||
|
public JwtAuthorizationFilter(
|
||||||
|
ILogger<JwtAuthorizationFilter> logger,
|
||||||
|
IOptions<Configuration> appSettings,
|
||||||
|
ISettingsService settingsService
|
||||||
|
) : base(logger) {
|
||||||
|
_jwtSettingsConfiguration = appSettings.Value.Auth;
|
||||||
|
_settingsService = settingsService;
|
||||||
|
}
|
||||||
|
|
||||||
|
public override async Task OnAuthorizationAsync(AuthorizationFilterContext context) {
|
||||||
|
var request = context.HttpContext.Request;
|
||||||
|
if (!request.Headers.TryGetValue(BearerTokenHeaderName, out var authorization)) {
|
||||||
|
context.Result = Result.Forbidden("Authorization header missing").ToActionResult();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var token = authorization.FirstOrDefault()?.Split(' ').Last();
|
||||||
|
var validationResult = await ValidateJwtTokenAsync(token);
|
||||||
|
if (!validationResult.IsSuccess) {
|
||||||
|
context.Result = validationResult.ToActionResult();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var tokenData = validationResult.Value;
|
||||||
|
|
||||||
|
context.HttpContext.Items[HttpContextValue.JwtTokenData] = tokenData;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task<Result<JwtTokenData?>> ValidateJwtTokenAsync(string? token) {
|
||||||
|
if (string.IsNullOrWhiteSpace(token))
|
||||||
|
return Result<JwtTokenData?>.Forbidden(null, "Token is missing");
|
||||||
|
|
||||||
|
if (!JwtGenerator.TryValidateToken(
|
||||||
|
_jwtSettingsConfiguration.Secret,
|
||||||
|
_jwtSettingsConfiguration.Issuer,
|
||||||
|
_jwtSettingsConfiguration.Audience,
|
||||||
|
token,
|
||||||
|
out var jwtTokenClaims,
|
||||||
|
out string? errorMessage
|
||||||
|
)) {
|
||||||
|
return Result<JwtTokenData?>.InternalServerError(null, errorMessage);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (jwtTokenClaims == null ||
|
||||||
|
jwtTokenClaims.Username == null ||
|
||||||
|
jwtTokenClaims.Roles == null ||
|
||||||
|
jwtTokenClaims.IssuedAt == null ||
|
||||||
|
jwtTokenClaims.ExpiresAt == null
|
||||||
|
) {
|
||||||
|
return Result<JwtTokenData?>.Forbidden(null, "Invalid JWT token or claims");
|
||||||
|
}
|
||||||
|
|
||||||
|
var settingsResult = await _settingsService.LoadAsync();
|
||||||
|
if (!settingsResult.IsSuccess || settingsResult.Value == null) {
|
||||||
|
return Result<JwtTokenData?>.InternalServerError(null, "Failed to load settings");
|
||||||
|
}
|
||||||
|
|
||||||
|
var settings = settingsResult.Value;
|
||||||
|
var userResult = settings.GetUserByName(jwtTokenClaims.Username);
|
||||||
|
if (!userResult.IsSuccess || userResult.Value == null) {
|
||||||
|
return Result<JwtTokenData?>.Forbidden(null, "User not found");
|
||||||
|
}
|
||||||
|
|
||||||
|
var user = userResult.Value;
|
||||||
|
var jwtTokenData = new JwtTokenData {
|
||||||
|
Token = token,
|
||||||
|
Username = jwtTokenClaims.Username,
|
||||||
|
IssuedAt = jwtTokenClaims.IssuedAt.Value,
|
||||||
|
ExpiresAt = jwtTokenClaims.ExpiresAt.Value,
|
||||||
|
UserId = user.Id,
|
||||||
|
};
|
||||||
|
return Result<JwtTokenData?>.Ok(jwtTokenData);
|
||||||
|
}
|
||||||
|
}
|
||||||
9
src/MaksIT.Webapi/Authorization/HttpContextValue.cs
Normal file
9
src/MaksIT.Webapi/Authorization/HttpContextValue.cs
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
using MaksIT.Core.Abstractions;
|
||||||
|
|
||||||
|
namespace MaksIT.Webapi.Authorization;
|
||||||
|
|
||||||
|
public class HttpContextValue : Enumeration {
|
||||||
|
public static readonly HttpContextValue JwtTokenData = new(0, "JwtTokenData");
|
||||||
|
|
||||||
|
private HttpContextValue(int id, string value) : base(id, value) { }
|
||||||
|
}
|
||||||
9
src/MaksIT.Webapi/Authorization/JwtTokenData.cs
Normal file
9
src/MaksIT.Webapi/Authorization/JwtTokenData.cs
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
namespace MaksIT.Webapi.Authorization;
|
||||||
|
|
||||||
|
public class JwtTokenData {
|
||||||
|
public Guid UserId { get; set; }
|
||||||
|
public required string Username { get; set; }
|
||||||
|
public required string Token { get; set; }
|
||||||
|
public required DateTime IssuedAt { get; set; }
|
||||||
|
public required DateTime ExpiresAt { get; set; }
|
||||||
|
}
|
||||||
@ -1,11 +1,11 @@
|
|||||||
using Microsoft.Extensions.Options;
|
using Microsoft.Extensions.Options;
|
||||||
|
|
||||||
|
|
||||||
using MaksIT.LetsEncryptServer.Services;
|
using MaksIT.Webapi.Services;
|
||||||
using MaksIT.LetsEncrypt.Entities;
|
using MaksIT.LetsEncrypt.Entities;
|
||||||
using MaksIT.Results;
|
using MaksIT.Results;
|
||||||
|
|
||||||
namespace MaksIT.LetsEncryptServer.BackgroundServices {
|
namespace MaksIT.Webapi.BackgroundServices {
|
||||||
public class AutoRenewal : BackgroundService {
|
public class AutoRenewal : BackgroundService {
|
||||||
|
|
||||||
private readonly IOptions<Configuration> _appSettings;
|
private readonly IOptions<Configuration> _appSettings;
|
||||||
@ -1,9 +1,9 @@
|
|||||||
using Microsoft.Extensions.Options;
|
using Microsoft.Extensions.Options;
|
||||||
using MaksIT.LetsEncryptServer.Domain;
|
using MaksIT.Webapi.Domain;
|
||||||
using MaksIT.LetsEncryptServer.Services;
|
using MaksIT.Webapi.Services;
|
||||||
|
|
||||||
|
|
||||||
namespace MaksIT.LetsEncryptServer.BackgroundServices {
|
namespace MaksIT.Webapi.BackgroundServices {
|
||||||
|
|
||||||
public class Initialization : BackgroundService {
|
public class Initialization : BackgroundService {
|
||||||
private readonly IServiceProvider _serviceProvider;
|
private readonly IServiceProvider _serviceProvider;
|
||||||
@ -1,6 +1,6 @@
|
|||||||
using MaksIT.LetsEncrypt;
|
using MaksIT.LetsEncrypt;
|
||||||
|
|
||||||
namespace MaksIT.LetsEncryptServer {
|
namespace MaksIT.Webapi {
|
||||||
|
|
||||||
public class Agent {
|
public class Agent {
|
||||||
public required string AgentHostname { get; set; }
|
public required string AgentHostname { get; set; }
|
||||||
@ -1,12 +1,13 @@
|
|||||||
using Microsoft.AspNetCore.Mvc;
|
using MaksIT.Models.LetsEncryptServer.Account.Requests;
|
||||||
|
using MaksIT.Webapi.Authorization.Filters;
|
||||||
|
using MaksIT.Webapi.Services;
|
||||||
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
|
||||||
using MaksIT.LetsEncryptServer.Services;
|
namespace MaksIT.Webapi.Controllers;
|
||||||
using MaksIT.Models.LetsEncryptServer.Account.Requests;
|
|
||||||
|
|
||||||
namespace MaksIT.LetsEncryptServer.Controllers;
|
|
||||||
|
|
||||||
[ApiController]
|
[ApiController]
|
||||||
[Route("api")]
|
[Route("api")]
|
||||||
|
[ServiceFilter(typeof(JwtAuthorizationFilter))]
|
||||||
public class AccountController : ControllerBase {
|
public class AccountController : ControllerBase {
|
||||||
private readonly IAccountService _accountService;
|
private readonly IAccountService _accountService;
|
||||||
|
|
||||||
@ -18,6 +19,7 @@ public class AccountController : ControllerBase {
|
|||||||
|
|
||||||
#region Accounts
|
#region Accounts
|
||||||
|
|
||||||
|
[ServiceFilter(typeof(JwtAuthorizationFilter))]
|
||||||
[HttpGet("accounts")]
|
[HttpGet("accounts")]
|
||||||
public async Task<IActionResult> GetAccounts() {
|
public async Task<IActionResult> GetAccounts() {
|
||||||
var result = await _accountService.GetAccountsAsync();
|
var result = await _accountService.GetAccountsAsync();
|
||||||
@ -1,4 +1,5 @@
|
|||||||
using MaksIT.LetsEncryptServer.Services;
|
using MaksIT.Webapi.Authorization.Filters;
|
||||||
|
using MaksIT.Webapi.Services;
|
||||||
using Microsoft.AspNetCore.Mvc;
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
|
||||||
namespace LetsEncryptServer.Controllers;
|
namespace LetsEncryptServer.Controllers;
|
||||||
@ -6,6 +7,7 @@ namespace LetsEncryptServer.Controllers;
|
|||||||
|
|
||||||
[ApiController]
|
[ApiController]
|
||||||
[Route("api")]
|
[Route("api")]
|
||||||
|
[ServiceFilter(typeof(JwtAuthorizationFilter))]
|
||||||
public class AgentController : ControllerBase {
|
public class AgentController : ControllerBase {
|
||||||
|
|
||||||
private readonly IAgentService _agentController;
|
private readonly IAgentService _agentController;
|
||||||
@ -1,10 +1,12 @@
|
|||||||
using MaksIT.LetsEncryptServer.Services;
|
using MaksIT.Webapi.Authorization.Filters;
|
||||||
|
using MaksIT.Webapi.Services;
|
||||||
using Microsoft.AspNetCore.Mvc;
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
|
||||||
namespace LetsEncryptServer.Controllers;
|
namespace LetsEncryptServer.Controllers;
|
||||||
|
|
||||||
[ApiController]
|
[ApiController]
|
||||||
[Route("api")]
|
[Route("api")]
|
||||||
|
[ServiceFilter(typeof(JwtAuthorizationFilter))]
|
||||||
public class CacheController(ICacheService cacheService) : ControllerBase {
|
public class CacheController(ICacheService cacheService) : ControllerBase {
|
||||||
private readonly ICacheService _cacheService = cacheService;
|
private readonly ICacheService _cacheService = cacheService;
|
||||||
|
|
||||||
@ -32,8 +34,8 @@ public class CacheController(ICacheService cacheService) : ControllerBase {
|
|||||||
}
|
}
|
||||||
|
|
||||||
[HttpDelete("cache")]
|
[HttpDelete("cache")]
|
||||||
public IActionResult DeleteCache() {
|
public async Task<IActionResult> DeleteCache() {
|
||||||
var result = _cacheService.DeleteCacheAsync();
|
var result = await _cacheService.DeleteCacheAsync();
|
||||||
return result.ToActionResult();
|
return result.ToActionResult();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1,13 +1,15 @@
|
|||||||
using Microsoft.AspNetCore.Mvc;
|
|
||||||
using MaksIT.LetsEncryptServer.Services;
|
|
||||||
using MaksIT.Models.LetsEncryptServer.CertsFlow.Requests;
|
using MaksIT.Models.LetsEncryptServer.CertsFlow.Requests;
|
||||||
|
using MaksIT.Webapi.Authorization.Filters;
|
||||||
|
using MaksIT.Webapi.Services;
|
||||||
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
|
||||||
namespace MaksIT.LetsEncryptServer.Controllers {
|
namespace MaksIT.Webapi.Controllers {
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Certificates flow controller, used for granular testing purposes
|
/// Certificates flow controller, used for granular testing purposes
|
||||||
/// </summary>
|
/// </summary>
|
||||||
[ApiController]
|
[ApiController]
|
||||||
[Route("api/certs")]
|
[Route("api/certs")]
|
||||||
|
[ServiceFilter(typeof(JwtAuthorizationFilter))]
|
||||||
public class CertsFlowController : ControllerBase {
|
public class CertsFlowController : ControllerBase {
|
||||||
private readonly ICertsFlowService _certsFlowService;
|
private readonly ICertsFlowService _certsFlowService;
|
||||||
|
|
||||||
65
src/MaksIT.Webapi/Controllers/IdentityController.cs
Normal file
65
src/MaksIT.Webapi/Controllers/IdentityController.cs
Normal file
@ -0,0 +1,65 @@
|
|||||||
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
using MaksIT.Webapi.Authorization.Extensions;
|
||||||
|
using MaksIT.Webapi.Authorization.Filters;
|
||||||
|
using MaksIT.Webapi.Services;
|
||||||
|
|
||||||
|
using MaksIT.Models.LetsEncryptServer.Identity.Login;
|
||||||
|
using MaksIT.Models.LetsEncryptServer.Identity.Logout;
|
||||||
|
using MaksIT.Models.LetsEncryptServer.Identity.User;
|
||||||
|
|
||||||
|
|
||||||
|
namespace MaksIT.Webapi.Controllers;
|
||||||
|
|
||||||
|
[ApiController]
|
||||||
|
[Route("api/identity")]
|
||||||
|
public class IdentityController(
|
||||||
|
IIdentityService identityService
|
||||||
|
) : ControllerBase {
|
||||||
|
|
||||||
|
private readonly IIdentityService _identityService = identityService;
|
||||||
|
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Patch user data.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="id">Nullable Id as user can patch his own data</param>
|
||||||
|
/// <param name="requestData"></param>
|
||||||
|
/// <returns></returns>
|
||||||
|
[ServiceFilter(typeof(JwtAuthorizationFilter))]
|
||||||
|
[HttpPatch("user/{id:guid}")]
|
||||||
|
[ProducesResponseType(typeof(UserResponse), StatusCodes.Status200OK)]
|
||||||
|
public async Task<IActionResult> PatchUser(Guid id, [FromBody] PatchUserRequest requestData) {
|
||||||
|
var jwtTokenDataResult = HttpContext.GetJwtTokenData();
|
||||||
|
if (!jwtTokenDataResult.IsSuccess || jwtTokenDataResult.Value == null)
|
||||||
|
return jwtTokenDataResult.ToActionResult();
|
||||||
|
|
||||||
|
var jwtTokenData = jwtTokenDataResult.Value;
|
||||||
|
|
||||||
|
var result = await _identityService.PatchUserAsync(jwtTokenData, id, requestData);
|
||||||
|
return result.ToActionResult();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
#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();
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpPost("logout")]
|
||||||
|
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||||
|
public async Task<IActionResult> Logout([FromBody] LogoutRequest requestData) {
|
||||||
|
var result = await _identityService.Logout(requestData);
|
||||||
|
return result.ToActionResult();
|
||||||
|
}
|
||||||
|
#endregion
|
||||||
|
}
|
||||||
@ -1,10 +1,10 @@
|
|||||||
using Microsoft.AspNetCore.Mvc;
|
using Microsoft.AspNetCore.Mvc;
|
||||||
using Microsoft.Extensions.Options;
|
using Microsoft.Extensions.Options;
|
||||||
|
|
||||||
using MaksIT.LetsEncryptServer.Services;
|
using MaksIT.Webapi.Services;
|
||||||
|
|
||||||
|
|
||||||
namespace MaksIT.LetsEncryptServer.Controllers;
|
namespace MaksIT.Webapi.Controllers;
|
||||||
|
|
||||||
[ApiController]
|
[ApiController]
|
||||||
[Route(".well-known")]
|
[Route(".well-known")]
|
||||||
@ -10,17 +10,17 @@ ARG BUILD_CONFIGURATION=Release
|
|||||||
WORKDIR /src
|
WORKDIR /src
|
||||||
COPY ["Models/Models.csproj", "Models/"]
|
COPY ["Models/Models.csproj", "Models/"]
|
||||||
COPY ["LetsEncrypt/LetsEncrypt.csproj", "LetsEncrypt/"]
|
COPY ["LetsEncrypt/LetsEncrypt.csproj", "LetsEncrypt/"]
|
||||||
COPY ["LetsEncryptServer/LetsEncryptServer.csproj", "LetsEncryptServer/"]
|
COPY ["MaksIT.Webapi/MaksIT.Webapi.csproj", "MaksIT.Webapi/"]
|
||||||
RUN dotnet restore "./LetsEncryptServer/LetsEncryptServer.csproj"
|
RUN dotnet restore "./MaksIT.Webapi/MaksIT.Webapi.csproj"
|
||||||
COPY . .
|
COPY . .
|
||||||
WORKDIR "/src/LetsEncryptServer"
|
WORKDIR "/src/MaksIT.Webapi"
|
||||||
RUN dotnet build "./LetsEncryptServer.csproj" -c $BUILD_CONFIGURATION -o /app/build
|
RUN dotnet build "./MaksIT.Webapi.csproj" -c $BUILD_CONFIGURATION -o /app/build
|
||||||
|
|
||||||
FROM build AS publish
|
FROM build AS publish
|
||||||
ARG BUILD_CONFIGURATION=Release
|
ARG BUILD_CONFIGURATION=Release
|
||||||
RUN dotnet publish "./LetsEncryptServer.csproj" -c $BUILD_CONFIGURATION -o /app/publish /p:UseAppHost=false
|
RUN dotnet publish "./MaksIT.Webapi.csproj" -c $BUILD_CONFIGURATION -o /app/publish /p:UseAppHost=false
|
||||||
|
|
||||||
FROM base AS final
|
FROM base AS final
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
COPY --from=publish /app/publish .
|
COPY --from=publish /app/publish .
|
||||||
ENTRYPOINT ["dotnet", "LetsEncryptServer.dll"]
|
ENTRYPOINT ["dotnet", "MaksIT.Webapi.dll"]
|
||||||
@ -1,7 +1,7 @@
|
|||||||
using MaksIT.Core.Abstractions.Domain;
|
using MaksIT.Core.Abstractions.Domain;
|
||||||
using System.Linq.Dynamic.Core.Tokenizer;
|
using System.Linq.Dynamic.Core.Tokenizer;
|
||||||
|
|
||||||
namespace MaksIT.LetsEncryptServer.Domain;
|
namespace MaksIT.Webapi.Domain;
|
||||||
|
|
||||||
public class JwtToken(Guid id) : DomainDocumentBase<Guid>(id) {
|
public class JwtToken(Guid id) : DomainDocumentBase<Guid>(id) {
|
||||||
|
|
||||||
@ -2,7 +2,7 @@
|
|||||||
using MaksIT.Core.Security;
|
using MaksIT.Core.Security;
|
||||||
using MaksIT.Results;
|
using MaksIT.Results;
|
||||||
|
|
||||||
namespace MaksIT.LetsEncryptServer.Domain;
|
namespace MaksIT.Webapi.Domain;
|
||||||
|
|
||||||
public class Settings : DomainObjectBase {
|
public class Settings : DomainObjectBase {
|
||||||
public bool Init { get; set; }
|
public bool Init { get; set; }
|
||||||
@ -25,6 +25,13 @@ public class Settings : DomainObjectBase {
|
|||||||
return Result<Settings?>.Ok(this);
|
return Result<Settings?>.Ok(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public Result<User?> GetUserById(Guid id) {
|
||||||
|
var user = Users.FirstOrDefault(x => x.Id == id);
|
||||||
|
if (user == null)
|
||||||
|
return Result<User?>.NotFound(null, "User not found.");
|
||||||
|
return Result<User?>.Ok(user);
|
||||||
|
}
|
||||||
|
|
||||||
public Result<User?> GetUserByName(string name) {
|
public Result<User?> GetUserByName(string name) {
|
||||||
var user = Users.FirstOrDefault(x => x.Name == name);
|
var user = Users.FirstOrDefault(x => x.Name == name);
|
||||||
if (user == null)
|
if (user == null)
|
||||||
@ -2,7 +2,7 @@
|
|||||||
using MaksIT.Core.Security;
|
using MaksIT.Core.Security;
|
||||||
using MaksIT.Results;
|
using MaksIT.Results;
|
||||||
|
|
||||||
namespace MaksIT.LetsEncryptServer.Domain;
|
namespace MaksIT.Webapi.Domain;
|
||||||
|
|
||||||
public class User(
|
public class User(
|
||||||
Guid id
|
Guid id
|
||||||
@ -1,6 +1,6 @@
|
|||||||
using MaksIT.Core.Abstractions.Dto;
|
using MaksIT.Core.Abstractions.Dto;
|
||||||
|
|
||||||
namespace MaksIT.LetsEncryptServer.Dto;
|
namespace MaksIT.Webapi.Dto;
|
||||||
|
|
||||||
public class JwtTokenDto : DtoDocumentBase<Guid> {
|
public class JwtTokenDto : DtoDocumentBase<Guid> {
|
||||||
public required string Token { get; set; }
|
public required string Token { get; set; }
|
||||||
@ -1,4 +1,4 @@
|
|||||||
namespace MaksIT.LetsEncryptServer.Dto;
|
namespace MaksIT.Webapi.Dto;
|
||||||
|
|
||||||
public class SettingsDto {
|
public class SettingsDto {
|
||||||
public required bool Init { get; set; }
|
public required bool Init { get; set; }
|
||||||
@ -1,6 +1,6 @@
|
|||||||
using MaksIT.Core.Abstractions.Dto;
|
using MaksIT.Core.Abstractions.Dto;
|
||||||
|
|
||||||
namespace MaksIT.LetsEncryptServer.Dto;
|
namespace MaksIT.Webapi.Dto;
|
||||||
|
|
||||||
public class UserDto : DtoDocumentBase<Guid> {
|
public class UserDto : DtoDocumentBase<Guid> {
|
||||||
public required string Name { get; set; } = string.Empty;
|
public required string Name { get; set; } = string.Empty;
|
||||||
@ -1,24 +1,24 @@
|
|||||||
<Project Sdk="Microsoft.NET.Sdk.Web">
|
<Project Sdk="Microsoft.NET.Sdk.Web">
|
||||||
|
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<TargetFramework>net8.0</TargetFramework>
|
<TargetFramework>net8.0</TargetFramework>
|
||||||
<Nullable>enable</Nullable>
|
<Nullable>enable</Nullable>
|
||||||
<ImplicitUsings>enable</ImplicitUsings>
|
<ImplicitUsings>enable</ImplicitUsings>
|
||||||
<RootNamespace>MaksIT.$(MSBuildProjectName.Replace(" ", "_"))</RootNamespace>
|
<RootNamespace>$(MSBuildProjectName.Replace(" ", "_"))</RootNamespace>
|
||||||
<DockerDefaultTargetOS>Linux</DockerDefaultTargetOS>
|
<DockerDefaultTargetOS>Linux</DockerDefaultTargetOS>
|
||||||
<DockerComposeProjectPath>..\docker-compose.dcproj</DockerComposeProjectPath>
|
<DockerComposeProjectPath>..\docker-compose.dcproj</DockerComposeProjectPath>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="MaksIT.Results" Version="1.1.1" />
|
<PackageReference Include="MaksIT.Results" Version="1.1.1" />
|
||||||
<PackageReference Include="Microsoft.Extensions.Caching.Memory" Version="9.0.10" />
|
<PackageReference Include="Microsoft.Extensions.Caching.Memory" Version="10.0.0" />
|
||||||
<PackageReference Include="Microsoft.VisualStudio.Azure.Containers.Tools.Targets" Version="1.22.1" />
|
<PackageReference Include="Microsoft.VisualStudio.Azure.Containers.Tools.Targets" Version="1.22.1" />
|
||||||
<PackageReference Include="Swashbuckle.AspNetCore" Version="9.0.6" />
|
<PackageReference Include="Swashbuckle.AspNetCore" Version="10.0.0" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<ProjectReference Include="..\LetsEncrypt\LetsEncrypt.csproj" />
|
<ProjectReference Include="..\LetsEncrypt\LetsEncrypt.csproj" />
|
||||||
<ProjectReference Include="..\Models\Models.csproj" />
|
<ProjectReference Include="..\Models\MaksIT.Models.csproj" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
</Project>
|
</Project>
|
||||||
@ -1,19 +1,17 @@
|
|||||||
using System.Text.Json.Serialization;
|
|
||||||
using MaksIT.Core.Logging;
|
using MaksIT.Core.Logging;
|
||||||
using MaksIT.Core.Webapi.Middlewares;
|
using MaksIT.Core.Webapi.Middlewares;
|
||||||
using MaksIT.LetsEncrypt.Extensions;
|
using MaksIT.LetsEncrypt.Extensions;
|
||||||
using MaksIT.LetsEncryptServer;
|
using MaksIT.Webapi;
|
||||||
using MaksIT.LetsEncryptServer.Services;
|
using MaksIT.Webapi.Authorization.Filters;
|
||||||
using MaksIT.LetsEncryptServer.BackgroundServices;
|
using MaksIT.Webapi.BackgroundServices;
|
||||||
|
using MaksIT.Webapi.Services;
|
||||||
|
using System.Text.Json.Serialization;
|
||||||
|
|
||||||
var builder = WebApplication.CreateBuilder(args);
|
var builder = WebApplication.CreateBuilder(args);
|
||||||
|
|
||||||
// Extract configuration
|
#region Configuration setup
|
||||||
var configuration = builder.Configuration;
|
var configuration = builder.Configuration;
|
||||||
|
|
||||||
// Add logging
|
|
||||||
builder.Logging.AddConsoleLogger();
|
|
||||||
|
|
||||||
var configMapPath = Path.Combine(Path.DirectorySeparatorChar.ToString(), "configMap", "appsettings.json");
|
var configMapPath = Path.Combine(Path.DirectorySeparatorChar.ToString(), "configMap", "appsettings.json");
|
||||||
if (File.Exists(configMapPath)) {
|
if (File.Exists(configMapPath)) {
|
||||||
configuration.AddJsonFile(configMapPath, optional: false, reloadOnChange: true);
|
configuration.AddJsonFile(configMapPath, optional: false, reloadOnChange: true);
|
||||||
@ -30,7 +28,10 @@ var appSettings = configurationSection.Get<Configuration>() ?? throw new Argumen
|
|||||||
|
|
||||||
// Allow configurations to be available through IOptions<Configuration>
|
// Allow configurations to be available through IOptions<Configuration>
|
||||||
builder.Services.Configure<Configuration>(configurationSection);
|
builder.Services.Configure<Configuration>(configurationSection);
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
// Add logging
|
||||||
|
builder.Logging.AddConsoleLogger();
|
||||||
|
|
||||||
// Add services to the container.
|
// Add services to the container.
|
||||||
builder.Services.AddControllers()
|
builder.Services.AddControllers()
|
||||||
@ -38,9 +39,14 @@ builder.Services.AddControllers()
|
|||||||
options.JsonSerializerOptions.DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull;
|
options.JsonSerializerOptions.DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Add custom authorization filter
|
||||||
|
builder.Services.AddScoped<JwtAuthorizationFilter>();
|
||||||
|
|
||||||
|
#region Swagger
|
||||||
// Learn more about configuring Swagger/OpenAPI at https://aka.ms/aspnetcore/swashbuckle
|
// Learn more about configuring Swagger/OpenAPI at https://aka.ms/aspnetcore/swashbuckle
|
||||||
builder.Services.AddEndpointsApiExplorer();
|
builder.Services.AddEndpointsApiExplorer();
|
||||||
builder.Services.AddSwaggerGen();
|
builder.Services.AddSwaggerGen();
|
||||||
|
#endregion
|
||||||
|
|
||||||
builder.Services.AddCors();
|
builder.Services.AddCors();
|
||||||
|
|
||||||
@ -58,9 +64,10 @@ builder.Services.AddHttpClient<IAgentService, AgentService>();
|
|||||||
builder.Services.AddScoped<IAccountService, AccountService>();
|
builder.Services.AddScoped<IAccountService, AccountService>();
|
||||||
builder.Services.AddScoped<IIdentityService, IdentityService>();
|
builder.Services.AddScoped<IIdentityService, IdentityService>();
|
||||||
|
|
||||||
// Hosted services
|
#region Hosted services
|
||||||
builder.Services.AddHostedService<AutoRenewal>();
|
builder.Services.AddHostedService<AutoRenewal>();
|
||||||
builder.Services.AddHostedService<Initialization>();
|
builder.Services.AddHostedService<Initialization>();
|
||||||
|
#endregion
|
||||||
|
|
||||||
var app = builder.Build();
|
var app = builder.Build();
|
||||||
|
|
||||||
@ -1,13 +1,13 @@
|
|||||||
|
using MaksIT.Core.Webapi.Models;
|
||||||
using LetsEncryptServer.Abstractions;
|
|
||||||
using MaksIT.Core.Webapi.Models;
|
|
||||||
using MaksIT.LetsEncrypt.Entities;
|
using MaksIT.LetsEncrypt.Entities;
|
||||||
using MaksIT.Models.LetsEncryptServer.Account.Requests;
|
using MaksIT.Models.LetsEncryptServer.Account.Requests;
|
||||||
using MaksIT.Models.LetsEncryptServer.Account.Responses;
|
using MaksIT.Models.LetsEncryptServer.Account.Responses;
|
||||||
using MaksIT.Results;
|
using MaksIT.Results;
|
||||||
|
using MaksIT.Webapi.Abstractions.Services;
|
||||||
|
using Microsoft.Extensions.Options;
|
||||||
|
|
||||||
namespace MaksIT.LetsEncryptServer.Services;
|
|
||||||
|
|
||||||
|
namespace MaksIT.Webapi.Services;
|
||||||
|
|
||||||
public interface IAccountService {
|
public interface IAccountService {
|
||||||
Task<Result<GetAccountResponse[]?>> GetAccountsAsync();
|
Task<Result<GetAccountResponse[]?>> GetAccountsAsync();
|
||||||
@ -17,27 +17,21 @@ public interface IAccountService {
|
|||||||
Task<Result> DeleteAccountAsync(Guid accountId);
|
Task<Result> DeleteAccountAsync(Guid accountId);
|
||||||
}
|
}
|
||||||
|
|
||||||
public class AccountService : ServiceBase, IAccountService {
|
public class AccountService(
|
||||||
|
ILogger<CacheService> logger,
|
||||||
private readonly ILogger<CacheService> _logger;
|
IOptions<Configuration> appSettings,
|
||||||
private readonly ICacheService _cacheService;
|
ICacheService cacheService,
|
||||||
private readonly ICertsFlowService _certsFlowService;
|
ICertsFlowService certsFlowService
|
||||||
|
) : ServiceBase(
|
||||||
public AccountService(
|
logger,
|
||||||
ILogger<CacheService> logger,
|
appSettings
|
||||||
ICacheService cacheService,
|
), IAccountService {
|
||||||
ICertsFlowService certsFlowService
|
|
||||||
) {
|
|
||||||
_logger = logger;
|
|
||||||
_cacheService = cacheService;
|
|
||||||
_certsFlowService = certsFlowService;
|
|
||||||
}
|
|
||||||
|
|
||||||
#region Accounts
|
#region Accounts
|
||||||
|
|
||||||
public async Task<Result<GetAccountResponse[]?>> GetAccountsAsync() {
|
public async Task<Result<GetAccountResponse[]?>> GetAccountsAsync() {
|
||||||
|
|
||||||
var accountsFromCacheResult = await _cacheService.LoadAccountsFromCacheAsync();
|
var accountsFromCacheResult = await cacheService.LoadAccountsFromCacheAsync();
|
||||||
if (!accountsFromCacheResult.IsSuccess || accountsFromCacheResult.Value == null) {
|
if (!accountsFromCacheResult.IsSuccess || accountsFromCacheResult.Value == null) {
|
||||||
return accountsFromCacheResult
|
return accountsFromCacheResult
|
||||||
.ToResultOfType<GetAccountResponse[]?>(_ => null);
|
.ToResultOfType<GetAccountResponse[]?>(_ => null);
|
||||||
@ -51,7 +45,7 @@ public class AccountService : ServiceBase, IAccountService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public async Task<Result<GetAccountResponse?>> GetAccountAsync(Guid accountId) {
|
public async Task<Result<GetAccountResponse?>> GetAccountAsync(Guid accountId) {
|
||||||
var loadFromCacheResult = await _cacheService.LoadAccountFromCacheAsync(accountId);
|
var loadFromCacheResult = await cacheService.LoadAccountFromCacheAsync(accountId);
|
||||||
if (!loadFromCacheResult.IsSuccess || loadFromCacheResult.Value == null) {
|
if (!loadFromCacheResult.IsSuccess || loadFromCacheResult.Value == null) {
|
||||||
return loadFromCacheResult.ToResultOfType<GetAccountResponse?>(_ => null);
|
return loadFromCacheResult.ToResultOfType<GetAccountResponse?>(_ => null);
|
||||||
}
|
}
|
||||||
@ -63,7 +57,7 @@ public class AccountService : ServiceBase, IAccountService {
|
|||||||
|
|
||||||
public async Task<Result<GetAccountResponse?>> PostAccountAsync(PostAccountRequest requestData) {
|
public async Task<Result<GetAccountResponse?>> PostAccountAsync(PostAccountRequest requestData) {
|
||||||
|
|
||||||
var fullFlowResult = await _certsFlowService.FullFlow(
|
var fullFlowResult = await certsFlowService.FullFlow(
|
||||||
requestData.IsStaging,
|
requestData.IsStaging,
|
||||||
null,
|
null,
|
||||||
requestData.Description,
|
requestData.Description,
|
||||||
@ -77,7 +71,7 @@ public class AccountService : ServiceBase, IAccountService {
|
|||||||
|
|
||||||
var accountId = fullFlowResult.Value.Value;
|
var accountId = fullFlowResult.Value.Value;
|
||||||
|
|
||||||
var loadAccountFromCacheResult = await _cacheService.LoadAccountFromCacheAsync(accountId);
|
var loadAccountFromCacheResult = await cacheService.LoadAccountFromCacheAsync(accountId);
|
||||||
if (!loadAccountFromCacheResult.IsSuccess || loadAccountFromCacheResult.Value == null) {
|
if (!loadAccountFromCacheResult.IsSuccess || loadAccountFromCacheResult.Value == null) {
|
||||||
return loadAccountFromCacheResult.ToResultOfType<GetAccountResponse?>(_ => null);
|
return loadAccountFromCacheResult.ToResultOfType<GetAccountResponse?>(_ => null);
|
||||||
}
|
}
|
||||||
@ -88,7 +82,7 @@ public class AccountService : ServiceBase, IAccountService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public async Task<Result<GetAccountResponse?>> PatchAccountAsync(Guid accountId, PatchAccountRequest requestData) {
|
public async Task<Result<GetAccountResponse?>> PatchAccountAsync(Guid accountId, PatchAccountRequest requestData) {
|
||||||
var loadAccountResult = await _cacheService.LoadAccountFromCacheAsync(accountId);
|
var loadAccountResult = await cacheService.LoadAccountFromCacheAsync(accountId);
|
||||||
if (!loadAccountResult.IsSuccess || loadAccountResult.Value == null) {
|
if (!loadAccountResult.IsSuccess || loadAccountResult.Value == null) {
|
||||||
return loadAccountResult.ToResultOfType<GetAccountResponse?>(_ => null);
|
return loadAccountResult.ToResultOfType<GetAccountResponse?>(_ => null);
|
||||||
}
|
}
|
||||||
@ -153,13 +147,13 @@ public class AccountService : ServiceBase, IAccountService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var saveResult = await _cacheService.SaveToCacheAsync(accountId, cache);
|
var saveResult = await cacheService.SaveToCacheAsync(accountId, cache);
|
||||||
if (!saveResult.IsSuccess) {
|
if (!saveResult.IsSuccess) {
|
||||||
return saveResult.ToResultOfType<GetAccountResponse?>(default);
|
return saveResult.ToResultOfType<GetAccountResponse?>(default);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (hostnamesToAdd.Count > 0) {
|
if (hostnamesToAdd.Count > 0) {
|
||||||
var fullFlowResult = await _certsFlowService.FullFlow(
|
var fullFlowResult = await certsFlowService.FullFlow(
|
||||||
cache.IsStaging,
|
cache.IsStaging,
|
||||||
cache.AccountId,
|
cache.AccountId,
|
||||||
cache.Description,
|
cache.Description,
|
||||||
@ -173,7 +167,7 @@ public class AccountService : ServiceBase, IAccountService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (hostnamesToRemove.Count > 0) {
|
if (hostnamesToRemove.Count > 0) {
|
||||||
var revokeResult = await _certsFlowService.FullRevocationFlow(
|
var revokeResult = await certsFlowService.FullRevocationFlow(
|
||||||
cache.IsStaging,
|
cache.IsStaging,
|
||||||
cache.AccountId,
|
cache.AccountId,
|
||||||
cache.Description,
|
cache.Description,
|
||||||
@ -186,7 +180,7 @@ public class AccountService : ServiceBase, IAccountService {
|
|||||||
}
|
}
|
||||||
#endregion
|
#endregion
|
||||||
|
|
||||||
loadAccountResult = await _cacheService.LoadAccountFromCacheAsync(accountId);
|
loadAccountResult = await cacheService.LoadAccountFromCacheAsync(accountId);
|
||||||
if (!loadAccountResult.IsSuccess || loadAccountResult.Value == null) {
|
if (!loadAccountResult.IsSuccess || loadAccountResult.Value == null) {
|
||||||
return loadAccountResult.ToResultOfType<GetAccountResponse?>(_ => null);
|
return loadAccountResult.ToResultOfType<GetAccountResponse?>(_ => null);
|
||||||
}
|
}
|
||||||
@ -198,7 +192,7 @@ public class AccountService : ServiceBase, IAccountService {
|
|||||||
// TODO: Revoke all certificates
|
// TODO: Revoke all certificates
|
||||||
|
|
||||||
// Remove from cache
|
// Remove from cache
|
||||||
return await _cacheService.DeleteFromCacheAsync(accountId);
|
return await cacheService.DeleteAccountCacheAsync(accountId);
|
||||||
}
|
}
|
||||||
#endregion
|
#endregion
|
||||||
|
|
||||||
@ -1,12 +1,13 @@
|
|||||||
using MaksIT.Models.Agent.Requests;
|
using MaksIT.Models.Agent.Requests;
|
||||||
using MaksIT.Results;
|
using MaksIT.Results;
|
||||||
|
using MaksIT.Webapi.Abstractions.Services;
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
using Microsoft.Extensions.Options;
|
using Microsoft.Extensions.Options;
|
||||||
using Models.Agent.Responses;
|
using MaksIT.Models.Agent.Responses;
|
||||||
using System.Text;
|
using System.Text;
|
||||||
using System.Text.Json;
|
using System.Text.Json;
|
||||||
|
|
||||||
namespace MaksIT.LetsEncryptServer.Services {
|
namespace MaksIT.Webapi.Services {
|
||||||
|
|
||||||
public interface IAgentService {
|
public interface IAgentService {
|
||||||
Task<Result<HelloWorldResponse?>> GetHelloWorld();
|
Task<Result<HelloWorldResponse?>> GetHelloWorld();
|
||||||
@ -14,21 +15,14 @@ namespace MaksIT.LetsEncryptServer.Services {
|
|||||||
Task<Result> ReloadService(string serviceName);
|
Task<Result> ReloadService(string serviceName);
|
||||||
}
|
}
|
||||||
|
|
||||||
public class AgentService : IAgentService {
|
public class AgentService(
|
||||||
|
IOptions<Configuration> appSettings,
|
||||||
private readonly Configuration _appSettings;
|
ILogger<AgentService> logger,
|
||||||
private readonly ILogger<AgentService> _logger;
|
HttpClient httpClient
|
||||||
private readonly HttpClient _httpClient;
|
) : ServiceBase(
|
||||||
|
logger,
|
||||||
public AgentService(
|
appSettings
|
||||||
IOptions<Configuration> appSettings,
|
), IAgentService {
|
||||||
ILogger<AgentService> logger,
|
|
||||||
HttpClient httpClient
|
|
||||||
) {
|
|
||||||
_appSettings = appSettings.Value;
|
|
||||||
_logger = logger;
|
|
||||||
_httpClient = httpClient;
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task<Result<HelloWorldResponse?>> GetHelloWorld() {
|
public async Task<Result<HelloWorldResponse?>> GetHelloWorld() {
|
||||||
try {
|
try {
|
||||||
@ -39,12 +33,11 @@ namespace MaksIT.LetsEncryptServer.Services {
|
|||||||
var request = new HttpRequestMessage(HttpMethod.Get, fullAddress);
|
var request = new HttpRequestMessage(HttpMethod.Get, fullAddress);
|
||||||
request.Headers.Add("x-api-key", _appSettings.Agent.AgentKey);
|
request.Headers.Add("x-api-key", _appSettings.Agent.AgentKey);
|
||||||
|
|
||||||
_logger.LogInformation($"Sending GET request to {fullAddress}");
|
logger.LogInformation($"Sending GET request to {fullAddress}");
|
||||||
|
|
||||||
var response = await _httpClient.SendAsync(request);
|
var response = await httpClient.SendAsync(request);
|
||||||
|
|
||||||
if (response.IsSuccessStatusCode) {
|
if (response.IsSuccessStatusCode) {
|
||||||
|
|
||||||
var content = await response.Content.ReadAsStringAsync();
|
var content = await response.Content.ReadAsStringAsync();
|
||||||
|
|
||||||
return Result<HelloWorldResponse?>.Ok(new HelloWorldResponse {
|
return Result<HelloWorldResponse?>.Ok(new HelloWorldResponse {
|
||||||
@ -52,7 +45,7 @@ namespace MaksIT.LetsEncryptServer.Services {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
_logger.LogError($"Request to {endpoint} failed with status code: {response.StatusCode}");
|
logger.LogError($"Request to {endpoint} failed with status code: {response.StatusCode}");
|
||||||
return Result<HelloWorldResponse?>.InternalServerError(null, $"Request to {endpoint} failed with status code: {response.StatusCode}");
|
return Result<HelloWorldResponse?>.InternalServerError(null, $"Request to {endpoint} failed with status code: {response.StatusCode}");
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -60,7 +53,7 @@ namespace MaksIT.LetsEncryptServer.Services {
|
|||||||
catch (Exception ex) {
|
catch (Exception ex) {
|
||||||
List<string> messages = new() { "Something went wrong" };
|
List<string> messages = new() { "Something went wrong" };
|
||||||
|
|
||||||
_logger.LogError(ex, messages.FirstOrDefault());
|
logger.LogError(ex, messages.FirstOrDefault());
|
||||||
|
|
||||||
messages.Add(ex.Message);
|
messages.Add(ex.Message);
|
||||||
|
|
||||||
@ -89,20 +82,20 @@ namespace MaksIT.LetsEncryptServer.Services {
|
|||||||
request.Headers.Add("x-api-key", _appSettings.Agent.AgentKey);
|
request.Headers.Add("x-api-key", _appSettings.Agent.AgentKey);
|
||||||
request.Headers.Add("accept", "application/json");
|
request.Headers.Add("accept", "application/json");
|
||||||
|
|
||||||
var response = await _httpClient.SendAsync(request);
|
var response = await httpClient.SendAsync(request);
|
||||||
|
|
||||||
if (response.IsSuccessStatusCode) {
|
if (response.IsSuccessStatusCode) {
|
||||||
return Result.Ok();
|
return Result.Ok();
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
_logger.LogError($"Request to {endpoint} failed with status code: {response.StatusCode}");
|
logger.LogError($"Request to {endpoint} failed with status code: {response.StatusCode}");
|
||||||
return Result.InternalServerError($"Request to {endpoint} failed with status code: {response.StatusCode}");
|
return Result.InternalServerError($"Request to {endpoint} failed with status code: {response.StatusCode}");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
catch (Exception ex) {
|
catch (Exception ex) {
|
||||||
List<string> messages = new() { "Something went wrong" };
|
List<string> messages = new() { "Something went wrong" };
|
||||||
|
|
||||||
_logger.LogError(ex, messages.FirstOrDefault());
|
logger.LogError(ex, messages.FirstOrDefault());
|
||||||
|
|
||||||
messages.Add(ex.Message);
|
messages.Add(ex.Message);
|
||||||
|
|
||||||
@ -1,145 +1,95 @@
|
|||||||
using MaksIT.Core.Extensions;
|
using System.IO.Compression;
|
||||||
|
using Microsoft.Extensions.Options;
|
||||||
|
using MaksIT.Core.Extensions;
|
||||||
using MaksIT.Core.Threading;
|
using MaksIT.Core.Threading;
|
||||||
using MaksIT.LetsEncrypt.Entities;
|
using MaksIT.LetsEncrypt.Entities;
|
||||||
using MaksIT.Results;
|
using MaksIT.Results;
|
||||||
using Microsoft.Extensions.Logging;
|
using MaksIT.Webapi.Abstractions.Services;
|
||||||
using Microsoft.Extensions.Options;
|
|
||||||
using System.IO.Compression;
|
|
||||||
using System.Text.Json;
|
|
||||||
|
|
||||||
namespace MaksIT.LetsEncryptServer.Services;
|
|
||||||
|
namespace MaksIT.Webapi.Services;
|
||||||
|
|
||||||
public interface ICacheService {
|
public interface ICacheService {
|
||||||
Task<Result<RegistrationCache[]?>> LoadAccountsFromCacheAsync();
|
Task<Result<RegistrationCache[]?>> LoadAccountsFromCacheAsync();
|
||||||
Task<Result<RegistrationCache?>> LoadAccountFromCacheAsync(Guid accountId);
|
Task<Result<RegistrationCache?>> LoadAccountFromCacheAsync(Guid accountId);
|
||||||
Task<Result> SaveToCacheAsync(Guid accountId, RegistrationCache cache);
|
Task<Result> SaveToCacheAsync(Guid accountId, RegistrationCache cache);
|
||||||
Task<Result> DeleteFromCacheAsync(Guid accountId);
|
|
||||||
|
|
||||||
Task<Result<byte[]>> DownloadCacheZipAsync();
|
Task<Result<byte[]>> DownloadCacheZipAsync();
|
||||||
Task<Result<byte[]?>> DownloadAccountCacheZipAsync(Guid accountId);
|
Task<Result<byte[]?>> DownloadAccountCacheZipAsync(Guid accountId);
|
||||||
Task<Result> UploadCacheZipAsync(byte[] zipBytes);
|
Task<Result> UploadCacheZipAsync(byte[] zipBytes);
|
||||||
Task<Result> UploadAccountCacheZipAsync(Guid accountId, byte[] zipBytes);
|
Task<Result> UploadAccountCacheZipAsync(Guid accountId, byte[] zipBytes);
|
||||||
Result DeleteCacheAsync();
|
Task<Result> DeleteCacheAsync();
|
||||||
|
Task<Result> DeleteAccountCacheAsync(Guid accountId);
|
||||||
}
|
}
|
||||||
|
|
||||||
public class CacheService : ICacheService, IDisposable {
|
public class CacheService(
|
||||||
private readonly ILogger<CacheService> _logger;
|
ILogger<CacheService> logger,
|
||||||
private readonly string _cacheDirectory;
|
IOptions<Configuration> appSettings
|
||||||
private readonly LockManager _lockManager;
|
) : ServiceBase(
|
||||||
|
logger,
|
||||||
|
appSettings
|
||||||
|
), ICacheService, IDisposable {
|
||||||
|
|
||||||
|
private readonly string _cacheDirectory = appSettings.Value.CacheFolder;
|
||||||
|
private readonly LockManager _lockManager = new();
|
||||||
private readonly string tmpDir = "/tmp";
|
private readonly string tmpDir = "/tmp";
|
||||||
|
|
||||||
public CacheService(
|
|
||||||
ILogger<CacheService> logger,
|
|
||||||
IOptions<Configuration> appsettings
|
|
||||||
) {
|
|
||||||
_logger = logger;
|
|
||||||
_cacheDirectory = appsettings.Value.CacheFolder;
|
|
||||||
_lockManager = new LockManager();
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Generates the cache file path for the given account ID.
|
|
||||||
/// </summary>
|
|
||||||
private string GetCacheFilePath(Guid accountId) {
|
|
||||||
return Path.Combine(_cacheDirectory, $"{accountId}.json");
|
|
||||||
}
|
|
||||||
|
|
||||||
private Guid[] GetCachedAccounts() {
|
|
||||||
return GetCacheFilesPaths().Select(x => Path.GetFileNameWithoutExtension(x).ToGuid()).Where(x => x != Guid.Empty).ToArray();
|
|
||||||
}
|
|
||||||
|
|
||||||
private string[] GetCacheFilesPaths() {
|
|
||||||
return Directory.GetFiles(_cacheDirectory);
|
|
||||||
}
|
|
||||||
|
|
||||||
#region Cache Operations
|
|
||||||
|
|
||||||
public async Task<Result<RegistrationCache[]?>> LoadAccountsFromCacheAsync() {
|
public async Task<Result<RegistrationCache[]?>> LoadAccountsFromCacheAsync() {
|
||||||
return await _lockManager.ExecuteWithLockAsync(async () => {
|
return await _lockManager.ExecuteWithLockAsync(async () => {
|
||||||
var accountIds = GetCachedAccounts();
|
var accountIds = GetCachedAccounts();
|
||||||
var cacheLoadTasks = accountIds.Select(accountId => LoadFromCacheInternalAsync(accountId)).ToList();
|
|
||||||
|
|
||||||
var caches = new List<RegistrationCache>();
|
var caches = new List<RegistrationCache>();
|
||||||
foreach (var task in cacheLoadTasks) {
|
foreach (var accountId in accountIds) {
|
||||||
var taskResult = await task;
|
var cacheFilePath = GetCacheFilePath(accountId);
|
||||||
if (!taskResult.IsSuccess || taskResult.Value == null) {
|
if (!File.Exists(cacheFilePath)) {
|
||||||
// Depending on how you want to handle partial failures, you might want to return here
|
logger.LogWarning($"Cache file not found for account {accountId}");
|
||||||
// or continue loading other caches. For now, let's continue.
|
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
var json = await File.ReadAllTextAsync(cacheFilePath);
|
||||||
var registrationCache = taskResult.Value;
|
if (string.IsNullOrEmpty(json)) {
|
||||||
|
logger.LogWarning($"Cache file is empty for account {accountId}");
|
||||||
caches.Add(registrationCache);
|
continue;
|
||||||
|
}
|
||||||
|
var cache = json.ToObject<RegistrationCache>();
|
||||||
|
if (cache != null)
|
||||||
|
caches.Add(cache);
|
||||||
}
|
}
|
||||||
|
|
||||||
return Result<RegistrationCache[]?>.Ok(caches.ToArray());
|
return Result<RegistrationCache[]?>.Ok(caches.ToArray());
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task<Result<RegistrationCache?>> LoadFromCacheInternalAsync(Guid accountId) {
|
public async Task<Result<RegistrationCache?>> LoadAccountFromCacheAsync(Guid accountId) {
|
||||||
var cacheFilePath = GetCacheFilePath(accountId);
|
return await _lockManager.ExecuteWithLockAsync(async () => {
|
||||||
|
var cacheFilePath = GetCacheFilePath(accountId);
|
||||||
if (!File.Exists(cacheFilePath)) {
|
if (!File.Exists(cacheFilePath)) {
|
||||||
var message = $"Cache file not found for account {accountId}";
|
var message = $"Cache file not found for account {accountId}";
|
||||||
_logger.LogWarning(message);
|
logger.LogWarning(message);
|
||||||
return Result<RegistrationCache?>.InternalServerError(null, message);
|
return Result<RegistrationCache?>.InternalServerError(null, message);
|
||||||
}
|
}
|
||||||
|
var json = await File.ReadAllTextAsync(cacheFilePath);
|
||||||
var json = await File.ReadAllTextAsync(cacheFilePath);
|
if (string.IsNullOrEmpty(json)) {
|
||||||
if (string.IsNullOrEmpty(json)) {
|
var message = $"Cache file is empty for account {accountId}";
|
||||||
var message = $"Cache file is empty for account {accountId}";
|
logger.LogWarning(message);
|
||||||
_logger.LogWarning(message);
|
return Result<RegistrationCache?>.InternalServerError(null, message);
|
||||||
return Result<RegistrationCache?>.InternalServerError(null, message);
|
}
|
||||||
}
|
var cache = json.ToObject<RegistrationCache>();
|
||||||
|
return Result<RegistrationCache?>.Ok(cache);
|
||||||
var cache = json.ToObject<RegistrationCache>();
|
});
|
||||||
return Result<RegistrationCache?>.Ok(cache);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task<Result> SaveToCacheInternalAsync(Guid accountId, RegistrationCache cache) {
|
public async Task<Result> SaveToCacheAsync(Guid accountId, RegistrationCache cache) {
|
||||||
var cacheFilePath = GetCacheFilePath(accountId);
|
return await _lockManager.ExecuteWithLockAsync(async () => {
|
||||||
var json = cache.ToJson();
|
var cacheFilePath = GetCacheFilePath(accountId);
|
||||||
await File.WriteAllTextAsync(cacheFilePath, json);
|
var json = cache.ToJson();
|
||||||
_logger.LogInformation($"Cache file saved for account {accountId}");
|
await File.WriteAllTextAsync(cacheFilePath, json);
|
||||||
return Result.Ok();
|
logger.LogInformation($"Cache file saved for account {accountId}");
|
||||||
}
|
return Result.Ok();
|
||||||
|
});
|
||||||
private Result DeleteFromCacheInternal(Guid accountId) {
|
|
||||||
var cacheFilePath = GetCacheFilePath(accountId);
|
|
||||||
if (File.Exists(cacheFilePath)) {
|
|
||||||
File.Delete(cacheFilePath);
|
|
||||||
_logger.LogInformation($"Cache file deleted for account {accountId}");
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
_logger.LogWarning($"Cache file not found for account {accountId}");
|
|
||||||
}
|
|
||||||
return Result.Ok();
|
|
||||||
}
|
|
||||||
|
|
||||||
#endregion
|
|
||||||
|
|
||||||
|
|
||||||
#region
|
|
||||||
private string GetTempZipPath(string prefix)
|
|
||||||
{
|
|
||||||
var zipName = $"{prefix}_{DateTime.UtcNow:yyyyMMddHHmmss}.zip";
|
|
||||||
return Path.Combine(tmpDir, zipName);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void EnsureTempDirAndDeleteFile(string filePath)
|
|
||||||
{
|
|
||||||
Directory.CreateDirectory(tmpDir);
|
|
||||||
if (File.Exists(filePath))
|
|
||||||
File.Delete(filePath);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<Result<byte[]>> DownloadCacheZipAsync() {
|
public async Task<Result<byte[]>> DownloadCacheZipAsync() {
|
||||||
try {
|
try {
|
||||||
if (!Directory.Exists(_cacheDirectory)) {
|
if (!Directory.Exists(_cacheDirectory)) {
|
||||||
var message = "Cache directory not found.";
|
var message = "Cache directory not found.";
|
||||||
_logger.LogWarning(message);
|
logger.LogWarning(message);
|
||||||
return Result<byte[]>.NotFound(null, message);
|
return Result<byte[]>.NotFound(null, message);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -148,12 +98,12 @@ public class CacheService : ICacheService, IDisposable {
|
|||||||
ZipFile.CreateFromDirectory(_cacheDirectory, zipPath);
|
ZipFile.CreateFromDirectory(_cacheDirectory, zipPath);
|
||||||
var zipBytes = await File.ReadAllBytesAsync(zipPath);
|
var zipBytes = await File.ReadAllBytesAsync(zipPath);
|
||||||
File.Delete(zipPath);
|
File.Delete(zipPath);
|
||||||
_logger.LogInformation("Cache zipped to {ZipPath}", zipPath);
|
logger.LogInformation("Cache zipped to {ZipPath}", zipPath);
|
||||||
return Result<byte[]>.Ok(zipBytes);
|
return Result<byte[]>.Ok(zipBytes);
|
||||||
}
|
}
|
||||||
catch (Exception ex) {
|
catch (Exception ex) {
|
||||||
var message = "Error creating or reading cache zip file.";
|
var message = "Error creating or reading cache zip file.";
|
||||||
_logger.LogError(ex, message);
|
logger.LogError(ex, message);
|
||||||
return Result<byte[]>.InternalServerError(null, [message, .. ex.ExtractMessages()]);
|
return Result<byte[]>.InternalServerError(null, [message, .. ex.ExtractMessages()]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -163,7 +113,7 @@ public class CacheService : ICacheService, IDisposable {
|
|||||||
var cacheFilePath = GetCacheFilePath(accountId);
|
var cacheFilePath = GetCacheFilePath(accountId);
|
||||||
if (!File.Exists(cacheFilePath)) {
|
if (!File.Exists(cacheFilePath)) {
|
||||||
var message = $"Cache file not found for account {accountId}.";
|
var message = $"Cache file not found for account {accountId}.";
|
||||||
_logger.LogWarning(message);
|
logger.LogWarning(message);
|
||||||
return Result<byte[]?>.NotFound(null, message);
|
return Result<byte[]?>.NotFound(null, message);
|
||||||
}
|
}
|
||||||
var zipPath = GetTempZipPath($"account_cache_{accountId}");
|
var zipPath = GetTempZipPath($"account_cache_{accountId}");
|
||||||
@ -173,12 +123,12 @@ public class CacheService : ICacheService, IDisposable {
|
|||||||
}
|
}
|
||||||
var zipBytes = await File.ReadAllBytesAsync(zipPath);
|
var zipBytes = await File.ReadAllBytesAsync(zipPath);
|
||||||
File.Delete(zipPath);
|
File.Delete(zipPath);
|
||||||
_logger.LogInformation("Account cache zipped to {ZipPath}", zipPath);
|
logger.LogInformation("Account cache zipped to {ZipPath}", zipPath);
|
||||||
return Result<byte[]?>.Ok(zipBytes);
|
return Result<byte[]?>.Ok(zipBytes);
|
||||||
}
|
}
|
||||||
catch (Exception ex) {
|
catch (Exception ex) {
|
||||||
var message = "Error creating or reading account cache zip file.";
|
var message = "Error creating or reading account cache zip file.";
|
||||||
_logger.LogError(ex, message);
|
logger.LogError(ex, message);
|
||||||
return Result<byte[]?>.InternalServerError(null, [message, .. ex.ExtractMessages()]);
|
return Result<byte[]?>.InternalServerError(null, [message, .. ex.ExtractMessages()]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -190,12 +140,12 @@ public class CacheService : ICacheService, IDisposable {
|
|||||||
await File.WriteAllBytesAsync(zipPath, zipBytes);
|
await File.WriteAllBytesAsync(zipPath, zipBytes);
|
||||||
ZipFile.ExtractToDirectory(zipPath, _cacheDirectory, true);
|
ZipFile.ExtractToDirectory(zipPath, _cacheDirectory, true);
|
||||||
File.Delete(zipPath);
|
File.Delete(zipPath);
|
||||||
_logger.LogInformation("Cache unzipped from {ZipPath}", zipPath);
|
logger.LogInformation("Cache unzipped from {ZipPath}", zipPath);
|
||||||
return Result.Ok();
|
return Result.Ok();
|
||||||
}
|
}
|
||||||
catch (Exception ex) {
|
catch (Exception ex) {
|
||||||
var message = "Error uploading or extracting cache zip file.";
|
var message = "Error uploading or extracting cache zip file.";
|
||||||
_logger.LogError(ex, message);
|
logger.LogError(ex, message);
|
||||||
return Result.InternalServerError([message, .. ex.ExtractMessages()]);
|
return Result.InternalServerError([message, .. ex.ExtractMessages()]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -212,55 +162,85 @@ public class CacheService : ICacheService, IDisposable {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
File.Delete(zipPath);
|
File.Delete(zipPath);
|
||||||
_logger.LogInformation("Account cache unzipped from {ZipPath}", zipPath);
|
logger.LogInformation("Account cache unzipped from {ZipPath}", zipPath);
|
||||||
return Result.Ok();
|
return Result.Ok();
|
||||||
}
|
}
|
||||||
catch (Exception ex) {
|
catch (Exception ex) {
|
||||||
var message = "Error uploading or extracting account cache zip file.";
|
var message = "Error uploading or extracting account cache zip file.";
|
||||||
_logger.LogError(ex, message);
|
logger.LogError(ex, message);
|
||||||
return Result.InternalServerError([message, .. ex.ExtractMessages()]);
|
return Result.InternalServerError([message, .. ex.ExtractMessages()]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public Result DeleteCacheAsync() {
|
public async Task<Result> DeleteCacheAsync() {
|
||||||
try {
|
return await _lockManager.ExecuteWithLockAsync(() => {
|
||||||
if (Directory.Exists(_cacheDirectory)) {
|
try {
|
||||||
// Delete all files
|
if (Directory.Exists(_cacheDirectory)) {
|
||||||
foreach (var file in Directory.GetFiles(_cacheDirectory)) {
|
// Delete all files
|
||||||
File.Delete(file);
|
foreach (var file in Directory.GetFiles(_cacheDirectory)) {
|
||||||
|
File.Delete(file);
|
||||||
|
}
|
||||||
|
// Delete all subdirectories
|
||||||
|
foreach (var dir in Directory.GetDirectories(_cacheDirectory)) {
|
||||||
|
Directory.Delete(dir, true);
|
||||||
|
}
|
||||||
|
logger.LogInformation("Cache directory contents cleared.");
|
||||||
}
|
}
|
||||||
// Delete all subdirectories
|
else {
|
||||||
foreach (var dir in Directory.GetDirectories(_cacheDirectory)) {
|
logger.LogWarning("Cache directory not found to clear.");
|
||||||
Directory.Delete(dir, true);
|
|
||||||
}
|
}
|
||||||
_logger.LogInformation("Cache directory contents cleared.");
|
return Result.Ok();
|
||||||
|
}
|
||||||
|
catch (Exception ex) {
|
||||||
|
var message = "Error clearing cache directory contents.";
|
||||||
|
logger.LogError(ex, message);
|
||||||
|
return Result.InternalServerError([message, .. ex.ExtractMessages()]);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<Result> DeleteAccountCacheAsync(Guid accountId) {
|
||||||
|
return await _lockManager.ExecuteWithLockAsync(() => {
|
||||||
|
var cacheFilePath = GetCacheFilePath(accountId);
|
||||||
|
if (File.Exists(cacheFilePath)) {
|
||||||
|
File.Delete(cacheFilePath);
|
||||||
|
logger.LogInformation($"Cache file deleted for account {accountId}");
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
_logger.LogWarning("Cache directory not found to clear.");
|
logger.LogWarning($"Cache file not found for account {accountId}");
|
||||||
}
|
}
|
||||||
return Result.Ok();
|
return Task.FromResult(Result.Ok());
|
||||||
}
|
});
|
||||||
catch (Exception ex) {
|
|
||||||
var message = "Error clearing cache directory contents.";
|
|
||||||
_logger.LogError(ex, message);
|
|
||||||
return Result.InternalServerError([message, .. ex.ExtractMessages()]);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#region Helpers
|
||||||
|
/// <summary>
|
||||||
|
/// Generates the cache file path for the given account ID.
|
||||||
|
/// </summary>
|
||||||
|
private string GetCacheFilePath(Guid accountId) {
|
||||||
|
return Path.Combine(_cacheDirectory, $"{accountId}.json");
|
||||||
|
}
|
||||||
|
|
||||||
|
private Guid[] GetCachedAccounts() {
|
||||||
|
return GetCacheFilesPaths().Select(x => Path.GetFileNameWithoutExtension(x).ToGuid()).Where(x => x != Guid.Empty).ToArray();
|
||||||
|
}
|
||||||
|
|
||||||
|
private string[] GetCacheFilesPaths() {
|
||||||
|
return Directory.GetFiles(_cacheDirectory);
|
||||||
|
}
|
||||||
|
|
||||||
|
private string GetTempZipPath(string prefix) {
|
||||||
|
var zipName = $"{prefix}_{DateTime.UtcNow:yyyyMMddHHmmss}.zip";
|
||||||
|
return Path.Combine(tmpDir, zipName);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void EnsureTempDirAndDeleteFile(string filePath) {
|
||||||
|
Directory.CreateDirectory(tmpDir);
|
||||||
|
if (File.Exists(filePath))
|
||||||
|
File.Delete(filePath);
|
||||||
|
}
|
||||||
#endregion
|
#endregion
|
||||||
|
|
||||||
public async Task<Result<RegistrationCache?>> LoadAccountFromCacheAsync(Guid accountId) {
|
|
||||||
return await _lockManager.ExecuteWithLockAsync(() => LoadFromCacheInternalAsync(accountId));
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task<Result> SaveToCacheAsync(Guid accountId, RegistrationCache cache) {
|
|
||||||
return await _lockManager.ExecuteWithLockAsync(() => SaveToCacheInternalAsync(accountId, cache));
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task<Result> DeleteFromCacheAsync(Guid accountId) {
|
|
||||||
return await _lockManager.ExecuteWithLockAsync(() => DeleteFromCacheInternal(accountId));
|
|
||||||
}
|
|
||||||
|
|
||||||
public void Dispose() {
|
public void Dispose() {
|
||||||
_lockManager.Dispose();
|
_lockManager.Dispose();
|
||||||
}
|
}
|
||||||
@ -3,9 +3,10 @@ using MaksIT.Results;
|
|||||||
using MaksIT.LetsEncrypt.Entities;
|
using MaksIT.LetsEncrypt.Entities;
|
||||||
using MaksIT.LetsEncrypt.Entities.LetsEncrypt;
|
using MaksIT.LetsEncrypt.Entities.LetsEncrypt;
|
||||||
using MaksIT.LetsEncrypt.Services;
|
using MaksIT.LetsEncrypt.Services;
|
||||||
|
using MaksIT.Webapi.Abstractions.Services;
|
||||||
|
|
||||||
|
|
||||||
namespace MaksIT.LetsEncryptServer.Services;
|
namespace MaksIT.Webapi.Services;
|
||||||
|
|
||||||
public interface ICertsFlowService {
|
public interface ICertsFlowService {
|
||||||
Result<string?> GetTermsOfService(Guid sessionId);
|
Result<string?> GetTermsOfService(Guid sessionId);
|
||||||
@ -22,77 +23,62 @@ public interface ICertsFlowService {
|
|||||||
Result<string?> AcmeChallenge(string fileName);
|
Result<string?> AcmeChallenge(string fileName);
|
||||||
}
|
}
|
||||||
|
|
||||||
public class CertsFlowService : ICertsFlowService {
|
public class CertsFlowService(
|
||||||
private readonly Configuration _appSettings;
|
IOptions<Configuration> appSettings,
|
||||||
private readonly ILogger<CertsFlowService> _logger;
|
ILogger<CertsFlowService> logger,
|
||||||
private readonly HttpClient _httpClient;
|
HttpClient httpClient,
|
||||||
private readonly ILetsEncryptService _letsEncryptService;
|
ILetsEncryptService letsEncryptService,
|
||||||
private readonly ICacheService _cacheService;
|
ICacheService cacheService,
|
||||||
private readonly IAgentService _agentService;
|
IAgentService agentService
|
||||||
private readonly string _acmePath;
|
) : ServiceBase(
|
||||||
|
logger,
|
||||||
|
appSettings
|
||||||
|
), ICertsFlowService {
|
||||||
|
|
||||||
public CertsFlowService(
|
private readonly string _acmePath = appSettings.Value.AcmeFolder;
|
||||||
IOptions<Configuration> appSettings,
|
|
||||||
ILogger<CertsFlowService> logger,
|
|
||||||
HttpClient httpClient,
|
|
||||||
ILetsEncryptService letsEncryptService,
|
|
||||||
ICacheService cashService,
|
|
||||||
IAgentService agentService
|
|
||||||
) {
|
|
||||||
_appSettings = appSettings.Value;
|
|
||||||
_logger = logger;
|
|
||||||
_httpClient = httpClient;
|
|
||||||
_letsEncryptService = letsEncryptService;
|
|
||||||
_cacheService = cashService;
|
|
||||||
_agentService = agentService;
|
|
||||||
_acmePath = _appSettings.AcmeFolder;
|
|
||||||
}
|
|
||||||
|
|
||||||
public Result<string?> GetTermsOfService(Guid sessionId) {
|
public Result<string?> GetTermsOfService(Guid sessionId) {
|
||||||
var result = _letsEncryptService.GetTermsOfServiceUri(sessionId);
|
var result = letsEncryptService.GetTermsOfServiceUri(sessionId);
|
||||||
if (!result.IsSuccess || result.Value == null)
|
if (!result.IsSuccess || result.Value == null)
|
||||||
return result;
|
return result;
|
||||||
|
|
||||||
var termsOfServiceUrl = result.Value;
|
var termsOfServiceUrl = result.Value;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
var fileName = Path.GetFileName(new Uri(termsOfServiceUrl).LocalPath);
|
var fileName = Path.GetFileName(new Uri(termsOfServiceUrl).LocalPath);
|
||||||
|
var termsOfServicePdfPath = Path.Combine(_appSettings.DataFolder, fileName);
|
||||||
var termsOfServicePdfPath = Path.Combine(_appSettings.DataFolder, fileName);
|
foreach (var file in Directory.GetFiles(_appSettings.DataFolder, "*.pdf")) {
|
||||||
|
if (!string.Equals(Path.GetFileName(file), fileName, StringComparison.OrdinalIgnoreCase)) {
|
||||||
// Clean up old PDF files except the current one
|
try {
|
||||||
foreach (var file in Directory.GetFiles(_appSettings.DataFolder, "*.pdf")) {
|
File.Delete(file);
|
||||||
if (!string.Equals(Path.GetFileName(file), fileName, StringComparison.OrdinalIgnoreCase)) {
|
}
|
||||||
try {
|
catch { /* ignore */ }
|
||||||
File.Delete(file);
|
|
||||||
}
|
|
||||||
catch { /* ignore */ }
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
byte[] pdfBytes;
|
byte[] pdfBytes;
|
||||||
if (File.Exists(termsOfServicePdfPath)) {
|
if (File.Exists(termsOfServicePdfPath)) {
|
||||||
pdfBytes = File.ReadAllBytes(termsOfServicePdfPath);
|
pdfBytes = File.ReadAllBytes(termsOfServicePdfPath);
|
||||||
} else {
|
}
|
||||||
pdfBytes = _httpClient.GetByteArrayAsync(termsOfServiceUrl).GetAwaiter().GetResult();
|
else {
|
||||||
File.WriteAllBytes(termsOfServicePdfPath, pdfBytes);
|
pdfBytes = httpClient.GetByteArrayAsync(termsOfServiceUrl).GetAwaiter().GetResult();
|
||||||
}
|
File.WriteAllBytes(termsOfServicePdfPath, pdfBytes);
|
||||||
var base64 = Convert.ToBase64String(pdfBytes);
|
}
|
||||||
return Result<string?>.Ok(base64);
|
var base64 = Convert.ToBase64String(pdfBytes);
|
||||||
|
return Result<string?>.Ok(base64);
|
||||||
}
|
}
|
||||||
catch (Exception ex) {
|
catch (Exception ex) {
|
||||||
_logger.LogError(ex, "Failed to download, cache, or convert Terms of Service PDF");
|
logger.LogError(ex, "Failed to download, cache, or convert Terms of Service PDF");
|
||||||
return Result<string?>.InternalServerError(null, $"Failed to download, cache, or convert Terms of Service PDF: {ex.Message}");
|
return Result<string?>.InternalServerError(null, $"Failed to download, cache, or convert Terms of Service PDF: {ex.Message}");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<Result> CompleteChallengesAsync(Guid sessionId) {
|
public async Task<Result> CompleteChallengesAsync(Guid sessionId) {
|
||||||
return await _letsEncryptService.CompleteChallenges(sessionId);
|
return await letsEncryptService.CompleteChallenges(sessionId);
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<Result<Guid?>> ConfigureClientAsync(bool isStaging) {
|
public async Task<Result<Guid?>> ConfigureClientAsync(bool isStaging) {
|
||||||
var sessionId = Guid.NewGuid();
|
var sessionId = Guid.NewGuid();
|
||||||
var result = await _letsEncryptService.ConfigureClient(sessionId, isStaging);
|
var result = await letsEncryptService.ConfigureClient(sessionId, isStaging);
|
||||||
if (!result.IsSuccess)
|
if (!result.IsSuccess)
|
||||||
return result.ToResultOfType<Guid?>(default);
|
return result.ToResultOfType<Guid?>(default);
|
||||||
return Result<Guid?>.Ok(sessionId);
|
return Result<Guid?>.Ok(sessionId);
|
||||||
@ -100,13 +86,11 @@ public class CertsFlowService : ICertsFlowService {
|
|||||||
|
|
||||||
public async Task<Result<Guid?>> InitAsync(Guid sessionId, Guid? accountId, string description, string[] contacts) {
|
public async Task<Result<Guid?>> InitAsync(Guid sessionId, Guid? accountId, string description, string[] contacts) {
|
||||||
RegistrationCache? cache = null;
|
RegistrationCache? cache = null;
|
||||||
|
|
||||||
if (accountId == null) {
|
if (accountId == null) {
|
||||||
accountId = Guid.NewGuid();
|
accountId = Guid.NewGuid();
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
var cacheResult = await _cacheService.LoadAccountFromCacheAsync(accountId.Value);
|
var cacheResult = await cacheService.LoadAccountFromCacheAsync(accountId.Value);
|
||||||
|
|
||||||
if (!cacheResult.IsSuccess || cacheResult.Value == null) {
|
if (!cacheResult.IsSuccess || cacheResult.Value == null) {
|
||||||
accountId = Guid.NewGuid();
|
accountId = Guid.NewGuid();
|
||||||
}
|
}
|
||||||
@ -114,94 +98,76 @@ public class CertsFlowService : ICertsFlowService {
|
|||||||
cache = cacheResult.Value;
|
cache = cacheResult.Value;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
var result = await letsEncryptService.Init(sessionId, accountId.Value, description, contacts, cache);
|
||||||
var result = await _letsEncryptService.Init(sessionId, accountId.Value, description, contacts, cache);
|
|
||||||
if (!result.IsSuccess)
|
if (!result.IsSuccess)
|
||||||
return result.ToResultOfType<Guid?>(default);
|
return result.ToResultOfType<Guid?>(default);
|
||||||
|
|
||||||
return Result<Guid?>.Ok(accountId.Value);
|
return Result<Guid?>.Ok(accountId.Value);
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<Result<List<string>?>> NewOrderAsync(Guid sessionId, string[] hostnames, string challengeType) {
|
public async Task<Result<List<string>?>> NewOrderAsync(Guid sessionId, string[] hostnames, string challengeType) {
|
||||||
var orderResult = await _letsEncryptService.NewOrder(sessionId, hostnames, challengeType);
|
var orderResult = await letsEncryptService.NewOrder(sessionId, hostnames, challengeType);
|
||||||
if (!orderResult.IsSuccess || orderResult.Value == null)
|
if (!orderResult.IsSuccess || orderResult.Value == null)
|
||||||
return orderResult.ToResultOfType<List<string>?>(_ => null);
|
return orderResult.ToResultOfType<List<string>?>(_ => null);
|
||||||
|
|
||||||
var challenges = new List<string>();
|
var challenges = new List<string>();
|
||||||
|
|
||||||
foreach (var kvp in orderResult.Value) {
|
foreach (var kvp in orderResult.Value) {
|
||||||
string[] splitToken = kvp.Value.Split('.');
|
string[] splitToken = kvp.Value.Split('.');
|
||||||
File.WriteAllText(Path.Combine(_acmePath, splitToken[0]), kvp.Value);
|
File.WriteAllText(Path.Combine(_acmePath, splitToken[0]), kvp.Value);
|
||||||
challenges.Add(splitToken[0]);
|
challenges.Add(splitToken[0]);
|
||||||
}
|
}
|
||||||
|
|
||||||
return Result<List<string>?>.Ok(challenges);
|
return Result<List<string>?>.Ok(challenges);
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<Result> GetCertificatesAsync(Guid sessionId, string[] hostnames) {
|
public async Task<Result> GetCertificatesAsync(Guid sessionId, string[] hostnames) {
|
||||||
foreach (var subject in hostnames) {
|
foreach (var subject in hostnames) {
|
||||||
var result = await _letsEncryptService.GetCertificate(sessionId, subject);
|
var result = await letsEncryptService.GetCertificate(sessionId, subject);
|
||||||
if (!result.IsSuccess)
|
if (!result.IsSuccess)
|
||||||
return result;
|
return result;
|
||||||
Thread.Sleep(1000);
|
Thread.Sleep(1000);
|
||||||
}
|
}
|
||||||
|
var cacheResult = letsEncryptService.GetRegistrationCache(sessionId);
|
||||||
var cacheResult = _letsEncryptService.GetRegistrationCache(sessionId);
|
|
||||||
if (!cacheResult.IsSuccess || cacheResult.Value == null)
|
if (!cacheResult.IsSuccess || cacheResult.Value == null)
|
||||||
return cacheResult;
|
return cacheResult;
|
||||||
|
var saveResult = await cacheService.SaveToCacheAsync(cacheResult.Value.AccountId, cacheResult.Value);
|
||||||
var saveResult = await _cacheService.SaveToCacheAsync(cacheResult.Value.AccountId, cacheResult.Value);
|
|
||||||
if (!saveResult.IsSuccess)
|
if (!saveResult.IsSuccess)
|
||||||
return saveResult;
|
return saveResult;
|
||||||
|
|
||||||
return Result.Ok();
|
return Result.Ok();
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<Result> GetOrderAsync(Guid sessionId, string[] hostnames) {
|
public async Task<Result> GetOrderAsync(Guid sessionId, string[] hostnames) {
|
||||||
return await _letsEncryptService.GetOrder(sessionId, hostnames);
|
return await letsEncryptService.GetOrder(sessionId, hostnames);
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<Result<Dictionary<string, string>?>> ApplyCertificatesAsync(Guid accountId) {
|
public async Task<Result<Dictionary<string, string>?>> ApplyCertificatesAsync(Guid accountId) {
|
||||||
var cacheResult = await _cacheService.LoadAccountFromCacheAsync(accountId);
|
var cacheResult = await cacheService.LoadAccountFromCacheAsync(accountId);
|
||||||
if (!cacheResult.IsSuccess || cacheResult.Value?.CachedCerts == null)
|
if (!cacheResult.IsSuccess || cacheResult.Value?.CachedCerts == null)
|
||||||
return cacheResult.ToResultOfType<Dictionary<string, string>?>(_ => null);
|
return cacheResult.ToResultOfType<Dictionary<string, string>?>(_ => null);
|
||||||
|
|
||||||
var cache = cacheResult.Value;
|
var cache = cacheResult.Value;
|
||||||
var results = cache.GetCertsPemPerHostname();
|
var results = cache.GetCertsPemPerHostname();
|
||||||
|
|
||||||
|
|
||||||
if (cache.IsDisabled)
|
if (cache.IsDisabled)
|
||||||
return Result<Dictionary<string, string>?>.BadRequest(null, $"Account {accountId} is disabled");
|
return Result<Dictionary<string, string>?>.BadRequest(null, $"Account {accountId} is disabled");
|
||||||
|
|
||||||
if (cache.IsStaging)
|
if (cache.IsStaging)
|
||||||
return Result<Dictionary<string, string>?>.UnprocessableEntity(null, $"Found certs for {string.Join(',', results.Keys)} (staging environment)");
|
return Result<Dictionary<string, string>?>.UnprocessableEntity(null, $"Found certs for {string.Join(',', results.Keys)} (staging environment)");
|
||||||
|
var uploadResult = await agentService.UploadCerts(results);
|
||||||
var uploadResult = await _agentService.UploadCerts(results);
|
|
||||||
if (!uploadResult.IsSuccess)
|
if (!uploadResult.IsSuccess)
|
||||||
return uploadResult.ToResultOfType<Dictionary<string, string>?>(default);
|
return uploadResult.ToResultOfType<Dictionary<string, string>?>(default);
|
||||||
|
var reloadResult = await agentService.ReloadService(_appSettings.Agent.ServiceToReload);
|
||||||
var reloadResult = await _agentService.ReloadService(_appSettings.Agent.ServiceToReload);
|
|
||||||
if (!reloadResult.IsSuccess)
|
if (!reloadResult.IsSuccess)
|
||||||
return reloadResult.ToResultOfType<Dictionary<string, string>?>(default);
|
return reloadResult.ToResultOfType<Dictionary<string, string>?>(default);
|
||||||
|
|
||||||
return Result<Dictionary<string, string>?>.Ok(results);
|
return Result<Dictionary<string, string>?>.Ok(results);
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<Result> RevokeCertificatesAsync(Guid sessionId, string[] hostnames) {
|
public async Task<Result> RevokeCertificatesAsync(Guid sessionId, string[] hostnames) {
|
||||||
foreach (var hostname in hostnames) {
|
foreach (var hostname in hostnames) {
|
||||||
var result = await _letsEncryptService.RevokeCertificate(sessionId, hostname, RevokeReason.Unspecified);
|
var result = await letsEncryptService.RevokeCertificate(sessionId, hostname, RevokeReason.Unspecified);
|
||||||
if (!result.IsSuccess)
|
if (!result.IsSuccess)
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
var cacheResult = letsEncryptService.GetRegistrationCache(sessionId);
|
||||||
var cacheResult = _letsEncryptService.GetRegistrationCache(sessionId);
|
|
||||||
if (!cacheResult.IsSuccess || cacheResult.Value == null)
|
if (!cacheResult.IsSuccess || cacheResult.Value == null)
|
||||||
return cacheResult;
|
return cacheResult;
|
||||||
|
var saveResult = await cacheService.SaveToCacheAsync(cacheResult.Value.AccountId, cacheResult.Value);
|
||||||
var saveResult = await _cacheService.SaveToCacheAsync(cacheResult.Value.AccountId, cacheResult.Value);
|
|
||||||
if (!saveResult.IsSuccess)
|
if (!saveResult.IsSuccess)
|
||||||
return saveResult;
|
return saveResult;
|
||||||
|
|
||||||
return Result.Ok();
|
return Result.Ok();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -209,40 +175,31 @@ public class CertsFlowService : ICertsFlowService {
|
|||||||
var sessionResult = await ConfigureClientAsync(isStaging);
|
var sessionResult = await ConfigureClientAsync(isStaging);
|
||||||
if (!sessionResult.IsSuccess || sessionResult.Value == null)
|
if (!sessionResult.IsSuccess || sessionResult.Value == null)
|
||||||
return sessionResult;
|
return sessionResult;
|
||||||
|
|
||||||
var sessionId = sessionResult.Value.Value;
|
var sessionId = sessionResult.Value.Value;
|
||||||
|
|
||||||
var initResult = await InitAsync(sessionId, accountId, description, contacts);
|
var initResult = await InitAsync(sessionId, accountId, description, contacts);
|
||||||
if (!initResult.IsSuccess || initResult.Value == null)
|
if (!initResult.IsSuccess || initResult.Value == null)
|
||||||
return initResult.ToResultOfType<Guid?>(_ => null);
|
return initResult.ToResultOfType<Guid?>(_ => null);
|
||||||
|
|
||||||
if (accountId == null)
|
if (accountId == null)
|
||||||
accountId = initResult.Value;
|
accountId = initResult.Value;
|
||||||
|
|
||||||
var challengesResult = await NewOrderAsync(sessionId, hostnames, challengeType);
|
var challengesResult = await NewOrderAsync(sessionId, hostnames, challengeType);
|
||||||
if (!challengesResult.IsSuccess)
|
if (!challengesResult.IsSuccess)
|
||||||
return challengesResult.ToResultOfType<Guid?>(_ => null);
|
return challengesResult.ToResultOfType<Guid?>(_ => null);
|
||||||
|
|
||||||
if (challengesResult.Value?.Count > 0) {
|
if (challengesResult.Value?.Count > 0) {
|
||||||
var challengeResult = await CompleteChallengesAsync(sessionId);
|
var challengeResult = await CompleteChallengesAsync(sessionId);
|
||||||
if (!challengeResult.IsSuccess)
|
if (!challengeResult.IsSuccess)
|
||||||
return challengeResult.ToResultOfType<Guid?>(default);
|
return challengeResult.ToResultOfType<Guid?>(default);
|
||||||
}
|
}
|
||||||
|
|
||||||
var getOrderResult = await GetOrderAsync(sessionId, hostnames);
|
var getOrderResult = await GetOrderAsync(sessionId, hostnames);
|
||||||
if (!getOrderResult.IsSuccess)
|
if (!getOrderResult.IsSuccess)
|
||||||
return getOrderResult.ToResultOfType<Guid?>(default);
|
return getOrderResult.ToResultOfType<Guid?>(default);
|
||||||
|
|
||||||
var certsResult = await GetCertificatesAsync(sessionId, hostnames);
|
var certsResult = await GetCertificatesAsync(sessionId, hostnames);
|
||||||
if (!certsResult.IsSuccess)
|
if (!certsResult.IsSuccess)
|
||||||
return certsResult.ToResultOfType<Guid?>(default);
|
return certsResult.ToResultOfType<Guid?>(default);
|
||||||
|
|
||||||
if (!isStaging) {
|
if (!isStaging) {
|
||||||
var applyCertsResult = await ApplyCertificatesAsync(accountId.Value);
|
var applyCertsResult = await ApplyCertificatesAsync(accountId.Value);
|
||||||
if (!applyCertsResult.IsSuccess)
|
if (!applyCertsResult.IsSuccess)
|
||||||
return applyCertsResult.ToResultOfType<Guid?>(_ => null);
|
return applyCertsResult.ToResultOfType<Guid?>(_ => null);
|
||||||
}
|
}
|
||||||
|
|
||||||
return Result<Guid?>.Ok(initResult.Value);
|
return Result<Guid?>.Ok(initResult.Value);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -250,27 +207,21 @@ public class CertsFlowService : ICertsFlowService {
|
|||||||
var sessionResult = await ConfigureClientAsync(isStaging);
|
var sessionResult = await ConfigureClientAsync(isStaging);
|
||||||
if (!sessionResult.IsSuccess || sessionResult.Value == null)
|
if (!sessionResult.IsSuccess || sessionResult.Value == null)
|
||||||
return sessionResult;
|
return sessionResult;
|
||||||
|
|
||||||
var sessionId = sessionResult.Value.Value;
|
var sessionId = sessionResult.Value.Value;
|
||||||
|
|
||||||
var initResult = await InitAsync(sessionId, accountId, description, contacts);
|
var initResult = await InitAsync(sessionId, accountId, description, contacts);
|
||||||
if (!initResult.IsSuccess)
|
if (!initResult.IsSuccess)
|
||||||
return initResult;
|
return initResult;
|
||||||
|
|
||||||
var revokeResult = await RevokeCertificatesAsync(sessionId, hostnames);
|
var revokeResult = await RevokeCertificatesAsync(sessionId, hostnames);
|
||||||
if (!revokeResult.IsSuccess)
|
if (!revokeResult.IsSuccess)
|
||||||
return revokeResult;
|
return revokeResult;
|
||||||
|
|
||||||
return Result.Ok();
|
return Result.Ok();
|
||||||
}
|
}
|
||||||
|
|
||||||
public Result<string?> AcmeChallenge(string fileName) {
|
public Result<string?> AcmeChallenge(string fileName) {
|
||||||
DeleteExporedChallenges();
|
DeleteExporedChallenges();
|
||||||
|
|
||||||
var challengePath = Path.Combine(_acmePath, fileName);
|
var challengePath = Path.Combine(_acmePath, fileName);
|
||||||
if (!File.Exists(challengePath))
|
if (!File.Exists(challengePath))
|
||||||
return Result<string?>.NotFound(null);
|
return Result<string?>.NotFound(null);
|
||||||
|
|
||||||
var fileContent = File.ReadAllText(Path.Combine(_acmePath, fileName));
|
var fileContent = File.ReadAllText(Path.Combine(_acmePath, fileName));
|
||||||
return Result<string?>.Ok(fileContent);
|
return Result<string?>.Ok(fileContent);
|
||||||
}
|
}
|
||||||
@ -281,15 +232,13 @@ public class CertsFlowService : ICertsFlowService {
|
|||||||
try {
|
try {
|
||||||
var creationTime = File.GetCreationTime(file);
|
var creationTime = File.GetCreationTime(file);
|
||||||
var timeDifference = currentDate - creationTime;
|
var timeDifference = currentDate - creationTime;
|
||||||
|
|
||||||
|
|
||||||
if (timeDifference.TotalDays > 1) {
|
if (timeDifference.TotalDays > 1) {
|
||||||
File.Delete(file);
|
File.Delete(file);
|
||||||
_logger.LogInformation($"Deleted file: {file}");
|
logger.LogInformation($"Deleted file: {file}");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
catch (Exception ex) {
|
catch (Exception ex) {
|
||||||
_logger.LogWarning(ex, "File cannot be deleted");
|
logger.LogWarning(ex, "File cannot be deleted");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -1,16 +1,24 @@
|
|||||||
using MaksIT.Core.Security.JWT;
|
|
||||||
using MaksIT.LetsEncryptServer.Domain;
|
|
||||||
using MaksIT.Results;
|
|
||||||
using Microsoft.Extensions.Options;
|
using Microsoft.Extensions.Options;
|
||||||
using Models.LetsEncryptServer.Identity.Login;
|
using MaksIT.Results;
|
||||||
using Models.LetsEncryptServer.Identity.Logout;
|
using MaksIT.Webapi.Domain;
|
||||||
using System.Linq.Dynamic.Core.Tokenizer;
|
using MaksIT.Webapi.Authorization;
|
||||||
using System.Security.Claims;
|
using MaksIT.Webapi.Abstractions.Services;
|
||||||
|
using MaksIT.Core.Security.JWT;
|
||||||
|
using MaksIT.Core.Webapi.Models;
|
||||||
|
using MaksIT.Models.LetsEncryptServer.Identity.Login;
|
||||||
|
using MaksIT.Models.LetsEncryptServer.Identity.Logout;
|
||||||
|
using MaksIT.Models.LetsEncryptServer.Identity.User;
|
||||||
|
|
||||||
namespace MaksIT.LetsEncryptServer.Services;
|
|
||||||
|
|
||||||
|
namespace MaksIT.Webapi.Services;
|
||||||
|
|
||||||
public interface IIdentityService {
|
public interface IIdentityService {
|
||||||
|
|
||||||
|
#region Patch
|
||||||
|
Task<Result<UserResponse?>> PatchUserAsync(JwtTokenData jwtTokenData, Guid id, PatchUserRequest requestData);
|
||||||
|
#endregion
|
||||||
|
|
||||||
#region Login/Refresh/Logout
|
#region Login/Refresh/Logout
|
||||||
Task<Result<LoginResponse?>> LoginAsync(LoginRequest requestData);
|
Task<Result<LoginResponse?>> LoginAsync(LoginRequest requestData);
|
||||||
Task<Result<LoginResponse?>> RefreshTokenAsync(RefreshTokenRequest requestData);
|
Task<Result<LoginResponse?>> RefreshTokenAsync(RefreshTokenRequest requestData);
|
||||||
@ -19,12 +27,53 @@ public interface IIdentityService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public class IdentityService(
|
public class IdentityService(
|
||||||
IOptions<Configuration> appsettings,
|
ILogger<IdentityService> logger,
|
||||||
|
IOptions<Configuration> appSettings,
|
||||||
ISettingsService settingsService
|
ISettingsService settingsService
|
||||||
) : IIdentityService {
|
) : ServiceBase(logger, appSettings), IIdentityService {
|
||||||
|
|
||||||
|
#region Patch
|
||||||
|
public async Task<Result<UserResponse?>> PatchUserAsync(JwtTokenData jwtTokenData, Guid id, PatchUserRequest requestData) {
|
||||||
|
var loadSettingsResult = await settingsService.LoadAsync();
|
||||||
|
if (!loadSettingsResult.IsSuccess || loadSettingsResult.Value == null) {
|
||||||
|
return loadSettingsResult.ToResultOfType<UserResponse?>(_ => null);
|
||||||
|
}
|
||||||
|
|
||||||
|
var settings = loadSettingsResult.Value;
|
||||||
|
|
||||||
|
var userResult = settings.GetUserById(id);
|
||||||
|
if (!userResult.IsSuccess || userResult.Value == null)
|
||||||
|
return userResult.ToResultOfType<UserResponse?>(_ => null);
|
||||||
|
|
||||||
|
var user = userResult.Value;
|
||||||
|
|
||||||
|
#region Authentication properties
|
||||||
|
if (requestData.TryGetOperation(nameof(requestData.Password), out var patchOperation)) {
|
||||||
|
switch (patchOperation) {
|
||||||
|
case PatchOperation.SetField:
|
||||||
|
if (requestData.Password == null)
|
||||||
|
return PatchFieldIsNotDefined<UserResponse?>(nameof(requestData.Password));
|
||||||
|
user.SetPassword(requestData.Password, _appSettings.Auth.Pepper);
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
return UnsupportedPatchOperationResponse<UserResponse?>();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
settings.UpsertUser(user);
|
||||||
|
|
||||||
|
var saveSettingsResult = await settingsService.SaveAsync(settings);
|
||||||
|
if (!saveSettingsResult.IsSuccess)
|
||||||
|
return saveSettingsResult.ToResultOfType<UserResponse?>(default);
|
||||||
|
|
||||||
|
return Result<UserResponse?>.Ok(new UserResponse {
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
}
|
||||||
|
#endregion
|
||||||
|
|
||||||
private readonly Configuration _appSettings = appsettings.Value;
|
|
||||||
|
|
||||||
#region Login/Refresh/Logout
|
#region Login/Refresh/Logout
|
||||||
public async Task<Result<LoginResponse?>> LoginAsync(LoginRequest requestData) {
|
public async Task<Result<LoginResponse?>> LoginAsync(LoginRequest requestData) {
|
||||||
@ -90,59 +139,59 @@ public class IdentityService(
|
|||||||
public async Task<Result<LoginResponse?>> RefreshTokenAsync(RefreshTokenRequest requestData) {
|
public async Task<Result<LoginResponse?>> RefreshTokenAsync(RefreshTokenRequest requestData) {
|
||||||
var loadSettingsResult = await settingsService.LoadAsync();
|
var loadSettingsResult = await settingsService.LoadAsync();
|
||||||
if (!loadSettingsResult.IsSuccess || loadSettingsResult.Value == null)
|
if (!loadSettingsResult.IsSuccess || loadSettingsResult.Value == null)
|
||||||
return loadSettingsResult.ToResultOfType<LoginResponse?>(_ => null);
|
return loadSettingsResult.ToResultOfType<LoginResponse?>(_ => null);
|
||||||
|
|
||||||
var settings = loadSettingsResult.Value;
|
var settings = loadSettingsResult.Value;
|
||||||
var userResult = settings.GetByRefreshToken(requestData.RefreshToken);
|
var userResult = settings.GetByRefreshToken(requestData.RefreshToken);
|
||||||
if (!userResult.IsSuccess || userResult.Value == null)
|
if (!userResult.IsSuccess || userResult.Value == null)
|
||||||
return Result<LoginResponse?>.Unauthorized(null, "Invalid refresh token.");
|
return Result<LoginResponse?>.Unauthorized(null, "Invalid refresh token.");
|
||||||
|
|
||||||
var user = userResult.Value.RemoveRevokedJwtTokens();
|
var user = userResult.Value.RemoveRevokedJwtTokens();
|
||||||
var tokenDomain = user.JwtTokens.SingleOrDefault(t => t.RefreshToken == requestData.RefreshToken);
|
var tokenDomain = user.JwtTokens.SingleOrDefault(t => t.RefreshToken == requestData.RefreshToken);
|
||||||
|
|
||||||
if (tokenDomain == null)
|
if (tokenDomain == null)
|
||||||
return Result<LoginResponse?>.Unauthorized(null, "Invalid refresh token.");
|
return Result<LoginResponse?>.Unauthorized(null, "Invalid refresh token.");
|
||||||
|
|
||||||
// Token is still valid
|
// Token is still valid
|
||||||
if (DateTime.UtcNow <= tokenDomain.ExpiresAt) {
|
if (DateTime.UtcNow <= tokenDomain.ExpiresAt) {
|
||||||
user.SetLastLogin();
|
user.SetLastLogin();
|
||||||
settings.UpsertUser(user);
|
settings.UpsertUser(user);
|
||||||
|
|
||||||
var saveResult = await settingsService.SaveAsync(settings);
|
var saveResult = await settingsService.SaveAsync(settings);
|
||||||
if (!saveResult.IsSuccess)
|
if (!saveResult.IsSuccess)
|
||||||
return saveResult.ToResultOfType<LoginResponse?>(default);
|
return saveResult.ToResultOfType<LoginResponse?>(default);
|
||||||
|
|
||||||
return Result<LoginResponse?>.Ok(new LoginResponse {
|
return Result<LoginResponse?>.Ok(new LoginResponse {
|
||||||
TokenType = tokenDomain.TokenType,
|
TokenType = tokenDomain.TokenType,
|
||||||
Token = tokenDomain.Token,
|
Token = tokenDomain.Token,
|
||||||
ExpiresAt = tokenDomain.ExpiresAt,
|
ExpiresAt = tokenDomain.ExpiresAt,
|
||||||
RefreshToken = tokenDomain.RefreshToken,
|
RefreshToken = tokenDomain.RefreshToken,
|
||||||
RefreshTokenExpiresAt = tokenDomain.RefreshTokenExpiresAt
|
RefreshTokenExpiresAt = tokenDomain.RefreshTokenExpiresAt
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Refresh token expired
|
// Refresh token expired
|
||||||
if (DateTime.UtcNow > tokenDomain.RefreshTokenExpiresAt) {
|
if (DateTime.UtcNow > tokenDomain.RefreshTokenExpiresAt) {
|
||||||
user.RemoveJwtToken(tokenDomain.Id);
|
user.RemoveJwtToken(tokenDomain.Id);
|
||||||
return Result<LoginResponse?>.Unauthorized(null, "Refresh token has expired.");
|
return Result<LoginResponse?>.Unauthorized(null, "Refresh token has expired.");
|
||||||
}
|
}
|
||||||
|
|
||||||
// Refresh token is valid - generate new tokens
|
// Refresh token is valid - generate new tokens
|
||||||
|
|
||||||
if (!JwtGenerator.TryGenerateToken(new JWTTokenGenerateRequest {
|
if (!JwtGenerator.TryGenerateToken(new JWTTokenGenerateRequest {
|
||||||
Secret = _appSettings.Auth.Secret,
|
Secret = _appSettings.Auth.Secret,
|
||||||
Issuer = _appSettings.Auth.Issuer,
|
Issuer = _appSettings.Auth.Issuer,
|
||||||
Audience = _appSettings.Auth.Audience,
|
Audience = _appSettings.Auth.Audience,
|
||||||
Expiration = _appSettings.Auth.Expiration,
|
Expiration = _appSettings.Auth.Expiration,
|
||||||
UserId = user.Id.ToString(),
|
UserId = user.Id.ToString(),
|
||||||
Username = user.Name,
|
Username = user.Name,
|
||||||
}, out (string token, JWTTokenClaims claims)? tokenData, out string? errorMessage))
|
}, out (string token, JWTTokenClaims claims)? tokenData, out string? errorMessage))
|
||||||
return Result<LoginResponse?>.InternalServerError(null, errorMessage);
|
return Result<LoginResponse?>.InternalServerError(null, errorMessage);
|
||||||
|
|
||||||
var (token, claims) = tokenData.Value;
|
var (token, claims) = tokenData.Value;
|
||||||
|
|
||||||
if (claims.IssuedAt == null || claims.ExpiresAt == null)
|
if (claims.IssuedAt == null || claims.ExpiresAt == null)
|
||||||
return Result<LoginResponse?>.InternalServerError(null, "Token claims are missing required fields.");
|
return Result<LoginResponse?>.InternalServerError(null, "Token claims are missing required fields.");
|
||||||
|
|
||||||
string refreshToken = JwtGenerator.GenerateRefreshToken();
|
string refreshToken = JwtGenerator.GenerateRefreshToken();
|
||||||
|
|
||||||
@ -156,14 +205,14 @@ public class IdentityService(
|
|||||||
|
|
||||||
var writeResult = await settingsService.SaveAsync(settings);
|
var writeResult = await settingsService.SaveAsync(settings);
|
||||||
if (!writeResult.IsSuccess)
|
if (!writeResult.IsSuccess)
|
||||||
return writeResult.ToResultOfType<LoginResponse?>(default);
|
return writeResult.ToResultOfType<LoginResponse?>(default);
|
||||||
|
|
||||||
return Result<LoginResponse?>.Ok(new LoginResponse {
|
return Result<LoginResponse?>.Ok(new LoginResponse {
|
||||||
TokenType = tokenDomain.TokenType,
|
TokenType = tokenDomain.TokenType,
|
||||||
Token = tokenDomain.Token,
|
Token = tokenDomain.Token,
|
||||||
ExpiresAt = claims.ExpiresAt.Value,
|
ExpiresAt = claims.ExpiresAt.Value,
|
||||||
RefreshToken = tokenDomain.RefreshToken,
|
RefreshToken = tokenDomain.RefreshToken,
|
||||||
RefreshTokenExpiresAt = tokenDomain.RefreshTokenExpiresAt
|
RefreshTokenExpiresAt = tokenDomain.RefreshTokenExpiresAt
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
105
src/MaksIT.Webapi/Services/SettingsService.cs
Normal file
105
src/MaksIT.Webapi/Services/SettingsService.cs
Normal file
@ -0,0 +1,105 @@
|
|||||||
|
using Microsoft.Extensions.Options;
|
||||||
|
using MaksIT.Results;
|
||||||
|
using MaksIT.Core.Extensions;
|
||||||
|
using MaksIT.Core.Threading;
|
||||||
|
using MaksIT.Webapi.Domain;
|
||||||
|
using MaksIT.Webapi.Dto;
|
||||||
|
using MaksIT.Webapi.Abstractions.Services;
|
||||||
|
|
||||||
|
|
||||||
|
namespace MaksIT.Webapi.Services;
|
||||||
|
|
||||||
|
public interface ISettingsService {
|
||||||
|
Task<Result<Settings?>> LoadAsync();
|
||||||
|
Task<Result> SaveAsync(Settings settings);
|
||||||
|
}
|
||||||
|
|
||||||
|
public class SettingsService(
|
||||||
|
ILogger<SettingsService> logger,
|
||||||
|
IOptions<Configuration> appSettings
|
||||||
|
) : ServiceBase(logger, appSettings), ISettingsService, IDisposable {
|
||||||
|
|
||||||
|
private readonly LockManager _lockManager = new LockManager();
|
||||||
|
private readonly string _settingsPath = appSettings.Value.SettingsFile;
|
||||||
|
|
||||||
|
#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)
|
||||||
|
.SetName(userDto.Name)
|
||||||
|
.SetSaltedHash(userDto.Salt, userDto.Hash)
|
||||||
|
.SetJwtTokens([.. userDto.JwtTokens.Select(jtDto =>
|
||||||
|
new JwtToken(jtDto.Id)
|
||||||
|
.SetAccessTokenData(jtDto.Token, jtDto.IssuedAt, jtDto.ExpiresAt)
|
||||||
|
.SetRefreshTokenData(jtDto.RefreshToken, jtDto.RefreshTokenExpiresAt)
|
||||||
|
)])
|
||||||
|
.SetLastLogin(userDto.LastLogin)
|
||||||
|
)]
|
||||||
|
};
|
||||||
|
return Result<Settings?>.Ok(settings);
|
||||||
|
}
|
||||||
|
catch (Exception ex) {
|
||||||
|
var message = "Error loading settings file.";
|
||||||
|
_logger.LogError(ex, message);
|
||||||
|
return Result<Settings?>.InternalServerError(null, [message, .. ex.ExtractMessages()]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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,
|
||||||
|
Name = u.Name,
|
||||||
|
Salt = u.Salt,
|
||||||
|
Hash = u.Hash,
|
||||||
|
JwtTokens = [.. u.JwtTokens.Select(jt => new JwtTokenDto {
|
||||||
|
Id = jt.Id,
|
||||||
|
Token = jt.Token,
|
||||||
|
ExpiresAt = jt.ExpiresAt,
|
||||||
|
IssuedAt = jt.IssuedAt,
|
||||||
|
RefreshToken = jt.RefreshToken,
|
||||||
|
RefreshTokenExpiresAt = jt.RefreshTokenExpiresAt,
|
||||||
|
IsRevoked = jt.IsRevoked
|
||||||
|
})],
|
||||||
|
LastLogin = u.LastLogin,
|
||||||
|
})]
|
||||||
|
};
|
||||||
|
|
||||||
|
await File.WriteAllTextAsync(_settingsPath, settingsDto.ToJson());
|
||||||
|
_logger.LogInformation("Settings file saved.");
|
||||||
|
return Result.Ok();
|
||||||
|
}
|
||||||
|
catch (Exception ex) {
|
||||||
|
var message = "Error saving settings file.";
|
||||||
|
_logger.LogError(ex, message);
|
||||||
|
return Result.InternalServerError([message, .. ex.ExtractMessages()]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#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,31 +8,29 @@
|
|||||||
"AllowedHosts": "*",
|
"AllowedHosts": "*",
|
||||||
|
|
||||||
"Configuration": {
|
"Configuration": {
|
||||||
"SettingsFile": "/data/settings.json",
|
|
||||||
"Auth": {
|
"Auth": {
|
||||||
"Secret": "",
|
"Secret": "",
|
||||||
"Issuer": "",
|
"Pepper": "",
|
||||||
"Audience": "",
|
"Issuer": "LetsEncryptServer",
|
||||||
"Expiration": 60,
|
"Audience": "LetsEncryptServerUsers",
|
||||||
"RefreshExpiration": 120,
|
"Expiration": 15, // Access token lifetime in minutes (default: 15 minutes)
|
||||||
|
"RefreshExpiration": 180 // Refresh token lifetime in days (default: 180 days)
|
||||||
|
|
||||||
"Pepper": ""
|
|
||||||
},
|
},
|
||||||
|
|
||||||
"Production": "https://acme-v02.api.letsencrypt.org/directory",
|
|
||||||
"Staging": "https://acme-staging-v02.api.letsencrypt.org/directory",
|
|
||||||
|
|
||||||
"CacheFolder": "/cache",
|
|
||||||
"AcmeFolder": "/acme",
|
|
||||||
"DataFolder": "/data",
|
|
||||||
|
|
||||||
"Agent": {
|
"Agent": {
|
||||||
"AgentHostname": "",
|
"AgentHostname": "",
|
||||||
"AgentPort": 9000,
|
"AgentPort": 9000,
|
||||||
"AgentKey": "",
|
"AgentKey": "",
|
||||||
|
|
||||||
"ServiceToReload": "haproxy"
|
"ServiceToReload": "haproxy"
|
||||||
}
|
},
|
||||||
|
|
||||||
|
"Production": "https://acme-v02.api.letsencrypt.org/directory",
|
||||||
|
"Staging": "https://acme-staging-v02.api.letsencrypt.org/directory",
|
||||||
|
"AcmeFolder": "/acme",
|
||||||
|
"CacheFolder": "/cache",
|
||||||
|
|
||||||
|
"DataFolder": "/data",
|
||||||
|
"SettingsFile": "/data/settings.json"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -1,7 +1,8 @@
|
|||||||
namespace MaksIT.Models.Agent.Requests {
|
using MaksIT.Core.Abstractions.Webapi;
|
||||||
public class CertsUploadRequest {
|
|
||||||
|
|
||||||
public Dictionary<string, string> Certs { get; set; }
|
|
||||||
|
|
||||||
}
|
namespace MaksIT.Models.Agent.Requests;
|
||||||
|
|
||||||
|
public class CertsUploadRequest : RequestModelBase {
|
||||||
|
public Dictionary<string, string> Certs { get; set; }
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,11 +1,8 @@
|
|||||||
using System;
|
using MaksIT.Core.Abstractions.Webapi;
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.Linq;
|
|
||||||
using System.Text;
|
|
||||||
using System.Threading.Tasks;
|
|
||||||
|
|
||||||
namespace MaksIT.Models.Agent.Requests {
|
|
||||||
public class ServiceReloadRequest {
|
namespace MaksIT.Models.Agent.Requests;
|
||||||
public string ServiceName { get; set; }
|
|
||||||
}
|
public class ServiceReloadRequest : RequestModelBase {
|
||||||
|
public string ServiceName { get; set; }
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,10 +1,8 @@
|
|||||||
using System;
|
using MaksIT.Core.Abstractions.Webapi;
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.Linq;
|
|
||||||
using System.Text;
|
|
||||||
using System.Threading.Tasks;
|
|
||||||
|
|
||||||
namespace Models.Agent.Responses;
|
|
||||||
public class HelloWorldResponse {
|
namespace MaksIT.Models.Agent.Responses;
|
||||||
|
|
||||||
|
public class HelloWorldResponse : ResponseModelBase {
|
||||||
public string Message { get; set; }
|
public string Message { get; set; }
|
||||||
}
|
}
|
||||||
|
|||||||
@ -4,12 +4,8 @@
|
|||||||
namespace MaksIT.Models.LetsEncryptServer.Account.Requests;
|
namespace MaksIT.Models.LetsEncryptServer.Account.Requests;
|
||||||
|
|
||||||
public class PatchAccountRequest : PatchRequestModelBase {
|
public class PatchAccountRequest : PatchRequestModelBase {
|
||||||
|
|
||||||
public string? Description { get; set; }
|
public string? Description { get; set; }
|
||||||
|
|
||||||
public bool? IsDisabled { get; set; }
|
public bool? IsDisabled { get; set; }
|
||||||
|
|
||||||
public List<string>? Contacts { get; set; }
|
public List<string>? Contacts { get; set; }
|
||||||
|
|
||||||
public List<PatchHostnameRequest>? Hostnames { get; set; }
|
public List<PatchHostnameRequest>? Hostnames { get; set; }
|
||||||
}
|
}
|
||||||
|
|||||||
@ -2,9 +2,9 @@
|
|||||||
|
|
||||||
|
|
||||||
namespace MaksIT.Models.LetsEncryptServer.Account.Requests;
|
namespace MaksIT.Models.LetsEncryptServer.Account.Requests;
|
||||||
|
|
||||||
public class PatchHostnameRequest : PatchRequestModelBase {
|
public class PatchHostnameRequest : PatchRequestModelBase {
|
||||||
public string? Hostname { get; set; }
|
public string? Hostname { get; set; }
|
||||||
|
|
||||||
public bool? IsDisabled { get; set; }
|
public bool? IsDisabled { get; set; }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -1,7 +1,8 @@
|
|||||||
using MaksIT.Core.Abstractions.Webapi;
|
using System.ComponentModel.DataAnnotations;
|
||||||
using System.ComponentModel.DataAnnotations;
|
using MaksIT.Core.Abstractions.Webapi;
|
||||||
|
|
||||||
namespace MaksIT.Models.LetsEncryptServer.Account.Requests;
|
namespace MaksIT.Models.LetsEncryptServer.Account.Requests;
|
||||||
|
|
||||||
public class PostAccountRequest : RequestModelBase {
|
public class PostAccountRequest : RequestModelBase {
|
||||||
public required string Description { get; set; }
|
public required string Description { get; set; }
|
||||||
public required string[] Contacts { get; set; }
|
public required string[] Contacts { get; set; }
|
||||||
@ -11,15 +12,15 @@ public class PostAccountRequest : RequestModelBase {
|
|||||||
|
|
||||||
public override IEnumerable<ValidationResult> Validate(ValidationContext validationContext) {
|
public override IEnumerable<ValidationResult> Validate(ValidationContext validationContext) {
|
||||||
if (string.IsNullOrWhiteSpace(Description))
|
if (string.IsNullOrWhiteSpace(Description))
|
||||||
yield return new ValidationResult("Description is required", new[] { nameof(Description) });
|
yield return new ValidationResult("Description is required", [nameof(Description)]);
|
||||||
|
|
||||||
if (Contacts == null || Contacts.Length == 0)
|
if (Contacts == null || Contacts.Length == 0)
|
||||||
yield return new ValidationResult("Contacts is required", new[] { nameof(Contacts) });
|
yield return new ValidationResult("Contacts is required", [nameof(Contacts)]);
|
||||||
|
|
||||||
if (Hostnames == null || Hostnames.Length == 0)
|
if (Hostnames == null || Hostnames.Length == 0)
|
||||||
yield return new ValidationResult("Hostnames is required", new[] { nameof(Hostnames) });
|
yield return new ValidationResult("Hostnames is required", [nameof(Hostnames)]);
|
||||||
|
|
||||||
if (string.IsNullOrWhiteSpace(ChallengeType) && ChallengeType != "http-01")
|
if (string.IsNullOrWhiteSpace(ChallengeType) && ChallengeType != "http-01")
|
||||||
yield return new ValidationResult("ChallengeType is required", new[] { nameof(ChallengeType) });
|
yield return new ValidationResult("ChallengeType is required", [nameof(ChallengeType)]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,23 +1,14 @@
|
|||||||
using MaksIT.Core.Abstractions.Webapi;
|
using MaksIT.Core.Abstractions.Webapi;
|
||||||
using System;
|
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.Linq;
|
|
||||||
using System.Text;
|
|
||||||
using System.Threading.Tasks;
|
|
||||||
|
|
||||||
namespace MaksIT.Models.LetsEncryptServer.Account.Responses {
|
|
||||||
public class GetAccountResponse : ResponseModelBase {
|
|
||||||
public Guid AccountId { get; set; }
|
|
||||||
public required bool IsDisabled { get; set; }
|
|
||||||
|
|
||||||
public string? Description { get; set; }
|
namespace MaksIT.Models.LetsEncryptServer.Account.Responses;
|
||||||
|
|
||||||
public required string[] Contacts { get; set; }
|
public class GetAccountResponse : ResponseModelBase {
|
||||||
|
public Guid AccountId { get; set; }
|
||||||
public string? ChallengeType { get; set; }
|
public required bool IsDisabled { get; set; }
|
||||||
|
public string? Description { get; set; }
|
||||||
public GetHostnameResponse[]? Hostnames { get; set; }
|
public required string[] Contacts { get; set; }
|
||||||
|
public string? ChallengeType { get; set; }
|
||||||
public required bool IsStaging { get; set; }
|
public GetHostnameResponse[]? Hostnames { get; set; }
|
||||||
}
|
public required bool IsStaging { get; set; }
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,16 +1,11 @@
|
|||||||
using MaksIT.Core.Abstractions.Webapi;
|
using MaksIT.Core.Abstractions.Webapi;
|
||||||
using System;
|
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.Linq;
|
|
||||||
using System.Text;
|
|
||||||
using System.Threading.Tasks;
|
|
||||||
|
|
||||||
namespace MaksIT.Models.LetsEncryptServer.Account.Responses {
|
|
||||||
public class GetHostnameResponse : ResponseModelBase {
|
|
||||||
public required string Hostname { get; set; }
|
|
||||||
public DateTime Expires { get; set; }
|
|
||||||
public bool IsUpcomingExpire { get; set; }
|
|
||||||
public bool IsDisabled { get; set; }
|
|
||||||
}
|
|
||||||
|
|
||||||
|
namespace MaksIT.Models.LetsEncryptServer.Account.Responses;
|
||||||
|
|
||||||
|
public class GetHostnameResponse : ResponseModelBase {
|
||||||
|
public required string Hostname { get; set; }
|
||||||
|
public DateTime Expires { get; set; }
|
||||||
|
public bool IsUpcomingExpire { get; set; }
|
||||||
|
public bool IsDisabled { get; set; }
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,12 +1,8 @@
|
|||||||
using MaksIT.Core.Abstractions.Webapi;
|
using MaksIT.Core.Abstractions.Webapi;
|
||||||
using System;
|
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.Linq;
|
|
||||||
using System.Text;
|
|
||||||
using System.Threading.Tasks;
|
|
||||||
|
|
||||||
namespace MaksIT.Models.LetsEncryptServer.CertsFlow.Requests {
|
|
||||||
public class ConfigureClientRequest : RequestModelBase {
|
namespace MaksIT.Models.LetsEncryptServer.CertsFlow.Requests;
|
||||||
public bool IsStaging { get; set; }
|
|
||||||
}
|
public class ConfigureClientRequest : RequestModelBase {
|
||||||
|
public bool IsStaging { get; set; }
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,14 +1,14 @@
|
|||||||
using MaksIT.Core.Abstractions.Webapi;
|
using System.ComponentModel.DataAnnotations;
|
||||||
using System.ComponentModel.DataAnnotations;
|
using MaksIT.Core.Abstractions.Webapi;
|
||||||
|
|
||||||
namespace MaksIT.Models.LetsEncryptServer.CertsFlow.Requests
|
|
||||||
{
|
|
||||||
public class GetCertificatesRequest : RequestModelBase {
|
|
||||||
public required string[] Hostnames { get; set; }
|
|
||||||
|
|
||||||
public override IEnumerable<ValidationResult> Validate(ValidationContext validationContext) {
|
namespace MaksIT.Models.LetsEncryptServer.CertsFlow.Requests;
|
||||||
if (Hostnames == null || Hostnames.Length == 0)
|
|
||||||
yield return new ValidationResult("Hostnames is required", new[] { nameof(Hostnames) });
|
public class GetCertificatesRequest : RequestModelBase {
|
||||||
}
|
public required string[] Hostnames { get; set; }
|
||||||
|
|
||||||
|
public override IEnumerable<ValidationResult> Validate(ValidationContext validationContext) {
|
||||||
|
if (Hostnames == null || Hostnames.Length == 0)
|
||||||
|
yield return new ValidationResult("Hostnames is required", new[] { nameof(Hostnames) });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,14 +1,14 @@
|
|||||||
using MaksIT.Core.Abstractions.Webapi;
|
using System.ComponentModel.DataAnnotations;
|
||||||
using System.ComponentModel.DataAnnotations;
|
using MaksIT.Core.Abstractions.Webapi;
|
||||||
|
|
||||||
namespace MaksIT.Models.LetsEncryptServer.CertsFlow.Requests
|
|
||||||
{
|
|
||||||
public class GetOrderRequest : RequestModelBase {
|
|
||||||
public required string[] Hostnames { get; set; }
|
|
||||||
|
|
||||||
public override IEnumerable<ValidationResult> Validate(ValidationContext validationContext) {
|
namespace MaksIT.Models.LetsEncryptServer.CertsFlow.Requests;
|
||||||
if (Hostnames == null || Hostnames.Length == 0)
|
|
||||||
yield return new ValidationResult("Hostnames is required", new[] { nameof(Hostnames) });
|
public class GetOrderRequest : RequestModelBase {
|
||||||
}
|
public required string[] Hostnames { get; set; }
|
||||||
|
|
||||||
|
public override IEnumerable<ValidationResult> Validate(ValidationContext validationContext) {
|
||||||
|
if (Hostnames == null || Hostnames.Length == 0)
|
||||||
|
yield return new ValidationResult("Hostnames is required", new[] { nameof(Hostnames) });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,22 +1,18 @@
|
|||||||
using MaksIT.Core.Abstractions.Webapi;
|
using System.ComponentModel.DataAnnotations;
|
||||||
using System;
|
using MaksIT.Core.Abstractions.Webapi;
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.ComponentModel.DataAnnotations;
|
|
||||||
using System.Linq;
|
|
||||||
using System.Text;
|
|
||||||
using System.Threading.Tasks;
|
|
||||||
|
|
||||||
namespace MaksIT.Models.LetsEncryptServer.CertsFlow.Requests {
|
|
||||||
public class InitRequest : RequestModelBase {
|
|
||||||
public required string Description { get; set; }
|
|
||||||
public required string[] Contacts { get; set; }
|
|
||||||
|
|
||||||
public override IEnumerable<ValidationResult> Validate(ValidationContext validationContext) {
|
namespace MaksIT.Models.LetsEncryptServer.CertsFlow.Requests;
|
||||||
if (string.IsNullOrWhiteSpace(Description))
|
|
||||||
yield return new ValidationResult("Description is required", new[] { nameof(Description) });
|
|
||||||
|
|
||||||
if (Contacts == null || Contacts.Length == 0)
|
public class InitRequest : RequestModelBase {
|
||||||
yield return new ValidationResult("Contacts is required", new[] { nameof(Contacts) });
|
public required string Description { get; set; }
|
||||||
}
|
public required string[] Contacts { get; set; }
|
||||||
|
|
||||||
|
public override IEnumerable<ValidationResult> Validate(ValidationContext validationContext) {
|
||||||
|
if (string.IsNullOrWhiteSpace(Description))
|
||||||
|
yield return new ValidationResult("Description is required", new[] { nameof(Description) });
|
||||||
|
|
||||||
|
if (Contacts == null || Contacts.Length == 0)
|
||||||
|
yield return new ValidationResult("Contacts is required", new[] { nameof(Contacts) });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,19 +1,18 @@
|
|||||||
using MaksIT.Core.Abstractions.Webapi;
|
using System.ComponentModel.DataAnnotations;
|
||||||
using System.ComponentModel.DataAnnotations;
|
using MaksIT.Core.Abstractions.Webapi;
|
||||||
|
|
||||||
namespace MaksIT.Models.LetsEncryptServer.CertsFlow.Requests
|
|
||||||
{
|
|
||||||
public class NewOrderRequest : RequestModelBase {
|
|
||||||
public required string[] Hostnames { get; set; }
|
|
||||||
|
|
||||||
public required string ChallengeType { get; set; }
|
namespace MaksIT.Models.LetsEncryptServer.CertsFlow.Requests;
|
||||||
|
|
||||||
public override IEnumerable<ValidationResult> Validate(ValidationContext validationContext) {
|
public class NewOrderRequest : RequestModelBase {
|
||||||
if (Hostnames == null || Hostnames.Length == 0)
|
public required string[] Hostnames { get; set; }
|
||||||
yield return new ValidationResult("Hostnames is required", new[] { nameof(Hostnames) });
|
public required string ChallengeType { get; set; }
|
||||||
|
|
||||||
if (string.IsNullOrWhiteSpace(ChallengeType) && ChallengeType != "http-01")
|
public override IEnumerable<ValidationResult> Validate(ValidationContext validationContext) {
|
||||||
yield return new ValidationResult("ChallengeType is required", new[] { nameof(ChallengeType) });
|
if (Hostnames == null || Hostnames.Length == 0)
|
||||||
}
|
yield return new ValidationResult("Hostnames is required", new[] { nameof(Hostnames) });
|
||||||
|
|
||||||
|
if (string.IsNullOrWhiteSpace(ChallengeType) && ChallengeType != "http-01")
|
||||||
|
yield return new ValidationResult("ChallengeType is required", new[] { nameof(ChallengeType) });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,19 +1,14 @@
|
|||||||
using MaksIT.Core.Abstractions.Webapi;
|
using System.ComponentModel.DataAnnotations;
|
||||||
using System;
|
using MaksIT.Core.Abstractions.Webapi;
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.ComponentModel.DataAnnotations;
|
|
||||||
using System.Linq;
|
|
||||||
using System.Text;
|
|
||||||
using System.Threading.Tasks;
|
|
||||||
|
|
||||||
namespace MaksIT.Models.LetsEncryptServer.CertsFlow.Requests {
|
|
||||||
public class RevokeCertificatesRequest : RequestModelBase {
|
|
||||||
|
|
||||||
public required string [] Hostnames { get; set; }
|
namespace MaksIT.Models.LetsEncryptServer.CertsFlow.Requests;
|
||||||
|
|
||||||
public override IEnumerable<ValidationResult> Validate(ValidationContext validationContext) {
|
public class RevokeCertificatesRequest : RequestModelBase {
|
||||||
if (Hostnames == null || Hostnames.Length == 0)
|
public required string [] Hostnames { get; set; }
|
||||||
yield return new ValidationResult("Hostnames is required", new[] { nameof(Hostnames) });
|
|
||||||
}
|
public override IEnumerable<ValidationResult> Validate(ValidationContext validationContext) {
|
||||||
|
if (Hostnames == null || Hostnames.Length == 0)
|
||||||
|
yield return new ValidationResult("Hostnames is required", new[] { nameof(Hostnames) });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
using MaksIT.Core.Abstractions.Webapi;
|
using MaksIT.Core.Abstractions.Webapi;
|
||||||
|
|
||||||
|
|
||||||
namespace Models.LetsEncryptServer.Identity.Login;
|
namespace MaksIT.Models.LetsEncryptServer.Identity.Login;
|
||||||
|
|
||||||
public class LoginRequest : RequestModelBase {
|
public class LoginRequest : RequestModelBase {
|
||||||
public required string Username { get; set; }
|
public required string Username { get; set; }
|
||||||
|
|||||||
@ -1,10 +1,9 @@
|
|||||||
using MaksIT.Core.Abstractions.Webapi;
|
using MaksIT.Core.Abstractions.Webapi;
|
||||||
|
|
||||||
|
|
||||||
namespace Models.LetsEncryptServer.Identity.Login;
|
namespace MaksIT.Models.LetsEncryptServer.Identity.Login;
|
||||||
|
|
||||||
public class LoginResponse : ResponseModelBase {
|
public class LoginResponse : ResponseModelBase {
|
||||||
|
|
||||||
public required string TokenType { get; set; }
|
public required string TokenType { get; set; }
|
||||||
public required string Token { get; set; }
|
public required string Token { get; set; }
|
||||||
public required DateTime ExpiresAt { get; set; }
|
public required DateTime ExpiresAt { get; set; }
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
using MaksIT.Core.Abstractions.Webapi;
|
using MaksIT.Core.Abstractions.Webapi;
|
||||||
|
|
||||||
|
|
||||||
namespace Models.LetsEncryptServer.Identity.Login;
|
namespace MaksIT.Models.LetsEncryptServer.Identity.Login;
|
||||||
|
|
||||||
public class RefreshTokenRequest : RequestModelBase {
|
public class RefreshTokenRequest : RequestModelBase {
|
||||||
public required string RefreshToken { get; set; } // The refresh token used for renewing access
|
public required string RefreshToken { get; set; } // The refresh token used for renewing access
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
using MaksIT.Core.Abstractions.Webapi;
|
using MaksIT.Core.Abstractions.Webapi;
|
||||||
|
|
||||||
|
|
||||||
namespace Models.LetsEncryptServer.Identity.Logout;
|
namespace MaksIT.Models.LetsEncryptServer.Identity.Logout;
|
||||||
|
|
||||||
public class LogoutRequest : RequestModelBase {
|
public class LogoutRequest : RequestModelBase {
|
||||||
public required string Token { get; set; }
|
public required string Token { get; set; }
|
||||||
|
|||||||
@ -0,0 +1,8 @@
|
|||||||
|
using MaksIT.Core.Abstractions.Webapi;
|
||||||
|
|
||||||
|
|
||||||
|
namespace MaksIT.Models.LetsEncryptServer.Identity.User;
|
||||||
|
|
||||||
|
public class PatchUserRequest : PatchRequestModelBase {
|
||||||
|
public string? Password { get; set; }
|
||||||
|
}
|
||||||
@ -0,0 +1,7 @@
|
|||||||
|
using MaksIT.Core.Abstractions.Webapi;
|
||||||
|
|
||||||
|
|
||||||
|
namespace MaksIT.Models.LetsEncryptServer.Identity.User;
|
||||||
|
|
||||||
|
public class UserResponse : ResponseModelBase {
|
||||||
|
}
|
||||||
@ -11,7 +11,7 @@
|
|||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="MaksIT.Core" Version="1.5.3" />
|
<PackageReference Include="MaksIT.Core" Version="1.5.4" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
</Project>
|
</Project>
|
||||||
@ -8,7 +8,7 @@
|
|||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="Microsoft.AspNetCore.DataProtection.Extensions" Version="9.0.10" />
|
<PackageReference Include="Microsoft.AspNetCore.DataProtection.Extensions" Version="10.0.0" />
|
||||||
<PackageReference Include="Microsoft.VisualStudio.Azure.Containers.Tools.Targets" Version="1.22.1" />
|
<PackageReference Include="Microsoft.VisualStudio.Azure.Containers.Tools.Targets" Version="1.22.1" />
|
||||||
<PackageReference Include="Yarp.ReverseProxy" Version="2.3.0" />
|
<PackageReference Include="Yarp.ReverseProxy" Version="2.3.0" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|||||||
@ -15,4 +15,4 @@ services:
|
|||||||
image: ${DOCKER_REGISTRY-}certs-ui-server
|
image: ${DOCKER_REGISTRY-}certs-ui-server
|
||||||
build:
|
build:
|
||||||
context: .
|
context: .
|
||||||
dockerfile: LetsEncryptServer/Dockerfile
|
dockerfile: MaksIT.Webapi/Dockerfile
|
||||||
|
|||||||
@ -19,14 +19,14 @@ This chart deploys the MaksIT CertsUI tool for automated Let's Encrypt HTTPS cer
|
|||||||
The server uses a ConfigMap (`appsettings.json`) for application settings.
|
The server uses a ConfigMap (`appsettings.json`) for application settings.
|
||||||
|
|
||||||
- **Persistence**:
|
- **Persistence**:
|
||||||
PVCs are created for `/acme` and `/cache` directories.
|
PVCs are created for `/acme`, `/cache` and `/data` directories.
|
||||||
|
|
||||||
------------------------------------------------------------
|
------------------------------------------------------------
|
||||||
## Uninstall
|
## Uninstall
|
||||||
|
|
||||||
To remove all resources created by this chart:
|
To remove all resources created by this chart:
|
||||||
```
|
```
|
||||||
helm uninstall {{ .Release.Name }}
|
helm uninstall {{ .Release.Name }} -n {{ .Release.Name }}
|
||||||
```
|
```
|
||||||
|
|
||||||
------------------------------------------------------------
|
------------------------------------------------------------
|
||||||
|
|||||||
@ -38,17 +38,29 @@ components:
|
|||||||
storageClass: local-path
|
storageClass: local-path
|
||||||
size: 50Mi
|
size: 50Mi
|
||||||
accessModes: [ReadWriteOnce]
|
accessModes: [ReadWriteOnce]
|
||||||
|
- name: data
|
||||||
|
mountPath: /data
|
||||||
|
type: pvc
|
||||||
|
pvc:
|
||||||
|
create: true
|
||||||
|
storageClass: local-path
|
||||||
|
size: 50Mi
|
||||||
|
accessModes: [ReadWriteOnce]
|
||||||
secretsFile:
|
secretsFile:
|
||||||
key: appsecrets.json
|
key: appsecrets.json
|
||||||
mountPath: /secrets/appsecrets.json
|
mountPath: /secrets/appsecrets.json
|
||||||
content: |
|
content: |
|
||||||
{
|
{
|
||||||
|
"Auth": {
|
||||||
|
"Secret": "",
|
||||||
|
"Pepper": ""
|
||||||
|
},
|
||||||
"Agent": {
|
"Agent": {
|
||||||
}
|
"AgentKey": ""
|
||||||
|
},
|
||||||
}
|
}
|
||||||
keep: true
|
keep: true
|
||||||
forceUpdate: false
|
forceUpdate: false
|
||||||
|
|
||||||
configMapFile:
|
configMapFile:
|
||||||
key: appsettings.json
|
key: appsettings.json
|
||||||
mountPath: /configMap/appsettings.json
|
mountPath: /configMap/appsettings.json
|
||||||
@ -61,17 +73,26 @@ components:
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"Configuration": {
|
"Configuration": {
|
||||||
"Production": "https://acme-v02.api.letsencrypt.org/directory",
|
"Auth": {
|
||||||
"Staging": "https://acme-staging-v02.api.letsencrypt.org/directory",
|
"Issuer": "",
|
||||||
|
"Audience": "",
|
||||||
"CacheFolder": "/cache",
|
"Expiration": 15, // Access token lifetime in minutes (default: 15 minutes)
|
||||||
"AcmeFolder": "/acme",
|
"RefreshExpiration": 180, // Refresh token lifetime in days (default: 180 days)
|
||||||
|
},
|
||||||
|
|
||||||
"Agent": {
|
"Agent": {
|
||||||
"AgentHostname": "http://websrv0001.corp.maks-it.com",
|
"AgentHostname": "http://websrv0001.corp.maks-it.com",
|
||||||
"AgentPort": 5000,
|
"AgentPort": 5000,
|
||||||
"ServiceToReload": "haproxy"
|
"ServiceToReload": "haproxy"
|
||||||
}
|
},
|
||||||
|
|
||||||
|
"Production": "https://acme-v02.api.letsencrypt.org/directory",
|
||||||
|
"Staging": "https://acme-staging-v02.api.letsencrypt.org/directory",
|
||||||
|
"CacheFolder": "/cache",
|
||||||
|
"AcmeFolder": "/acme",
|
||||||
|
|
||||||
|
"DataFolder": "/data",
|
||||||
|
"SettingsFile": "/data/settings.json",
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
keep: true
|
keep: true
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user