(feature): patch account form complete

This commit is contained in:
Maksym Sadovnychyy 2024-07-04 23:34:23 +02:00
parent a7ea95fd2b
commit 3e0c25158e
9 changed files with 340 additions and 196 deletions

View File

@ -18,6 +18,7 @@ import { FaPlus, FaTrash } from 'react-icons/fa'
import { PageContainer } from '@/components/pageContainer' import { PageContainer } from '@/components/pageContainer'
import { OffCanvas } from '@/components/offcanvas' import { OffCanvas } from '@/components/offcanvas'
import { AccountEdit } from '@/partials/accoutEdit' import { AccountEdit } from '@/partials/accoutEdit'
import { get } from 'http'
export default function Page() { export default function Page() {
const [accounts, setAccounts] = useState<CacheAccount[]>([]) const [accounts, setAccounts] = useState<CacheAccount[]>([])
@ -34,11 +35,13 @@ export default function Page() {
const fetchAccounts = async () => { const fetchAccounts = async () => {
const newAccounts: CacheAccount[] = [] const newAccounts: CacheAccount[] = []
const accounts = await httpService.get<GetAccountResponse[]>( const gatAccountsResult = await httpService.get<GetAccountResponse[]>(
GetApiRoute(ApiRoutes.ACCOUNTS) GetApiRoute(ApiRoutes.ACCOUNTS)
) )
accounts?.forEach((account) => { if (!gatAccountsResult.isSuccess) return
gatAccountsResult.data?.forEach((account) => {
newAccounts.push({ newAccounts.push({
accountId: account.accountId, accountId: account.accountId,
isDisabled: account.isDisabled, isDisabled: account.isDisabled,
@ -79,10 +82,15 @@ export default function Page() {
} }
const deleteAccount = (accountId: string) => { const deleteAccount = (accountId: string) => {
setAccounts(accounts.filter((account) => account.accountId !== accountId)) httpService
.delete(GetApiRoute(ApiRoutes.ACCOUNT_ID, accountId))
// TODO: Revoke all certificates .then((response) => {
// TODO: Remove from cache if (response.isSuccess) {
setAccounts(
accounts.filter((account) => account.accountId !== accountId)
)
}
})
} }
return ( return (
@ -195,7 +203,14 @@ export default function Page() {
isOpen={editingAccount !== null} isOpen={editingAccount !== null}
onClose={() => setEditingAccount(null)} onClose={() => setEditingAccount(null)}
> >
{editingAccount && <AccountEdit account={editingAccount} />} {editingAccount && (
<AccountEdit
account={editingAccount}
setAccount={setEditingAccount}
onCancel={() => setEditingAccount(null)}
onDelete={deleteAccount}
/>
)}
</OffCanvas> </OffCanvas>
</> </>
) )

View File

@ -8,7 +8,7 @@ export interface CustomSelectOption {
export interface CustomSelectPropsBase { export interface CustomSelectPropsBase {
selectedValue: string | null | undefined selectedValue: string | null | undefined
onChange: (value: string) => void onChange?: (value: string) => void
readOnly?: boolean readOnly?: boolean
disabled?: boolean disabled?: boolean
title?: string title?: string
@ -45,7 +45,7 @@ const CustomSelect: React.FC<CustomSelectProps> = ({
const handleOptionClick = (option: CustomSelectOption) => { const handleOptionClick = (option: CustomSelectOption) => {
if (!readOnly && !disabled) { if (!readOnly && !disabled) {
onChange(option.value) onChange?.(option.value)
setIsOpen(false) setIsOpen(false)
} }
} }

View File

@ -1,3 +1,5 @@
import { PatchOperation } from './PatchOperation'
export interface PatchAction<T> { export interface PatchAction<T> {
op: PatchOperation // Enum for operation type op: PatchOperation // Enum for operation type
index?: number // Index for the operation (for arrays/lists) index?: number // Index for the operation (for arrays/lists)

View File

@ -1,5 +1,6 @@
enum PatchOperation { export enum PatchOperation {
Add, Add,
Remove, Remove,
Replace Replace,
None
} }

View File

@ -1,6 +1,13 @@
import { PatchAction } from '@/models/PatchAction' import { PatchAction } from '@/models/PatchAction'
export interface PatchHostnameRequest {
hostname?: PatchAction<string>
isDisabled?: PatchAction<boolean>
}
export interface PatchAccountRequest { export interface PatchAccountRequest {
description?: PatchAction<string> description?: PatchAction<string>
isDisabled?: PatchAction<boolean>
contacts?: PatchAction<string>[] contacts?: PatchAction<string>[]
hostnames?: PatchHostnameRequest[]
} }

View File

@ -1,6 +1,6 @@
'use client' 'use client'
import { FormEvent, useCallback, useState } from 'react' import { Dispatch, FormEvent, SetStateAction, useEffect, useState } from 'react'
import { import {
useValidation, useValidation,
isValidEmail, isValidEmail,
@ -17,39 +17,71 @@ import {
import { CacheAccount } from '@/entities/CacheAccount' import { CacheAccount } from '@/entities/CacheAccount'
import { FaPlus, FaTrash } from 'react-icons/fa' import { FaPlus, FaTrash } from 'react-icons/fa'
import { ChallengeTypes } from '@/entities/ChallengeTypes' import { ChallengeTypes } from '@/entities/ChallengeTypes'
import { deepCopy } from '@/functions'
import { ApiRoutes, GetApiRoute } from '@/ApiRoutes'
import { httpService } from '@/services/httpService'
import { PatchAccountRequest } from '@/models/letsEncryptServer/account/requests/PatchAccountRequest'
import { PatchOperation } from '@/models/PatchOperation'
import { useAppDispatch } from '@/redux/store'
import { showToast } from '@/redux/slices/toastSlice'
interface AccountEditProps { interface AccountEditProps {
account: CacheAccount account: CacheAccount
setAccount: Dispatch<SetStateAction<CacheAccount | null>>
onCancel?: () => void onCancel?: () => void
onSave?: (account: CacheAccount) => void onSubmit?: (account: CacheAccount) => void
onDelete?: (accountId: string) => void onDelete: (accountId: string) => void
} }
const AccountEdit: React.FC<AccountEditProps> = (props) => { const AccountEdit: React.FC<AccountEditProps> = ({
const { account, onCancel, onSave, onDelete } = props account,
setAccount,
onCancel,
onSubmit,
onDelete
}) => {
const dispatch = useAppDispatch()
const [newAccount, setNewAccount] = useState<PatchAccountRequest>({
description: { op: PatchOperation.None, value: account.description },
isDisabled: { op: PatchOperation.None, value: account.isDisabled },
contacts: account.contacts.map((contact) => ({
op: PatchOperation.None,
value: contact
})),
hostnames: account.hostnames?.map((hostname) => ({
hostname: { op: PatchOperation.None, value: hostname.hostname },
isDisabled: { op: PatchOperation.None, value: hostname.isDisabled }
}))
})
const [editingAccount, setEditingAccount] = useState<CacheAccount>(account)
const [newContact, setNewContact] = useState('') const [newContact, setNewContact] = useState('')
const [newHostname, setNewHostname] = useState('') const [newHostname, setNewHostname] = useState('')
const setDescription = useCallback( useEffect(() => {
(newDescription: string) => { console.log(newAccount)
if (editingAccount) { }, [newAccount])
setEditingAccount({ ...editingAccount, description: newDescription })
}
},
[editingAccount]
)
const { const {
value: description, value: description,
error: descriptionError, error: descriptionError,
handleChange: handleDescriptionChange, handleChange: handleDescriptionChange
reset: resetDescription
} = useValidation<string>({ } = useValidation<string>({
defaultValue: '', defaultValue: '',
externalValue: account.description, externalValue: newAccount.description?.value ?? '',
setExternalValue: setDescription, setExternalValue: (newDescription) => {
setNewAccount((prev) => {
const newAccount = deepCopy(prev)
newAccount.description = {
op:
newDescription !== account.description
? PatchOperation.Replace
: PatchOperation.None,
value: newDescription
}
return newAccount
})
},
validateFn: isBypass, validateFn: isBypass,
errorMessage: '' errorMessage: ''
}) })
@ -81,122 +113,147 @@ const AccountEdit: React.FC<AccountEditProps> = (props) => {
}) })
const handleIsDisabledChange = (value: boolean) => { const handleIsDisabledChange = (value: boolean) => {
// setAccount({ ...account, isDisabled: value }) setNewAccount((prev) => {
const newAccount = deepCopy(prev)
newAccount.isDisabled = {
op:
value !== account.isDisabled
? PatchOperation.Replace
: PatchOperation.None,
value
}
return newAccount
})
} }
const handleChallengeTypeChange = (option: any) => { const handleAddContact = () => {
//setAccount({ ...account, challengeType: option.value }) if (newContact === '' || contactError) return
// Check if the contact already exists in the account
const contactExists = newAccount.contacts?.some(
(contact) => contact.value === newContact
)
if (contactExists) {
// Optionally, handle the duplicate contact case, e.g., show an error message
dispatch(
showToast({ message: 'Contact already exists.', type: 'warning' })
)
resetContact()
return
}
// If the contact does not exist, add it
setNewAccount((prev) => {
const newAccount = deepCopy(prev)
newAccount.contacts?.push({ op: PatchOperation.Add, value: newContact })
return newAccount
})
resetContact()
}
const handleDeleteContact = (contact: string) => {
setNewAccount((prev) => {
const newAccount = deepCopy(prev)
newAccount.contacts = newAccount.contacts
?.map((c) => {
if (c.value === contact && c.op !== PatchOperation.Add)
c.op = PatchOperation.Remove
return c
})
.filter((c) => !(c.value === contact && c.op === PatchOperation.Add))
return newAccount
})
}
const handleAddHostname = () => {
if (newHostname === '' || hostnameError) return
// Check if the hostname already exists in the account
const hostnameExists = newAccount.hostnames?.some(
(hostname) => hostname.hostname?.value === newHostname
)
if (hostnameExists) {
// Optionally, handle the duplicate hostname case, e.g., show an error message
dispatch(
showToast({ message: 'Hostname already exists.', type: 'warning' })
)
resetHostname()
return
}
// If the hostname does not exist, add it
setNewAccount((prev) => {
const newAccount = deepCopy(prev)
newAccount.hostnames?.push({
hostname: { op: PatchOperation.Add, value: newHostname },
isDisabled: { op: PatchOperation.Add, value: false }
})
return newAccount
})
resetHostname()
} }
const handleHostnameDisabledChange = (hostname: string, value: boolean) => { const handleHostnameDisabledChange = (hostname: string, value: boolean) => {
// setAccount({ setNewAccount((prev) => {
// ...account, const newAccount = deepCopy(prev)
// hostnames: account.hostnames.map((h) => const targetHostname = newAccount.hostnames?.find(
// h.hostname === hostname ? { ...h, isDisabled: value } : h (h) => h.hostname?.value === hostname
// ) )
// }) if (targetHostname) {
// } targetHostname.isDisabled = {
// const handleStagingChange = (value: string) => { op:
// setAccount({ ...account, isStaging: value === 'staging' }) value !== targetHostname.isDisabled?.value
? PatchOperation.Replace
: PatchOperation.None,
value
}
}
return newAccount
})
} }
const deleteContact = (contact: string) => { const handleDeleteHostname = (hostname: string) => {
if (account?.contacts.length ?? 0 < 1) return setNewAccount((prev) => {
const newAccount = deepCopy(prev)
// setAccount({ newAccount.hostnames = newAccount.hostnames
// ...account, ?.map((h) => {
// contacts: account.contacts.filter((c) => c !== contact) if (
// }) h.hostname?.value === hostname &&
// } h.hostname?.op !== PatchOperation.Add
)
// const addContact = () => { h.hostname.op = PatchOperation.Remove
// if (newContact === '' || contactError) { return h
// return })
// } .filter(
(h) =>
// if (account.contacts.includes(newContact)) return !(
h.hostname?.value === hostname &&
// setAccount({ ...account, contacts: [...account.contacts, newContact] }) h.hostname?.op === PatchOperation.Add
// resetContact() )
)
return newAccount
})
} }
const deleteHostname = (hostname: string) => { const handleCancel = () => {
//if (account?.hostnames.length ?? 0 < 1) return onCancel?.()
// 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>) => { const handleSubmit = async (e: FormEvent<HTMLFormElement>) => {
e.preventDefault() e.preventDefault()
// const contactChanges = { httpService.patch<PatchAccountRequest, CacheAccount>(
// added: account.contacts.filter( GetApiRoute(ApiRoutes.ACCOUNT_ID, account.accountId),
// (contact) => !initialAccountState.contacts.includes(contact) newAccount
// ), )
// removed: initialAccountState.contacts.filter( }
// (contact) => !account.contacts.includes(contact)
// )
// }
// const hostnameChanges = { const handleDelete = (accountId: string) => {
// added: account.hostnames.filter( onDelete?.(accountId)
// (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 ( return (
@ -217,19 +274,9 @@ const AccountEdit: React.FC<AccountEditProps> = (props) => {
<div className="mb-4"> <div className="mb-4">
<CustomCheckbox <CustomCheckbox
checked={account.isDisabled} checked={newAccount.isDisabled?.value ?? false}
label="Disabled" label="Disabled"
onChange={(value) => handleIsDisabledChange(value)} onChange={handleIsDisabledChange}
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" className="mr-2 flex-grow"
/> />
</div> </div>
@ -237,22 +284,24 @@ const AccountEdit: React.FC<AccountEditProps> = (props) => {
<div className="mb-4"> <div className="mb-4">
<h3 className="text-xl font-medium mb-2">Contacts:</h3> <h3 className="text-xl font-medium mb-2">Contacts:</h3>
<ul className="list-disc list-inside pl-4 mb-2"> <ul className="list-disc list-inside pl-4 mb-2">
{account.contacts.map((contact) => ( {newAccount.contacts?.map((contact) => (
<li key={contact} className="text-gray-700 mb-2 inline-flex"> <li key={contact.value} className="text-gray-700 mb-2">
{contact} <div className="inline-flex">
<CustomButton {contact.value}
type="button" <CustomButton
onClick={() => deleteContact(contact)} type="button"
className="bg-red-500 text-white p-2 rounded ml-2" onClick={() => handleDeleteContact(contact.value ?? '')}
> className="bg-red-500 text-white p-2 rounded ml-2"
<FaTrash /> >
</CustomButton> <FaTrash />
</CustomButton>
</div>
</li> </li>
))} ))}
</ul> </ul>
<div className="flex items-center mb-4"> <div className="flex items-center mb-4">
<CustomInput <CustomInput
value={newContact} value={contact}
onChange={handleContactChange} onChange={handleContactChange}
placeholder="Add new contact" placeholder="Add new contact"
type="email" type="email"
@ -264,43 +313,49 @@ const AccountEdit: React.FC<AccountEditProps> = (props) => {
/> />
<CustomButton <CustomButton
type="button" type="button"
//onClick={addContact} onClick={handleAddContact}
className="bg-green-500 text-white p-2 rounded ml-2" className="bg-green-500 text-white p-2 rounded ml-2"
> >
<FaPlus /> <FaPlus />
</CustomButton> </CustomButton>
</div> </div>
</div> </div>
<div className="mb-4">
<CustomEnumSelect
title="Challenge Type"
enumType={ChallengeTypes}
selectedValue={account.challengeType}
className="mr-2 flex-grow"
disabled={true}
/>
</div>
<div> <div>
<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) => ( {newAccount.hostnames?.map((hostname) => (
<li key={hostname.hostname} className="text-gray-700 mb-2"> <li key={hostname.hostname?.value} className="text-gray-700 mb-2">
<div className="inline-flex"> <div className="inline-flex">
{hostname.hostname} - {hostname.expires.toDateString()} -{' '} {hostname.hostname?.value} -{' '}
<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 <CustomCheckbox
className="ml-2" className="ml-2"
checked={hostname.isDisabled} checked={hostname.isDisabled?.value ?? false}
label="Disabled" label="Disabled"
onChange={(value) => onChange={(value) =>
handleHostnameDisabledChange(hostname.hostname, value) handleHostnameDisabledChange(
hostname.hostname?.value ?? '',
value
)
} }
/> />
</div> </div>
<CustomButton <CustomButton
type="button" type="button"
onClick={() => deleteHostname(hostname.hostname)} onClick={() =>
handleDeleteHostname(hostname.hostname?.value ?? '')
}
className="bg-red-500 text-white p-2 rounded ml-2" className="bg-red-500 text-white p-2 rounded ml-2"
> >
<FaTrash /> <FaTrash />
@ -310,7 +365,7 @@ const AccountEdit: React.FC<AccountEditProps> = (props) => {
</ul> </ul>
<div className="flex items-center"> <div className="flex items-center">
<CustomInput <CustomInput
value={newHostname} value={hostname}
onChange={handleHostnameChange} onChange={handleHostnameChange}
placeholder="Add new hostname" placeholder="Add new hostname"
type="text" type="text"
@ -322,7 +377,7 @@ const AccountEdit: React.FC<AccountEditProps> = (props) => {
/> />
<CustomButton <CustomButton
type="button" type="button"
//onClick={addHostname} onClick={handleAddHostname}
className="bg-green-500 text-white p-2 rounded ml-2" className="bg-green-500 text-white p-2 rounded ml-2"
> >
<FaPlus /> <FaPlus />
@ -345,13 +400,15 @@ const AccountEdit: React.FC<AccountEditProps> = (props) => {
</div> </div>
<div className="flex justify-between mt-4"> <div className="flex justify-between mt-4">
<CustomButton <CustomButton
//onClick={() => onDelete(account.accountId)} type="button"
onClick={() => handleDelete(account.accountId)}
className="bg-red-500 text-white p-2 rounded ml-2" className="bg-red-500 text-white p-2 rounded ml-2"
> >
<FaTrash /> <FaTrash />
</CustomButton> </CustomButton>
<CustomButton <CustomButton
onClick={onCancel} type="button"
onClick={handleCancel}
className="bg-yellow-500 text-white p-2 rounded ml-2" className="bg-yellow-500 text-white p-2 rounded ml-2"
> >
Cancel Cancel

View File

@ -1,6 +1,13 @@
import { store } from '@/redux/store' import { store } from '@/redux/store'
import { increment, decrement } from '@/redux/slices/loaderSlice' import { increment, decrement } from '@/redux/slices/loaderSlice'
import { showToast } from '@/redux/slices/toastSlice' import { showToast } from '@/redux/slices/toastSlice'
import { PatchOperation } from '@/models/PatchOperation'
interface HttpResponse<T> {
data: T | null
status: number
isSuccess: boolean
}
interface RequestInterceptor { interface RequestInterceptor {
(req: XMLHttpRequest): void (req: XMLHttpRequest): void
@ -47,7 +54,7 @@ class HttpService {
method: string, method: string,
url: string, url: string,
data?: any data?: any
): Promise<TResponse | null> { ): Promise<HttpResponse<TResponse>> {
const xhr = new XMLHttpRequest() const xhr = new XMLHttpRequest()
xhr.open(method, url) xhr.open(method, url)
@ -59,7 +66,7 @@ class HttpService {
this.invokeIncrement() this.invokeIncrement()
return new Promise<TResponse | null>((resolve) => { return new Promise<HttpResponse<TResponse>>((resolve) => {
xhr.onload = () => this.handleLoad<TResponse>(xhr, resolve) xhr.onload = () => this.handleLoad<TResponse>(xhr, resolve)
xhr.onerror = () => this.handleNetworkError(resolve) xhr.onerror = () => this.handleNetworkError(resolve)
xhr.send(data ? JSON.stringify(data) : null) xhr.send(data ? JSON.stringify(data) : null)
@ -102,7 +109,7 @@ class HttpService {
private handleLoad<TResponse>( private handleLoad<TResponse>(
xhr: XMLHttpRequest, xhr: XMLHttpRequest,
resolve: (value: TResponse | null) => void resolve: (value: HttpResponse<TResponse>) => void
): void { ): void {
this.invokeDecrement() this.invokeDecrement()
if (xhr.status >= 200 && xhr.status < 300) { if (xhr.status >= 200 && xhr.status < 300) {
@ -114,14 +121,22 @@ class HttpService {
private handleSuccessfulResponse<TResponse>( private handleSuccessfulResponse<TResponse>(
xhr: XMLHttpRequest, xhr: XMLHttpRequest,
resolve: (value: TResponse | null) => void resolve: (value: HttpResponse<TResponse>) => void
): void { ): void {
try { try {
if (xhr.response) { if (xhr.response) {
const response = JSON.parse(xhr.response) const response = JSON.parse(xhr.response)
resolve(this.handleResponseInterceptors(response, null) as TResponse) resolve({
data: this.handleResponseInterceptors(response, null) as TResponse,
status: xhr.status,
isSuccess: true
})
} else { } else {
resolve(null) resolve({
data: null,
status: xhr.status,
isSuccess: true
})
} }
} catch (error) { } catch (error) {
const problemDetails = this.createProblemDetails( const problemDetails = this.createProblemDetails(
@ -130,13 +145,17 @@ class HttpService {
xhr.status xhr.status
) )
this.showProblemDetails(problemDetails) this.showProblemDetails(problemDetails)
resolve(null) resolve({
data: null,
status: xhr.status,
isSuccess: false
})
} }
} }
private handleErrorResponse<TResponse>( private handleErrorResponse<TResponse>(
xhr: XMLHttpRequest, xhr: XMLHttpRequest,
resolve: (value: TResponse | null) => void resolve: (value: HttpResponse<TResponse>) => void
): void { ): void {
const problemDetails = this.createProblemDetails( const problemDetails = this.createProblemDetails(
xhr.statusText, xhr.statusText,
@ -144,15 +163,23 @@ class HttpService {
xhr.status xhr.status
) )
this.showProblemDetails(problemDetails) this.showProblemDetails(problemDetails)
resolve(this.handleResponseInterceptors(null, problemDetails)) resolve({
data: this.handleResponseInterceptors(null, problemDetails),
status: xhr.status,
isSuccess: false
})
} }
private handleNetworkError<TResponse>( private handleNetworkError<TResponse>(
resolve: (value: TResponse | null) => void resolve: (value: HttpResponse<TResponse>) => void
): void { ): void {
const problemDetails = this.createProblemDetails('Network Error', null, 0) const problemDetails = this.createProblemDetails('Network Error', null, 0)
this.showProblemDetails(problemDetails) this.showProblemDetails(problemDetails)
resolve(this.handleResponseInterceptors(null, problemDetails)) resolve({
data: this.handleResponseInterceptors(null, problemDetails),
status: 0,
isSuccess: false
})
} }
private createProblemDetails( private createProblemDetails(
@ -178,26 +205,57 @@ class HttpService {
} }
} }
public async get<TResponse>(url: string): Promise<TResponse | null> { public async get<TResponse>(url: string): Promise<HttpResponse<TResponse>> {
return await this.request<TResponse>('GET', url) return await this.request<TResponse>('GET', url)
} }
public async post<TRequest, TResponse>( public async post<TRequest, TResponse>(
url: string, url: string,
data: TRequest data: TRequest
): Promise<TResponse | null> { ): Promise<HttpResponse<TResponse>> {
return await this.request<TResponse>('POST', url, data) return await this.request<TResponse>('POST', url, data)
} }
public async put<TRequest, TResponse>( public async put<TRequest, TResponse>(
url: string, url: string,
data: TRequest data: TRequest
): Promise<TResponse | null> { ): Promise<HttpResponse<TResponse>> {
return await this.request<TResponse>('PUT', url, data) return await this.request<TResponse>('PUT', url, data)
} }
public async delete<TResponse>(url: string): Promise<TResponse | null> { private cleanPatchRequest(obj: any): any {
return await this.request<TResponse>('DELETE', url) if (Array.isArray(obj)) {
const cleanedArray = obj
.map(this.cleanPatchRequest)
.filter((item) => item !== null && item !== undefined)
return cleanedArray.length > 0 ? cleanedArray : null
} else if (typeof obj === 'object' && obj !== null) {
if (obj.op !== undefined && obj.op === PatchOperation.None) {
return null
}
const cleanedObject: any = {}
Object.keys(obj).forEach((key) => {
const cleanedValue = this.cleanPatchRequest(obj[key])
if (cleanedValue !== null) {
cleanedObject[key] = cleanedValue
}
})
return Object.keys(cleanedObject).length > 0 ? cleanedObject : null
}
return obj
}
public async patch<TRequest, TResponse>(
url: string,
data: TRequest
): Promise<HttpResponse<TResponse>> {
const cleanedData = this.cleanPatchRequest(data)
return await this.request<TResponse>('PATCH', url, cleanedData)
}
public async delete(url: string): Promise<HttpResponse<null>> {
return await this.request<null>('DELETE', url)
} }
public addRequestInterceptor(interceptor: RequestInterceptor): void { public addRequestInterceptor(interceptor: RequestInterceptor): void {
@ -233,10 +291,10 @@ export { httpService }
// Example usage of the httpService // Example usage of the httpService
// async function fetchData() { // async function fetchData() {
// const data = await httpService.get<any>('/api/data'); // const response = await httpService.get<any>('/api/data');
// if (data) { // if (response.isSuccess) {
// console.log('Data received:', data); // console.log('Data received:', response.data);
// } else { // } else {
// console.error('Failed to fetch data'); // console.error('Failed to fetch data, status code:', response.status);
// } // }
// } // }

View File

@ -182,6 +182,9 @@ public class AccountService : IAccountService {
} }
public async Task<IDomainResult> DeleteAccountAsync(Guid accountId) { public async Task<IDomainResult> DeleteAccountAsync(Guid accountId) {
// TODO: Revoke all certificates
// Remove from cache
return await _cacheService.DeleteFromCacheAsync(accountId); return await _cacheService.DeleteFromCacheAsync(accountId);
} }
#endregion #endregion

View File

@ -216,10 +216,11 @@ public class CertsFlowService : ICertsFlowService {
public (string?, IDomainResult) AcmeChallenge(string fileName) { public (string?, IDomainResult) AcmeChallenge(string fileName) {
DeleteExporedChallenges(); DeleteExporedChallenges();
var fileContent = File.ReadAllText(Path.Combine(_acmePath, fileName)); var challengePath = Path.Combine(_acmePath, fileName);
if (fileContent == null) if(!File.Exists(challengePath))
return IDomainResult.NotFound<string?>(); return IDomainResult.NotFound<string?>();
var fileContent = File.ReadAllText(Path.Combine(_acmePath, fileName));
return IDomainResult.Success(fileContent); return IDomainResult.Success(fileContent);
} }