@@ -66,7 +71,6 @@ const LoginScreen: FC = () => {
onChange={(e) => handleInputChange('username', e.target.value)}
errorText={errors.username}
/>
-
{
errorText={errors.password}
/>
-
- {/* 2FA Options */}
-
-
- {use2FA && (
- setUse2FARecovery(e.target.checked)}
- />
- )}
-
-
- {/* 2FA Inputs */}
- {use2FA && (
-
- {use2FARecovery ? (
- handleInputChange('twoFactorRecoveryCode', e.target.value)}
- errorText={errors.twoFactorRecoveryCode}
- />
- ) : (
- handleInputChange('twoFactorCode', e.target.value)}
- errorText={errors.twoFactorCode}
- />
- )}
-
- )}
-
{/* Submit */}
{
- return Edit Identity Form
-}
-
-export {
- EditIdentity
-}
-
diff --git a/src/MaksIT.WebUI/src/forms/Users/EditUser/ChangePassword.tsx b/src/MaksIT.WebUI/src/forms/Users/EditUser/ChangePassword.tsx
new file mode 100644
index 0000000..a621312
--- /dev/null
+++ b/src/MaksIT.WebUI/src/forms/Users/EditUser/ChangePassword.tsx
@@ -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 = (props) => {
+ const {
+ userId,
+ isOpen = false,
+ onClose
+ } = props
+
+ const {
+ formState,
+ errors,
+ formIsValid,
+ handleInputChange,
+ setInitialState
+ } = useFormState({
+ 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(GetApiRoute(ApiRoutes.identityPatch).route.replace('{userId}', userId), data)
+ .then(response => {
+ if (!response) return
+
+ handleOnClose()
+ })
+ }
+ }
+
+ return
+
+ Change password
+
+
+ handleInputChange('password', e.target.value)}
+ />
+ handleInputChange('confirmPassword', e.target.value)}
+ />
+
+
+
+
+
+
+}
+
+export {
+ ChangePassword
+}
\ No newline at end of file
diff --git a/src/MaksIT.WebUI/src/forms/Users/EditUser/index.tsx b/src/MaksIT.WebUI/src/forms/Users/EditUser/index.tsx
new file mode 100644
index 0000000..b65567f
--- /dev/null
+++ b/src/MaksIT.WebUI/src/forms/Users/EditUser/index.tsx
@@ -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 = (props) => {
+ const {
+ userId,
+ onSubmitted,
+ cancelEnabled = false,
+ onCancel
+ } = props
+
+ const [showChangePassword, setShowChangePassword] = useState(false)
+
+ return <>
+
+ Edit user
+
+
+ setShowChangePassword(true)}
+ />
+
+
+
+
+
+
+ setShowChangePassword(false)}
+ />
+ >
+}
+
+export {
+ EditUser
+}
+
diff --git a/src/MaksIT.WebUI/src/pages/UserPage.tsx b/src/MaksIT.WebUI/src/pages/UserPage.tsx
index 8131373..f7fa70c 100644
--- a/src/MaksIT.WebUI/src/pages/UserPage.tsx
+++ b/src/MaksIT.WebUI/src/pages/UserPage.tsx
@@ -1,8 +1,11 @@
-import { EditIdentity } from '../forms/EditIdentity'
+import { useParams } from 'react-router-dom'
+import { EditUser } from '../forms/Users/EditUser'
const UserPage = () => {
- return
+ const { userId } = useParams<{ userId: string }>()
+
+ return userId ? : <>User not found>
}
export {
diff --git a/src/MaksIT.Webapi/Abstractions/Authorization/Filters/BaseAsyncAuthorizationFilter.cs b/src/MaksIT.Webapi/Abstractions/Authorization/Filters/BaseAsyncAuthorizationFilter.cs
new file mode 100644
index 0000000..7432965
--- /dev/null
+++ b/src/MaksIT.Webapi/Abstractions/Authorization/Filters/BaseAsyncAuthorizationFilter.cs
@@ -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);
+}
\ No newline at end of file
diff --git a/src/LetsEncryptServer/Abstractions/ServiceBase.cs b/src/MaksIT.Webapi/Abstractions/Services/ServiceBase.cs
similarity index 61%
rename from src/LetsEncryptServer/Abstractions/ServiceBase.cs
rename to src/MaksIT.Webapi/Abstractions/Services/ServiceBase.cs
index ef0cf92..bd87aa3 100644
--- a/src/LetsEncryptServer/Abstractions/ServiceBase.cs
+++ b/src/MaksIT.Webapi/Abstractions/Services/ServiceBase.cs
@@ -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 appSettings) {
+ protected readonly ILogger _logger = logger;
+ protected readonly Configuration _appSettings = appSettings.Value;
protected Result UnsupportedPatchOperationResponse() {
return Result.BadRequest("Unsupported operation");
diff --git a/src/MaksIT.Webapi/Authorization/Extensions/HttpContextExtension.cs b/src/MaksIT.Webapi/Authorization/Extensions/HttpContextExtension.cs
new file mode 100644
index 0000000..75606dc
--- /dev/null
+++ b/src/MaksIT.Webapi/Authorization/Extensions/HttpContextExtension.cs
@@ -0,0 +1,14 @@
+using MaksIT.Results;
+
+namespace MaksIT.Webapi.Authorization.Extensions;
+
+public static class HttpContextExtension {
+ public static Result GetJwtTokenData(this HttpContext context) {
+ var jwtTokenData = context.Items[HttpContextValue.JwtTokenData] as JwtTokenData;
+
+ if (jwtTokenData == null)
+ return Result.Forbidden(null, "JWT token data not found in the request");
+
+ return Result.Ok(jwtTokenData);
+ }
+}
diff --git a/src/MaksIT.Webapi/Authorization/Filters/JwtAuthorizationFilter.cs b/src/MaksIT.Webapi/Authorization/Filters/JwtAuthorizationFilter.cs
new file mode 100644
index 0000000..b112d5e
--- /dev/null
+++ b/src/MaksIT.Webapi/Authorization/Filters/JwtAuthorizationFilter.cs
@@ -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 logger,
+ IOptions 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> ValidateJwtTokenAsync(string? token) {
+ if (string.IsNullOrWhiteSpace(token))
+ return Result.Forbidden(null, "Token is missing");
+
+ if (!JwtGenerator.TryValidateToken(
+ _jwtSettingsConfiguration.Secret,
+ _jwtSettingsConfiguration.Issuer,
+ _jwtSettingsConfiguration.Audience,
+ token,
+ out var jwtTokenClaims,
+ out string? errorMessage
+ )) {
+ return Result.InternalServerError(null, errorMessage);
+ }
+
+ if (jwtTokenClaims == null ||
+ jwtTokenClaims.Username == null ||
+ jwtTokenClaims.Roles == null ||
+ jwtTokenClaims.IssuedAt == null ||
+ jwtTokenClaims.ExpiresAt == null
+ ) {
+ return Result.Forbidden(null, "Invalid JWT token or claims");
+ }
+
+ var settingsResult = await _settingsService.LoadAsync();
+ if (!settingsResult.IsSuccess || settingsResult.Value == null) {
+ return Result.InternalServerError(null, "Failed to load settings");
+ }
+
+ var settings = settingsResult.Value;
+ var userResult = settings.GetUserByName(jwtTokenClaims.Username);
+ if (!userResult.IsSuccess || userResult.Value == null) {
+ return Result.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.Ok(jwtTokenData);
+ }
+}
\ No newline at end of file
diff --git a/src/MaksIT.Webapi/Authorization/HttpContextValue.cs b/src/MaksIT.Webapi/Authorization/HttpContextValue.cs
new file mode 100644
index 0000000..716ea9c
--- /dev/null
+++ b/src/MaksIT.Webapi/Authorization/HttpContextValue.cs
@@ -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) { }
+}
diff --git a/src/MaksIT.Webapi/Authorization/JwtTokenData.cs b/src/MaksIT.Webapi/Authorization/JwtTokenData.cs
new file mode 100644
index 0000000..65170ce
--- /dev/null
+++ b/src/MaksIT.Webapi/Authorization/JwtTokenData.cs
@@ -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; }
+}
\ No newline at end of file
diff --git a/src/LetsEncryptServer/BackgroundServices/AutoRenewal.cs b/src/MaksIT.Webapi/BackgroundServices/AutoRenewal.cs
similarity index 96%
rename from src/LetsEncryptServer/BackgroundServices/AutoRenewal.cs
rename to src/MaksIT.Webapi/BackgroundServices/AutoRenewal.cs
index 2c506f3..de2dcbb 100644
--- a/src/LetsEncryptServer/BackgroundServices/AutoRenewal.cs
+++ b/src/MaksIT.Webapi/BackgroundServices/AutoRenewal.cs
@@ -1,11 +1,11 @@
using Microsoft.Extensions.Options;
-using MaksIT.LetsEncryptServer.Services;
+using MaksIT.Webapi.Services;
using MaksIT.LetsEncrypt.Entities;
using MaksIT.Results;
-namespace MaksIT.LetsEncryptServer.BackgroundServices {
+namespace MaksIT.Webapi.BackgroundServices {
public class AutoRenewal : BackgroundService {
private readonly IOptions _appSettings;
diff --git a/src/LetsEncryptServer/BackgroundServices/Initialization.cs b/src/MaksIT.Webapi/BackgroundServices/Initialization.cs
similarity index 93%
rename from src/LetsEncryptServer/BackgroundServices/Initialization.cs
rename to src/MaksIT.Webapi/BackgroundServices/Initialization.cs
index fb9e696..07448a8 100644
--- a/src/LetsEncryptServer/BackgroundServices/Initialization.cs
+++ b/src/MaksIT.Webapi/BackgroundServices/Initialization.cs
@@ -1,9 +1,9 @@
using Microsoft.Extensions.Options;
-using MaksIT.LetsEncryptServer.Domain;
-using MaksIT.LetsEncryptServer.Services;
+using MaksIT.Webapi.Domain;
+using MaksIT.Webapi.Services;
-namespace MaksIT.LetsEncryptServer.BackgroundServices {
+namespace MaksIT.Webapi.BackgroundServices {
public class Initialization : BackgroundService {
private readonly IServiceProvider _serviceProvider;
diff --git a/src/LetsEncryptServer/Configuration.cs b/src/MaksIT.Webapi/Configuration.cs
similarity index 96%
rename from src/LetsEncryptServer/Configuration.cs
rename to src/MaksIT.Webapi/Configuration.cs
index c6e3832..e34fe31 100644
--- a/src/LetsEncryptServer/Configuration.cs
+++ b/src/MaksIT.Webapi/Configuration.cs
@@ -1,6 +1,6 @@
using MaksIT.LetsEncrypt;
-namespace MaksIT.LetsEncryptServer {
+namespace MaksIT.Webapi {
public class Agent {
public required string AgentHostname { get; set; }
diff --git a/src/LetsEncryptServer/Controllers/AccountController.cs b/src/MaksIT.Webapi/Controllers/AccountController.cs
similarity index 82%
rename from src/LetsEncryptServer/Controllers/AccountController.cs
rename to src/MaksIT.Webapi/Controllers/AccountController.cs
index b79fca5..563bb5b 100644
--- a/src/LetsEncryptServer/Controllers/AccountController.cs
+++ b/src/MaksIT.Webapi/Controllers/AccountController.cs
@@ -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;
-using MaksIT.Models.LetsEncryptServer.Account.Requests;
-
-namespace MaksIT.LetsEncryptServer.Controllers;
+namespace MaksIT.Webapi.Controllers;
[ApiController]
[Route("api")]
+[ServiceFilter(typeof(JwtAuthorizationFilter))]
public class AccountController : ControllerBase {
private readonly IAccountService _accountService;
@@ -18,6 +19,7 @@ public class AccountController : ControllerBase {
#region Accounts
+ [ServiceFilter(typeof(JwtAuthorizationFilter))]
[HttpGet("accounts")]
public async Task GetAccounts() {
var result = await _accountService.GetAccountsAsync();
diff --git a/src/LetsEncryptServer/Controllers/AgentController.cs b/src/MaksIT.Webapi/Controllers/AgentController.cs
similarity index 79%
rename from src/LetsEncryptServer/Controllers/AgentController.cs
rename to src/MaksIT.Webapi/Controllers/AgentController.cs
index 5ed23e1..73d276e 100644
--- a/src/LetsEncryptServer/Controllers/AgentController.cs
+++ b/src/MaksIT.Webapi/Controllers/AgentController.cs
@@ -1,4 +1,5 @@
-using MaksIT.LetsEncryptServer.Services;
+using MaksIT.Webapi.Authorization.Filters;
+using MaksIT.Webapi.Services;
using Microsoft.AspNetCore.Mvc;
namespace LetsEncryptServer.Controllers;
@@ -6,6 +7,7 @@ namespace LetsEncryptServer.Controllers;
[ApiController]
[Route("api")]
+[ServiceFilter(typeof(JwtAuthorizationFilter))]
public class AgentController : ControllerBase {
private readonly IAgentService _agentController;
diff --git a/src/LetsEncryptServer/Controllers/CacheController.cs b/src/MaksIT.Webapi/Controllers/CacheController.cs
similarity index 88%
rename from src/LetsEncryptServer/Controllers/CacheController.cs
rename to src/MaksIT.Webapi/Controllers/CacheController.cs
index 4955853..6494e6d 100644
--- a/src/LetsEncryptServer/Controllers/CacheController.cs
+++ b/src/MaksIT.Webapi/Controllers/CacheController.cs
@@ -1,10 +1,12 @@
-using MaksIT.LetsEncryptServer.Services;
+using MaksIT.Webapi.Authorization.Filters;
+using MaksIT.Webapi.Services;
using Microsoft.AspNetCore.Mvc;
namespace LetsEncryptServer.Controllers;
[ApiController]
[Route("api")]
+[ServiceFilter(typeof(JwtAuthorizationFilter))]
public class CacheController(ICacheService cacheService) : ControllerBase {
private readonly ICacheService _cacheService = cacheService;
@@ -32,8 +34,8 @@ public class CacheController(ICacheService cacheService) : ControllerBase {
}
[HttpDelete("cache")]
- public IActionResult DeleteCache() {
- var result = _cacheService.DeleteCacheAsync();
+ public async Task DeleteCache() {
+ var result = await _cacheService.DeleteCacheAsync();
return result.ToActionResult();
}
diff --git a/src/LetsEncryptServer/Controllers/CertsFlowController.cs b/src/MaksIT.Webapi/Controllers/CertsFlowController.cs
similarity index 94%
rename from src/LetsEncryptServer/Controllers/CertsFlowController.cs
rename to src/MaksIT.Webapi/Controllers/CertsFlowController.cs
index 17a54e2..cc8fafc 100644
--- a/src/LetsEncryptServer/Controllers/CertsFlowController.cs
+++ b/src/MaksIT.Webapi/Controllers/CertsFlowController.cs
@@ -1,13 +1,15 @@
-using Microsoft.AspNetCore.Mvc;
-using MaksIT.LetsEncryptServer.Services;
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 {
///
/// Certificates flow controller, used for granular testing purposes
///
[ApiController]
[Route("api/certs")]
+ [ServiceFilter(typeof(JwtAuthorizationFilter))]
public class CertsFlowController : ControllerBase {
private readonly ICertsFlowService _certsFlowService;
diff --git a/src/MaksIT.Webapi/Controllers/IdentityController.cs b/src/MaksIT.Webapi/Controllers/IdentityController.cs
new file mode 100644
index 0000000..2f0fe81
--- /dev/null
+++ b/src/MaksIT.Webapi/Controllers/IdentityController.cs
@@ -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;
+
+
+ ///
+ /// Patch user data.
+ ///
+ /// Nullable Id as user can patch his own data
+ ///
+ ///
+ [ServiceFilter(typeof(JwtAuthorizationFilter))]
+ [HttpPatch("user/{id:guid}")]
+ [ProducesResponseType(typeof(UserResponse), StatusCodes.Status200OK)]
+ public async Task 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 Login([FromBody] LoginRequest requestData) {
+ var result = await _identityService.LoginAsync(requestData);
+ return result.ToActionResult();
+ }
+
+ [HttpPost("refresh")]
+ [ProducesResponseType(typeof(LoginResponse), StatusCodes.Status200OK)]
+ public async Task RefreshToken([FromBody] RefreshTokenRequest requestData) {
+ var result = await _identityService.RefreshTokenAsync(requestData);
+ return result.ToActionResult();
+ }
+
+ [HttpPost("logout")]
+ [ProducesResponseType(StatusCodes.Status200OK)]
+ public async Task Logout([FromBody] LogoutRequest requestData) {
+ var result = await _identityService.Logout(requestData);
+ return result.ToActionResult();
+ }
+ #endregion
+}
diff --git a/src/LetsEncryptServer/Controllers/WellKnownController.cs b/src/MaksIT.Webapi/Controllers/WellKnownController.cs
similarity index 88%
rename from src/LetsEncryptServer/Controllers/WellKnownController.cs
rename to src/MaksIT.Webapi/Controllers/WellKnownController.cs
index 7b995e8..df4b1a3 100644
--- a/src/LetsEncryptServer/Controllers/WellKnownController.cs
+++ b/src/MaksIT.Webapi/Controllers/WellKnownController.cs
@@ -1,10 +1,10 @@
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Options;
-using MaksIT.LetsEncryptServer.Services;
+using MaksIT.Webapi.Services;
-namespace MaksIT.LetsEncryptServer.Controllers;
+namespace MaksIT.Webapi.Controllers;
[ApiController]
[Route(".well-known")]
diff --git a/src/LetsEncryptServer/Dockerfile b/src/MaksIT.Webapi/Dockerfile
similarity index 58%
rename from src/LetsEncryptServer/Dockerfile
rename to src/MaksIT.Webapi/Dockerfile
index aae13d1..8537442 100644
--- a/src/LetsEncryptServer/Dockerfile
+++ b/src/MaksIT.Webapi/Dockerfile
@@ -10,17 +10,17 @@ ARG BUILD_CONFIGURATION=Release
WORKDIR /src
COPY ["Models/Models.csproj", "Models/"]
COPY ["LetsEncrypt/LetsEncrypt.csproj", "LetsEncrypt/"]
-COPY ["LetsEncryptServer/LetsEncryptServer.csproj", "LetsEncryptServer/"]
-RUN dotnet restore "./LetsEncryptServer/LetsEncryptServer.csproj"
+COPY ["MaksIT.Webapi/MaksIT.Webapi.csproj", "MaksIT.Webapi/"]
+RUN dotnet restore "./MaksIT.Webapi/MaksIT.Webapi.csproj"
COPY . .
-WORKDIR "/src/LetsEncryptServer"
-RUN dotnet build "./LetsEncryptServer.csproj" -c $BUILD_CONFIGURATION -o /app/build
+WORKDIR "/src/MaksIT.Webapi"
+RUN dotnet build "./MaksIT.Webapi.csproj" -c $BUILD_CONFIGURATION -o /app/build
FROM build AS publish
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
WORKDIR /app
COPY --from=publish /app/publish .
-ENTRYPOINT ["dotnet", "LetsEncryptServer.dll"]
\ No newline at end of file
+ENTRYPOINT ["dotnet", "MaksIT.Webapi.dll"]
\ No newline at end of file
diff --git a/src/LetsEncryptServer/Domain/JwtToken.cs b/src/MaksIT.Webapi/Domain/JwtToken.cs
similarity index 97%
rename from src/LetsEncryptServer/Domain/JwtToken.cs
rename to src/MaksIT.Webapi/Domain/JwtToken.cs
index 7d6ea76..489146c 100644
--- a/src/LetsEncryptServer/Domain/JwtToken.cs
+++ b/src/MaksIT.Webapi/Domain/JwtToken.cs
@@ -1,7 +1,7 @@
using MaksIT.Core.Abstractions.Domain;
using System.Linq.Dynamic.Core.Tokenizer;
-namespace MaksIT.LetsEncryptServer.Domain;
+namespace MaksIT.Webapi.Domain;
public class JwtToken(Guid id) : DomainDocumentBase(id) {
diff --git a/src/LetsEncryptServer/Domain/Settings.cs b/src/MaksIT.Webapi/Domain/Settings.cs
similarity index 91%
rename from src/LetsEncryptServer/Domain/Settings.cs
rename to src/MaksIT.Webapi/Domain/Settings.cs
index 7318a7a..2da7fa0 100644
--- a/src/LetsEncryptServer/Domain/Settings.cs
+++ b/src/MaksIT.Webapi/Domain/Settings.cs
@@ -2,7 +2,7 @@
using MaksIT.Core.Security;
using MaksIT.Results;
-namespace MaksIT.LetsEncryptServer.Domain;
+namespace MaksIT.Webapi.Domain;
public class Settings : DomainObjectBase {
public bool Init { get; set; }
@@ -25,6 +25,13 @@ public class Settings : DomainObjectBase {
return Result.Ok(this);
}
+ public Result GetUserById(Guid id) {
+ var user = Users.FirstOrDefault(x => x.Id == id);
+ if (user == null)
+ return Result.NotFound(null, "User not found.");
+ return Result.Ok(user);
+ }
+
public Result GetUserByName(string name) {
var user = Users.FirstOrDefault(x => x.Name == name);
if (user == null)
diff --git a/src/LetsEncryptServer/Domain/User.cs b/src/MaksIT.Webapi/Domain/User.cs
similarity index 99%
rename from src/LetsEncryptServer/Domain/User.cs
rename to src/MaksIT.Webapi/Domain/User.cs
index 432f8f2..50a256e 100644
--- a/src/LetsEncryptServer/Domain/User.cs
+++ b/src/MaksIT.Webapi/Domain/User.cs
@@ -2,7 +2,7 @@
using MaksIT.Core.Security;
using MaksIT.Results;
-namespace MaksIT.LetsEncryptServer.Domain;
+namespace MaksIT.Webapi.Domain;
public class User(
Guid id
diff --git a/src/LetsEncryptServer/Dto/JwtTokenDto.cs b/src/MaksIT.Webapi/Dto/JwtTokenDto.cs
similarity index 90%
rename from src/LetsEncryptServer/Dto/JwtTokenDto.cs
rename to src/MaksIT.Webapi/Dto/JwtTokenDto.cs
index d598d50..e07b776 100644
--- a/src/LetsEncryptServer/Dto/JwtTokenDto.cs
+++ b/src/MaksIT.Webapi/Dto/JwtTokenDto.cs
@@ -1,6 +1,6 @@
using MaksIT.Core.Abstractions.Dto;
-namespace MaksIT.LetsEncryptServer.Dto;
+namespace MaksIT.Webapi.Dto;
public class JwtTokenDto : DtoDocumentBase {
public required string Token { get; set; }
diff --git a/src/LetsEncryptServer/Dto/SettingsDto.cs b/src/MaksIT.Webapi/Dto/SettingsDto.cs
similarity index 75%
rename from src/LetsEncryptServer/Dto/SettingsDto.cs
rename to src/MaksIT.Webapi/Dto/SettingsDto.cs
index 00b9111..88828e6 100644
--- a/src/LetsEncryptServer/Dto/SettingsDto.cs
+++ b/src/MaksIT.Webapi/Dto/SettingsDto.cs
@@ -1,4 +1,4 @@
-namespace MaksIT.LetsEncryptServer.Dto;
+namespace MaksIT.Webapi.Dto;
public class SettingsDto {
public required bool Init { get; set; }
diff --git a/src/LetsEncryptServer/Dto/UserDto.cs b/src/MaksIT.Webapi/Dto/UserDto.cs
similarity index 90%
rename from src/LetsEncryptServer/Dto/UserDto.cs
rename to src/MaksIT.Webapi/Dto/UserDto.cs
index f4132e7..6bc6667 100644
--- a/src/LetsEncryptServer/Dto/UserDto.cs
+++ b/src/MaksIT.Webapi/Dto/UserDto.cs
@@ -1,6 +1,6 @@
using MaksIT.Core.Abstractions.Dto;
-namespace MaksIT.LetsEncryptServer.Dto;
+namespace MaksIT.Webapi.Dto;
public class UserDto : DtoDocumentBase {
public required string Name { get; set; } = string.Empty;
diff --git a/src/LetsEncryptServer/LetsEncryptServer.http b/src/MaksIT.Webapi/LetsEncryptServer.http
similarity index 100%
rename from src/LetsEncryptServer/LetsEncryptServer.http
rename to src/MaksIT.Webapi/LetsEncryptServer.http
diff --git a/src/LetsEncryptServer/LetsEncryptServer.csproj b/src/MaksIT.Webapi/MaksIT.Webapi.csproj
similarity index 77%
rename from src/LetsEncryptServer/LetsEncryptServer.csproj
rename to src/MaksIT.Webapi/MaksIT.Webapi.csproj
index 653d94b..25876c2 100644
--- a/src/LetsEncryptServer/LetsEncryptServer.csproj
+++ b/src/MaksIT.Webapi/MaksIT.Webapi.csproj
@@ -1,24 +1,24 @@
-
+
net8.0
enable
enable
- MaksIT.$(MSBuildProjectName.Replace(" ", "_"))
+ $(MSBuildProjectName.Replace(" ", "_"))
Linux
..\docker-compose.dcproj
-
+
-
+
-
+
diff --git a/src/LetsEncryptServer/Program.cs b/src/MaksIT.Webapi/Program.cs
similarity index 88%
rename from src/LetsEncryptServer/Program.cs
rename to src/MaksIT.Webapi/Program.cs
index 241eb06..6bead25 100644
--- a/src/LetsEncryptServer/Program.cs
+++ b/src/MaksIT.Webapi/Program.cs
@@ -1,19 +1,17 @@
-using System.Text.Json.Serialization;
using MaksIT.Core.Logging;
using MaksIT.Core.Webapi.Middlewares;
using MaksIT.LetsEncrypt.Extensions;
-using MaksIT.LetsEncryptServer;
-using MaksIT.LetsEncryptServer.Services;
-using MaksIT.LetsEncryptServer.BackgroundServices;
+using MaksIT.Webapi;
+using MaksIT.Webapi.Authorization.Filters;
+using MaksIT.Webapi.BackgroundServices;
+using MaksIT.Webapi.Services;
+using System.Text.Json.Serialization;
var builder = WebApplication.CreateBuilder(args);
-// Extract configuration
+#region Configuration setup
var configuration = builder.Configuration;
-// Add logging
-builder.Logging.AddConsoleLogger();
-
var configMapPath = Path.Combine(Path.DirectorySeparatorChar.ToString(), "configMap", "appsettings.json");
if (File.Exists(configMapPath)) {
configuration.AddJsonFile(configMapPath, optional: false, reloadOnChange: true);
@@ -30,7 +28,10 @@ var appSettings = configurationSection.Get() ?? throw new Argumen
// Allow configurations to be available through IOptions
builder.Services.Configure(configurationSection);
+#endregion
+// Add logging
+builder.Logging.AddConsoleLogger();
// Add services to the container.
builder.Services.AddControllers()
@@ -38,9 +39,14 @@ builder.Services.AddControllers()
options.JsonSerializerOptions.DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull;
});
+// Add custom authorization filter
+builder.Services.AddScoped();
+
+#region Swagger
// Learn more about configuring Swagger/OpenAPI at https://aka.ms/aspnetcore/swashbuckle
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen();
+#endregion
builder.Services.AddCors();
@@ -58,9 +64,10 @@ builder.Services.AddHttpClient();
builder.Services.AddScoped();
builder.Services.AddScoped();
-// Hosted services
+#region Hosted services
builder.Services.AddHostedService();
builder.Services.AddHostedService();
+#endregion
var app = builder.Build();
diff --git a/src/LetsEncryptServer/Properties/launchSettings.json b/src/MaksIT.Webapi/Properties/launchSettings.json
similarity index 100%
rename from src/LetsEncryptServer/Properties/launchSettings.json
rename to src/MaksIT.Webapi/Properties/launchSettings.json
diff --git a/src/LetsEncryptServer/Services/AccoutService.cs b/src/MaksIT.Webapi/Services/AccoutService.cs
similarity index 83%
rename from src/LetsEncryptServer/Services/AccoutService.cs
rename to src/MaksIT.Webapi/Services/AccoutService.cs
index a91f4fe..e3d0584 100644
--- a/src/LetsEncryptServer/Services/AccoutService.cs
+++ b/src/MaksIT.Webapi/Services/AccoutService.cs
@@ -1,13 +1,13 @@
-
-using LetsEncryptServer.Abstractions;
-using MaksIT.Core.Webapi.Models;
+using MaksIT.Core.Webapi.Models;
using MaksIT.LetsEncrypt.Entities;
using MaksIT.Models.LetsEncryptServer.Account.Requests;
using MaksIT.Models.LetsEncryptServer.Account.Responses;
using MaksIT.Results;
+using MaksIT.Webapi.Abstractions.Services;
+using Microsoft.Extensions.Options;
-namespace MaksIT.LetsEncryptServer.Services;
+namespace MaksIT.Webapi.Services;
public interface IAccountService {
Task> GetAccountsAsync();
@@ -17,27 +17,21 @@ public interface IAccountService {
Task DeleteAccountAsync(Guid accountId);
}
-public class AccountService : ServiceBase, IAccountService {
-
- private readonly ILogger _logger;
- private readonly ICacheService _cacheService;
- private readonly ICertsFlowService _certsFlowService;
-
- public AccountService(
- ILogger logger,
- ICacheService cacheService,
- ICertsFlowService certsFlowService
- ) {
- _logger = logger;
- _cacheService = cacheService;
- _certsFlowService = certsFlowService;
- }
+public class AccountService(
+ ILogger logger,
+ IOptions appSettings,
+ ICacheService cacheService,
+ ICertsFlowService certsFlowService
+) : ServiceBase(
+ logger,
+ appSettings
+), IAccountService {
#region Accounts
public async Task> GetAccountsAsync() {
- var accountsFromCacheResult = await _cacheService.LoadAccountsFromCacheAsync();
+ var accountsFromCacheResult = await cacheService.LoadAccountsFromCacheAsync();
if (!accountsFromCacheResult.IsSuccess || accountsFromCacheResult.Value == null) {
return accountsFromCacheResult
.ToResultOfType(_ => null);
@@ -51,7 +45,7 @@ public class AccountService : ServiceBase, IAccountService {
}
public async Task> GetAccountAsync(Guid accountId) {
- var loadFromCacheResult = await _cacheService.LoadAccountFromCacheAsync(accountId);
+ var loadFromCacheResult = await cacheService.LoadAccountFromCacheAsync(accountId);
if (!loadFromCacheResult.IsSuccess || loadFromCacheResult.Value == null) {
return loadFromCacheResult.ToResultOfType(_ => null);
}
@@ -63,7 +57,7 @@ public class AccountService : ServiceBase, IAccountService {
public async Task> PostAccountAsync(PostAccountRequest requestData) {
- var fullFlowResult = await _certsFlowService.FullFlow(
+ var fullFlowResult = await certsFlowService.FullFlow(
requestData.IsStaging,
null,
requestData.Description,
@@ -77,7 +71,7 @@ public class AccountService : ServiceBase, IAccountService {
var accountId = fullFlowResult.Value.Value;
- var loadAccountFromCacheResult = await _cacheService.LoadAccountFromCacheAsync(accountId);
+ var loadAccountFromCacheResult = await cacheService.LoadAccountFromCacheAsync(accountId);
if (!loadAccountFromCacheResult.IsSuccess || loadAccountFromCacheResult.Value == null) {
return loadAccountFromCacheResult.ToResultOfType(_ => null);
}
@@ -88,7 +82,7 @@ public class AccountService : ServiceBase, IAccountService {
}
public async Task> PatchAccountAsync(Guid accountId, PatchAccountRequest requestData) {
- var loadAccountResult = await _cacheService.LoadAccountFromCacheAsync(accountId);
+ var loadAccountResult = await cacheService.LoadAccountFromCacheAsync(accountId);
if (!loadAccountResult.IsSuccess || loadAccountResult.Value == null) {
return loadAccountResult.ToResultOfType(_ => 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) {
return saveResult.ToResultOfType(default);
}
if (hostnamesToAdd.Count > 0) {
- var fullFlowResult = await _certsFlowService.FullFlow(
+ var fullFlowResult = await certsFlowService.FullFlow(
cache.IsStaging,
cache.AccountId,
cache.Description,
@@ -173,7 +167,7 @@ public class AccountService : ServiceBase, IAccountService {
}
if (hostnamesToRemove.Count > 0) {
- var revokeResult = await _certsFlowService.FullRevocationFlow(
+ var revokeResult = await certsFlowService.FullRevocationFlow(
cache.IsStaging,
cache.AccountId,
cache.Description,
@@ -186,7 +180,7 @@ public class AccountService : ServiceBase, IAccountService {
}
#endregion
- loadAccountResult = await _cacheService.LoadAccountFromCacheAsync(accountId);
+ loadAccountResult = await cacheService.LoadAccountFromCacheAsync(accountId);
if (!loadAccountResult.IsSuccess || loadAccountResult.Value == null) {
return loadAccountResult.ToResultOfType(_ => null);
}
@@ -198,7 +192,7 @@ public class AccountService : ServiceBase, IAccountService {
// TODO: Revoke all certificates
// Remove from cache
- return await _cacheService.DeleteFromCacheAsync(accountId);
+ return await cacheService.DeleteAccountCacheAsync(accountId);
}
#endregion
diff --git a/src/LetsEncryptServer/Services/AgentService.cs b/src/MaksIT.Webapi/Services/AgentService.cs
similarity index 73%
rename from src/LetsEncryptServer/Services/AgentService.cs
rename to src/MaksIT.Webapi/Services/AgentService.cs
index 63b7cd0..b997a6d 100644
--- a/src/LetsEncryptServer/Services/AgentService.cs
+++ b/src/MaksIT.Webapi/Services/AgentService.cs
@@ -1,12 +1,13 @@
using MaksIT.Models.Agent.Requests;
using MaksIT.Results;
+using MaksIT.Webapi.Abstractions.Services;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
-using Models.Agent.Responses;
+using MaksIT.Models.Agent.Responses;
using System.Text;
using System.Text.Json;
-namespace MaksIT.LetsEncryptServer.Services {
+namespace MaksIT.Webapi.Services {
public interface IAgentService {
Task> GetHelloWorld();
@@ -14,21 +15,14 @@ namespace MaksIT.LetsEncryptServer.Services {
Task ReloadService(string serviceName);
}
- public class AgentService : IAgentService {
-
- private readonly Configuration _appSettings;
- private readonly ILogger _logger;
- private readonly HttpClient _httpClient;
-
- public AgentService(
- IOptions appSettings,
- ILogger logger,
- HttpClient httpClient
- ) {
- _appSettings = appSettings.Value;
- _logger = logger;
- _httpClient = httpClient;
- }
+ public class AgentService(
+ IOptions appSettings,
+ ILogger logger,
+ HttpClient httpClient
+ ) : ServiceBase(
+ logger,
+ appSettings
+ ), IAgentService {
public async Task> GetHelloWorld() {
try {
@@ -39,12 +33,11 @@ namespace MaksIT.LetsEncryptServer.Services {
var request = new HttpRequestMessage(HttpMethod.Get, fullAddress);
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) {
-
var content = await response.Content.ReadAsStringAsync();
return Result.Ok(new HelloWorldResponse {
@@ -52,7 +45,7 @@ namespace MaksIT.LetsEncryptServer.Services {
});
}
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(null, $"Request to {endpoint} failed with status code: {response.StatusCode}");
}
@@ -60,7 +53,7 @@ namespace MaksIT.LetsEncryptServer.Services {
catch (Exception ex) {
List messages = new() { "Something went wrong" };
- _logger.LogError(ex, messages.FirstOrDefault());
+ logger.LogError(ex, messages.FirstOrDefault());
messages.Add(ex.Message);
@@ -89,20 +82,20 @@ namespace MaksIT.LetsEncryptServer.Services {
request.Headers.Add("x-api-key", _appSettings.Agent.AgentKey);
request.Headers.Add("accept", "application/json");
- var response = await _httpClient.SendAsync(request);
+ var response = await httpClient.SendAsync(request);
if (response.IsSuccessStatusCode) {
return Result.Ok();
}
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}");
}
}
catch (Exception ex) {
List messages = new() { "Something went wrong" };
- _logger.LogError(ex, messages.FirstOrDefault());
+ logger.LogError(ex, messages.FirstOrDefault());
messages.Add(ex.Message);
diff --git a/src/LetsEncryptServer/Services/CacheService.cs b/src/MaksIT.Webapi/Services/CacheService.cs
similarity index 53%
rename from src/LetsEncryptServer/Services/CacheService.cs
rename to src/MaksIT.Webapi/Services/CacheService.cs
index 6f53eab..7e54a22 100644
--- a/src/LetsEncryptServer/Services/CacheService.cs
+++ b/src/MaksIT.Webapi/Services/CacheService.cs
@@ -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.LetsEncrypt.Entities;
using MaksIT.Results;
-using Microsoft.Extensions.Logging;
-using Microsoft.Extensions.Options;
-using System.IO.Compression;
-using System.Text.Json;
+using MaksIT.Webapi.Abstractions.Services;
-namespace MaksIT.LetsEncryptServer.Services;
+
+namespace MaksIT.Webapi.Services;
public interface ICacheService {
Task> LoadAccountsFromCacheAsync();
Task> LoadAccountFromCacheAsync(Guid accountId);
Task SaveToCacheAsync(Guid accountId, RegistrationCache cache);
- Task DeleteFromCacheAsync(Guid accountId);
-
Task> DownloadCacheZipAsync();
Task> DownloadAccountCacheZipAsync(Guid accountId);
Task UploadCacheZipAsync(byte[] zipBytes);
Task UploadAccountCacheZipAsync(Guid accountId, byte[] zipBytes);
- Result DeleteCacheAsync();
+ Task DeleteCacheAsync();
+ Task DeleteAccountCacheAsync(Guid accountId);
}
-public class CacheService : ICacheService, IDisposable {
- private readonly ILogger _logger;
- private readonly string _cacheDirectory;
- private readonly LockManager _lockManager;
+public class CacheService(
+ ILogger logger,
+ IOptions appSettings
+) : ServiceBase(
+ logger,
+ appSettings
+), ICacheService, IDisposable {
+ private readonly string _cacheDirectory = appSettings.Value.CacheFolder;
+ private readonly LockManager _lockManager = new();
private readonly string tmpDir = "/tmp";
- public CacheService(
- ILogger logger,
- IOptions appsettings
- ) {
- _logger = logger;
- _cacheDirectory = appsettings.Value.CacheFolder;
- _lockManager = new LockManager();
- }
-
- ///
- /// Generates the cache file path for the given account ID.
- ///
- 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> LoadAccountsFromCacheAsync() {
return await _lockManager.ExecuteWithLockAsync(async () => {
var accountIds = GetCachedAccounts();
- var cacheLoadTasks = accountIds.Select(accountId => LoadFromCacheInternalAsync(accountId)).ToList();
-
var caches = new List();
- foreach (var task in cacheLoadTasks) {
- var taskResult = await task;
- if (!taskResult.IsSuccess || taskResult.Value == null) {
- // Depending on how you want to handle partial failures, you might want to return here
- // or continue loading other caches. For now, let's continue.
+ foreach (var accountId in accountIds) {
+ var cacheFilePath = GetCacheFilePath(accountId);
+ if (!File.Exists(cacheFilePath)) {
+ logger.LogWarning($"Cache file not found for account {accountId}");
continue;
}
-
- var registrationCache = taskResult.Value;
-
- caches.Add(registrationCache);
+ var json = await File.ReadAllTextAsync(cacheFilePath);
+ if (string.IsNullOrEmpty(json)) {
+ logger.LogWarning($"Cache file is empty for account {accountId}");
+ continue;
+ }
+ var cache = json.ToObject();
+ if (cache != null)
+ caches.Add(cache);
}
-
return Result.Ok(caches.ToArray());
});
}
- private async Task> LoadFromCacheInternalAsync(Guid accountId) {
- var cacheFilePath = GetCacheFilePath(accountId);
-
- if (!File.Exists(cacheFilePath)) {
- var message = $"Cache file not found for account {accountId}";
- _logger.LogWarning(message);
- return Result.InternalServerError(null, message);
- }
-
- var json = await File.ReadAllTextAsync(cacheFilePath);
- if (string.IsNullOrEmpty(json)) {
- var message = $"Cache file is empty for account {accountId}";
- _logger.LogWarning(message);
- return Result.InternalServerError(null, message);
- }
-
- var cache = json.ToObject();
- return Result.Ok(cache);
+ public async Task> LoadAccountFromCacheAsync(Guid accountId) {
+ return await _lockManager.ExecuteWithLockAsync(async () => {
+ var cacheFilePath = GetCacheFilePath(accountId);
+ if (!File.Exists(cacheFilePath)) {
+ var message = $"Cache file not found for account {accountId}";
+ logger.LogWarning(message);
+ return Result.InternalServerError(null, message);
+ }
+ var json = await File.ReadAllTextAsync(cacheFilePath);
+ if (string.IsNullOrEmpty(json)) {
+ var message = $"Cache file is empty for account {accountId}";
+ logger.LogWarning(message);
+ return Result.InternalServerError(null, message);
+ }
+ var cache = json.ToObject();
+ return Result.Ok(cache);
+ });
}
- private async Task SaveToCacheInternalAsync(Guid accountId, RegistrationCache cache) {
- var cacheFilePath = GetCacheFilePath(accountId);
- var json = cache.ToJson();
- await File.WriteAllTextAsync(cacheFilePath, json);
- _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 SaveToCacheAsync(Guid accountId, RegistrationCache cache) {
+ return await _lockManager.ExecuteWithLockAsync(async () => {
+ var cacheFilePath = GetCacheFilePath(accountId);
+ var json = cache.ToJson();
+ await File.WriteAllTextAsync(cacheFilePath, json);
+ logger.LogInformation($"Cache file saved for account {accountId}");
+ return Result.Ok();
+ });
}
public async Task> DownloadCacheZipAsync() {
try {
if (!Directory.Exists(_cacheDirectory)) {
var message = "Cache directory not found.";
- _logger.LogWarning(message);
+ logger.LogWarning(message);
return Result.NotFound(null, message);
}
@@ -148,12 +98,12 @@ public class CacheService : ICacheService, IDisposable {
ZipFile.CreateFromDirectory(_cacheDirectory, zipPath);
var zipBytes = await File.ReadAllBytesAsync(zipPath);
File.Delete(zipPath);
- _logger.LogInformation("Cache zipped to {ZipPath}", zipPath);
+ logger.LogInformation("Cache zipped to {ZipPath}", zipPath);
return Result.Ok(zipBytes);
}
catch (Exception ex) {
var message = "Error creating or reading cache zip file.";
- _logger.LogError(ex, message);
+ logger.LogError(ex, message);
return Result.InternalServerError(null, [message, .. ex.ExtractMessages()]);
}
}
@@ -163,7 +113,7 @@ public class CacheService : ICacheService, IDisposable {
var cacheFilePath = GetCacheFilePath(accountId);
if (!File.Exists(cacheFilePath)) {
var message = $"Cache file not found for account {accountId}.";
- _logger.LogWarning(message);
+ logger.LogWarning(message);
return Result.NotFound(null, message);
}
var zipPath = GetTempZipPath($"account_cache_{accountId}");
@@ -173,12 +123,12 @@ public class CacheService : ICacheService, IDisposable {
}
var zipBytes = await File.ReadAllBytesAsync(zipPath);
File.Delete(zipPath);
- _logger.LogInformation("Account cache zipped to {ZipPath}", zipPath);
+ logger.LogInformation("Account cache zipped to {ZipPath}", zipPath);
return Result.Ok(zipBytes);
}
catch (Exception ex) {
var message = "Error creating or reading account cache zip file.";
- _logger.LogError(ex, message);
+ logger.LogError(ex, message);
return Result.InternalServerError(null, [message, .. ex.ExtractMessages()]);
}
}
@@ -190,12 +140,12 @@ public class CacheService : ICacheService, IDisposable {
await File.WriteAllBytesAsync(zipPath, zipBytes);
ZipFile.ExtractToDirectory(zipPath, _cacheDirectory, true);
File.Delete(zipPath);
- _logger.LogInformation("Cache unzipped from {ZipPath}", zipPath);
+ logger.LogInformation("Cache unzipped from {ZipPath}", zipPath);
return Result.Ok();
}
catch (Exception ex) {
var message = "Error uploading or extracting cache zip file.";
- _logger.LogError(ex, message);
+ logger.LogError(ex, message);
return Result.InternalServerError([message, .. ex.ExtractMessages()]);
}
}
@@ -212,55 +162,85 @@ public class CacheService : ICacheService, IDisposable {
}
}
File.Delete(zipPath);
- _logger.LogInformation("Account cache unzipped from {ZipPath}", zipPath);
+ logger.LogInformation("Account cache unzipped from {ZipPath}", zipPath);
return Result.Ok();
}
catch (Exception ex) {
var message = "Error uploading or extracting account cache zip file.";
- _logger.LogError(ex, message);
+ logger.LogError(ex, message);
return Result.InternalServerError([message, .. ex.ExtractMessages()]);
}
}
- public Result DeleteCacheAsync() {
- try {
- if (Directory.Exists(_cacheDirectory)) {
- // Delete all files
- foreach (var file in Directory.GetFiles(_cacheDirectory)) {
- File.Delete(file);
+ public async Task DeleteCacheAsync() {
+ return await _lockManager.ExecuteWithLockAsync(() => {
+ try {
+ if (Directory.Exists(_cacheDirectory)) {
+ // Delete all files
+ 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
- foreach (var dir in Directory.GetDirectories(_cacheDirectory)) {
- Directory.Delete(dir, true);
+ else {
+ logger.LogWarning("Cache directory not found to clear.");
}
- _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 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 {
- _logger.LogWarning("Cache directory not found to clear.");
+ logger.LogWarning($"Cache file not found for account {accountId}");
}
- return Result.Ok();
- }
- catch (Exception ex) {
- var message = "Error clearing cache directory contents.";
- _logger.LogError(ex, message);
- return Result.InternalServerError([message, .. ex.ExtractMessages()]);
- }
+ return Task.FromResult(Result.Ok());
+ });
}
+ #region Helpers
+ ///
+ /// Generates the cache file path for the given account ID.
+ ///
+ 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
- public async Task> LoadAccountFromCacheAsync(Guid accountId) {
- return await _lockManager.ExecuteWithLockAsync(() => LoadFromCacheInternalAsync(accountId));
- }
-
- public async Task SaveToCacheAsync(Guid accountId, RegistrationCache cache) {
- return await _lockManager.ExecuteWithLockAsync(() => SaveToCacheInternalAsync(accountId, cache));
- }
-
- public async Task DeleteFromCacheAsync(Guid accountId) {
- return await _lockManager.ExecuteWithLockAsync(() => DeleteFromCacheInternal(accountId));
- }
-
public void Dispose() {
_lockManager.Dispose();
}
diff --git a/src/LetsEncryptServer/Services/CertsFlowService.cs b/src/MaksIT.Webapi/Services/CertsFlowService.cs
similarity index 68%
rename from src/LetsEncryptServer/Services/CertsFlowService.cs
rename to src/MaksIT.Webapi/Services/CertsFlowService.cs
index 7fb7edc..526139b 100644
--- a/src/LetsEncryptServer/Services/CertsFlowService.cs
+++ b/src/MaksIT.Webapi/Services/CertsFlowService.cs
@@ -3,9 +3,10 @@ using MaksIT.Results;
using MaksIT.LetsEncrypt.Entities;
using MaksIT.LetsEncrypt.Entities.LetsEncrypt;
using MaksIT.LetsEncrypt.Services;
+using MaksIT.Webapi.Abstractions.Services;
-namespace MaksIT.LetsEncryptServer.Services;
+namespace MaksIT.Webapi.Services;
public interface ICertsFlowService {
Result GetTermsOfService(Guid sessionId);
@@ -22,77 +23,62 @@ public interface ICertsFlowService {
Result AcmeChallenge(string fileName);
}
-public class CertsFlowService : ICertsFlowService {
- private readonly Configuration _appSettings;
- private readonly ILogger _logger;
- private readonly HttpClient _httpClient;
- private readonly ILetsEncryptService _letsEncryptService;
- private readonly ICacheService _cacheService;
- private readonly IAgentService _agentService;
- private readonly string _acmePath;
+public class CertsFlowService(
+ IOptions appSettings,
+ ILogger logger,
+ HttpClient httpClient,
+ ILetsEncryptService letsEncryptService,
+ ICacheService cacheService,
+ IAgentService agentService
+) : ServiceBase(
+ logger,
+ appSettings
+), ICertsFlowService {
- public CertsFlowService(
- IOptions appSettings,
- ILogger 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;
- }
+ private readonly string _acmePath = appSettings.Value.AcmeFolder;
public Result GetTermsOfService(Guid sessionId) {
- var result = _letsEncryptService.GetTermsOfServiceUri(sessionId);
+ var result = letsEncryptService.GetTermsOfServiceUri(sessionId);
if (!result.IsSuccess || result.Value == null)
return result;
var termsOfServiceUrl = result.Value;
try {
- var fileName = Path.GetFileName(new Uri(termsOfServiceUrl).LocalPath);
-
- var termsOfServicePdfPath = Path.Combine(_appSettings.DataFolder, fileName);
-
- // Clean up old PDF files except the current one
- foreach (var file in Directory.GetFiles(_appSettings.DataFolder, "*.pdf")) {
- if (!string.Equals(Path.GetFileName(file), fileName, StringComparison.OrdinalIgnoreCase)) {
- try {
- File.Delete(file);
- }
- catch { /* ignore */ }
- }
+ var fileName = Path.GetFileName(new Uri(termsOfServiceUrl).LocalPath);
+ 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)) {
+ try {
+ File.Delete(file);
+ }
+ catch { /* ignore */ }
}
-
- byte[] pdfBytes;
- if (File.Exists(termsOfServicePdfPath)) {
- pdfBytes = File.ReadAllBytes(termsOfServicePdfPath);
- } else {
- pdfBytes = _httpClient.GetByteArrayAsync(termsOfServiceUrl).GetAwaiter().GetResult();
- File.WriteAllBytes(termsOfServicePdfPath, pdfBytes);
- }
- var base64 = Convert.ToBase64String(pdfBytes);
- return Result.Ok(base64);
+ }
+ byte[] pdfBytes;
+ if (File.Exists(termsOfServicePdfPath)) {
+ pdfBytes = File.ReadAllBytes(termsOfServicePdfPath);
+ }
+ else {
+ pdfBytes = httpClient.GetByteArrayAsync(termsOfServiceUrl).GetAwaiter().GetResult();
+ File.WriteAllBytes(termsOfServicePdfPath, pdfBytes);
+ }
+ var base64 = Convert.ToBase64String(pdfBytes);
+ return Result.Ok(base64);
}
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.InternalServerError(null, $"Failed to download, cache, or convert Terms of Service PDF: {ex.Message}");
}
}
public async Task CompleteChallengesAsync(Guid sessionId) {
- return await _letsEncryptService.CompleteChallenges(sessionId);
+ return await letsEncryptService.CompleteChallenges(sessionId);
}
public async Task> ConfigureClientAsync(bool isStaging) {
var sessionId = Guid.NewGuid();
- var result = await _letsEncryptService.ConfigureClient(sessionId, isStaging);
+ var result = await letsEncryptService.ConfigureClient(sessionId, isStaging);
if (!result.IsSuccess)
return result.ToResultOfType(default);
return Result.Ok(sessionId);
@@ -100,13 +86,11 @@ public class CertsFlowService : ICertsFlowService {
public async Task> InitAsync(Guid sessionId, Guid? accountId, string description, string[] contacts) {
RegistrationCache? cache = null;
-
if (accountId == null) {
accountId = Guid.NewGuid();
}
else {
- var cacheResult = await _cacheService.LoadAccountFromCacheAsync(accountId.Value);
-
+ var cacheResult = await cacheService.LoadAccountFromCacheAsync(accountId.Value);
if (!cacheResult.IsSuccess || cacheResult.Value == null) {
accountId = Guid.NewGuid();
}
@@ -114,94 +98,76 @@ public class CertsFlowService : ICertsFlowService {
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)
return result.ToResultOfType(default);
-
return Result.Ok(accountId.Value);
}
public async Task?>> 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)
return orderResult.ToResultOfType?>(_ => null);
-
var challenges = new List();
-
foreach (var kvp in orderResult.Value) {
string[] splitToken = kvp.Value.Split('.');
File.WriteAllText(Path.Combine(_acmePath, splitToken[0]), kvp.Value);
challenges.Add(splitToken[0]);
}
-
return Result?>.Ok(challenges);
}
public async Task GetCertificatesAsync(Guid sessionId, string[] hostnames) {
foreach (var subject in hostnames) {
- var result = await _letsEncryptService.GetCertificate(sessionId, subject);
+ var result = await letsEncryptService.GetCertificate(sessionId, subject);
if (!result.IsSuccess)
return result;
Thread.Sleep(1000);
}
-
- var cacheResult = _letsEncryptService.GetRegistrationCache(sessionId);
+ var cacheResult = letsEncryptService.GetRegistrationCache(sessionId);
if (!cacheResult.IsSuccess || cacheResult.Value == null)
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)
return saveResult;
-
return Result.Ok();
}
public async Task GetOrderAsync(Guid sessionId, string[] hostnames) {
- return await _letsEncryptService.GetOrder(sessionId, hostnames);
+ return await letsEncryptService.GetOrder(sessionId, hostnames);
}
public async Task?>> ApplyCertificatesAsync(Guid accountId) {
- var cacheResult = await _cacheService.LoadAccountFromCacheAsync(accountId);
+ var cacheResult = await cacheService.LoadAccountFromCacheAsync(accountId);
if (!cacheResult.IsSuccess || cacheResult.Value?.CachedCerts == null)
return cacheResult.ToResultOfType?>(_ => null);
-
var cache = cacheResult.Value;
var results = cache.GetCertsPemPerHostname();
-
-
if (cache.IsDisabled)
return Result?>.BadRequest(null, $"Account {accountId} is disabled");
-
if (cache.IsStaging)
return Result?>.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)
return uploadResult.ToResultOfType?>(default);
-
- var reloadResult = await _agentService.ReloadService(_appSettings.Agent.ServiceToReload);
+ var reloadResult = await agentService.ReloadService(_appSettings.Agent.ServiceToReload);
if (!reloadResult.IsSuccess)
return reloadResult.ToResultOfType?>(default);
-
return Result?>.Ok(results);
}
public async Task RevokeCertificatesAsync(Guid sessionId, string[] 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)
return result;
}
-
- var cacheResult = _letsEncryptService.GetRegistrationCache(sessionId);
+ var cacheResult = letsEncryptService.GetRegistrationCache(sessionId);
if (!cacheResult.IsSuccess || cacheResult.Value == null)
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)
return saveResult;
-
return Result.Ok();
}
@@ -209,40 +175,31 @@ public class CertsFlowService : ICertsFlowService {
var sessionResult = await ConfigureClientAsync(isStaging);
if (!sessionResult.IsSuccess || sessionResult.Value == null)
return sessionResult;
-
var sessionId = sessionResult.Value.Value;
-
var initResult = await InitAsync(sessionId, accountId, description, contacts);
if (!initResult.IsSuccess || initResult.Value == null)
return initResult.ToResultOfType(_ => null);
-
if (accountId == null)
accountId = initResult.Value;
-
var challengesResult = await NewOrderAsync(sessionId, hostnames, challengeType);
if (!challengesResult.IsSuccess)
return challengesResult.ToResultOfType(_ => null);
-
if (challengesResult.Value?.Count > 0) {
var challengeResult = await CompleteChallengesAsync(sessionId);
if (!challengeResult.IsSuccess)
return challengeResult.ToResultOfType(default);
}
-
var getOrderResult = await GetOrderAsync(sessionId, hostnames);
if (!getOrderResult.IsSuccess)
return getOrderResult.ToResultOfType(default);
-
var certsResult = await GetCertificatesAsync(sessionId, hostnames);
if (!certsResult.IsSuccess)
return certsResult.ToResultOfType(default);
-
if (!isStaging) {
var applyCertsResult = await ApplyCertificatesAsync(accountId.Value);
if (!applyCertsResult.IsSuccess)
return applyCertsResult.ToResultOfType(_ => null);
}
-
return Result.Ok(initResult.Value);
}
@@ -250,27 +207,21 @@ public class CertsFlowService : ICertsFlowService {
var sessionResult = await ConfigureClientAsync(isStaging);
if (!sessionResult.IsSuccess || sessionResult.Value == null)
return sessionResult;
-
var sessionId = sessionResult.Value.Value;
-
var initResult = await InitAsync(sessionId, accountId, description, contacts);
if (!initResult.IsSuccess)
return initResult;
-
var revokeResult = await RevokeCertificatesAsync(sessionId, hostnames);
if (!revokeResult.IsSuccess)
return revokeResult;
-
return Result.Ok();
}
public Result AcmeChallenge(string fileName) {
DeleteExporedChallenges();
-
var challengePath = Path.Combine(_acmePath, fileName);
if (!File.Exists(challengePath))
return Result.NotFound(null);
-
var fileContent = File.ReadAllText(Path.Combine(_acmePath, fileName));
return Result.Ok(fileContent);
}
@@ -281,15 +232,13 @@ public class CertsFlowService : ICertsFlowService {
try {
var creationTime = File.GetCreationTime(file);
var timeDifference = currentDate - creationTime;
-
-
if (timeDifference.TotalDays > 1) {
File.Delete(file);
- _logger.LogInformation($"Deleted file: {file}");
+ logger.LogInformation($"Deleted file: {file}");
}
}
catch (Exception ex) {
- _logger.LogWarning(ex, "File cannot be deleted");
+ logger.LogWarning(ex, "File cannot be deleted");
}
}
}
diff --git a/src/LetsEncryptServer/Services/IdentityService.cs b/src/MaksIT.Webapi/Services/IdentityService.cs
similarity index 60%
rename from src/LetsEncryptServer/Services/IdentityService.cs
rename to src/MaksIT.Webapi/Services/IdentityService.cs
index b619e71..b11bba2 100644
--- a/src/LetsEncryptServer/Services/IdentityService.cs
+++ b/src/MaksIT.Webapi/Services/IdentityService.cs
@@ -1,16 +1,24 @@
-using MaksIT.Core.Security.JWT;
-using MaksIT.LetsEncryptServer.Domain;
-using MaksIT.Results;
+
using Microsoft.Extensions.Options;
-using Models.LetsEncryptServer.Identity.Login;
-using Models.LetsEncryptServer.Identity.Logout;
-using System.Linq.Dynamic.Core.Tokenizer;
-using System.Security.Claims;
+using MaksIT.Results;
+using MaksIT.Webapi.Domain;
+using MaksIT.Webapi.Authorization;
+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 {
+
+ #region Patch
+ Task> PatchUserAsync(JwtTokenData jwtTokenData, Guid id, PatchUserRequest requestData);
+ #endregion
+
#region Login/Refresh/Logout
Task> LoginAsync(LoginRequest requestData);
Task> RefreshTokenAsync(RefreshTokenRequest requestData);
@@ -19,12 +27,53 @@ public interface IIdentityService {
}
public class IdentityService(
- IOptions appsettings,
+ ILogger logger,
+ IOptions appSettings,
ISettingsService settingsService
-) : IIdentityService {
+) : ServiceBase(logger, appSettings), IIdentityService {
+ #region Patch
+ public async Task> PatchUserAsync(JwtTokenData jwtTokenData, Guid id, PatchUserRequest requestData) {
+ var loadSettingsResult = await settingsService.LoadAsync();
+ if (!loadSettingsResult.IsSuccess || loadSettingsResult.Value == null) {
+ return loadSettingsResult.ToResultOfType(_ => null);
+ }
+
+ var settings = loadSettingsResult.Value;
+
+ var userResult = settings.GetUserById(id);
+ if (!userResult.IsSuccess || userResult.Value == null)
+ return userResult.ToResultOfType(_ => 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(nameof(requestData.Password));
+ user.SetPassword(requestData.Password, _appSettings.Auth.Pepper);
+ break;
+ default:
+ return UnsupportedPatchOperationResponse();
+ }
+ }
+ #endregion
+
+ settings.UpsertUser(user);
+
+ var saveSettingsResult = await settingsService.SaveAsync(settings);
+ if (!saveSettingsResult.IsSuccess)
+ return saveSettingsResult.ToResultOfType(default);
+
+ return Result.Ok(new UserResponse {
+
+ });
+
+ }
+ #endregion
- private readonly Configuration _appSettings = appsettings.Value;
#region Login/Refresh/Logout
public async Task> LoginAsync(LoginRequest requestData) {
@@ -90,59 +139,59 @@ public class IdentityService(
public async Task> RefreshTokenAsync(RefreshTokenRequest requestData) {
var loadSettingsResult = await settingsService.LoadAsync();
if (!loadSettingsResult.IsSuccess || loadSettingsResult.Value == null)
- return loadSettingsResult.ToResultOfType(_ => null);
+ return loadSettingsResult.ToResultOfType(_ => null);
var settings = loadSettingsResult.Value;
var userResult = settings.GetByRefreshToken(requestData.RefreshToken);
if (!userResult.IsSuccess || userResult.Value == null)
- return Result.Unauthorized(null, "Invalid refresh token.");
+ return Result.Unauthorized(null, "Invalid refresh token.");
var user = userResult.Value.RemoveRevokedJwtTokens();
var tokenDomain = user.JwtTokens.SingleOrDefault(t => t.RefreshToken == requestData.RefreshToken);
if (tokenDomain == null)
- return Result.Unauthorized(null, "Invalid refresh token.");
+ return Result.Unauthorized(null, "Invalid refresh token.");
// Token is still valid
if (DateTime.UtcNow <= tokenDomain.ExpiresAt) {
- user.SetLastLogin();
- settings.UpsertUser(user);
+ user.SetLastLogin();
+ settings.UpsertUser(user);
- var saveResult = await settingsService.SaveAsync(settings);
- if (!saveResult.IsSuccess)
- return saveResult.ToResultOfType(default);
+ var saveResult = await settingsService.SaveAsync(settings);
+ if (!saveResult.IsSuccess)
+ return saveResult.ToResultOfType(default);
- return Result.Ok(new LoginResponse {
- TokenType = tokenDomain.TokenType,
- Token = tokenDomain.Token,
- ExpiresAt = tokenDomain.ExpiresAt,
- RefreshToken = tokenDomain.RefreshToken,
- RefreshTokenExpiresAt = tokenDomain.RefreshTokenExpiresAt
- });
+ return Result.Ok(new LoginResponse {
+ TokenType = tokenDomain.TokenType,
+ Token = tokenDomain.Token,
+ ExpiresAt = tokenDomain.ExpiresAt,
+ RefreshToken = tokenDomain.RefreshToken,
+ RefreshTokenExpiresAt = tokenDomain.RefreshTokenExpiresAt
+ });
}
// Refresh token expired
if (DateTime.UtcNow > tokenDomain.RefreshTokenExpiresAt) {
- user.RemoveJwtToken(tokenDomain.Id);
- return Result.Unauthorized(null, "Refresh token has expired.");
+ user.RemoveJwtToken(tokenDomain.Id);
+ return Result.Unauthorized(null, "Refresh token has expired.");
}
// Refresh token is valid - generate new tokens
if (!JwtGenerator.TryGenerateToken(new JWTTokenGenerateRequest {
- Secret = _appSettings.Auth.Secret,
- Issuer = _appSettings.Auth.Issuer,
- Audience = _appSettings.Auth.Audience,
- Expiration = _appSettings.Auth.Expiration,
- UserId = user.Id.ToString(),
- Username = user.Name,
+ Secret = _appSettings.Auth.Secret,
+ Issuer = _appSettings.Auth.Issuer,
+ Audience = _appSettings.Auth.Audience,
+ Expiration = _appSettings.Auth.Expiration,
+ UserId = user.Id.ToString(),
+ Username = user.Name,
}, out (string token, JWTTokenClaims claims)? tokenData, out string? errorMessage))
- return Result.InternalServerError(null, errorMessage);
+ return Result.InternalServerError(null, errorMessage);
var (token, claims) = tokenData.Value;
if (claims.IssuedAt == null || claims.ExpiresAt == null)
- return Result.InternalServerError(null, "Token claims are missing required fields.");
+ return Result.InternalServerError(null, "Token claims are missing required fields.");
string refreshToken = JwtGenerator.GenerateRefreshToken();
@@ -156,14 +205,14 @@ public class IdentityService(
var writeResult = await settingsService.SaveAsync(settings);
if (!writeResult.IsSuccess)
- return writeResult.ToResultOfType(default);
+ return writeResult.ToResultOfType(default);
return Result.Ok(new LoginResponse {
- TokenType = tokenDomain.TokenType,
- Token = tokenDomain.Token,
- ExpiresAt = claims.ExpiresAt.Value,
- RefreshToken = tokenDomain.RefreshToken,
- RefreshTokenExpiresAt = tokenDomain.RefreshTokenExpiresAt
+ TokenType = tokenDomain.TokenType,
+ Token = tokenDomain.Token,
+ ExpiresAt = claims.ExpiresAt.Value,
+ RefreshToken = tokenDomain.RefreshToken,
+ RefreshTokenExpiresAt = tokenDomain.RefreshTokenExpiresAt
});
}
diff --git a/src/MaksIT.Webapi/Services/SettingsService.cs b/src/MaksIT.Webapi/Services/SettingsService.cs
new file mode 100644
index 0000000..e4d548a
--- /dev/null
+++ b/src/MaksIT.Webapi/Services/SettingsService.cs
@@ -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> LoadAsync();
+ Task SaveAsync(Settings settings);
+}
+
+public class SettingsService(
+ ILogger logger,
+ IOptions 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> LoadInternalAsync() {
+ try {
+ if (!File.Exists(_settingsPath))
+ return Result.Ok(new Settings());
+
+ var json = await File.ReadAllTextAsync(_settingsPath);
+ var settingsDto = json.ToObject();
+ if (settingsDto == null)
+ return Result.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.Ok(settings);
+ }
+ catch (Exception ex) {
+ var message = "Error loading settings file.";
+ _logger.LogError(ex, message);
+ return Result.InternalServerError(null, [message, .. ex.ExtractMessages()]);
+ }
+ }
+
+ private async Task 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> LoadAsync() {
+ return await _lockManager.ExecuteWithLockAsync(() => LoadInternalAsync());
+ }
+
+ public async Task SaveAsync(Settings settings) {
+ return await _lockManager.ExecuteWithLockAsync(() => SaveInternalAsync(settings));
+ }
+
+ public void Dispose() {
+ _lockManager.Dispose();
+ }
+}
diff --git a/src/LetsEncryptServer/appsettings.Development.json b/src/MaksIT.Webapi/appsettings.Development.json
similarity index 100%
rename from src/LetsEncryptServer/appsettings.Development.json
rename to src/MaksIT.Webapi/appsettings.Development.json
diff --git a/src/LetsEncryptServer/appsettings.json b/src/MaksIT.Webapi/appsettings.json
similarity index 63%
rename from src/LetsEncryptServer/appsettings.json
rename to src/MaksIT.Webapi/appsettings.json
index b88ded1..39904c7 100644
--- a/src/LetsEncryptServer/appsettings.json
+++ b/src/MaksIT.Webapi/appsettings.json
@@ -8,31 +8,29 @@
"AllowedHosts": "*",
"Configuration": {
- "SettingsFile": "/data/settings.json",
+
"Auth": {
"Secret": "",
- "Issuer": "",
- "Audience": "",
- "Expiration": 60,
- "RefreshExpiration": 120,
-
-
- "Pepper": ""
+ "Pepper": "",
+ "Issuer": "LetsEncryptServer",
+ "Audience": "LetsEncryptServerUsers",
+ "Expiration": 15, // Access token lifetime in minutes (default: 15 minutes)
+ "RefreshExpiration": 180 // Refresh token lifetime in days (default: 180 days)
},
- "Production": "https://acme-v02.api.letsencrypt.org/directory",
- "Staging": "https://acme-staging-v02.api.letsencrypt.org/directory",
-
- "CacheFolder": "/cache",
- "AcmeFolder": "/acme",
- "DataFolder": "/data",
-
"Agent": {
"AgentHostname": "",
"AgentPort": 9000,
"AgentKey": "",
-
"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"
}
}
diff --git a/src/Models/Agent/Requests/CertsUploadRequest.cs b/src/Models/Agent/Requests/CertsUploadRequest.cs
index d2dbb7c..c732b40 100644
--- a/src/Models/Agent/Requests/CertsUploadRequest.cs
+++ b/src/Models/Agent/Requests/CertsUploadRequest.cs
@@ -1,7 +1,8 @@
-namespace MaksIT.Models.Agent.Requests {
- public class CertsUploadRequest {
+using MaksIT.Core.Abstractions.Webapi;
- public Dictionary Certs { get; set; }
- }
+namespace MaksIT.Models.Agent.Requests;
+
+public class CertsUploadRequest : RequestModelBase {
+ public Dictionary Certs { get; set; }
}
diff --git a/src/Models/Agent/Requests/ServiceReloadRequest.cs b/src/Models/Agent/Requests/ServiceReloadRequest.cs
index 9921ba9..bbff92c 100644
--- a/src/Models/Agent/Requests/ServiceReloadRequest.cs
+++ b/src/Models/Agent/Requests/ServiceReloadRequest.cs
@@ -1,11 +1,8 @@
-using System;
-using System.Collections.Generic;
-using System.Linq;
-using System.Text;
-using System.Threading.Tasks;
+using MaksIT.Core.Abstractions.Webapi;
-namespace MaksIT.Models.Agent.Requests {
- public class ServiceReloadRequest {
- public string ServiceName { get; set; }
- }
+
+namespace MaksIT.Models.Agent.Requests;
+
+public class ServiceReloadRequest : RequestModelBase {
+ public string ServiceName { get; set; }
}
diff --git a/src/Models/Agent/Responses/HelloWorldResponse.cs b/src/Models/Agent/Responses/HelloWorldResponse.cs
index c9cfdbc..3f46d89 100644
--- a/src/Models/Agent/Responses/HelloWorldResponse.cs
+++ b/src/Models/Agent/Responses/HelloWorldResponse.cs
@@ -1,10 +1,8 @@
-using System;
-using System.Collections.Generic;
-using System.Linq;
-using System.Text;
-using System.Threading.Tasks;
+using MaksIT.Core.Abstractions.Webapi;
-namespace Models.Agent.Responses;
-public class HelloWorldResponse {
+
+namespace MaksIT.Models.Agent.Responses;
+
+public class HelloWorldResponse : ResponseModelBase {
public string Message { get; set; }
}
diff --git a/src/Models/LetsEncryptServer/Account/Requests/PatchAccountRequest.cs b/src/Models/LetsEncryptServer/Account/Requests/PatchAccountRequest.cs
index a322f6c..97c60ab 100644
--- a/src/Models/LetsEncryptServer/Account/Requests/PatchAccountRequest.cs
+++ b/src/Models/LetsEncryptServer/Account/Requests/PatchAccountRequest.cs
@@ -4,12 +4,8 @@
namespace MaksIT.Models.LetsEncryptServer.Account.Requests;
public class PatchAccountRequest : PatchRequestModelBase {
-
public string? Description { get; set; }
-
public bool? IsDisabled { get; set; }
-
public List? Contacts { get; set; }
-
public List? Hostnames { get; set; }
}
diff --git a/src/Models/LetsEncryptServer/Account/Requests/PatchHostnameRequest.cs b/src/Models/LetsEncryptServer/Account/Requests/PatchHostnameRequest.cs
index 3d72f48..9245a5b 100644
--- a/src/Models/LetsEncryptServer/Account/Requests/PatchHostnameRequest.cs
+++ b/src/Models/LetsEncryptServer/Account/Requests/PatchHostnameRequest.cs
@@ -2,9 +2,9 @@
namespace MaksIT.Models.LetsEncryptServer.Account.Requests;
+
public class PatchHostnameRequest : PatchRequestModelBase {
public string? Hostname { get; set; }
-
public bool? IsDisabled { get; set; }
}
diff --git a/src/Models/LetsEncryptServer/Account/Requests/PostAccountRequest.cs b/src/Models/LetsEncryptServer/Account/Requests/PostAccountRequest.cs
index dd8eb7d..a816ef1 100644
--- a/src/Models/LetsEncryptServer/Account/Requests/PostAccountRequest.cs
+++ b/src/Models/LetsEncryptServer/Account/Requests/PostAccountRequest.cs
@@ -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;
+
public class PostAccountRequest : RequestModelBase {
public required string Description { get; set; }
public required string[] Contacts { get; set; }
@@ -11,15 +12,15 @@ public class PostAccountRequest : RequestModelBase {
public override IEnumerable Validate(ValidationContext validationContext) {
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)
- 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)
- 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")
- yield return new ValidationResult("ChallengeType is required", new[] { nameof(ChallengeType) });
+ yield return new ValidationResult("ChallengeType is required", [nameof(ChallengeType)]);
}
}
diff --git a/src/Models/LetsEncryptServer/Account/Responses/GetAccountResponse.cs b/src/Models/LetsEncryptServer/Account/Responses/GetAccountResponse.cs
index 1c5d5e5..ef8d586 100644
--- a/src/Models/LetsEncryptServer/Account/Responses/GetAccountResponse.cs
+++ b/src/Models/LetsEncryptServer/Account/Responses/GetAccountResponse.cs
@@ -1,23 +1,14 @@
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 string? ChallengeType { get; set; }
-
- public GetHostnameResponse[]? Hostnames { get; set; }
-
- public required bool IsStaging { get; set; }
- }
+public class GetAccountResponse : ResponseModelBase {
+ public Guid AccountId { get; set; }
+ public required bool IsDisabled { get; set; }
+ public string? Description { get; set; }
+ public required string[] Contacts { get; set; }
+ public string? ChallengeType { get; set; }
+ public GetHostnameResponse[]? Hostnames { get; set; }
+ public required bool IsStaging { get; set; }
}
diff --git a/src/Models/LetsEncryptServer/Account/Responses/GetHostnameResponse.cs b/src/Models/LetsEncryptServer/Account/Responses/GetHostnameResponse.cs
index 4067c05..3080d31 100644
--- a/src/Models/LetsEncryptServer/Account/Responses/GetHostnameResponse.cs
+++ b/src/Models/LetsEncryptServer/Account/Responses/GetHostnameResponse.cs
@@ -1,16 +1,11 @@
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; }
}
diff --git a/src/Models/LetsEncryptServer/CertsFlow/Requests/ConfigureClientRequest.cs b/src/Models/LetsEncryptServer/CertsFlow/Requests/ConfigureClientRequest.cs
index 938bcb8..f955c27 100644
--- a/src/Models/LetsEncryptServer/CertsFlow/Requests/ConfigureClientRequest.cs
+++ b/src/Models/LetsEncryptServer/CertsFlow/Requests/ConfigureClientRequest.cs
@@ -1,12 +1,8 @@
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 {
- public bool IsStaging { get; set; }
- }
+
+namespace MaksIT.Models.LetsEncryptServer.CertsFlow.Requests;
+
+public class ConfigureClientRequest : RequestModelBase {
+ public bool IsStaging { get; set; }
}
diff --git a/src/Models/LetsEncryptServer/CertsFlow/Requests/GetCerificatesRequest.cs b/src/Models/LetsEncryptServer/CertsFlow/Requests/GetCerificatesRequest.cs
index afb9494..58b80c5 100644
--- a/src/Models/LetsEncryptServer/CertsFlow/Requests/GetCerificatesRequest.cs
+++ b/src/Models/LetsEncryptServer/CertsFlow/Requests/GetCerificatesRequest.cs
@@ -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 Validate(ValidationContext validationContext) {
- if (Hostnames == null || Hostnames.Length == 0)
- yield return new ValidationResult("Hostnames is required", new[] { nameof(Hostnames) });
- }
+namespace MaksIT.Models.LetsEncryptServer.CertsFlow.Requests;
+
+public class GetCertificatesRequest : RequestModelBase {
+ public required string[] Hostnames { get; set; }
+
+ public override IEnumerable Validate(ValidationContext validationContext) {
+ if (Hostnames == null || Hostnames.Length == 0)
+ yield return new ValidationResult("Hostnames is required", new[] { nameof(Hostnames) });
}
}
diff --git a/src/Models/LetsEncryptServer/CertsFlow/Requests/GetOrderRequest.cs b/src/Models/LetsEncryptServer/CertsFlow/Requests/GetOrderRequest.cs
index b610987..7225759 100644
--- a/src/Models/LetsEncryptServer/CertsFlow/Requests/GetOrderRequest.cs
+++ b/src/Models/LetsEncryptServer/CertsFlow/Requests/GetOrderRequest.cs
@@ -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 Validate(ValidationContext validationContext) {
- if (Hostnames == null || Hostnames.Length == 0)
- yield return new ValidationResult("Hostnames is required", new[] { nameof(Hostnames) });
- }
+namespace MaksIT.Models.LetsEncryptServer.CertsFlow.Requests;
+
+public class GetOrderRequest : RequestModelBase {
+ public required string[] Hostnames { get; set; }
+
+ public override IEnumerable Validate(ValidationContext validationContext) {
+ if (Hostnames == null || Hostnames.Length == 0)
+ yield return new ValidationResult("Hostnames is required", new[] { nameof(Hostnames) });
}
}
diff --git a/src/Models/LetsEncryptServer/CertsFlow/Requests/InitRequest.cs b/src/Models/LetsEncryptServer/CertsFlow/Requests/InitRequest.cs
index f85dc53..4062946 100644
--- a/src/Models/LetsEncryptServer/CertsFlow/Requests/InitRequest.cs
+++ b/src/Models/LetsEncryptServer/CertsFlow/Requests/InitRequest.cs
@@ -1,22 +1,18 @@
-using MaksIT.Core.Abstractions.Webapi;
-using System;
-using System.Collections.Generic;
-using System.ComponentModel.DataAnnotations;
-using System.Linq;
-using System.Text;
-using System.Threading.Tasks;
+using System.ComponentModel.DataAnnotations;
+using MaksIT.Core.Abstractions.Webapi;
-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 Validate(ValidationContext validationContext) {
- if (string.IsNullOrWhiteSpace(Description))
- yield return new ValidationResult("Description is required", new[] { nameof(Description) });
+namespace MaksIT.Models.LetsEncryptServer.CertsFlow.Requests;
- if (Contacts == null || Contacts.Length == 0)
- yield return new ValidationResult("Contacts is required", new[] { nameof(Contacts) });
- }
+public class InitRequest : RequestModelBase {
+ public required string Description { get; set; }
+ public required string[] Contacts { get; set; }
+
+ public override IEnumerable 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) });
}
}
diff --git a/src/Models/LetsEncryptServer/CertsFlow/Requests/NewOrderRequest.cs b/src/Models/LetsEncryptServer/CertsFlow/Requests/NewOrderRequest.cs
index c6edba4..1aaca4b 100644
--- a/src/Models/LetsEncryptServer/CertsFlow/Requests/NewOrderRequest.cs
+++ b/src/Models/LetsEncryptServer/CertsFlow/Requests/NewOrderRequest.cs
@@ -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 Validate(ValidationContext validationContext) {
- if (Hostnames == null || Hostnames.Length == 0)
- yield return new ValidationResult("Hostnames is required", new[] { nameof(Hostnames) });
+public class NewOrderRequest : RequestModelBase {
+ public required string[] Hostnames { get; set; }
+ public required string ChallengeType { get; set; }
- if (string.IsNullOrWhiteSpace(ChallengeType) && ChallengeType != "http-01")
- yield return new ValidationResult("ChallengeType is required", new[] { nameof(ChallengeType) });
- }
+ public override IEnumerable Validate(ValidationContext validationContext) {
+ 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) });
}
}
diff --git a/src/Models/LetsEncryptServer/CertsFlow/Requests/RevokeCertificatesRequest.cs b/src/Models/LetsEncryptServer/CertsFlow/Requests/RevokeCertificatesRequest.cs
index 9bb1d57..fa6990e 100644
--- a/src/Models/LetsEncryptServer/CertsFlow/Requests/RevokeCertificatesRequest.cs
+++ b/src/Models/LetsEncryptServer/CertsFlow/Requests/RevokeCertificatesRequest.cs
@@ -1,19 +1,14 @@
-using MaksIT.Core.Abstractions.Webapi;
-using System;
-using System.Collections.Generic;
-using System.ComponentModel.DataAnnotations;
-using System.Linq;
-using System.Text;
-using System.Threading.Tasks;
+using System.ComponentModel.DataAnnotations;
+using MaksIT.Core.Abstractions.Webapi;
-namespace MaksIT.Models.LetsEncryptServer.CertsFlow.Requests {
- public class RevokeCertificatesRequest : RequestModelBase {
-
- public required string [] Hostnames { get; set; }
- public override IEnumerable Validate(ValidationContext validationContext) {
- if (Hostnames == null || Hostnames.Length == 0)
- yield return new ValidationResult("Hostnames is required", new[] { nameof(Hostnames) });
- }
+namespace MaksIT.Models.LetsEncryptServer.CertsFlow.Requests;
+
+public class RevokeCertificatesRequest : RequestModelBase {
+ public required string [] Hostnames { get; set; }
+
+ public override IEnumerable Validate(ValidationContext validationContext) {
+ if (Hostnames == null || Hostnames.Length == 0)
+ yield return new ValidationResult("Hostnames is required", new[] { nameof(Hostnames) });
}
}
diff --git a/src/Models/LetsEncryptServer/Identity/Login/LoginRequest.cs b/src/Models/LetsEncryptServer/Identity/Login/LoginRequest.cs
index 684861a..b472e54 100644
--- a/src/Models/LetsEncryptServer/Identity/Login/LoginRequest.cs
+++ b/src/Models/LetsEncryptServer/Identity/Login/LoginRequest.cs
@@ -1,7 +1,7 @@
using MaksIT.Core.Abstractions.Webapi;
-namespace Models.LetsEncryptServer.Identity.Login;
+namespace MaksIT.Models.LetsEncryptServer.Identity.Login;
public class LoginRequest : RequestModelBase {
public required string Username { get; set; }
diff --git a/src/Models/LetsEncryptServer/Identity/Login/LoginResponse.cs b/src/Models/LetsEncryptServer/Identity/Login/LoginResponse.cs
index 33ce2a7..32c66f0 100644
--- a/src/Models/LetsEncryptServer/Identity/Login/LoginResponse.cs
+++ b/src/Models/LetsEncryptServer/Identity/Login/LoginResponse.cs
@@ -1,10 +1,9 @@
using MaksIT.Core.Abstractions.Webapi;
-namespace Models.LetsEncryptServer.Identity.Login;
+namespace MaksIT.Models.LetsEncryptServer.Identity.Login;
public class LoginResponse : ResponseModelBase {
-
public required string TokenType { get; set; }
public required string Token { get; set; }
public required DateTime ExpiresAt { get; set; }
diff --git a/src/Models/LetsEncryptServer/Identity/Login/RefreshTokenRequest.cs b/src/Models/LetsEncryptServer/Identity/Login/RefreshTokenRequest.cs
index 93b15c1..f9a5455 100644
--- a/src/Models/LetsEncryptServer/Identity/Login/RefreshTokenRequest.cs
+++ b/src/Models/LetsEncryptServer/Identity/Login/RefreshTokenRequest.cs
@@ -1,7 +1,7 @@
using MaksIT.Core.Abstractions.Webapi;
-namespace Models.LetsEncryptServer.Identity.Login;
+namespace MaksIT.Models.LetsEncryptServer.Identity.Login;
public class RefreshTokenRequest : RequestModelBase {
public required string RefreshToken { get; set; } // The refresh token used for renewing access
diff --git a/src/Models/LetsEncryptServer/Identity/Logout/LogoutRequest.cs b/src/Models/LetsEncryptServer/Identity/Logout/LogoutRequest.cs
index c8b79e0..1814dfa 100644
--- a/src/Models/LetsEncryptServer/Identity/Logout/LogoutRequest.cs
+++ b/src/Models/LetsEncryptServer/Identity/Logout/LogoutRequest.cs
@@ -1,7 +1,7 @@
using MaksIT.Core.Abstractions.Webapi;
-namespace Models.LetsEncryptServer.Identity.Logout;
+namespace MaksIT.Models.LetsEncryptServer.Identity.Logout;
public class LogoutRequest : RequestModelBase {
public required string Token { get; set; }
diff --git a/src/Models/LetsEncryptServer/Identity/User/PatchUserRequest.cs b/src/Models/LetsEncryptServer/Identity/User/PatchUserRequest.cs
new file mode 100644
index 0000000..bf128ab
--- /dev/null
+++ b/src/Models/LetsEncryptServer/Identity/User/PatchUserRequest.cs
@@ -0,0 +1,8 @@
+using MaksIT.Core.Abstractions.Webapi;
+
+
+namespace MaksIT.Models.LetsEncryptServer.Identity.User;
+
+public class PatchUserRequest : PatchRequestModelBase {
+ public string? Password { get; set; }
+}
diff --git a/src/Models/LetsEncryptServer/Identity/User/UserResponse.cs b/src/Models/LetsEncryptServer/Identity/User/UserResponse.cs
new file mode 100644
index 0000000..99cffd7
--- /dev/null
+++ b/src/Models/LetsEncryptServer/Identity/User/UserResponse.cs
@@ -0,0 +1,7 @@
+using MaksIT.Core.Abstractions.Webapi;
+
+
+namespace MaksIT.Models.LetsEncryptServer.Identity.User;
+
+public class UserResponse : ResponseModelBase {
+}
diff --git a/src/Models/Models.csproj b/src/Models/MaksIT.Models.csproj
similarity index 84%
rename from src/Models/Models.csproj
rename to src/Models/MaksIT.Models.csproj
index 32b2a26..ff6c3e0 100644
--- a/src/Models/Models.csproj
+++ b/src/Models/MaksIT.Models.csproj
@@ -11,7 +11,7 @@
-
+
diff --git a/src/ReverseProxy/ReverseProxy.csproj b/src/ReverseProxy/ReverseProxy.csproj
index 36f9dcb..a9d42fe 100644
--- a/src/ReverseProxy/ReverseProxy.csproj
+++ b/src/ReverseProxy/ReverseProxy.csproj
@@ -8,7 +8,7 @@
-
+
diff --git a/src/docker-compose.yml b/src/docker-compose.yml
index 55f51a8..38d85f6 100644
--- a/src/docker-compose.yml
+++ b/src/docker-compose.yml
@@ -15,4 +15,4 @@ services:
image: ${DOCKER_REGISTRY-}certs-ui-server
build:
context: .
- dockerfile: LetsEncryptServer/Dockerfile
+ dockerfile: MaksIT.Webapi/Dockerfile
diff --git a/src/helm/templates/NOTES.txt b/src/helm/templates/NOTES.txt
index 739f4e2..a5978bd 100644
--- a/src/helm/templates/NOTES.txt
+++ b/src/helm/templates/NOTES.txt
@@ -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.
- **Persistence**:
- PVCs are created for `/acme` and `/cache` directories.
+ PVCs are created for `/acme`, `/cache` and `/data` directories.
------------------------------------------------------------
## Uninstall
To remove all resources created by this chart:
```
-helm uninstall {{ .Release.Name }}
+helm uninstall {{ .Release.Name }} -n {{ .Release.Name }}
```
------------------------------------------------------------
diff --git a/src/helm/values.yaml b/src/helm/values.yaml
index 9c0ca7b..35be399 100644
--- a/src/helm/values.yaml
+++ b/src/helm/values.yaml
@@ -38,17 +38,29 @@ components:
storageClass: local-path
size: 50Mi
accessModes: [ReadWriteOnce]
+ - name: data
+ mountPath: /data
+ type: pvc
+ pvc:
+ create: true
+ storageClass: local-path
+ size: 50Mi
+ accessModes: [ReadWriteOnce]
secretsFile:
key: appsecrets.json
mountPath: /secrets/appsecrets.json
content: |
{
+ "Auth": {
+ "Secret": "",
+ "Pepper": ""
+ },
"Agent": {
- }
+ "AgentKey": ""
+ },
}
keep: true
forceUpdate: false
-
configMapFile:
key: appsettings.json
mountPath: /configMap/appsettings.json
@@ -61,17 +73,26 @@ components:
}
},
"Configuration": {
- "Production": "https://acme-v02.api.letsencrypt.org/directory",
- "Staging": "https://acme-staging-v02.api.letsencrypt.org/directory",
-
- "CacheFolder": "/cache",
- "AcmeFolder": "/acme",
+ "Auth": {
+ "Issuer": "",
+ "Audience": "",
+ "Expiration": 15, // Access token lifetime in minutes (default: 15 minutes)
+ "RefreshExpiration": 180, // Refresh token lifetime in days (default: 180 days)
+ },
"Agent": {
"AgentHostname": "http://websrv0001.corp.maks-it.com",
"AgentPort": 5000,
"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