mirror of
https://github.com/MAKS-IT-COM/maksit-certs-ui.git
synced 2025-12-31 04:00:03 +01:00
(refactor): fe layout review, be models review
This commit is contained in:
parent
7d7ebd298a
commit
322845d82d
6
src/ClientApp/.vscode/settings.json
vendored
6
src/ClientApp/.vscode/settings.json
vendored
@ -1,8 +1,8 @@
|
||||
{
|
||||
"editor.tabSize": 2,
|
||||
// "editor.codeActionsOnSave": {
|
||||
// "source.fixAll.eslint": true
|
||||
// },
|
||||
"editor.codeActionsOnSave": {
|
||||
"source.fixAll.eslint": true
|
||||
},
|
||||
"[javascript]": {
|
||||
"editor.defaultFormatter": "esbenp.prettier-vscode"
|
||||
},
|
||||
|
||||
@ -2,7 +2,7 @@ enum ApiRoutes {
|
||||
CACHE_ACCOUNTS = 'api/cache/accounts',
|
||||
CACHE_ACCOUNT = 'api/cache/account/{accountId}',
|
||||
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'
|
||||
|
||||
// CERTS_FLOW_CONFIGURE_CLIENT = `api/CertsFlow/ConfigureClient`,
|
||||
|
||||
@ -21,17 +21,20 @@ export default function Page() {
|
||||
const {
|
||||
value: newContact,
|
||||
error: contactError,
|
||||
handleChange: handleContactChange
|
||||
} = useValidation({
|
||||
handleChange: handleContactChange,
|
||||
reset: resetContact
|
||||
} = useValidation<string>({
|
||||
initialValue: '',
|
||||
validateFn: isValidEmail,
|
||||
errorMessage: 'Invalid email format.'
|
||||
})
|
||||
|
||||
const {
|
||||
value: newHostname,
|
||||
error: hostnameError,
|
||||
handleChange: handleHostnameChange
|
||||
} = useValidation({
|
||||
handleChange: handleHostnameChange,
|
||||
reset: resetHostname
|
||||
} = useValidation<string>({
|
||||
initialValue: '',
|
||||
validateFn: isValidHostname,
|
||||
errorMessage: 'Invalid hostname format.'
|
||||
@ -110,25 +113,25 @@ export default function Page() {
|
||||
}
|
||||
|
||||
const addContact = (accountId: string) => {
|
||||
if (newContact.trim() === '' || contactError) {
|
||||
if (newContact === '' || contactError) {
|
||||
return
|
||||
}
|
||||
|
||||
if (
|
||||
accounts
|
||||
.find((account) => account.accountId === accountId)
|
||||
?.contacts.includes(newContact.trim())
|
||||
?.contacts.includes(newContact)
|
||||
)
|
||||
return
|
||||
|
||||
setAccounts(
|
||||
accounts.map((account) =>
|
||||
account.accountId === accountId
|
||||
? { ...account, contacts: [...account.contacts, newContact.trim()] }
|
||||
? { ...account, contacts: [...account.contacts, newContact] }
|
||||
: account
|
||||
)
|
||||
)
|
||||
handleContactChange('')
|
||||
resetContact()
|
||||
}
|
||||
|
||||
const deleteHostname = (accountId: string, hostname: string) => {
|
||||
@ -153,14 +156,14 @@ export default function Page() {
|
||||
}
|
||||
|
||||
const addHostname = (accountId: string) => {
|
||||
if (newHostname.trim() === '' || hostnameError) {
|
||||
if (newHostname === '' || hostnameError) {
|
||||
return
|
||||
}
|
||||
|
||||
if (
|
||||
accounts
|
||||
.find((account) => account.accountId === accountId)
|
||||
?.hostnames.some((h) => h.hostname === newHostname.trim())
|
||||
?.hostnames.some((h) => h.hostname === newHostname)
|
||||
)
|
||||
return
|
||||
|
||||
@ -172,7 +175,7 @@ export default function Page() {
|
||||
hostnames: [
|
||||
...account.hostnames,
|
||||
{
|
||||
hostname: newHostname.trim(),
|
||||
hostname: newHostname,
|
||||
expires: new Date(),
|
||||
isUpcomingExpire: false
|
||||
}
|
||||
@ -181,7 +184,7 @@ export default function Page() {
|
||||
: account
|
||||
)
|
||||
)
|
||||
handleHostnameChange('')
|
||||
resetHostname()
|
||||
}
|
||||
|
||||
const handleSubmit = async (
|
||||
@ -279,14 +282,15 @@ export default function Page() {
|
||||
className="text-gray-700 flex justify-between items-center mb-2"
|
||||
>
|
||||
{contact}
|
||||
<button
|
||||
<CustomButton
|
||||
type="button"
|
||||
onClick={() =>
|
||||
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" />
|
||||
</button>
|
||||
</CustomButton>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
@ -301,13 +305,15 @@ export default function Page() {
|
||||
inputClassName="border p-2 rounded w-full"
|
||||
errorClassName="text-red-500 text-sm mt-1"
|
||||
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" />
|
||||
</button>
|
||||
<CustomButton
|
||||
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>
|
||||
@ -329,14 +335,15 @@ export default function Page() {
|
||||
: 'Not Upcoming'}
|
||||
</span>
|
||||
</div>
|
||||
<button
|
||||
<CustomButton
|
||||
type="button"
|
||||
onClick={() =>
|
||||
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" />
|
||||
</button>
|
||||
</CustomButton>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
@ -351,25 +358,27 @@ export default function Page() {
|
||||
inputClassName="border p-2 rounded w-full"
|
||||
errorClassName="text-red-500 text-sm mt-1"
|
||||
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" />
|
||||
</button>
|
||||
<CustomButton
|
||||
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 className="flex justify-between mt-4">
|
||||
<button
|
||||
<CustomButton
|
||||
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" />
|
||||
</button>
|
||||
</CustomButton>
|
||||
<CustomButton
|
||||
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
|
||||
</CustomButton>
|
||||
|
||||
@ -10,24 +10,26 @@ import {
|
||||
} from '@/hooks/useValidation'
|
||||
import { CustomButton, CustomInput } from '@/controls'
|
||||
import { FaTrash, FaPlus } from 'react-icons/fa'
|
||||
import { GetAccountResponse } from '@/models/letsEncryptServer/cache/responses/GetAccountResponse'
|
||||
import { deepCopy } from '../functions'
|
||||
|
||||
interface CacheAccount {
|
||||
description?: string
|
||||
contacts: string[]
|
||||
hostnames: string[]
|
||||
}
|
||||
import {
|
||||
PostAccountRequest,
|
||||
validatePostAccountRequest
|
||||
} from '@/models/letsEncryptServer/certsFlow/PostAccountRequest'
|
||||
import App from 'next/app'
|
||||
import { useAppDispatch } from '@/redux/store'
|
||||
import { showToast } from '@/redux/slices/toastSlice'
|
||||
|
||||
const RegisterPage = () => {
|
||||
const [account, setAccount] = useState<CacheAccount | null>(null)
|
||||
const [account, setAccount] = useState<PostAccountRequest | null>(null)
|
||||
|
||||
const dispatch = useAppDispatch()
|
||||
|
||||
const {
|
||||
value: newContact,
|
||||
error: contactError,
|
||||
handleChange: handleContactChange,
|
||||
reset: resetContact
|
||||
} = useValidation({
|
||||
} = useValidation<string>({
|
||||
initialValue: '',
|
||||
validateFn: isValidContact,
|
||||
errorMessage: 'Invalid contact. Must be a valid email or phone number.'
|
||||
@ -38,7 +40,7 @@ const RegisterPage = () => {
|
||||
error: hostnameError,
|
||||
handleChange: handleHostnameChange,
|
||||
reset: resetHostname
|
||||
} = useValidation({
|
||||
} = useValidation<string>({
|
||||
initialValue: '',
|
||||
validateFn: isValidHostname,
|
||||
errorMessage: 'Invalid hostname format.'
|
||||
@ -55,10 +57,17 @@ const RegisterPage = () => {
|
||||
const handleDescription = (description: string) => {}
|
||||
|
||||
const handleAddContact = () => {
|
||||
if (newContact !== '' || contactError) return
|
||||
if (
|
||||
newContact === '' ||
|
||||
account?.contacts.includes(newContact) ||
|
||||
contactError !== ''
|
||||
) {
|
||||
resetContact()
|
||||
return
|
||||
}
|
||||
|
||||
setAccount((prev) => {
|
||||
const newAccount: CacheAccount =
|
||||
const newAccount: PostAccountRequest =
|
||||
prev !== null
|
||||
? deepCopy(prev)
|
||||
: {
|
||||
@ -75,10 +84,17 @@ const RegisterPage = () => {
|
||||
}
|
||||
|
||||
const handleAddHostname = () => {
|
||||
if (newHostname !== '' || hostnameError) return
|
||||
if (
|
||||
newHostname === '' ||
|
||||
account?.hostnames.includes(newHostname) ||
|
||||
hostnameError !== ''
|
||||
) {
|
||||
resetHostname()
|
||||
return
|
||||
}
|
||||
|
||||
setAccount((prev) => {
|
||||
const newAccount: CacheAccount =
|
||||
const newAccount: PostAccountRequest =
|
||||
prev !== null
|
||||
? deepCopy(prev)
|
||||
: {
|
||||
@ -119,6 +135,17 @@ const RegisterPage = () => {
|
||||
const handleSubmit = async (e: FormEvent<HTMLFormElement>) => {
|
||||
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)
|
||||
}
|
||||
|
||||
@ -148,13 +175,13 @@ const RegisterPage = () => {
|
||||
className="text-gray-700 flex justify-between items-center mb-2"
|
||||
>
|
||||
{contact}
|
||||
<button
|
||||
<CustomButton
|
||||
type="button"
|
||||
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 />
|
||||
</button>
|
||||
</CustomButton>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
@ -169,14 +196,15 @@ const RegisterPage = () => {
|
||||
inputClassName="border p-2 rounded w-full"
|
||||
errorClassName="text-red-500 text-sm mt-1"
|
||||
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 />
|
||||
</button>
|
||||
<CustomButton
|
||||
type="button"
|
||||
onClick={handleAddContact}
|
||||
className="bg-green-500 text-white p-2 rounded ml-2"
|
||||
>
|
||||
<FaPlus />
|
||||
</CustomButton>
|
||||
</CustomInput>
|
||||
</div>
|
||||
</div>
|
||||
<div className="mb-4">
|
||||
@ -188,13 +216,13 @@ const RegisterPage = () => {
|
||||
className="text-gray-700 flex justify-between items-center mb-2"
|
||||
>
|
||||
{hostname}
|
||||
<button
|
||||
<CustomButton
|
||||
type="button"
|
||||
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 />
|
||||
</button>
|
||||
</CustomButton>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
@ -209,14 +237,15 @@ const RegisterPage = () => {
|
||||
inputClassName="border p-2 rounded w-full"
|
||||
errorClassName="text-red-500 text-sm mt-1"
|
||||
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 />
|
||||
</button>
|
||||
<CustomButton
|
||||
type="button"
|
||||
onClick={handleAddHostname}
|
||||
className="bg-green-500 text-white p-2 rounded ml-2"
|
||||
>
|
||||
<FaPlus />
|
||||
</CustomButton>
|
||||
</CustomInput>
|
||||
</div>
|
||||
</div>
|
||||
<CustomButton
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
// components/CustomInput.tsx
|
||||
'use client'
|
||||
import React from 'react'
|
||||
import React, { FC } from 'react'
|
||||
|
||||
interface CustomInputProps {
|
||||
value: string
|
||||
@ -12,9 +12,10 @@ interface CustomInputProps {
|
||||
inputClassName?: string
|
||||
errorClassName?: string
|
||||
className?: string
|
||||
children?: React.ReactNode // Added for additional elements
|
||||
}
|
||||
|
||||
const CustomInput: React.FC<CustomInputProps> = ({
|
||||
const CustomInput: FC<CustomInputProps> = ({
|
||||
value,
|
||||
onChange,
|
||||
placeholder = '',
|
||||
@ -23,23 +24,29 @@ const CustomInput: React.FC<CustomInputProps> = ({
|
||||
title,
|
||||
inputClassName = '',
|
||||
errorClassName = '',
|
||||
className = ''
|
||||
className = '',
|
||||
children // Added for additional elements
|
||||
}) => {
|
||||
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
onChange?.(e.target.value)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={className}>
|
||||
{title && <label>{title}</label>}
|
||||
<input
|
||||
type={type}
|
||||
value={value}
|
||||
onChange={handleChange}
|
||||
placeholder={placeholder}
|
||||
className={inputClassName}
|
||||
/>
|
||||
{error && <p className={errorClassName}>{error}</p>}
|
||||
<div className={`flex flex-col ${className}`}>
|
||||
{title && <label className="mb-1">{title}</label>}
|
||||
<div className="flex items-center">
|
||||
<input
|
||||
type={type}
|
||||
value={value}
|
||||
onChange={handleChange}
|
||||
placeholder={placeholder}
|
||||
className={`flex-grow ${inputClassName}`}
|
||||
/>
|
||||
{children && <div className="ml-2">{children}</div>}
|
||||
</div>
|
||||
{error && (
|
||||
<p className={`text-red-500 mt-1 ${errorClassName}`}>{error}</p>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
import { useState, useEffect } from 'react'
|
||||
import { useState, useEffect, useCallback } from 'react'
|
||||
|
||||
// Helper functions for validation
|
||||
const isValidEmail = (email: string) => {
|
||||
@ -21,38 +21,68 @@ const isValidHostname = (hostname: string) => {
|
||||
}
|
||||
|
||||
// Props interface for useValidation hook
|
||||
interface UseValidationProps {
|
||||
initialValue: string
|
||||
validateFn: (value: string) => boolean
|
||||
interface UseValidationProps<T> {
|
||||
initialValue: T
|
||||
validateFn: (value: T) => boolean
|
||||
errorMessage: string
|
||||
emptyFieldMessage?: string // Optional custom message for empty fields
|
||||
defaultResetValue?: T // Optional default reset value
|
||||
}
|
||||
|
||||
// Custom hook for input validation
|
||||
const useValidation = ({
|
||||
initialValue,
|
||||
validateFn,
|
||||
errorMessage
|
||||
}: UseValidationProps) => {
|
||||
const [value, setValue] = useState(initialValue)
|
||||
const useValidation = <T extends string | number | Date>(
|
||||
props: UseValidationProps<T>
|
||||
) => {
|
||||
const {
|
||||
initialValue,
|
||||
validateFn,
|
||||
errorMessage,
|
||||
emptyFieldMessage = 'This field cannot be empty.', // Default message
|
||||
defaultResetValue
|
||||
} = props
|
||||
|
||||
const [value, setValue] = useState<T>(initialValue)
|
||||
const [error, setError] = useState('')
|
||||
|
||||
const handleChange = (newValue: string) => {
|
||||
console.log(newValue)
|
||||
setValue(newValue)
|
||||
if (newValue.trim() === '') {
|
||||
setError('This field cannot be empty.')
|
||||
} else if (!validateFn(newValue.trim())) {
|
||||
setError(errorMessage)
|
||||
} else {
|
||||
setError('')
|
||||
}
|
||||
}
|
||||
const handleChange = useCallback(
|
||||
(newValue: T) => {
|
||||
setValue(newValue)
|
||||
const stringValue =
|
||||
newValue instanceof Date
|
||||
? newValue.toISOString()
|
||||
: newValue.toString().trim()
|
||||
if (stringValue === '') {
|
||||
setError(emptyFieldMessage)
|
||||
} else if (!validateFn(newValue)) {
|
||||
setError(errorMessage)
|
||||
} else {
|
||||
setError('')
|
||||
}
|
||||
},
|
||||
[emptyFieldMessage, errorMessage, validateFn]
|
||||
)
|
||||
|
||||
useEffect(() => {
|
||||
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 {
|
||||
|
||||
5
src/ClientApp/models/PatchAction.ts
Normal file
5
src/ClientApp/models/PatchAction.ts
Normal 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
|
||||
}
|
||||
5
src/ClientApp/models/PatchOperation.ts
Normal file
5
src/ClientApp/models/PatchOperation.ts
Normal file
@ -0,0 +1,5 @@
|
||||
enum PatchOperation {
|
||||
Add,
|
||||
Remove,
|
||||
Replace
|
||||
}
|
||||
6
src/ClientApp/models/letsEncryptServer/cache/requests/PatchAccountRequest.ts
vendored
Normal file
6
src/ClientApp/models/letsEncryptServer/cache/requests/PatchAccountRequest.ts
vendored
Normal file
@ -0,0 +1,6 @@
|
||||
import { PatchAction } from '@/models/PatchAction'
|
||||
|
||||
export interface PatchAccountRequest {
|
||||
description?: PatchAction<string>
|
||||
contacts?: PatchAction<string>[]
|
||||
}
|
||||
5
src/ClientApp/models/letsEncryptServer/cache/requests/PatchContactsRequest.ts
vendored
Normal file
5
src/ClientApp/models/letsEncryptServer/cache/requests/PatchContactsRequest.ts
vendored
Normal file
@ -0,0 +1,5 @@
|
||||
import { PatchAction } from '@/models/PatchAction'
|
||||
|
||||
export interface PatchContactsRequest {
|
||||
contacts: PatchAction<string>[]
|
||||
}
|
||||
3
src/ClientApp/models/letsEncryptServer/cache/requests/PostContactsRequest.ts
vendored
Normal file
3
src/ClientApp/models/letsEncryptServer/cache/requests/PostContactsRequest.ts
vendored
Normal file
@ -0,0 +1,3 @@
|
||||
export interface PostContactsRequest {
|
||||
contacts: string[]
|
||||
}
|
||||
4
src/ClientApp/models/letsEncryptServer/cache/requests/PutAccountRequest.ts
vendored
Normal file
4
src/ClientApp/models/letsEncryptServer/cache/requests/PutAccountRequest.ts
vendored
Normal file
@ -0,0 +1,4 @@
|
||||
export interface PutAccountRequest {
|
||||
description: string
|
||||
contacts: string[]
|
||||
}
|
||||
3
src/ClientApp/models/letsEncryptServer/cache/requests/PutContactsRequest.ts
vendored
Normal file
3
src/ClientApp/models/letsEncryptServer/cache/requests/PutContactsRequest.ts
vendored
Normal file
@ -0,0 +1,3 @@
|
||||
export interface PutContactsRequest {
|
||||
contacts: string[]
|
||||
}
|
||||
@ -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 }
|
||||
@ -1,5 +1,11 @@
|
||||
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'
|
||||
|
||||
export const store = configureStore({
|
||||
@ -11,3 +17,9 @@ export const store = configureStore({
|
||||
|
||||
export type RootState = ReturnType<typeof store.getState>
|
||||
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>()
|
||||
|
||||
@ -4,9 +4,8 @@ using DomainResults.Common;
|
||||
|
||||
|
||||
using MaksIT.LetsEncryptServer.Services;
|
||||
using Models.LetsEncryptServer.CertsFlow.Requests;
|
||||
using Models.LetsEncryptServer.Cache.Responses;
|
||||
using MaksIT.LetsEncrypt.Entities;
|
||||
using MaksIT.Models.LetsEncryptServer.CertsFlow.Requests;
|
||||
|
||||
namespace MaksIT.LetsEncryptServer.BackgroundServices {
|
||||
public class AutoRenewal : BackgroundService {
|
||||
@ -62,7 +61,7 @@ namespace MaksIT.LetsEncryptServer.BackgroundServices {
|
||||
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)
|
||||
return renewResult;
|
||||
|
||||
@ -71,7 +70,7 @@ namespace MaksIT.LetsEncryptServer.BackgroundServices {
|
||||
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();
|
||||
if (!configureClientResult.IsSuccess || sessionId == null) {
|
||||
LogErrors(configureClientResult.Errors);
|
||||
@ -81,6 +80,7 @@ namespace MaksIT.LetsEncryptServer.BackgroundServices {
|
||||
var sessionIdValue = sessionId.Value;
|
||||
|
||||
var (_, initResult) = await _certsFlowService.InitAsync(sessionIdValue, accountId, new InitRequest {
|
||||
Description = description,
|
||||
Contacts = contacts
|
||||
});
|
||||
if (!initResult.IsSuccess) {
|
||||
|
||||
@ -57,7 +57,7 @@ public class CacheController : ControllerBase {
|
||||
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) {
|
||||
var result = await _cacheService.DeleteContactAsync(accountId, index);
|
||||
return result.ToActionResult();
|
||||
|
||||
@ -2,11 +2,11 @@ using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.Extensions.Options;
|
||||
using DomainResults.Mvc;
|
||||
using MaksIT.LetsEncryptServer.Services;
|
||||
using Models.LetsEncryptServer.CertsFlow.Requests;
|
||||
using MaksIT.Models.LetsEncryptServer.CertsFlow.Requests;
|
||||
|
||||
namespace MaksIT.LetsEncryptServer.Controllers {
|
||||
[ApiController]
|
||||
[Route("api/[controller]")]
|
||||
[Route("api/certs")]
|
||||
public class CertsFlowController : ControllerBase {
|
||||
private readonly Configuration _appSettings;
|
||||
private readonly ICertsFlowService _certsFlowService;
|
||||
@ -34,7 +34,7 @@ namespace MaksIT.LetsEncryptServer.Controllers {
|
||||
/// </summary>
|
||||
/// <param name="sessionId">Session ID</param>
|
||||
/// <returns>Terms of service</returns>
|
||||
[HttpGet("terms-of-service/{sessionId}")]
|
||||
[HttpGet("{sessionId}/terms-of-service")]
|
||||
public IActionResult TermsOfService(Guid sessionId) {
|
||||
var result = _certsFlowService.GetTermsOfService(sessionId);
|
||||
return result.ToActionResult();
|
||||
|
||||
@ -13,7 +13,7 @@ namespace MaksIT.LetsEncryptServer.Controllers;
|
||||
public class WellKnownController : ControllerBase {
|
||||
|
||||
private readonly Configuration _appSettings;
|
||||
private readonly ICertsFlowServiceBase _certsFlowService;
|
||||
private readonly ICertsRestChallengeService _certsFlowService;
|
||||
|
||||
private readonly string _acmePath = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "acme");
|
||||
|
||||
|
||||
@ -6,7 +6,7 @@ using MaksIT.Core.Extensions;
|
||||
using MaksIT.LetsEncrypt.Entities;
|
||||
using MaksIT.Models;
|
||||
using MaksIT.Models.LetsEncryptServer.Cache.Requests;
|
||||
using Models.LetsEncryptServer.Cache.Responses;
|
||||
using MaksIT.Models.LetsEncryptServer.Cache.Responses;
|
||||
|
||||
namespace MaksIT.LetsEncryptServer.Services;
|
||||
|
||||
|
||||
@ -6,16 +6,21 @@ using DomainResults.Common;
|
||||
|
||||
using MaksIT.LetsEncrypt.Entities;
|
||||
using MaksIT.LetsEncrypt.Services;
|
||||
using Models.LetsEncryptServer.CertsFlow.Requests;
|
||||
using MaksIT.Models.LetsEncryptServer.CertsFlow.Requests;
|
||||
|
||||
|
||||
namespace MaksIT.LetsEncryptServer.Services;
|
||||
|
||||
public interface ICertsFlowServiceBase {
|
||||
public interface ICertsInternalService {
|
||||
|
||||
}
|
||||
|
||||
|
||||
public interface ICertsRestChallengeService {
|
||||
(string?, IDomainResult) AcmeChallenge(string fileName);
|
||||
}
|
||||
|
||||
public interface ICertsFlowService : ICertsFlowServiceBase {
|
||||
public interface ICertsRestService {
|
||||
Task<(Guid?, IDomainResult)> ConfigureClientAsync();
|
||||
(string?, IDomainResult) GetTermsOfService(Guid sessionId);
|
||||
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> GetCertificatesAsync(Guid sessionId, GetCertificatesRequest requestData);
|
||||
Task<(Dictionary<string, string>?, IDomainResult)> ApplyCertificatesAsync(Guid sessionId, GetCertificatesRequest requestData);
|
||||
}
|
||||
}
|
||||
|
||||
public interface ICertsFlowService
|
||||
: ICertsInternalService,
|
||||
ICertsRestService,
|
||||
ICertsRestChallengeService {}
|
||||
|
||||
public class CertsFlowService : ICertsFlowService {
|
||||
|
||||
|
||||
@ -1,12 +1,21 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace MaksIT.Models.LetsEncryptServer.Cache.Requests {
|
||||
public class PutAccountRequest {
|
||||
public string Description { get; set; }
|
||||
public string[] Contacts { get; set; }
|
||||
public class PutAccountRequest : IValidatableObject {
|
||||
public required string Description { 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) });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -4,7 +4,7 @@ using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Models.LetsEncryptServer.Cache.Responses {
|
||||
namespace MaksIT.Models.LetsEncryptServer.Cache.Responses {
|
||||
public class GetAccountResponse {
|
||||
public Guid AccountId { get; set; }
|
||||
|
||||
|
||||
@ -4,7 +4,7 @@ using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Models.LetsEncryptServer.Cache.Responses {
|
||||
namespace MaksIT.Models.LetsEncryptServer.Cache.Responses {
|
||||
public class GetContactsResponse {
|
||||
public string[] Contacts { get; set; }
|
||||
}
|
||||
|
||||
@ -4,7 +4,7 @@ using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Models.LetsEncryptServer.Cache.Responses {
|
||||
namespace MaksIT.Models.LetsEncryptServer.Cache.Responses {
|
||||
|
||||
public class GetHostnamesResponse {
|
||||
public List<HostnameResponse> Hostnames { get; set; }
|
||||
|
||||
@ -4,7 +4,7 @@ using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Models.LetsEncryptServer.Cache.Responses {
|
||||
namespace MaksIT.Models.LetsEncryptServer.Cache.Responses {
|
||||
public class HostnameResponse {
|
||||
public string Hostname { get; set; }
|
||||
public DateTime Expires { get; set; }
|
||||
|
||||
@ -1,7 +1,13 @@
|
||||
namespace Models.LetsEncryptServer.CertsFlow.Requests
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
|
||||
namespace MaksIT.Models.LetsEncryptServer.CertsFlow.Requests
|
||||
{
|
||||
public class GetCertificatesRequest
|
||||
{
|
||||
public string[] Hostnames { get; set; }
|
||||
public class GetCertificatesRequest : IValidatableObject {
|
||||
public required 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) });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,7 +1,13 @@
|
||||
namespace Models.LetsEncryptServer.CertsFlow.Requests
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
|
||||
namespace MaksIT.Models.LetsEncryptServer.CertsFlow.Requests
|
||||
{
|
||||
public class GetOrderRequest
|
||||
{
|
||||
public string[] Hostnames { get; set; }
|
||||
public class GetOrderRequest : IValidatableObject {
|
||||
public required 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) });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,13 +1,21 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Models.LetsEncryptServer.CertsFlow.Requests
|
||||
{
|
||||
public class InitRequest
|
||||
{
|
||||
public string[] Contacts { get; set; }
|
||||
namespace MaksIT.Models.LetsEncryptServer.CertsFlow.Requests {
|
||||
public class InitRequest: IValidatableObject {
|
||||
public required string Description { 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) });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,9 +1,18 @@
|
||||
namespace Models.LetsEncryptServer.CertsFlow.Requests
|
||||
{
|
||||
public class NewOrderRequest
|
||||
{
|
||||
public string[] Hostnames { get; set; }
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
|
||||
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) });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -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) });
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user