mirror of
https://github.com/MAKS-IT-COM/maksit-certs-ui.git
synced 2025-12-31 04:00:03 +01:00
(feature): new account ui implementation
This commit is contained in:
parent
c817bc1038
commit
a7ea95fd2b
@ -81,13 +81,12 @@ const Layout: FC<{ children: React.ReactNode }> = ({ children }) => {
|
|||||||
/>
|
/>
|
||||||
|
|
||||||
<div className="flex flex-col flex-1 overflow-hidden">
|
<div className="flex flex-col flex-1 overflow-hidden">
|
||||||
<TopMenu onToggleOffCanvas={toggleOffCanvas} />
|
<TopMenu />
|
||||||
<main className="flex-1 p-4 overflow-y-auto">{children}</main>
|
<main className="flex-1 p-4 overflow-y-auto">{children}</main>
|
||||||
<Footer className="flex-shrink-0" />
|
<Footer className="flex-shrink-0" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<OffCanvas isOpen={isOffCanvasOpen} onClose={toggleOffCanvas} />
|
|
||||||
<Toast />
|
<Toast />
|
||||||
</Provider>
|
</Provider>
|
||||||
</body>
|
</body>
|
||||||
|
|||||||
@ -3,11 +3,6 @@
|
|||||||
import { ApiRoutes, GetApiRoute } from '@/ApiRoutes'
|
import { ApiRoutes, GetApiRoute } from '@/ApiRoutes'
|
||||||
import { httpService } from '@/services/httpService'
|
import { httpService } from '@/services/httpService'
|
||||||
import { FormEvent, useEffect, useRef, useState } from 'react'
|
import { FormEvent, useEffect, useRef, useState } from 'react'
|
||||||
import {
|
|
||||||
useValidation,
|
|
||||||
isValidEmail,
|
|
||||||
isValidHostname
|
|
||||||
} from '@/hooks/useValidation'
|
|
||||||
import {
|
import {
|
||||||
CustomButton,
|
CustomButton,
|
||||||
CustomCheckbox,
|
CustomCheckbox,
|
||||||
@ -16,37 +11,19 @@ import {
|
|||||||
CustomRadioGroup
|
CustomRadioGroup
|
||||||
} from '@/controls'
|
} from '@/controls'
|
||||||
import { GetAccountResponse } from '@/models/letsEncryptServer/account/responses/GetAccountResponse'
|
import { GetAccountResponse } from '@/models/letsEncryptServer/account/responses/GetAccountResponse'
|
||||||
import { deepCopy, enumToArray } from './functions'
|
import { deepCopy, enumToArray } from '../functions'
|
||||||
import { CacheAccount } from '@/entities/CacheAccount'
|
import { CacheAccount } from '@/entities/CacheAccount'
|
||||||
import { ChallengeTypes } from '@/entities/ChallengeTypes'
|
import { ChallengeTypes } from '@/entities/ChallengeTypes'
|
||||||
import { FaPlus, FaTrash } from 'react-icons/fa'
|
import { FaPlus, FaTrash } from 'react-icons/fa'
|
||||||
import { PageContainer } from '@/components/pageContainer'
|
import { PageContainer } from '@/components/pageContainer'
|
||||||
|
import { OffCanvas } from '@/components/offcanvas'
|
||||||
|
import { AccountEdit } from '@/partials/accoutEdit'
|
||||||
|
|
||||||
export default function Page() {
|
export default function Page() {
|
||||||
const [accounts, setAccounts] = useState<CacheAccount[]>([])
|
const [accounts, setAccounts] = useState<CacheAccount[]>([])
|
||||||
const [initialAccounts, setInitialAccounts] = useState<CacheAccount[]>([])
|
const [editingAccount, setEditingAccount] = useState<CacheAccount | null>(
|
||||||
|
null
|
||||||
const {
|
)
|
||||||
value: newContact,
|
|
||||||
error: contactError,
|
|
||||||
handleChange: handleContactChange,
|
|
||||||
reset: resetContact
|
|
||||||
} = useValidation<string>({
|
|
||||||
initialValue: '',
|
|
||||||
validateFn: isValidEmail,
|
|
||||||
errorMessage: 'Invalid email format.'
|
|
||||||
})
|
|
||||||
|
|
||||||
const {
|
|
||||||
value: newHostname,
|
|
||||||
error: hostnameError,
|
|
||||||
handleChange: handleHostnameChange,
|
|
||||||
reset: resetHostname
|
|
||||||
} = useValidation<string>({
|
|
||||||
initialValue: '',
|
|
||||||
validateFn: isValidHostname,
|
|
||||||
errorMessage: 'Invalid hostname format.'
|
|
||||||
})
|
|
||||||
|
|
||||||
const init = useRef(false)
|
const init = useRef(false)
|
||||||
|
|
||||||
@ -66,67 +43,36 @@ export default function Page() {
|
|||||||
accountId: account.accountId,
|
accountId: account.accountId,
|
||||||
isDisabled: account.isDisabled,
|
isDisabled: account.isDisabled,
|
||||||
description: account.description,
|
description: account.description,
|
||||||
contacts: account.contacts,
|
contacts: account.contacts.map((contact) => contact),
|
||||||
challengeType: account.challengeType,
|
challengeType: account.challengeType,
|
||||||
hostnames:
|
hostnames:
|
||||||
account.hostnames?.map((h) => ({
|
account.hostnames?.map((hostname) => ({
|
||||||
hostname: h.hostname,
|
hostname: hostname.hostname,
|
||||||
expires: new Date(h.expires),
|
expires: new Date(hostname.expires),
|
||||||
isUpcomingExpire: h.isUpcomingExpire,
|
isUpcomingExpire: hostname.isUpcomingExpire,
|
||||||
isDisabled: h.isDisabled
|
isDisabled: hostname.isDisabled
|
||||||
})) ?? [],
|
})) ?? [],
|
||||||
isEditMode: false,
|
isStaging: account.isStaging,
|
||||||
isStaging: account.isStaging
|
isEditMode: false
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
setAccounts(newAccounts)
|
setAccounts(newAccounts)
|
||||||
setInitialAccounts(deepCopy(newAccounts)) // Clone initial state
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fetchAccounts()
|
fetchAccounts()
|
||||||
init.current = true
|
init.current = true
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
const toggleEditMode = (accountId: string) => {
|
useEffect(() => {
|
||||||
|
console.log(editingAccount)
|
||||||
|
}, [editingAccount])
|
||||||
|
|
||||||
|
const handleAccountUpdate = (updatedAccount: CacheAccount) => {
|
||||||
setAccounts(
|
setAccounts(
|
||||||
accounts.map((account) =>
|
accounts.map((account) =>
|
||||||
account.accountId === accountId
|
account.accountId === updatedAccount.accountId
|
||||||
? { ...account, isEditMode: !account.isEditMode }
|
? updatedAccount
|
||||||
: account
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
const handleDescriptionChange = (accountId: string, value: string) => {
|
|
||||||
setAccounts(
|
|
||||||
accounts.map((account) =>
|
|
||||||
account.accountId === accountId
|
|
||||||
? { ...account, description: value }
|
|
||||||
: account
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
const validateDescription = (description: string) => {
|
|
||||||
return description.length > 0 ? '' : 'Description is required.'
|
|
||||||
}
|
|
||||||
|
|
||||||
const handleIsDisabledChange = (accountId: string, value: boolean) => {
|
|
||||||
setAccounts(
|
|
||||||
accounts.map((account) =>
|
|
||||||
account.accountId === accountId
|
|
||||||
? { ...account, isDisabled: value }
|
|
||||||
: account
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
const handleChallengeTypeChange = (accountId: string, option: any) => {
|
|
||||||
setAccounts(
|
|
||||||
accounts.map((account) =>
|
|
||||||
account.accountId === accountId
|
|
||||||
? { ...account, challengeType: option.value }
|
|
||||||
: account
|
: account
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
@ -139,164 +85,8 @@ export default function Page() {
|
|||||||
// TODO: Remove from cache
|
// TODO: Remove from cache
|
||||||
}
|
}
|
||||||
|
|
||||||
const deleteContact = (accountId: string, contact: string) => {
|
|
||||||
const account = accounts.find((account) => account.accountId === accountId)
|
|
||||||
if (account?.contacts.length ?? 0 < 1) return
|
|
||||||
|
|
||||||
// TODO: Remove from cache
|
|
||||||
// httpService.delete(
|
|
||||||
// GetApiRoute(ApiRoutes.ACCOUNT_CONTACT, accountId, contact)
|
|
||||||
// )
|
|
||||||
|
|
||||||
// setAccounts(
|
|
||||||
// accounts.map((account) =>
|
|
||||||
// account.accountId === accountId
|
|
||||||
// ? {
|
|
||||||
// ...account,
|
|
||||||
// contacts: account.contacts.filter((c) => c !== contact)
|
|
||||||
// }
|
|
||||||
// : account
|
|
||||||
// )
|
|
||||||
// )
|
|
||||||
}
|
|
||||||
|
|
||||||
const addContact = (accountId: string) => {
|
|
||||||
if (newContact === '' || contactError) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if (
|
|
||||||
accounts
|
|
||||||
.find((account) => account.accountId === accountId)
|
|
||||||
?.contacts.includes(newContact)
|
|
||||||
)
|
|
||||||
return
|
|
||||||
|
|
||||||
setAccounts(
|
|
||||||
accounts.map((account) =>
|
|
||||||
account.accountId === accountId
|
|
||||||
? { ...account, contacts: [...account.contacts, newContact] }
|
|
||||||
: account
|
|
||||||
)
|
|
||||||
)
|
|
||||||
resetContact()
|
|
||||||
}
|
|
||||||
|
|
||||||
const deleteHostname = (accountId: string, hostname: string) => {
|
|
||||||
const account = accounts.find((account) => account.accountId === accountId)
|
|
||||||
if (account?.hostnames.length ?? 0 < 1) return
|
|
||||||
|
|
||||||
// TODO: Revoke certificate
|
|
||||||
// TODO: Remove from cache
|
|
||||||
|
|
||||||
setAccounts(
|
|
||||||
accounts.map((account) =>
|
|
||||||
account.accountId === accountId
|
|
||||||
? {
|
|
||||||
...account,
|
|
||||||
hostnames: account.hostnames.filter(
|
|
||||||
(h) => h.hostname !== hostname
|
|
||||||
)
|
|
||||||
}
|
|
||||||
: account
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
const addHostname = (accountId: string) => {
|
|
||||||
if (newHostname === '' || hostnameError) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if (
|
|
||||||
accounts
|
|
||||||
.find((account) => account.accountId === accountId)
|
|
||||||
?.hostnames.some((h) => h.hostname === newHostname)
|
|
||||||
)
|
|
||||||
return
|
|
||||||
|
|
||||||
setAccounts(
|
|
||||||
accounts.map((account) =>
|
|
||||||
account.accountId === accountId
|
|
||||||
? {
|
|
||||||
...account,
|
|
||||||
hostnames: [
|
|
||||||
...account.hostnames,
|
|
||||||
{
|
|
||||||
hostname: newHostname,
|
|
||||||
expires: new Date(),
|
|
||||||
isUpcomingExpire: false,
|
|
||||||
isDisabled: false
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
: account
|
|
||||||
)
|
|
||||||
)
|
|
||||||
resetHostname()
|
|
||||||
}
|
|
||||||
|
|
||||||
const handleSubmit = async (
|
|
||||||
e: FormEvent<HTMLFormElement>,
|
|
||||||
accountId: string
|
|
||||||
) => {
|
|
||||||
e.preventDefault()
|
|
||||||
|
|
||||||
const account = accounts.find((acc) => acc.accountId === accountId)
|
|
||||||
const initialAccount = initialAccounts.find(
|
|
||||||
(acc) => acc.accountId === accountId
|
|
||||||
)
|
|
||||||
|
|
||||||
if (!account || !initialAccount) return
|
|
||||||
|
|
||||||
const contactChanges = {
|
|
||||||
added: account.contacts.filter(
|
|
||||||
(contact) => !initialAccount.contacts.includes(contact)
|
|
||||||
),
|
|
||||||
removed: initialAccount.contacts.filter(
|
|
||||||
(contact) => !account.contacts.includes(contact)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
const hostnameChanges = {
|
|
||||||
added: account.hostnames.filter(
|
|
||||||
(hostname) =>
|
|
||||||
!initialAccount.hostnames.some(
|
|
||||||
(h) => h.hostname === hostname.hostname
|
|
||||||
)
|
|
||||||
),
|
|
||||||
removed: initialAccount.hostnames.filter(
|
|
||||||
(hostname) =>
|
|
||||||
!account.hostnames.some((h) => h.hostname === hostname.hostname)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Handle contact changes
|
|
||||||
if (contactChanges.added.length > 0) {
|
|
||||||
// TODO: POST new contacts
|
|
||||||
console.log('Added contacts:', contactChanges.added)
|
|
||||||
}
|
|
||||||
if (contactChanges.removed.length > 0) {
|
|
||||||
// TODO: DELETE removed contacts
|
|
||||||
console.log('Removed contacts:', contactChanges.removed)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Handle hostname changes
|
|
||||||
if (hostnameChanges.added.length > 0) {
|
|
||||||
// TODO: POST new hostnames
|
|
||||||
console.log('Added hostnames:', hostnameChanges.added)
|
|
||||||
}
|
|
||||||
if (hostnameChanges.removed.length > 0) {
|
|
||||||
// TODO: DELETE removed hostnames
|
|
||||||
console.log('Removed hostnames:', hostnameChanges.removed)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Save current state as initial state
|
|
||||||
setInitialAccounts(deepCopy(accounts))
|
|
||||||
toggleEditMode(accountId)
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
<>
|
||||||
<PageContainer title="LetsEncrypt Auto Renew">
|
<PageContainer title="LetsEncrypt Auto Renew">
|
||||||
{accounts.map((account) => (
|
{accounts.map((account) => (
|
||||||
<div
|
<div
|
||||||
@ -308,153 +98,15 @@ export default function Page() {
|
|||||||
Account: {account.accountId}
|
Account: {account.accountId}
|
||||||
</h2>
|
</h2>
|
||||||
<CustomButton
|
<CustomButton
|
||||||
onClick={() => toggleEditMode(account.accountId)}
|
onClick={() => {
|
||||||
|
setEditingAccount(account)
|
||||||
|
}}
|
||||||
className="bg-blue-500 text-white px-3 py-1 rounded"
|
className="bg-blue-500 text-white px-3 py-1 rounded"
|
||||||
>
|
>
|
||||||
{account.isEditMode ? 'View Mode' : 'Edit Mode'}
|
Edit
|
||||||
</CustomButton>
|
</CustomButton>
|
||||||
</div>
|
</div>
|
||||||
{account.isEditMode ? (
|
|
||||||
<form onSubmit={(e) => handleSubmit(e, account.accountId)}>
|
|
||||||
<div className="mb-4">
|
|
||||||
<CustomInput
|
|
||||||
value={account.description ?? ''}
|
|
||||||
onChange={(value) =>
|
|
||||||
handleDescriptionChange(account.accountId, value)
|
|
||||||
}
|
|
||||||
placeholder="Add new description"
|
|
||||||
type="text"
|
|
||||||
error={validateDescription(account.description ?? '')}
|
|
||||||
title="Description"
|
|
||||||
inputClassName="border p-2 rounded w-full"
|
|
||||||
errorClassName="text-red-500 text-sm mt-1"
|
|
||||||
className="mr-2 flex-grow"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="mb-4">
|
|
||||||
<CustomCheckbox
|
|
||||||
checked={account.isDisabled}
|
|
||||||
label="Disabled"
|
|
||||||
onChange={(value) =>
|
|
||||||
handleIsDisabledChange(account.accountId, value)
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="mb-4">
|
|
||||||
<h3 className="text-xl font-medium mb-2">Contacts:</h3>
|
|
||||||
<ul className="list-disc list-inside pl-4 mb-2">
|
|
||||||
{account.contacts.map((contact) => (
|
|
||||||
<li
|
|
||||||
key={contact}
|
|
||||||
className="text-gray-700 flex justify-between items-center mb-2"
|
|
||||||
>
|
|
||||||
{contact}
|
|
||||||
<CustomButton
|
|
||||||
type="button"
|
|
||||||
onClick={() =>
|
|
||||||
deleteContact(account.accountId, contact)
|
|
||||||
}
|
|
||||||
className="bg-red-500 text-white p-2 rounded ml-2"
|
|
||||||
>
|
|
||||||
<FaTrash />
|
|
||||||
</CustomButton>
|
|
||||||
</li>
|
|
||||||
))}
|
|
||||||
</ul>
|
|
||||||
<div className="flex items-center mb-4">
|
|
||||||
<CustomInput
|
|
||||||
value={newContact}
|
|
||||||
onChange={handleContactChange}
|
|
||||||
placeholder="Add new contact"
|
|
||||||
type="email"
|
|
||||||
error={contactError}
|
|
||||||
title="New Contact"
|
|
||||||
inputClassName="border p-2 rounded w-full"
|
|
||||||
errorClassName="text-red-500 text-sm mt-1"
|
|
||||||
className="mr-2 flex-grow"
|
|
||||||
>
|
|
||||||
<CustomButton
|
|
||||||
type="button"
|
|
||||||
onClick={() => addContact(account.accountId)}
|
|
||||||
className="bg-green-500 text-white p-2 rounded ml-2"
|
|
||||||
>
|
|
||||||
<FaPlus />
|
|
||||||
</CustomButton>
|
|
||||||
</CustomInput>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<h3 className="text-xl font-medium mb-2">Hostnames:</h3>
|
|
||||||
<ul className="list-disc list-inside pl-4 mb-2">
|
|
||||||
{account.hostnames.map((hostname) => (
|
|
||||||
<li
|
|
||||||
key={hostname.hostname}
|
|
||||||
className="text-gray-700 flex justify-between items-center mb-2"
|
|
||||||
>
|
|
||||||
<div>
|
|
||||||
{hostname.hostname} - {hostname.expires.toDateString()}{' '}
|
|
||||||
-
|
|
||||||
<span
|
|
||||||
className={`ml-2 px-2 py-1 rounded ${hostname.isUpcomingExpire ? 'bg-yellow-200 text-yellow-800' : 'bg-green-200 text-green-800'}`}
|
|
||||||
>
|
|
||||||
{hostname.isUpcomingExpire
|
|
||||||
? 'Upcoming'
|
|
||||||
: 'Not Upcoming'}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<CustomButton
|
|
||||||
type="button"
|
|
||||||
onClick={() =>
|
|
||||||
deleteHostname(account.accountId, hostname.hostname)
|
|
||||||
}
|
|
||||||
className="bg-red-500 text-white p-2 rounded ml-2"
|
|
||||||
>
|
|
||||||
<FaTrash />
|
|
||||||
</CustomButton>
|
|
||||||
</li>
|
|
||||||
))}
|
|
||||||
</ul>
|
|
||||||
<div className="flex items-center">
|
|
||||||
<CustomInput
|
|
||||||
value={newHostname}
|
|
||||||
onChange={handleHostnameChange}
|
|
||||||
placeholder="Add new hostname"
|
|
||||||
type="text"
|
|
||||||
error={hostnameError}
|
|
||||||
title="New Hostname"
|
|
||||||
inputClassName="border p-2 rounded w-full"
|
|
||||||
errorClassName="text-red-500 text-sm mt-1"
|
|
||||||
className="mr-2 flex-grow"
|
|
||||||
>
|
|
||||||
<CustomButton
|
|
||||||
type="button"
|
|
||||||
onClick={() => addHostname(account.accountId)}
|
|
||||||
className="bg-green-500 text-white p-2 rounded ml-2"
|
|
||||||
>
|
|
||||||
<FaPlus />
|
|
||||||
</CustomButton>
|
|
||||||
</CustomInput>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="flex justify-between mt-4">
|
|
||||||
<CustomButton
|
|
||||||
onClick={() => deleteAccount(account.accountId)}
|
|
||||||
className="bg-red-500 text-white p-2 rounded ml-2"
|
|
||||||
>
|
|
||||||
<FaTrash />
|
|
||||||
</CustomButton>
|
|
||||||
<CustomButton
|
|
||||||
type="submit"
|
|
||||||
className="bg-green-500 text-white p-2 rounded ml-2"
|
|
||||||
>
|
|
||||||
Submit
|
|
||||||
</CustomButton>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
) : (
|
|
||||||
<>
|
|
||||||
<div className="mb-4">
|
<div className="mb-4">
|
||||||
<h3 className="text-xl font-medium mb-2">
|
<h3 className="text-xl font-medium mb-2">
|
||||||
Description: {account.description}
|
Description: {account.description}
|
||||||
@ -485,7 +137,8 @@ export default function Page() {
|
|||||||
enumType={ChallengeTypes}
|
enumType={ChallengeTypes}
|
||||||
selectedValue={account.challengeType}
|
selectedValue={account.challengeType}
|
||||||
onChange={(option) =>
|
onChange={(option) =>
|
||||||
handleChallengeTypeChange(account.accountId, option)
|
//handleChallengeTypeChange(account.accountId, option)
|
||||||
|
console.log('')
|
||||||
}
|
}
|
||||||
disabled={true}
|
disabled={true}
|
||||||
/>
|
/>
|
||||||
@ -493,8 +146,9 @@ export default function Page() {
|
|||||||
<div className="mb-4">
|
<div className="mb-4">
|
||||||
<h3 className="text-xl font-medium mb-2">Hostnames:</h3>
|
<h3 className="text-xl font-medium mb-2">Hostnames:</h3>
|
||||||
<ul className="list-disc list-inside pl-4 mb-2">
|
<ul className="list-disc list-inside pl-4 mb-2">
|
||||||
{account.hostnames.map((hostname) => (
|
{account.hostnames?.map((hostname) => (
|
||||||
<li key={hostname.hostname} className="text-gray-700 mb-2">
|
<li key={hostname.hostname} className="text-gray-700 mb-2">
|
||||||
|
<div className="inline-flex">
|
||||||
{hostname.hostname} - {hostname.expires.toDateString()} -
|
{hostname.hostname} - {hostname.expires.toDateString()} -
|
||||||
<span
|
<span
|
||||||
className={`ml-2 px-2 py-1 rounded ${
|
className={`ml-2 px-2 py-1 rounded ${
|
||||||
@ -512,6 +166,7 @@ export default function Page() {
|
|||||||
label="Disabled"
|
label="Disabled"
|
||||||
disabled={true}
|
disabled={true}
|
||||||
/>
|
/>
|
||||||
|
</div>
|
||||||
</li>
|
</li>
|
||||||
))}
|
))}
|
||||||
</ul>
|
</ul>
|
||||||
@ -531,10 +186,17 @@ export default function Page() {
|
|||||||
disabled={true}
|
disabled={true}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
</PageContainer>
|
</PageContainer>
|
||||||
|
|
||||||
|
<OffCanvas
|
||||||
|
title="Edit Account"
|
||||||
|
isOpen={editingAccount !== null}
|
||||||
|
onClose={() => setEditingAccount(null)}
|
||||||
|
>
|
||||||
|
{editingAccount && <AccountEdit account={editingAccount} />}
|
||||||
|
</OffCanvas>
|
||||||
|
</>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
'use client'
|
'use client'
|
||||||
|
|
||||||
import { FormEvent, useEffect, useRef, useState } from 'react'
|
import { FormEvent, useCallback, useState } from 'react'
|
||||||
import {
|
import {
|
||||||
useValidation,
|
useValidation,
|
||||||
isBypass,
|
isBypass,
|
||||||
@ -14,7 +14,7 @@ import {
|
|||||||
CustomRadioGroup
|
CustomRadioGroup
|
||||||
} from '@/controls'
|
} from '@/controls'
|
||||||
import { FaTrash, FaPlus } from 'react-icons/fa'
|
import { FaTrash, FaPlus } from 'react-icons/fa'
|
||||||
import { deepCopy } from '../functions'
|
import { deepCopy } from '../../functions'
|
||||||
import {
|
import {
|
||||||
PostAccountRequest,
|
PostAccountRequest,
|
||||||
validatePostAccountRequest
|
validatePostAccountRequest
|
||||||
@ -31,11 +31,14 @@ const RegisterPage = () => {
|
|||||||
const [account, setAccount] = useState<PostAccountRequest>({
|
const [account, setAccount] = useState<PostAccountRequest>({
|
||||||
description: '',
|
description: '',
|
||||||
contacts: [],
|
contacts: [],
|
||||||
challengeType: '',
|
challengeType: ChallengeTypes.http01,
|
||||||
hostnames: [],
|
hostnames: [],
|
||||||
isStaging: true
|
isStaging: true
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const [newHostname, setNewHostname] = useState('')
|
||||||
|
const [newContact, setNewContact] = useState('')
|
||||||
|
|
||||||
const dispatch = useAppDispatch()
|
const dispatch = useAppDispatch()
|
||||||
|
|
||||||
const {
|
const {
|
||||||
@ -44,7 +47,15 @@ const RegisterPage = () => {
|
|||||||
handleChange: handleDescriptionChange,
|
handleChange: handleDescriptionChange,
|
||||||
reset: resetDescription
|
reset: resetDescription
|
||||||
} = useValidation<string>({
|
} = useValidation<string>({
|
||||||
initialValue: '',
|
defaultValue: '',
|
||||||
|
externalValue: account.description,
|
||||||
|
setExternalValue: (newDescription) => {
|
||||||
|
setAccount((prev) => {
|
||||||
|
const newAccount = deepCopy(prev)
|
||||||
|
newAccount.description = newDescription
|
||||||
|
return newAccount
|
||||||
|
})
|
||||||
|
},
|
||||||
validateFn: isBypass,
|
validateFn: isBypass,
|
||||||
errorMessage: ''
|
errorMessage: ''
|
||||||
})
|
})
|
||||||
@ -55,62 +66,56 @@ const RegisterPage = () => {
|
|||||||
handleChange: handleContactChange,
|
handleChange: handleContactChange,
|
||||||
reset: resetContact
|
reset: resetContact
|
||||||
} = useValidation<string>({
|
} = useValidation<string>({
|
||||||
initialValue: '',
|
defaultValue: '',
|
||||||
|
externalValue: newContact,
|
||||||
|
setExternalValue: setNewContact,
|
||||||
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.'
|
||||||
})
|
})
|
||||||
|
|
||||||
const {
|
|
||||||
value: challengeType,
|
|
||||||
error: challengeTypeError,
|
|
||||||
handleChange: handleChallengeTypeChange,
|
|
||||||
reset: resetChallengeType
|
|
||||||
} = useValidation<string>({
|
|
||||||
initialValue: ChallengeTypes.http01,
|
|
||||||
validateFn: isBypass,
|
|
||||||
errorMessage: ''
|
|
||||||
})
|
|
||||||
|
|
||||||
const {
|
const {
|
||||||
value: hostname,
|
value: hostname,
|
||||||
error: hostnameError,
|
error: hostnameError,
|
||||||
handleChange: handleHostnameChange,
|
handleChange: handleHostnameChange,
|
||||||
reset: resetHostname
|
reset: resetHostname
|
||||||
} = useValidation<string>({
|
} = useValidation<string>({
|
||||||
initialValue: '',
|
defaultValue: '',
|
||||||
|
externalValue: newHostname,
|
||||||
|
setExternalValue: setNewHostname,
|
||||||
validateFn: isValidHostname,
|
validateFn: isValidHostname,
|
||||||
errorMessage: 'Invalid hostname format.'
|
errorMessage: 'Invalid hostname format.'
|
||||||
})
|
})
|
||||||
|
|
||||||
const init = useRef(false)
|
const {
|
||||||
useEffect(() => {
|
value: challengeType,
|
||||||
if (init.current) return
|
error: challengeTypeError,
|
||||||
|
handleChange: handleChallengeTypeChange,
|
||||||
init.current = true
|
reset: resetChallengeType
|
||||||
}, [])
|
} = useValidation<string>({
|
||||||
|
defaultValue: ChallengeTypes.http01,
|
||||||
useEffect(() => {
|
externalValue: account.challengeType,
|
||||||
|
setExternalValue: (newChallengeType) => {
|
||||||
setAccount((prev) => {
|
setAccount((prev) => {
|
||||||
const newAccount = deepCopy(prev)
|
const newAccount = deepCopy(prev)
|
||||||
newAccount.description = description
|
newAccount.challengeType = newChallengeType
|
||||||
newAccount.challengeType = challengeType
|
|
||||||
return newAccount
|
return newAccount
|
||||||
})
|
})
|
||||||
}, [description, challengeType])
|
},
|
||||||
|
validateFn: isBypass,
|
||||||
|
errorMessage: ''
|
||||||
|
})
|
||||||
|
|
||||||
const handleAddContact = () => {
|
const handleAddContact = () => {
|
||||||
if (
|
if (contactError !== '') {
|
||||||
contact === '' ||
|
|
||||||
account?.contacts.includes(contact) ||
|
|
||||||
contactError !== ''
|
|
||||||
) {
|
|
||||||
resetContact()
|
resetContact()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
setAccount((prev) => {
|
setAccount((prev) => {
|
||||||
const newAccount = deepCopy(prev)
|
const newAccount = deepCopy(prev)
|
||||||
|
if (!newAccount.contacts.includes(contact)) {
|
||||||
newAccount.contacts.push(contact)
|
newAccount.contacts.push(contact)
|
||||||
|
}
|
||||||
return newAccount
|
return newAccount
|
||||||
})
|
})
|
||||||
|
|
||||||
@ -126,18 +131,16 @@ const RegisterPage = () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const handleAddHostname = () => {
|
const handleAddHostname = () => {
|
||||||
if (
|
if (hostnameError !== '') {
|
||||||
hostname === '' ||
|
|
||||||
account?.hostnames.includes(hostname) ||
|
|
||||||
hostnameError !== ''
|
|
||||||
) {
|
|
||||||
resetHostname()
|
resetHostname()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
setAccount((prev) => {
|
setAccount((prev) => {
|
||||||
const newAccount = deepCopy(prev)
|
const newAccount = deepCopy(prev)
|
||||||
|
if (!newAccount.hostnames.includes(hostname)) {
|
||||||
newAccount.hostnames.push(hostname)
|
newAccount.hostnames.push(hostname)
|
||||||
|
}
|
||||||
return newAccount
|
return newAccount
|
||||||
})
|
})
|
||||||
|
|
||||||
@ -183,7 +186,7 @@ const RegisterPage = () => {
|
|||||||
<form onSubmit={handleSubmit}>
|
<form onSubmit={handleSubmit}>
|
||||||
<div className="mb-4">
|
<div className="mb-4">
|
||||||
<CustomInput
|
<CustomInput
|
||||||
value={account.description}
|
value={description}
|
||||||
onChange={handleDescriptionChange}
|
onChange={handleDescriptionChange}
|
||||||
placeholder="Account Description"
|
placeholder="Account Description"
|
||||||
type="text"
|
type="text"
|
||||||
@ -224,7 +227,7 @@ 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"
|
||||||
>
|
/>
|
||||||
<CustomButton
|
<CustomButton
|
||||||
type="button"
|
type="button"
|
||||||
onClick={handleAddContact}
|
onClick={handleAddContact}
|
||||||
@ -232,7 +235,6 @@ const RegisterPage = () => {
|
|||||||
>
|
>
|
||||||
<FaPlus />
|
<FaPlus />
|
||||||
</CustomButton>
|
</CustomButton>
|
||||||
</CustomInput>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="mb-4">
|
<div className="mb-4">
|
||||||
@ -240,7 +242,7 @@ const RegisterPage = () => {
|
|||||||
error={challengeTypeError}
|
error={challengeTypeError}
|
||||||
title="Challenge Type"
|
title="Challenge Type"
|
||||||
enumType={ChallengeTypes}
|
enumType={ChallengeTypes}
|
||||||
selectedValue={account.challengeType}
|
selectedValue={challengeType}
|
||||||
onChange={handleChallengeTypeChange}
|
onChange={handleChallengeTypeChange}
|
||||||
selectBoxClassName="border p-2 rounded w-full"
|
selectBoxClassName="border p-2 rounded w-full"
|
||||||
errorClassName="text-red-500 text-sm mt-1"
|
errorClassName="text-red-500 text-sm mt-1"
|
||||||
@ -278,7 +280,7 @@ 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"
|
||||||
>
|
/>
|
||||||
<CustomButton
|
<CustomButton
|
||||||
type="button"
|
type="button"
|
||||||
onClick={handleAddHostname}
|
onClick={handleAddHostname}
|
||||||
@ -286,7 +288,6 @@ const RegisterPage = () => {
|
|||||||
>
|
>
|
||||||
<FaPlus />
|
<FaPlus />
|
||||||
</CustomButton>
|
</CustomButton>
|
||||||
</CustomInput>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@ -16,7 +16,7 @@ const Loader: React.FC = () => {
|
|||||||
if (activeRequests > 0) {
|
if (activeRequests > 0) {
|
||||||
timeout = setTimeout(() => {
|
timeout = setTimeout(() => {
|
||||||
dispatch(reset())
|
dispatch(reset())
|
||||||
}, 10000) // Adjust the timeout as necessary
|
}, 120000) // Adjust the timeout as necessary
|
||||||
}
|
}
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
|
|||||||
@ -1,29 +1,37 @@
|
|||||||
import React, { FC } from 'react'
|
import React, { FC } from 'react'
|
||||||
|
|
||||||
interface OffCanvasProps {
|
interface OffCanvasProps {
|
||||||
|
title?: string
|
||||||
|
children: React.ReactNode
|
||||||
isOpen: boolean
|
isOpen: boolean
|
||||||
onClose: () => void
|
onClose?: () => void
|
||||||
}
|
}
|
||||||
|
|
||||||
const OffCanvas: FC<OffCanvasProps> = ({ isOpen, onClose }) => {
|
const OffCanvas: FC<OffCanvasProps> = (props) => {
|
||||||
|
const { title, children, isOpen, onClose } = props
|
||||||
|
|
||||||
|
const handleOnClose = () => {
|
||||||
|
onClose?.()
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={`fixed inset-0 bg-gray-800 bg-opacity-50 z-50 transform transition-transform duration-300 ${
|
className={`fixed inset-0 bg-gray-800 bg-opacity-50 z-50 transform transition-transform duration-300 ${
|
||||||
isOpen ? 'translate-x-0' : 'translate-x-full'
|
isOpen ? 'translate-x-0' : 'translate-x-full'
|
||||||
}`}
|
}`}
|
||||||
onClick={onClose}
|
onClick={handleOnClose}
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
className="absolute top-0 right-0 bg-white w-64 h-full shadow-lg"
|
className="absolute top-0 right-0 bg-white max-w-full md:max-w-md lg:max-w-lg xl:max-w-xl h-full shadow-lg overflow-y-auto"
|
||||||
onClick={(e) => e.stopPropagation()}
|
onClick={(e) => e.stopPropagation()}
|
||||||
>
|
>
|
||||||
<div className="p-4">
|
<div className="p-4">
|
||||||
<h2 className="text-xl font-bold">Settings</h2>
|
{title && <h2 className="text-xl font-bold">{title}</h2>}
|
||||||
<button onClick={onClose} className="mt-4 text-red-500">
|
<button onClick={handleOnClose} className="mt-4 text-red-500">
|
||||||
Close
|
Close
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
{/* Your off-canvas content goes here */}
|
<div className="p-4">{children}</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
|
|||||||
@ -4,11 +4,9 @@ import React, { FC, useState } from 'react'
|
|||||||
import { FaCog, FaBars } from 'react-icons/fa'
|
import { FaCog, FaBars } from 'react-icons/fa'
|
||||||
import Link from 'next/link'
|
import Link from 'next/link'
|
||||||
|
|
||||||
interface TopMenuProps {
|
interface TopMenuProps {}
|
||||||
onToggleOffCanvas: () => void
|
|
||||||
}
|
|
||||||
|
|
||||||
const TopMenu: FC<TopMenuProps> = ({ onToggleOffCanvas }) => {
|
const TopMenu: FC<TopMenuProps> = () => {
|
||||||
const [isMenuOpen, setIsMenuOpen] = useState(false)
|
const [isMenuOpen, setIsMenuOpen] = useState(false)
|
||||||
|
|
||||||
const toggleMenu = () => {
|
const toggleMenu = () => {
|
||||||
@ -44,9 +42,6 @@ const TopMenu: FC<TopMenuProps> = ({ onToggleOffCanvas }) => {
|
|||||||
</ul>
|
</ul>
|
||||||
)}
|
)}
|
||||||
</nav>
|
</nav>
|
||||||
<button onClick={onToggleOffCanvas} className="ml-4">
|
|
||||||
<FaCog />
|
|
||||||
</button>
|
|
||||||
<button onClick={toggleMenu} className="md:hidden">
|
<button onClick={toggleMenu} className="md:hidden">
|
||||||
<FaBars />
|
<FaBars />
|
||||||
</button>
|
</button>
|
||||||
|
|||||||
@ -4,7 +4,7 @@ import {
|
|||||||
CustomSelectOption,
|
CustomSelectOption,
|
||||||
CustomSelectPropsBase
|
CustomSelectPropsBase
|
||||||
} from './customSelect'
|
} from './customSelect'
|
||||||
import { enumToArray } from '@/app/functions'
|
import { enumToArray } from '@/functions'
|
||||||
|
|
||||||
interface CustomEnumSelectProps extends CustomSelectPropsBase {
|
interface CustomEnumSelectProps extends CustomSelectPropsBase {
|
||||||
enumType: any
|
enumType: any
|
||||||
|
|||||||
@ -3,10 +3,10 @@ import { CacheAccountHostname } from './CacheAccountHostname'
|
|||||||
export interface CacheAccount {
|
export interface CacheAccount {
|
||||||
accountId: string
|
accountId: string
|
||||||
isDisabled: boolean
|
isDisabled: boolean
|
||||||
description?: string
|
description: string
|
||||||
contacts: string[]
|
contacts: string[]
|
||||||
challengeType?: string
|
challengeType?: string
|
||||||
hostnames: CacheAccountHostname[]
|
hostnames?: CacheAccountHostname[]
|
||||||
isEditMode: boolean
|
isEditMode: boolean
|
||||||
isStaging: boolean
|
isStaging: boolean
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,63 +1,47 @@
|
|||||||
import { useState, useEffect, useCallback } from 'react'
|
import { useState, useEffect, useCallback } from 'react'
|
||||||
|
|
||||||
// Helper functions for validation
|
// Helper functions for validation
|
||||||
const isBypass = (value: any) => {
|
const isBypass = (value: any) => true
|
||||||
return true
|
const isValidEmail = (email: string) => /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email)
|
||||||
}
|
const isValidPhoneNumber = (phone: string) => /^\+?[1-9]\d{1,14}$/.test(phone)
|
||||||
|
const isValidContact = (contact: string) =>
|
||||||
const isValidEmail = (email: string) => {
|
isValidEmail(contact) || isValidPhoneNumber(contact)
|
||||||
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/
|
const isValidHostname = (hostname: string) =>
|
||||||
return emailRegex.test(email)
|
/^(?!:\/\/)([a-zA-Z0-9-_]{1,63}\.?)+[a-zA-Z]{2,6}$/.test(hostname)
|
||||||
}
|
|
||||||
|
|
||||||
const isValidPhoneNumber = (phone: string) => {
|
|
||||||
const phoneRegex = /^\+?[1-9]\d{1,14}$/
|
|
||||||
return phoneRegex.test(phone)
|
|
||||||
}
|
|
||||||
|
|
||||||
const isValidContact = (contact: string) => {
|
|
||||||
return isValidEmail(contact) || isValidPhoneNumber(contact)
|
|
||||||
}
|
|
||||||
|
|
||||||
const isValidHostname = (hostname: string) => {
|
|
||||||
const hostnameRegex = /^(?!:\/\/)([a-zA-Z0-9-_]{1,63}\.?)+[a-zA-Z]{2,6}$/
|
|
||||||
return hostnameRegex.test(hostname)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Props interface for useValidation hook
|
// Props interface for useValidation hook
|
||||||
interface UseValidationProps<T> {
|
interface UseValidationProps<T> {
|
||||||
initialValue: T
|
externalValue: T
|
||||||
|
setExternalValue: (value: T) => void
|
||||||
validateFn: (value: T) => boolean
|
validateFn: (value: T) => boolean
|
||||||
errorMessage: string
|
errorMessage: string
|
||||||
emptyFieldMessage?: string // Optional custom message for empty fields
|
emptyFieldMessage?: string
|
||||||
defaultResetValue?: T // Optional default reset value
|
defaultValue: T
|
||||||
}
|
}
|
||||||
|
|
||||||
// Custom hook for input validation
|
// Custom hook for input validation
|
||||||
const useValidation = <T extends string | number | Date>(
|
const useValidation = <T extends string | number | Date | boolean>(
|
||||||
props: UseValidationProps<T>
|
props: UseValidationProps<T>
|
||||||
) => {
|
) => {
|
||||||
const {
|
const {
|
||||||
initialValue,
|
externalValue,
|
||||||
|
setExternalValue,
|
||||||
validateFn,
|
validateFn,
|
||||||
errorMessage,
|
errorMessage,
|
||||||
emptyFieldMessage = 'This field cannot be empty.', // Default message
|
emptyFieldMessage = 'This field cannot be empty.',
|
||||||
defaultResetValue
|
defaultValue
|
||||||
} = props
|
} = props
|
||||||
|
|
||||||
const [value, setValue] = useState<T>(initialValue)
|
const [internalValue, setInternalValue] = useState(externalValue)
|
||||||
const [error, setError] = useState('')
|
const [error, setError] = useState('')
|
||||||
|
|
||||||
const handleChange = useCallback(
|
const validate = useCallback(
|
||||||
(newValue: T) => {
|
(value: T) => {
|
||||||
setValue(newValue)
|
|
||||||
const stringValue =
|
const stringValue =
|
||||||
newValue instanceof Date
|
value instanceof Date ? value.toISOString() : value.toString().trim()
|
||||||
? newValue.toISOString()
|
|
||||||
: newValue.toString().trim()
|
|
||||||
if (stringValue === '') {
|
if (stringValue === '') {
|
||||||
setError(emptyFieldMessage)
|
setError(emptyFieldMessage)
|
||||||
} else if (!validateFn(newValue)) {
|
} else if (!validateFn(value)) {
|
||||||
setError(errorMessage)
|
setError(errorMessage)
|
||||||
} else {
|
} else {
|
||||||
setError('')
|
setError('')
|
||||||
@ -66,27 +50,26 @@ const useValidation = <T extends string | number | Date>(
|
|||||||
[emptyFieldMessage, errorMessage, validateFn]
|
[emptyFieldMessage, errorMessage, validateFn]
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const handleChange = useCallback(
|
||||||
|
(newValue: T) => {
|
||||||
|
setInternalValue(newValue)
|
||||||
|
setExternalValue(newValue)
|
||||||
|
validate(newValue)
|
||||||
|
},
|
||||||
|
[setExternalValue, validate]
|
||||||
|
)
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
handleChange(initialValue)
|
setInternalValue(externalValue)
|
||||||
}, [initialValue, handleChange])
|
validate(externalValue)
|
||||||
|
}, [externalValue, validate])
|
||||||
|
|
||||||
const reset = useCallback(() => {
|
const reset = useCallback(() => {
|
||||||
const resetValue =
|
setInternalValue(defaultValue)
|
||||||
defaultResetValue !== undefined ? defaultResetValue : initialValue
|
|
||||||
|
|
||||||
setValue(resetValue)
|
|
||||||
const stringValue =
|
|
||||||
resetValue instanceof Date
|
|
||||||
? resetValue.toISOString()
|
|
||||||
: resetValue.toString().trim()
|
|
||||||
if (stringValue === '') {
|
|
||||||
setError(emptyFieldMessage)
|
|
||||||
} else {
|
|
||||||
setError('')
|
setError('')
|
||||||
}
|
}, [defaultValue])
|
||||||
}, [defaultResetValue, initialValue, emptyFieldMessage])
|
|
||||||
|
|
||||||
return { value, error, handleChange, reset }
|
return { value: internalValue, error, handleChange, reset }
|
||||||
}
|
}
|
||||||
|
|
||||||
export {
|
export {
|
||||||
|
|||||||
370
src/ClientApp/partials/accoutEdit.tsx
Normal file
370
src/ClientApp/partials/accoutEdit.tsx
Normal file
@ -0,0 +1,370 @@
|
|||||||
|
'use client'
|
||||||
|
|
||||||
|
import { FormEvent, useCallback, useState } from 'react'
|
||||||
|
import {
|
||||||
|
useValidation,
|
||||||
|
isValidEmail,
|
||||||
|
isValidHostname,
|
||||||
|
isBypass
|
||||||
|
} from '@/hooks/useValidation'
|
||||||
|
import {
|
||||||
|
CustomButton,
|
||||||
|
CustomCheckbox,
|
||||||
|
CustomEnumSelect,
|
||||||
|
CustomInput,
|
||||||
|
CustomRadioGroup
|
||||||
|
} from '@/controls'
|
||||||
|
import { CacheAccount } from '@/entities/CacheAccount'
|
||||||
|
import { FaPlus, FaTrash } from 'react-icons/fa'
|
||||||
|
import { ChallengeTypes } from '@/entities/ChallengeTypes'
|
||||||
|
|
||||||
|
interface AccountEditProps {
|
||||||
|
account: CacheAccount
|
||||||
|
onCancel?: () => void
|
||||||
|
onSave?: (account: CacheAccount) => void
|
||||||
|
onDelete?: (accountId: string) => void
|
||||||
|
}
|
||||||
|
|
||||||
|
const AccountEdit: React.FC<AccountEditProps> = (props) => {
|
||||||
|
const { account, onCancel, onSave, onDelete } = props
|
||||||
|
|
||||||
|
const [editingAccount, setEditingAccount] = useState<CacheAccount>(account)
|
||||||
|
const [newContact, setNewContact] = useState('')
|
||||||
|
const [newHostname, setNewHostname] = useState('')
|
||||||
|
|
||||||
|
const setDescription = useCallback(
|
||||||
|
(newDescription: string) => {
|
||||||
|
if (editingAccount) {
|
||||||
|
setEditingAccount({ ...editingAccount, description: newDescription })
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[editingAccount]
|
||||||
|
)
|
||||||
|
|
||||||
|
const {
|
||||||
|
value: description,
|
||||||
|
error: descriptionError,
|
||||||
|
handleChange: handleDescriptionChange,
|
||||||
|
reset: resetDescription
|
||||||
|
} = useValidation<string>({
|
||||||
|
defaultValue: '',
|
||||||
|
externalValue: account.description,
|
||||||
|
setExternalValue: setDescription,
|
||||||
|
validateFn: isBypass,
|
||||||
|
errorMessage: ''
|
||||||
|
})
|
||||||
|
|
||||||
|
const {
|
||||||
|
value: contact,
|
||||||
|
error: contactError,
|
||||||
|
handleChange: handleContactChange,
|
||||||
|
reset: resetContact
|
||||||
|
} = useValidation<string>({
|
||||||
|
defaultValue: '',
|
||||||
|
externalValue: newContact,
|
||||||
|
setExternalValue: setNewContact,
|
||||||
|
validateFn: isValidEmail,
|
||||||
|
errorMessage: 'Invalid email format.'
|
||||||
|
})
|
||||||
|
|
||||||
|
const {
|
||||||
|
value: hostname,
|
||||||
|
error: hostnameError,
|
||||||
|
handleChange: handleHostnameChange,
|
||||||
|
reset: resetHostname
|
||||||
|
} = useValidation<string>({
|
||||||
|
defaultValue: '',
|
||||||
|
externalValue: newHostname,
|
||||||
|
setExternalValue: setNewHostname,
|
||||||
|
validateFn: isValidHostname,
|
||||||
|
errorMessage: 'Invalid hostname format.'
|
||||||
|
})
|
||||||
|
|
||||||
|
const handleIsDisabledChange = (value: boolean) => {
|
||||||
|
// setAccount({ ...account, isDisabled: value })
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleChallengeTypeChange = (option: any) => {
|
||||||
|
//setAccount({ ...account, challengeType: option.value })
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleHostnameDisabledChange = (hostname: string, value: boolean) => {
|
||||||
|
// setAccount({
|
||||||
|
// ...account,
|
||||||
|
// hostnames: account.hostnames.map((h) =>
|
||||||
|
// h.hostname === hostname ? { ...h, isDisabled: value } : h
|
||||||
|
// )
|
||||||
|
// })
|
||||||
|
// }
|
||||||
|
// const handleStagingChange = (value: string) => {
|
||||||
|
// setAccount({ ...account, isStaging: value === 'staging' })
|
||||||
|
}
|
||||||
|
|
||||||
|
const deleteContact = (contact: string) => {
|
||||||
|
if (account?.contacts.length ?? 0 < 1) return
|
||||||
|
|
||||||
|
// setAccount({
|
||||||
|
// ...account,
|
||||||
|
// contacts: account.contacts.filter((c) => c !== contact)
|
||||||
|
// })
|
||||||
|
// }
|
||||||
|
|
||||||
|
// const addContact = () => {
|
||||||
|
// if (newContact === '' || contactError) {
|
||||||
|
// return
|
||||||
|
// }
|
||||||
|
|
||||||
|
// if (account.contacts.includes(newContact)) return
|
||||||
|
|
||||||
|
// setAccount({ ...account, contacts: [...account.contacts, newContact] })
|
||||||
|
// resetContact()
|
||||||
|
}
|
||||||
|
|
||||||
|
const deleteHostname = (hostname: string) => {
|
||||||
|
//if (account?.hostnames.length ?? 0 < 1) return
|
||||||
|
|
||||||
|
// setAccount({
|
||||||
|
// ...account,
|
||||||
|
// hostnames: account.hostnames.filter((h) => h.hostname !== hostname)
|
||||||
|
// })
|
||||||
|
// }
|
||||||
|
|
||||||
|
// const addHostname = () => {
|
||||||
|
// if (newHostname === '' || hostnameError) {
|
||||||
|
// return
|
||||||
|
// }
|
||||||
|
|
||||||
|
// if (account.hostnames.some((h) => h.hostname === newHostname)) return
|
||||||
|
|
||||||
|
// setAccount({
|
||||||
|
// ...account,
|
||||||
|
// hostnames: [
|
||||||
|
// ...account.hostnames,
|
||||||
|
// {
|
||||||
|
// hostname: newHostname,
|
||||||
|
// expires: new Date(),
|
||||||
|
// isUpcomingExpire: false,
|
||||||
|
// isDisabled: false
|
||||||
|
// }
|
||||||
|
// ]
|
||||||
|
// })
|
||||||
|
resetHostname()
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleSubmit = async (e: FormEvent<HTMLFormElement>) => {
|
||||||
|
e.preventDefault()
|
||||||
|
|
||||||
|
// const contactChanges = {
|
||||||
|
// added: account.contacts.filter(
|
||||||
|
// (contact) => !initialAccountState.contacts.includes(contact)
|
||||||
|
// ),
|
||||||
|
// removed: initialAccountState.contacts.filter(
|
||||||
|
// (contact) => !account.contacts.includes(contact)
|
||||||
|
// )
|
||||||
|
// }
|
||||||
|
|
||||||
|
// const hostnameChanges = {
|
||||||
|
// added: account.hostnames.filter(
|
||||||
|
// (hostname) =>
|
||||||
|
// !initialAccountState.hostnames.some(
|
||||||
|
// (h) => h.hostname === hostname.hostname
|
||||||
|
// )
|
||||||
|
// ),
|
||||||
|
// removed: initialAccountState.hostnames.filter(
|
||||||
|
// (hostname) =>
|
||||||
|
// !account.hostnames.some((h) => h.hostname === hostname.hostname)
|
||||||
|
// )
|
||||||
|
// }
|
||||||
|
|
||||||
|
// // Handle contact changes
|
||||||
|
// if (contactChanges.added.length > 0) {
|
||||||
|
// // TODO: POST new contacts
|
||||||
|
// console.log('Added contacts:', contactChanges.added)
|
||||||
|
// }
|
||||||
|
// if (contactChanges.removed.length > 0) {
|
||||||
|
// // TODO: DELETE removed contacts
|
||||||
|
// console.log('Removed contacts:', contactChanges.removed)
|
||||||
|
// }
|
||||||
|
|
||||||
|
// // Handle hostname changes
|
||||||
|
// if (hostnameChanges.added.length > 0) {
|
||||||
|
// // TODO: POST new hostnames
|
||||||
|
// console.log('Added hostnames:', hostnameChanges.added)
|
||||||
|
// }
|
||||||
|
// if (hostnameChanges.removed.length > 0) {
|
||||||
|
// // TODO: DELETE removed hostnames
|
||||||
|
// console.log('Removed hostnames:', hostnameChanges.removed)
|
||||||
|
// }
|
||||||
|
|
||||||
|
// onSave(account)
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<form onSubmit={handleSubmit}>
|
||||||
|
<div className="mb-4">
|
||||||
|
<CustomInput
|
||||||
|
value={description}
|
||||||
|
onChange={handleDescriptionChange}
|
||||||
|
placeholder="Add new description"
|
||||||
|
type="text"
|
||||||
|
error={descriptionError}
|
||||||
|
title="Description"
|
||||||
|
inputClassName="border p-2 rounded w-full"
|
||||||
|
errorClassName="text-red-500 text-sm mt-1"
|
||||||
|
className="mr-2 flex-grow"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="mb-4">
|
||||||
|
<CustomCheckbox
|
||||||
|
checked={account.isDisabled}
|
||||||
|
label="Disabled"
|
||||||
|
onChange={(value) => handleIsDisabledChange(value)}
|
||||||
|
className="mr-2 flex-grow"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="mb-4">
|
||||||
|
<CustomEnumSelect
|
||||||
|
title="Challenge Type"
|
||||||
|
enumType={ChallengeTypes}
|
||||||
|
selectedValue={account.challengeType}
|
||||||
|
onChange={(option) => handleChallengeTypeChange(option)}
|
||||||
|
className="mr-2 flex-grow"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="mb-4">
|
||||||
|
<h3 className="text-xl font-medium mb-2">Contacts:</h3>
|
||||||
|
<ul className="list-disc list-inside pl-4 mb-2">
|
||||||
|
{account.contacts.map((contact) => (
|
||||||
|
<li key={contact} className="text-gray-700 mb-2 inline-flex">
|
||||||
|
{contact}
|
||||||
|
<CustomButton
|
||||||
|
type="button"
|
||||||
|
onClick={() => deleteContact(contact)}
|
||||||
|
className="bg-red-500 text-white p-2 rounded ml-2"
|
||||||
|
>
|
||||||
|
<FaTrash />
|
||||||
|
</CustomButton>
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
<div className="flex items-center mb-4">
|
||||||
|
<CustomInput
|
||||||
|
value={newContact}
|
||||||
|
onChange={handleContactChange}
|
||||||
|
placeholder="Add new contact"
|
||||||
|
type="email"
|
||||||
|
error={contactError}
|
||||||
|
title="New Contact"
|
||||||
|
inputClassName="border p-2 rounded w-full"
|
||||||
|
errorClassName="text-red-500 text-sm mt-1"
|
||||||
|
className="mr-2 flex-grow"
|
||||||
|
/>
|
||||||
|
<CustomButton
|
||||||
|
type="button"
|
||||||
|
//onClick={addContact}
|
||||||
|
className="bg-green-500 text-white p-2 rounded ml-2"
|
||||||
|
>
|
||||||
|
<FaPlus />
|
||||||
|
</CustomButton>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<h3 className="text-xl font-medium mb-2">Hostnames:</h3>
|
||||||
|
<ul className="list-disc list-inside pl-4 mb-2">
|
||||||
|
{account.hostnames?.map((hostname) => (
|
||||||
|
<li key={hostname.hostname} className="text-gray-700 mb-2">
|
||||||
|
<div className="inline-flex">
|
||||||
|
{hostname.hostname} - {hostname.expires.toDateString()} -{' '}
|
||||||
|
<span
|
||||||
|
className={`ml-2 px-2 py-1 rounded ${
|
||||||
|
hostname.isUpcomingExpire
|
||||||
|
? 'bg-yellow-200 text-yellow-800'
|
||||||
|
: 'bg-green-200 text-green-800'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
{hostname.isUpcomingExpire ? 'Upcoming' : 'Not Upcoming'}
|
||||||
|
</span>{' '}
|
||||||
|
-{' '}
|
||||||
|
<CustomCheckbox
|
||||||
|
className="ml-2"
|
||||||
|
checked={hostname.isDisabled}
|
||||||
|
label="Disabled"
|
||||||
|
onChange={(value) =>
|
||||||
|
handleHostnameDisabledChange(hostname.hostname, value)
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<CustomButton
|
||||||
|
type="button"
|
||||||
|
onClick={() => deleteHostname(hostname.hostname)}
|
||||||
|
className="bg-red-500 text-white p-2 rounded ml-2"
|
||||||
|
>
|
||||||
|
<FaTrash />
|
||||||
|
</CustomButton>
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
<div className="flex items-center">
|
||||||
|
<CustomInput
|
||||||
|
value={newHostname}
|
||||||
|
onChange={handleHostnameChange}
|
||||||
|
placeholder="Add new hostname"
|
||||||
|
type="text"
|
||||||
|
error={hostnameError}
|
||||||
|
title="New Hostname"
|
||||||
|
inputClassName="border p-2 rounded w-full"
|
||||||
|
errorClassName="text-red-500 text-sm mt-1"
|
||||||
|
className="mr-2 flex-grow"
|
||||||
|
/>
|
||||||
|
<CustomButton
|
||||||
|
type="button"
|
||||||
|
//onClick={addHostname}
|
||||||
|
className="bg-green-500 text-white p-2 rounded ml-2"
|
||||||
|
>
|
||||||
|
<FaPlus />
|
||||||
|
</CustomButton>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="mb-4">
|
||||||
|
<CustomRadioGroup
|
||||||
|
options={[
|
||||||
|
{ value: 'staging', label: 'Staging' },
|
||||||
|
{ value: 'production', label: 'Production' }
|
||||||
|
]}
|
||||||
|
initialValue={account.isStaging ? 'staging' : 'production'}
|
||||||
|
title="LetsEncrypt Environment"
|
||||||
|
className="mr-2 flex-grow"
|
||||||
|
radioClassName=""
|
||||||
|
errorClassName="text-red-500 text-sm mt-1"
|
||||||
|
disabled={true}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="flex justify-between mt-4">
|
||||||
|
<CustomButton
|
||||||
|
//onClick={() => onDelete(account.accountId)}
|
||||||
|
className="bg-red-500 text-white p-2 rounded ml-2"
|
||||||
|
>
|
||||||
|
<FaTrash />
|
||||||
|
</CustomButton>
|
||||||
|
<CustomButton
|
||||||
|
onClick={onCancel}
|
||||||
|
className="bg-yellow-500 text-white p-2 rounded ml-2"
|
||||||
|
>
|
||||||
|
Cancel
|
||||||
|
</CustomButton>
|
||||||
|
<CustomButton
|
||||||
|
type="submit"
|
||||||
|
className="bg-green-500 text-white p-2 rounded ml-2"
|
||||||
|
>
|
||||||
|
Save
|
||||||
|
</CustomButton>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export { AccountEdit }
|
||||||
@ -72,6 +72,9 @@ public class AccountService : IAccountService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public async Task<(GetAccountResponse?, IDomainResult)> PostAccountAsync(PostAccountRequest requestData) {
|
public async Task<(GetAccountResponse?, IDomainResult)> PostAccountAsync(PostAccountRequest requestData) {
|
||||||
|
|
||||||
|
// TODO: check for overlapping hostnames in already existing accounts
|
||||||
|
|
||||||
var (sessionId, configureClientResult) = await _certsFlowService.ConfigureClientAsync(requestData.IsStaging);
|
var (sessionId, configureClientResult) = await _certsFlowService.ConfigureClientAsync(requestData.IsStaging);
|
||||||
if (!configureClientResult.IsSuccess || sessionId == null) {
|
if (!configureClientResult.IsSuccess || sessionId == null) {
|
||||||
//LogErrors(configureClientResult.Errors);
|
//LogErrors(configureClientResult.Errors);
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user