-
-
+
{type === 'password' ? (
// Wrapper che contiene input e bottone show/hide, ma bottone solo se c'è contenuto
@@ -111,9 +110,7 @@ const TextBoxComponent: FC
= (props) => {
disabled={disabled}
/>
)}
-
- {errorText && {errorText}
}
-
+
)
}
diff --git a/src/MaksIT.WebUI/src/components/editors/index.ts b/src/MaksIT.WebUI/src/components/editors/index.ts
index 0b9d4ba..121f270 100644
--- a/src/MaksIT.WebUI/src/components/editors/index.ts
+++ b/src/MaksIT.WebUI/src/components/editors/index.ts
@@ -1,3 +1,7 @@
+import {
+ FieldContainer,
+} from './FieldContainer'
+
import { ButtonComponent } from './ButtonComponent'
import { CheckBoxComponent } from './CheckBoxComponent'
import { TextBoxComponent } from './TextBoxComponent'
@@ -13,6 +17,7 @@ import { FileUploadComponent } from './FileUploadComponent'
export {
+ FieldContainer as EditorWrapper,
ButtonComponent,
CheckBoxComponent,
DateTimePickerComponent,
diff --git a/src/MaksIT.WebUI/src/forms/EditAccount.tsx b/src/MaksIT.WebUI/src/forms/EditAccount.tsx
new file mode 100644
index 0000000..d8394c7
--- /dev/null
+++ b/src/MaksIT.WebUI/src/forms/EditAccount.tsx
@@ -0,0 +1,96 @@
+import { FC } from 'react'
+import { FormContainer, FormContent, FormFooter, FormHeader } from '../components/FormLayout'
+import { ButtonComponent, CheckBoxComponent, TextBoxComponent } from '../components/editors'
+import { GetAccountResponse } from '../models/letsEncryptServer/account/responses/GetAccountResponse'
+import { useFormState } from '../hooks/useFormState'
+import { boolean, object, Schema, string } from 'zod'
+
+
+interface EditAccountFormProps {
+ description: string
+ disabled: boolean
+}
+
+const RegisterFormProto = (): EditAccountFormProps => ({
+ description: '',
+ disabled: false
+})
+
+const RegisterFormSchema: Schema
= object({
+ description: string(),
+ disabled: boolean()
+})
+
+interface EditAccountProps {
+ accountId: string
+ onSubmitted?: (entity: GetAccountResponse) => void
+ cancelEnabled?: boolean
+ onCancel?: () => void
+}
+
+const EditAccount: FC = (props) => {
+ const {
+ accountId,
+ onSubmitted,
+ cancelEnabled,
+ onCancel
+ } = props
+
+
+ const {
+ formState,
+ errors,
+ formIsValid,
+ handleInputChange
+ } = useFormState({
+ initialState: RegisterFormProto(),
+ validationSchema: RegisterFormSchema
+ })
+
+ const handleSubmit = () => {
+ // onSubmitted && onSubmitted(updatedEntity)
+ }
+
+ const handleCancel = () => {
+ onCancel?.()
+ }
+
+ return
+ Edit Account {accountId}
+
+
+ handleInputChange('description', e.target.value)}
+ placeholder={'Account Description'}
+ errorText={errors.description}
+ />
+ handleInputChange('disabled', e.target.checked)}
+ errorText={errors.disabled}
+ />
+
+ Contacts:
+
+
+
+
+ }
+ leftChildren={
+ cancelEnabled &&
+ }
+ />
+
+}
+
+export {
+ EditAccount
+}
\ No newline at end of file
diff --git a/src/MaksIT.WebUI/src/forms/Home.tsx b/src/MaksIT.WebUI/src/forms/Home.tsx
index ac77890..929881d 100644
--- a/src/MaksIT.WebUI/src/forms/Home.tsx
+++ b/src/MaksIT.WebUI/src/forms/Home.tsx
@@ -1,32 +1,31 @@
-import { FC, useEffect, useState } from 'react'
+import { FC, useCallback, useEffect, useState } from 'react'
import { FormContainer, FormContent, FormFooter, FormHeader } from '../components/FormLayout'
import { ButtonComponent, CheckBoxComponent, RadioGroupComponent, SelectBoxComponent } from '../components/editors'
import { CacheAccount } from '../entities/CacheAccount'
import { GetAccountResponse } from '../models/letsEncryptServer/account/responses/GetAccountResponse'
-import { deleteData, getData } from '../axiosConfig'
+import { deleteData, getData, postData } from '../axiosConfig'
import { ApiRoutes, GetApiRoute } from '../AppMap'
-import { enumToArr, formatISODateString } from '../functions'
-import { ChallengeType } from '../entities/ChallengeType'
-import { Radio } from 'lucide-react'
+import { formatISODateString } from '../functions'
+import { addToast } from '../components/Toast/addToast'
+import { Offcanvas } from '../components/Offcanvas'
+import { EditAccount } from './EditAccount'
const Home: FC = () => {
const [rawd, setRawd] = useState([])
- const [editingAccount, setEditingAccount] = useState(
- null
- )
+ const [accountId, setAccountId] = useState(undefined)
- useEffect(() => {
- console.log(GetApiRoute(ApiRoutes.ACCOUNTS).route)
-
- getData(GetApiRoute(ApiRoutes.ACCOUNTS).route).then((response) => {
+ const loadData = useCallback(() => {
+ getData(GetApiRoute(ApiRoutes.ACCOUNTS).route).then((response) => {
if (!response) return
-
setRawd(response)
})
-
}, [])
+ useEffect(() => {
+ loadData()
+ }, [loadData])
+
const handleAccountUpdate = (updatedAccount: CacheAccount) => {
// setAccounts(
// accounts.map((account) =>
@@ -37,118 +36,144 @@ const Home: FC = () => {
// )
}
- const deleteAccount = (accountId: string) => {
+ const handleDeleteAccount = (accountId: string) => {
deleteData(
GetApiRoute(ApiRoutes.ACCOUNT_DELETE)
.route.replace('{accountId}', accountId)
- ).then((result) => {
- if (!result) return
-
+ ).then(_ => {
setRawd(rawd.filter((account) => account.accountId !== accountId))
})
}
- return
- Home
-
-
- {rawd.length === 0 ?
-
+ const handleEditCancel = () => {
+ setAccountId(undefined)
+ }
+
+ const handleRedeployCerts = (accountId: string) => {
+ postData
(GetApiRoute(ApiRoutes.CERTS_FLOW_CERTIFICATES_APPLY).route
+ .replace('{accountId}', accountId)
+ ).then(response => {
+ if (!response?.message) return
+
+ addToast(response?.message, 'info')
+ })
+ }
+
+ const handleOnSubmitted = (_: GetAccountResponse) => {
+ setAccountId(undefined)
+ loadData()
+ }
+
+ return <>
+
+ Home
+
+
+ {rawd.length === 0 ?
+
No accounts registered.
-
:
- rawd.map((acc) => (
-
-
-
+
:
+ rawd.map((acc) => (
+
+
+
Account: {acc.accountId}
-
-
deleteAccount(acc.accountId)}
- label={'Delete'}
- buttonHierarchy={'error'}
- />
- setEditingAccount(acc)}
- label={'Edit'}
- />
-
+
+ handleDeleteAccount(acc.accountId)}
+ label={'Delete Account'}
+ buttonHierarchy={'error'}
+ />
+ handleRedeployCerts(acc.accountId)}
+ />
+ setAccountId(acc.accountId)}
+ label={'Edit'}
+ />
+
Description: {acc.description}
-
-
- Contacts:
-
- {acc.contacts.map((contact) => (
- -
- {contact}
-
- ))}
-
-
- Hostnames:
-
- {acc.hostnames?.map((hostname) => (
- -
- {hostname.hostname}
- {formatISODateString(hostname.expires)}
-
- {hostname.isUpcomingExpire
- ? 'Upcoming'
- : 'Not Upcoming'}
+
+
+ Contacts:
+
+ {acc.contacts.map((contact) => (
+ -
+ {contact}
+
+ ))}
+
+
+ Hostnames:
+
+ {acc.hostnames?.map((hostname) => (
+ -
+ {hostname.hostname}
+ Exp: {formatISODateString(hostname.expires)}
+
+ {hostname.isUpcomingExpire
+ ? 'Upcoming'
+ : 'Not Upcoming'}
+
-
-
-
+
-
- ))}
-
-
-
+
+ ))}
+
+
+
-
- ))}
+ ))}
+
+
+
+
-
-
+
+ {accountId && }
-
-
-
-
-
-
+
+ >
}
export { Home }
\ No newline at end of file
diff --git a/src/MaksIT.WebUI/src/forms/LetsEncryptTermsOfService.tsx b/src/MaksIT.WebUI/src/forms/LetsEncryptTermsOfService.tsx
new file mode 100644
index 0000000..5bcb585
--- /dev/null
+++ b/src/MaksIT.WebUI/src/forms/LetsEncryptTermsOfService.tsx
@@ -0,0 +1,120 @@
+import { FC, useEffect, useRef, useState } from 'react'
+
+import { FormContainer, FormContent, FormFooter, FormHeader } from '../components/FormLayout'
+import { ApiRoutes, GetApiRoute } from '../AppMap'
+import { getData, postData } from '../axiosConfig'
+
+import { pdfjs, Document, Page } from 'react-pdf'
+import 'react-pdf/dist/Page/AnnotationLayer.css'
+import 'react-pdf/dist/Page/TextLayer.css'
+
+import type { PDFDocumentProxy } from 'pdfjs-dist'
+
+const LetsEncryptTermsOfService: FC = () => {
+
+ const [pdfUrl, setPdfUrl] = useState
(null)
+ const [objectUrl, setObjectUrl] = useState(null)
+ const [loading, setLoading] = useState(true)
+ const [error, setError] = useState(null)
+ const [numPages, setNumPages] = useState()
+ const containerRef = useRef(null)
+ const [containerWidth, setContainerWidth] = useState()
+
+ // Set up pdfjs worker
+ pdfjs.GlobalWorkerOptions.workerSrc = new URL(
+ 'pdfjs-dist/build/pdf.worker.min.mjs',
+ import.meta.url,
+ ).toString()
+ useEffect(() => {
+ const handleResize = () => {
+ if (containerRef.current) {
+ const { x } = containerRef.current.getBoundingClientRect()
+ const width = window.innerWidth - x
+ setContainerWidth(width)
+ }
+ }
+ handleResize()
+ window.addEventListener('resize', handleResize)
+ return () => {
+ window.removeEventListener('resize', handleResize)
+ }
+ }, [])
+
+ useEffect(() => {
+ setLoading(true)
+ postData<{ [key: string]: boolean }, string>(GetApiRoute(ApiRoutes.CERTS_FLOW_CONFIGURE_CLIENT).route, {
+ isStaging: true
+ })
+ .then(response => {
+ if (!response) return
+ return getData(GetApiRoute(ApiRoutes.CERTS_FLOW_TERMS_OF_SERVICE).route.replace('{sessionId}', response))
+ })
+ .then(base64Pdf => {
+ if (base64Pdf) {
+ setPdfUrl(base64Pdf)
+ } else {
+ setError('Failed to retrieve PDF.')
+ }
+ })
+ .catch(() => setError('Failed to load Terms of Service.'))
+ .finally(() => setLoading(false))
+ }, [])
+
+ // Convert base64 to Blob and create object URL
+ useEffect(() => {
+ if (!pdfUrl) return
+ // Remove data URL prefix if present
+ const base64 = pdfUrl.replace(/^data:application\/pdf;base64,/, '')
+ const byteCharacters = atob(base64)
+ const byteNumbers = new Array(byteCharacters.length)
+ for (let i = 0; i < byteCharacters.length; i++) {
+ byteNumbers[i] = byteCharacters.charCodeAt(i)
+ }
+ const byteArray = new Uint8Array(byteNumbers)
+ const blob = new Blob([byteArray], { type: 'application/pdf' })
+ const url = URL.createObjectURL(blob)
+ setObjectUrl(url)
+ return () => {
+ URL.revokeObjectURL(url)
+ }
+ }, [pdfUrl])
+
+ const handleDocumentLoadSuccess = ({ numPages: nextNumPages }: PDFDocumentProxy): void => {
+ setNumPages(nextNumPages)
+ }
+
+ return (
+
+ Let's Encrypt Terms of Service
+
+ {loading && Loading Terms of Service...
}
+ {error && {error}
}
+ {objectUrl && (
+
+
+ {numPages ? (
+ Array.from(new Array(numPages), (_, index) => (
+
+
0 ? containerWidth : 600}
+ />
+
+ Page {index + 1} / {numPages}
+
+
+ ))
+ ) : (
+ Loading PDF pages...
+ )}
+
+
+ )}
+
+
+
+ )
+}
+
+export { LetsEncryptTermsOfService }
\ No newline at end of file
diff --git a/src/MaksIT.WebUI/src/forms/Register.tsx b/src/MaksIT.WebUI/src/forms/Register.tsx
index a25f411..65c8bb9 100644
--- a/src/MaksIT.WebUI/src/forms/Register.tsx
+++ b/src/MaksIT.WebUI/src/forms/Register.tsx
@@ -11,6 +11,8 @@ import { enumToArr } from '../functions'
import { PostAccountRequest, PostAccountRequestSchema } from '../models/letsEncryptServer/account/requests/PostAccountRequest'
import { addToast } from '../components/Toast/addToast'
import { useNavigate } from 'react-router-dom'
+import { PlusIcon, TrashIcon } from 'lucide-react'
+import { FieldContainer } from '../components/editors/FieldContainer'
interface RegisterFormProps {
@@ -90,7 +92,7 @@ const Register: FC = () => {
return
}
- postData(GetApiRoute(ApiRoutes.ACCOUNT_POST).route, request.data)
+ postData(GetApiRoute(ApiRoutes.ACCOUNT_POST).route, request.data, 120000)
.then(response => {
if (!response) return
@@ -115,14 +117,16 @@ const Register: FC = () => {
{formState.contacts.map((contact) => (
{contact}
- {
- const updatedContacts = formState.contacts.filter(c => c !== contact)
- handleInputChange('contacts', updatedContacts)
- }}
- />
+
+ {
+ const updatedContacts = formState.contacts.filter(c => c !== contact)
+ handleInputChange('contacts', updatedContacts)
+ }}
+ >
+
+
+
))}
@@ -140,19 +144,23 @@ const Register: FC = () => {
type={'text'}
errorText={errors.contact}
/>
- {
- handleInputChange('contacts', [...formState.contacts, formState.contact])
- handleInputChange('contact', '')
- }}
- disabled={formState.contact.trim() === ''}
- />
+
+ {
+ handleInputChange('contacts', [...formState.contacts, formState.contact])
+ handleInputChange('contact', '')
+ }}
+ disabled={formState.contact.trim() === ''}
+ >
+
+
+
({ value: ct.value, label: ct.displayValue }))}
+ options={enumToArr(ChallengeType)
+ .map(ct => ({ value: ct.value, label: ct.displayValue }))
+ .filter(ct => ct.value !== ChallengeType.dns01)}
value={formState.challengeType}
placeholder={'Select Challenge Type'}
onChange={(e) => handleInputChange('challengeType', e.target.value)}
@@ -164,14 +172,16 @@ const Register: FC = () => {
{formState.hostnames.map((hostname) => (
{hostname}
- {
- const updatedHostnames = formState.hostnames.filter(h => h !== hostname)
- handleInputChange('hostnames', updatedHostnames)
- }}
- />
+
+ {
+ const updatedHostnames = formState.hostnames.filter(h => h !== hostname)
+ handleInputChange('hostnames', updatedHostnames)
+ }}
+ >
+
+
+
))}
@@ -189,15 +199,17 @@ const Register: FC = () => {
type={'text'}
errorText={errors.hostname}
/>
- {
- handleInputChange('hostnames', [...formState.hostnames, formState.hostname])
- handleInputChange('hostname', '')
- }}
- disabled={formState.hostname.trim() === ''}
- />
+
+ {
+ handleInputChange('hostnames', [...formState.hostnames, formState.hostname])
+ handleInputChange('hostname', '')
+ }}
+ disabled={formState.hostname.trim() === ''}
+ >
+
+
+
{
const [files, setFiles] = useState([])
+ const hadnleTestAgent = () => {
+ getData(GetApiRoute(ApiRoutes.AGENT_TEST).route)
+ .then((response) => {
+ if (!response) return
+
+ addToast(response?.message, 'info')
+ })
+ }
+
return
Utilities
@@ -13,15 +25,8 @@ const Utilities: FC = () => {
{}}
- />
-
- {}}
+ buttonHierarchy={'warning'}
+ onClick={hadnleTestAgent}
/>
{
onChange={setFiles}
/>
-
+ {}}
+ />
+
+ {}}
+ />
-
diff --git a/src/MaksIT.WebUI/src/functions/deep/deepPatternMatch.ts b/src/MaksIT.WebUI/src/functions/deep/deepPatternMatch.ts
new file mode 100644
index 0000000..a0cb2b1
--- /dev/null
+++ b/src/MaksIT.WebUI/src/functions/deep/deepPatternMatch.ts
@@ -0,0 +1,16 @@
+const deepPatternMatch = (pattern: T, obj: unknown): boolean => {
+ if (typeof obj !== 'object' || obj === null) return false
+ const objKeys = Object.keys(obj as object)
+ const patternKeys = Object.keys(pattern)
+ // obj must not have more keys than pattern
+ if (objKeys.length > patternKeys.length) return false
+ for (const key of objKeys) {
+ if (!(key in pattern)) return false
+ if (typeof (obj as T)[key as keyof T] !== typeof pattern[key as keyof T]) return false
+ }
+ return true
+}
+
+export {
+ deepPatternMatch
+}
diff --git a/src/MaksIT.WebUI/src/functions/deep/index.ts b/src/MaksIT.WebUI/src/functions/deep/index.ts
index 9a6286f..ab34996 100644
--- a/src/MaksIT.WebUI/src/functions/deep/index.ts
+++ b/src/MaksIT.WebUI/src/functions/deep/index.ts
@@ -5,7 +5,7 @@ import {
} from './deepDelta'
import { deepEqualArrays, deepEqual } from './deepEqual'
import { deepMerge } from './deepMerge'
-
+import { deepPatternMatch } from './deepPatternMatch'
export {
@@ -14,5 +14,6 @@ export {
deltaHasOperations,
deepEqualArrays,
deepEqual,
- deepMerge
+ deepMerge,
+ deepPatternMatch
}
\ No newline at end of file
diff --git a/src/MaksIT.WebUI/src/functions/index.ts b/src/MaksIT.WebUI/src/functions/index.ts
index 38c31d9..c46b784 100644
--- a/src/MaksIT.WebUI/src/functions/index.ts
+++ b/src/MaksIT.WebUI/src/functions/index.ts
@@ -9,6 +9,7 @@ import {
deltaHasOperations,
deepEqual,
deepMerge,
+ deepPatternMatch
} from './deep'
import {
@@ -39,6 +40,7 @@ export {
deltaHasOperations,
deepEqual,
deepMerge,
+ deepPatternMatch,
enumToArr,
enumToObj,
diff --git a/src/MaksIT.WebUI/src/models/ProblemDetails.ts b/src/MaksIT.WebUI/src/models/ProblemDetails.ts
new file mode 100644
index 0000000..305ab84
--- /dev/null
+++ b/src/MaksIT.WebUI/src/models/ProblemDetails.ts
@@ -0,0 +1,15 @@
+export interface ProblemDetails {
+ status?: number;
+ title?: string;
+ detail?: string;
+ instance?: string;
+ extensions: { [key: string]: never };
+}
+
+export const ProblemDetailsProto = (): ProblemDetails => ({
+ status: undefined,
+ title: undefined,
+ detail: undefined,
+ instance: undefined,
+ extensions: {}
+})
\ No newline at end of file
diff --git a/src/MaksIT.WebUI/src/pages/LetsEncryptTermsOfServicePage.tsx b/src/MaksIT.WebUI/src/pages/LetsEncryptTermsOfServicePage.tsx
new file mode 100644
index 0000000..6f214fb
--- /dev/null
+++ b/src/MaksIT.WebUI/src/pages/LetsEncryptTermsOfServicePage.tsx
@@ -0,0 +1,7 @@
+import { LetsEncryptTermsOfService } from '../forms/LetsEncryptTermsOfService'
+
+
+const LetsEncryptTermsOfServicePage = () => {
+ return
+}
+export { LetsEncryptTermsOfServicePage }
\ No newline at end of file