(refactor): fe layout review, be models review

This commit is contained in:
Maksym Sadovnychyy 2024-06-20 23:19:56 +02:00
parent 7d7ebd298a
commit 322845d82d
31 changed files with 372 additions and 149 deletions

View File

@ -1,8 +1,8 @@
{ {
"editor.tabSize": 2, "editor.tabSize": 2,
// "editor.codeActionsOnSave": { "editor.codeActionsOnSave": {
// "source.fixAll.eslint": true "source.fixAll.eslint": true
// }, },
"[javascript]": { "[javascript]": {
"editor.defaultFormatter": "esbenp.prettier-vscode" "editor.defaultFormatter": "esbenp.prettier-vscode"
}, },

View File

@ -2,7 +2,7 @@ enum ApiRoutes {
CACHE_ACCOUNTS = 'api/cache/accounts', CACHE_ACCOUNTS = 'api/cache/accounts',
CACHE_ACCOUNT = 'api/cache/account/{accountId}', CACHE_ACCOUNT = 'api/cache/account/{accountId}',
CACHE_ACCOUNT_CONTACTS = 'api/cache/account/{accountId}/contacts', CACHE_ACCOUNT_CONTACTS = 'api/cache/account/{accountId}/contacts',
CACHE_ACCOUNT_CONTACT = 'api/cache/account/{accountId}/contacts/{index}', CACHE_ACCOUNT_CONTACT = 'api/cache/account/{accountId}/contact/{index}',
CACHE_ACCOUNT_HOSTNAMES = 'api/cache/account/{accountId}/hostnames' CACHE_ACCOUNT_HOSTNAMES = 'api/cache/account/{accountId}/hostnames'
// CERTS_FLOW_CONFIGURE_CLIENT = `api/CertsFlow/ConfigureClient`, // CERTS_FLOW_CONFIGURE_CLIENT = `api/CertsFlow/ConfigureClient`,

View File

@ -21,17 +21,20 @@ export default function Page() {
const { const {
value: newContact, value: newContact,
error: contactError, error: contactError,
handleChange: handleContactChange handleChange: handleContactChange,
} = useValidation({ reset: resetContact
} = useValidation<string>({
initialValue: '', initialValue: '',
validateFn: isValidEmail, validateFn: isValidEmail,
errorMessage: 'Invalid email format.' errorMessage: 'Invalid email format.'
}) })
const { const {
value: newHostname, value: newHostname,
error: hostnameError, error: hostnameError,
handleChange: handleHostnameChange handleChange: handleHostnameChange,
} = useValidation({ reset: resetHostname
} = useValidation<string>({
initialValue: '', initialValue: '',
validateFn: isValidHostname, validateFn: isValidHostname,
errorMessage: 'Invalid hostname format.' errorMessage: 'Invalid hostname format.'
@ -110,25 +113,25 @@ export default function Page() {
} }
const addContact = (accountId: string) => { const addContact = (accountId: string) => {
if (newContact.trim() === '' || contactError) { if (newContact === '' || contactError) {
return return
} }
if ( if (
accounts accounts
.find((account) => account.accountId === accountId) .find((account) => account.accountId === accountId)
?.contacts.includes(newContact.trim()) ?.contacts.includes(newContact)
) )
return return
setAccounts( setAccounts(
accounts.map((account) => accounts.map((account) =>
account.accountId === accountId account.accountId === accountId
? { ...account, contacts: [...account.contacts, newContact.trim()] } ? { ...account, contacts: [...account.contacts, newContact] }
: account : account
) )
) )
handleContactChange('') resetContact()
} }
const deleteHostname = (accountId: string, hostname: string) => { const deleteHostname = (accountId: string, hostname: string) => {
@ -153,14 +156,14 @@ export default function Page() {
} }
const addHostname = (accountId: string) => { const addHostname = (accountId: string) => {
if (newHostname.trim() === '' || hostnameError) { if (newHostname === '' || hostnameError) {
return return
} }
if ( if (
accounts accounts
.find((account) => account.accountId === accountId) .find((account) => account.accountId === accountId)
?.hostnames.some((h) => h.hostname === newHostname.trim()) ?.hostnames.some((h) => h.hostname === newHostname)
) )
return return
@ -172,7 +175,7 @@ export default function Page() {
hostnames: [ hostnames: [
...account.hostnames, ...account.hostnames,
{ {
hostname: newHostname.trim(), hostname: newHostname,
expires: new Date(), expires: new Date(),
isUpcomingExpire: false isUpcomingExpire: false
} }
@ -181,7 +184,7 @@ export default function Page() {
: account : account
) )
) )
handleHostnameChange('') resetHostname()
} }
const handleSubmit = async ( const handleSubmit = async (
@ -279,14 +282,15 @@ export default function Page() {
className="text-gray-700 flex justify-between items-center mb-2" className="text-gray-700 flex justify-between items-center mb-2"
> >
{contact} {contact}
<button <CustomButton
type="button"
onClick={() => onClick={() =>
deleteContact(account.accountId, contact) deleteContact(account.accountId, contact)
} }
className="bg-red-500 text-white px-2 py-1 rounded ml-4 h-10" className="bg-red-500 text-white p-2 rounded ml-2"
> >
<TrashIcon className="h-5 w-5 text-white" /> <TrashIcon className="h-5 w-5 text-white" />
</button> </CustomButton>
</li> </li>
))} ))}
</ul> </ul>
@ -301,13 +305,15 @@ export default function Page() {
inputClassName="border p-2 rounded w-full" inputClassName="border p-2 rounded w-full"
errorClassName="text-red-500 text-sm mt-1" errorClassName="text-red-500 text-sm mt-1"
className="mr-2 flex-grow" className="mr-2 flex-grow"
/>
<button
onClick={() => addContact(account.accountId)}
className="bg-green-500 text-white p-2 rounded ml-2 h-10 flex items-center"
> >
<PlusIcon className="h-5 w-5 text-white" /> <CustomButton
</button> type="button"
onClick={() => addContact(account.accountId)}
className="bg-green-500 text-white p-2 rounded ml-2"
>
<PlusIcon className="h-5 w-5 text-white" />
</CustomButton>
</CustomInput>
</div> </div>
</div> </div>
<div> <div>
@ -329,14 +335,15 @@ export default function Page() {
: 'Not Upcoming'} : 'Not Upcoming'}
</span> </span>
</div> </div>
<button <CustomButton
type="button"
onClick={() => onClick={() =>
deleteHostname(account.accountId, hostname.hostname) deleteHostname(account.accountId, hostname.hostname)
} }
className="bg-red-500 text-white px-2 py-1 rounded ml-4 h-10" className="bg-red-500 text-white p-2 rounded ml-2"
> >
<TrashIcon className="h-5 w-5 text-white" /> <TrashIcon className="h-5 w-5 text-white" />
</button> </CustomButton>
</li> </li>
))} ))}
</ul> </ul>
@ -351,25 +358,27 @@ export default function Page() {
inputClassName="border p-2 rounded w-full" inputClassName="border p-2 rounded w-full"
errorClassName="text-red-500 text-sm mt-1" errorClassName="text-red-500 text-sm mt-1"
className="mr-2 flex-grow" className="mr-2 flex-grow"
/>
<button
onClick={() => addHostname(account.accountId)}
className="bg-green-500 text-white p-2 rounded ml-2 h-10 flex items-center"
> >
<PlusIcon className="h-5 w-5 text-white" /> <CustomButton
</button> type="button"
onClick={() => addHostname(account.accountId)}
className="bg-green-500 text-white p-2 rounded ml-2"
>
<PlusIcon className="h-5 w-5 text-white" />
</CustomButton>
</CustomInput>
</div> </div>
</div> </div>
<div className="flex justify-between mt-4"> <div className="flex justify-between mt-4">
<button <CustomButton
onClick={() => deleteAccount(account.accountId)} onClick={() => deleteAccount(account.accountId)}
className="bg-red-500 text-white px-3 py-1 rounded" className="bg-red-500 text-white p-2 rounded ml-2"
> >
<TrashIcon className="h-5 w-5 text-white" /> <TrashIcon className="h-5 w-5 text-white" />
</button> </CustomButton>
<CustomButton <CustomButton
type="submit" type="submit"
className="bg-green-500 text-white px-3 py-1 rounded" className="bg-green-500 text-white p-2 rounded ml-2"
> >
Submit Submit
</CustomButton> </CustomButton>

View File

@ -10,24 +10,26 @@ import {
} from '@/hooks/useValidation' } from '@/hooks/useValidation'
import { CustomButton, CustomInput } from '@/controls' import { CustomButton, CustomInput } from '@/controls'
import { FaTrash, FaPlus } from 'react-icons/fa' import { FaTrash, FaPlus } from 'react-icons/fa'
import { GetAccountResponse } from '@/models/letsEncryptServer/cache/responses/GetAccountResponse'
import { deepCopy } from '../functions' import { deepCopy } from '../functions'
import {
interface CacheAccount { PostAccountRequest,
description?: string validatePostAccountRequest
contacts: string[] } from '@/models/letsEncryptServer/certsFlow/PostAccountRequest'
hostnames: string[] import App from 'next/app'
} import { useAppDispatch } from '@/redux/store'
import { showToast } from '@/redux/slices/toastSlice'
const RegisterPage = () => { const RegisterPage = () => {
const [account, setAccount] = useState<CacheAccount | null>(null) const [account, setAccount] = useState<PostAccountRequest | null>(null)
const dispatch = useAppDispatch()
const { const {
value: newContact, value: newContact,
error: contactError, error: contactError,
handleChange: handleContactChange, handleChange: handleContactChange,
reset: resetContact reset: resetContact
} = useValidation({ } = useValidation<string>({
initialValue: '', initialValue: '',
validateFn: isValidContact, validateFn: isValidContact,
errorMessage: 'Invalid contact. Must be a valid email or phone number.' errorMessage: 'Invalid contact. Must be a valid email or phone number.'
@ -38,7 +40,7 @@ const RegisterPage = () => {
error: hostnameError, error: hostnameError,
handleChange: handleHostnameChange, handleChange: handleHostnameChange,
reset: resetHostname reset: resetHostname
} = useValidation({ } = useValidation<string>({
initialValue: '', initialValue: '',
validateFn: isValidHostname, validateFn: isValidHostname,
errorMessage: 'Invalid hostname format.' errorMessage: 'Invalid hostname format.'
@ -55,10 +57,17 @@ const RegisterPage = () => {
const handleDescription = (description: string) => {} const handleDescription = (description: string) => {}
const handleAddContact = () => { const handleAddContact = () => {
if (newContact !== '' || contactError) return if (
newContact === '' ||
account?.contacts.includes(newContact) ||
contactError !== ''
) {
resetContact()
return
}
setAccount((prev) => { setAccount((prev) => {
const newAccount: CacheAccount = const newAccount: PostAccountRequest =
prev !== null prev !== null
? deepCopy(prev) ? deepCopy(prev)
: { : {
@ -75,10 +84,17 @@ const RegisterPage = () => {
} }
const handleAddHostname = () => { const handleAddHostname = () => {
if (newHostname !== '' || hostnameError) return if (
newHostname === '' ||
account?.hostnames.includes(newHostname) ||
hostnameError !== ''
) {
resetHostname()
return
}
setAccount((prev) => { setAccount((prev) => {
const newAccount: CacheAccount = const newAccount: PostAccountRequest =
prev !== null prev !== null
? deepCopy(prev) ? deepCopy(prev)
: { : {
@ -119,6 +135,17 @@ const RegisterPage = () => {
const handleSubmit = async (e: FormEvent<HTMLFormElement>) => { const handleSubmit = async (e: FormEvent<HTMLFormElement>) => {
e.preventDefault() e.preventDefault()
const error = validatePostAccountRequest(account)
if (error) {
console.error(`Validation failed: ${error}`)
// dipatch toasterror
dispatch(showToast({ message: error, type: 'error' }))
return
}
// httpService.post<PostAccountRequest, GetAccountResponse>('', account)
console.log(account) console.log(account)
} }
@ -148,13 +175,13 @@ const RegisterPage = () => {
className="text-gray-700 flex justify-between items-center mb-2" className="text-gray-700 flex justify-between items-center mb-2"
> >
{contact} {contact}
<button <CustomButton
type="button" type="button"
onClick={() => handleDeleteContact(contact)} onClick={() => handleDeleteContact(contact)}
className="bg-red-500 text-white px-2 py-1 rounded ml-4" className="bg-red-500 text-white p-2 rounded ml-2"
> >
<FaTrash /> <FaTrash />
</button> </CustomButton>
</li> </li>
))} ))}
</ul> </ul>
@ -169,14 +196,15 @@ const RegisterPage = () => {
inputClassName="border p-2 rounded w-full" inputClassName="border p-2 rounded w-full"
errorClassName="text-red-500 text-sm mt-1" errorClassName="text-red-500 text-sm mt-1"
className="mr-2 flex-grow" className="mr-2 flex-grow"
/>
<button
type="button"
onClick={handleAddContact}
className="bg-green-500 text-white p-2 rounded ml-2 h-10 flex items-center"
> >
<FaPlus /> <CustomButton
</button> type="button"
onClick={handleAddContact}
className="bg-green-500 text-white p-2 rounded ml-2"
>
<FaPlus />
</CustomButton>
</CustomInput>
</div> </div>
</div> </div>
<div className="mb-4"> <div className="mb-4">
@ -188,13 +216,13 @@ const RegisterPage = () => {
className="text-gray-700 flex justify-between items-center mb-2" className="text-gray-700 flex justify-between items-center mb-2"
> >
{hostname} {hostname}
<button <CustomButton
type="button" type="button"
onClick={() => handleDeleteHostname(hostname)} onClick={() => handleDeleteHostname(hostname)}
className="bg-red-500 text-white px-2 py-1 rounded ml-4" className="bg-red-500 text-white p-2 rounded ml-2"
> >
<FaTrash /> <FaTrash />
</button> </CustomButton>
</li> </li>
))} ))}
</ul> </ul>
@ -209,14 +237,15 @@ const RegisterPage = () => {
inputClassName="border p-2 rounded w-full" inputClassName="border p-2 rounded w-full"
errorClassName="text-red-500 text-sm mt-1" errorClassName="text-red-500 text-sm mt-1"
className="mr-2 flex-grow" className="mr-2 flex-grow"
/>
<button
type="button"
onClick={handleAddHostname}
className="bg-green-500 text-white p-2 rounded ml-2 h-10 flex items-center"
> >
<FaPlus /> <CustomButton
</button> type="button"
onClick={handleAddHostname}
className="bg-green-500 text-white p-2 rounded ml-2"
>
<FaPlus />
</CustomButton>
</CustomInput>
</div> </div>
</div> </div>
<CustomButton <CustomButton

View File

@ -1,6 +1,6 @@
// components/CustomInput.tsx // components/CustomInput.tsx
'use client' 'use client'
import React from 'react' import React, { FC } from 'react'
interface CustomInputProps { interface CustomInputProps {
value: string value: string
@ -12,9 +12,10 @@ interface CustomInputProps {
inputClassName?: string inputClassName?: string
errorClassName?: string errorClassName?: string
className?: string className?: string
children?: React.ReactNode // Added for additional elements
} }
const CustomInput: React.FC<CustomInputProps> = ({ const CustomInput: FC<CustomInputProps> = ({
value, value,
onChange, onChange,
placeholder = '', placeholder = '',
@ -23,23 +24,29 @@ const CustomInput: React.FC<CustomInputProps> = ({
title, title,
inputClassName = '', inputClassName = '',
errorClassName = '', errorClassName = '',
className = '' className = '',
children // Added for additional elements
}) => { }) => {
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => { const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
onChange?.(e.target.value) onChange?.(e.target.value)
} }
return ( return (
<div className={className}> <div className={`flex flex-col ${className}`}>
{title && <label>{title}</label>} {title && <label className="mb-1">{title}</label>}
<input <div className="flex items-center">
type={type} <input
value={value} type={type}
onChange={handleChange} value={value}
placeholder={placeholder} onChange={handleChange}
className={inputClassName} placeholder={placeholder}
/> className={`flex-grow ${inputClassName}`}
{error && <p className={errorClassName}>{error}</p>} />
{children && <div className="ml-2">{children}</div>}
</div>
{error && (
<p className={`text-red-500 mt-1 ${errorClassName}`}>{error}</p>
)}
</div> </div>
) )
} }

View File

@ -1,4 +1,4 @@
import { useState, useEffect } from 'react' import { useState, useEffect, useCallback } from 'react'
// Helper functions for validation // Helper functions for validation
const isValidEmail = (email: string) => { const isValidEmail = (email: string) => {
@ -21,38 +21,68 @@ const isValidHostname = (hostname: string) => {
} }
// Props interface for useValidation hook // Props interface for useValidation hook
interface UseValidationProps { interface UseValidationProps<T> {
initialValue: string initialValue: T
validateFn: (value: string) => boolean validateFn: (value: T) => boolean
errorMessage: string errorMessage: string
emptyFieldMessage?: string // Optional custom message for empty fields
defaultResetValue?: T // Optional default reset value
} }
// Custom hook for input validation // Custom hook for input validation
const useValidation = ({ const useValidation = <T extends string | number | Date>(
initialValue, props: UseValidationProps<T>
validateFn, ) => {
errorMessage const {
}: UseValidationProps) => { initialValue,
const [value, setValue] = useState(initialValue) validateFn,
errorMessage,
emptyFieldMessage = 'This field cannot be empty.', // Default message
defaultResetValue
} = props
const [value, setValue] = useState<T>(initialValue)
const [error, setError] = useState('') const [error, setError] = useState('')
const handleChange = (newValue: string) => { const handleChange = useCallback(
console.log(newValue) (newValue: T) => {
setValue(newValue) setValue(newValue)
if (newValue.trim() === '') { const stringValue =
setError('This field cannot be empty.') newValue instanceof Date
} else if (!validateFn(newValue.trim())) { ? newValue.toISOString()
setError(errorMessage) : newValue.toString().trim()
} else { if (stringValue === '') {
setError('') setError(emptyFieldMessage)
} } else if (!validateFn(newValue)) {
} setError(errorMessage)
} else {
setError('')
}
},
[emptyFieldMessage, errorMessage, validateFn]
)
useEffect(() => { useEffect(() => {
handleChange(initialValue) handleChange(initialValue)
}, [initialValue]) }, [initialValue, handleChange])
return { value, error, handleChange, reset: () => setValue('') } const reset = useCallback(() => {
const resetValue =
defaultResetValue !== undefined ? defaultResetValue : initialValue
setValue(resetValue)
const stringValue =
resetValue instanceof Date
? resetValue.toISOString()
: resetValue.toString().trim()
if (stringValue === '') {
setError(emptyFieldMessage)
} else {
setError('')
}
}, [defaultResetValue, initialValue, emptyFieldMessage])
return { value, error, handleChange, reset }
} }
export { export {

View File

@ -0,0 +1,5 @@
export interface PatchAction<T> {
op: PatchOperation // Enum for operation type
index?: number // Index for the operation (for arrays/lists)
value?: T // Value for the operation
}

View File

@ -0,0 +1,5 @@
enum PatchOperation {
Add,
Remove,
Replace
}

View File

@ -0,0 +1,6 @@
import { PatchAction } from '@/models/PatchAction'
export interface PatchAccountRequest {
description?: PatchAction<string>
contacts?: PatchAction<string>[]
}

View File

@ -0,0 +1,5 @@
import { PatchAction } from '@/models/PatchAction'
export interface PatchContactsRequest {
contacts: PatchAction<string>[]
}

View File

@ -0,0 +1,3 @@
export interface PostContactsRequest {
contacts: string[]
}

View File

@ -0,0 +1,4 @@
export interface PutAccountRequest {
description: string
contacts: string[]
}

View File

@ -0,0 +1,3 @@
export interface PutContactsRequest {
contacts: string[]
}

View File

@ -0,0 +1,32 @@
import { isValidContact, isValidHostname } from '@/hooks/useValidation'
export interface PostAccountRequest {
description?: string
contacts: string[]
hostnames: string[]
}
const validatePostAccountRequest = (
request: PostAccountRequest | null
): string | null => {
if (request === null) return 'Request is null'
// Validate contacts
for (const contact of request.contacts) {
if (!isValidContact(contact)) {
return `Invalid contact: ${contact}`
}
}
// Validate hostnames
for (const hostname of request.hostnames) {
if (!isValidHostname(hostname)) {
return `Invalid hostname: ${hostname}`
}
}
// If all validations pass, return null
return null
}
export { validatePostAccountRequest }

View File

@ -1,5 +1,11 @@
import { configureStore } from '@reduxjs/toolkit' import { configureStore } from '@reduxjs/toolkit'
import loaderReducer from '@/redux/slices//loaderSlice' import {
TypedUseSelectorHook,
useDispatch,
useSelector,
useStore
} from 'react-redux'
import loaderReducer from '@/redux/slices/loaderSlice'
import toastReducer from '@/redux/slices/toastSlice' import toastReducer from '@/redux/slices/toastSlice'
export const store = configureStore({ export const store = configureStore({
@ -11,3 +17,9 @@ export const store = configureStore({
export type RootState = ReturnType<typeof store.getState> export type RootState = ReturnType<typeof store.getState>
export type AppDispatch = typeof store.dispatch export type AppDispatch = typeof store.dispatch
export type AppStore = typeof store
// Use throughout your app instead of plain `useDispatch` and `useSelector`
export const useAppDispatch = () => useDispatch<AppDispatch>()
export const useAppSelector: TypedUseSelectorHook<RootState> = useSelector
export const useAppStore = () => useStore<AppStore>()

View File

@ -4,9 +4,8 @@ using DomainResults.Common;
using MaksIT.LetsEncryptServer.Services; using MaksIT.LetsEncryptServer.Services;
using Models.LetsEncryptServer.CertsFlow.Requests;
using Models.LetsEncryptServer.Cache.Responses;
using MaksIT.LetsEncrypt.Entities; using MaksIT.LetsEncrypt.Entities;
using MaksIT.Models.LetsEncryptServer.CertsFlow.Requests;
namespace MaksIT.LetsEncryptServer.BackgroundServices { namespace MaksIT.LetsEncryptServer.BackgroundServices {
public class AutoRenewal : BackgroundService { public class AutoRenewal : BackgroundService {
@ -62,7 +61,7 @@ namespace MaksIT.LetsEncryptServer.BackgroundServices {
return IDomainResult.Success(); return IDomainResult.Success();
} }
var renewResult = await RenewCertificatesForHostnames(cache.AccountId, cache.Contacts, hostnames); var renewResult = await RenewCertificatesForHostnames(cache.AccountId, cache.Description, cache.Contacts, hostnames);
if (!renewResult.IsSuccess) if (!renewResult.IsSuccess)
return renewResult; return renewResult;
@ -71,7 +70,7 @@ namespace MaksIT.LetsEncryptServer.BackgroundServices {
return IDomainResult.Success(); return IDomainResult.Success();
} }
private async Task<IDomainResult> RenewCertificatesForHostnames(Guid accountId, string[] contacts, string[] hostnames) { private async Task<IDomainResult> RenewCertificatesForHostnames(Guid accountId, string description, string[] contacts, string[] hostnames) {
var (sessionId, configureClientResult) = await _certsFlowService.ConfigureClientAsync(); var (sessionId, configureClientResult) = await _certsFlowService.ConfigureClientAsync();
if (!configureClientResult.IsSuccess || sessionId == null) { if (!configureClientResult.IsSuccess || sessionId == null) {
LogErrors(configureClientResult.Errors); LogErrors(configureClientResult.Errors);
@ -81,6 +80,7 @@ namespace MaksIT.LetsEncryptServer.BackgroundServices {
var sessionIdValue = sessionId.Value; var sessionIdValue = sessionId.Value;
var (_, initResult) = await _certsFlowService.InitAsync(sessionIdValue, accountId, new InitRequest { var (_, initResult) = await _certsFlowService.InitAsync(sessionIdValue, accountId, new InitRequest {
Description = description,
Contacts = contacts Contacts = contacts
}); });
if (!initResult.IsSuccess) { if (!initResult.IsSuccess) {

View File

@ -57,7 +57,7 @@ public class CacheController : ControllerBase {
return result.ToActionResult(); return result.ToActionResult();
} }
[HttpDelete("account/{accountId:guid}/contacts/{index:int}")] [HttpDelete("account/{accountId:guid}/contact/{index:int}")]
public async Task<IActionResult> DeleteContact(Guid accountId, int index) { public async Task<IActionResult> DeleteContact(Guid accountId, int index) {
var result = await _cacheService.DeleteContactAsync(accountId, index); var result = await _cacheService.DeleteContactAsync(accountId, index);
return result.ToActionResult(); return result.ToActionResult();

View File

@ -2,11 +2,11 @@ using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Options; using Microsoft.Extensions.Options;
using DomainResults.Mvc; using DomainResults.Mvc;
using MaksIT.LetsEncryptServer.Services; using MaksIT.LetsEncryptServer.Services;
using Models.LetsEncryptServer.CertsFlow.Requests; using MaksIT.Models.LetsEncryptServer.CertsFlow.Requests;
namespace MaksIT.LetsEncryptServer.Controllers { namespace MaksIT.LetsEncryptServer.Controllers {
[ApiController] [ApiController]
[Route("api/[controller]")] [Route("api/certs")]
public class CertsFlowController : ControllerBase { public class CertsFlowController : ControllerBase {
private readonly Configuration _appSettings; private readonly Configuration _appSettings;
private readonly ICertsFlowService _certsFlowService; private readonly ICertsFlowService _certsFlowService;
@ -34,7 +34,7 @@ namespace MaksIT.LetsEncryptServer.Controllers {
/// </summary> /// </summary>
/// <param name="sessionId">Session ID</param> /// <param name="sessionId">Session ID</param>
/// <returns>Terms of service</returns> /// <returns>Terms of service</returns>
[HttpGet("terms-of-service/{sessionId}")] [HttpGet("{sessionId}/terms-of-service")]
public IActionResult TermsOfService(Guid sessionId) { public IActionResult TermsOfService(Guid sessionId) {
var result = _certsFlowService.GetTermsOfService(sessionId); var result = _certsFlowService.GetTermsOfService(sessionId);
return result.ToActionResult(); return result.ToActionResult();

View File

@ -13,7 +13,7 @@ namespace MaksIT.LetsEncryptServer.Controllers;
public class WellKnownController : ControllerBase { public class WellKnownController : ControllerBase {
private readonly Configuration _appSettings; private readonly Configuration _appSettings;
private readonly ICertsFlowServiceBase _certsFlowService; private readonly ICertsRestChallengeService _certsFlowService;
private readonly string _acmePath = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "acme"); private readonly string _acmePath = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "acme");

View File

@ -6,7 +6,7 @@ using MaksIT.Core.Extensions;
using MaksIT.LetsEncrypt.Entities; using MaksIT.LetsEncrypt.Entities;
using MaksIT.Models; using MaksIT.Models;
using MaksIT.Models.LetsEncryptServer.Cache.Requests; using MaksIT.Models.LetsEncryptServer.Cache.Requests;
using Models.LetsEncryptServer.Cache.Responses; using MaksIT.Models.LetsEncryptServer.Cache.Responses;
namespace MaksIT.LetsEncryptServer.Services; namespace MaksIT.LetsEncryptServer.Services;

View File

@ -6,16 +6,21 @@ using DomainResults.Common;
using MaksIT.LetsEncrypt.Entities; using MaksIT.LetsEncrypt.Entities;
using MaksIT.LetsEncrypt.Services; using MaksIT.LetsEncrypt.Services;
using Models.LetsEncryptServer.CertsFlow.Requests; using MaksIT.Models.LetsEncryptServer.CertsFlow.Requests;
namespace MaksIT.LetsEncryptServer.Services; namespace MaksIT.LetsEncryptServer.Services;
public interface ICertsFlowServiceBase { public interface ICertsInternalService {
}
public interface ICertsRestChallengeService {
(string?, IDomainResult) AcmeChallenge(string fileName); (string?, IDomainResult) AcmeChallenge(string fileName);
} }
public interface ICertsFlowService : ICertsFlowServiceBase { public interface ICertsRestService {
Task<(Guid?, IDomainResult)> ConfigureClientAsync(); Task<(Guid?, IDomainResult)> ConfigureClientAsync();
(string?, IDomainResult) GetTermsOfService(Guid sessionId); (string?, IDomainResult) GetTermsOfService(Guid sessionId);
Task<(Guid?, IDomainResult)> InitAsync(Guid sessionId, Guid? accountId, InitRequest requestData); Task<(Guid?, IDomainResult)> InitAsync(Guid sessionId, Guid? accountId, InitRequest requestData);
@ -24,7 +29,12 @@ public interface ICertsFlowService : ICertsFlowServiceBase {
Task<IDomainResult> GetOrderAsync(Guid sessionId, GetOrderRequest requestData); Task<IDomainResult> GetOrderAsync(Guid sessionId, GetOrderRequest requestData);
Task<IDomainResult> GetCertificatesAsync(Guid sessionId, GetCertificatesRequest requestData); Task<IDomainResult> GetCertificatesAsync(Guid sessionId, GetCertificatesRequest requestData);
Task<(Dictionary<string, string>?, IDomainResult)> ApplyCertificatesAsync(Guid sessionId, GetCertificatesRequest requestData); Task<(Dictionary<string, string>?, IDomainResult)> ApplyCertificatesAsync(Guid sessionId, GetCertificatesRequest requestData);
} }
public interface ICertsFlowService
: ICertsInternalService,
ICertsRestService,
ICertsRestChallengeService {}
public class CertsFlowService : ICertsFlowService { public class CertsFlowService : ICertsFlowService {

View File

@ -1,12 +1,21 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.Linq; using System.Linq;
using System.Text; using System.Text;
using System.Threading.Tasks; using System.Threading.Tasks;
namespace MaksIT.Models.LetsEncryptServer.Cache.Requests { namespace MaksIT.Models.LetsEncryptServer.Cache.Requests {
public class PutAccountRequest { public class PutAccountRequest : IValidatableObject {
public string Description { get; set; } public required string Description { get; set; }
public string[] Contacts { get; set; } public required string[] Contacts { get; set; }
public IEnumerable<ValidationResult> Validate(ValidationContext validationContext) {
if (string.IsNullOrWhiteSpace(Description))
yield return new ValidationResult("Description is required", new[] { nameof(Description) });
if (Contacts == null || Contacts.Length == 0)
yield return new ValidationResult("Contacts is required", new[] { nameof(Contacts) });
}
} }
} }

View File

@ -4,7 +4,7 @@ using System.Linq;
using System.Text; using System.Text;
using System.Threading.Tasks; using System.Threading.Tasks;
namespace Models.LetsEncryptServer.Cache.Responses { namespace MaksIT.Models.LetsEncryptServer.Cache.Responses {
public class GetAccountResponse { public class GetAccountResponse {
public Guid AccountId { get; set; } public Guid AccountId { get; set; }

View File

@ -4,7 +4,7 @@ using System.Linq;
using System.Text; using System.Text;
using System.Threading.Tasks; using System.Threading.Tasks;
namespace Models.LetsEncryptServer.Cache.Responses { namespace MaksIT.Models.LetsEncryptServer.Cache.Responses {
public class GetContactsResponse { public class GetContactsResponse {
public string[] Contacts { get; set; } public string[] Contacts { get; set; }
} }

View File

@ -4,7 +4,7 @@ using System.Linq;
using System.Text; using System.Text;
using System.Threading.Tasks; using System.Threading.Tasks;
namespace Models.LetsEncryptServer.Cache.Responses { namespace MaksIT.Models.LetsEncryptServer.Cache.Responses {
public class GetHostnamesResponse { public class GetHostnamesResponse {
public List<HostnameResponse> Hostnames { get; set; } public List<HostnameResponse> Hostnames { get; set; }

View File

@ -4,7 +4,7 @@ using System.Linq;
using System.Text; using System.Text;
using System.Threading.Tasks; using System.Threading.Tasks;
namespace Models.LetsEncryptServer.Cache.Responses { namespace MaksIT.Models.LetsEncryptServer.Cache.Responses {
public class HostnameResponse { public class HostnameResponse {
public string Hostname { get; set; } public string Hostname { get; set; }
public DateTime Expires { get; set; } public DateTime Expires { get; set; }

View File

@ -1,7 +1,13 @@
namespace Models.LetsEncryptServer.CertsFlow.Requests using System.ComponentModel.DataAnnotations;
namespace MaksIT.Models.LetsEncryptServer.CertsFlow.Requests
{ {
public class GetCertificatesRequest public class GetCertificatesRequest : IValidatableObject {
{ public required string[] Hostnames { get; set; }
public string[] Hostnames { get; set; }
public IEnumerable<ValidationResult> Validate(ValidationContext validationContext) {
if (Hostnames == null || Hostnames.Length == 0)
yield return new ValidationResult("Hostnames is required", new[] { nameof(Hostnames) });
} }
}
} }

View File

@ -1,7 +1,13 @@
namespace Models.LetsEncryptServer.CertsFlow.Requests using System.ComponentModel.DataAnnotations;
namespace MaksIT.Models.LetsEncryptServer.CertsFlow.Requests
{ {
public class GetOrderRequest public class GetOrderRequest : IValidatableObject {
{ public required string[] Hostnames { get; set; }
public string[] Hostnames { get; set; }
public IEnumerable<ValidationResult> Validate(ValidationContext validationContext) {
if (Hostnames == null || Hostnames.Length == 0)
yield return new ValidationResult("Hostnames is required", new[] { nameof(Hostnames) });
} }
}
} }

View File

@ -1,13 +1,21 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.Linq; using System.Linq;
using System.Text; using System.Text;
using System.Threading.Tasks; using System.Threading.Tasks;
namespace Models.LetsEncryptServer.CertsFlow.Requests namespace MaksIT.Models.LetsEncryptServer.CertsFlow.Requests {
{ public class InitRequest: IValidatableObject {
public class InitRequest public required string Description { get; set; }
{ public required string[] Contacts { get; set; }
public string[] Contacts { get; set; }
public IEnumerable<ValidationResult> Validate(ValidationContext validationContext) {
if (string.IsNullOrWhiteSpace(Description))
yield return new ValidationResult("Description is required", new[] { nameof(Description) });
if (Contacts == null || Contacts.Length == 0)
yield return new ValidationResult("Contacts is required", new[] { nameof(Contacts) });
} }
}
} }

View File

@ -1,9 +1,18 @@
namespace Models.LetsEncryptServer.CertsFlow.Requests using System.ComponentModel.DataAnnotations;
{
public class NewOrderRequest
{
public string[] Hostnames { get; set; }
public string ChallengeType { get; set; } namespace MaksIT.Models.LetsEncryptServer.CertsFlow.Requests
{
public class NewOrderRequest : IValidatableObject {
public required string[] Hostnames { get; set; }
public required string ChallengeType { get; set; }
public IEnumerable<ValidationResult> 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) });
} }
}
} }

View File

@ -0,0 +1,25 @@
using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace MaksIT.Models.LetsEncryptServer.CertsFlow.Requests {
public class PostAccountRequest : IValidatableObject {
public required string Description { get; set; }
public required string[] Contacts { get; set; }
public required string [] Hostnames { get; set; }
public IEnumerable<ValidationResult> Validate(ValidationContext validationContext) {
if (Description == null || Description.Length == 0)
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) });
if (Hostnames == null || Hostnames.Length == 0)
yield return new ValidationResult("Hostnames is required", new[] { nameof(Hostnames) });
}
}
}