mirror of
https://github.com/MAKS-IT-COM/maksit-certs-ui.git
synced 2025-12-31 04:00:03 +01:00
(feature): terms of service
This commit is contained in:
parent
105ca2aa70
commit
4495b2209c
@ -1,15 +1,16 @@
|
|||||||
# LetsEncrypt C# Client by Maks-IT.com
|
# LetsEncrypt C# Client by MaksIT
|
||||||
|
|
||||||
Simple client to obtain Let's Encrypt HTTPS certificates developed with .net core and curently works only with http challange
|
Simple client to obtain Let's Encrypt HTTPS certificates developed with .net core and currently works only with http challenge
|
||||||
|
|
||||||
## Versions History
|
## Versions History
|
||||||
|
|
||||||
* 29 Jun, 2019 - V1.0
|
* 29 Jun, 2019 - V1.0
|
||||||
* 01 Nov, 2019 - V2.0 (Dependency Injection pattern impelemtation)
|
* 01 Nov, 2019 - V2.0 (Dependency Injection pattern implementation)
|
||||||
* 31 May, 2024 - V3.0 (Webapi and containerization)
|
* 31 May, 2024 - V3.0 (Webapi and containerization)
|
||||||
* 11 Aug, 2024 - V3.1 (Release)
|
* 11 Aug, 2024 - V3.1 (Release)
|
||||||
* 11 Sep, 2025 - V3.2 New WebUI with authentication
|
* 11 Sep, 2025 - V3.2 New WebUI with authentication
|
||||||
|
* 15 Nov, 2025 - V3.3 Pre release
|
||||||
|
*
|
||||||
## Haproxy configuration
|
## Haproxy configuration
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
|
|||||||
@ -5,8 +5,15 @@
|
|||||||
<Nullable>enable</Nullable>
|
<Nullable>enable</Nullable>
|
||||||
<ImplicitUsings>enable</ImplicitUsings>
|
<ImplicitUsings>enable</ImplicitUsings>
|
||||||
<DockerDefaultTargetOS>Linux</DockerDefaultTargetOS>
|
<DockerDefaultTargetOS>Linux</DockerDefaultTargetOS>
|
||||||
|
<PackageReadmeFile>README.md</PackageReadmeFile>
|
||||||
|
<PackageLicenseFile>LICENSE.md</PackageLicenseFile>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<None Include="../../README.md" Pack="true" PackagePath="" />
|
||||||
|
<None Include="../../LICENSE.md" Pack="true" PackagePath="" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="Microsoft.VisualStudio.Azure.Containers.Tools.Targets" Version="1.22.1" />
|
<PackageReference Include="Microsoft.VisualStudio.Azure.Containers.Tools.Targets" Version="1.22.1" />
|
||||||
<PackageReference Include="Swashbuckle.AspNetCore" Version="10.0.1" />
|
<PackageReference Include="Swashbuckle.AspNetCore" Version="10.0.1" />
|
||||||
|
|||||||
@ -5,8 +5,15 @@
|
|||||||
<ImplicitUsings>enable</ImplicitUsings>
|
<ImplicitUsings>enable</ImplicitUsings>
|
||||||
<Nullable>enable</Nullable>
|
<Nullable>enable</Nullable>
|
||||||
<RootNamespace>MaksIT.$(MSBuildProjectName.Replace(" ", "_"))</RootNamespace>
|
<RootNamespace>MaksIT.$(MSBuildProjectName.Replace(" ", "_"))</RootNamespace>
|
||||||
|
<PackageReadmeFile>README.md</PackageReadmeFile>
|
||||||
|
<PackageLicenseFile>LICENSE.md</PackageLicenseFile>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<None Include="../../README.md" Pack="true" PackagePath="" />
|
||||||
|
<None Include="../../LICENSE.md" Pack="true" PackagePath="" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="MaksIT.Core" Version="1.5.9" />
|
<PackageReference Include="MaksIT.Core" Version="1.5.9" />
|
||||||
<PackageReference Include="MaksIT.Results" Version="1.1.1" />
|
<PackageReference Include="MaksIT.Results" Version="1.1.1" />
|
||||||
|
|||||||
@ -26,9 +26,9 @@ using System.Text;
|
|||||||
namespace MaksIT.LetsEncrypt.Services;
|
namespace MaksIT.LetsEncrypt.Services;
|
||||||
|
|
||||||
public interface ILetsEncryptService {
|
public interface ILetsEncryptService {
|
||||||
|
Result<RegistrationCache?> GetRegistrationCache(Guid sessionId);
|
||||||
Task<Result> ConfigureClient(Guid sessionId, bool isStaging);
|
Task<Result> ConfigureClient(Guid sessionId, bool isStaging);
|
||||||
Task<Result> Init(Guid sessionId,Guid accountId, string description, string[] contacts, RegistrationCache? registrationCache);
|
Task<Result> Init(Guid sessionId,Guid accountId, string description, string[] contacts, RegistrationCache? registrationCache);
|
||||||
Result<RegistrationCache?> GetRegistrationCache(Guid sessionId);
|
|
||||||
Result<string?> GetTermsOfServiceUri(Guid sessionId);
|
Result<string?> GetTermsOfServiceUri(Guid sessionId);
|
||||||
Task<Result<Dictionary<string, string>?>> NewOrder(Guid sessionId, string[] hostnames, string challengeType);
|
Task<Result<Dictionary<string, string>?>> NewOrder(Guid sessionId, string[] hostnames, string challengeType);
|
||||||
Task<Result> CompleteChallenges(Guid sessionId);
|
Task<Result> CompleteChallenges(Guid sessionId);
|
||||||
@ -59,7 +59,14 @@ public class LetsEncryptService : ILetsEncryptService {
|
|||||||
_memoryCache = cache;
|
_memoryCache = cache;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public Result<RegistrationCache?> GetRegistrationCache(Guid sessionId) {
|
||||||
|
var state = GetOrCreateState(sessionId);
|
||||||
|
|
||||||
|
if (state?.Cache == null)
|
||||||
|
return Result<RegistrationCache?>.InternalServerError(null);
|
||||||
|
|
||||||
|
return Result<RegistrationCache?>.Ok(state.Cache);
|
||||||
|
}
|
||||||
|
|
||||||
#region ConfigureClient
|
#region ConfigureClient
|
||||||
public async Task<Result> ConfigureClient(Guid sessionId, bool isStaging) {
|
public async Task<Result> ConfigureClient(Guid sessionId, bool isStaging) {
|
||||||
@ -205,15 +212,6 @@ public class LetsEncryptService : ILetsEncryptService {
|
|||||||
}
|
}
|
||||||
#endregion
|
#endregion
|
||||||
|
|
||||||
public Result<RegistrationCache?> GetRegistrationCache(Guid sessionId) {
|
|
||||||
var state = GetOrCreateState(sessionId);
|
|
||||||
|
|
||||||
if(state?.Cache == null)
|
|
||||||
return Result<RegistrationCache?>.InternalServerError(null);
|
|
||||||
|
|
||||||
return Result<RegistrationCache?>.Ok(state.Cache);
|
|
||||||
}
|
|
||||||
|
|
||||||
#region GetTermsOfService
|
#region GetTermsOfService
|
||||||
public Result<string?> GetTermsOfServiceUri(Guid sessionId) {
|
public Result<string?> GetTermsOfServiceUri(Guid sessionId) {
|
||||||
try {
|
try {
|
||||||
@ -882,31 +880,6 @@ public class LetsEncryptService : ILetsEncryptService {
|
|||||||
ResponseText = responseText
|
ResponseText = responseText
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
#endregion
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
// Helper for status comparison
|
// Helper for status comparison
|
||||||
private static bool StatusEquals(string? status, OrderStatus expected) => status == expected.GetDisplayName();
|
private static bool StatusEquals(string? status, OrderStatus expected) => status == expected.GetDisplayName();
|
||||||
@ -920,4 +893,5 @@ public class LetsEncryptService : ILetsEncryptService {
|
|||||||
_logger.LogError(ex, defaultMessage);
|
_logger.LogError(ex, defaultMessage);
|
||||||
return Result<T?>.InternalServerError(defaultValue, [.. ex.ExtractMessages()]);
|
return Result<T?>.InternalServerError(defaultValue, [.. ex.ExtractMessages()]);
|
||||||
}
|
}
|
||||||
|
#endregion
|
||||||
}
|
}
|
||||||
|
|||||||
@ -97,7 +97,6 @@ const AppMap: AppMapType[] = [
|
|||||||
title: 'Terms of Service',
|
title: 'Terms of Service',
|
||||||
routes: ['/terms-of-service'],
|
routes: ['/terms-of-service'],
|
||||||
page: LetsEncryptTermsOfServicePage,
|
page: LetsEncryptTermsOfServicePage,
|
||||||
linkArea: [LinkArea.SideMenu]
|
|
||||||
},
|
},
|
||||||
|
|
||||||
{
|
{
|
||||||
|
|||||||
@ -134,7 +134,7 @@ const EditAccount: FC<EditAccountProps> = (props) => {
|
|||||||
return patchRequest
|
return patchRequest
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleSubmit = () => {
|
const handleSubmit = async () => {
|
||||||
if (!formIsValid) return
|
if (!formIsValid) return
|
||||||
|
|
||||||
const fromFormState = mapFormStateToPatchRequest(formState)
|
const fromFormState = mapFormStateToPatchRequest(formState)
|
||||||
@ -164,14 +164,15 @@ const EditAccount: FC<EditAccountProps> = (props) => {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
patchData<PatchAccountRequest, GetAccountResponse>(GetApiRoute(ApiRoutes.ACCOUNT_PATCH).route
|
const response = await patchData<PatchAccountRequest, GetAccountResponse>(GetApiRoute(ApiRoutes.ACCOUNT_PATCH).route
|
||||||
.replace('{accountId}', accountId), delta, 120000
|
.replace('{accountId}', accountId), delta, 120000
|
||||||
).then((response) => {
|
)
|
||||||
|
|
||||||
if (!response) return
|
if (!response) return
|
||||||
|
|
||||||
handleInitialization(response)
|
handleInitialization(response)
|
||||||
onSubmitted?.(response)
|
onSubmitted?.(response)
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleCancel = () => {
|
const handleCancel = () => {
|
||||||
|
|||||||
@ -3,14 +3,14 @@ import { FormContainer, FormContent, FormFooter, FormHeader } from '../component
|
|||||||
import { postData } from '../axiosConfig'
|
import { postData } from '../axiosConfig'
|
||||||
import { GetAccountResponse } from '../models/letsEncryptServer/account/responses/GetAccountResponse'
|
import { GetAccountResponse } from '../models/letsEncryptServer/account/responses/GetAccountResponse'
|
||||||
import { ApiRoutes, GetApiRoute } from '../AppMap'
|
import { ApiRoutes, GetApiRoute } from '../AppMap'
|
||||||
import { ButtonComponent, RadioGroupComponent, SelectBoxComponent, TextBoxComponent } from '../components/editors'
|
import { ButtonComponent, CheckBoxComponent, RadioGroupComponent, SelectBoxComponent, TextBoxComponent } from '../components/editors'
|
||||||
import { ChallengeType } from '../entities/ChallengeType'
|
import { ChallengeType } from '../entities/ChallengeType'
|
||||||
import z, { array, boolean, object, Schema, string } from 'zod'
|
import z, { array, boolean, object, Schema, string } from 'zod'
|
||||||
import { useFormState } from '../hooks/useFormState'
|
import { useFormState } from '../hooks/useFormState'
|
||||||
import { enumToArr } from '../functions'
|
import { enumToArr } from '../functions'
|
||||||
import { PostAccountRequest, PostAccountRequestSchema } from '../models/letsEncryptServer/account/requests/PostAccountRequest'
|
import { PostAccountRequest, PostAccountRequestSchema } from '../models/letsEncryptServer/account/requests/PostAccountRequest'
|
||||||
import { addToast } from '../components/Toast/addToast'
|
import { addToast } from '../components/Toast/addToast'
|
||||||
import { useNavigate } from 'react-router-dom'
|
import { Link, useNavigate } from 'react-router-dom'
|
||||||
import { PlusIcon, TrashIcon } from 'lucide-react'
|
import { PlusIcon, TrashIcon } from 'lucide-react'
|
||||||
import { FieldContainer } from '../components/editors/FieldContainer'
|
import { FieldContainer } from '../components/editors/FieldContainer'
|
||||||
|
|
||||||
@ -26,6 +26,8 @@ interface RegisterFormProps {
|
|||||||
|
|
||||||
challengeType: ChallengeType
|
challengeType: ChallengeType
|
||||||
isStaging: boolean
|
isStaging: boolean
|
||||||
|
|
||||||
|
agreeToS: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
const RegisterFormProto = (): RegisterFormProps => ({
|
const RegisterFormProto = (): RegisterFormProps => ({
|
||||||
@ -38,7 +40,9 @@ const RegisterFormProto = (): RegisterFormProps => ({
|
|||||||
hostnames: [],
|
hostnames: [],
|
||||||
|
|
||||||
challengeType: ChallengeType.http01,
|
challengeType: ChallengeType.http01,
|
||||||
isStaging: true
|
isStaging: true,
|
||||||
|
|
||||||
|
agreeToS: false,
|
||||||
})
|
})
|
||||||
|
|
||||||
const RegisterFormSchema: Schema<RegisterFormProps> = object({
|
const RegisterFormSchema: Schema<RegisterFormProps> = object({
|
||||||
@ -51,7 +55,41 @@ const RegisterFormSchema: Schema<RegisterFormProps> = object({
|
|||||||
hostnames: array(string()),
|
hostnames: array(string()),
|
||||||
|
|
||||||
challengeType: z.enum(ChallengeType),
|
challengeType: z.enum(ChallengeType),
|
||||||
isStaging: boolean()
|
isStaging: boolean(),
|
||||||
|
|
||||||
|
agreeToS: boolean()
|
||||||
|
}).superRefine((data, ctx) => {
|
||||||
|
if (data.description.trim() === '') {
|
||||||
|
ctx.addIssue({
|
||||||
|
code: 'custom',
|
||||||
|
message: 'Description is required',
|
||||||
|
path: ['description']
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
if (data.contacts.length === 0) {
|
||||||
|
ctx.addIssue({
|
||||||
|
code: 'custom',
|
||||||
|
message: 'At least one contact is required',
|
||||||
|
path: ['contacts']
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
if (data.hostnames.length === 0) {
|
||||||
|
ctx.addIssue({
|
||||||
|
code: 'custom',
|
||||||
|
message: 'At least one hostname is required',
|
||||||
|
path: ['hostnames']
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!data.agreeToS) {
|
||||||
|
ctx.addIssue({
|
||||||
|
code: 'custom',
|
||||||
|
message: 'You must agree to the Terms of Service',
|
||||||
|
path: ['agreeToS']
|
||||||
|
})
|
||||||
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
interface RegisterProps {
|
interface RegisterProps {
|
||||||
@ -71,7 +109,7 @@ const Register: FC<RegisterProps> = () => {
|
|||||||
validationSchema: RegisterFormSchema,
|
validationSchema: RegisterFormSchema,
|
||||||
})
|
})
|
||||||
|
|
||||||
const handleSubmit = () => {
|
const handleSubmit = async () => {
|
||||||
if (!formIsValid) return
|
if (!formIsValid) return
|
||||||
|
|
||||||
const requestData: PostAccountRequest = {
|
const requestData: PostAccountRequest = {
|
||||||
@ -80,6 +118,7 @@ const Register: FC<RegisterProps> = () => {
|
|||||||
hostnames: formState.hostnames,
|
hostnames: formState.hostnames,
|
||||||
challengeType: formState.challengeType,
|
challengeType: formState.challengeType,
|
||||||
isStaging: formState.isStaging,
|
isStaging: formState.isStaging,
|
||||||
|
agreeToS: formState.agreeToS
|
||||||
}
|
}
|
||||||
|
|
||||||
const request = PostAccountRequestSchema.safeParse(requestData)
|
const request = PostAccountRequestSchema.safeParse(requestData)
|
||||||
@ -92,12 +131,15 @@ const Register: FC<RegisterProps> = () => {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
postData<PostAccountRequest, GetAccountResponse>(GetApiRoute(ApiRoutes.ACCOUNT_POST).route, request.data, 120000)
|
const response = await postData<PostAccountRequest, GetAccountResponse>(
|
||||||
.then(response => {
|
GetApiRoute(ApiRoutes.ACCOUNT_POST).route,
|
||||||
|
request.data,
|
||||||
|
120000
|
||||||
|
)
|
||||||
|
|
||||||
if (!response) return
|
if (!response) return
|
||||||
|
|
||||||
navigate('/')
|
navigate('/')
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return <FormContainer>
|
return <FormContainer>
|
||||||
@ -112,8 +154,12 @@ const Register: FC<RegisterProps> = () => {
|
|||||||
placeholder={'Account Description'}
|
placeholder={'Account Description'}
|
||||||
errorText={errors.description}
|
errorText={errors.description}
|
||||||
/>
|
/>
|
||||||
<h3 className={'col-span-12'}>Contacts:</h3>
|
<FieldContainer
|
||||||
<ul className={'col-span-12'}>
|
colspan={12}
|
||||||
|
label={'Contacts'}
|
||||||
|
errorText={errors.contacts}
|
||||||
|
>
|
||||||
|
<ul>
|
||||||
{formState.contacts.map((contact) => (
|
{formState.contacts.map((contact) => (
|
||||||
<li key={contact} className={'grid grid-cols-12 gap-4 w-full pb-2'}>
|
<li key={contact} className={'grid grid-cols-12 gap-4 w-full pb-2'}>
|
||||||
<span className={'col-span-10'}>{contact}</span>
|
<span className={'col-span-10'}>{contact}</span>
|
||||||
@ -129,6 +175,7 @@ const Register: FC<RegisterProps> = () => {
|
|||||||
</li>
|
</li>
|
||||||
))}
|
))}
|
||||||
</ul>
|
</ul>
|
||||||
|
</FieldContainer>
|
||||||
<TextBoxComponent
|
<TextBoxComponent
|
||||||
colspan={10}
|
colspan={10}
|
||||||
label={'New Contact'}
|
label={'New Contact'}
|
||||||
@ -166,8 +213,12 @@ const Register: FC<RegisterProps> = () => {
|
|||||||
errorText={errors.challengeType}
|
errorText={errors.challengeType}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<h3 className={'col-span-12'}>Hostnames:</h3>
|
<FieldContainer
|
||||||
<ul className={'col-span-12'}>
|
colspan={12}
|
||||||
|
label={'Hostnames'}
|
||||||
|
errorText={errors.hostnames}
|
||||||
|
>
|
||||||
|
<ul>
|
||||||
{formState.hostnames.map((hostname) => (
|
{formState.hostnames.map((hostname) => (
|
||||||
<li key={hostname} className={'grid grid-cols-12 gap-4 w-full pb-2'}>
|
<li key={hostname} className={'grid grid-cols-12 gap-4 w-full pb-2'}>
|
||||||
<span className={'col-span-10'}>{hostname}</span>
|
<span className={'col-span-10'}>{hostname}</span>
|
||||||
@ -183,6 +234,7 @@ const Register: FC<RegisterProps> = () => {
|
|||||||
</li>
|
</li>
|
||||||
))}
|
))}
|
||||||
</ul>
|
</ul>
|
||||||
|
</FieldContainer>
|
||||||
<TextBoxComponent
|
<TextBoxComponent
|
||||||
colspan={10}
|
colspan={10}
|
||||||
label={'New Hostname'}
|
label={'New Hostname'}
|
||||||
@ -222,6 +274,18 @@ const Register: FC<RegisterProps> = () => {
|
|||||||
}}
|
}}
|
||||||
errorText={errors.isStaging}
|
errorText={errors.isStaging}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
<Link to={'/terms-of-service'} className={'col-span-12 text-blue-600 underline'}> Terms of Service</Link>
|
||||||
|
<CheckBoxComponent
|
||||||
|
colspan={12}
|
||||||
|
label={'I agree to the LetsEncrypt Terms of Service'}
|
||||||
|
value={formState.agreeToS}
|
||||||
|
onChange={(e) => {
|
||||||
|
handleInputChange('agreeToS', e.target.checked)
|
||||||
|
}}
|
||||||
|
errorText={errors.agreeToS}
|
||||||
|
/>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
</FormContent>
|
</FormContent>
|
||||||
<FormFooter rightChildren={
|
<FormFooter rightChildren={
|
||||||
|
|||||||
@ -45,7 +45,7 @@ const ChangePassword: FC<ChangePasswordProps> = (props) => {
|
|||||||
onClose?.()
|
onClose?.()
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleSave = () => {
|
const handleSave = async () => {
|
||||||
if (formIsValid) {
|
if (formIsValid) {
|
||||||
const data: PatchUserChangePasswordRequest = {
|
const data: PatchUserChangePasswordRequest = {
|
||||||
password: formState.password,
|
password: formState.password,
|
||||||
@ -54,12 +54,11 @@ const ChangePassword: FC<ChangePasswordProps> = (props) => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
patchData<PatchUserChangePasswordRequest, UserResponse>(GetApiRoute(ApiRoutes.identityPatch).route.replace('{userId}', userId), data)
|
const response = await patchData<PatchUserChangePasswordRequest, UserResponse>(GetApiRoute(ApiRoutes.identityPatch).route.replace('{userId}', userId), data)
|
||||||
.then(response => {
|
|
||||||
if (!response) return
|
if (!response) return
|
||||||
|
|
||||||
handleOnClose()
|
handleOnClose()
|
||||||
})
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -8,6 +8,7 @@ export interface PostAccountRequest extends RequestModelBase {
|
|||||||
challengeType: string
|
challengeType: string
|
||||||
hostnames: string[]
|
hostnames: string[]
|
||||||
isStaging: boolean
|
isStaging: boolean
|
||||||
|
agreeToS: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
export const PostAccountRequestSchema: Schema<PostAccountRequest> = RequestModelBaseSchema.and(
|
export const PostAccountRequestSchema: Schema<PostAccountRequest> = RequestModelBaseSchema.and(
|
||||||
@ -16,6 +17,7 @@ export const PostAccountRequestSchema: Schema<PostAccountRequest> = RequestModel
|
|||||||
contacts: array(string()),
|
contacts: array(string()),
|
||||||
hostnames: array(string()),
|
hostnames: array(string()),
|
||||||
challengeType: z.enum(ChallengeType),
|
challengeType: z.enum(ChallengeType),
|
||||||
isStaging: boolean()
|
isStaging: boolean(),
|
||||||
|
agreeToS: boolean()
|
||||||
})
|
})
|
||||||
)
|
)
|
||||||
@ -1,9 +1,8 @@
|
|||||||
using Microsoft.Extensions.Options;
|
using MaksIT.LetsEncrypt.Entities;
|
||||||
|
|
||||||
|
|
||||||
using MaksIT.Webapi.Services;
|
|
||||||
using MaksIT.LetsEncrypt.Entities;
|
|
||||||
using MaksIT.Results;
|
using MaksIT.Results;
|
||||||
|
using MaksIT.Webapi.Services;
|
||||||
|
using Microsoft.Extensions.Options;
|
||||||
|
using System;
|
||||||
|
|
||||||
namespace MaksIT.Webapi.BackgroundServices {
|
namespace MaksIT.Webapi.BackgroundServices {
|
||||||
public class AutoRenewal : BackgroundService {
|
public class AutoRenewal : BackgroundService {
|
||||||
@ -13,6 +12,8 @@ namespace MaksIT.Webapi.BackgroundServices {
|
|||||||
private readonly ICacheService _cacheService;
|
private readonly ICacheService _cacheService;
|
||||||
private readonly ICertsFlowService _certsFlowService;
|
private readonly ICertsFlowService _certsFlowService;
|
||||||
|
|
||||||
|
private static readonly Random _random = new();
|
||||||
|
|
||||||
public AutoRenewal(
|
public AutoRenewal(
|
||||||
IOptions<Configuration> appSettings,
|
IOptions<Configuration> appSettings,
|
||||||
ILogger<AutoRenewal> logger,
|
ILogger<AutoRenewal> logger,
|
||||||
@ -47,23 +48,37 @@ namespace MaksIT.Webapi.BackgroundServices {
|
|||||||
|
|
||||||
private async Task<Result> ProcessAccountAsync(RegistrationCache cache) {
|
private async Task<Result> ProcessAccountAsync(RegistrationCache cache) {
|
||||||
|
|
||||||
var hostnames = cache.GetHostsWithUpcomingSslExpiry();
|
var hosts = cache.GetHosts();
|
||||||
if (hostnames == null) {
|
var toRenew = new List<string>();
|
||||||
_logger.LogError("Unexpected hostnames null");
|
|
||||||
|
foreach (var host in hosts) {
|
||||||
|
if (host.IsDisabled)
|
||||||
|
continue;
|
||||||
|
|
||||||
|
// Only consider certs expiring within 30 days
|
||||||
|
if ((host.Expires - DateTime.UtcNow).TotalDays < 30) {
|
||||||
|
// Randomize renewal between 1 and 5 days before expiry
|
||||||
|
int randomDays = _random.Next(1, 6);
|
||||||
|
var renewalTime = host.Expires.AddDays(-randomDays);
|
||||||
|
if (DateTime.UtcNow >= renewalTime) {
|
||||||
|
toRenew.Add(host.Hostname);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!toRenew.Any()) {
|
||||||
|
_logger.LogInformation("No certificates are due for randomized renewal at this time.");
|
||||||
return Result.Ok();
|
return Result.Ok();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var fullFlowResult = await _certsFlowService.FullFlow(
|
||||||
|
cache.IsStaging, cache.AccountId, cache.Description, cache.Contacts, cache.ChallengeType, toRenew.ToArray()
|
||||||
|
);
|
||||||
|
|
||||||
if (!hostnames.Any()) {
|
|
||||||
_logger.LogInformation("No hosts found with upcoming SSL expiry");
|
|
||||||
return Result.Ok();
|
|
||||||
}
|
|
||||||
|
|
||||||
var fullFlowResult = await _certsFlowService.FullFlow(cache.IsStaging, cache.AccountId, cache.Description, cache.Contacts, cache.ChallengeType, hostnames);
|
|
||||||
if (!fullFlowResult.IsSuccess)
|
if (!fullFlowResult.IsSuccess)
|
||||||
return fullFlowResult;
|
return fullFlowResult;
|
||||||
|
|
||||||
_logger.LogInformation($"Certificates renewed for account {cache.AccountId}");
|
_logger.LogInformation($"Certificates renewed for account {cache.AccountId}: {string.Join(", ", toRenew)}");
|
||||||
|
|
||||||
return Result.Ok();
|
return Result.Ok();
|
||||||
}
|
}
|
||||||
|
|||||||
@ -7,8 +7,15 @@
|
|||||||
<RootNamespace>$(MSBuildProjectName.Replace(" ", "_"))</RootNamespace>
|
<RootNamespace>$(MSBuildProjectName.Replace(" ", "_"))</RootNamespace>
|
||||||
<DockerDefaultTargetOS>Linux</DockerDefaultTargetOS>
|
<DockerDefaultTargetOS>Linux</DockerDefaultTargetOS>
|
||||||
<DockerComposeProjectPath>..\docker-compose.dcproj</DockerComposeProjectPath>
|
<DockerComposeProjectPath>..\docker-compose.dcproj</DockerComposeProjectPath>
|
||||||
|
<PackageReadmeFile>README.md</PackageReadmeFile>
|
||||||
|
<PackageLicenseFile>LICENSE.md</PackageLicenseFile>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<None Include="../../README.md" Pack="true" PackagePath="" />
|
||||||
|
<None Include="../../LICENSE.md" Pack="true" PackagePath="" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="MaksIT.Results" Version="1.1.1" />
|
<PackageReference Include="MaksIT.Results" Version="1.1.1" />
|
||||||
<PackageReference Include="Microsoft.Extensions.Caching.Memory" Version="10.0.0" />
|
<PackageReference Include="Microsoft.Extensions.Caching.Memory" Version="10.0.0" />
|
||||||
|
|||||||
@ -9,6 +9,7 @@ public class PostAccountRequest : RequestModelBase {
|
|||||||
public required string ChallengeType { get; set; }
|
public required string ChallengeType { get; set; }
|
||||||
public required string[] Hostnames { get; set; }
|
public required string[] Hostnames { get; set; }
|
||||||
public required bool IsStaging { get; set; }
|
public required bool IsStaging { get; set; }
|
||||||
|
public required bool AgreeToS { get; set; }
|
||||||
|
|
||||||
public override IEnumerable<ValidationResult> Validate(ValidationContext validationContext) {
|
public override IEnumerable<ValidationResult> Validate(ValidationContext validationContext) {
|
||||||
if (string.IsNullOrWhiteSpace(Description))
|
if (string.IsNullOrWhiteSpace(Description))
|
||||||
@ -22,5 +23,8 @@ public class PostAccountRequest : RequestModelBase {
|
|||||||
|
|
||||||
if (string.IsNullOrWhiteSpace(ChallengeType) && ChallengeType != "http-01")
|
if (string.IsNullOrWhiteSpace(ChallengeType) && ChallengeType != "http-01")
|
||||||
yield return new ValidationResult("ChallengeType is required", [nameof(ChallengeType)]);
|
yield return new ValidationResult("ChallengeType is required", [nameof(ChallengeType)]);
|
||||||
|
|
||||||
|
if (!AgreeToS)
|
||||||
|
yield return new ValidationResult("You must agree to the Terms of Service", [nameof(AgreeToS)]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user