mirror of
https://github.com/MAKS-IT-COM/maksit-certs-ui.git
synced 2025-12-31 04:00:03 +01:00
(feature): registration page init
This commit is contained in:
parent
2dfc7259fb
commit
e321a3237f
@ -1,25 +1,21 @@
|
|||||||
enum ApiRoutes {
|
enum ApiRoutes {
|
||||||
|
|
||||||
CACHE_GET_ACCOUNTS = `api/Cache/GetAccounts`,
|
CACHE_ACCOUNTS = 'api/cache/accounts',
|
||||||
|
CACHE_ACCOUNT = 'api/cache/account/{accountId}',
|
||||||
CACHE_GET_CONTACTS = `api/Cache/GetContacts/{accountId}`,
|
CACHE_ACCOUNT_CONTACTS = 'api/cache/account/{accountId}/contacts',
|
||||||
CACHE_ADD_CONTACT = `api/Cache/AddContact/{accountId}`,
|
CACHE_ACCOUNT_CONTACT = 'api/cache/account/{accountId}/contacts/{index}',
|
||||||
CACHE_DELETE_CONTACT = `api/Cache/DeleteContact/{accountId}?contact={contact}`,
|
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_CONFIGURE_CLIENT = `api/CertsFlow/ConfigureClient`,
|
||||||
CERTS_FLOW_TERMS_OF_SERVICE = `api/CertsFlow/TermsOfService/{sessionId}`,
|
// CERTS_FLOW_TERMS_OF_SERVICE = `api/CertsFlow/TermsOfService/{sessionId}`,
|
||||||
CERTS_FLOW_INIT = `api/CertsFlow/Init/{sessionId}/{accountId}`,
|
// CERTS_FLOW_INIT = `api/CertsFlow/Init/{sessionId}/{accountId}`,
|
||||||
CERTS_FLOW_NEW_ORDER = `api/CertsFlow/NewOrder/{sessionId}`,
|
// CERTS_FLOW_NEW_ORDER = `api/CertsFlow/NewOrder/{sessionId}`,
|
||||||
CERTS_FLOW_GET_ORDER = `api/CertsFlow/GetOrder/{sessionId}`,
|
// CERTS_FLOW_GET_ORDER = `api/CertsFlow/GetOrder/{sessionId}`,
|
||||||
CERTS_FLOW_GET_CERTIFICATES = `api/CertsFlow/GetCertificates/{sessionId}`,
|
// CERTS_FLOW_GET_CERTIFICATES = `api/CertsFlow/GetCertificates/{sessionId}`,
|
||||||
CERTS_FLOW_APPLY_CERTIFICATES = `api/CertsFlow/ApplyCertificates/{sessionId}`,
|
// CERTS_FLOW_APPLY_CERTIFICATES = `api/CertsFlow/ApplyCertificates/{sessionId}`,
|
||||||
CERTS_FLOW_HOSRS_WITH_UPCOMING_SSL_EXPIRY = `api/CertsFlow/HostsWithUpcomingSslExpiry/{sessionId}`
|
// CERTS_FLOW_HOSRS_WITH_UPCOMING_SSL_EXPIRY = `api/CertsFlow/HostsWithUpcomingSslExpiry/{sessionId}`
|
||||||
}
|
}
|
||||||
|
|
||||||
const GetApiRoute = (route: ApiRoutes, ...args: string[]): string => {
|
const GetApiRoute = (route: ApiRoutes, ...args: string[]): string => {
|
||||||
@ -27,7 +23,7 @@ const GetApiRoute = (route: ApiRoutes, ...args: string[]): string => {
|
|||||||
args.forEach(arg => {
|
args.forEach(arg => {
|
||||||
result = result.replace(/{.*?}/, arg);
|
result = result.replace(/{.*?}/, arg);
|
||||||
});
|
});
|
||||||
return 'http://localhost:5000/' + result;
|
return `http://localhost:5000/${result}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@ -1,16 +1,16 @@
|
|||||||
"use client" // Add this line
|
"use client"
|
||||||
|
|
||||||
import React, { FC, useState, useEffect, useRef } from 'react'
|
import React, { FC, useState, useEffect, useRef } from 'react'
|
||||||
import './globals.css'
|
|
||||||
import { SideMenu } from '@/components/sidemenu'
|
import { SideMenu } from '@/components/sidemenu'
|
||||||
import { TopMenu } from '@/components/topmenu'
|
import { TopMenu } from '@/components/topmenu'
|
||||||
import { Footer } from '@/components/footer'
|
import { Footer } from '@/components/footer'
|
||||||
import { OffCanvas } from '@/components/offcanvas'
|
import { OffCanvas } from '@/components/offcanvas'
|
||||||
import { Loader } from '@/components/loader' // Import the Loader component
|
import { Loader } from '@/components/loader'
|
||||||
import { Metadata } from 'next'
|
import { Metadata } from 'next'
|
||||||
import { Toast } from '@/components/toast'
|
import { Toast } from '@/components/toast'
|
||||||
import { Provider } from 'react-redux'
|
import { Provider } from 'react-redux'
|
||||||
import { store } from '@/redux/store'
|
import { store } from '@/redux/store'
|
||||||
|
import './globals.css'
|
||||||
|
|
||||||
const metadata: Metadata = {
|
const metadata: Metadata = {
|
||||||
title: "Create Next App",
|
title: "Create Next App",
|
||||||
@ -20,7 +20,6 @@ const metadata: Metadata = {
|
|||||||
const Layout: FC<{ children: React.ReactNode }> = ({ children }) => {
|
const Layout: FC<{ children: React.ReactNode }> = ({ children }) => {
|
||||||
const [isSidebarCollapsed, setIsSidebarCollapsed] = useState(false)
|
const [isSidebarCollapsed, setIsSidebarCollapsed] = useState(false)
|
||||||
const [isManuallyCollapsed, setManuallyCollapsed] = useState(false)
|
const [isManuallyCollapsed, setManuallyCollapsed] = useState(false)
|
||||||
//const [isLoading, setIsLoading] = useState(true) // State to control the loader visibility
|
|
||||||
|
|
||||||
const init = useRef(false)
|
const init = useRef(false)
|
||||||
|
|
||||||
@ -43,7 +42,6 @@ const Layout: FC<{ children: React.ReactNode }> = ({ children }) => {
|
|||||||
} else {
|
} else {
|
||||||
if (isManuallyCollapsed) return
|
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) {
|
if (window.innerWidth > 768 && isSidebarCollapsed) {
|
||||||
setIsSidebarCollapsed(false)
|
setIsSidebarCollapsed(false)
|
||||||
} else if (window.innerWidth <= 768 && !isSidebarCollapsed) {
|
} else if (window.innerWidth <= 768 && !isSidebarCollapsed) {
|
||||||
@ -54,7 +52,7 @@ const Layout: FC<{ children: React.ReactNode }> = ({ children }) => {
|
|||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!init.current) {
|
if (!init.current) {
|
||||||
handleResize() // Set the initial state based on the current window width
|
handleResize()
|
||||||
init.current = true
|
init.current = true
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -72,20 +70,23 @@ const Layout: FC<{ children: React.ReactNode }> = ({ children }) => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<html lang="en">
|
<html lang="en">
|
||||||
<body className="h-screen overflow-hidden">
|
<body className="h-screen overflow-hidden flex flex-col">
|
||||||
<Provider store={store}>
|
<Provider store={store}>
|
||||||
<Loader />
|
<Loader />
|
||||||
|
|
||||||
<div className="flex h-full">
|
<div className="flex flex-1 overflow-hidden">
|
||||||
<SideMenu isCollapsed={isSidebarCollapsed} toggleSidebar={manuallyToggleSidebar} />
|
<SideMenu isCollapsed={isSidebarCollapsed} toggleSidebar={manuallyToggleSidebar} />
|
||||||
<div className="flex flex-col flex-1">
|
|
||||||
|
<div className="flex flex-col flex-1 overflow-hidden">
|
||||||
<TopMenu onToggleOffCanvas={toggleOffCanvas} />
|
<TopMenu onToggleOffCanvas={toggleOffCanvas} />
|
||||||
<main className="flex-1 p-4 transition-transform duration-300">
|
<main className="flex-1 p-4 overflow-y-auto">
|
||||||
{children}
|
{children}
|
||||||
</main>
|
</main>
|
||||||
<Footer />
|
<Footer className="flex-shrink-0" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<OffCanvas isOpen={isOffCanvasOpen} onClose={toggleOffCanvas} />
|
<OffCanvas isOpen={isOffCanvasOpen} onClose={toggleOffCanvas} />
|
||||||
<Toast />
|
<Toast />
|
||||||
</Provider>
|
</Provider>
|
||||||
|
|||||||
@ -1,14 +1,12 @@
|
|||||||
"use client"
|
"use client"
|
||||||
|
|
||||||
import { ApiRoutes, GetApiRoute } from "@/ApiRoutes"
|
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 { httpService } from "@/services/httpService"
|
||||||
import { FormEvent, useEffect, useRef, useState } from "react"
|
import { FormEvent, useEffect, useRef, useState } from "react"
|
||||||
import { useValidation, isValidEmail, isValidHostname } from "@/hooks/useValidation"
|
import { useValidation, isValidEmail, isValidHostname } from "@/hooks/useValidation"
|
||||||
import { CustomButton, CustomInput } from "@/controls"
|
import { CustomButton, CustomInput } from "@/controls"
|
||||||
import { TrashIcon, PlusIcon } from "@heroicons/react/24/solid"
|
import { TrashIcon, PlusIcon } from "@heroicons/react/24/solid"
|
||||||
|
import { GetAccountResponse } from "@/models/letsEncryptServer/cache/responses/GetAccountResponse"
|
||||||
|
|
||||||
interface CacheAccountHostname {
|
interface CacheAccountHostname {
|
||||||
hostname: string
|
hostname: string
|
||||||
@ -32,39 +30,44 @@ export default function Page() {
|
|||||||
value: newContact,
|
value: newContact,
|
||||||
error: contactError,
|
error: contactError,
|
||||||
handleChange: handleContactChange
|
handleChange: handleContactChange
|
||||||
} = useValidation("", isValidEmail, "Invalid email format.")
|
} = useValidation({
|
||||||
|
initialValue:"",
|
||||||
|
validateFn: isValidEmail,
|
||||||
|
errorMessage: "Invalid email format."
|
||||||
|
})
|
||||||
const {
|
const {
|
||||||
value: newHostname,
|
value: newHostname,
|
||||||
error: hostnameError,
|
error: hostnameError,
|
||||||
handleChange: handleHostnameChange
|
handleChange: handleHostnameChange
|
||||||
} = useValidation("", isValidHostname, "Invalid hostname format.")
|
} = useValidation({
|
||||||
|
initialValue: "",
|
||||||
|
validateFn: isValidHostname,
|
||||||
|
errorMessage: "Invalid hostname format."})
|
||||||
|
|
||||||
const init = useRef(false)
|
const init = useRef(false)
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (init.current) return
|
if (init.current) return
|
||||||
|
|
||||||
|
|
||||||
|
console.log("Fetching accounts")
|
||||||
|
|
||||||
const fetchAccounts = async () => {
|
const fetchAccounts = async () => {
|
||||||
const newAccounts: CacheAccount[] = []
|
const newAccounts: CacheAccount[] = []
|
||||||
const accountsResponse = await httpService.get<GetAccountsResponse>(GetApiRoute(ApiRoutes.CACHE_GET_ACCOUNTS))
|
const accounts = await httpService.get<GetAccountResponse []>(GetApiRoute(ApiRoutes.CACHE_ACCOUNTS))
|
||||||
|
|
||||||
for (const accountId of accountsResponse.accountIds) {
|
|
||||||
const [contactsResponse, hostnamesResponse] = await Promise.all([
|
|
||||||
httpService.get<GetContactsResponse>(GetApiRoute(ApiRoutes.CACHE_GET_CONTACTS, accountId)),
|
|
||||||
httpService.get<GetHostnamesResponse>(GetApiRoute(ApiRoutes.CACHE_GET_HOSTNAMES, accountId))
|
|
||||||
])
|
|
||||||
|
|
||||||
|
accounts?.forEach((account) => {
|
||||||
newAccounts.push({
|
newAccounts.push({
|
||||||
accountId: accountId,
|
accountId: account.accountId,
|
||||||
contacts: contactsResponse.contacts,
|
contacts: account.contacts,
|
||||||
hostnames: hostnamesResponse.hostnames.map(h => ({
|
hostnames: account.hostnames.map(h => ({
|
||||||
hostname: h.hostname,
|
hostname: h.hostname,
|
||||||
expires: new Date(h.expires),
|
expires: new Date(h.expires),
|
||||||
isUpcomingExpire: h.isUpcomingExpire
|
isUpcomingExpire: h.isUpcomingExpire
|
||||||
})),
|
})),
|
||||||
isEditMode: false
|
isEditMode: false
|
||||||
})
|
})
|
||||||
}
|
});
|
||||||
|
|
||||||
setAccounts(newAccounts)
|
setAccounts(newAccounts)
|
||||||
setInitialAccounts(JSON.parse(JSON.stringify(newAccounts))) // Clone initial state
|
setInitialAccounts(JSON.parse(JSON.stringify(newAccounts))) // Clone initial state
|
||||||
@ -92,7 +95,7 @@ export default function Page() {
|
|||||||
if (account?.contacts.length ?? 0 < 1) return
|
if (account?.contacts.length ?? 0 < 1) return
|
||||||
|
|
||||||
// TODO: Remove from cache
|
// 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 =>
|
setAccounts(accounts.map(account =>
|
||||||
account.accountId === accountId
|
account.accountId === accountId
|
||||||
@ -192,7 +195,7 @@ export default function Page() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="container mx-auto p-4">
|
<div className="container mx-auto p-4">
|
||||||
<h1 className="text-4xl font-bold text-center mb-8">LetsEncrypt Client Dashboard</h1>
|
<h1 className="text-4xl font-bold text-center mb-8">LetsEncrypt Auto Renew</h1>
|
||||||
{
|
{
|
||||||
accounts.map(account => (
|
accounts.map(account => (
|
||||||
<div key={account.accountId} className="bg-white shadow-lg rounded-lg p-6 mb-6">
|
<div key={account.accountId} className="bg-white shadow-lg rounded-lg p-6 mb-6">
|
||||||
|
|||||||
209
src/ClientApp/app/register/page.tsx
Normal file
209
src/ClientApp/app/register/page.tsx
Normal file
@ -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<CacheAccount[]>([])
|
||||||
|
const [initialAccounts, setInitialAccounts] = useState<CacheAccount[]>([])
|
||||||
|
const [description, setDescription] = useState("")
|
||||||
|
const [contacts, setContacts] = useState<string[]>([])
|
||||||
|
const [hostnames, setHostnames] = useState<string[]>([])
|
||||||
|
|
||||||
|
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<GetAccountResponse[]>(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<HTMLFormElement>) => {
|
||||||
|
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 (
|
||||||
|
<div className="container mx-auto p-4">
|
||||||
|
<h1 className="text-4xl font-bold text-center mb-8">Register LetsEncrypt Account</h1>
|
||||||
|
<form onSubmit={handleSubmit}>
|
||||||
|
<div className="mb-4">
|
||||||
|
<CustomInput
|
||||||
|
type="text"
|
||||||
|
value={description}
|
||||||
|
onChange={(e) => setDescription(e.target.value)}
|
||||||
|
placeholder="Account Description"
|
||||||
|
title="Description"
|
||||||
|
inputClassName="border p-2 rounded w-full"
|
||||||
|
className="mb-4"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="mb-4">
|
||||||
|
<h3 className="text-xl font-medium mb-2">Contacts:</h3>
|
||||||
|
<ul className="list-disc list-inside pl-4 mb-2">
|
||||||
|
{contacts.map(contact => (
|
||||||
|
<li key={contact} className="text-gray-700 flex justify-between items-center mb-2">
|
||||||
|
{contact}
|
||||||
|
<button type="button" onClick={() => handleDeleteContact(contact)} className="bg-red-500 text-white px-2 py-1 rounded ml-4">
|
||||||
|
<FaTrash />
|
||||||
|
</button>
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
<div className="flex items-center mb-4">
|
||||||
|
<CustomInput
|
||||||
|
value={newContact}
|
||||||
|
onChange={handleContactChange}
|
||||||
|
placeholder="Add contact"
|
||||||
|
type="text"
|
||||||
|
error={contactError}
|
||||||
|
title="New Contact"
|
||||||
|
inputClassName="border p-2 rounded w-full"
|
||||||
|
errorClassName="text-red-500 text-sm mt-1"
|
||||||
|
className="mr-2 flex-grow"
|
||||||
|
/>
|
||||||
|
<button type="button" onClick={handleAddContact} className="bg-green-500 text-white p-2 rounded ml-2 h-10 flex items-center">
|
||||||
|
<FaPlus />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="mb-4">
|
||||||
|
<h3 className="text-xl font-medium mb-2">Hostnames:</h3>
|
||||||
|
<ul className="list-disc list-inside pl-4 mb-2">
|
||||||
|
{hostnames.map(hostname => (
|
||||||
|
<li key={hostname} className="text-gray-700 flex justify-between items-center mb-2">
|
||||||
|
{hostname}
|
||||||
|
<button type="button" onClick={() => handleDeleteHostname(hostname)} className="bg-red-500 text-white px-2 py-1 rounded ml-4">
|
||||||
|
<FaTrash />
|
||||||
|
</button>
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
<div className="flex items-center">
|
||||||
|
<CustomInput
|
||||||
|
value={newHostname}
|
||||||
|
onChange={handleHostnameChange}
|
||||||
|
placeholder="Add hostname"
|
||||||
|
type="text"
|
||||||
|
error={hostnameError}
|
||||||
|
title="New Hostname"
|
||||||
|
inputClassName="border p-2 rounded w-full"
|
||||||
|
errorClassName="text-red-500 text-sm mt-1"
|
||||||
|
className="mr-2 flex-grow"
|
||||||
|
/>
|
||||||
|
<button type="button" onClick={handleAddHostname} className="bg-green-500 text-white p-2 rounded ml-2 h-10 flex items-center">
|
||||||
|
<FaPlus />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<CustomButton type="submit" className="bg-green-500 text-white px-3 py-1 rounded">
|
||||||
|
Create Account
|
||||||
|
</CustomButton>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default RegisterPage
|
||||||
@ -1,9 +1,16 @@
|
|||||||
import React from 'react'
|
import React from 'react'
|
||||||
|
|
||||||
const Footer = () => {
|
|
||||||
|
interface FooterProps {
|
||||||
|
className?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
const Footer = (props: FooterProps) => {
|
||||||
|
|
||||||
|
const { className } = props
|
||||||
return (
|
return (
|
||||||
<footer className="bg-gray-900 text-white text-center p-4">
|
<footer className={`bg-gray-900 text-white text-center p-4 ${className}`}>
|
||||||
<p>© {new Date().getFullYear()} MAKS-IT</p>
|
<p>{`© ${new Date().getFullYear()} MAKS-IT`}</p>
|
||||||
</footer>
|
</footer>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -30,8 +30,7 @@ const Loader: React.FC = () => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="loader-overlay">
|
<div className="loader-overlay">
|
||||||
<div className="spinner"></div>
|
<span className="loader"></span>
|
||||||
<div className="loading-text">Loading...</div>
|
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,34 +1,49 @@
|
|||||||
.loader-overlay {
|
.loader-overlay {
|
||||||
position: fixed;
|
position: fixed;
|
||||||
top: 0;
|
top: 0;
|
||||||
left: 0;
|
left: 0;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
background: rgba(0, 0, 0, 0.1); /* 10% transparent background */
|
background: rgba(0, 0, 0, 0.1); /* 10% transparent background */
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
z-index: 9999;
|
z-index: 9999;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
}
|
}
|
||||||
|
|
||||||
.spinner {
|
.loader {
|
||||||
border: 8px solid rgba(255, 255, 255, 0.3);
|
width: 48px;
|
||||||
border-top: 8px solid #3498db;
|
height: 48px;
|
||||||
border-radius: 50%;
|
display: inline-block;
|
||||||
width: 80px;
|
position: relative;
|
||||||
height: 80px;
|
}
|
||||||
animation: spin 1s linear infinite;
|
.loader::after,
|
||||||
}
|
.loader::before {
|
||||||
|
content: '';
|
||||||
|
box-sizing: border-box;
|
||||||
|
width: 48px;
|
||||||
|
height: 48px;
|
||||||
|
border-radius: 50%;
|
||||||
|
background: #FFF;
|
||||||
|
position: absolute;
|
||||||
|
left: 0;
|
||||||
|
top: 0;
|
||||||
|
animation: animloader 2s linear infinite;
|
||||||
|
}
|
||||||
|
.loader::after {
|
||||||
|
animation-delay: 1s;
|
||||||
|
}
|
||||||
|
|
||||||
.loading-text {
|
@keyframes animloader {
|
||||||
margin-top: 20px;
|
0% {
|
||||||
font-size: 1.2em;
|
transform: scale(0);
|
||||||
color: #3498db;
|
opacity: 1;
|
||||||
}
|
}
|
||||||
|
100% {
|
||||||
@keyframes spin {
|
transform: scale(1);
|
||||||
0% { transform: rotate(0deg); }
|
opacity: 0;
|
||||||
100% { transform: rotate(360deg); }
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
@ -1,41 +1,42 @@
|
|||||||
import React, { FC, useEffect, useRef } from 'react'
|
import React, { FC } from 'react';
|
||||||
import { FaHome, FaUser, FaCog, FaBars } from 'react-icons/fa'
|
import { FaHome, FaUserPlus, FaBars, FaSyncAlt } from 'react-icons/fa';
|
||||||
|
import Link from 'next/link';
|
||||||
|
|
||||||
interface SideMenuProps {
|
interface SideMenuProps {
|
||||||
isCollapsed: boolean
|
isCollapsed: boolean;
|
||||||
toggleSidebar: () => void
|
toggleSidebar: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
const SideMenu: FC<SideMenuProps> = ({ isCollapsed, toggleSidebar }) => {
|
const menuItems = [
|
||||||
|
{ icon: <FaSyncAlt />, label: 'Auto Renew', path: '/' },
|
||||||
|
{ icon: <FaUserPlus />, label: 'Register', path: '/register' }
|
||||||
|
];
|
||||||
|
|
||||||
|
const SideMenu: FC<SideMenuProps> = ({ isCollapsed, toggleSidebar }) => {
|
||||||
return (
|
return (
|
||||||
<div className={`flex flex-col bg-gray-800 text-white transition-all duration-300 ${isCollapsed ? 'w-16' : 'w-64'} h-full`}>
|
<div className={`flex flex-col bg-gray-800 text-white transition-all duration-300 ${isCollapsed ? 'w-16' : 'w-64'} h-full`}>
|
||||||
<div className="flex items-center h-16 bg-gray-900 relative">
|
<div className="flex items-center h-16 bg-gray-900 relative">
|
||||||
<button onClick={toggleSidebar} className="absolute left-4">
|
<button onClick={toggleSidebar} className="absolute left-4">
|
||||||
<FaBars />
|
<FaBars />
|
||||||
</button>
|
</button>
|
||||||
<h1 className={`${isCollapsed ? 'hidden' : 'block'} text-2xl font-bold ml-12`}>Logo</h1>
|
<h1 className={`${isCollapsed ? 'hidden' : 'block'} text-2xl font-bold ml-12`}>Certs UI</h1>
|
||||||
</div>
|
</div>
|
||||||
<nav className="flex-1">
|
<nav className="flex-1">
|
||||||
<ul>
|
<ul>
|
||||||
<li className="flex items-center p-4 hover:bg-gray-700">
|
{menuItems.map((item, index) => (
|
||||||
<FaHome className="mr-4" />
|
<li key={index} className="hover:bg-gray-700">
|
||||||
<span className={`${isCollapsed ? 'hidden' : 'block'}`}>Home</span>
|
<Link href={item.path} className="flex items-center w-full p-4">
|
||||||
</li>
|
<span className={`${isCollapsed ? 'mr-0' : 'mr-4'}`}>{item.icon}</span>
|
||||||
<li className="flex items-center p-4 hover:bg-gray-700">
|
<span className={`${isCollapsed ? 'hidden' : 'block'}`}>{item.label}</span>
|
||||||
<FaUser className="mr-4" />
|
</Link>
|
||||||
<span className={`${isCollapsed ? 'hidden' : 'block'}`}>Profile</span>
|
</li>
|
||||||
</li>
|
))}
|
||||||
<li className="flex items-center p-4 hover:bg-gray-700">
|
|
||||||
<FaCog className="mr-4" />
|
|
||||||
<span className={`${isCollapsed ? 'hidden' : 'block'}`}>Settings</span>
|
|
||||||
</li>
|
|
||||||
</ul>
|
</ul>
|
||||||
</nav>
|
</nav>
|
||||||
</div>
|
</div>
|
||||||
)
|
);
|
||||||
}
|
};
|
||||||
|
|
||||||
export {
|
export {
|
||||||
SideMenu
|
SideMenu
|
||||||
}
|
};
|
||||||
|
|||||||
@ -4,7 +4,7 @@ import React from 'react'
|
|||||||
|
|
||||||
interface CustomInputProps {
|
interface CustomInputProps {
|
||||||
value: string
|
value: string
|
||||||
onChange: (value: string) => void
|
onChange?: (value: string) => void
|
||||||
placeholder?: string
|
placeholder?: string
|
||||||
type: 'text' | 'password' | 'email' | 'number' | 'tel' | 'url'
|
type: 'text' | 'password' | 'email' | 'number' | 'tel' | 'url'
|
||||||
error?: string
|
error?: string
|
||||||
@ -25,13 +25,18 @@ const CustomInput: React.FC<CustomInputProps> = ({
|
|||||||
errorClassName = '',
|
errorClassName = '',
|
||||||
className = ''
|
className = ''
|
||||||
}) => {
|
}) => {
|
||||||
|
|
||||||
|
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
|
onChange?.(e.target.value);
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={className}>
|
<div className={className}>
|
||||||
{title && <label>{title}</label>}
|
{title && <label>{title}</label>}
|
||||||
<input
|
<input
|
||||||
type={type}
|
type={type}
|
||||||
value={value}
|
value={value}
|
||||||
onChange={(e) => onChange(e.target.value)}
|
onChange={handleChange}
|
||||||
placeholder={placeholder}
|
placeholder={placeholder}
|
||||||
className={inputClassName}
|
className={inputClassName}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
import { CustomButton } from "./customButton"
|
import { CustomButton } from "./customButton";
|
||||||
import { CustomInput } from "./customInput"
|
import { CustomInput } from "./customInput";
|
||||||
|
|
||||||
export {
|
export {
|
||||||
CustomButton,
|
CustomButton,
|
||||||
|
|||||||
@ -6,17 +6,35 @@ const isValidEmail = (email: string) => {
|
|||||||
return emailRegex.test(email)
|
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 isValidHostname = (hostname: string) => {
|
||||||
const hostnameRegex = /^(?!:\/\/)([a-zA-Z0-9-_]{1,63}\.?)+[a-zA-Z]{2,6}$/
|
const hostnameRegex = /^(?!:\/\/)([a-zA-Z0-9-_]{1,63}\.?)+[a-zA-Z]{2,6}$/
|
||||||
return hostnameRegex.test(hostname)
|
return hostnameRegex.test(hostname)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Props interface for useValidation hook
|
||||||
|
interface UseValidationProps {
|
||||||
|
initialValue: string
|
||||||
|
validateFn: (value: string) => boolean
|
||||||
|
errorMessage: string
|
||||||
|
}
|
||||||
|
|
||||||
// Custom hook for input validation
|
// Custom hook for input validation
|
||||||
const useValidation = (initialValue: string, validateFn: (value: string) => boolean, errorMessage: string) => {
|
const useValidation = ({ initialValue, validateFn, errorMessage }: UseValidationProps) => {
|
||||||
const [value, setValue] = useState(initialValue)
|
const [value, setValue] = useState(initialValue)
|
||||||
const [error, setError] = useState("")
|
const [error, setError] = useState("")
|
||||||
|
|
||||||
const handleChange = (newValue: string) => {
|
const handleChange = (newValue: string) => {
|
||||||
|
|
||||||
|
console.log(newValue)
|
||||||
setValue(newValue)
|
setValue(newValue)
|
||||||
if (newValue.trim() === "") {
|
if (newValue.trim() === "") {
|
||||||
setError("This field cannot be empty.")
|
setError("This field cannot be empty.")
|
||||||
@ -31,7 +49,7 @@ const useValidation = (initialValue: string, validateFn: (value: string) => bool
|
|||||||
handleChange(initialValue)
|
handleChange(initialValue)
|
||||||
}, [initialValue])
|
}, [initialValue])
|
||||||
|
|
||||||
return { value, error, handleChange }
|
return { value, error, handleChange, reset: () => setValue("") }
|
||||||
}
|
}
|
||||||
|
|
||||||
export { useValidation, isValidEmail, isValidHostname }
|
export { useValidation, isValidEmail, isValidPhoneNumber, isValidContact, isValidHostname }
|
||||||
|
|||||||
@ -1,3 +0,0 @@
|
|||||||
export interface GetAccountsResponse {
|
|
||||||
accountIds: string[]
|
|
||||||
}
|
|
||||||
@ -1,9 +0,0 @@
|
|||||||
interface HostnameResponse {
|
|
||||||
hostname: string
|
|
||||||
expires: string,
|
|
||||||
isUpcomingExpire: boolean
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface GetHostnamesResponse {
|
|
||||||
hostnames: HostnameResponse[]
|
|
||||||
}
|
|
||||||
9
src/ClientApp/models/letsEncryptServer/cache/responses/GetAccountResponse.ts
vendored
Normal file
9
src/ClientApp/models/letsEncryptServer/cache/responses/GetAccountResponse.ts
vendored
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
import { HostnameResponse } from "./HostnameResponse";
|
||||||
|
|
||||||
|
export interface GetAccountResponse {
|
||||||
|
accountId: string,
|
||||||
|
contacts: string[],
|
||||||
|
hostnames: HostnameResponse[],
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
5
src/ClientApp/models/letsEncryptServer/cache/responses/GetHostnamesResponse.ts
vendored
Normal file
5
src/ClientApp/models/letsEncryptServer/cache/responses/GetHostnamesResponse.ts
vendored
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
import { HostnameResponse } from "./HostnameResponse";
|
||||||
|
|
||||||
|
export interface GetHostnamesResponse {
|
||||||
|
hostnames: HostnameResponse[]
|
||||||
|
}
|
||||||
5
src/ClientApp/models/letsEncryptServer/cache/responses/HostnameResponse.ts
vendored
Normal file
5
src/ClientApp/models/letsEncryptServer/cache/responses/HostnameResponse.ts
vendored
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
export interface HostnameResponse {
|
||||||
|
hostname: string
|
||||||
|
expires: string
|
||||||
|
isUpcomingExpire: boolean
|
||||||
|
}
|
||||||
@ -1,55 +1,105 @@
|
|||||||
using System;
|
using System;
|
||||||
|
using System.Collections.Concurrent;
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
public class LockManager : IDisposable {
|
public class LockManager : IDisposable {
|
||||||
private readonly SemaphoreSlim _semaphore;
|
private readonly SemaphoreSlim _semaphore = new SemaphoreSlim(1, 1);
|
||||||
|
private readonly ConcurrentDictionary<int, int> _reentrantCounts = new ConcurrentDictionary<int, int>();
|
||||||
public LockManager(int initialCount, int maxCount) {
|
|
||||||
_semaphore = new SemaphoreSlim(initialCount, maxCount);
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task<T> ExecuteWithLockAsync<T>(Func<Task<T>> action) {
|
public async Task<T> ExecuteWithLockAsync<T>(Func<Task<T>> action) {
|
||||||
await _semaphore.WaitAsync();
|
var threadId = Thread.CurrentThread.ManagedThreadId;
|
||||||
|
|
||||||
|
if (!_reentrantCounts.ContainsKey(threadId)) {
|
||||||
|
_reentrantCounts[threadId] = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (_reentrantCounts[threadId] == 0) {
|
||||||
|
await _semaphore.WaitAsync();
|
||||||
|
}
|
||||||
|
|
||||||
|
_reentrantCounts[threadId]++;
|
||||||
try {
|
try {
|
||||||
return await action();
|
return await action();
|
||||||
}
|
}
|
||||||
finally {
|
finally {
|
||||||
_semaphore.Release();
|
_reentrantCounts[threadId]--;
|
||||||
|
if (_reentrantCounts[threadId] == 0) {
|
||||||
|
_semaphore.Release();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task ExecuteWithLockAsync(Func<Task> action) {
|
public async Task ExecuteWithLockAsync(Func<Task> action) {
|
||||||
await _semaphore.WaitAsync();
|
var threadId = Thread.CurrentThread.ManagedThreadId;
|
||||||
|
|
||||||
|
if (!_reentrantCounts.ContainsKey(threadId)) {
|
||||||
|
_reentrantCounts[threadId] = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (_reentrantCounts[threadId] == 0) {
|
||||||
|
await _semaphore.WaitAsync();
|
||||||
|
}
|
||||||
|
|
||||||
|
_reentrantCounts[threadId]++;
|
||||||
try {
|
try {
|
||||||
await action();
|
await action();
|
||||||
}
|
}
|
||||||
finally {
|
finally {
|
||||||
_semaphore.Release();
|
_reentrantCounts[threadId]--;
|
||||||
|
if (_reentrantCounts[threadId] == 0) {
|
||||||
|
_semaphore.Release();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<T> ExecuteWithLockAsync<T>(Func<T> action) {
|
public async Task<T> ExecuteWithLockAsync<T>(Func<T> action) {
|
||||||
await _semaphore.WaitAsync();
|
var threadId = Thread.CurrentThread.ManagedThreadId;
|
||||||
|
|
||||||
|
if (!_reentrantCounts.ContainsKey(threadId)) {
|
||||||
|
_reentrantCounts[threadId] = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (_reentrantCounts[threadId] == 0) {
|
||||||
|
await _semaphore.WaitAsync();
|
||||||
|
}
|
||||||
|
|
||||||
|
_reentrantCounts[threadId]++;
|
||||||
try {
|
try {
|
||||||
return await Task.Run(action);
|
return await Task.Run(action);
|
||||||
}
|
}
|
||||||
finally {
|
finally {
|
||||||
_semaphore.Release();
|
_reentrantCounts[threadId]--;
|
||||||
|
if (_reentrantCounts[threadId] == 0) {
|
||||||
|
_semaphore.Release();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task ExecuteWithLockAsync(Action action) {
|
public async Task ExecuteWithLockAsync(Action action) {
|
||||||
await _semaphore.WaitAsync();
|
var threadId = Thread.CurrentThread.ManagedThreadId;
|
||||||
|
|
||||||
|
if (!_reentrantCounts.ContainsKey(threadId)) {
|
||||||
|
_reentrantCounts[threadId] = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (_reentrantCounts[threadId] == 0) {
|
||||||
|
await _semaphore.WaitAsync();
|
||||||
|
}
|
||||||
|
|
||||||
|
_reentrantCounts[threadId]++;
|
||||||
try {
|
try {
|
||||||
await Task.Run(action);
|
await Task.Run(action);
|
||||||
}
|
}
|
||||||
finally {
|
finally {
|
||||||
_semaphore.Release();
|
_reentrantCounts[threadId]--;
|
||||||
|
if (_reentrantCounts[threadId] == 0) {
|
||||||
|
_semaphore.Release();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public void Dispose() {
|
public void Dispose() {
|
||||||
_semaphore?.Dispose();
|
_semaphore.Dispose();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -5,6 +5,8 @@ using DomainResults.Common;
|
|||||||
|
|
||||||
using MaksIT.LetsEncryptServer.Services;
|
using MaksIT.LetsEncryptServer.Services;
|
||||||
using Models.LetsEncryptServer.CertsFlow.Requests;
|
using Models.LetsEncryptServer.CertsFlow.Requests;
|
||||||
|
using Models.LetsEncryptServer.Cache.Responses;
|
||||||
|
using MaksIT.LetsEncrypt.Entities;
|
||||||
|
|
||||||
namespace MaksIT.LetsEncryptServer.BackgroundServices {
|
namespace MaksIT.LetsEncryptServer.BackgroundServices {
|
||||||
public class AutoRenewal : BackgroundService {
|
public class AutoRenewal : BackgroundService {
|
||||||
@ -30,26 +32,23 @@ namespace MaksIT.LetsEncryptServer.BackgroundServices {
|
|||||||
while (!stoppingToken.IsCancellationRequested) {
|
while (!stoppingToken.IsCancellationRequested) {
|
||||||
_logger.LogInformation("Background service is running.");
|
_logger.LogInformation("Background service is running.");
|
||||||
|
|
||||||
var (accountsResponse, getAccountIdsResult) = await _cacheService.GetAccountsAsync();
|
var (accountsResponse, getAccountIdsResult) = await _cacheService.LoadAccountsFromCacheAsync();
|
||||||
if (!getAccountIdsResult.IsSuccess || accountsResponse == null) {
|
if (!getAccountIdsResult.IsSuccess || accountsResponse == null) {
|
||||||
LogErrors(getAccountIdsResult.Errors);
|
LogErrors(getAccountIdsResult.Errors);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
foreach (var accountId in accountsResponse.AccountIds) {
|
foreach (var account in accountsResponse) {
|
||||||
await ProcessAccountAsync(accountId);
|
await ProcessAccountAsync(account);
|
||||||
}
|
}
|
||||||
|
|
||||||
await Task.Delay(TimeSpan.FromMinutes(5), stoppingToken);
|
await Task.Delay(TimeSpan.FromMinutes(5), stoppingToken);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task<IDomainResult> ProcessAccountAsync(Guid accountId) {
|
private async Task<IDomainResult> ProcessAccountAsync(RegistrationCache cache) {
|
||||||
var (cache, loadResult) = await _cacheService.LoadFromCacheAsync(accountId);
|
|
||||||
if (!loadResult.IsSuccess || cache == null) {
|
|
||||||
LogErrors(loadResult.Errors);
|
|
||||||
return loadResult;
|
|
||||||
}
|
|
||||||
|
|
||||||
var hostnames = cache.GetHostsWithUpcomingSslExpiry();
|
var hostnames = cache.GetHostsWithUpcomingSslExpiry();
|
||||||
if (hostnames == null) {
|
if (hostnames == null) {
|
||||||
@ -63,11 +62,11 @@ namespace MaksIT.LetsEncryptServer.BackgroundServices {
|
|||||||
return IDomainResult.Success();
|
return IDomainResult.Success();
|
||||||
}
|
}
|
||||||
|
|
||||||
var renewResult = await RenewCertificatesForHostnames(accountId, cache.Contacts, hostnames);
|
var renewResult = await RenewCertificatesForHostnames(cache.AccountId, cache.Contacts, hostnames);
|
||||||
if (!renewResult.IsSuccess)
|
if (!renewResult.IsSuccess)
|
||||||
return renewResult;
|
return renewResult;
|
||||||
|
|
||||||
_logger.LogInformation($"Certificates renewed for account {accountId}");
|
_logger.LogInformation($"Certificates renewed for account {cache.AccountId}");
|
||||||
|
|
||||||
return IDomainResult.Success();
|
return IDomainResult.Success();
|
||||||
}
|
}
|
||||||
|
|||||||
@ -11,15 +11,12 @@ namespace MaksIT.LetsEncryptServer.Controllers;
|
|||||||
[ApiController]
|
[ApiController]
|
||||||
[Route("api/cache")]
|
[Route("api/cache")]
|
||||||
public class CacheController : ControllerBase {
|
public class CacheController : ControllerBase {
|
||||||
private readonly Configuration _appSettings;
|
|
||||||
private readonly ICacheRestService _cacheService;
|
private readonly ICacheRestService _cacheService;
|
||||||
|
|
||||||
public CacheController(
|
public CacheController(
|
||||||
IOptions<Configuration> appSettings,
|
|
||||||
ICacheService cacheService
|
ICacheService cacheService
|
||||||
) {
|
) {
|
||||||
_appSettings = appSettings.Value;
|
_cacheService = cacheService;
|
||||||
_cacheService = (ICacheRestService)cacheService;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
[HttpGet("accounts")]
|
[HttpGet("accounts")]
|
||||||
|
|||||||
@ -10,8 +10,9 @@ using Models.LetsEncryptServer.Cache.Responses;
|
|||||||
|
|
||||||
namespace MaksIT.LetsEncryptServer.Services;
|
namespace MaksIT.LetsEncryptServer.Services;
|
||||||
|
|
||||||
public interface ICacheService {
|
public interface ICacheInternalsService {
|
||||||
Task<(RegistrationCache?, IDomainResult)> LoadFromCacheAsync(Guid accountId);
|
Task<(RegistrationCache[]?, IDomainResult)> LoadAccountsFromCacheAsync();
|
||||||
|
Task<(RegistrationCache?, IDomainResult)> LoadAccountFromCacheAsync(Guid accountId);
|
||||||
Task<IDomainResult> SaveToCacheAsync(Guid accountId, RegistrationCache cache);
|
Task<IDomainResult> SaveToCacheAsync(Guid accountId, RegistrationCache cache);
|
||||||
Task<IDomainResult> DeleteFromCacheAsync(Guid accountId);
|
Task<IDomainResult> DeleteFromCacheAsync(Guid accountId);
|
||||||
}
|
}
|
||||||
@ -28,7 +29,9 @@ public interface ICacheRestService {
|
|||||||
Task<(GetHostnamesResponse?, IDomainResult)> GetHostnames(Guid accountId);
|
Task<(GetHostnamesResponse?, IDomainResult)> GetHostnames(Guid accountId);
|
||||||
}
|
}
|
||||||
|
|
||||||
public class CacheService : ICacheService, ICacheRestService, IDisposable {
|
public interface ICacheService : ICacheInternalsService, ICacheRestService {}
|
||||||
|
|
||||||
|
public class CacheService : ICacheService, IDisposable {
|
||||||
private readonly ILogger<CacheService> _logger;
|
private readonly ILogger<CacheService> _logger;
|
||||||
private readonly string _cacheDirectory;
|
private readonly string _cacheDirectory;
|
||||||
private readonly LockManager _lockManager;
|
private readonly LockManager _lockManager;
|
||||||
@ -36,7 +39,7 @@ public class CacheService : ICacheService, ICacheRestService, IDisposable {
|
|||||||
public CacheService(ILogger<CacheService> logger) {
|
public CacheService(ILogger<CacheService> logger) {
|
||||||
_logger = logger;
|
_logger = logger;
|
||||||
_cacheDirectory = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "cache");
|
_cacheDirectory = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "cache");
|
||||||
_lockManager = new LockManager(1, 1);
|
_lockManager = new LockManager();
|
||||||
|
|
||||||
if (!Directory.Exists(_cacheDirectory)) {
|
if (!Directory.Exists(_cacheDirectory)) {
|
||||||
Directory.CreateDirectory(_cacheDirectory);
|
Directory.CreateDirectory(_cacheDirectory);
|
||||||
@ -50,9 +53,39 @@ public class CacheService : ICacheService, ICacheRestService, IDisposable {
|
|||||||
return Path.Combine(_cacheDirectory, $"{accountId}.json");
|
return Path.Combine(_cacheDirectory, $"{accountId}.json");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private Guid[] GetCachedAccounts() {
|
||||||
|
return GetCacheFilesPaths().Select(x => Path.GetFileNameWithoutExtension(x).ToGuid()).Where(x => x != Guid.Empty).ToArray();
|
||||||
|
}
|
||||||
|
|
||||||
|
private string[] GetCacheFilesPaths() {
|
||||||
|
return Directory.GetFiles(_cacheDirectory);
|
||||||
|
}
|
||||||
|
|
||||||
#region Cache Operations
|
#region Cache Operations
|
||||||
|
|
||||||
public Task<(RegistrationCache?, IDomainResult)> LoadFromCacheAsync(Guid accountId) {
|
public async Task<(RegistrationCache[]?, IDomainResult)> LoadAccountsFromCacheAsync() {
|
||||||
|
return await _lockManager.ExecuteWithLockAsync(async () => {
|
||||||
|
var accountIds = GetCachedAccounts();
|
||||||
|
var cacheLoadTasks = accountIds.Select(accountId => LoadFromCacheInternalAsync(accountId)).ToList();
|
||||||
|
|
||||||
|
var caches = new List<RegistrationCache>();
|
||||||
|
foreach (var task in cacheLoadTasks) {
|
||||||
|
var (registrationCache, getRegistrationCacheResult) = await task;
|
||||||
|
if (!getRegistrationCacheResult.IsSuccess || registrationCache == null) {
|
||||||
|
// Depending on how you want to handle partial failures, you might want to return here
|
||||||
|
// or continue loading other caches. For now, let's continue.
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
caches.Add(registrationCache);
|
||||||
|
}
|
||||||
|
|
||||||
|
return IDomainResult.Success(caches.ToArray());
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public Task<(RegistrationCache?, IDomainResult)> LoadAccountFromCacheAsync(Guid accountId) {
|
||||||
return _lockManager.ExecuteWithLockAsync(() => LoadFromCacheInternalAsync(accountId));
|
return _lockManager.ExecuteWithLockAsync(() => LoadFromCacheInternalAsync(accountId));
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -110,8 +143,8 @@ public class CacheService : ICacheService, ICacheRestService, IDisposable {
|
|||||||
|
|
||||||
public async Task<(GetAccountResponse[]?, IDomainResult)> GetAccountsAsync() {
|
public async Task<(GetAccountResponse[]?, IDomainResult)> GetAccountsAsync() {
|
||||||
return await _lockManager.ExecuteWithLockAsync(async () => {
|
return await _lockManager.ExecuteWithLockAsync(async () => {
|
||||||
var cacheFiles = Directory.GetFiles(_cacheDirectory);
|
|
||||||
var accountIds = cacheFiles.Select(x => Path.GetFileNameWithoutExtension(x).ToGuid()).ToArray();
|
var accountIds = GetCachedAccounts();
|
||||||
var accounts = new List<GetAccountResponse>();
|
var accounts = new List<GetAccountResponse>();
|
||||||
|
|
||||||
foreach (var accountId in accountIds) {
|
foreach (var accountId in accountIds) {
|
||||||
@ -128,7 +161,7 @@ public class CacheService : ICacheService, ICacheRestService, IDisposable {
|
|||||||
|
|
||||||
public async Task<(GetAccountResponse?, IDomainResult)> GetAccountAsync(Guid accountId) {
|
public async Task<(GetAccountResponse?, IDomainResult)> GetAccountAsync(Guid accountId) {
|
||||||
return await _lockManager.ExecuteWithLockAsync(async () => {
|
return await _lockManager.ExecuteWithLockAsync(async () => {
|
||||||
var (cache, result) = await LoadFromCacheAsync(accountId);
|
var (cache, result) = await LoadAccountFromCacheAsync(accountId);
|
||||||
if (!result.IsSuccess || cache == null) {
|
if (!result.IsSuccess || cache == null) {
|
||||||
return (null, result);
|
return (null, result);
|
||||||
}
|
}
|
||||||
@ -145,7 +178,7 @@ public class CacheService : ICacheService, ICacheRestService, IDisposable {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public async Task<(GetAccountResponse?, IDomainResult)> PutAccountAsync(Guid accountId, PutAccountRequest requestData) {
|
public async Task<(GetAccountResponse?, IDomainResult)> PutAccountAsync(Guid accountId, PutAccountRequest requestData) {
|
||||||
var (cache, loadResult) = await LoadFromCacheAsync(accountId);
|
var (cache, loadResult) = await LoadAccountFromCacheAsync(accountId);
|
||||||
if (!loadResult.IsSuccess || cache == null) {
|
if (!loadResult.IsSuccess || cache == null) {
|
||||||
return (null, loadResult);
|
return (null, loadResult);
|
||||||
}
|
}
|
||||||
@ -162,7 +195,7 @@ public class CacheService : ICacheService, ICacheRestService, IDisposable {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public async Task<(GetAccountResponse?, IDomainResult)> PatchAccountAsync(Guid accountId, PatchAccountRequest requestData) {
|
public async Task<(GetAccountResponse?, IDomainResult)> PatchAccountAsync(Guid accountId, PatchAccountRequest requestData) {
|
||||||
var (cache, loadResult) = await LoadFromCacheAsync(accountId);
|
var (cache, loadResult) = await LoadAccountFromCacheAsync(accountId);
|
||||||
if (!loadResult.IsSuccess || cache == null) {
|
if (!loadResult.IsSuccess || cache == null) {
|
||||||
return (null, loadResult);
|
return (null, loadResult);
|
||||||
}
|
}
|
||||||
@ -209,7 +242,7 @@ public class CacheService : ICacheService, ICacheRestService, IDisposable {
|
|||||||
#region Contacts Operations
|
#region Contacts Operations
|
||||||
|
|
||||||
public async Task<(GetContactsResponse?, IDomainResult)> GetContactsAsync(Guid accountId) {
|
public async Task<(GetContactsResponse?, IDomainResult)> GetContactsAsync(Guid accountId) {
|
||||||
var (cache, loadResult) = await LoadFromCacheAsync(accountId);
|
var (cache, loadResult) = await LoadAccountFromCacheAsync(accountId);
|
||||||
if (!loadResult.IsSuccess || cache == null) {
|
if (!loadResult.IsSuccess || cache == null) {
|
||||||
return (null, loadResult);
|
return (null, loadResult);
|
||||||
}
|
}
|
||||||
@ -220,7 +253,7 @@ public class CacheService : ICacheService, ICacheRestService, IDisposable {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public async Task<(GetAccountResponse?, IDomainResult)> PutContactsAsync(Guid accountId, PutContactsRequest requestData) {
|
public async Task<(GetAccountResponse?, IDomainResult)> PutContactsAsync(Guid accountId, PutContactsRequest requestData) {
|
||||||
var (cache, loadResult) = await LoadFromCacheAsync(accountId);
|
var (cache, loadResult) = await LoadAccountFromCacheAsync(accountId);
|
||||||
if (!loadResult.IsSuccess || cache == null) {
|
if (!loadResult.IsSuccess || cache == null) {
|
||||||
return (null, loadResult);
|
return (null, loadResult);
|
||||||
}
|
}
|
||||||
@ -235,7 +268,7 @@ public class CacheService : ICacheService, ICacheRestService, IDisposable {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public async Task<(GetAccountResponse?, IDomainResult)> PatchContactsAsync(Guid accountId, PatchContactsRequest requestData) {
|
public async Task<(GetAccountResponse?, IDomainResult)> PatchContactsAsync(Guid accountId, PatchContactsRequest requestData) {
|
||||||
var (cache, loadResult) = await LoadFromCacheAsync(accountId);
|
var (cache, loadResult) = await LoadAccountFromCacheAsync(accountId);
|
||||||
if (!loadResult.IsSuccess || cache == null) {
|
if (!loadResult.IsSuccess || cache == null) {
|
||||||
return (null, loadResult);
|
return (null, loadResult);
|
||||||
}
|
}
|
||||||
@ -274,7 +307,7 @@ public class CacheService : ICacheService, ICacheRestService, IDisposable {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public async Task<IDomainResult> DeleteContactAsync(Guid accountId, int index) {
|
public async Task<IDomainResult> DeleteContactAsync(Guid accountId, int index) {
|
||||||
var (cache, loadResult) = await LoadFromCacheAsync(accountId);
|
var (cache, loadResult) = await LoadAccountFromCacheAsync(accountId);
|
||||||
if (!loadResult.IsSuccess || cache == null) {
|
if (!loadResult.IsSuccess || cache == null) {
|
||||||
return loadResult;
|
return loadResult;
|
||||||
}
|
}
|
||||||
@ -299,7 +332,7 @@ public class CacheService : ICacheService, ICacheRestService, IDisposable {
|
|||||||
#region Hostnames Operations
|
#region Hostnames Operations
|
||||||
|
|
||||||
public async Task<(GetHostnamesResponse?, IDomainResult)> GetHostnames(Guid accountId) {
|
public async Task<(GetHostnamesResponse?, IDomainResult)> GetHostnames(Guid accountId) {
|
||||||
var (cache, loadResult) = await LoadFromCacheAsync(accountId);
|
var (cache, loadResult) = await LoadAccountFromCacheAsync(accountId);
|
||||||
if (!loadResult.IsSuccess || cache?.CachedCerts == null) {
|
if (!loadResult.IsSuccess || cache?.CachedCerts == null) {
|
||||||
return (null, loadResult);
|
return (null, loadResult);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -83,7 +83,7 @@ public class CertsFlowService : ICertsFlowService {
|
|||||||
accountId = Guid.NewGuid();
|
accountId = Guid.NewGuid();
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
var (loadedCache, loadCaceResutl) = await _cacheService.LoadFromCacheAsync(accountId.Value);
|
var (loadedCache, loadCaceResutl) = await _cacheService.LoadAccountFromCacheAsync(accountId.Value);
|
||||||
if (!loadCaceResutl.IsSuccess || loadCaceResutl == null) {
|
if (!loadCaceResutl.IsSuccess || loadCaceResutl == null) {
|
||||||
accountId = Guid.NewGuid();
|
accountId = Guid.NewGuid();
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user