(feature): front end layout improve

This commit is contained in:
Maksym Sadovnychyy 2024-06-12 22:25:03 +02:00
parent 1fe7c0a3da
commit 2f87b455ca
20 changed files with 420 additions and 64 deletions

4
.gitignore vendored
View File

@ -262,5 +262,5 @@ __pycache__/
*.pyc *.pyc
**/*docker_compose/LetsEncryptServer/acme **/*docker-compose/LetsEncryptServer/acme
**/*docker_compose/LetsEncryptServer/cache **/*docker-compose/LetsEncryptServer/cache

View File

@ -0,0 +1,15 @@
enum ApiRoutes {
CACHE_GET_ACCOUNTS = `/api/Cache/GetAccounts`,
CACHE_GET_CONTACTS = `/api/Cache/GetContacts/{accountId}`,
CACHE_SET_CONTACTS = `/api/Cache/SetContacts/{accountId}`,
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}`
}

View File

@ -1,11 +1,12 @@
"use client"; // Add this line "use client"; // Add this line
import React, { FC, useState } from 'react'; import React, { FC, useState, useEffect, useRef } from 'react';
import './globals.css'; 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 { Metadata } from 'next'; import { Metadata } from 'next';
const metadata: Metadata = { const metadata: Metadata = {
@ -13,32 +14,71 @@ const metadata: Metadata = {
description: "Generated by create next app", description: "Generated by create next app",
}; };
const Layout = ({ children }: Readonly<{ const Layout: FC<{ children: React.ReactNode }> = ({ children }) => {
children: React.ReactNode;
}>) => {
const [isSidebarCollapsed, setIsSidebarCollapsed] = useState(false); const [isSidebarCollapsed, setIsSidebarCollapsed] = useState(false);
const [isOffCanvasOpen, setIsOffCanvasOpen] = useState(false); const [isManuallyCollapsed, setManuallyCollapsed] = useState(false);
const [isLoading, setIsLoading] = useState(true); // State to control the loader visibility
const init = useRef(false);
const toggleSidebar = () => { const toggleSidebar = () => {
setIsSidebarCollapsed(!isSidebarCollapsed); setIsSidebarCollapsed((prev) => !prev);
}; };
const manuallyToggleSidebar = () => {
setManuallyCollapsed((prev) => !prev);
toggleSidebar();
};
const handleResize = () => {
if (!isManuallyCollapsed) {
if (window.innerWidth <= 768) {
if (!isSidebarCollapsed) setIsSidebarCollapsed(true);
} else {
if (isSidebarCollapsed) setIsSidebarCollapsed(false);
}
} 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) {
setIsSidebarCollapsed(true);
}
}
};
useEffect(() => {
if (!init.current) {
handleResize(); // Set the initial state based on the current window width
init.current = true;
}
window.addEventListener('resize', handleResize);
setTimeout(() => setIsLoading(false), 2000); // Simulate loading for 2 seconds
return () => {
window.removeEventListener('resize', handleResize);
};
}, [isSidebarCollapsed, isManuallyCollapsed]);
const [isOffCanvasOpen, setIsOffCanvasOpen] = useState(false);
const toggleOffCanvas = () => { const toggleOffCanvas = () => {
setIsOffCanvasOpen(!isOffCanvasOpen); setIsOffCanvasOpen((prev) => !prev);
}; };
return ( return (
<html lang="en"> <html lang="en">
{/* <head>
<meta name="viewport" content="width=device-width, initial-scale=1" />
<link rel="icon" href="/favicon.ico" />
</head> */}
<body className="h-screen overflow-hidden"> <body className="h-screen overflow-hidden">
{isLoading && <Loader />} {/* Show loader if isLoading is true */}
<div className="flex h-full"> <div className="flex h-full">
<SideMenu isCollapsed={isSidebarCollapsed} toggleSidebar={toggleSidebar} /> <SideMenu isCollapsed={isSidebarCollapsed} toggleSidebar={manuallyToggleSidebar} />
<div className="flex flex-col flex-1"> <div className="flex flex-col flex-1">
<TopMenu onToggleOffCanvas={toggleOffCanvas} /> <TopMenu onToggleOffCanvas={toggleOffCanvas} />
<main className={`flex-1 p-4 transition-transform duration-300 ${isOffCanvasOpen ? 'transform translate-x-64' : ''}`}> <main className="flex-1 p-4 transition-transform duration-300">
{children} {children}
</main> </main>
<Footer /> <Footer />

View File

@ -3,7 +3,7 @@ import React from 'react';
const Footer = () => { const Footer = () => {
return ( return (
<footer className="bg-gray-900 text-white text-center p-4"> <footer className="bg-gray-900 text-white text-center p-4">
<p>&copy; 2024 Your Company</p> <p>&copy; {new Date().getFullYear()} MAKS-IT</p>
</footer> </footer>
); );
}; };

View File

@ -0,0 +1,13 @@
import React from 'react';
import './loader.css'; // Add your loader styles here
const Loader: React.FC = () => {
return (
<div className="loader-overlay">
<div className="spinner"></div>
<div className="loading-text">Loading...</div>
</div>
);
};
export default Loader;

View File

@ -0,0 +1,34 @@
.loader-overlay {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(0, 0, 0, 0.1); /* 10% transparent background */
display: flex;
justify-content: center;
align-items: center;
z-index: 9999;
flex-direction: column;
}
.spinner {
border: 8px solid rgba(255, 255, 255, 0.3);
border-top: 8px solid #3498db;
border-radius: 50%;
width: 80px;
height: 80px;
animation: spin 1s linear infinite;
}
.loading-text {
margin-top: 20px;
font-size: 1.2em;
color: #3498db;
}
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}

View File

@ -1,4 +1,4 @@
import React, { FC } from 'react'; import React, { FC, useEffect, useRef } from 'react';
import { FaHome, FaUser, FaCog, FaBars } from 'react-icons/fa'; import { FaHome, FaUser, FaCog, FaBars } from 'react-icons/fa';
interface SideMenuProps { interface SideMenuProps {
@ -7,12 +7,13 @@ interface SideMenuProps {
} }
const SideMenu: FC<SideMenuProps> = ({ isCollapsed, toggleSidebar }) => { 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`}>Logo</h1>
</div> </div>
<nav className="flex-1"> <nav className="flex-1">

View File

@ -17,7 +17,7 @@ const TopMenu: FC<TopMenuProps> = ({ onToggleOffCanvas }) => {
return ( return (
<header className="bg-gray-900 text-white flex items-center p-4"> <header className="bg-gray-900 text-white flex items-center p-4">
<nav className="flex-1 flex justify-between items-center"> <nav className="flex-1 flex justify-between items-center h-8">
<ul className="hidden md:flex space-x-4"> <ul className="hidden md:flex space-x-4">
<li className="hover:bg-gray-700 p-2 rounded"> <li className="hover:bg-gray-700 p-2 rounded">
<Link href="/">Home</Link> <Link href="/">Home</Link>
@ -29,9 +29,7 @@ const TopMenu: FC<TopMenuProps> = ({ onToggleOffCanvas }) => {
<Link href="/contact">Contact</Link> <Link href="/contact">Contact</Link>
</li> </li>
</ul> </ul>
<button onClick={toggleMenu} className="md:hidden">
<FaBars />
</button>
{isMenuOpen && ( {isMenuOpen && (
<ul className="absolute top-16 right-0 bg-gray-900 w-48 md:hidden"> <ul className="absolute top-16 right-0 bg-gray-900 w-48 md:hidden">
<li className="hover:bg-gray-700 p-2"> <li className="hover:bg-gray-700 p-2">
@ -49,6 +47,9 @@ const TopMenu: FC<TopMenuProps> = ({ onToggleOffCanvas }) => {
<button onClick={onToggleOffCanvas} className="ml-4"> <button onClick={onToggleOffCanvas} className="ml-4">
<FaCog /> <FaCog />
</button> </button>
<button onClick={toggleMenu} className="md:hidden">
<FaBars />
</button>
</header> </header>
); );
}; };

View File

@ -0,0 +1,12 @@
import { HttpService } from "./services/HttpService";
const httpService = new HttpService();
httpService.addRequestInterceptor(xhr => {
});
httpService.addResponseInterceptor(response => {
return response;
});

View File

@ -8,10 +8,12 @@
"name": "my-nextjs-app", "name": "my-nextjs-app",
"version": "0.1.0", "version": "0.1.0",
"dependencies": { "dependencies": {
"@reduxjs/toolkit": "^2.2.5",
"next": "14.2.3", "next": "14.2.3",
"react": "^18", "react": "^18",
"react-dom": "^18", "react-dom": "^18",
"react-icons": "^5.2.1" "react-icons": "^5.2.1",
"react-redux": "^9.1.2"
}, },
"devDependencies": { "devDependencies": {
"@types/node": "^20", "@types/node": "^20",
@ -423,6 +425,29 @@
"node": ">=14" "node": ">=14"
} }
}, },
"node_modules/@reduxjs/toolkit": {
"version": "2.2.5",
"resolved": "https://registry.npmjs.org/@reduxjs/toolkit/-/toolkit-2.2.5.tgz",
"integrity": "sha512-aeFA/s5NCG7NoJe/MhmwREJxRkDs0ZaSqt0MxhWUrwCf1UQXpwR87RROJEql0uAkLI6U7snBOYOcKw83ew3FPg==",
"dependencies": {
"immer": "^10.0.3",
"redux": "^5.0.1",
"redux-thunk": "^3.1.0",
"reselect": "^5.1.0"
},
"peerDependencies": {
"react": "^16.9.0 || ^17.0.0 || ^18",
"react-redux": "^7.2.1 || ^8.1.3 || ^9.0.0"
},
"peerDependenciesMeta": {
"react": {
"optional": true
},
"react-redux": {
"optional": true
}
}
},
"node_modules/@rushstack/eslint-patch": { "node_modules/@rushstack/eslint-patch": {
"version": "1.10.3", "version": "1.10.3",
"resolved": "https://registry.npmjs.org/@rushstack/eslint-patch/-/eslint-patch-1.10.3.tgz", "resolved": "https://registry.npmjs.org/@rushstack/eslint-patch/-/eslint-patch-1.10.3.tgz",
@ -462,13 +487,13 @@
"version": "15.7.12", "version": "15.7.12",
"resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.12.tgz", "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.12.tgz",
"integrity": "sha512-5zvhXYtRNRluoE/jAp4GVsSduVUzNWKkOZrCDBWYtE7biZywwdC2AcEzg+cSMLFRfVgeAFqpfNabiPjxFddV1Q==", "integrity": "sha512-5zvhXYtRNRluoE/jAp4GVsSduVUzNWKkOZrCDBWYtE7biZywwdC2AcEzg+cSMLFRfVgeAFqpfNabiPjxFddV1Q==",
"dev": true "devOptional": true
}, },
"node_modules/@types/react": { "node_modules/@types/react": {
"version": "18.3.3", "version": "18.3.3",
"resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.3.tgz", "resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.3.tgz",
"integrity": "sha512-hti/R0pS0q1/xx+TsI73XIqk26eBsISZ2R0wUijXIngRK9R/e7Xw/cXVxQK7R5JjW+SV4zGcn5hXjudkN/pLIw==", "integrity": "sha512-hti/R0pS0q1/xx+TsI73XIqk26eBsISZ2R0wUijXIngRK9R/e7Xw/cXVxQK7R5JjW+SV4zGcn5hXjudkN/pLIw==",
"dev": true, "devOptional": true,
"dependencies": { "dependencies": {
"@types/prop-types": "*", "@types/prop-types": "*",
"csstype": "^3.0.2" "csstype": "^3.0.2"
@ -483,6 +508,11 @@
"@types/react": "*" "@types/react": "*"
} }
}, },
"node_modules/@types/use-sync-external-store": {
"version": "0.0.3",
"resolved": "https://registry.npmjs.org/@types/use-sync-external-store/-/use-sync-external-store-0.0.3.tgz",
"integrity": "sha512-EwmlvuaxPNej9+T4v5AuBPJa2x2UOJVdjCtDHgcDqitUeOtjnJKJ+apYjVcAoBEMjKW1VVFGZLUb5+qqa09XFA=="
},
"node_modules/@typescript-eslint/parser": { "node_modules/@typescript-eslint/parser": {
"version": "7.2.0", "version": "7.2.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-7.2.0.tgz", "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-7.2.0.tgz",
@ -1154,7 +1184,7 @@
"version": "3.1.3", "version": "3.1.3",
"resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz", "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz",
"integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==", "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==",
"dev": true "devOptional": true
}, },
"node_modules/damerau-levenshtein": { "node_modules/damerau-levenshtein": {
"version": "1.0.8", "version": "1.0.8",
@ -2388,6 +2418,15 @@
"node": ">= 4" "node": ">= 4"
} }
}, },
"node_modules/immer": {
"version": "10.1.1",
"resolved": "https://registry.npmjs.org/immer/-/immer-10.1.1.tgz",
"integrity": "sha512-s2MPrmjovJcoMaHtx6K11Ra7oD05NT97w1IC5zpMkT6Atjr7H8LjaDd81iIxUYpMKSRRNMJE703M1Fhr/TctHw==",
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/immer"
}
},
"node_modules/import-fresh": { "node_modules/import-fresh": {
"version": "3.3.0", "version": "3.3.0",
"resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz", "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz",
@ -3737,6 +3776,28 @@
"integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==", "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==",
"dev": true "dev": true
}, },
"node_modules/react-redux": {
"version": "9.1.2",
"resolved": "https://registry.npmjs.org/react-redux/-/react-redux-9.1.2.tgz",
"integrity": "sha512-0OA4dhM1W48l3uzmv6B7TXPCGmokUU4p1M44DGN2/D9a1FjVPukVjER1PcPX97jIg6aUeLq1XJo1IpfbgULn0w==",
"dependencies": {
"@types/use-sync-external-store": "^0.0.3",
"use-sync-external-store": "^1.0.0"
},
"peerDependencies": {
"@types/react": "^18.2.25",
"react": "^18.0",
"redux": "^5.0.0"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
},
"redux": {
"optional": true
}
}
},
"node_modules/read-cache": { "node_modules/read-cache": {
"version": "1.0.0", "version": "1.0.0",
"resolved": "https://registry.npmjs.org/read-cache/-/read-cache-1.0.0.tgz", "resolved": "https://registry.npmjs.org/read-cache/-/read-cache-1.0.0.tgz",
@ -3758,6 +3819,19 @@
"node": ">=8.10.0" "node": ">=8.10.0"
} }
}, },
"node_modules/redux": {
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/redux/-/redux-5.0.1.tgz",
"integrity": "sha512-M9/ELqF6fy8FwmkpnF0S3YKOqMyoWJ4+CS5Efg2ct3oY9daQvd/Pc71FpGZsVsbl3Cpb+IIcjBDUnnyBdQbq4w=="
},
"node_modules/redux-thunk": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/redux-thunk/-/redux-thunk-3.1.0.tgz",
"integrity": "sha512-NW2r5T6ksUKXCabzhL9z+h206HQw/NJkcLm1GPImRQ8IzfXwRGqjVhKJGauHirT0DAuyy6hjdnMZaRoAcy0Klw==",
"peerDependencies": {
"redux": "^5.0.0"
}
},
"node_modules/reflect.getprototypeof": { "node_modules/reflect.getprototypeof": {
"version": "1.0.6", "version": "1.0.6",
"resolved": "https://registry.npmjs.org/reflect.getprototypeof/-/reflect.getprototypeof-1.0.6.tgz", "resolved": "https://registry.npmjs.org/reflect.getprototypeof/-/reflect.getprototypeof-1.0.6.tgz",
@ -3803,6 +3877,11 @@
"url": "https://github.com/sponsors/ljharb" "url": "https://github.com/sponsors/ljharb"
} }
}, },
"node_modules/reselect": {
"version": "5.1.1",
"resolved": "https://registry.npmjs.org/reselect/-/reselect-5.1.1.tgz",
"integrity": "sha512-K/BG6eIky/SBpzfHZv/dd+9JBFiS4SWV7FIujVyJRux6e45+73RaUHXLmIR1f7WOMaQ0U1km6qwklRQxpJJY0w=="
},
"node_modules/resolve": { "node_modules/resolve": {
"version": "1.22.8", "version": "1.22.8",
"resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.8.tgz", "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.8.tgz",
@ -4585,6 +4664,14 @@
"punycode": "^2.1.0" "punycode": "^2.1.0"
} }
}, },
"node_modules/use-sync-external-store": {
"version": "1.2.2",
"resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.2.2.tgz",
"integrity": "sha512-PElTlVMwpblvbNqQ82d2n6RjStvdSoNe9FG28kNfz3WiXilJm4DdNkEzRhCZuIDwY8U08WVihhGR5iRqAwfDiw==",
"peerDependencies": {
"react": "^16.8.0 || ^17.0.0 || ^18.0.0"
}
},
"node_modules/util-deprecate": { "node_modules/util-deprecate": {
"version": "1.0.2", "version": "1.0.2",
"resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",

View File

@ -9,10 +9,12 @@
"lint": "next lint" "lint": "next lint"
}, },
"dependencies": { "dependencies": {
"@reduxjs/toolkit": "^2.2.5",
"next": "14.2.3", "next": "14.2.3",
"react": "^18", "react": "^18",
"react-dom": "^18", "react-dom": "^18",
"react-icons": "^5.2.1" "react-icons": "^5.2.1",
"react-redux": "^9.1.2"
}, },
"devDependencies": { "devDependencies": {
"@types/node": "^20", "@types/node": "^20",

View File

@ -0,0 +1,122 @@
interface RequestInterceptor {
(req: XMLHttpRequest): void;
}
interface ResponseInterceptor<T> {
(response: T): T;
}
interface ProblemDetails {
Title: string;
Detail: string | null;
Status: number;
}
class HttpService {
private requestInterceptors: Array<RequestInterceptor> = [];
private responseInterceptors: Array<ResponseInterceptor<any>> = [];
private request<TResponse>(method: string, url: string, data?: any): Promise<TResponse> {
return new Promise((resolve, reject) => {
const xhr = new XMLHttpRequest();
xhr.open(method, url);
// 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;
}
});
// Set Content-Type header for JSON data
if (data && typeof data !== 'string') {
xhr.setRequestHeader('Content-Type', 'application/json');
}
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;
}
// 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;
}
resolve(response);
} else {
const problemDetails: ProblemDetails = {
Title: xhr.statusText,
Detail: xhr.responseText,
Status: xhr.status
};
reject(problemDetails);
}
};
xhr.onerror = () => {
const problemDetails: ProblemDetails = {
Title: 'Network Error',
Detail: null,
Status: 0
};
reject(problemDetails);
};
xhr.send(data ? JSON.stringify(data) : null);
});
}
public get<TResponse>(url: string): Promise<TResponse | ProblemDetails> {
return this.request<TResponse>('GET', url);
}
public post<TRequest, TResponse>(url: string, data: TRequest): Promise<TResponse | ProblemDetails> {
return this.request<TResponse>('POST', url, data);
}
public put<TRequest, TResponse>(url: string, data: TRequest): Promise<TResponse | ProblemDetails> {
return this.request<TResponse>('PUT', url, data);
}
public delete<TResponse>(url: string): Promise<TResponse | ProblemDetails> {
return this.request<TResponse>('DELETE', url);
}
public addRequestInterceptor(interceptor: RequestInterceptor): void {
this.requestInterceptors.push(interceptor);
}
public addResponseInterceptor<TResponse>(interceptor: ResponseInterceptor<TResponse | ProblemDetails>): void {
this.responseInterceptors.push(interceptor);
}
}
export {
HttpService
};

View File

@ -27,6 +27,12 @@ public class CacheController {
} }
[HttpGet("[action]")]
public async Task<IActionResult> GetAccounts() {
var result = await _cacheService.ListCachedAccountsAsync();
return result.ToActionResult();
}
[HttpGet("[action]/{accountId}")] [HttpGet("[action]/{accountId}")]
public async Task<IActionResult> GetContacts(Guid accountId) { public async Task<IActionResult> GetContacts(Guid accountId) {
var result = await _cacheService.GetContactsAsync(accountId); var result = await _cacheService.GetContactsAsync(accountId);

View File

@ -0,0 +1,32 @@
using System.Net;
namespace MaksIT.LetsEncryptServer.Middlewares {
public class GlobalExceptionMiddleware {
private readonly RequestDelegate _next;
private readonly ILogger<GlobalExceptionMiddleware> _logger;
public GlobalExceptionMiddleware(RequestDelegate next, ILogger<GlobalExceptionMiddleware> logger) {
_next = next;
_logger = logger;
}
public async Task InvokeAsync(HttpContext context) {
try {
await _next(context);
}
catch (Exception ex) {
_logger.LogError(ex, "An unhandled exception occurred.");
await HandleExceptionAsync(context);
}
}
private static Task HandleExceptionAsync(HttpContext context) {
context.Response.StatusCode = (int)HttpStatusCode.InternalServerError;
context.Response.ContentType = "application/json";
var response = new { message = "An error occurred while processing your request." };
return context.Response.WriteAsJsonAsync(response);
}
}
}

View File

@ -2,6 +2,7 @@ using MaksIT.LetsEncryptServer;
using MaksIT.LetsEncrypt.Services; using MaksIT.LetsEncrypt.Services;
using MaksIT.LetsEncryptServer.Services; using MaksIT.LetsEncryptServer.Services;
using MaksIT.LetsEncryptServer.BackgroundServices; using MaksIT.LetsEncryptServer.BackgroundServices;
using MaksIT.LetsEncryptServer.Middlewares;
var builder = WebApplication.CreateBuilder(args); var builder = WebApplication.CreateBuilder(args);
@ -23,6 +24,8 @@ builder.Services.AddControllers();
builder.Services.AddEndpointsApiExplorer(); builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen(); builder.Services.AddSwaggerGen();
builder.Services.AddCors();
builder.Services.AddMemoryCache(); builder.Services.AddMemoryCache();
builder.Services.AddHttpClient<ILetsEncryptService, LetsEncryptService>(); builder.Services.AddHttpClient<ILetsEncryptService, LetsEncryptService>();
@ -37,7 +40,13 @@ var app = builder.Build();
if (app.Environment.IsDevelopment()) { if (app.Environment.IsDevelopment()) {
app.UseSwagger(); app.UseSwagger();
app.UseSwaggerUI(); app.UseSwaggerUI();
app.UseCors(builder => builder.AllowAnyOrigin().AllowAnyMethod().AllowAnyHeader());
} }
else {
// app.UseMiddleware<GlobalExceptionMiddleware>();
}
app.UseMiddleware<GlobalExceptionMiddleware>();
app.UseAuthorization(); app.UseAuthorization();

View File

@ -1,18 +1,18 @@
version: '3.9' version: '3.9'
services: services:
haproxy: # haproxy:
ports: # ports:
- "8080:8080" # - "8080:8080"
volumes: # volumes:
- ./docker-compose/haproxy/haproxy.cfg:/usr/local/etc/haproxy/haproxy.cfg:ro # - ./docker-compose/haproxy/haproxy.cfg:/usr/local/etc/haproxy/haproxy.cfg:ro
depends_on: # depends_on:
- letsencryptapp # - letsencryptapp
- letsencryptserver # - letsencryptserver
letsencryptapp: # letsencryptapp:
ports: # ports:
- "3000:3000" # - "3000:3000"
letsencryptserver: letsencryptserver:
environment: environment:

View File

@ -2,14 +2,14 @@ version: '3.9'
services: services:
haproxy: # haproxy:
image: haproxy:3.0.0-alpine # image: haproxy:3.0.0-alpine
letsencryptapp: # letsencryptapp:
image: ${DOCKER_REGISTRY-}letsencryptapp # image: ${DOCKER_REGISTRY-}letsencryptapp
build: # build:
context: . # context: .
dockerfile: ClientApp/Dockerfile # dockerfile: ClientApp/Dockerfile
letsencryptserver: letsencryptserver:
image: ${DOCKER_REGISTRY-}letsencryptserver image: ${DOCKER_REGISTRY-}letsencryptserver

View File

@ -1,16 +0,0 @@
server {
listen 3000;
server_name localhost;
location / {
root /usr/share/nginx/html;
index index.html index.htm;
try_files $uri $uri/ /index.html;
}
error_page 500 502 503 504 /50x.html;
location = /50x.html {
root /usr/share/nginx/html;
}
}

View File

@ -1 +0,0 @@
VTpgCMVyk7RMm0qKEURrgcfHI5Y0RKRWlF0Up1y1xMs.CmDayuKv1cGaB2xr6W5cZk_Jqbyonzm29xtXVNgBMAQ

View File

@ -1 +0,0 @@
sf6oq1qazlokoWqGJam03udFEYOanTus2DmShjnfAcw.CmDayuKv1cGaB2xr6W5cZk_Jqbyonzm29xtXVNgBMAQ