From e321a3237f39bf07e620b47fe28cbb3f34ade2f1 Mon Sep 17 00:00:00 2001 From: Maksym Sadovnychyy Date: Wed, 19 Jun 2024 18:12:27 +0200 Subject: [PATCH] (feature): registration page init --- src/ClientApp/ApiRoutes.tsx | 32 ++- src/ClientApp/app/layout.tsx | 25 ++- src/ClientApp/app/page.tsx | 39 ++-- src/ClientApp/app/register/page.tsx | 209 ++++++++++++++++++ src/ClientApp/components/footer.tsx | 13 +- src/ClientApp/components/loader/index.tsx | 3 +- src/ClientApp/components/loader/loader.css | 75 ++++--- src/ClientApp/components/sidemenu.tsx | 43 ++-- src/ClientApp/controls/customInput.tsx | 9 +- src/ClientApp/controls/index.ts | 4 +- src/ClientApp/hooks/useValidation.tsx | 24 +- .../cache/GetAccountsResponse.ts | 3 - .../cache/GetHostnamesResponse.ts | 9 - .../cache/responses/GetAccountResponse.ts | 9 + .../{ => responses}/GetContactsResponse.ts | 0 .../cache/responses/GetHostnamesResponse.ts | 5 + .../cache/responses/HostnameResponse.ts | 5 + src/Core/LockManager.cs | 78 +++++-- .../BackgroundServices/AutoRenewal.cs | 21 +- .../Controllers/CacheController.cs | 5 +- .../Services/CacheService.cs | 63 ++++-- .../Services/CertsFlowService.cs | 2 +- 22 files changed, 508 insertions(+), 168 deletions(-) create mode 100644 src/ClientApp/app/register/page.tsx delete mode 100644 src/ClientApp/models/letsEncryptServer/cache/GetAccountsResponse.ts delete mode 100644 src/ClientApp/models/letsEncryptServer/cache/GetHostnamesResponse.ts create mode 100644 src/ClientApp/models/letsEncryptServer/cache/responses/GetAccountResponse.ts rename src/ClientApp/models/letsEncryptServer/cache/{ => responses}/GetContactsResponse.ts (100%) create mode 100644 src/ClientApp/models/letsEncryptServer/cache/responses/GetHostnamesResponse.ts create mode 100644 src/ClientApp/models/letsEncryptServer/cache/responses/HostnameResponse.ts diff --git a/src/ClientApp/ApiRoutes.tsx b/src/ClientApp/ApiRoutes.tsx index 0d08f71..746524f 100644 --- a/src/ClientApp/ApiRoutes.tsx +++ b/src/ClientApp/ApiRoutes.tsx @@ -1,25 +1,21 @@ enum ApiRoutes { - CACHE_GET_ACCOUNTS = `api/Cache/GetAccounts`, - - CACHE_GET_CONTACTS = `api/Cache/GetContacts/{accountId}`, - CACHE_ADD_CONTACT = `api/Cache/AddContact/{accountId}`, - CACHE_DELETE_CONTACT = `api/Cache/DeleteContact/{accountId}?contact={contact}`, + CACHE_ACCOUNTS = 'api/cache/accounts', + CACHE_ACCOUNT = 'api/cache/account/{accountId}', + CACHE_ACCOUNT_CONTACTS = 'api/cache/account/{accountId}/contacts', + CACHE_ACCOUNT_CONTACT = 'api/cache/account/{accountId}/contacts/{index}', + CACHE_ACCOUNT_HOSTNAMES = 'api/cache/account/{accountId}/hostnames', - CACHE_GET_HOSTNAMES = `api/Cache/GetHostnames/{accountId}`, - // TODO: here is different flow via CertsFlowController, cache update is the result of add order and invalidate cert - // CACHE_ADD_HOSTNAME = `api/Cache/AddHostname/{accountId}`, - // CACHE_DELETE_HOSTNAME = `api/Cache/DeleteHostname/{accountId}?hostname={hostname}`, - CERTS_FLOW_CONFIGURE_CLIENT = `api/CertsFlow/ConfigureClient`, - CERTS_FLOW_TERMS_OF_SERVICE = `api/CertsFlow/TermsOfService/{sessionId}`, - CERTS_FLOW_INIT = `api/CertsFlow/Init/{sessionId}/{accountId}`, - CERTS_FLOW_NEW_ORDER = `api/CertsFlow/NewOrder/{sessionId}`, - CERTS_FLOW_GET_ORDER = `api/CertsFlow/GetOrder/{sessionId}`, - CERTS_FLOW_GET_CERTIFICATES = `api/CertsFlow/GetCertificates/{sessionId}`, - CERTS_FLOW_APPLY_CERTIFICATES = `api/CertsFlow/ApplyCertificates/{sessionId}`, - CERTS_FLOW_HOSRS_WITH_UPCOMING_SSL_EXPIRY = `api/CertsFlow/HostsWithUpcomingSslExpiry/{sessionId}` + // CERTS_FLOW_CONFIGURE_CLIENT = `api/CertsFlow/ConfigureClient`, + // CERTS_FLOW_TERMS_OF_SERVICE = `api/CertsFlow/TermsOfService/{sessionId}`, + // CERTS_FLOW_INIT = `api/CertsFlow/Init/{sessionId}/{accountId}`, + // CERTS_FLOW_NEW_ORDER = `api/CertsFlow/NewOrder/{sessionId}`, + // CERTS_FLOW_GET_ORDER = `api/CertsFlow/GetOrder/{sessionId}`, + // CERTS_FLOW_GET_CERTIFICATES = `api/CertsFlow/GetCertificates/{sessionId}`, + // CERTS_FLOW_APPLY_CERTIFICATES = `api/CertsFlow/ApplyCertificates/{sessionId}`, + // CERTS_FLOW_HOSRS_WITH_UPCOMING_SSL_EXPIRY = `api/CertsFlow/HostsWithUpcomingSslExpiry/{sessionId}` } const GetApiRoute = (route: ApiRoutes, ...args: string[]): string => { @@ -27,7 +23,7 @@ const GetApiRoute = (route: ApiRoutes, ...args: string[]): string => { args.forEach(arg => { result = result.replace(/{.*?}/, arg); }); - return 'http://localhost:5000/' + result; + return `http://localhost:5000/${result}`; } diff --git a/src/ClientApp/app/layout.tsx b/src/ClientApp/app/layout.tsx index daffe9b..d359020 100644 --- a/src/ClientApp/app/layout.tsx +++ b/src/ClientApp/app/layout.tsx @@ -1,16 +1,16 @@ -"use client" // Add this line +"use client" import React, { FC, useState, useEffect, useRef } from 'react' -import './globals.css' import { SideMenu } from '@/components/sidemenu' import { TopMenu } from '@/components/topmenu' import { Footer } from '@/components/footer' import { OffCanvas } from '@/components/offcanvas' -import { Loader } from '@/components/loader' // Import the Loader component +import { Loader } from '@/components/loader' import { Metadata } from 'next' import { Toast } from '@/components/toast' import { Provider } from 'react-redux' -import { store } from '@/redux/store' +import { store } from '@/redux/store' +import './globals.css' const metadata: Metadata = { title: "Create Next App", @@ -20,7 +20,6 @@ const metadata: Metadata = { const Layout: FC<{ children: React.ReactNode }> = ({ children }) => { const [isSidebarCollapsed, setIsSidebarCollapsed] = useState(false) const [isManuallyCollapsed, setManuallyCollapsed] = useState(false) - //const [isLoading, setIsLoading] = useState(true) // State to control the loader visibility const init = useRef(false) @@ -43,7 +42,6 @@ const Layout: FC<{ children: React.ReactNode }> = ({ children }) => { } else { if (isManuallyCollapsed) return - // Reset manualCollapse if the window is resized to a state that should automatically collapse/expand the sidebar if (window.innerWidth > 768 && isSidebarCollapsed) { setIsSidebarCollapsed(false) } else if (window.innerWidth <= 768 && !isSidebarCollapsed) { @@ -54,7 +52,7 @@ const Layout: FC<{ children: React.ReactNode }> = ({ children }) => { useEffect(() => { if (!init.current) { - handleResize() // Set the initial state based on the current window width + handleResize() init.current = true } @@ -72,20 +70,23 @@ const Layout: FC<{ children: React.ReactNode }> = ({ children }) => { return ( - + -
+
-
+ +
-
+
{children}
-
+
+
+ diff --git a/src/ClientApp/app/page.tsx b/src/ClientApp/app/page.tsx index eaf0bc9..8884e8a 100644 --- a/src/ClientApp/app/page.tsx +++ b/src/ClientApp/app/page.tsx @@ -1,14 +1,12 @@ "use client" import { ApiRoutes, GetApiRoute } from "@/ApiRoutes" -import { GetAccountsResponse } from "@/models/letsEncryptServer/cache/GetAccountsResponse" -import { GetContactsResponse } from "@/models/letsEncryptServer/cache/GetContactsResponse" -import { GetHostnamesResponse } from "@/models/letsEncryptServer/cache/GetHostnamesResponse" import { httpService } from "@/services/httpService" import { FormEvent, useEffect, useRef, useState } from "react" import { useValidation, isValidEmail, isValidHostname } from "@/hooks/useValidation" import { CustomButton, CustomInput } from "@/controls" import { TrashIcon, PlusIcon } from "@heroicons/react/24/solid" +import { GetAccountResponse } from "@/models/letsEncryptServer/cache/responses/GetAccountResponse" interface CacheAccountHostname { hostname: string @@ -32,39 +30,44 @@ export default function Page() { value: newContact, error: contactError, handleChange: handleContactChange - } = useValidation("", isValidEmail, "Invalid email format.") + } = useValidation({ + initialValue:"", + validateFn: isValidEmail, + errorMessage: "Invalid email format." + }) const { value: newHostname, error: hostnameError, handleChange: handleHostnameChange - } = useValidation("", isValidHostname, "Invalid hostname format.") + } = useValidation({ + initialValue: "", + validateFn: isValidHostname, + errorMessage: "Invalid hostname format."}) const init = useRef(false) useEffect(() => { if (init.current) return + + console.log("Fetching accounts") + const fetchAccounts = async () => { const newAccounts: CacheAccount[] = [] - const accountsResponse = await httpService.get(GetApiRoute(ApiRoutes.CACHE_GET_ACCOUNTS)) - - for (const accountId of accountsResponse.accountIds) { - const [contactsResponse, hostnamesResponse] = await Promise.all([ - httpService.get(GetApiRoute(ApiRoutes.CACHE_GET_CONTACTS, accountId)), - httpService.get(GetApiRoute(ApiRoutes.CACHE_GET_HOSTNAMES, accountId)) - ]) + const accounts = await httpService.get(GetApiRoute(ApiRoutes.CACHE_ACCOUNTS)) + accounts?.forEach((account) => { newAccounts.push({ - accountId: accountId, - contacts: contactsResponse.contacts, - hostnames: hostnamesResponse.hostnames.map(h => ({ + accountId: account.accountId, + contacts: account.contacts, + hostnames: account.hostnames.map(h => ({ hostname: h.hostname, expires: new Date(h.expires), isUpcomingExpire: h.isUpcomingExpire })), isEditMode: false }) - } + }); setAccounts(newAccounts) setInitialAccounts(JSON.parse(JSON.stringify(newAccounts))) // Clone initial state @@ -92,7 +95,7 @@ export default function Page() { if (account?.contacts.length ?? 0 < 1) return // TODO: Remove from cache - httpService.delete(GetApiRoute(ApiRoutes.CACHE_DELETE_CONTACT, accountId, contact)) + httpService.delete(GetApiRoute(ApiRoutes.CACHE_ACCOUNT_CONTACT, accountId, contact)) setAccounts(accounts.map(account => account.accountId === accountId @@ -192,7 +195,7 @@ export default function Page() { return (
-

LetsEncrypt Client Dashboard

+

LetsEncrypt Auto Renew

{ accounts.map(account => (
diff --git a/src/ClientApp/app/register/page.tsx b/src/ClientApp/app/register/page.tsx new file mode 100644 index 0000000..65c94f0 --- /dev/null +++ b/src/ClientApp/app/register/page.tsx @@ -0,0 +1,209 @@ +"use client" + +import { ApiRoutes, GetApiRoute } from "@/ApiRoutes" +import { httpService } from "@/services/httpService" +import { FormEvent, useEffect, useRef, useState } from "react" +import { useValidation, isValidContact, isValidHostname } from "@/hooks/useValidation" +import { CustomButton, CustomInput } from "@/controls" +import { FaTrash, FaPlus } from "react-icons/fa" +import { GetAccountResponse } from "@/models/letsEncryptServer/cache/responses/GetAccountResponse" + +interface CacheAccountHostname { + hostname: string + expires: Date + isUpcomingExpire: boolean +} + +interface CacheAccount { + accountId: string + description?: string + contacts: string[] + hostnames: CacheAccountHostname[] + isEditMode: boolean +} + +const RegisterPage = () => { + const [accounts, setAccounts] = useState([]) + const [initialAccounts, setInitialAccounts] = useState([]) + const [description, setDescription] = useState("") + const [contacts, setContacts] = useState([]) + const [hostnames, setHostnames] = useState([]) + + const { + value: newContact, + error: contactError, + handleChange: handleContactChange, + reset: resetContact + } = useValidation({ + initialValue: "", + validateFn: isValidContact, + errorMessage: "Invalid contact. Must be a valid email or phone number." + }) + + const { + value: newHostname, + error: hostnameError, + handleChange: handleHostnameChange, + reset: resetHostname + } = useValidation({ + initialValue: "", + validateFn: isValidHostname, + errorMessage: "Invalid hostname format." + }) + + const init = useRef(false) + + useEffect(() => { + if (init.current) return + + const fetchAccounts = async () => { + const newAccounts: CacheAccount[] = [] + const accounts = await httpService.get(GetApiRoute(ApiRoutes.CACHE_ACCOUNTS)) + + accounts?.forEach((account) => { + newAccounts.push({ + accountId: account.accountId, + contacts: account.contacts, + hostnames: account.hostnames.map(h => ({ + hostname: h.hostname, + expires: new Date(h.expires), + isUpcomingExpire: h.isUpcomingExpire + })), + isEditMode: false + }) + }) + + setAccounts(newAccounts) + setInitialAccounts(JSON.parse(JSON.stringify(newAccounts))) // Clone initial state + } + + fetchAccounts() + init.current = true + }, []) + + const handleAddContact = () => { + if (newContact.trim() !== "" && !contactError) { + setContacts([...contacts, newContact.trim()]) + resetContact() + } + } + + const handleAddHostname = () => { + if (newHostname.trim() !== "" && !hostnameError) { + setHostnames([...hostnames, newHostname.trim()]) + resetHostname() + } + } + + const handleDeleteContact = (contact: string) => { + setContacts(contacts.filter(c => c !== contact)) + } + + const handleDeleteHostname = (hostname: string) => { + setHostnames(hostnames.filter(h => h !== hostname)) + } + + const handleSubmit = async (e: FormEvent) => { + e.preventDefault() + + if (!description || contacts.length === 0 || hostnames.length === 0) { + return + } + + const newAccount = { + description, + contacts, + hostnames: hostnames.map(hostname => ({ hostname, expires: new Date(), isUpcomingExpire: false })) + } + + // TODO: Implement API call to create new account + console.log("New account data:", newAccount) + + // Reset form fields + setDescription("") + setContacts([]) + setHostnames([]) + } + + return ( +
+

Register LetsEncrypt Account

+
+
+ setDescription(e.target.value)} + placeholder="Account Description" + title="Description" + inputClassName="border p-2 rounded w-full" + className="mb-4" + /> +
+
+

Contacts:

+
    + {contacts.map(contact => ( +
  • + {contact} + +
  • + ))} +
+
+ + +
+
+
+

Hostnames:

+
    + {hostnames.map(hostname => ( +
  • + {hostname} + +
  • + ))} +
+
+ + +
+
+ + Create Account + +
+
+ ) +} + +export default RegisterPage diff --git a/src/ClientApp/components/footer.tsx b/src/ClientApp/components/footer.tsx index 3489b9a..48ce615 100644 --- a/src/ClientApp/components/footer.tsx +++ b/src/ClientApp/components/footer.tsx @@ -1,9 +1,16 @@ import React from 'react' -const Footer = () => { + +interface FooterProps { + className?: string +} + +const Footer = (props: FooterProps) => { + + const { className } = props return ( -