Account: {account.accountId}
- {isEditMode && (
-
- )}
+ toggleEditMode(account.accountId)} className="bg-blue-500 text-white px-3 py-1 rounded">
+ {account.isEditMode ? "View Mode" : "Edit Mode"}
+
-
-
Contacts:
-
- {
- account.contacts.map(contact => (
- -
- {contact}
- {isEditMode && (
-
- )}
-
- ))
- }
-
- {isEditMode && (
-
-
handleContactChange(e.target.value)}
- className="border p-2 rounded mr-2 flex-grow h-10"
- placeholder="Add new contact"
- />
-
+ {account.isEditMode ? (
+
-
-
Hostnames:
-
- {isEditMode && (
-
-
handleHostnameChange(e.target.value)}
- className="border p-2 rounded mr-2 flex-grow h-10"
- placeholder="Add new hostname"
- />
-
+
+
Contacts:
+
+ {
+ account.contacts.map(contact => (
+ -
+ {contact}
+
+
+ ))
+ }
+
+
+
+
+
- )}
- {isEditMode && hostnameError &&
{hostnameError}
}
-
+
+
Hostnames:
+
+
+
+
+
+
+
+
+
+ Submit
+
+
+
+ ) : (
+ <>
+
+
Description:
+
+
+
Contacts:
+
+ {
+ account.contacts.map(contact => (
+ -
+ {contact}
+
+ ))
+ }
+
+
+
+
Hostnames:
+
+ {
+ account.hostnames.map(hostname => (
+ -
+ {hostname.hostname} - {hostname.expires.toDateString()} -
+
+ {hostname.isUpcomingExpire ? 'Upcoming' : 'Not Upcoming'}
+
+
+ ))
+ }
+
+
+ >
+ )}
))
}
- );
+ )
}
diff --git a/src/ClientApp/components/footer.tsx b/src/ClientApp/components/footer.tsx
index 7619904..3489b9a 100644
--- a/src/ClientApp/components/footer.tsx
+++ b/src/ClientApp/components/footer.tsx
@@ -1,13 +1,13 @@
-import React from 'react';
+import React from 'react'
const Footer = () => {
return (
)
}
export {
Footer
-};
+}
diff --git a/src/ClientApp/components/loader/index.tsx b/src/ClientApp/components/loader/index.tsx
index 9d08275..0961b2c 100644
--- a/src/ClientApp/components/loader/index.tsx
+++ b/src/ClientApp/components/loader/index.tsx
@@ -1,7 +1,33 @@
-import React from 'react'
-import './loader.css' // Add your loader styles here
+// components/Loader.tsx
+import React, { useEffect } from 'react'
+import { useSelector, useDispatch } from 'react-redux'
+import { RootState } from '@/redux/store'
+import { reset } from '@/redux/slices/loaderSlice'
+import './loader.css'
const Loader: React.FC = () => {
+ const dispatch = useDispatch()
+ const activeRequests = useSelector((state: RootState) => state.loader.activeRequests)
+
+ useEffect(() => {
+ let timeout: NodeJS.Timeout | null = null
+ if (activeRequests > 0) {
+ timeout = setTimeout(() => {
+ dispatch(reset())
+ }, 10000) // Adjust the timeout as necessary
+ }
+
+ return () => {
+ if (timeout) {
+ clearTimeout(timeout)
+ }
+ }
+ }, [activeRequests, dispatch])
+
+ if (activeRequests === 0) {
+ return null
+ }
+
return (
@@ -10,4 +36,6 @@ const Loader: React.FC = () => {
)
}
-export default Loader
+export {
+ Loader
+}
diff --git a/src/ClientApp/components/offcanvas.tsx b/src/ClientApp/components/offcanvas.tsx
index ac28720..8fc87e6 100644
--- a/src/ClientApp/components/offcanvas.tsx
+++ b/src/ClientApp/components/offcanvas.tsx
@@ -1,4 +1,4 @@
-import React, { FC } from 'react';
+import React, { FC } from 'react'
interface OffCanvasProps {
isOpen: boolean
diff --git a/src/ClientApp/components/sidemenu.tsx b/src/ClientApp/components/sidemenu.tsx
index 981a6af..bf76891 100644
--- a/src/ClientApp/components/sidemenu.tsx
+++ b/src/ClientApp/components/sidemenu.tsx
@@ -1,9 +1,9 @@
-import React, { FC, useEffect, useRef } from 'react';
-import { FaHome, FaUser, FaCog, FaBars } from 'react-icons/fa';
+import React, { FC, useEffect, useRef } from 'react'
+import { FaHome, FaUser, FaCog, FaBars } from 'react-icons/fa'
interface SideMenuProps {
- isCollapsed: boolean;
- toggleSidebar: () => void;
+ isCollapsed: boolean
+ toggleSidebar: () => void
}
const SideMenu: FC
= ({ isCollapsed, toggleSidebar }) => {
@@ -33,9 +33,9 @@ const SideMenu: FC = ({ isCollapsed, toggleSidebar }) => {
- );
-};
+ )
+}
export {
SideMenu
-};
+}
diff --git a/src/ClientApp/components/toast.tsx b/src/ClientApp/components/toast.tsx
new file mode 100644
index 0000000..0d6c659
--- /dev/null
+++ b/src/ClientApp/components/toast.tsx
@@ -0,0 +1,52 @@
+// components/Toast.tsx
+import React, { useEffect } from 'react'
+import { ToastContainer, toast } from 'react-toastify'
+import 'react-toastify/dist/ReactToastify.css'
+import { useDispatch, useSelector } from 'react-redux'
+import { RootState } from '@/redux/store'
+import { clearToast } from '@/redux/slices/toastSlice'
+
+const Toast = () => {
+ const dispatch = useDispatch()
+ const toastState = useSelector((state: RootState) => state.toast)
+
+ useEffect(() => {
+ if (toastState.message) {
+ switch (toastState.type) {
+ case 'success':
+ toast.success(toastState.message)
+ break
+ case 'error':
+ toast.error(toastState.message)
+ break
+ case 'info':
+ toast.info(toastState.message)
+ break
+ case 'warning':
+ toast.warn(toastState.message)
+ break
+ default:
+ toast(toastState.message)
+ break
+ }
+ dispatch(clearToast())
+ }
+ }, [toastState, dispatch])
+
+ return (
+
+ )
+}
+
+export { Toast }
diff --git a/src/ClientApp/components/topmenu.tsx b/src/ClientApp/components/topmenu.tsx
index 33a9860..46bf021 100644
--- a/src/ClientApp/components/topmenu.tsx
+++ b/src/ClientApp/components/topmenu.tsx
@@ -9,7 +9,7 @@ interface TopMenuProps {
}
const TopMenu: FC
= ({ onToggleOffCanvas }) => {
- const [isMenuOpen, setIsMenuOpen] = useState(false);
+ const [isMenuOpen, setIsMenuOpen] = useState(false)
const toggleMenu = () => {
setIsMenuOpen(!isMenuOpen)
diff --git a/src/ClientApp/controls/customButton.tsx b/src/ClientApp/controls/customButton.tsx
new file mode 100644
index 0000000..b284f01
--- /dev/null
+++ b/src/ClientApp/controls/customButton.tsx
@@ -0,0 +1,28 @@
+"use client"
+import React, { FC } from 'react'
+
+interface CustomButtonProps {
+ onClick?: () => void
+ className?: string
+ children: React.ReactNode
+ disabled?: boolean
+ type?: "button" | "submit" | "reset"
+}
+
+const CustomButton: FC = (props) => {
+
+ const { onClick, className = '', children, disabled = false, type = 'button' } = props
+
+ return (
+
+ )
+}
+
+export { CustomButton }
diff --git a/src/ClientApp/controls/customInput.tsx b/src/ClientApp/controls/customInput.tsx
new file mode 100644
index 0000000..16ad6be
--- /dev/null
+++ b/src/ClientApp/controls/customInput.tsx
@@ -0,0 +1,43 @@
+// components/CustomInput.tsx
+"use client"
+import React from 'react'
+
+interface CustomInputProps {
+ value: string
+ onChange: (value: string) => void
+ placeholder?: string
+ type: 'text' | 'password' | 'email' | 'number' | 'tel' | 'url'
+ error?: string
+ title?: string
+ inputClassName?: string
+ errorClassName?: string
+ className?: string
+}
+
+const CustomInput: React.FC = ({
+ value,
+ onChange,
+ placeholder = '',
+ type = 'text',
+ error,
+ title,
+ inputClassName = '',
+ errorClassName = '',
+ className = ''
+}) => {
+ return (
+
+ {title &&
}
+
onChange(e.target.value)}
+ placeholder={placeholder}
+ className={inputClassName}
+ />
+ {error &&
{error}
}
+
+ )
+}
+
+export { CustomInput }
diff --git a/src/ClientApp/controls/index.ts b/src/ClientApp/controls/index.ts
new file mode 100644
index 0000000..43b80c5
--- /dev/null
+++ b/src/ClientApp/controls/index.ts
@@ -0,0 +1,7 @@
+import { CustomButton } from "./customButton"
+import { CustomInput } from "./customInput"
+
+export {
+ CustomButton,
+ CustomInput
+}
\ No newline at end of file
diff --git a/src/ClientApp/hooks/useValidation.tsx b/src/ClientApp/hooks/useValidation.tsx
index 18e31a7..4238a89 100644
--- a/src/ClientApp/hooks/useValidation.tsx
+++ b/src/ClientApp/hooks/useValidation.tsx
@@ -1,37 +1,37 @@
-import { useState, useEffect } from "react";
+import { useState, useEffect } from "react"
// Helper functions for validation
const isValidEmail = (email: string) => {
- const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
- return emailRegex.test(email);
+ const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/
+ return emailRegex.test(email)
}
const isValidHostname = (hostname: string) => {
- const hostnameRegex = /^(?!:\/\/)([a-zA-Z0-9-_]{1,63}\.?)+[a-zA-Z]{2,6}$/;
- return hostnameRegex.test(hostname);
+ const hostnameRegex = /^(?!:\/\/)([a-zA-Z0-9-_]{1,63}\.?)+[a-zA-Z]{2,6}$/
+ return hostnameRegex.test(hostname)
}
// Custom hook for input validation
const useValidation = (initialValue: string, validateFn: (value: string) => boolean, errorMessage: string) => {
- const [value, setValue] = useState(initialValue);
- const [error, setError] = useState("");
+ const [value, setValue] = useState(initialValue)
+ const [error, setError] = useState("")
const handleChange = (newValue: string) => {
- setValue(newValue);
+ setValue(newValue)
if (newValue.trim() === "") {
- setError("This field cannot be empty.");
+ setError("This field cannot be empty.")
} else if (!validateFn(newValue.trim())) {
- setError(errorMessage);
+ setError(errorMessage)
} else {
- setError("");
+ setError("")
}
- };
+ }
useEffect(() => {
- handleChange(initialValue);
- }, [initialValue]);
+ handleChange(initialValue)
+ }, [initialValue])
- return { value, error, handleChange };
-};
+ return { value, error, handleChange }
+}
-export { useValidation, isValidEmail, isValidHostname };
+export { useValidation, isValidEmail, isValidHostname }
diff --git a/src/ClientApp/package-lock.json b/src/ClientApp/package-lock.json
index 647c6e9..7c26696 100644
--- a/src/ClientApp/package-lock.json
+++ b/src/ClientApp/package-lock.json
@@ -8,12 +8,14 @@
"name": "my-nextjs-app",
"version": "0.1.0",
"dependencies": {
+ "@heroicons/react": "^2.1.3",
"@reduxjs/toolkit": "^2.2.5",
"next": "14.2.3",
"react": "^18",
"react-dom": "^18",
"react-icons": "^5.2.1",
- "react-redux": "^9.1.2"
+ "react-redux": "^9.1.2",
+ "react-toastify": "^10.0.5"
},
"devDependencies": {
"@types/node": "^20",
@@ -106,6 +108,14 @@
"node": "^12.22.0 || ^14.17.0 || >=16.0.0"
}
},
+ "node_modules/@heroicons/react": {
+ "version": "2.1.3",
+ "resolved": "https://registry.npmjs.org/@heroicons/react/-/react-2.1.3.tgz",
+ "integrity": "sha512-fEcPfo4oN345SoqdlCDdSa4ivjaKbk0jTd+oubcgNxnNgAfzysfwWfQUr+51wigiWHQQRiZNd1Ao0M5Y3M2EGg==",
+ "peerDependencies": {
+ "react": ">= 16"
+ }
+ },
"node_modules/@humanwhocodes/config-array": {
"version": "0.11.14",
"resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.14.tgz",
@@ -1121,6 +1131,14 @@
"resolved": "https://registry.npmjs.org/client-only/-/client-only-0.0.1.tgz",
"integrity": "sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA=="
},
+ "node_modules/clsx": {
+ "version": "2.1.1",
+ "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz",
+ "integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==",
+ "engines": {
+ "node": ">=6"
+ }
+ },
"node_modules/color-convert": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
@@ -3798,6 +3816,18 @@
}
}
},
+ "node_modules/react-toastify": {
+ "version": "10.0.5",
+ "resolved": "https://registry.npmjs.org/react-toastify/-/react-toastify-10.0.5.tgz",
+ "integrity": "sha512-mNKt2jBXJg4O7pSdbNUfDdTsK9FIdikfsIE/yUCxbAEXl4HMyJaivrVFcn3Elvt5xvCQYhUZm+hqTIu1UXM3Pw==",
+ "dependencies": {
+ "clsx": "^2.1.0"
+ },
+ "peerDependencies": {
+ "react": ">=18",
+ "react-dom": ">=18"
+ }
+ },
"node_modules/read-cache": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/read-cache/-/read-cache-1.0.0.tgz",
diff --git a/src/ClientApp/package.json b/src/ClientApp/package.json
index eedc346..84e3ccc 100644
--- a/src/ClientApp/package.json
+++ b/src/ClientApp/package.json
@@ -9,12 +9,14 @@
"lint": "next lint"
},
"dependencies": {
+ "@heroicons/react": "^2.1.3",
"@reduxjs/toolkit": "^2.2.5",
"next": "14.2.3",
"react": "^18",
"react-dom": "^18",
"react-icons": "^5.2.1",
- "react-redux": "^9.1.2"
+ "react-redux": "^9.1.2",
+ "react-toastify": "^10.0.5"
},
"devDependencies": {
"@types/node": "^20",
diff --git a/src/ClientApp/redux/slices/loaderSlice.ts b/src/ClientApp/redux/slices/loaderSlice.ts
new file mode 100644
index 0000000..2bcaf3b
--- /dev/null
+++ b/src/ClientApp/redux/slices/loaderSlice.ts
@@ -0,0 +1,31 @@
+// loaderSlice.ts
+import { createSlice, PayloadAction } from '@reduxjs/toolkit'
+
+interface LoaderState {
+ activeRequests: number
+}
+
+const initialState: LoaderState = {
+ activeRequests: 0,
+}
+
+const loaderSlice = createSlice({
+ name: 'loader',
+ initialState,
+ reducers: {
+ increment: (state) => {
+ state.activeRequests += 1
+ },
+ decrement: (state) => {
+ if (state.activeRequests > 0) {
+ state.activeRequests -= 1
+ }
+ },
+ reset: (state) => {
+ state.activeRequests = 0
+ },
+ },
+})
+
+export const { increment, decrement, reset } = loaderSlice.actions
+export default loaderSlice.reducer
diff --git a/src/ClientApp/redux/slices/toastSlice.ts b/src/ClientApp/redux/slices/toastSlice.ts
new file mode 100644
index 0000000..2752704
--- /dev/null
+++ b/src/ClientApp/redux/slices/toastSlice.ts
@@ -0,0 +1,33 @@
+// store/toastSlice.ts
+import { createSlice, PayloadAction } from '@reduxjs/toolkit'
+
+interface ToastState {
+ message: string
+ type: 'success' | 'error' | 'info' | 'warning'
+}
+
+const initialState: ToastState = {
+ message: '',
+ type: 'info',
+}
+
+const toastSlice = createSlice({
+ name: 'toast',
+ initialState,
+ reducers: {
+ showToast: (state, action: PayloadAction<{
+ message: string
+ type: 'success' | 'error' | 'info' | 'warning'
+ }>) => {
+ state.message = action.payload.message
+ state.type = action.payload.type
+ },
+ clearToast: (state) => {
+ state.message = ''
+ state.type = 'info'
+ },
+ },
+})
+
+export const { showToast, clearToast } = toastSlice.actions
+export default toastSlice.reducer
diff --git a/src/ClientApp/redux/store.ts b/src/ClientApp/redux/store.ts
new file mode 100644
index 0000000..95687fd
--- /dev/null
+++ b/src/ClientApp/redux/store.ts
@@ -0,0 +1,13 @@
+import { configureStore } from '@reduxjs/toolkit'
+import loaderReducer from '@/redux/slices//loaderSlice'
+import toastReducer from '@/redux/slices/toastSlice'
+
+export const store = configureStore({
+ reducer: {
+ loader: loaderReducer,
+ toast: toastReducer,
+ },
+})
+
+export type RootState = ReturnType
+export type AppDispatch = typeof store.dispatch
\ No newline at end of file
diff --git a/src/ClientApp/services/HttpService.tsx b/src/ClientApp/services/HttpService.tsx
index 267bdb4..bc47ef9 100644
--- a/src/ClientApp/services/HttpService.tsx
+++ b/src/ClientApp/services/HttpService.tsx
@@ -1,9 +1,13 @@
+import { store } from '@/redux/store';
+import { increment, decrement } from '@/redux/slices/loaderSlice';
+import { showToast } from '@/redux/slices/toastSlice';
+
interface RequestInterceptor {
(req: XMLHttpRequest): void;
}
interface ResponseInterceptor {
- (response: T): T;
+ (response: T | null, error: ProblemDetails | null): T | void;
}
interface ProblemDetails {
@@ -12,100 +16,144 @@ interface ProblemDetails {
Status: number;
}
+interface HttpServiceCallbacks {
+ onIncrement?: () => void;
+ onDecrement?: () => void;
+ onShowToast?: (message: string, type: 'info' | 'error') => void;
+}
+
class HttpService {
- private requestInterceptors: Array = [];
+ private requestInterceptors: RequestInterceptor[] = [];
private responseInterceptors: Array> = [];
+ private callbacks: HttpServiceCallbacks;
- private request(method: string, url: string, data?: any): Promise {
- return new Promise((resolve, reject) => {
- const xhr = new XMLHttpRequest();
- xhr.open(method, url);
+ constructor(callbacks: HttpServiceCallbacks) {
+ this.callbacks = callbacks;
+ }
- // Apply request interceptors
- this.requestInterceptors.forEach(interceptor => {
- try {
- interceptor(xhr);
- } catch (error) {
- reject({
- Title: 'Request Interceptor Error',
- Detail: error instanceof Error ? error.message : 'Unknown error',
- Status: 0
- });
- return;
- }
- });
+ private invokeIncrement(): void {
+ this.callbacks.onIncrement?.();
+ }
- // Set Content-Type header for JSON data
- if (data && typeof data !== 'string') {
- xhr.setRequestHeader('Content-Type', 'application/json');
- }
+ private invokeDecrement(): void {
+ this.callbacks.onDecrement?.();
+ }
- xhr.onload = () => {
- if (xhr.status >= 200 && xhr.status < 300) {
- let response: TResponse;
- try {
- response = JSON.parse(xhr.response);
- } catch (error) {
- reject({
- Title: 'Response Parse Error',
- Detail: error instanceof Error ? error.message : 'Unknown error',
- Status: xhr.status
- });
- return;
- }
+ private invokeShowToast(message: string, type: 'info' | 'error'): void {
+ this.callbacks.onShowToast?.(message, type);
+ }
- // Apply response interceptors
- try {
- this.responseInterceptors.forEach(interceptor => {
- response = interceptor(response);
- });
- } catch (error) {
- reject({
- Title: 'Response Interceptor Error',
- Detail: error instanceof Error ? error.message : 'Unknown error',
- Status: xhr.status
- });
- return;
- }
+ private async request(method: string, url: string, data?: any): Promise {
+ const xhr = new XMLHttpRequest();
+ xhr.open(method, url);
- resolve(response);
- } else {
- const problemDetails: ProblemDetails = {
- Title: xhr.statusText,
- Detail: xhr.responseText,
- Status: xhr.status
- };
- reject(problemDetails);
- }
- };
+ this.handleRequestInterceptors(xhr);
- xhr.onerror = () => {
- const problemDetails: ProblemDetails = {
- Title: 'Network Error',
- Detail: null,
- Status: 0
- };
- reject(problemDetails);
- };
+ if (data && typeof data !== 'string') {
+ xhr.setRequestHeader('Content-Type', 'application/json');
+ }
+ this.invokeIncrement();
+
+ return new Promise((resolve) => {
+ xhr.onload = () => this.handleLoad(xhr, resolve);
+ xhr.onerror = () => this.handleNetworkError(resolve);
xhr.send(data ? JSON.stringify(data) : null);
});
}
- public get(url: string): Promise {
- return this.request('GET', url);
+ private handleRequestInterceptors(xhr: XMLHttpRequest): void {
+ this.requestInterceptors.forEach(interceptor => {
+ try {
+ interceptor(xhr);
+ } catch (error) {
+ const problemDetails = this.createProblemDetails('Request Interceptor Error', error, 0);
+ this.showProblemDetails(problemDetails);
+ }
+ });
}
- public post(url: string, data: TRequest): Promise {
- return this.request('POST', url, data);
+ private handleResponseInterceptors(response: TResponse | null, error: ProblemDetails | null): TResponse | null {
+ this.responseInterceptors.forEach(interceptor => {
+ try {
+ interceptor(response, error);
+ } catch (e) {
+ const problemDetails = this.createProblemDetails('Response Interceptor Error', e, 0);
+ this.showProblemDetails(problemDetails);
+ }
+ });
+ return response;
}
- public put(url: string, data: TRequest): Promise {
- return this.request('PUT', url, data);
+ private handleLoad(xhr: XMLHttpRequest, resolve: (value: TResponse | null) => void): void {
+ this.invokeDecrement();
+ if (xhr.status >= 200 && xhr.status < 300) {
+ this.handleSuccessfulResponse(xhr, resolve);
+ } else {
+ this.handleErrorResponse(xhr, resolve);
+ }
}
- public delete(url: string): Promise {
- return this.request('DELETE', url);
+ private handleSuccessfulResponse(xhr: XMLHttpRequest, resolve: (value: TResponse | null) => void): void {
+ try {
+ if (xhr.response) {
+ const response = JSON.parse(xhr.response);
+ resolve(this.handleResponseInterceptors(response, null) as TResponse);
+ } else {
+ resolve(null);
+ }
+ } catch (error) {
+ const problemDetails = this.createProblemDetails('Response Parse Error', error, xhr.status);
+ this.showProblemDetails(problemDetails);
+ resolve(null);
+ }
+ }
+
+ private handleErrorResponse(xhr: XMLHttpRequest, resolve: (value: TResponse | null) => void): void {
+ const problemDetails = this.createProblemDetails(xhr.statusText, xhr.responseText, xhr.status);
+ this.showProblemDetails(problemDetails);
+ resolve(this.handleResponseInterceptors(null, problemDetails));
+ }
+
+ private handleNetworkError(resolve: (value: TResponse | null) => void): void {
+ const problemDetails = this.createProblemDetails('Network Error', null, 0);
+ this.showProblemDetails(problemDetails);
+ resolve(this.handleResponseInterceptors(null, problemDetails));
+ }
+
+ private createProblemDetails(title: string, detail: any, status: number): ProblemDetails {
+ return {
+ Title: title,
+ Detail: detail instanceof Error ? detail.message : String(detail),
+ Status: status
+ };
+ }
+
+ private showProblemDetails(problemDetails: ProblemDetails): void {
+ if (problemDetails.Detail) {
+ const errorMessages = problemDetails.Detail.split(',');
+ errorMessages.forEach(message => {
+ this.invokeShowToast(message.trim(), 'error');
+ });
+ } else {
+ this.invokeShowToast('Unknown error', 'error');
+ }
+ }
+
+ public async get(url: string): Promise {
+ return await this.request('GET', url);
+ }
+
+ public async post(url: string, data: TRequest): Promise {
+ return await this.request('POST', url, data);
+ }
+
+ public async put(url: string, data: TRequest): Promise {
+ return await this.request('PUT', url, data);
+ }
+
+ public async delete(url: string): Promise {
+ return await this.request('DELETE', url);
}
public addRequestInterceptor(interceptor: RequestInterceptor): void {
@@ -117,19 +165,32 @@ class HttpService {
}
}
-
-const httpService = new HttpService();
-
-httpService.addRequestInterceptor(xhr => {
-
+// Instance of HttpService
+const httpService = new HttpService({
+ onIncrement: () => store.dispatch(increment()),
+ onDecrement: () => store.dispatch(decrement()),
+ onShowToast: (message: string, type: 'info' | 'error') => store.dispatch(showToast({ message, type })),
});
-httpService.addResponseInterceptor(response => {
+// Add loader state handling via interceptors
+httpService.addRequestInterceptor((xhr) => {
+ // Additional request logic can be added here
+});
+httpService.addResponseInterceptor((response, error) => {
+ // Additional response logic can be added here
return response;
});
+export { httpService };
+
+// Example usage of the httpService
+// async function fetchData() {
+// const data = await httpService.get('/api/data');
+// if (data) {
+// console.log('Data received:', data);
+// } else {
+// console.error('Failed to fetch data');
+// }
+// }
-export {
- httpService
-}
diff --git a/src/LetsEncrypt/Entities/LetsEncrypt/RegistrationCache.cs b/src/LetsEncrypt/Entities/LetsEncrypt/RegistrationCache.cs
index 5c11d7e..25d801d 100644
--- a/src/LetsEncrypt/Entities/LetsEncrypt/RegistrationCache.cs
+++ b/src/LetsEncrypt/Entities/LetsEncrypt/RegistrationCache.cs
@@ -13,11 +13,14 @@ public class CertificateCache {
public class RegistrationCache {
+ #region Custom Properties
///
/// Field used to identify cache by account id
///
public Guid AccountId { get; set; }
+ public string? Description { get; set; }
public string[]? Contacts { get; set; }
+ #endregion
public Dictionary? CachedCerts { get; set; }
diff --git a/src/LetsEncryptServer/Controllers/CacheController.cs b/src/LetsEncryptServer/Controllers/CacheController.cs
index ce91fe2..fe9d764 100644
--- a/src/LetsEncryptServer/Controllers/CacheController.cs
+++ b/src/LetsEncryptServer/Controllers/CacheController.cs
@@ -1,6 +1,4 @@
-
-
-using Microsoft.AspNetCore.Mvc;
+using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Options;
using DomainResults.Mvc;
@@ -12,44 +10,58 @@ namespace MaksIT.LetsEncryptServer.Controllers;
[ApiController]
[Route("api/[controller]")]
-public class CacheController {
-
+public class CacheController : ControllerBase {
private readonly Configuration _appSettings;
- private readonly ICacheService _cacheService;
+ private readonly ICacheRestService _cacheService;
public CacheController(
- IOptions appSettings,
- ICacheService cacheService
-
+ IOptions appSettings,
+ ICacheService cacheService
) {
_appSettings = appSettings.Value;
- _cacheService = cacheService;
+ _cacheService = (ICacheRestService)cacheService;
}
-
- [HttpGet("[action]")]
+ [HttpGet("accounts")]
public async Task GetAccounts() {
var result = await _cacheService.GetAccountsAsync();
return result.ToActionResult();
}
- [HttpGet("[action]/{accountId}")]
+ #region Contacts
+
+ [HttpGet("{accountId}/contacts")]
public async Task GetContacts(Guid accountId) {
var result = await _cacheService.GetContactsAsync(accountId);
return result.ToActionResult();
}
-
- [HttpPost("[action]/{accountId}")]
- public async Task SetContacts(Guid accountId, [FromBody] SetContactsRequest requestData) {
- var result = await _cacheService.SetContactsAsync(accountId, requestData);
+ [HttpPut("{accountId}/contacts")]
+ public async Task PutContacts(Guid accountId, [FromBody] PutContactsRequest requestData) {
+ var result = await _cacheService.PutContactsAsync(accountId, requestData);
return result.ToActionResult();
}
- [HttpGet("[action]/{accountId}")]
+ [HttpPatch("{accountId}/contacts")]
+ public async Task PatchContacts(Guid accountId, [FromBody] PatchContactRequest requestData) {
+ var result = await _cacheService.PatchContactsAsync(accountId, requestData);
+ return result.ToActionResult();
+ }
+
+ [HttpDelete("{accountId}/contacts/{index}")]
+ public async Task DeleteContact(Guid accountId, int index) {
+ var result = await _cacheService.DeleteContactAsync(accountId, index);
+ return result.ToActionResult();
+ }
+ #endregion
+
+ #region Hostnames
+
+ [HttpGet("{accountId}/hostnames")]
public async Task GetHostnames(Guid accountId) {
var result = await _cacheService.GetHostnames(accountId);
return result.ToActionResult();
}
-}
+ #endregion
+}
diff --git a/src/LetsEncryptServer/Controllers/CertsFlowController.cs b/src/LetsEncryptServer/Controllers/CertsFlowController.cs
index e7f9629..0d9faae 100644
--- a/src/LetsEncryptServer/Controllers/CertsFlowController.cs
+++ b/src/LetsEncryptServer/Controllers/CertsFlowController.cs
@@ -1,126 +1,115 @@
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Options;
-
using DomainResults.Mvc;
-
using MaksIT.LetsEncryptServer.Services;
using Models.LetsEncryptServer.CertsFlow.Requests;
+namespace MaksIT.LetsEncryptServer.Controllers {
+ [ApiController]
+ [Route("api/[controller]")]
+ public class CertsFlowController : ControllerBase {
+ private readonly Configuration _appSettings;
+ private readonly ICertsFlowService _certsFlowService;
-namespace MaksIT.LetsEncryptServer.Controllers;
+ public CertsFlowController(
+ IOptions appSettings,
+ ICertsFlowService certsFlowService
+ ) {
+ _appSettings = appSettings.Value;
+ _certsFlowService = certsFlowService;
+ }
-[ApiController]
-[Route("api/[controller]")]
-public class CertsFlowController : ControllerBase {
+ ///
+ /// Initialize certificate flow session
+ ///
+ /// sessionId
+ [HttpPost("configure-client")]
+ public async Task ConfigureClient() {
+ var result = await _certsFlowService.ConfigureClientAsync();
+ return result.ToActionResult();
+ }
- private readonly IOptions _appSettings;
- private readonly ICertsFlowService _certsFlowService;
+ ///
+ /// Retrieves terms of service
+ ///
+ /// Session ID
+ /// Terms of service
+ [HttpGet("terms-of-service/{sessionId}")]
+ public IActionResult TermsOfService(Guid sessionId) {
+ var result = _certsFlowService.GetTermsOfService(sessionId);
+ return result.ToActionResult();
+ }
- public CertsFlowController(
- IOptions appSettings,
- ICertsFlowService certsFlowService
- ) {
- _appSettings = appSettings;
- _certsFlowService = certsFlowService;
- }
+ ///
+ /// When a new certificate session is created, create or retrieve cache data by accountId
+ ///
+ /// Session ID
+ /// Account ID
+ /// Request data
+ /// Account ID
+ [HttpPost("{sessionId}/init/{accountId?}")]
+ public async Task Init(Guid sessionId, Guid? accountId, [FromBody] InitRequest requestData) {
+ var result = await _certsFlowService.InitAsync(sessionId, accountId, requestData);
+ return result.ToActionResult();
+ }
- ///
- /// Initialize certificate flow session
- ///
- /// sessionId
- [HttpPost("[action]")]
- public async Task ConfigureClient() {
- var result = await _certsFlowService.ConfigureClientAsync();
- return result.ToActionResult();
- }
+ ///
+ /// After account initialization, create a new order request
+ ///
+ /// Session ID
+ /// Request data
+ /// New order response
+ [HttpPost("{sessionId}/order")]
+ public async Task NewOrder(Guid sessionId, [FromBody] NewOrderRequest requestData) {
+ var result = await _certsFlowService.NewOrderAsync(sessionId, requestData);
+ return result.ToActionResult();
+ }
- [HttpGet("[action]/{sessionId}")]
- public IActionResult TermsOfService(Guid sessionId) {
- var result = _certsFlowService.GetTermsOfService(sessionId);
- return result.ToActionResult();
- }
+ ///
+ /// Complete challenges for the new order
+ ///
+ /// Session ID
+ /// Challenges completion response
+ [HttpPost("{sessionId}/complete-challenges")]
+ public async Task CompleteChallenges(Guid sessionId) {
+ var result = await _certsFlowService.CompleteChallengesAsync(sessionId);
+ return result.ToActionResult();
+ }
- ///
- /// When new certificate session is created, create or retrieve cache data by accountId
- ///
- ///
- ///
- ///
- /// accountId
- [HttpPost("[action]/{sessionId}/{accountId?}")]
- public async Task Init(Guid sessionId, Guid? accountId, [FromBody] InitRequest requestData) {
- var resurt = await _certsFlowService.InitAsync(sessionId, accountId, requestData);
- return resurt.ToActionResult();
- }
+ ///
+ /// Get order status before certificate retrieval
+ ///
+ /// Session ID
+ /// Request data
+ /// Order status
+ [HttpGet("{sessionId}/order-status")]
+ public async Task GetOrder(Guid sessionId, [FromBody] GetOrderRequest requestData) {
+ var result = await _certsFlowService.GetOrderAsync(sessionId, requestData);
+ return result.ToActionResult();
+ }
- ///
- /// After account initialization create new order request
- ///
- ///
- ///
- ///
- [HttpPost("[action]/{sessionId}")]
- public async Task NewOrder(Guid sessionId, [FromBody] NewOrderRequest requestData) {
- var result = await _certsFlowService.NewOrderAsync(sessionId, requestData);
- return result.ToActionResult();
- }
+ ///
+ /// Download certificates to local cache
+ ///
+ /// Session ID
+ /// Request data
+ /// Certificates download response
+ [HttpPost("{sessionId}/certificates/download")]
+ public async Task GetCertificates(Guid sessionId, [FromBody] GetCertificatesRequest requestData) {
+ var result = await _certsFlowService.GetCertificatesAsync(sessionId, requestData);
+ return result.ToActionResult();
+ }
- ///
- /// After new order request complete challenges
- ///
- ///
- ///
- [HttpPost("[action]/{sessionId}")]
- public async Task CompleteChallenges(Guid sessionId) {
- var result = await _certsFlowService.CompleteChallengesAsync(sessionId);
- return result.ToActionResult();
- }
-
- ///
- /// Get order status before certs retrieval
- ///
- ///
- ///
- ///
- [HttpPost("[action]/{sessionId}")]
- public async Task GetOrder(Guid sessionId, [FromBody] GetOrderRequest requestData) {
- var result = await _certsFlowService.GetOrderAsync(sessionId, requestData);
- return result.ToActionResult();
- }
-
- ///
- /// Download certs to local cache
- ///
- ///
- ///
- ///
- [HttpPost("[action]/{sessionId}")]
- public async Task GetCertificates(Guid sessionId, [FromBody] GetCertificatesRequest requestData) {
- var result = await _certsFlowService.GetCertificatesAsync(sessionId, requestData);
- return result.ToActionResult();
- }
-
- ///
- /// Apply certs from local cache to remote server
- ///
- ///
- ///
- ///
- [HttpPost("[action]/{sessionId}")]
- public async Task ApplyCertificates(Guid sessionId, [FromBody] GetCertificatesRequest requestData) {
- var result = await _certsFlowService.ApplyCertificatesAsync(sessionId, requestData);
- return result.ToActionResult();
- }
-
- ///
- /// Returns a list of hosts with upcoming SSL expiry
- ///
- ///
- ///
- [HttpGet("[action]/{sessionId}")]
- public IActionResult HostsWithUpcomingSslExpiry(Guid sessionId) {
- var result = _certsFlowService.HostsWithUpcomingSslExpiry(sessionId);
- return result.ToActionResult();
+ ///
+ /// Apply certificates from local cache to remote server
+ ///
+ /// Session ID
+ /// Request data
+ /// Certificates application response
+ [HttpPost("{sessionId}/certificates/apply")]
+ public async Task ApplyCertificates(Guid sessionId, [FromBody] GetCertificatesRequest requestData) {
+ var result = await _certsFlowService.ApplyCertificatesAsync(sessionId, requestData);
+ return result.ToActionResult();
+ }
}
}
-
diff --git a/src/LetsEncryptServer/Services/CacheService.cs b/src/LetsEncryptServer/Services/CacheService.cs
index b307683..e48bc13 100644
--- a/src/LetsEncryptServer/Services/CacheService.cs
+++ b/src/LetsEncryptServer/Services/CacheService.cs
@@ -5,7 +5,9 @@ using System.Text.Json;
using DomainResults.Common;
using MaksIT.Core.Extensions;
using MaksIT.LetsEncrypt.Entities;
+using MaksIT.Models;
using MaksIT.Models.LetsEncryptServer.Cache.Requests;
+using MaksIT.Models.LetsEncryptServer.Cache.Responses;
using Models.LetsEncryptServer.Cache.Responses;
namespace MaksIT.LetsEncryptServer.Services;
@@ -14,14 +16,25 @@ public interface ICacheService {
Task<(RegistrationCache?, IDomainResult)> LoadFromCacheAsync(Guid accountId);
Task SaveToCacheAsync(Guid accountId, RegistrationCache cache);
Task DeleteFromCacheAsync(Guid accountId);
- Task<(GetAccountsResponse?, IDomainResult)> GetAccountsAsync();
- Task<(GetContactsResponse?, IDomainResult)> GetContactsAsync(Guid accountId);
- Task SetContactsAsync(Guid accountId, SetContactsRequest requestData);
-
- Task<(GetHostnamesResponse?, IDomainResult)> GetHostnames(Guid accountId);
}
-public class CacheService : ICacheService, IDisposable {
+public interface ICacheRestService {
+ Task<(GetAccountsResponse?, IDomainResult)> GetAccountsAsync();
+ Task<(GetAccountResponse?, IDomainResult)> GetAccountAsync(Guid accountId);
+
+ #region Contacts
+ Task<(GetContactsResponse?, IDomainResult)> GetContactsAsync(Guid accountId);
+ Task<(GetAccountResponse?, IDomainResult)> PutContactsAsync(Guid accountId, PutContactsRequest requestData);
+ Task<(GetAccountResponse?, IDomainResult)> PatchContactsAsync(Guid accountId, PatchContactRequest requestData);
+ Task DeleteContactAsync(Guid accountId, int index);
+ #endregion
+
+ #region Hostnames
+ Task<(GetHostnamesResponse?, IDomainResult)> GetHostnames(Guid accountId);
+ #endregion
+}
+
+public class CacheService : ICacheService, ICacheRestService, IDisposable {
private readonly ILogger _logger;
private readonly string _cacheDirectory;
@@ -126,6 +139,9 @@ public class CacheService : ICacheService, IDisposable {
}
}
+
+
+ #region RestService
public async Task<(GetAccountsResponse?, IDomainResult)> GetAccountsAsync() {
await _cacheLock.WaitAsync();
@@ -133,13 +149,22 @@ public class CacheService : ICacheService, IDisposable {
var cacheFiles = Directory.GetFiles(_cacheDirectory);
if (cacheFiles == null)
return IDomainResult.Success(new GetAccountsResponse {
- AccountIds = Array.Empty()
+ Accounts = Array.Empty()
});
- var accountIds = cacheFiles.Select(x => Path.GetFileNameWithoutExtension(x).ToGuid()).ToArray();
+ var accountIds = cacheFiles.Select(x => Path.GetFileNameWithoutExtension(x).ToGuid());
+ var accounts = new List();
+ foreach (var accountId in accountIds) {
+ var (account, getAccountResult) = await GetAccountAsync(accountId);
+ if(!getAccountResult.IsSuccess || account == null)
+ return (null, getAccountResult);
+
+ accounts.Add(account);
+ }
+
return IDomainResult.Success(new GetAccountsResponse {
- AccountIds = accountIds
+ Accounts = accounts.ToArray()
});
}
catch (Exception ex) {
@@ -153,6 +178,40 @@ public class CacheService : ICacheService, IDisposable {
}
}
+ public async Task<(GetAccountResponse?, IDomainResult)> GetAccountAsync(Guid accountId) {
+
+ await _cacheLock.WaitAsync();
+
+ try {
+ var (registrationCache, gerRegistrationCacheResult) = await LoadFromCacheAsync(accountId);
+ if (!gerRegistrationCacheResult.IsSuccess || registrationCache == null)
+ return (null, gerRegistrationCacheResult);
+
+ return IDomainResult.Success(new GetAccountResponse {
+ AccountId = accountId,
+ Description = registrationCache.Description,
+ Contacts = registrationCache.Contacts,
+ Hostnames = registrationCache.GetHostsWithUpcomingSslExpiry()
+ });
+ }
+ catch (Exception ex) {
+ var message = "Error listing cache files";
+ _logger.LogError(ex, message);
+
+ return IDomainResult.Failed(message);
+ }
+ finally {
+ _cacheLock.Release();
+ }
+ }
+
+
+ #region Contacts
+ ///
+ /// Retrieves the contacts list for the account.
+ ///
+ /// The ID of the account.
+ /// The contacts list and domain result.
public async Task<(GetContactsResponse?, IDomainResult)> GetContactsAsync(Guid accountId) {
var (cache, loadResult) = await LoadFromCacheAsync(accountId);
if (!loadResult.IsSuccess || cache == null)
@@ -163,16 +222,132 @@ public class CacheService : ICacheService, IDisposable {
});
}
+ ///
+ /// Adds new contacts to the account. This method initializes the contacts list if it is null.
+ ///
+ /// The ID of the account.
+ /// The request containing the contacts to add.
+ /// The updated account response and domain result.
+ public async Task<(GetAccountResponse?, IDomainResult)> PostContactAsync(Guid accountId, PostContactsRequest requestData) {
+ var (cache, loadResult) = await LoadFromCacheAsync(accountId);
+ if (!loadResult.IsSuccess || cache == null)
+ return (null, loadResult);
- public async Task SetContactsAsync(Guid accountId, SetContactsRequest requestData) {
+ var contacts = cache.Contacts?.ToList() ?? new List();
+
+ if (requestData.Contacts != null) {
+ contacts.AddRange(requestData.Contacts);
+ }
+
+ cache.Contacts = contacts.ToArray();
+ var saveResult = await SaveToCacheAsync(accountId, cache);
+ if (!saveResult.IsSuccess)
+ return (null, saveResult);
+
+ return (new GetAccountResponse {
+ AccountId = accountId,
+ Description = cache.Description,
+ Contacts = cache.Contacts,
+ Hostnames = cache.GetHostsWithUpcomingSslExpiry()
+ }, IDomainResult.Success());
+ }
+
+ ///
+ /// Replaces the entire contacts list for the account.
+ ///
+ /// The ID of the account.
+ /// The request containing the new contacts list.
+ /// The updated account response and domain result.
+ public async Task<(GetAccountResponse?, IDomainResult)> PutContactsAsync(Guid accountId, PutContactsRequest requestData) {
+ var (cache, loadResult) = await LoadFromCacheAsync(accountId);
+ if (!loadResult.IsSuccess || cache == null)
+ return (null, loadResult);
+
+ cache.Contacts = requestData.Contacts;
+ var saveResult = await SaveToCacheAsync(accountId, cache);
+ if (!saveResult.IsSuccess)
+ return (null, saveResult);
+
+ return (new GetAccountResponse {
+ AccountId = accountId,
+ Description = cache.Description,
+ Contacts = cache.Contacts,
+ Hostnames = cache.GetHostsWithUpcomingSslExpiry()
+ }, IDomainResult.Success());
+ }
+
+ ///
+ /// Partially updates the contacts list for the account. Supports add, replace, and remove operations.
+ ///
+ /// The ID of the account.
+ /// The request containing the patch operations for contacts.
+ /// The updated account response and domain result.
+ public async Task<(GetAccountResponse?, IDomainResult)> PatchContactsAsync(Guid accountId, PatchContactRequest requestData) {
+ var (cache, loadResult) = await LoadFromCacheAsync(accountId);
+ if (!loadResult.IsSuccess || cache == null)
+ return (null, loadResult);
+
+ var contacts = cache.Contacts?.ToList() ?? new List();
+
+ foreach (var contact in requestData.Contacts) {
+ switch (contact.Op) {
+ case PatchOperation.Add:
+ if (contact.Value != null)
+ contacts.Add(contact.Value);
+ break;
+ case PatchOperation.Replace:
+ if (contact.Index.HasValue && contact.Index.Value >= 0 && contact.Index.Value < contacts.Count && contact.Value != null)
+ contacts[contact.Index.Value] = contact.Value;
+ break;
+ case PatchOperation.Remove:
+ if (contact.Index.HasValue && contact.Index.Value >= 0 && contact.Index.Value < contacts.Count)
+ contacts.RemoveAt(contact.Index.Value);
+ break;
+ default:
+ return (null, IDomainResult.Failed("Invalid patch operation."));
+ }
+ }
+
+ cache.Contacts = contacts.ToArray();
+ var saveResult = await SaveToCacheAsync(accountId, cache);
+ if (!saveResult.IsSuccess)
+ return (null, saveResult);
+
+ return (new GetAccountResponse {
+ AccountId = accountId,
+ Description = cache.Description,
+ Contacts = cache.Contacts,
+ Hostnames = cache.GetHostsWithUpcomingSslExpiry()
+ }, IDomainResult.Success());
+ }
+
+ ///
+ /// Deletes a contact from the account by index.
+ ///
+ /// The ID of the account.
+ /// The index of the contact to remove.
+ /// The domain result indicating success or failure.
+ public async Task DeleteContactAsync(Guid accountId, int index) {
var (cache, loadResult) = await LoadFromCacheAsync(accountId);
if (!loadResult.IsSuccess || cache == null)
return loadResult;
- cache.Contacts = requestData.Contacts;
- return await SaveToCacheAsync(accountId, cache);
+ var contacts = cache.Contacts?.ToList() ?? new List();
+
+ if (index >= 0 && index < contacts.Count)
+ contacts.RemoveAt(index);
+
+ cache.Contacts = contacts.ToArray();
+ var saveResult = await SaveToCacheAsync(accountId, cache);
+ if (!saveResult.IsSuccess)
+ return saveResult;
+
+ return IDomainResult.Success();
}
+ #endregion
+
+ #region Hostnames
public async Task<(GetHostnamesResponse?, IDomainResult)> GetHostnames(Guid accountId) {
var (cache, loadResult) = await LoadFromCacheAsync(accountId);
if (!loadResult.IsSuccess || cache?.CachedCerts == null)
@@ -199,6 +374,9 @@ public class CacheService : ICacheService, IDisposable {
return IDomainResult.Success(response);
}
+ #endregion
+
+ #endregion
public void Dispose() {
diff --git a/src/LetsEncryptServer/Services/CertsFlowService.cs b/src/LetsEncryptServer/Services/CertsFlowService.cs
index 6f53d23..e9a0007 100644
--- a/src/LetsEncryptServer/Services/CertsFlowService.cs
+++ b/src/LetsEncryptServer/Services/CertsFlowService.cs
@@ -24,8 +24,7 @@ public interface ICertsFlowService : ICertsFlowServiceBase {
Task GetOrderAsync(Guid sessionId, GetOrderRequest requestData);
Task GetCertificatesAsync(Guid sessionId, GetCertificatesRequest requestData);
Task<(Dictionary?, IDomainResult)> ApplyCertificatesAsync(Guid sessionId, GetCertificatesRequest requestData);
- (string[]?, IDomainResult) HostsWithUpcomingSslExpiry(Guid sessionId);
-}
+ }
public class CertsFlowService : ICertsFlowService {
@@ -166,17 +165,6 @@ public class CertsFlowService : ICertsFlowService {
}
- public (string[]?, IDomainResult) HostsWithUpcomingSslExpiry(Guid sessionId) {
-
- var (hostnames, hostnamesResult) = _letsEncryptService.HostsWithUpcomingSslExpiry(sessionId);
- if(!hostnamesResult.IsSuccess)
- return (null, hostnamesResult);
-
- return IDomainResult.Success(hostnames);
- }
-
-
-
diff --git a/src/Models/LetsEncryptServer/Cache/Requests/PatchContactRequest.cs b/src/Models/LetsEncryptServer/Cache/Requests/PatchContactRequest.cs
new file mode 100644
index 0000000..c1bd057
--- /dev/null
+++ b/src/Models/LetsEncryptServer/Cache/Requests/PatchContactRequest.cs
@@ -0,0 +1,12 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+
+namespace MaksIT.Models.LetsEncryptServer.Cache.Requests {
+
+ public class PatchContactRequest {
+ public List> Contacts { get; set; }
+ }
+}
diff --git a/src/Models/LetsEncryptServer/Cache/Requests/PostContactsRequest.cs b/src/Models/LetsEncryptServer/Cache/Requests/PostContactsRequest.cs
new file mode 100644
index 0000000..2e64e95
--- /dev/null
+++ b/src/Models/LetsEncryptServer/Cache/Requests/PostContactsRequest.cs
@@ -0,0 +1,19 @@
+using System;
+using System.Collections.Generic;
+using System.ComponentModel.DataAnnotations;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+
+namespace MaksIT.Models.LetsEncryptServer.Cache.Requests {
+ public class PostContactsRequest : IValidatableObject {
+
+ public required string[] Contacts { get; set; }
+
+ public IEnumerable Validate(ValidationContext validationContext) {
+
+ if (Contacts == null || Contacts.Length == 0)
+ yield return new ValidationResult("Contacts is required", new[] { nameof(Contacts) });
+ }
+ }
+ }
\ No newline at end of file
diff --git a/src/Models/LetsEncryptServer/Cache/Requests/SetContactsRequest.cs b/src/Models/LetsEncryptServer/Cache/Requests/PutContactsRequest.cs
similarity index 87%
rename from src/Models/LetsEncryptServer/Cache/Requests/SetContactsRequest.cs
rename to src/Models/LetsEncryptServer/Cache/Requests/PutContactsRequest.cs
index 00dc7f5..94ce291 100644
--- a/src/Models/LetsEncryptServer/Cache/Requests/SetContactsRequest.cs
+++ b/src/Models/LetsEncryptServer/Cache/Requests/PutContactsRequest.cs
@@ -2,7 +2,7 @@
namespace MaksIT.Models.LetsEncryptServer.Cache.Requests {
- public class SetContactsRequest : IValidatableObject {
+ public class PutContactsRequest : IValidatableObject {
public required string[] Contacts { get; set; }
diff --git a/src/Models/LetsEncryptServer/Cache/Responses/GetAccountResponse.cs b/src/Models/LetsEncryptServer/Cache/Responses/GetAccountResponse.cs
new file mode 100644
index 0000000..6e75356
--- /dev/null
+++ b/src/Models/LetsEncryptServer/Cache/Responses/GetAccountResponse.cs
@@ -0,0 +1,17 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+
+namespace Models.LetsEncryptServer.Cache.Responses {
+ public class GetAccountResponse {
+ public Guid AccountId { get; set; }
+
+ public string? Description { get; set; }
+
+ public string []? Contacts { get; set; }
+
+ public string[]? Hostnames { get; set; }
+ }
+}
diff --git a/src/Models/LetsEncryptServer/Cache/Responses/GetAccountsResponse.cs b/src/Models/LetsEncryptServer/Cache/Responses/GetAccountsResponse.cs
index db76fae..a56ba94 100644
--- a/src/Models/LetsEncryptServer/Cache/Responses/GetAccountsResponse.cs
+++ b/src/Models/LetsEncryptServer/Cache/Responses/GetAccountsResponse.cs
@@ -1,11 +1,12 @@
-using System;
+using Models.LetsEncryptServer.Cache.Responses;
+using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
-namespace Models.LetsEncryptServer.Cache.Responses {
+namespace MaksIT.Models.LetsEncryptServer.Cache.Responses {
public class GetAccountsResponse {
- public Guid[] AccountIds { get; set; }
+ public GetAccountResponse[] Accounts { get; set; }
}
}
diff --git a/src/Models/PatchAction.cs b/src/Models/PatchAction.cs
new file mode 100644
index 0000000..d4f6cfc
--- /dev/null
+++ b/src/Models/PatchAction.cs
@@ -0,0 +1,13 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+
+namespace MaksIT.Models {
+ public class PatchAction {
+ public PatchOperation Op { get; set; } // Enum for operation type
+ public int? Index { get; set; } // Index for the operation (for arrays/lists)
+ public T? Value { get; set; } // Value for the operation
+ }
+}
diff --git a/src/Models/PatchOperation.cs b/src/Models/PatchOperation.cs
new file mode 100644
index 0000000..f899a54
--- /dev/null
+++ b/src/Models/PatchOperation.cs
@@ -0,0 +1,13 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+
+namespace MaksIT.Models {
+ public enum PatchOperation {
+ Add,
+ Remove,
+ Replace
+ }
+}