From a7ea95fd2b78f9275b476027f04d96e6c167d52b Mon Sep 17 00:00:00 2001 From: Maksym Sadovnychyy Date: Sat, 29 Jun 2024 18:12:54 +0200 Subject: [PATCH] (feature): new account ui implementation --- src/ClientApp/app/layout.tsx | 3 +- src/ClientApp/app/page.tsx | 562 ++++-------------- src/ClientApp/app/register/page.tsx | 125 ++-- src/ClientApp/components/loader/index.tsx | 2 +- src/ClientApp/components/offcanvas.tsx | 22 +- src/ClientApp/components/topmenu.tsx | 9 +- src/ClientApp/controls/customEnumSelect.tsx | 2 +- src/ClientApp/entities/CacheAccount.ts | 4 +- src/ClientApp/{app => }/functions/deepCopy.ts | 0 src/ClientApp/{app => }/functions/enums.ts | 0 src/ClientApp/{app => }/functions/index.ts | 0 src/ClientApp/hooks/useValidation.tsx | 91 ++- src/ClientApp/partials/accoutEdit.tsx | 370 ++++++++++++ .../Services/AccoutService.cs | 3 + 14 files changed, 607 insertions(+), 586 deletions(-) rename src/ClientApp/{app => }/functions/deepCopy.ts (100%) rename src/ClientApp/{app => }/functions/enums.ts (100%) rename src/ClientApp/{app => }/functions/index.ts (100%) create mode 100644 src/ClientApp/partials/accoutEdit.tsx diff --git a/src/ClientApp/app/layout.tsx b/src/ClientApp/app/layout.tsx index 6e4971e..61ca127 100644 --- a/src/ClientApp/app/layout.tsx +++ b/src/ClientApp/app/layout.tsx @@ -81,13 +81,12 @@ const Layout: FC<{ children: React.ReactNode }> = ({ children }) => { />
- +
{children}
- diff --git a/src/ClientApp/app/page.tsx b/src/ClientApp/app/page.tsx index eb0da81..2cd72a8 100644 --- a/src/ClientApp/app/page.tsx +++ b/src/ClientApp/app/page.tsx @@ -3,11 +3,6 @@ import { ApiRoutes, GetApiRoute } from '@/ApiRoutes' import { httpService } from '@/services/httpService' import { FormEvent, useEffect, useRef, useState } from 'react' -import { - useValidation, - isValidEmail, - isValidHostname -} from '@/hooks/useValidation' import { CustomButton, CustomCheckbox, @@ -16,37 +11,19 @@ import { CustomRadioGroup } from '@/controls' import { GetAccountResponse } from '@/models/letsEncryptServer/account/responses/GetAccountResponse' -import { deepCopy, enumToArray } from './functions' +import { deepCopy, enumToArray } from '../functions' import { CacheAccount } from '@/entities/CacheAccount' import { ChallengeTypes } from '@/entities/ChallengeTypes' import { FaPlus, FaTrash } from 'react-icons/fa' import { PageContainer } from '@/components/pageContainer' +import { OffCanvas } from '@/components/offcanvas' +import { AccountEdit } from '@/partials/accoutEdit' export default function Page() { const [accounts, setAccounts] = useState([]) - const [initialAccounts, setInitialAccounts] = useState([]) - - const { - value: newContact, - error: contactError, - handleChange: handleContactChange, - reset: resetContact - } = useValidation({ - initialValue: '', - validateFn: isValidEmail, - errorMessage: 'Invalid email format.' - }) - - const { - value: newHostname, - error: hostnameError, - handleChange: handleHostnameChange, - reset: resetHostname - } = useValidation({ - initialValue: '', - validateFn: isValidHostname, - errorMessage: 'Invalid hostname format.' - }) + const [editingAccount, setEditingAccount] = useState( + null + ) const init = useRef(false) @@ -66,67 +43,36 @@ export default function Page() { accountId: account.accountId, isDisabled: account.isDisabled, description: account.description, - contacts: account.contacts, + contacts: account.contacts.map((contact) => contact), challengeType: account.challengeType, hostnames: - account.hostnames?.map((h) => ({ - hostname: h.hostname, - expires: new Date(h.expires), - isUpcomingExpire: h.isUpcomingExpire, - isDisabled: h.isDisabled + account.hostnames?.map((hostname) => ({ + hostname: hostname.hostname, + expires: new Date(hostname.expires), + isUpcomingExpire: hostname.isUpcomingExpire, + isDisabled: hostname.isDisabled })) ?? [], - isEditMode: false, - isStaging: account.isStaging + isStaging: account.isStaging, + isEditMode: false }) }) setAccounts(newAccounts) - setInitialAccounts(deepCopy(newAccounts)) // Clone initial state } fetchAccounts() init.current = true }, []) - const toggleEditMode = (accountId: string) => { + useEffect(() => { + console.log(editingAccount) + }, [editingAccount]) + + const handleAccountUpdate = (updatedAccount: CacheAccount) => { setAccounts( accounts.map((account) => - account.accountId === accountId - ? { ...account, isEditMode: !account.isEditMode } - : 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.accountId === updatedAccount.accountId + ? updatedAccount : account ) ) @@ -139,362 +85,70 @@ export default function Page() { // 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, - 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 ( - - {accounts.map((account) => ( -
-
-

- Account: {account.accountId} -

- toggleEditMode(account.accountId)} - className="bg-blue-500 text-white px-3 py-1 rounded" - > - {account.isEditMode ? 'View Mode' : 'Edit Mode'} - -
- {account.isEditMode ? ( -
handleSubmit(e, account.accountId)}> -
- - 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" - /> -
+ <> + + {accounts.map((account) => ( +
+
+

+ Account: {account.accountId} +

+ { + setEditingAccount(account) + }} + className="bg-blue-500 text-white px-3 py-1 rounded" + > + Edit + +
-
- - handleIsDisabledChange(account.accountId, value) - } - /> -
+
+

+ Description: {account.description} +

+
-
-

Contacts:

-
    - {account.contacts.map((contact) => ( -
  • - {contact} - - deleteContact(account.accountId, contact) - } - className="bg-red-500 text-white p-2 rounded ml-2" - > - - -
  • - ))} -
-
- - addContact(account.accountId)} - className="bg-green-500 text-white p-2 rounded ml-2" - > - - - -
-
-
-

Hostnames:

-
    - {account.hostnames.map((hostname) => ( -
  • -
    - {hostname.hostname} - {hostname.expires.toDateString()}{' '} - - - - {hostname.isUpcomingExpire - ? 'Upcoming' - : 'Not Upcoming'} - -
    - - deleteHostname(account.accountId, hostname.hostname) - } - className="bg-red-500 text-white p-2 rounded ml-2" - > - - -
  • - ))} -
-
- - addHostname(account.accountId)} - className="bg-green-500 text-white p-2 rounded ml-2" - > - - - -
-
-
- deleteAccount(account.accountId)} - className="bg-red-500 text-white p-2 rounded ml-2" - > - - - - Submit - -
- - ) : ( - <> -
-

- Description: {account.description} -

-
+
+ +
-
- -
- -
-

Contacts:

-
    - {account.contacts.map((contact) => ( -
  • - {contact} -
  • - ))} -
-
-
- - handleChallengeTypeChange(account.accountId, option) - } - disabled={true} - /> -
-
-

Hostnames:

-
    - {account.hostnames.map((hostname) => ( -
  • +
    +

    Contacts:

    +
      + {account.contacts.map((contact) => ( +
    • + {contact} +
    • + ))} +
    +
    +
    + + //handleChallengeTypeChange(account.accountId, option) + console.log('') + } + disabled={true} + /> +
    +
    +

    Hostnames:

    +
      + {account.hostnames?.map((hostname) => ( +
    • +
      {hostname.hostname} - {hostname.expires.toDateString()} - -
    • - ))} -
    -
    +
+ + ))} + +
-
- -
- - )} -
- ))} -
+
+ +
+ + ))} + + + setEditingAccount(null)} + > + {editingAccount && } + + ) } diff --git a/src/ClientApp/app/register/page.tsx b/src/ClientApp/app/register/page.tsx index 7d31692..d6c1183 100644 --- a/src/ClientApp/app/register/page.tsx +++ b/src/ClientApp/app/register/page.tsx @@ -1,6 +1,6 @@ 'use client' -import { FormEvent, useEffect, useRef, useState } from 'react' +import { FormEvent, useCallback, useState } from 'react' import { useValidation, isBypass, @@ -14,7 +14,7 @@ import { CustomRadioGroup } from '@/controls' import { FaTrash, FaPlus } from 'react-icons/fa' -import { deepCopy } from '../functions' +import { deepCopy } from '../../functions' import { PostAccountRequest, validatePostAccountRequest @@ -31,11 +31,14 @@ const RegisterPage = () => { const [account, setAccount] = useState({ description: '', contacts: [], - challengeType: '', + challengeType: ChallengeTypes.http01, hostnames: [], isStaging: true }) + const [newHostname, setNewHostname] = useState('') + const [newContact, setNewContact] = useState('') + const dispatch = useAppDispatch() const { @@ -44,7 +47,15 @@ const RegisterPage = () => { handleChange: handleDescriptionChange, reset: resetDescription } = useValidation({ - initialValue: '', + defaultValue: '', + externalValue: account.description, + setExternalValue: (newDescription) => { + setAccount((prev) => { + const newAccount = deepCopy(prev) + newAccount.description = newDescription + return newAccount + }) + }, validateFn: isBypass, errorMessage: '' }) @@ -55,62 +66,56 @@ const RegisterPage = () => { handleChange: handleContactChange, reset: resetContact } = useValidation({ - initialValue: '', + defaultValue: '', + externalValue: newContact, + setExternalValue: setNewContact, validateFn: isValidContact, errorMessage: 'Invalid contact. Must be a valid email or phone number.' }) - const { - value: challengeType, - error: challengeTypeError, - handleChange: handleChallengeTypeChange, - reset: resetChallengeType - } = useValidation({ - initialValue: ChallengeTypes.http01, - validateFn: isBypass, - errorMessage: '' - }) - const { value: hostname, error: hostnameError, handleChange: handleHostnameChange, reset: resetHostname } = useValidation({ - initialValue: '', + defaultValue: '', + externalValue: newHostname, + setExternalValue: setNewHostname, validateFn: isValidHostname, errorMessage: 'Invalid hostname format.' }) - const init = useRef(false) - useEffect(() => { - if (init.current) return - - init.current = true - }, []) - - useEffect(() => { - setAccount((prev) => { - const newAccount = deepCopy(prev) - newAccount.description = description - newAccount.challengeType = challengeType - return newAccount - }) - }, [description, challengeType]) + const { + value: challengeType, + error: challengeTypeError, + handleChange: handleChallengeTypeChange, + reset: resetChallengeType + } = useValidation({ + defaultValue: ChallengeTypes.http01, + externalValue: account.challengeType, + setExternalValue: (newChallengeType) => { + setAccount((prev) => { + const newAccount = deepCopy(prev) + newAccount.challengeType = newChallengeType + return newAccount + }) + }, + validateFn: isBypass, + errorMessage: '' + }) const handleAddContact = () => { - if ( - contact === '' || - account?.contacts.includes(contact) || - contactError !== '' - ) { + if (contactError !== '') { resetContact() return } setAccount((prev) => { const newAccount = deepCopy(prev) - newAccount.contacts.push(contact) + if (!newAccount.contacts.includes(contact)) { + newAccount.contacts.push(contact) + } return newAccount }) @@ -126,18 +131,16 @@ const RegisterPage = () => { } const handleAddHostname = () => { - if ( - hostname === '' || - account?.hostnames.includes(hostname) || - hostnameError !== '' - ) { + if (hostnameError !== '') { resetHostname() return } setAccount((prev) => { const newAccount = deepCopy(prev) - newAccount.hostnames.push(hostname) + if (!newAccount.hostnames.includes(hostname)) { + newAccount.hostnames.push(hostname) + } return newAccount }) @@ -183,7 +186,7 @@ const RegisterPage = () => {
{ inputClassName="border p-2 rounded w-full" errorClassName="text-red-500 text-sm mt-1" className="mr-2 flex-grow" + /> + - - - - + +
@@ -240,7 +242,7 @@ const RegisterPage = () => { error={challengeTypeError} title="Challenge Type" enumType={ChallengeTypes} - selectedValue={account.challengeType} + selectedValue={challengeType} onChange={handleChallengeTypeChange} selectBoxClassName="border p-2 rounded w-full" errorClassName="text-red-500 text-sm mt-1" @@ -278,15 +280,14 @@ const RegisterPage = () => { inputClassName="border p-2 rounded w-full" errorClassName="text-red-500 text-sm mt-1" className="mr-2 flex-grow" + /> + - - - - + +
diff --git a/src/ClientApp/components/loader/index.tsx b/src/ClientApp/components/loader/index.tsx index 27232a0..9089ca3 100644 --- a/src/ClientApp/components/loader/index.tsx +++ b/src/ClientApp/components/loader/index.tsx @@ -16,7 +16,7 @@ const Loader: React.FC = () => { if (activeRequests > 0) { timeout = setTimeout(() => { dispatch(reset()) - }, 10000) // Adjust the timeout as necessary + }, 120000) // Adjust the timeout as necessary } return () => { diff --git a/src/ClientApp/components/offcanvas.tsx b/src/ClientApp/components/offcanvas.tsx index 16ea4cf..b56bc9f 100644 --- a/src/ClientApp/components/offcanvas.tsx +++ b/src/ClientApp/components/offcanvas.tsx @@ -1,29 +1,37 @@ import React, { FC } from 'react' interface OffCanvasProps { + title?: string + children: React.ReactNode isOpen: boolean - onClose: () => void + onClose?: () => void } -const OffCanvas: FC = ({ isOpen, onClose }) => { +const OffCanvas: FC = (props) => { + const { title, children, isOpen, onClose } = props + + const handleOnClose = () => { + onClose?.() + } + return (
e.stopPropagation()} >
-

Settings

-
- {/* Your off-canvas content goes here */} +
{children}
) diff --git a/src/ClientApp/components/topmenu.tsx b/src/ClientApp/components/topmenu.tsx index f3cf096..2c4ebb0 100644 --- a/src/ClientApp/components/topmenu.tsx +++ b/src/ClientApp/components/topmenu.tsx @@ -4,11 +4,9 @@ import React, { FC, useState } from 'react' import { FaCog, FaBars } from 'react-icons/fa' import Link from 'next/link' -interface TopMenuProps { - onToggleOffCanvas: () => void -} +interface TopMenuProps {} -const TopMenu: FC = ({ onToggleOffCanvas }) => { +const TopMenu: FC = () => { const [isMenuOpen, setIsMenuOpen] = useState(false) const toggleMenu = () => { @@ -44,9 +42,6 @@ const TopMenu: FC = ({ onToggleOffCanvas }) => { )} - diff --git a/src/ClientApp/controls/customEnumSelect.tsx b/src/ClientApp/controls/customEnumSelect.tsx index d3cf3fc..1417b18 100644 --- a/src/ClientApp/controls/customEnumSelect.tsx +++ b/src/ClientApp/controls/customEnumSelect.tsx @@ -4,7 +4,7 @@ import { CustomSelectOption, CustomSelectPropsBase } from './customSelect' -import { enumToArray } from '@/app/functions' +import { enumToArray } from '@/functions' interface CustomEnumSelectProps extends CustomSelectPropsBase { enumType: any diff --git a/src/ClientApp/entities/CacheAccount.ts b/src/ClientApp/entities/CacheAccount.ts index 67a0407..e11a036 100644 --- a/src/ClientApp/entities/CacheAccount.ts +++ b/src/ClientApp/entities/CacheAccount.ts @@ -3,10 +3,10 @@ import { CacheAccountHostname } from './CacheAccountHostname' export interface CacheAccount { accountId: string isDisabled: boolean - description?: string + description: string contacts: string[] challengeType?: string - hostnames: CacheAccountHostname[] + hostnames?: CacheAccountHostname[] isEditMode: boolean isStaging: boolean } diff --git a/src/ClientApp/app/functions/deepCopy.ts b/src/ClientApp/functions/deepCopy.ts similarity index 100% rename from src/ClientApp/app/functions/deepCopy.ts rename to src/ClientApp/functions/deepCopy.ts diff --git a/src/ClientApp/app/functions/enums.ts b/src/ClientApp/functions/enums.ts similarity index 100% rename from src/ClientApp/app/functions/enums.ts rename to src/ClientApp/functions/enums.ts diff --git a/src/ClientApp/app/functions/index.ts b/src/ClientApp/functions/index.ts similarity index 100% rename from src/ClientApp/app/functions/index.ts rename to src/ClientApp/functions/index.ts diff --git a/src/ClientApp/hooks/useValidation.tsx b/src/ClientApp/hooks/useValidation.tsx index 198e89e..3746fb3 100644 --- a/src/ClientApp/hooks/useValidation.tsx +++ b/src/ClientApp/hooks/useValidation.tsx @@ -1,63 +1,47 @@ import { useState, useEffect, useCallback } from 'react' // Helper functions for validation -const isBypass = (value: any) => { - return true -} - -const isValidEmail = (email: string) => { - const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/ - return emailRegex.test(email) -} - -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) -} +const isBypass = (value: any) => 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) => + isValidEmail(contact) || isValidPhoneNumber(contact) +const isValidHostname = (hostname: string) => + /^(?!:\/\/)([a-zA-Z0-9-_]{1,63}\.?)+[a-zA-Z]{2,6}$/.test(hostname) // Props interface for useValidation hook interface UseValidationProps { - initialValue: T + externalValue: T + setExternalValue: (value: T) => void validateFn: (value: T) => boolean errorMessage: string - emptyFieldMessage?: string // Optional custom message for empty fields - defaultResetValue?: T // Optional default reset value + emptyFieldMessage?: string + defaultValue: T } // Custom hook for input validation -const useValidation = ( +const useValidation = ( props: UseValidationProps ) => { const { - initialValue, + externalValue, + setExternalValue, validateFn, errorMessage, - emptyFieldMessage = 'This field cannot be empty.', // Default message - defaultResetValue + emptyFieldMessage = 'This field cannot be empty.', + defaultValue } = props - const [value, setValue] = useState(initialValue) + const [internalValue, setInternalValue] = useState(externalValue) const [error, setError] = useState('') - const handleChange = useCallback( - (newValue: T) => { - setValue(newValue) + const validate = useCallback( + (value: T) => { const stringValue = - newValue instanceof Date - ? newValue.toISOString() - : newValue.toString().trim() + value instanceof Date ? value.toISOString() : value.toString().trim() if (stringValue === '') { setError(emptyFieldMessage) - } else if (!validateFn(newValue)) { + } else if (!validateFn(value)) { setError(errorMessage) } else { setError('') @@ -66,27 +50,26 @@ const useValidation = ( [emptyFieldMessage, errorMessage, validateFn] ) + const handleChange = useCallback( + (newValue: T) => { + setInternalValue(newValue) + setExternalValue(newValue) + validate(newValue) + }, + [setExternalValue, validate] + ) + useEffect(() => { - handleChange(initialValue) - }, [initialValue, handleChange]) + setInternalValue(externalValue) + validate(externalValue) + }, [externalValue, validate]) const reset = useCallback(() => { - const resetValue = - defaultResetValue !== undefined ? defaultResetValue : initialValue + setInternalValue(defaultValue) + setError('') + }, [defaultValue]) - 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 } + return { value: internalValue, error, handleChange, reset } } export { diff --git a/src/ClientApp/partials/accoutEdit.tsx b/src/ClientApp/partials/accoutEdit.tsx new file mode 100644 index 0000000..3a9a260 --- /dev/null +++ b/src/ClientApp/partials/accoutEdit.tsx @@ -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 = (props) => { + const { account, onCancel, onSave, onDelete } = props + + const [editingAccount, setEditingAccount] = useState(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({ + defaultValue: '', + externalValue: account.description, + setExternalValue: setDescription, + validateFn: isBypass, + errorMessage: '' + }) + + const { + value: contact, + error: contactError, + handleChange: handleContactChange, + reset: resetContact + } = useValidation({ + defaultValue: '', + externalValue: newContact, + setExternalValue: setNewContact, + validateFn: isValidEmail, + errorMessage: 'Invalid email format.' + }) + + const { + value: hostname, + error: hostnameError, + handleChange: handleHostnameChange, + reset: resetHostname + } = useValidation({ + 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) => { + 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 ( + +
+ +
+ +
+ handleIsDisabledChange(value)} + className="mr-2 flex-grow" + /> +
+ +
+ handleChallengeTypeChange(option)} + className="mr-2 flex-grow" + /> +
+ +
+

Contacts:

+
    + {account.contacts.map((contact) => ( +
  • + {contact} + deleteContact(contact)} + className="bg-red-500 text-white p-2 rounded ml-2" + > + + +
  • + ))} +
+
+ + + + +
+
+
+

Hostnames:

+
    + {account.hostnames?.map((hostname) => ( +
  • +
    + {hostname.hostname} - {hostname.expires.toDateString()} -{' '} + + {hostname.isUpcomingExpire ? 'Upcoming' : 'Not Upcoming'} + {' '} + -{' '} + + handleHostnameDisabledChange(hostname.hostname, value) + } + /> +
    + + deleteHostname(hostname.hostname)} + className="bg-red-500 text-white p-2 rounded ml-2" + > + + +
  • + ))} +
+
+ + + + +
+
+
+ +
+
+ onDelete(account.accountId)} + className="bg-red-500 text-white p-2 rounded ml-2" + > + + + + Cancel + + + Save + +
+ + ) +} + +export { AccountEdit } diff --git a/src/LetsEncryptServer/Services/AccoutService.cs b/src/LetsEncryptServer/Services/AccoutService.cs index 0411e0b..fe4e3d9 100644 --- a/src/LetsEncryptServer/Services/AccoutService.cs +++ b/src/LetsEncryptServer/Services/AccoutService.cs @@ -72,6 +72,9 @@ public class AccountService : IAccountService { } 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); if (!configureClientResult.IsSuccess || sessionId == null) { //LogErrors(configureClientResult.Errors);