You've already forked nginx-proxy-manager
							
							
				mirror of
				https://github.com/NginxProxyManager/nginx-proxy-manager.git
				synced 2025-10-30 18:05:34 +03:00 
			
		
		
		
	React
This commit is contained in:
		
							
								
								
									
										7
									
								
								frontend/src/App.css
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										7
									
								
								frontend/src/App.css
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,7 @@ | ||||
| .modal-backdrop { | ||||
| 	--tblr-backdrop-opacity: 0.8 !important; | ||||
| } | ||||
|  | ||||
| .domain-name { | ||||
| 	font-family: monospace; | ||||
| } | ||||
							
								
								
									
										28
									
								
								frontend/src/App.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										28
									
								
								frontend/src/App.tsx
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,28 @@ | ||||
| import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; | ||||
| import { ReactQueryDevtools } from "@tanstack/react-query-devtools"; | ||||
| import { RawIntlProvider } from "react-intl"; | ||||
| import { AuthProvider, LocaleProvider, ThemeProvider } from "src/context"; | ||||
| import { intl } from "src/locale"; | ||||
| import Router from "src/Router.tsx"; | ||||
|  | ||||
| // Create a client | ||||
| const queryClient = new QueryClient(); | ||||
|  | ||||
| function App() { | ||||
| 	return ( | ||||
| 		<RawIntlProvider value={intl}> | ||||
| 			<LocaleProvider> | ||||
| 				<ThemeProvider> | ||||
| 					<QueryClientProvider client={queryClient}> | ||||
| 						<AuthProvider> | ||||
| 							<Router /> | ||||
| 						</AuthProvider> | ||||
| 						<ReactQueryDevtools buttonPosition="bottom-right" position="right" /> | ||||
| 					</QueryClientProvider> | ||||
| 				</ThemeProvider> | ||||
| 			</LocaleProvider> | ||||
| 		</RawIntlProvider> | ||||
| 	); | ||||
| } | ||||
|  | ||||
| export default App; | ||||
							
								
								
									
										78
									
								
								frontend/src/Router.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										78
									
								
								frontend/src/Router.tsx
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,78 @@ | ||||
| import { lazy, Suspense } from "react"; | ||||
| import { BrowserRouter, Route, Routes } from "react-router-dom"; | ||||
| import { | ||||
| 	ErrorNotFound, | ||||
| 	LoadingPage, | ||||
| 	Page, | ||||
| 	SiteContainer, | ||||
| 	SiteFooter, | ||||
| 	SiteHeader, | ||||
| 	SiteMenu, | ||||
| 	Unhealthy, | ||||
| } from "src/components"; | ||||
| import { useAuthState } from "src/context"; | ||||
| import { useHealth } from "src/hooks"; | ||||
|  | ||||
| const Dashboard = lazy(() => import("src/pages/Dashboard")); | ||||
| const Login = lazy(() => import("src/pages/Login")); | ||||
| const Settings = lazy(() => import("src/pages/Settings")); | ||||
| const Certificates = lazy(() => import("src/pages/Certificates")); | ||||
| const Access = lazy(() => import("src/pages/Access")); | ||||
| const AuditLog = lazy(() => import("src/pages/AuditLog")); | ||||
| const Users = lazy(() => import("src/pages/Users")); | ||||
| const ProxyHosts = lazy(() => import("src/pages/Nginx/ProxyHosts")); | ||||
| const RedirectionHosts = lazy(() => import("src/pages/Nginx/RedirectionHosts")); | ||||
| const DeadHosts = lazy(() => import("src/pages/Nginx/DeadHosts")); | ||||
| const Streams = lazy(() => import("src/pages/Nginx/Streams")); | ||||
|  | ||||
| function Router() { | ||||
| 	const health = useHealth(); | ||||
| 	const { authenticated } = useAuthState(); | ||||
|  | ||||
| 	if (health.isLoading) { | ||||
| 		return <LoadingPage />; | ||||
| 	} | ||||
|  | ||||
| 	if (health.isError || health.data?.status !== "OK") { | ||||
| 		return <Unhealthy />; | ||||
| 	} | ||||
|  | ||||
| 	if (!authenticated) { | ||||
| 		return ( | ||||
| 			<Suspense fallback={<LoadingPage />}> | ||||
| 				<Login /> | ||||
| 			</Suspense> | ||||
| 		); | ||||
| 	} | ||||
|  | ||||
| 	return ( | ||||
| 		<BrowserRouter> | ||||
| 			<Page> | ||||
| 				<div> | ||||
| 					<SiteHeader /> | ||||
| 					<SiteMenu /> | ||||
| 				</div> | ||||
| 				<SiteContainer> | ||||
| 					<Suspense fallback={<LoadingPage noLogo />}> | ||||
| 						<Routes> | ||||
| 							<Route path="*" element={<ErrorNotFound />} /> | ||||
| 							<Route path="/certificates" element={<Certificates />} /> | ||||
| 							<Route path="/access" element={<Access />} /> | ||||
| 							<Route path="/audit-log" element={<AuditLog />} /> | ||||
| 							<Route path="/settings" element={<Settings />} /> | ||||
| 							<Route path="/users" element={<Users />} /> | ||||
| 							<Route path="/nginx/proxy" element={<ProxyHosts />} /> | ||||
| 							<Route path="/nginx/redirection" element={<RedirectionHosts />} /> | ||||
| 							<Route path="/nginx/404" element={<DeadHosts />} /> | ||||
| 							<Route path="/nginx/stream" element={<Streams />} /> | ||||
| 							<Route path="/" element={<Dashboard />} /> | ||||
| 						</Routes> | ||||
| 					</Suspense> | ||||
| 				</SiteContainer> | ||||
| 				<SiteFooter /> | ||||
| 			</Page> | ||||
| 		</BrowserRouter> | ||||
| 	); | ||||
| } | ||||
|  | ||||
| export default Router; | ||||
							
								
								
									
										152
									
								
								frontend/src/api/backend/base.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										152
									
								
								frontend/src/api/backend/base.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,152 @@ | ||||
| import { QueryClient } from "@tanstack/react-query"; | ||||
| import { camelizeKeys, decamelize, decamelizeKeys } from "humps"; | ||||
| import queryString, { type StringifiableRecord } from "query-string"; | ||||
| import AuthStore from "src/modules/AuthStore"; | ||||
|  | ||||
| const queryClient = new QueryClient(); | ||||
| const contentTypeHeader = "Content-Type"; | ||||
|  | ||||
| interface BuildUrlArgs { | ||||
| 	url: string; | ||||
| 	params?: StringifiableRecord; | ||||
| } | ||||
|  | ||||
| function decamelizeParams(params?: StringifiableRecord): StringifiableRecord | undefined { | ||||
| 	if (!params) { | ||||
| 		return undefined; | ||||
| 	} | ||||
| 	const result: StringifiableRecord = {}; | ||||
| 	for (const [key, value] of Object.entries(params)) { | ||||
| 		result[decamelize(key)] = value; | ||||
| 	} | ||||
|  | ||||
| 	return result; | ||||
| } | ||||
|  | ||||
| function buildUrl({ url, params }: BuildUrlArgs) { | ||||
| 	const endpoint = url.replace(/^\/|\/$/g, ""); | ||||
| 	const baseUrl = `/api/${endpoint}`; | ||||
| 	const apiUrl = queryString.stringifyUrl({ | ||||
| 		url: baseUrl, | ||||
| 		query: decamelizeParams(params), | ||||
| 	}); | ||||
| 	return apiUrl; | ||||
| } | ||||
|  | ||||
| function buildAuthHeader(): Record<string, string> | undefined { | ||||
| 	if (AuthStore.token) { | ||||
| 		return { Authorization: `Bearer ${AuthStore.token.token}` }; | ||||
| 	} | ||||
| 	return {}; | ||||
| } | ||||
|  | ||||
| function buildBody(data?: Record<string, any>): string | undefined { | ||||
| 	if (data) { | ||||
| 		return JSON.stringify(decamelizeKeys(data)); | ||||
| 	} | ||||
| } | ||||
|  | ||||
| async function processResponse(response: Response) { | ||||
| 	const payload = await response.json(); | ||||
| 	if (!response.ok) { | ||||
| 		if (response.status === 401) { | ||||
| 			// Force logout user and reload the page if Unauthorized | ||||
| 			AuthStore.clear(); | ||||
| 			queryClient.clear(); | ||||
| 			window.location.reload(); | ||||
| 		} | ||||
| 		throw new Error( | ||||
| 			typeof payload.error.messageI18n !== "undefined" ? payload.error.messageI18n : payload.error.message, | ||||
| 		); | ||||
| 	} | ||||
| 	return camelizeKeys(payload) as any; | ||||
| } | ||||
|  | ||||
| interface GetArgs { | ||||
| 	url: string; | ||||
| 	params?: queryString.StringifiableRecord; | ||||
| } | ||||
|  | ||||
| async function baseGet({ url, params }: GetArgs, abortController?: AbortController) { | ||||
| 	const apiUrl = buildUrl({ url, params }); | ||||
| 	const method = "GET"; | ||||
| 	const headers = buildAuthHeader(); | ||||
| 	const signal = abortController?.signal; | ||||
| 	const response = await fetch(apiUrl, { method, headers, signal }); | ||||
| 	return response; | ||||
| } | ||||
|  | ||||
| export async function get(args: GetArgs, abortController?: AbortController) { | ||||
| 	return processResponse(await baseGet(args, abortController)); | ||||
| } | ||||
|  | ||||
| export async function download(args: GetArgs, abortController?: AbortController) { | ||||
| 	return (await baseGet(args, abortController)).text(); | ||||
| } | ||||
|  | ||||
| interface PostArgs { | ||||
| 	url: string; | ||||
| 	params?: queryString.StringifiableRecord; | ||||
| 	data?: any; | ||||
| } | ||||
|  | ||||
| export async function post({ url, params, data }: PostArgs, abortController?: AbortController) { | ||||
| 	const apiUrl = buildUrl({ url, params }); | ||||
| 	const method = "POST"; | ||||
|  | ||||
| 	let headers = { | ||||
| 		...buildAuthHeader(), | ||||
| 	}; | ||||
|  | ||||
| 	let body: string | FormData | undefined; | ||||
| 	// Check if the data is an instance of FormData | ||||
| 	// If data is FormData, let the browser set the Content-Type header | ||||
| 	if (data instanceof FormData) { | ||||
| 		body = data; | ||||
| 	} else { | ||||
| 		// If data is JSON, set the Content-Type header to 'application/json' | ||||
| 		headers = { | ||||
| 			...headers, | ||||
| 			[contentTypeHeader]: "application/json", | ||||
| 		}; | ||||
| 		body = buildBody(data); | ||||
| 	} | ||||
|  | ||||
| 	const signal = abortController?.signal; | ||||
| 	const response = await fetch(apiUrl, { method, headers, body, signal }); | ||||
| 	return processResponse(response); | ||||
| } | ||||
|  | ||||
| interface PutArgs { | ||||
| 	url: string; | ||||
| 	params?: queryString.StringifiableRecord; | ||||
| 	data?: Record<string, unknown>; | ||||
| } | ||||
| export async function put({ url, params, data }: PutArgs, abortController?: AbortController) { | ||||
| 	const apiUrl = buildUrl({ url, params }); | ||||
| 	const method = "PUT"; | ||||
| 	const headers = { | ||||
| 		...buildAuthHeader(), | ||||
| 		[contentTypeHeader]: "application/json", | ||||
| 	}; | ||||
| 	const signal = abortController?.signal; | ||||
| 	const body = buildBody(data); | ||||
| 	const response = await fetch(apiUrl, { method, headers, body, signal }); | ||||
| 	return processResponse(response); | ||||
| } | ||||
|  | ||||
| interface DeleteArgs { | ||||
| 	url: string; | ||||
| 	params?: queryString.StringifiableRecord; | ||||
| } | ||||
| export async function del({ url, params }: DeleteArgs, abortController?: AbortController) { | ||||
| 	const apiUrl = buildUrl({ url, params }); | ||||
| 	const method = "DELETE"; | ||||
| 	const headers = { | ||||
| 		...buildAuthHeader(), | ||||
| 		[contentTypeHeader]: "application/json", | ||||
| 	}; | ||||
| 	const signal = abortController?.signal; | ||||
| 	const response = await fetch(apiUrl, { method, headers, signal }); | ||||
| 	return processResponse(response); | ||||
| } | ||||
							
								
								
									
										13
									
								
								frontend/src/api/backend/createAccessList.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										13
									
								
								frontend/src/api/backend/createAccessList.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,13 @@ | ||||
| import * as api from "./base"; | ||||
| import type { AccessList } from "./models"; | ||||
|  | ||||
| export async function createAccessList(item: AccessList, abortController?: AbortController): Promise<AccessList> { | ||||
| 	return await api.post( | ||||
| 		{ | ||||
| 			url: "/nginx/access-lists", | ||||
| 			// todo: only use whitelist of fields for this data | ||||
| 			data: item, | ||||
| 		}, | ||||
| 		abortController, | ||||
| 	); | ||||
| } | ||||
							
								
								
									
										13
									
								
								frontend/src/api/backend/createCertificate.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										13
									
								
								frontend/src/api/backend/createCertificate.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,13 @@ | ||||
| import * as api from "./base"; | ||||
| import type { Certificate } from "./models"; | ||||
|  | ||||
| export async function createCertificate(item: Certificate, abortController?: AbortController): Promise<Certificate> { | ||||
| 	return await api.post( | ||||
| 		{ | ||||
| 			url: "/nginx/certificates", | ||||
| 			// todo: only use whitelist of fields for this data | ||||
| 			data: item, | ||||
| 		}, | ||||
| 		abortController, | ||||
| 	); | ||||
| } | ||||
							
								
								
									
										13
									
								
								frontend/src/api/backend/createDeadHost.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										13
									
								
								frontend/src/api/backend/createDeadHost.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,13 @@ | ||||
| import * as api from "./base"; | ||||
| import type { DeadHost } from "./models"; | ||||
|  | ||||
| export async function createDeadHost(item: DeadHost, abortController?: AbortController): Promise<DeadHost> { | ||||
| 	return await api.post( | ||||
| 		{ | ||||
| 			url: "/nginx/dead-hosts", | ||||
| 			// todo: only use whitelist of fields for this data | ||||
| 			data: item, | ||||
| 		}, | ||||
| 		abortController, | ||||
| 	); | ||||
| } | ||||
							
								
								
									
										13
									
								
								frontend/src/api/backend/createProxyHost.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										13
									
								
								frontend/src/api/backend/createProxyHost.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,13 @@ | ||||
| import * as api from "./base"; | ||||
| import type { ProxyHost } from "./models"; | ||||
|  | ||||
| export async function createProxyHost(item: ProxyHost, abortController?: AbortController): Promise<ProxyHost> { | ||||
| 	return await api.post( | ||||
| 		{ | ||||
| 			url: "/nginx/proxy-hosts", | ||||
| 			// todo: only use whitelist of fields for this data | ||||
| 			data: item, | ||||
| 		}, | ||||
| 		abortController, | ||||
| 	); | ||||
| } | ||||
							
								
								
									
										16
									
								
								frontend/src/api/backend/createRedirectionHost.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										16
									
								
								frontend/src/api/backend/createRedirectionHost.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,16 @@ | ||||
| import * as api from "./base"; | ||||
| import type { RedirectionHost } from "./models"; | ||||
|  | ||||
| export async function createRedirectionHost( | ||||
| 	item: RedirectionHost, | ||||
| 	abortController?: AbortController, | ||||
| ): Promise<RedirectionHost> { | ||||
| 	return await api.post( | ||||
| 		{ | ||||
| 			url: "/nginx/redirection-hosts", | ||||
| 			// todo: only use whitelist of fields for this data | ||||
| 			data: item, | ||||
| 		}, | ||||
| 		abortController, | ||||
| 	); | ||||
| } | ||||
							
								
								
									
										13
									
								
								frontend/src/api/backend/createStream.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										13
									
								
								frontend/src/api/backend/createStream.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,13 @@ | ||||
| import * as api from "./base"; | ||||
| import type { Stream } from "./models"; | ||||
|  | ||||
| export async function createStream(item: Stream, abortController?: AbortController): Promise<Stream> { | ||||
| 	return await api.post( | ||||
| 		{ | ||||
| 			url: "/nginx/streams", | ||||
| 			// todo: only use whitelist of fields for this data | ||||
| 			data: item, | ||||
| 		}, | ||||
| 		abortController, | ||||
| 	); | ||||
| } | ||||
							
								
								
									
										13
									
								
								frontend/src/api/backend/createUser.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										13
									
								
								frontend/src/api/backend/createUser.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,13 @@ | ||||
| import * as api from "./base"; | ||||
| import type { User } from "./models"; | ||||
|  | ||||
| export async function createUser(item: User, abortController?: AbortController): Promise<User> { | ||||
| 	return await api.post( | ||||
| 		{ | ||||
| 			url: "/users", | ||||
| 			// todo: only use whitelist of fields for this data | ||||
| 			data: item, | ||||
| 		}, | ||||
| 		abortController, | ||||
| 	); | ||||
| } | ||||
							
								
								
									
										10
									
								
								frontend/src/api/backend/deleteAccessList.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										10
									
								
								frontend/src/api/backend/deleteAccessList.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,10 @@ | ||||
| import * as api from "./base"; | ||||
|  | ||||
| export async function deleteAccessList(id: number, abortController?: AbortController): Promise<boolean> { | ||||
| 	return await api.del( | ||||
| 		{ | ||||
| 			url: `/nginx/access-lists/${id}`, | ||||
| 		}, | ||||
| 		abortController, | ||||
| 	); | ||||
| } | ||||
							
								
								
									
										10
									
								
								frontend/src/api/backend/deleteCertificate.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										10
									
								
								frontend/src/api/backend/deleteCertificate.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,10 @@ | ||||
| import * as api from "./base"; | ||||
|  | ||||
| export async function deleteCertificate(id: number, abortController?: AbortController): Promise<boolean> { | ||||
| 	return await api.del( | ||||
| 		{ | ||||
| 			url: `/nginx/certificates/${id}`, | ||||
| 		}, | ||||
| 		abortController, | ||||
| 	); | ||||
| } | ||||
							
								
								
									
										10
									
								
								frontend/src/api/backend/deleteDeadHost.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										10
									
								
								frontend/src/api/backend/deleteDeadHost.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,10 @@ | ||||
| import * as api from "./base"; | ||||
|  | ||||
| export async function deleteDeadHost(id: number, abortController?: AbortController): Promise<boolean> { | ||||
| 	return await api.del( | ||||
| 		{ | ||||
| 			url: `/nginx/dead-hosts/${id}`, | ||||
| 		}, | ||||
| 		abortController, | ||||
| 	); | ||||
| } | ||||
							
								
								
									
										10
									
								
								frontend/src/api/backend/deleteProxyHost.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										10
									
								
								frontend/src/api/backend/deleteProxyHost.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,10 @@ | ||||
| import * as api from "./base"; | ||||
|  | ||||
| export async function deleteProxyHost(id: number, abortController?: AbortController): Promise<boolean> { | ||||
| 	return await api.del( | ||||
| 		{ | ||||
| 			url: `/nginx/proxy-hosts/${id}`, | ||||
| 		}, | ||||
| 		abortController, | ||||
| 	); | ||||
| } | ||||
							
								
								
									
										10
									
								
								frontend/src/api/backend/deleteRedirectionHost.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										10
									
								
								frontend/src/api/backend/deleteRedirectionHost.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,10 @@ | ||||
| import * as api from "./base"; | ||||
|  | ||||
| export async function deleteRedirectionHost(id: number, abortController?: AbortController): Promise<boolean> { | ||||
| 	return await api.del( | ||||
| 		{ | ||||
| 			url: `/nginx/redirection-hosts/${id}`, | ||||
| 		}, | ||||
| 		abortController, | ||||
| 	); | ||||
| } | ||||
							
								
								
									
										10
									
								
								frontend/src/api/backend/deleteStream.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										10
									
								
								frontend/src/api/backend/deleteStream.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,10 @@ | ||||
| import * as api from "./base"; | ||||
|  | ||||
| export async function deleteStream(id: number, abortController?: AbortController): Promise<boolean> { | ||||
| 	return await api.del( | ||||
| 		{ | ||||
| 			url: `/nginx/streams/${id}`, | ||||
| 		}, | ||||
| 		abortController, | ||||
| 	); | ||||
| } | ||||
							
								
								
									
										10
									
								
								frontend/src/api/backend/deleteUser.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										10
									
								
								frontend/src/api/backend/deleteUser.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,10 @@ | ||||
| import * as api from "./base"; | ||||
|  | ||||
| export async function deleteUser(id: number, abortController?: AbortController): Promise<boolean> { | ||||
| 	return await api.del( | ||||
| 		{ | ||||
| 			url: `/users/${id}`, | ||||
| 		}, | ||||
| 		abortController, | ||||
| 	); | ||||
| } | ||||
							
								
								
									
										11
									
								
								frontend/src/api/backend/downloadCertificate.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										11
									
								
								frontend/src/api/backend/downloadCertificate.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,11 @@ | ||||
| import * as api from "./base"; | ||||
| import type { Binary } from "./responseTypes"; | ||||
|  | ||||
| export async function downloadCertificate(id: number, abortController?: AbortController): Promise<Binary> { | ||||
| 	return await api.get( | ||||
| 		{ | ||||
| 			url: `/nginx/certificates/${id}/download`, | ||||
| 		}, | ||||
| 		abortController, | ||||
| 	); | ||||
| } | ||||
							
								
								
									
										11
									
								
								frontend/src/api/backend/getAccessList.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										11
									
								
								frontend/src/api/backend/getAccessList.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,11 @@ | ||||
| import * as api from "./base"; | ||||
| import type { AccessList } from "./models"; | ||||
|  | ||||
| export async function getAccessList(id: number, abortController?: AbortController): Promise<AccessList> { | ||||
| 	return await api.get( | ||||
| 		{ | ||||
| 			url: `/nginx/access-lists/${id}`, | ||||
| 		}, | ||||
| 		abortController, | ||||
| 	); | ||||
| } | ||||
							
								
								
									
										14
									
								
								frontend/src/api/backend/getAccessLists.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										14
									
								
								frontend/src/api/backend/getAccessLists.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,14 @@ | ||||
| import * as api from "./base"; | ||||
| import type { AccessList } from "./models"; | ||||
|  | ||||
| export type AccessListExpansion = "owner" | "items" | "clients"; | ||||
|  | ||||
| export async function getAccessLists(expand?: AccessListExpansion[], params = {}): Promise<AccessList[]> { | ||||
| 	return await api.get({ | ||||
| 		url: "/nginx/access-lists", | ||||
| 		params: { | ||||
| 			expand: expand?.join(","), | ||||
| 			...params, | ||||
| 		}, | ||||
| 	}); | ||||
| } | ||||
							
								
								
									
										12
									
								
								frontend/src/api/backend/getAuditLog.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										12
									
								
								frontend/src/api/backend/getAuditLog.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,12 @@ | ||||
| import * as api from "./base"; | ||||
| import type { AuditLog } from "./models"; | ||||
|  | ||||
| export async function getAuditLog(expand?: string[], params = {}): Promise<AuditLog[]> { | ||||
| 	return await api.get({ | ||||
| 		url: "/audit-log", | ||||
| 		params: { | ||||
| 			expand: expand?.join(","), | ||||
| 			...params, | ||||
| 		}, | ||||
| 	}); | ||||
| } | ||||
							
								
								
									
										11
									
								
								frontend/src/api/backend/getCertificate.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										11
									
								
								frontend/src/api/backend/getCertificate.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,11 @@ | ||||
| import * as api from "./base"; | ||||
| import type { Certificate } from "./models"; | ||||
|  | ||||
| export async function getCertificate(id: number, abortController?: AbortController): Promise<Certificate> { | ||||
| 	return await api.get( | ||||
| 		{ | ||||
| 			url: `/nginx/certificates/${id}`, | ||||
| 		}, | ||||
| 		abortController, | ||||
| 	); | ||||
| } | ||||
							
								
								
									
										12
									
								
								frontend/src/api/backend/getCertificates.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										12
									
								
								frontend/src/api/backend/getCertificates.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,12 @@ | ||||
| import * as api from "./base"; | ||||
| import type { Certificate } from "./models"; | ||||
|  | ||||
| export async function getCertificates(expand?: string[], params = {}): Promise<Certificate[]> { | ||||
| 	return await api.get({ | ||||
| 		url: "/nginx/certificates", | ||||
| 		params: { | ||||
| 			expand: expand?.join(","), | ||||
| 			...params, | ||||
| 		}, | ||||
| 	}); | ||||
| } | ||||
							
								
								
									
										11
									
								
								frontend/src/api/backend/getDeadHost.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										11
									
								
								frontend/src/api/backend/getDeadHost.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,11 @@ | ||||
| import * as api from "./base"; | ||||
| import type { DeadHost } from "./models"; | ||||
|  | ||||
| export async function getDeadHost(id: number, abortController?: AbortController): Promise<DeadHost> { | ||||
| 	return await api.get( | ||||
| 		{ | ||||
| 			url: `/nginx/dead-hosts/${id}`, | ||||
| 		}, | ||||
| 		abortController, | ||||
| 	); | ||||
| } | ||||
							
								
								
									
										14
									
								
								frontend/src/api/backend/getDeadHosts.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										14
									
								
								frontend/src/api/backend/getDeadHosts.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,14 @@ | ||||
| import * as api from "./base"; | ||||
| import type { DeadHost } from "./models"; | ||||
|  | ||||
| export type DeadHostExpansion = "owner" | "certificate"; | ||||
|  | ||||
| export async function getDeadHosts(expand?: DeadHostExpansion[], params = {}): Promise<DeadHost[]> { | ||||
| 	return await api.get({ | ||||
| 		url: "/nginx/dead-hosts", | ||||
| 		params: { | ||||
| 			expand: expand?.join(","), | ||||
| 			...params, | ||||
| 		}, | ||||
| 	}); | ||||
| } | ||||
							
								
								
									
										11
									
								
								frontend/src/api/backend/getHealth.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										11
									
								
								frontend/src/api/backend/getHealth.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,11 @@ | ||||
| import * as api from "./base"; | ||||
| import type { HealthResponse } from "./responseTypes"; | ||||
|  | ||||
| export async function getHealth(abortController?: AbortController): Promise<HealthResponse> { | ||||
| 	return await api.get( | ||||
| 		{ | ||||
| 			url: "/", | ||||
| 		}, | ||||
| 		abortController, | ||||
| 	); | ||||
| } | ||||
							
								
								
									
										10
									
								
								frontend/src/api/backend/getHostsReport.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										10
									
								
								frontend/src/api/backend/getHostsReport.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,10 @@ | ||||
| import * as api from "./base"; | ||||
|  | ||||
| export async function getHostsReport(abortController?: AbortController): Promise<Record<string, number>> { | ||||
| 	return await api.get( | ||||
| 		{ | ||||
| 			url: "/reports/hosts", | ||||
| 		}, | ||||
| 		abortController, | ||||
| 	); | ||||
| } | ||||
							
								
								
									
										11
									
								
								frontend/src/api/backend/getProxyHost.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										11
									
								
								frontend/src/api/backend/getProxyHost.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,11 @@ | ||||
| import * as api from "./base"; | ||||
| import type { ProxyHost } from "./models"; | ||||
|  | ||||
| export async function getProxyHost(id: number, abortController?: AbortController): Promise<ProxyHost> { | ||||
| 	return await api.get( | ||||
| 		{ | ||||
| 			url: `/nginx/proxy-hosts/${id}`, | ||||
| 		}, | ||||
| 		abortController, | ||||
| 	); | ||||
| } | ||||
							
								
								
									
										14
									
								
								frontend/src/api/backend/getProxyHosts.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										14
									
								
								frontend/src/api/backend/getProxyHosts.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,14 @@ | ||||
| import * as api from "./base"; | ||||
| import type { ProxyHost } from "./models"; | ||||
|  | ||||
| export type ProxyHostExpansion = "owner" | "access_list" | "certificate"; | ||||
|  | ||||
| export async function getProxyHosts(expand?: ProxyHostExpansion[], params = {}): Promise<ProxyHost[]> { | ||||
| 	return await api.get({ | ||||
| 		url: "/nginx/proxy-hosts", | ||||
| 		params: { | ||||
| 			expand: expand?.join(","), | ||||
| 			...params, | ||||
| 		}, | ||||
| 	}); | ||||
| } | ||||
							
								
								
									
										11
									
								
								frontend/src/api/backend/getRedirectionHost.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										11
									
								
								frontend/src/api/backend/getRedirectionHost.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,11 @@ | ||||
| import * as api from "./base"; | ||||
| import type { ProxyHost } from "./models"; | ||||
|  | ||||
| export async function getRedirectionHost(id: number, abortController?: AbortController): Promise<ProxyHost> { | ||||
| 	return await api.get( | ||||
| 		{ | ||||
| 			url: `/nginx/redirection-hosts/${id}`, | ||||
| 		}, | ||||
| 		abortController, | ||||
| 	); | ||||
| } | ||||
							
								
								
									
										16
									
								
								frontend/src/api/backend/getRedirectionHosts.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										16
									
								
								frontend/src/api/backend/getRedirectionHosts.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,16 @@ | ||||
| import * as api from "./base"; | ||||
| import type { RedirectionHost } from "./models"; | ||||
|  | ||||
| export type RedirectionHostExpansion = "owner" | "certificate"; | ||||
| export async function getRedirectionHosts( | ||||
| 	expand?: RedirectionHostExpansion[], | ||||
| 	params = {}, | ||||
| ): Promise<RedirectionHost[]> { | ||||
| 	return await api.get({ | ||||
| 		url: "/nginx/redirection-hosts", | ||||
| 		params: { | ||||
| 			expand: expand?.join(","), | ||||
| 			...params, | ||||
| 		}, | ||||
| 	}); | ||||
| } | ||||
							
								
								
									
										11
									
								
								frontend/src/api/backend/getSetting.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										11
									
								
								frontend/src/api/backend/getSetting.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,11 @@ | ||||
| import * as api from "./base"; | ||||
| import type { Setting } from "./models"; | ||||
|  | ||||
| export async function getSetting(id: string, abortController?: AbortController): Promise<Setting> { | ||||
| 	return await api.get( | ||||
| 		{ | ||||
| 			url: `/settings/${id}`, | ||||
| 		}, | ||||
| 		abortController, | ||||
| 	); | ||||
| } | ||||
							
								
								
									
										12
									
								
								frontend/src/api/backend/getSettings.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										12
									
								
								frontend/src/api/backend/getSettings.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,12 @@ | ||||
| import * as api from "./base"; | ||||
| import type { Setting } from "./models"; | ||||
|  | ||||
| export async function getSettings(expand?: string[], params = {}): Promise<Setting[]> { | ||||
| 	return await api.get({ | ||||
| 		url: "/settings", | ||||
| 		params: { | ||||
| 			expand: expand?.join(","), | ||||
| 			...params, | ||||
| 		}, | ||||
| 	}); | ||||
| } | ||||
							
								
								
									
										11
									
								
								frontend/src/api/backend/getStream.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										11
									
								
								frontend/src/api/backend/getStream.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,11 @@ | ||||
| import * as api from "./base"; | ||||
| import type { Stream } from "./models"; | ||||
|  | ||||
| export async function getStream(id: number, abortController?: AbortController): Promise<Stream> { | ||||
| 	return await api.get( | ||||
| 		{ | ||||
| 			url: `/nginx/streams/${id}`, | ||||
| 		}, | ||||
| 		abortController, | ||||
| 	); | ||||
| } | ||||
							
								
								
									
										14
									
								
								frontend/src/api/backend/getStreams.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										14
									
								
								frontend/src/api/backend/getStreams.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,14 @@ | ||||
| import * as api from "./base"; | ||||
| import type { Stream } from "./models"; | ||||
|  | ||||
| export type StreamExpansion = "owner" | "certificate"; | ||||
|  | ||||
| export async function getStreams(expand?: StreamExpansion[], params = {}): Promise<Stream[]> { | ||||
| 	return await api.get({ | ||||
| 		url: "/nginx/streams", | ||||
| 		params: { | ||||
| 			expand: expand?.join(","), | ||||
| 			...params, | ||||
| 		}, | ||||
| 	}); | ||||
| } | ||||
							
								
								
									
										19
									
								
								frontend/src/api/backend/getToken.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										19
									
								
								frontend/src/api/backend/getToken.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,19 @@ | ||||
| import * as api from "./base"; | ||||
| import type { TokenResponse } from "./responseTypes"; | ||||
|  | ||||
| interface Options { | ||||
| 	payload: { | ||||
| 		identity: string; | ||||
| 		secret: string; | ||||
| 	}; | ||||
| } | ||||
|  | ||||
| export async function getToken({ payload }: Options, abortController?: AbortController): Promise<TokenResponse> { | ||||
| 	return await api.post( | ||||
| 		{ | ||||
| 			url: "/tokens", | ||||
| 			data: payload, | ||||
| 		}, | ||||
| 		abortController, | ||||
| 	); | ||||
| } | ||||
							
								
								
									
										10
									
								
								frontend/src/api/backend/getUser.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										10
									
								
								frontend/src/api/backend/getUser.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,10 @@ | ||||
| import * as api from "./base"; | ||||
| import type { User } from "./models"; | ||||
|  | ||||
| export async function getUser(id: number | string = "me", params = {}): Promise<User> { | ||||
| 	const userId = id ? id : "me"; | ||||
| 	return await api.get({ | ||||
| 		url: `/users/${userId}`, | ||||
| 		params, | ||||
| 	}); | ||||
| } | ||||
							
								
								
									
										14
									
								
								frontend/src/api/backend/getUsers.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										14
									
								
								frontend/src/api/backend/getUsers.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,14 @@ | ||||
| import * as api from "./base"; | ||||
| import type { User } from "./models"; | ||||
|  | ||||
| export type UserExpansion = "permissions"; | ||||
|  | ||||
| export async function getUsers(expand?: UserExpansion[], params = {}): Promise<User[]> { | ||||
| 	return await api.get({ | ||||
| 		url: "/users", | ||||
| 		params: { | ||||
| 			expand: expand?.join(","), | ||||
| 			...params, | ||||
| 		}, | ||||
| 	}); | ||||
| } | ||||
							
								
								
									
										54
									
								
								frontend/src/api/backend/helpers.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										54
									
								
								frontend/src/api/backend/helpers.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,54 @@ | ||||
| import { decamelize } from "humps"; | ||||
|  | ||||
| /** | ||||
|  * This will convert a react-table sort object into | ||||
|  * a string that the backend api likes: | ||||
|  *   name.asc,id.desc | ||||
|  */ | ||||
| export function tableSortToAPI(sortBy: any): string | undefined { | ||||
| 	if (sortBy?.length > 0) { | ||||
| 		const strs: string[] = []; | ||||
| 		sortBy.map((item: any) => { | ||||
| 			strs.push(`${decamelize(item.id)}.${item.desc ? "desc" : "asc"}`); | ||||
| 			return undefined; | ||||
| 		}); | ||||
| 		return strs.join(","); | ||||
| 	} | ||||
| 	return; | ||||
| } | ||||
|  | ||||
| /** | ||||
|  * This will convert a react-table filters object into | ||||
|  * a string that the backend api likes: | ||||
|  *   name:contains=jam | ||||
|  */ | ||||
| export function tableFiltersToAPI(filters: any[]): { [key: string]: string } { | ||||
| 	const items: { [key: string]: string } = {}; | ||||
| 	if (filters?.length > 0) { | ||||
| 		filters.map((item: any) => { | ||||
| 			items[`${decamelize(item.id)}:${item.value.modifier}`] = item.value.value; | ||||
| 			return undefined; | ||||
| 		}); | ||||
| 	} | ||||
| 	return items; | ||||
| } | ||||
|  | ||||
| /** | ||||
|  * Builds a filters object by removing entries with undefined, null, or empty string values. | ||||
|  * | ||||
|  */ | ||||
| export function buildFilters(filters?: Record<string, string | boolean | undefined | null>) { | ||||
| 	if (!filters) { | ||||
| 		return filters; | ||||
| 	} | ||||
| 	const result: Record<string, string> = {}; | ||||
| 	for (const key in filters) { | ||||
| 		const value = filters[key]; | ||||
| 		// If the value is undefined, null, or an empty string, skip it | ||||
| 		if (value === undefined || value === null || value === "") { | ||||
| 			continue; | ||||
| 		} | ||||
| 		result[key] = value.toString(); | ||||
| 	} | ||||
| 	return result; | ||||
| } | ||||
							
								
								
									
										54
									
								
								frontend/src/api/backend/index.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										54
									
								
								frontend/src/api/backend/index.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,54 @@ | ||||
| export * from "./createAccessList"; | ||||
| export * from "./createCertificate"; | ||||
| export * from "./createDeadHost"; | ||||
| export * from "./createProxyHost"; | ||||
| export * from "./createRedirectionHost"; | ||||
| export * from "./createStream"; | ||||
| export * from "./deleteAccessList"; | ||||
| export * from "./deleteCertificate"; | ||||
| export * from "./deleteDeadHost"; | ||||
| export * from "./deleteProxyHost"; | ||||
| export * from "./deleteRedirectionHost"; | ||||
| export * from "./deleteStream"; | ||||
| export * from "./deleteUser"; | ||||
| export * from "./downloadCertificate"; | ||||
| export * from "./getAccessList"; | ||||
| export * from "./getAccessLists"; | ||||
| export * from "./getAuditLog"; | ||||
| export * from "./getCertificate"; | ||||
| export * from "./getCertificates"; | ||||
| export * from "./getDeadHost"; | ||||
| export * from "./getDeadHosts"; | ||||
| export * from "./getHealth"; | ||||
| export * from "./getHostsReport"; | ||||
| export * from "./getProxyHost"; | ||||
| export * from "./getProxyHosts"; | ||||
| export * from "./getRedirectionHost"; | ||||
| export * from "./getRedirectionHosts"; | ||||
| export * from "./getSetting"; | ||||
| export * from "./getSettings"; | ||||
| export * from "./getStream"; | ||||
| export * from "./getStreams"; | ||||
| export * from "./getToken"; | ||||
| export * from "./getUser"; | ||||
| export * from "./getUsers"; | ||||
| export * from "./helpers"; | ||||
| export * from "./models"; | ||||
| export * from "./refreshToken"; | ||||
| export * from "./renewCertificate"; | ||||
| export * from "./responseTypes"; | ||||
| export * from "./testHttpCertificate"; | ||||
| export * from "./toggleDeadHost"; | ||||
| export * from "./toggleProxyHost"; | ||||
| export * from "./toggleRedirectionHost"; | ||||
| export * from "./toggleStream"; | ||||
| export * from "./updateAccessList"; | ||||
| export * from "./updateAuth"; | ||||
| export * from "./updateDeadHost"; | ||||
| export * from "./updateProxyHost"; | ||||
| export * from "./updateRedirectionHost"; | ||||
| export * from "./updateSetting"; | ||||
| export * from "./updateStream"; | ||||
| export * from "./updateUser"; | ||||
| export * from "./uploadCertificate"; | ||||
| export * from "./validateCertificate"; | ||||
							
								
								
									
										193
									
								
								frontend/src/api/backend/models.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										193
									
								
								frontend/src/api/backend/models.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,193 @@ | ||||
| export interface AppVersion { | ||||
| 	major: number; | ||||
| 	minor: number; | ||||
| 	revision: number; | ||||
| } | ||||
|  | ||||
| export interface UserPermissions { | ||||
| 	id: number; | ||||
| 	createdOn: string; | ||||
| 	modifiedOn: string; | ||||
| 	userId: number; | ||||
| 	visibility: string; | ||||
| 	proxyHosts: string; | ||||
| 	redirectionHosts: string; | ||||
| 	deadHosts: string; | ||||
| 	streams: string; | ||||
| 	accessLists: string; | ||||
| 	certificates: string; | ||||
| } | ||||
|  | ||||
| export interface User { | ||||
| 	id: number; | ||||
| 	createdOn: string; | ||||
| 	modifiedOn: string; | ||||
| 	isDisabled: boolean; | ||||
| 	email: string; | ||||
| 	name: string; | ||||
| 	nickname: string; | ||||
| 	avatar: string; | ||||
| 	roles: string[]; | ||||
| 	permissions?: UserPermissions; | ||||
| } | ||||
|  | ||||
| export interface AuditLog { | ||||
| 	id: number; | ||||
| 	createdOn: string; | ||||
| 	modifiedOn: string; | ||||
| 	userId: number; | ||||
| 	objectType: string; | ||||
| 	objectId: number; | ||||
| 	action: string; | ||||
| 	meta: Record<string, any>; | ||||
| } | ||||
|  | ||||
| export interface AccessList { | ||||
| 	id?: number; | ||||
| 	createdOn?: string; | ||||
| 	modifiedOn?: string; | ||||
| 	ownerUserId: number; | ||||
| 	name: string; | ||||
| 	meta: Record<string, any>; | ||||
| 	satisfyAny: boolean; | ||||
| 	passAuth: boolean; | ||||
| 	proxyHostCount: number; | ||||
| 	// Expansions: | ||||
| 	owner?: User; | ||||
| 	items?: AccessListItem[]; | ||||
| 	clients?: AccessListClient[]; | ||||
| } | ||||
|  | ||||
| export interface AccessListItem { | ||||
| 	id?: number; | ||||
| 	createdOn?: string; | ||||
| 	modifiedOn?: string; | ||||
| 	accessListId?: number; | ||||
| 	username: string; | ||||
| 	password: string; | ||||
| 	meta: Record<string, any>; | ||||
| 	hint: string; | ||||
| } | ||||
|  | ||||
| export type AccessListClient = { | ||||
| 	id?: number; | ||||
| 	createdOn?: string; | ||||
| 	modifiedOn?: string; | ||||
| 	accessListId?: number; | ||||
| 	address: string; | ||||
| 	directive: "allow" | "deny"; | ||||
| 	meta: Record<string, any>; | ||||
| }; | ||||
|  | ||||
| export interface Certificate { | ||||
| 	id: number; | ||||
| 	createdOn: string; | ||||
| 	modifiedOn: string; | ||||
| 	ownerUserId: number; | ||||
| 	provider: string; | ||||
| 	niceName: string; | ||||
| 	domainNames: string[]; | ||||
| 	expiresOn: string; | ||||
| 	meta: Record<string, any>; | ||||
| 	owner?: User; | ||||
| 	proxyHosts?: ProxyHost[]; | ||||
| 	deadHosts?: DeadHost[]; | ||||
| 	redirectionHosts?: RedirectionHost[]; | ||||
| } | ||||
|  | ||||
| export interface ProxyHost { | ||||
| 	id: number; | ||||
| 	createdOn: string; | ||||
| 	modifiedOn: string; | ||||
| 	ownerUserId: number; | ||||
| 	domainNames: string[]; | ||||
| 	forwardHost: string; | ||||
| 	forwardPort: number; | ||||
| 	accessListId: number; | ||||
| 	certificateId: number; | ||||
| 	sslForced: boolean; | ||||
| 	cachingEnabled: boolean; | ||||
| 	blockExploits: boolean; | ||||
| 	advancedConfig: string; | ||||
| 	meta: Record<string, any>; | ||||
| 	allowWebsocketUpgrade: boolean; | ||||
| 	http2Support: boolean; | ||||
| 	forwardScheme: string; | ||||
| 	enabled: boolean; | ||||
| 	locations: string[]; // todo: string or object? | ||||
| 	hstsEnabled: boolean; | ||||
| 	hstsSubdomains: boolean; | ||||
| 	// Expansions: | ||||
| 	owner?: User; | ||||
| 	accessList?: AccessList; | ||||
| 	certificate?: Certificate; | ||||
| } | ||||
|  | ||||
| export interface DeadHost { | ||||
| 	id: number; | ||||
| 	createdOn: string; | ||||
| 	modifiedOn: string; | ||||
| 	ownerUserId: number; | ||||
| 	domainNames: string[]; | ||||
| 	certificateId: number; | ||||
| 	sslForced: boolean; | ||||
| 	advancedConfig: string; | ||||
| 	meta: Record<string, any>; | ||||
| 	http2Support: boolean; | ||||
| 	enabled: boolean; | ||||
| 	hstsEnabled: boolean; | ||||
| 	hstsSubdomains: boolean; | ||||
| 	// Expansions: | ||||
| 	owner?: User; | ||||
| 	certificate?: Certificate; | ||||
| } | ||||
|  | ||||
| export interface RedirectionHost { | ||||
| 	id: number; | ||||
| 	createdOn: string; | ||||
| 	modifiedOn: string; | ||||
| 	ownerUserId: number; | ||||
| 	domainNames: string[]; | ||||
| 	forwardDomainName: string; | ||||
| 	preservePath: boolean; | ||||
| 	certificateId: number; | ||||
| 	sslForced: boolean; | ||||
| 	blockExploits: boolean; | ||||
| 	advancedConfig: string; | ||||
| 	meta: Record<string, any>; | ||||
| 	http2Support: boolean; | ||||
| 	forwardScheme: string; | ||||
| 	forwardHttpCode: number; | ||||
| 	enabled: boolean; | ||||
| 	hstsEnabled: boolean; | ||||
| 	hstsSubdomains: boolean; | ||||
| 	// Expansions: | ||||
| 	owner?: User; | ||||
| 	certificate?: Certificate; | ||||
| } | ||||
|  | ||||
| export interface Stream { | ||||
| 	id: number; | ||||
| 	createdOn: string; | ||||
| 	modifiedOn: string; | ||||
| 	ownerUserId: number; | ||||
| 	incomingPort: number; | ||||
| 	forwardingHost: string; | ||||
| 	forwardingPort: number; | ||||
| 	tcpForwarding: boolean; | ||||
| 	udpForwarding: boolean; | ||||
| 	meta: Record<string, any>; | ||||
| 	enabled: boolean; | ||||
| 	certificateId: number; | ||||
| 	// Expansions: | ||||
| 	owner?: User; | ||||
| 	certificate?: Certificate; | ||||
| } | ||||
|  | ||||
| export interface Setting { | ||||
| 	id: string; | ||||
| 	name: string; | ||||
| 	description: string; | ||||
| 	value: string; | ||||
| 	meta: Record<string, any>; | ||||
| } | ||||
							
								
								
									
										11
									
								
								frontend/src/api/backend/refreshToken.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										11
									
								
								frontend/src/api/backend/refreshToken.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,11 @@ | ||||
| import * as api from "./base"; | ||||
| import type { TokenResponse } from "./responseTypes"; | ||||
|  | ||||
| export async function refreshToken(abortController?: AbortController): Promise<TokenResponse> { | ||||
| 	return await api.get( | ||||
| 		{ | ||||
| 			url: "/tokens", | ||||
| 		}, | ||||
| 		abortController, | ||||
| 	); | ||||
| } | ||||
							
								
								
									
										11
									
								
								frontend/src/api/backend/renewCertificate.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										11
									
								
								frontend/src/api/backend/renewCertificate.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,11 @@ | ||||
| import * as api from "./base"; | ||||
| import type { Certificate } from "./models"; | ||||
|  | ||||
| export async function renewCertificate(id: number, abortController?: AbortController): Promise<Certificate> { | ||||
| 	return await api.post( | ||||
| 		{ | ||||
| 			url: `/nginx/certificates/${id}/renew`, | ||||
| 		}, | ||||
| 		abortController, | ||||
| 	); | ||||
| } | ||||
							
								
								
									
										18
									
								
								frontend/src/api/backend/responseTypes.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										18
									
								
								frontend/src/api/backend/responseTypes.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,18 @@ | ||||
| import type { AppVersion } from "./models"; | ||||
|  | ||||
| export interface HealthResponse { | ||||
| 	status: string; | ||||
| 	version: AppVersion; | ||||
| } | ||||
|  | ||||
| export interface TokenResponse { | ||||
| 	expires: number; | ||||
| 	token: string; | ||||
| } | ||||
|  | ||||
| export interface ValidatedCertificateResponse { | ||||
| 	certificate: Record<string, any>; | ||||
| 	certificateKey: boolean; | ||||
| } | ||||
|  | ||||
| export type Binary = number & { readonly __brand: unique symbol }; | ||||
							
								
								
									
										16
									
								
								frontend/src/api/backend/testHttpCertificate.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										16
									
								
								frontend/src/api/backend/testHttpCertificate.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,16 @@ | ||||
| import * as api from "./base"; | ||||
|  | ||||
| export async function testHttpCertificate( | ||||
| 	domains: string[], | ||||
| 	abortController?: AbortController, | ||||
| ): Promise<Record<string, string>> { | ||||
| 	return await api.get( | ||||
| 		{ | ||||
| 			url: "/nginx/certificates/test-http", | ||||
| 			params: { | ||||
| 				domains: domains.join(","), | ||||
| 			}, | ||||
| 		}, | ||||
| 		abortController, | ||||
| 	); | ||||
| } | ||||
							
								
								
									
										14
									
								
								frontend/src/api/backend/toggleDeadHost.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										14
									
								
								frontend/src/api/backend/toggleDeadHost.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,14 @@ | ||||
| import * as api from "./base"; | ||||
|  | ||||
| export async function toggleDeadHost( | ||||
| 	id: number, | ||||
| 	enabled: boolean, | ||||
| 	abortController?: AbortController, | ||||
| ): Promise<boolean> { | ||||
| 	return await api.post( | ||||
| 		{ | ||||
| 			url: `/nginx/dead-hosts/${id}/${enabled ? "enable" : "disable"}`, | ||||
| 		}, | ||||
| 		abortController, | ||||
| 	); | ||||
| } | ||||
							
								
								
									
										14
									
								
								frontend/src/api/backend/toggleProxyHost.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										14
									
								
								frontend/src/api/backend/toggleProxyHost.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,14 @@ | ||||
| import * as api from "./base"; | ||||
|  | ||||
| export async function toggleProxyHost( | ||||
| 	id: number, | ||||
| 	enabled: boolean, | ||||
| 	abortController?: AbortController, | ||||
| ): Promise<boolean> { | ||||
| 	return await api.post( | ||||
| 		{ | ||||
| 			url: `/nginx/proxy-hosts/${id}/${enabled ? "enable" : "disable"}`, | ||||
| 		}, | ||||
| 		abortController, | ||||
| 	); | ||||
| } | ||||
							
								
								
									
										14
									
								
								frontend/src/api/backend/toggleRedirectionHost.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										14
									
								
								frontend/src/api/backend/toggleRedirectionHost.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,14 @@ | ||||
| import * as api from "./base"; | ||||
|  | ||||
| export async function toggleRedirectionHost( | ||||
| 	id: number, | ||||
| 	enabled: boolean, | ||||
| 	abortController?: AbortController, | ||||
| ): Promise<boolean> { | ||||
| 	return await api.post( | ||||
| 		{ | ||||
| 			url: `/nginx/redirection-hosts/${id}/${enabled ? "enable" : "disable"}`, | ||||
| 		}, | ||||
| 		abortController, | ||||
| 	); | ||||
| } | ||||
							
								
								
									
										10
									
								
								frontend/src/api/backend/toggleStream.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										10
									
								
								frontend/src/api/backend/toggleStream.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,10 @@ | ||||
| import * as api from "./base"; | ||||
|  | ||||
| export async function toggleStream(id: number, enabled: boolean, abortController?: AbortController): Promise<boolean> { | ||||
| 	return await api.post( | ||||
| 		{ | ||||
| 			url: `/nginx/streams/${id}/${enabled ? "enable" : "disable"}`, | ||||
| 		}, | ||||
| 		abortController, | ||||
| 	); | ||||
| } | ||||
							
								
								
									
										15
									
								
								frontend/src/api/backend/updateAccessList.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										15
									
								
								frontend/src/api/backend/updateAccessList.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,15 @@ | ||||
| import * as api from "./base"; | ||||
| import type { AccessList } from "./models"; | ||||
|  | ||||
| export async function updateAccessList(item: AccessList, abortController?: AbortController): Promise<AccessList> { | ||||
| 	// Remove readonly fields | ||||
| 	const { id, createdOn: _, modifiedOn: __, ...data } = item; | ||||
|  | ||||
| 	return await api.put( | ||||
| 		{ | ||||
| 			url: `/nginx/access-lists/${id}`, | ||||
| 			data: data, | ||||
| 		}, | ||||
| 		abortController, | ||||
| 	); | ||||
| } | ||||
							
								
								
									
										26
									
								
								frontend/src/api/backend/updateAuth.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										26
									
								
								frontend/src/api/backend/updateAuth.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,26 @@ | ||||
| import * as api from "./base"; | ||||
| import type { User } from "./models"; | ||||
|  | ||||
| export async function updateAuth( | ||||
| 	userId: number | "me", | ||||
| 	newPassword: string, | ||||
| 	current?: string, | ||||
| 	abortController?: AbortController, | ||||
| ): Promise<User> { | ||||
| 	const data = { | ||||
| 		type: "password", | ||||
| 		current: current, | ||||
| 		secret: newPassword, | ||||
| 	}; | ||||
| 	if (userId === "me") { | ||||
| 		data.current = current; | ||||
| 	} | ||||
|  | ||||
| 	return await api.put( | ||||
| 		{ | ||||
| 			url: `/users/${userId}/auth`, | ||||
| 			data, | ||||
| 		}, | ||||
| 		abortController, | ||||
| 	); | ||||
| } | ||||
							
								
								
									
										15
									
								
								frontend/src/api/backend/updateDeadHost.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										15
									
								
								frontend/src/api/backend/updateDeadHost.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,15 @@ | ||||
| import * as api from "./base"; | ||||
| import type { DeadHost } from "./models"; | ||||
|  | ||||
| export async function updateDeadHost(item: DeadHost, abortController?: AbortController): Promise<DeadHost> { | ||||
| 	// Remove readonly fields | ||||
| 	const { id, createdOn: _, modifiedOn: __, ...data } = item; | ||||
|  | ||||
| 	return await api.put( | ||||
| 		{ | ||||
| 			url: `/nginx/dead-hosts/${id}`, | ||||
| 			data: data, | ||||
| 		}, | ||||
| 		abortController, | ||||
| 	); | ||||
| } | ||||
							
								
								
									
										15
									
								
								frontend/src/api/backend/updateProxyHost.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										15
									
								
								frontend/src/api/backend/updateProxyHost.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,15 @@ | ||||
| import * as api from "./base"; | ||||
| import type { ProxyHost } from "./models"; | ||||
|  | ||||
| export async function updateProxyHost(item: ProxyHost, abortController?: AbortController): Promise<ProxyHost> { | ||||
| 	// Remove readonly fields | ||||
| 	const { id, createdOn: _, modifiedOn: __, ...data } = item; | ||||
|  | ||||
| 	return await api.put( | ||||
| 		{ | ||||
| 			url: `/nginx/proxy-hosts/${id}`, | ||||
| 			data: data, | ||||
| 		}, | ||||
| 		abortController, | ||||
| 	); | ||||
| } | ||||
							
								
								
									
										18
									
								
								frontend/src/api/backend/updateRedirectionHost.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										18
									
								
								frontend/src/api/backend/updateRedirectionHost.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,18 @@ | ||||
| import * as api from "./base"; | ||||
| import type { RedirectionHost } from "./models"; | ||||
|  | ||||
| export async function updateRedirectionHost( | ||||
| 	item: RedirectionHost, | ||||
| 	abortController?: AbortController, | ||||
| ): Promise<RedirectionHost> { | ||||
| 	// Remove readonly fields | ||||
| 	const { id, createdOn: _, modifiedOn: __, ...data } = item; | ||||
|  | ||||
| 	return await api.put( | ||||
| 		{ | ||||
| 			url: `/nginx/redirection-hosts/${id}`, | ||||
| 			data: data, | ||||
| 		}, | ||||
| 		abortController, | ||||
| 	); | ||||
| } | ||||
							
								
								
									
										15
									
								
								frontend/src/api/backend/updateSetting.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										15
									
								
								frontend/src/api/backend/updateSetting.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,15 @@ | ||||
| import * as api from "./base"; | ||||
| import type { Setting } from "./models"; | ||||
|  | ||||
| export async function updateSetting(item: Setting, abortController?: AbortController): Promise<Setting> { | ||||
| 	// Remove readonly fields | ||||
| 	const { id, ...data } = item; | ||||
|  | ||||
| 	return await api.put( | ||||
| 		{ | ||||
| 			url: `/settings/${id}`, | ||||
| 			data: data, | ||||
| 		}, | ||||
| 		abortController, | ||||
| 	); | ||||
| } | ||||
							
								
								
									
										15
									
								
								frontend/src/api/backend/updateStream.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										15
									
								
								frontend/src/api/backend/updateStream.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,15 @@ | ||||
| import * as api from "./base"; | ||||
| import type { Stream } from "./models"; | ||||
|  | ||||
| export async function updateStream(item: Stream, abortController?: AbortController): Promise<Stream> { | ||||
| 	// Remove readonly fields | ||||
| 	const { id, createdOn: _, modifiedOn: __, ...data } = item; | ||||
|  | ||||
| 	return await api.put( | ||||
| 		{ | ||||
| 			url: `/nginx/streams/${id}`, | ||||
| 			data: data, | ||||
| 		}, | ||||
| 		abortController, | ||||
| 	); | ||||
| } | ||||
							
								
								
									
										15
									
								
								frontend/src/api/backend/updateUser.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										15
									
								
								frontend/src/api/backend/updateUser.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,15 @@ | ||||
| import * as api from "./base"; | ||||
| import type { User } from "./models"; | ||||
|  | ||||
| export async function updateUser(item: User, abortController?: AbortController): Promise<User> { | ||||
| 	// Remove readonly fields | ||||
| 	const { id, createdOn: _, modifiedOn: __, ...data } = item; | ||||
|  | ||||
| 	return await api.put( | ||||
| 		{ | ||||
| 			url: `/users/${id}`, | ||||
| 			data: data, | ||||
| 		}, | ||||
| 		abortController, | ||||
| 	); | ||||
| } | ||||
							
								
								
									
										18
									
								
								frontend/src/api/backend/uploadCertificate.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										18
									
								
								frontend/src/api/backend/uploadCertificate.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,18 @@ | ||||
| import * as api from "./base"; | ||||
| import type { Certificate } from "./models"; | ||||
|  | ||||
| export async function uploadCertificate( | ||||
| 	id: number, | ||||
| 	certificate: string, | ||||
| 	certificateKey: string, | ||||
| 	intermediateCertificate?: string, | ||||
| 	abortController?: AbortController, | ||||
| ): Promise<Certificate> { | ||||
| 	return await api.post( | ||||
| 		{ | ||||
| 			url: `/nginx/certificates/${id}/upload`, | ||||
| 			data: { certificate, certificateKey, intermediateCertificate }, | ||||
| 		}, | ||||
| 		abortController, | ||||
| 	); | ||||
| } | ||||
							
								
								
									
										17
									
								
								frontend/src/api/backend/validateCertificate.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										17
									
								
								frontend/src/api/backend/validateCertificate.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,17 @@ | ||||
| import * as api from "./base"; | ||||
| import type { ValidatedCertificateResponse } from "./responseTypes"; | ||||
|  | ||||
| export async function validateCertificate( | ||||
| 	certificate: string, | ||||
| 	certificateKey: string, | ||||
| 	intermediateCertificate?: string, | ||||
| 	abortController?: AbortController, | ||||
| ): Promise<ValidatedCertificateResponse> { | ||||
| 	return await api.post( | ||||
| 		{ | ||||
| 			url: "/nginx/certificates/validate", | ||||
| 			data: { certificate, certificateKey, intermediateCertificate }, | ||||
| 		}, | ||||
| 		abortController, | ||||
| 	); | ||||
| } | ||||
							
								
								
									
										64
									
								
								frontend/src/components/Button.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										64
									
								
								frontend/src/components/Button.tsx
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,64 @@ | ||||
| import cn from "classnames"; | ||||
| import type { ReactNode } from "react"; | ||||
|  | ||||
| interface Props { | ||||
| 	children: ReactNode; | ||||
| 	className?: string; | ||||
| 	type?: "button" | "submit"; | ||||
| 	actionType?: "primary" | "secondary" | "success" | "warning" | "danger" | "info" | "light" | "dark"; | ||||
| 	variant?: "ghost" | "outline" | "pill" | "square" | "action"; | ||||
| 	size?: "sm" | "md" | "lg" | "xl"; | ||||
| 	fullWidth?: boolean; | ||||
| 	isLoading?: boolean; | ||||
| 	disabled?: boolean; | ||||
| 	color?: | ||||
| 		| "blue" | ||||
| 		| "azure" | ||||
| 		| "indigo" | ||||
| 		| "purple" | ||||
| 		| "pink" | ||||
| 		| "red" | ||||
| 		| "orange" | ||||
| 		| "yellow" | ||||
| 		| "lime" | ||||
| 		| "green" | ||||
| 		| "teal" | ||||
| 		| "cyan"; | ||||
| 	onClick?: () => void; | ||||
| } | ||||
| function Button({ | ||||
| 	children, | ||||
| 	className, | ||||
| 	onClick, | ||||
| 	type, | ||||
| 	actionType, | ||||
| 	variant, | ||||
| 	size, | ||||
| 	color, | ||||
| 	fullWidth, | ||||
| 	isLoading, | ||||
| 	disabled, | ||||
| }: Props) { | ||||
| 	const myOnClick = () => { | ||||
| 		!isLoading && onClick && onClick(); | ||||
| 	}; | ||||
|  | ||||
| 	const cns = cn( | ||||
| 		"btn", | ||||
| 		className, | ||||
| 		actionType && `btn-${actionType}`, | ||||
| 		variant && `btn-${variant}`, | ||||
| 		size && `btn-${size}`, | ||||
| 		color && `btn-${color}`, | ||||
| 		fullWidth && "w-100", | ||||
| 		isLoading && "btn-loading", | ||||
| 	); | ||||
|  | ||||
| 	return ( | ||||
| 		<button type={type || "button"} className={cns} onClick={myOnClick} disabled={disabled}> | ||||
| 			{children} | ||||
| 		</button> | ||||
| 	); | ||||
| } | ||||
|  | ||||
| export { Button }; | ||||
							
								
								
									
										23
									
								
								frontend/src/components/ErrorNotFound.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										23
									
								
								frontend/src/components/ErrorNotFound.tsx
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,23 @@ | ||||
| import { intl } from "src/locale"; | ||||
| import { useNavigate } from "react-router-dom"; | ||||
| import { Button } from "src/components"; | ||||
|  | ||||
| export function ErrorNotFound() { | ||||
| 	const navigate = useNavigate(); | ||||
|  | ||||
| 	return ( | ||||
| 		<div className="container-tight py-4"> | ||||
| 			<div className="empty"> | ||||
| 				<p className="empty-title">{intl.formatMessage({ id: "notfound.title" })}</p> | ||||
| 				<p className="empty-subtitle text-secondary"> | ||||
| 					{intl.formatMessage({ id: "notfound.text" })} | ||||
| 				</p> | ||||
| 				<div className="empty-action"> | ||||
| 					<Button type="button" size="md" onClick={() => navigate("/")}> | ||||
| 						{intl.formatMessage({ id: "notfound.action" })} | ||||
| 					</Button> | ||||
| 				</div> | ||||
| 			</div> | ||||
| 		</div> | ||||
| 	); | ||||
| } | ||||
							
								
								
									
										24
									
								
								frontend/src/components/Flag.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										24
									
								
								frontend/src/components/Flag.tsx
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,24 @@ | ||||
| import { IconWorld } from "@tabler/icons-react"; | ||||
| import { hasFlag } from "country-flag-icons"; | ||||
| // @ts-expect-error Creating a typing for a subfolder is not easily possible | ||||
| import Flags from "country-flag-icons/react/3x2"; | ||||
|  | ||||
| interface FlagProps { | ||||
| 	className?: string; | ||||
| 	countryCode: string; | ||||
| } | ||||
| function Flag({ className, countryCode }: FlagProps) { | ||||
| 	countryCode = countryCode.toUpperCase(); | ||||
| 	if (countryCode === "EN") { | ||||
| 		return <IconWorld className={className} width={20} />; | ||||
| 	} | ||||
|  | ||||
| 	if (hasFlag(countryCode)) { | ||||
| 		const FlagElement = Flags[countryCode] as any; | ||||
| 		return <FlagElement title={countryCode} className={className} width={20} />; | ||||
| 	} | ||||
| 	console.error(`No flag for country ${countryCode} found!`); | ||||
| 	return null; | ||||
| } | ||||
|  | ||||
| export { Flag }; | ||||
							
								
								
									
										50
									
								
								frontend/src/components/HasPermission.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										50
									
								
								frontend/src/components/HasPermission.tsx
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,50 @@ | ||||
| import type { ReactNode } from "react"; | ||||
| import Alert from "react-bootstrap/Alert"; | ||||
| import { useUser } from "src/hooks"; | ||||
| import { intl } from "src/locale"; | ||||
|  | ||||
| interface Props { | ||||
| 	permission: string; | ||||
| 	type: "manage" | "view"; | ||||
| 	hideError?: boolean; | ||||
| 	children?: ReactNode; | ||||
| } | ||||
| function HasPermission({ permission, type, children, hideError = false }: Props) { | ||||
| 	const { data } = useUser("me"); | ||||
| 	const perms = data?.permissions; | ||||
|  | ||||
| 	let allowed = permission === ""; | ||||
| 	const acceptable = ["manage", type]; | ||||
|  | ||||
| 	switch (permission) { | ||||
| 		case "admin": | ||||
| 			allowed = data?.roles?.includes("admin") || false; | ||||
| 			break; | ||||
| 		case "proxyHosts": | ||||
| 			allowed = acceptable.indexOf(perms?.proxyHosts || "") !== -1; | ||||
| 			break; | ||||
| 		case "redirectionHosts": | ||||
| 			allowed = acceptable.indexOf(perms?.redirectionHosts || "") !== -1; | ||||
| 			break; | ||||
| 		case "deadHosts": | ||||
| 			allowed = acceptable.indexOf(perms?.deadHosts || "") !== -1; | ||||
| 			break; | ||||
| 		case "streams": | ||||
| 			allowed = acceptable.indexOf(perms?.streams || "") !== -1; | ||||
| 			break; | ||||
| 		case "accessLists": | ||||
| 			allowed = acceptable.indexOf(perms?.accessLists || "") !== -1; | ||||
| 			break; | ||||
| 		case "certificates": | ||||
| 			allowed = acceptable.indexOf(perms?.certificates || "") !== -1; | ||||
| 			break; | ||||
| 	} | ||||
|  | ||||
| 	if (allowed) { | ||||
| 		return <>{children}</>; | ||||
| 	} | ||||
|  | ||||
| 	return !hideError ? <Alert variant="danger">{intl.formatMessage({ id: "no-permission-error" })}</Alert> : null; | ||||
| } | ||||
|  | ||||
| export { HasPermission }; | ||||
							
								
								
									
										3
									
								
								frontend/src/components/LoadingPage.module.css
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										3
									
								
								frontend/src/components/LoadingPage.module.css
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,3 @@ | ||||
| .logo { | ||||
| 	max-height: 100px; | ||||
| } | ||||
							
								
								
									
										27
									
								
								frontend/src/components/LoadingPage.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										27
									
								
								frontend/src/components/LoadingPage.tsx
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,27 @@ | ||||
| import { Page } from "src/components"; | ||||
| import { intl } from "src/locale"; | ||||
| import styles from "./LoadingPage.module.css"; | ||||
|  | ||||
| interface Props { | ||||
| 	label?: string; | ||||
| 	noLogo?: boolean; | ||||
| } | ||||
| export function LoadingPage({ label, noLogo }: Props) { | ||||
| 	return ( | ||||
| 		<Page className="page-center"> | ||||
| 			<div className="container-tight py-4"> | ||||
| 				<div className="empty text-center"> | ||||
| 					{noLogo ? null : ( | ||||
| 						<div className="mb-3"> | ||||
| 							<img className={styles.logo} src="/images/logo-no-text.svg" alt="" /> | ||||
| 						</div> | ||||
| 					)} | ||||
| 					<div className="text-secondary mb-3">{label || intl.formatMessage({ id: "loading" })}</div> | ||||
| 					<div className="progress progress-sm"> | ||||
| 						<div className="progress-bar progress-bar-indeterminate" /> | ||||
| 					</div> | ||||
| 				</div> | ||||
| 			</div> | ||||
| 		</Page> | ||||
| 	); | ||||
| } | ||||
							
								
								
									
										15
									
								
								frontend/src/components/LocalePicker.module.css
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										15
									
								
								frontend/src/components/LocalePicker.module.css
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,15 @@ | ||||
| .darkBtn { | ||||
| 	color: var(--tblr-light) !important; | ||||
| 	&:hover { | ||||
| 		border: var(--tblr-btn-border-width) solid transparent !important; | ||||
| 		background: color-mix(in srgb, var(--tblr-btn-hover-bg) 10%, transparent) !important; | ||||
| 	} | ||||
| } | ||||
|  | ||||
| .lightBtn { | ||||
| 	color: var(--tblr-dark) !important; | ||||
| 	&:hover { | ||||
| 		border: var(--tblr-btn-border-width) solid transparent !important; | ||||
| 		background: color-mix(in srgb, var(--tblr-btn-hover-bg) 10%, transparent) !important; | ||||
| 	} | ||||
| } | ||||
							
								
								
									
										71
									
								
								frontend/src/components/LocalePicker.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										71
									
								
								frontend/src/components/LocalePicker.tsx
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,71 @@ | ||||
| import cn from "classnames"; | ||||
| import { Flag } from "src/components"; | ||||
| import { useLocaleState } from "src/context"; | ||||
| import { useTheme } from "src/hooks"; | ||||
| import { changeLocale, getFlagCodeForLocale, intl, localeOptions } from "src/locale"; | ||||
| import styles from "./LocalePicker.module.css"; | ||||
|  | ||||
| function LocalePicker() { | ||||
| 	const { locale, setLocale } = useLocaleState(); | ||||
| 	const { getTheme } = useTheme(); | ||||
|  | ||||
| 	const changeTo = (lang: string) => { | ||||
| 		changeLocale(lang); | ||||
| 		setLocale(lang); | ||||
| 		location.reload(); | ||||
| 	}; | ||||
|  | ||||
| 	const classes = ["btn", "dropdown-toggle", "btn-sm"]; | ||||
| 	let cns = cn(...classes, "btn-ghost-light", styles.lightBtn); | ||||
| 	if (getTheme() === "dark") { | ||||
| 		cns = cn(...classes, "btn-ghost-dark", styles.darkBtn); | ||||
| 	} | ||||
|  | ||||
| 	return ( | ||||
| 		<div className="dropdown"> | ||||
| 			<button type="button" className={cns} data-bs-toggle="dropdown"> | ||||
| 				<Flag countryCode={getFlagCodeForLocale(locale)} /> | ||||
| 			</button> | ||||
| 			<div className="dropdown-menu"> | ||||
| 				{localeOptions.map((item) => { | ||||
| 					return ( | ||||
| 						<a | ||||
| 							className="dropdown-item" | ||||
| 							href={`/locale/${item[0]}`} | ||||
| 							key={`locale-${item[0]}`} | ||||
| 							onClick={(e) => { | ||||
| 								e.preventDefault(); | ||||
| 								changeTo(item[0]); | ||||
| 							}} | ||||
| 						> | ||||
| 							<Flag countryCode={getFlagCodeForLocale(item[0])} />{" "} | ||||
| 							{intl.formatMessage({ id: `locale-${item[1]}` })} | ||||
| 						</a> | ||||
| 					); | ||||
| 				})} | ||||
| 			</div> | ||||
| 		</div> | ||||
| 	); | ||||
|  | ||||
| 	// <div className={className}> | ||||
| 	// 		<Menu> | ||||
| 	// 			<MenuButton as={Button} {...additionalProps}> | ||||
| 	// 				<Flag countryCode={getFlagCodeForLocale(locale)} /> | ||||
| 	// 			</MenuButton> | ||||
| 	// 			<MenuList> | ||||
| 	// 				{localeOptions.map((item) => { | ||||
| 	// 					return ( | ||||
| 	// 						<MenuItem | ||||
| 	// 							icon={<Flag countryCode={getFlagCodeForLocale(item[0])} />} | ||||
| 	// 							onClick={() => changeTo(item[0])} | ||||
| 	// 							key={`locale-${item[0]}`}> | ||||
| 	// 							<span>{intl.formatMessage({ id: `locale-${item[1]}` })}</span> | ||||
| 	// 						</MenuItem> | ||||
| 	// 					); | ||||
| 	// 				})} | ||||
| 	// 			</MenuList> | ||||
| 	// 		</Menu> | ||||
| 	// 	</Box> | ||||
| } | ||||
|  | ||||
| export { LocalePicker }; | ||||
							
								
								
									
										29
									
								
								frontend/src/components/NavLink.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										29
									
								
								frontend/src/components/NavLink.tsx
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,29 @@ | ||||
| import { useNavigate } from "react-router-dom"; | ||||
|  | ||||
| interface Props { | ||||
| 	children: React.ReactNode; | ||||
| 	to?: string; | ||||
| 	isDropdownItem?: boolean; | ||||
| 	onClick?: () => void; | ||||
| } | ||||
| export function NavLink({ children, to, isDropdownItem, onClick }: Props) { | ||||
| 	const navigate = useNavigate(); | ||||
|  | ||||
| 	return ( | ||||
| 		<a | ||||
| 			className={isDropdownItem ? "dropdown-item" : "nav-link"} | ||||
| 			href={to} | ||||
| 			onClick={(e) => { | ||||
| 				e.preventDefault(); | ||||
| 				if (onClick) { | ||||
| 					onClick(); | ||||
| 				} | ||||
| 				if (to) { | ||||
| 					navigate(to); | ||||
| 				} | ||||
| 			}} | ||||
| 		> | ||||
| 			{children} | ||||
| 		</a> | ||||
| 	); | ||||
| } | ||||
							
								
								
									
										5
									
								
								frontend/src/components/Page.module.css
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										5
									
								
								frontend/src/components/Page.module.css
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,5 @@ | ||||
| .page { | ||||
| 	display: grid; | ||||
| 	grid-template-rows: auto 1fr auto; /* Header, Main Content, Footer */ | ||||
| 	min-height: 100vh; | ||||
| } | ||||
							
								
								
									
										10
									
								
								frontend/src/components/Page.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										10
									
								
								frontend/src/components/Page.tsx
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,10 @@ | ||||
| import cn from "classnames"; | ||||
| import styles from "./Page.module.css"; | ||||
|  | ||||
| interface Props { | ||||
| 	children: React.ReactNode; | ||||
| 	className?: string; | ||||
| } | ||||
| export function Page({ children, className }: Props) { | ||||
| 	return <div className={cn(className, styles.page)}>{children}</div>; | ||||
| } | ||||
							
								
								
									
										6
									
								
								frontend/src/components/SiteContainer.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										6
									
								
								frontend/src/components/SiteContainer.tsx
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,6 @@ | ||||
| interface Props { | ||||
| 	children: React.ReactNode; | ||||
| } | ||||
| export function SiteContainer({ children }: Props) { | ||||
| 	return <div className="container-xl py-3">{children}</div>; | ||||
| } | ||||
							
								
								
									
										64
									
								
								frontend/src/components/SiteFooter.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										64
									
								
								frontend/src/components/SiteFooter.tsx
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,64 @@ | ||||
| import { useHealth } from "src/hooks"; | ||||
| import { intl } from "src/locale"; | ||||
|  | ||||
| export function SiteFooter() { | ||||
| 	const health = useHealth(); | ||||
|  | ||||
| 	const getVersion = () => { | ||||
| 		if (!health.data) { | ||||
| 			return ""; | ||||
| 		} | ||||
| 		const v = health.data.version; | ||||
| 		return `v${v.major}.${v.minor}.${v.revision}`; | ||||
| 	}; | ||||
|  | ||||
| 	return ( | ||||
| 		<footer className="footer d-print-none py-3"> | ||||
| 			<div className="container-xl"> | ||||
| 				<div className="row text-center align-items-center flex-row-reverse"> | ||||
| 					<div className="col-lg-auto ms-lg-auto"> | ||||
| 						<ul className="list-inline list-inline-dots mb-0"> | ||||
| 							<li className="list-inline-item"> | ||||
| 								<a | ||||
| 									href="https://github.com/NginxProxyManager/nginx-proxy-manager" | ||||
| 									target="_blank" | ||||
| 									className="link-secondary" | ||||
| 									rel="noopener" | ||||
| 								> | ||||
| 									{intl.formatMessage({ id: "footer.github-fork" })} | ||||
| 								</a> | ||||
| 							</li> | ||||
| 						</ul> | ||||
| 					</div> | ||||
| 					<div className="col-12 col-lg-auto mt-3 mt-lg-0"> | ||||
| 						<ul className="list-inline list-inline-dots mb-0"> | ||||
| 							<li className="list-inline-item"> | ||||
| 								© 2025{" "} | ||||
| 								<a href="https://jc21.com" rel="noreferrer" target="_blank" className="link-secondary"> | ||||
| 									jc21.com | ||||
| 								</a> | ||||
| 							</li> | ||||
| 							<li className="list-inline-item"> | ||||
| 								Theme by{" "} | ||||
| 								<a href="https://tabler.io" rel="noreferrer" target="_blank" className="link-secondary"> | ||||
| 									Tabler | ||||
| 								</a> | ||||
| 							</li> | ||||
| 							<li className="list-inline-item"> | ||||
| 								<a | ||||
| 									href={`https://github.com/NginxProxyManager/nginx-proxy-manager/releases/tag/${getVersion()}`} | ||||
| 									className="link-secondary" | ||||
| 									target="_blank" | ||||
| 									rel="noopener" | ||||
| 								> | ||||
| 									{" "} | ||||
| 									{getVersion()}{" "} | ||||
| 								</a> | ||||
| 							</li> | ||||
| 						</ul> | ||||
| 					</div> | ||||
| 				</div> | ||||
| 			</div> | ||||
| 		</footer> | ||||
| 	); | ||||
| } | ||||
							
								
								
									
										8
									
								
								frontend/src/components/SiteHeader.module.css
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										8
									
								
								frontend/src/components/SiteHeader.module.css
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,8 @@ | ||||
| .logo { | ||||
| 	font-size: 1.1rem; | ||||
| 	font-weight: 500; | ||||
|  | ||||
| 	img { | ||||
| 		margin-right: 0.8rem; | ||||
| 	} | ||||
| } | ||||
							
								
								
									
										119
									
								
								frontend/src/components/SiteHeader.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										119
									
								
								frontend/src/components/SiteHeader.tsx
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,119 @@ | ||||
| import { IconLock, IconLogout, IconUser } from "@tabler/icons-react"; | ||||
| import { useState } from "react"; | ||||
| import { LocalePicker, ThemeSwitcher } from "src/components"; | ||||
| import { useAuthState } from "src/context"; | ||||
| import { useUser } from "src/hooks"; | ||||
| import { intl } from "src/locale"; | ||||
| import { ChangePasswordModal, UserModal } from "src/modals"; | ||||
| import styles from "./SiteHeader.module.css"; | ||||
|  | ||||
| export function SiteHeader() { | ||||
| 	const { data: currentUser } = useUser("me"); | ||||
| 	const isAdmin = currentUser?.roles.includes("admin"); | ||||
| 	const { logout } = useAuthState(); | ||||
| 	const [showProfileEdit, setShowProfileEdit] = useState(false); | ||||
| 	const [showChangePassword, setShowChangePassword] = useState(false); | ||||
|  | ||||
| 	return ( | ||||
| 		<header className="navbar navbar-expand-md d-print-none"> | ||||
| 			<div className="container-xl"> | ||||
| 				<button | ||||
| 					className="navbar-toggler" | ||||
| 					type="button" | ||||
| 					data-bs-toggle="collapse" | ||||
| 					data-bs-target="#navbar-menu" | ||||
| 					aria-controls="navbar-menu" | ||||
| 					aria-expanded="false" | ||||
| 					aria-label="Toggle navigation" | ||||
| 				> | ||||
| 					<span className="navbar-toggler-icon" /> | ||||
| 				</button> | ||||
| 				<div className="navbar-brand navbar-brand-autodark d-none-navbar-horizontal pe-0 pe-md-3"> | ||||
| 					<span className={styles.logo}> | ||||
| 						<img | ||||
| 							src="/images/logo-no-text.svg" | ||||
| 							width={40} | ||||
| 							height={40} | ||||
| 							className="navbar-brand-image" | ||||
| 							alt="Logo" | ||||
| 						/> | ||||
| 						Nginx Proxy Manager | ||||
| 					</span> | ||||
| 				</div> | ||||
| 				<div className="navbar-nav flex-row order-md-last"> | ||||
| 					<div className="d-none d-md-flex"> | ||||
| 						<div className="nav-item"> | ||||
| 							<LocalePicker /> | ||||
| 						</div> | ||||
| 						<div className="nav-item"> | ||||
| 							<ThemeSwitcher /> | ||||
| 						</div> | ||||
| 					</div> | ||||
| 					<div className="nav-item d-none d-md-flex me-3"> | ||||
| 						<div className="nav-item dropdown"> | ||||
| 							<a | ||||
| 								href="/" | ||||
| 								className="nav-link d-flex lh-1 p-0 px-2" | ||||
| 								data-bs-toggle="dropdown" | ||||
| 								aria-label="Open user menu" | ||||
| 							> | ||||
| 								<span | ||||
| 									className="avatar avatar-sm" | ||||
| 									style={{ | ||||
| 										backgroundImage: `url(${currentUser?.avatar || "/images/default-avatar.jpg"})`, | ||||
| 									}} | ||||
| 								/> | ||||
| 								<div className="d-none d-xl-block ps-2"> | ||||
| 									<div>{currentUser?.nickname}</div> | ||||
| 									<div className="mt-1 small text-secondary"> | ||||
| 										{intl.formatMessage({ id: isAdmin ? "administrator" : "standard-user" })} | ||||
| 									</div> | ||||
| 								</div> | ||||
| 							</a> | ||||
| 							<div className="dropdown-menu dropdown-menu-end dropdown-menu-arrow"> | ||||
| 								<a | ||||
| 									href="?" | ||||
| 									className="dropdown-item" | ||||
| 									onClick={(e) => { | ||||
| 										e.preventDefault(); | ||||
| 										setShowProfileEdit(true); | ||||
| 									}} | ||||
| 								> | ||||
| 									<IconUser width={18} /> | ||||
| 									{intl.formatMessage({ id: "user.edit-profile" })} | ||||
| 								</a> | ||||
| 								<a | ||||
| 									href="?" | ||||
| 									className="dropdown-item" | ||||
| 									onClick={(e) => { | ||||
| 										e.preventDefault(); | ||||
| 										setShowChangePassword(true); | ||||
| 									}} | ||||
| 								> | ||||
| 									<IconLock width={18} /> | ||||
| 									{intl.formatMessage({ id: "user.change-password" })} | ||||
| 								</a> | ||||
| 								<div className="dropdown-divider" /> | ||||
| 								<a | ||||
| 									href="?" | ||||
| 									className="dropdown-item" | ||||
| 									onClick={(e) => { | ||||
| 										e.preventDefault(); | ||||
| 										logout(); | ||||
| 									}} | ||||
| 								> | ||||
| 									<IconLogout width={18} /> | ||||
| 									{intl.formatMessage({ id: "user.logout" })} | ||||
| 								</a> | ||||
| 							</div> | ||||
| 						</div> | ||||
| 					</div> | ||||
| 				</div> | ||||
| 			</div> | ||||
| 			{showProfileEdit ? <UserModal userId="me" onClose={() => setShowProfileEdit(false)} /> : null} | ||||
| 			{showChangePassword ? ( | ||||
| 				<ChangePasswordModal userId="me" onClose={() => setShowChangePassword(false)} /> | ||||
| 			) : null} | ||||
| 		</header> | ||||
| 	); | ||||
| } | ||||
							
								
								
									
										195
									
								
								frontend/src/components/SiteMenu.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										195
									
								
								frontend/src/components/SiteMenu.tsx
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,195 @@ | ||||
| import { | ||||
| 	IconBook, | ||||
| 	IconDeviceDesktop, | ||||
| 	IconHome, | ||||
| 	IconLock, | ||||
| 	IconSettings, | ||||
| 	IconShield, | ||||
| 	IconUser, | ||||
| } from "@tabler/icons-react"; | ||||
| import cn from "classnames"; | ||||
| import React from "react"; | ||||
| import { HasPermission, NavLink } from "src/components"; | ||||
| import { intl } from "src/locale"; | ||||
|  | ||||
| interface MenuItem { | ||||
| 	label: string; | ||||
| 	icon?: React.ElementType; | ||||
| 	to?: string; | ||||
| 	items?: MenuItem[]; | ||||
| 	permission?: string; | ||||
| 	permissionType?: "view" | "manage"; | ||||
| } | ||||
|  | ||||
| const menuItems: MenuItem[] = [ | ||||
| 	{ | ||||
| 		to: "/", | ||||
| 		icon: IconHome, | ||||
| 		label: "dashboard.title", | ||||
| 	}, | ||||
| 	{ | ||||
| 		icon: IconDeviceDesktop, | ||||
| 		label: "hosts.title", | ||||
| 		items: [ | ||||
| 			{ | ||||
| 				to: "/nginx/proxy", | ||||
| 				label: "proxy-hosts.title", | ||||
| 				permission: "proxyHosts", | ||||
| 				permissionType: "view", | ||||
| 			}, | ||||
| 			{ | ||||
| 				to: "/nginx/redirection", | ||||
| 				label: "redirection-hosts.title", | ||||
| 				permission: "redirectionHosts", | ||||
| 				permissionType: "view", | ||||
| 			}, | ||||
| 			{ | ||||
| 				to: "/nginx/stream", | ||||
| 				label: "streams.title", | ||||
| 				permission: "streams", | ||||
| 				permissionType: "view", | ||||
| 			}, | ||||
| 			{ | ||||
| 				to: "/nginx/404", | ||||
| 				label: "dead-hosts.title", | ||||
| 				permission: "deadHosts", | ||||
| 				permissionType: "view", | ||||
| 			}, | ||||
| 		], | ||||
| 	}, | ||||
| 	{ | ||||
| 		to: "/access", | ||||
| 		icon: IconLock, | ||||
| 		label: "access.title", | ||||
| 		permission: "accessLists", | ||||
| 		permissionType: "view", | ||||
| 	}, | ||||
| 	{ | ||||
| 		to: "/certificates", | ||||
| 		icon: IconShield, | ||||
| 		label: "certificates.title", | ||||
| 		permission: "certificates", | ||||
| 		permissionType: "view", | ||||
| 	}, | ||||
| 	{ | ||||
| 		to: "/users", | ||||
| 		icon: IconUser, | ||||
| 		label: "users.title", | ||||
| 		permission: "admin", | ||||
| 	}, | ||||
| 	{ | ||||
| 		to: "/audit-log", | ||||
| 		icon: IconBook, | ||||
| 		label: "auditlog.title", | ||||
| 		permission: "admin", | ||||
| 	}, | ||||
| 	{ | ||||
| 		to: "/settings", | ||||
| 		icon: IconSettings, | ||||
| 		label: "settings.title", | ||||
| 		permission: "admin", | ||||
| 	}, | ||||
| ]; | ||||
|  | ||||
| const getMenuItem = (item: MenuItem, onClick?: () => void) => { | ||||
| 	if (item.items && item.items.length > 0) { | ||||
| 		return getMenuDropown(item, onClick); | ||||
| 	} | ||||
|  | ||||
| 	return ( | ||||
| 		<HasPermission | ||||
| 			key={`item-${item.label}`} | ||||
| 			permission={item.permission || ""} | ||||
| 			type={item.permissionType || "view"} | ||||
| 			hideError | ||||
| 		> | ||||
| 			<li className="nav-item"> | ||||
| 				<NavLink to={item.to} onClick={onClick}> | ||||
| 					<span className="nav-link-icon d-md-none d-lg-inline-block"> | ||||
| 						{item.icon && React.createElement(item.icon, { height: 24, width: 24 })} | ||||
| 					</span> | ||||
| 					<span className="nav-link-title">{intl.formatMessage({ id: item.label })}</span> | ||||
| 				</NavLink> | ||||
| 			</li> | ||||
| 		</HasPermission> | ||||
| 	); | ||||
| }; | ||||
|  | ||||
| const getMenuDropown = (item: MenuItem, onClick?: () => void) => { | ||||
| 	const cns = cn("nav-item", "dropdown"); | ||||
| 	return ( | ||||
| 		<HasPermission | ||||
| 			key={`item-${item.label}`} | ||||
| 			permission={item.permission || ""} | ||||
| 			type={item.permissionType || "view"} | ||||
| 			hideError | ||||
| 		> | ||||
| 			<li className={cns}> | ||||
| 				<a | ||||
| 					className="nav-link dropdown-toggle" | ||||
| 					href={item.to} | ||||
| 					data-bs-toggle="dropdown" | ||||
| 					data-bs-auto-close="outside" | ||||
| 					aria-expanded="false" | ||||
| 					role="button" | ||||
| 				> | ||||
| 					<span className="nav-link-icon d-md-none d-lg-inline-block"> | ||||
| 						<IconDeviceDesktop height={24} width={24} /> | ||||
| 					</span> | ||||
| 					<span className="nav-link-title">{intl.formatMessage({ id: item.label })}</span> | ||||
| 				</a> | ||||
| 				<div className="dropdown-menu"> | ||||
| 					{item.items?.map((subitem, idx) => { | ||||
| 						return ( | ||||
| 							<HasPermission | ||||
| 								key={`${idx}-${subitem.to}`} | ||||
| 								permission={subitem.permission || ""} | ||||
| 								type={subitem.permissionType || "view"} | ||||
| 								hideError | ||||
| 							> | ||||
| 								<NavLink to={subitem.to} isDropdownItem onClick={onClick}> | ||||
| 									{intl.formatMessage({ id: subitem.label })} | ||||
| 								</NavLink> | ||||
| 							</HasPermission> | ||||
| 						); | ||||
| 					})} | ||||
| 				</div> | ||||
| 			</li> | ||||
| 		</HasPermission> | ||||
| 	); | ||||
| }; | ||||
|  | ||||
| export function SiteMenu() { | ||||
| 	// This is hacky AF. But that's the price of using a non-react UI kit. | ||||
| 	const closeMenus = () => { | ||||
| 		const navMenus = document.querySelectorAll(".nav-item.dropdown"); | ||||
| 		navMenus.forEach((menu) => { | ||||
| 			menu.classList.remove("show"); | ||||
| 			const dropdown = menu.querySelector(".dropdown-menu"); | ||||
| 			if (dropdown) { | ||||
| 				dropdown.classList.remove("show"); | ||||
| 			} | ||||
| 		}); | ||||
| 	}; | ||||
|  | ||||
| 	return ( | ||||
| 		<header className="navbar-expand-md"> | ||||
| 			<div className="collapse navbar-collapse"> | ||||
| 				<div className="navbar"> | ||||
| 					<div className="container-xl"> | ||||
| 						<div className="row flex-column flex-md-row flex-fill align-items-center"> | ||||
| 							<div className="col"> | ||||
| 								<ul className="navbar-nav"> | ||||
| 									{menuItems.length > 0 && | ||||
| 										menuItems.map((item) => { | ||||
| 											return getMenuItem(item, closeMenus); | ||||
| 										})} | ||||
| 								</ul> | ||||
| 							</div> | ||||
| 						</div> | ||||
| 					</div> | ||||
| 				</div> | ||||
| 			</div> | ||||
| 		</header> | ||||
| 	); | ||||
| } | ||||
							
								
								
									
										16
									
								
								frontend/src/components/Table/EmptyRow.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										16
									
								
								frontend/src/components/Table/EmptyRow.tsx
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,16 @@ | ||||
| import type { Table as ReactTable } from "@tanstack/react-table"; | ||||
|  | ||||
| interface Props { | ||||
| 	tableInstance: ReactTable<any>; | ||||
| } | ||||
| function EmptyRow({ tableInstance }: Props) { | ||||
| 	return ( | ||||
| 		<tr> | ||||
| 			<td colSpan={tableInstance.getVisibleFlatColumns().length}> | ||||
| 				<p className="text-center">There are no items</p> | ||||
| 			</td> | ||||
| 		</tr> | ||||
| 	); | ||||
| } | ||||
|  | ||||
| export { EmptyRow }; | ||||
| @@ -0,0 +1,13 @@ | ||||
| import type { Certificate } from "src/api/backend"; | ||||
| import { intl } from "src/locale"; | ||||
|  | ||||
| interface Props { | ||||
| 	certificate?: Certificate; | ||||
| } | ||||
| export function CertificateFormatter({ certificate }: Props) { | ||||
| 	if (certificate) { | ||||
| 		return intl.formatMessage({ id: "lets-encrypt" }); | ||||
| 	} | ||||
|  | ||||
| 	return intl.formatMessage({ id: "http-only" }); | ||||
| } | ||||
							
								
								
									
										25
									
								
								frontend/src/components/Table/Formatter/DomainsFormatter.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										25
									
								
								frontend/src/components/Table/Formatter/DomainsFormatter.tsx
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,25 @@ | ||||
| import { intlFormat, parseISO } from "date-fns"; | ||||
| import { intl } from "src/locale"; | ||||
|  | ||||
| interface Props { | ||||
| 	domains: string[]; | ||||
| 	createdOn?: string; | ||||
| } | ||||
| export function DomainsFormatter({ domains, createdOn }: Props) { | ||||
| 	return ( | ||||
| 		<div className="flex-fill"> | ||||
| 			<div className="font-weight-medium"> | ||||
| 				{domains.map((domain: string) => ( | ||||
| 					<span key={domain} className="badge badge-lg domain-name"> | ||||
| 						{domain} | ||||
| 					</span> | ||||
| 				))} | ||||
| 			</div> | ||||
| 			{createdOn ? ( | ||||
| 				<div className="text-secondary mt-1"> | ||||
| 					{intl.formatMessage({ id: "created-on" }, { date: intlFormat(parseISO(createdOn)) })} | ||||
| 				</div> | ||||
| 			) : null} | ||||
| 		</div> | ||||
| 	); | ||||
| } | ||||
| @@ -0,0 +1,17 @@ | ||||
| interface Props { | ||||
| 	url: string; | ||||
| 	name?: string; | ||||
| } | ||||
| export function GravatarFormatter({ url, name }: Props) { | ||||
| 	return ( | ||||
| 		<div className="d-flex py-1 align-items-center"> | ||||
| 			<span | ||||
| 				title={name} | ||||
| 				className="avatar avatar-2 me-2" | ||||
| 				style={{ | ||||
| 					backgroundImage: `url(${url})`, | ||||
| 				}} | ||||
| 			/> | ||||
| 		</div> | ||||
| 	); | ||||
| } | ||||
							
								
								
									
										11
									
								
								frontend/src/components/Table/Formatter/StatusFormatter.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										11
									
								
								frontend/src/components/Table/Formatter/StatusFormatter.tsx
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,11 @@ | ||||
| import { intl } from "src/locale"; | ||||
|  | ||||
| interface Props { | ||||
| 	enabled: boolean; | ||||
| } | ||||
| export function StatusFormatter({ enabled }: Props) { | ||||
| 	if (enabled) { | ||||
| 		return <span className="badge bg-lime-lt">{intl.formatMessage({ id: "online" })}</span>; | ||||
| 	} | ||||
| 	return <span className="badge bg-red-lt">{intl.formatMessage({ id: "offline" })}</span>; | ||||
| } | ||||
| @@ -0,0 +1,21 @@ | ||||
| import { intlFormat, parseISO } from "date-fns"; | ||||
| import { intl } from "src/locale"; | ||||
|  | ||||
| interface Props { | ||||
| 	value: string; | ||||
| 	createdOn?: string; | ||||
| } | ||||
| export function ValueWithDateFormatter({ value, createdOn }: Props) { | ||||
| 	return ( | ||||
| 		<div className="flex-fill"> | ||||
| 			<div className="font-weight-medium"> | ||||
| 				<div className="font-weight-medium">{value}</div> | ||||
| 			</div> | ||||
| 			{createdOn ? ( | ||||
| 				<div className="text-secondary mt-1"> | ||||
| 					{intl.formatMessage({ id: "created-on" }, { date: intlFormat(parseISO(createdOn)) })} | ||||
| 				</div> | ||||
| 			) : null} | ||||
| 		</div> | ||||
| 	); | ||||
| } | ||||
							
								
								
									
										5
									
								
								frontend/src/components/Table/Formatter/index.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										5
									
								
								frontend/src/components/Table/Formatter/index.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,5 @@ | ||||
| export * from "./CertificateFormatter"; | ||||
| export * from "./DomainsFormatter"; | ||||
| export * from "./GravatarFormatter"; | ||||
| export * from "./StatusFormatter"; | ||||
| export * from "./ValueWithDateFormatter"; | ||||
							
								
								
									
										39
									
								
								frontend/src/components/Table/TableBody.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										39
									
								
								frontend/src/components/Table/TableBody.tsx
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,39 @@ | ||||
| import { flexRender } from "@tanstack/react-table"; | ||||
| import type { TableLayoutProps } from "src/components"; | ||||
| import { EmptyRow } from "./EmptyRow"; | ||||
|  | ||||
| function TableBody<T>(props: TableLayoutProps<T>) { | ||||
| 	const { tableInstance, extraStyles, emptyState } = props; | ||||
| 	const rows = tableInstance.getRowModel().rows; | ||||
|  | ||||
| 	if (rows.length === 0) { | ||||
| 		return emptyState ? ( | ||||
| 			emptyState | ||||
| 		) : ( | ||||
| 			<tbody className="table-tbody"> | ||||
| 				<EmptyRow tableInstance={tableInstance} /> | ||||
| 			</tbody> | ||||
| 		); | ||||
| 	} | ||||
|  | ||||
| 	return ( | ||||
| 		<tbody className="table-tbody"> | ||||
| 			{rows.map((row: any) => { | ||||
| 				return ( | ||||
| 					<tr key={row.id} {...extraStyles?.row(row.original)}> | ||||
| 						{row.getVisibleCells().map((cell: any) => { | ||||
| 							const { className } = (cell.column.columnDef.meta as any) ?? {}; | ||||
| 							return ( | ||||
| 								<td key={cell.id} className={className}> | ||||
| 									{flexRender(cell.column.columnDef.cell, cell.getContext())} | ||||
| 								</td> | ||||
| 							); | ||||
| 						})} | ||||
| 					</tr> | ||||
| 				); | ||||
| 			})} | ||||
| 		</tbody> | ||||
| 	); | ||||
| } | ||||
|  | ||||
| export { TableBody }; | ||||
							
								
								
									
										26
									
								
								frontend/src/components/Table/TableHeader.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										26
									
								
								frontend/src/components/Table/TableHeader.tsx
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,26 @@ | ||||
| import type { TableLayoutProps } from "src/components"; | ||||
|  | ||||
| function TableHeader<T>(props: TableLayoutProps<T>) { | ||||
| 	const { tableInstance } = props; | ||||
| 	const headerGroups = tableInstance.getHeaderGroups(); | ||||
|  | ||||
| 	return ( | ||||
| 		<thead> | ||||
| 			{headerGroups.map((headerGroup: any) => ( | ||||
| 				<tr key={headerGroup.id}> | ||||
| 					{headerGroup.headers.map((header: any) => { | ||||
| 						const { column } = header; | ||||
| 						const { className } = (column.columnDef.meta as any) ?? {}; | ||||
| 						return ( | ||||
| 							<th key={header.id} className={className}> | ||||
| 								{typeof column.columnDef.header === "string" ? `${column.columnDef.header}` : null} | ||||
| 							</th> | ||||
| 						); | ||||
| 					})} | ||||
| 				</tr> | ||||
| 			))} | ||||
| 		</thead> | ||||
| 	); | ||||
| } | ||||
|  | ||||
| export { TableHeader }; | ||||
							
								
								
									
										64
									
								
								frontend/src/components/Table/TableHelpers.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										64
									
								
								frontend/src/components/Table/TableHelpers.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,64 @@ | ||||
| export interface TablePagination { | ||||
| 	limit: number; | ||||
| 	offset: number; | ||||
| 	total: number; | ||||
| } | ||||
|  | ||||
| export interface TableSortBy { | ||||
| 	id: string; | ||||
| 	desc: boolean; | ||||
| } | ||||
|  | ||||
| export interface TableFilter { | ||||
| 	id: string; | ||||
| 	value: any; | ||||
| } | ||||
|  | ||||
| const tableEvents = { | ||||
| 	FILTERS_CHANGED: "FILTERS_CHANGED", | ||||
| 	PAGE_CHANGED: "PAGE_CHANGED", | ||||
| 	PAGE_SIZE_CHANGED: "PAGE_SIZE_CHANGED", | ||||
| 	TOTAL_COUNT_CHANGED: "TOTAL_COUNT_CHANGED", | ||||
| 	SORT_CHANGED: "SORT_CHANGED", | ||||
| }; | ||||
|  | ||||
| const tableEventReducer = (state: any, { type, payload }: any) => { | ||||
| 	let offset = state.offset; | ||||
| 	switch (type) { | ||||
| 		case tableEvents.PAGE_CHANGED: | ||||
| 			return { | ||||
| 				...state, | ||||
| 				offset: payload * state.limit, | ||||
| 			}; | ||||
| 		case tableEvents.PAGE_SIZE_CHANGED: | ||||
| 			return { | ||||
| 				...state, | ||||
| 				limit: payload, | ||||
| 			}; | ||||
| 		case tableEvents.TOTAL_COUNT_CHANGED: | ||||
| 			return { | ||||
| 				...state, | ||||
| 				total: payload, | ||||
| 			}; | ||||
| 		case tableEvents.SORT_CHANGED: | ||||
| 			return { | ||||
| 				...state, | ||||
| 				sortBy: payload, | ||||
| 			}; | ||||
| 		case tableEvents.FILTERS_CHANGED: | ||||
| 			if (state.filters !== payload) { | ||||
| 				// this actually was a legit change | ||||
| 				// sets to page 1 when filter is modified | ||||
| 				offset = 0; | ||||
| 			} | ||||
| 			return { | ||||
| 				...state, | ||||
| 				filters: payload, | ||||
| 				offset, | ||||
| 			}; | ||||
| 		default: | ||||
| 			throw new Error(`Unhandled action type: ${type}`); | ||||
| 	} | ||||
| }; | ||||
|  | ||||
| export { tableEvents, tableEventReducer }; | ||||
							
								
								
									
										22
									
								
								frontend/src/components/Table/TableLayout.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										22
									
								
								frontend/src/components/Table/TableLayout.tsx
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,22 @@ | ||||
| import type { Table as ReactTable } from "@tanstack/react-table"; | ||||
| import { TableBody } from "./TableBody"; | ||||
| import { TableHeader } from "./TableHeader"; | ||||
|  | ||||
| interface TableLayoutProps<TFields> { | ||||
| 	tableInstance: ReactTable<TFields>; | ||||
| 	emptyState?: React.ReactNode; | ||||
| 	extraStyles?: { | ||||
| 		row: (rowData: TFields) => any | undefined; | ||||
| 	}; | ||||
| } | ||||
| function TableLayout<TFields>(props: TableLayoutProps<TFields>) { | ||||
| 	const hasRows = props.tableInstance.getRowModel().rows.length > 0; | ||||
| 	return ( | ||||
| 		<table className="table table-vcenter table-selectable mb-0"> | ||||
| 			{hasRows ? <TableHeader tableInstance={props.tableInstance} /> : null} | ||||
| 			<TableBody {...props} /> | ||||
| 		</table> | ||||
| 	); | ||||
| } | ||||
|  | ||||
| export { TableLayout, type TableLayoutProps }; | ||||
							
								
								
									
										4
									
								
								frontend/src/components/Table/index.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										4
									
								
								frontend/src/components/Table/index.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,4 @@ | ||||
| export * from "./Formatter"; | ||||
| export * from "./TableHeader"; | ||||
| export * from "./TableHelpers"; | ||||
| export * from "./TableLayout"; | ||||
							
								
								
									
										15
									
								
								frontend/src/components/ThemeSwitcher.module.css
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										15
									
								
								frontend/src/components/ThemeSwitcher.module.css
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,15 @@ | ||||
| .darkBtn { | ||||
| 	color: var(--tblr-light) !important; | ||||
| 	&:hover { | ||||
| 		border: var(--tblr-btn-border-width) solid transparent !important; | ||||
| 		background: color-mix(in srgb, var(--tblr-btn-hover-bg) 10%, transparent) !important; | ||||
| 	} | ||||
| } | ||||
|  | ||||
| .lightBtn { | ||||
| 	color: var(--tblr-dark) !important; | ||||
| 	&:hover { | ||||
| 		border: var(--tblr-btn-border-width) solid transparent !important; | ||||
| 		background: color-mix(in srgb, var(--tblr-btn-hover-bg) 10%, transparent) !important; | ||||
| 	} | ||||
| } | ||||
							
								
								
									
										41
									
								
								frontend/src/components/ThemeSwitcher.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										41
									
								
								frontend/src/components/ThemeSwitcher.tsx
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,41 @@ | ||||
| import { IconMoon, IconSun } from "@tabler/icons-react"; | ||||
| import cn from "classnames"; | ||||
| import { Button } from "src/components"; | ||||
| import { useTheme } from "src/hooks"; | ||||
| import styles from "./ThemeSwitcher.module.css"; | ||||
|  | ||||
| interface Props { | ||||
| 	className?: string; | ||||
| } | ||||
| function ThemeSwitcher({ className }: Props) { | ||||
| 	const { setTheme } = useTheme(); | ||||
|  | ||||
| 	return ( | ||||
| 		<div className={cn("d-print-none", "d-inline-block", className)}> | ||||
| 			<Button | ||||
| 				size="sm" | ||||
| 				className={cn("btn-ghost-dark", "hide-theme-dark", styles.lightBtn)} | ||||
| 				data-bs-toggle="tooltip" | ||||
| 				data-bs-placement="bottom" | ||||
| 				aria-label="Enable dark mode" | ||||
| 				data-bs-original-title="Enable dark mode" | ||||
| 				onClick={() => setTheme("dark")} | ||||
| 			> | ||||
| 				<IconMoon width={24} /> | ||||
| 			</Button> | ||||
| 			<Button | ||||
| 				size="sm" | ||||
| 				className={cn("btn-ghost-light", "hide-theme-light", styles.darkBtn)} | ||||
| 				data-bs-toggle="tooltip" | ||||
| 				data-bs-placement="bottom" | ||||
| 				aria-label="Enable dark mode" | ||||
| 				data-bs-original-title="Enable dark mode" | ||||
| 				onClick={() => setTheme("light")} | ||||
| 			> | ||||
| 				<IconSun width={24} /> | ||||
| 			</Button> | ||||
| 		</div> | ||||
| 	); | ||||
| } | ||||
|  | ||||
| export { ThemeSwitcher }; | ||||
							
								
								
									
										17
									
								
								frontend/src/components/Unhealthy.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										17
									
								
								frontend/src/components/Unhealthy.tsx
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,17 @@ | ||||
| import { Page } from "src/components"; | ||||
|  | ||||
| export function Unhealthy() { | ||||
| 	return ( | ||||
| 		<Page className="page-center"> | ||||
| 			<div className="container-tight py-4"> | ||||
| 				<div className="empty"> | ||||
| 					<div className="empty-img"> | ||||
| 						<img src="/images/unhealthy.svg" alt="" /> | ||||
| 					</div> | ||||
| 					<p className="empty-title">The API is not healthy.</p> | ||||
| 					<p className="empty-subtitle text-secondary">We'll keep checking and hope to be back soon!</p> | ||||
| 				</div> | ||||
| 			</div> | ||||
| 		</Page> | ||||
| 	); | ||||
| } | ||||
							
								
								
									
										15
									
								
								frontend/src/components/index.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										15
									
								
								frontend/src/components/index.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,15 @@ | ||||
| export * from "./Button"; | ||||
| export * from "./ErrorNotFound"; | ||||
| export * from "./Flag"; | ||||
| export * from "./HasPermission"; | ||||
| export * from "./LoadingPage"; | ||||
| export * from "./LocalePicker"; | ||||
| export * from "./NavLink"; | ||||
| export * from "./Page"; | ||||
| export * from "./SiteContainer"; | ||||
| export * from "./SiteFooter"; | ||||
| export * from "./SiteHeader"; | ||||
| export * from "./SiteMenu"; | ||||
| export * from "./Table"; | ||||
| export * from "./ThemeSwitcher"; | ||||
| export * from "./Unhealthy"; | ||||
							
								
								
									
										72
									
								
								frontend/src/context/AuthContext.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										72
									
								
								frontend/src/context/AuthContext.tsx
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,72 @@ | ||||
| import { useQueryClient } from "@tanstack/react-query"; | ||||
| import { createContext, type ReactNode, useContext, useState } from "react"; | ||||
| import { useIntervalWhen } from "rooks"; | ||||
| import { getToken, refreshToken, type TokenResponse } from "src/api/backend"; | ||||
| import AuthStore from "src/modules/AuthStore"; | ||||
|  | ||||
| // Context | ||||
| export interface AuthContextType { | ||||
| 	authenticated: boolean; | ||||
| 	login: (username: string, password: string) => Promise<void>; | ||||
| 	logout: () => void; | ||||
| 	token?: string; | ||||
| } | ||||
|  | ||||
| const initalValue = null; | ||||
| const AuthContext = createContext<AuthContextType | null>(initalValue); | ||||
|  | ||||
| // Provider | ||||
| interface Props { | ||||
| 	children?: ReactNode; | ||||
| 	tokenRefreshInterval?: number; | ||||
| } | ||||
| function AuthProvider({ children, tokenRefreshInterval = 5 * 60 * 1000 }: Props) { | ||||
| 	const queryClient = useQueryClient(); | ||||
| 	const [authenticated, setAuthenticated] = useState(AuthStore.hasActiveToken()); | ||||
|  | ||||
| 	const handleTokenUpdate = (response: TokenResponse) => { | ||||
| 		AuthStore.set(response); | ||||
| 		setAuthenticated(true); | ||||
| 	}; | ||||
|  | ||||
| 	const login = async (identity: string, secret: string) => { | ||||
| 		const response = await getToken({ payload: { identity, secret } }); | ||||
| 		handleTokenUpdate(response); | ||||
| 	}; | ||||
|  | ||||
| 	const logout = () => { | ||||
| 		AuthStore.clear(); | ||||
| 		setAuthenticated(false); | ||||
| 		queryClient.clear(); | ||||
| 	}; | ||||
|  | ||||
| 	const refresh = async () => { | ||||
| 		const response = await refreshToken(); | ||||
| 		handleTokenUpdate(response); | ||||
| 	}; | ||||
|  | ||||
| 	useIntervalWhen( | ||||
| 		() => { | ||||
| 			if (authenticated) { | ||||
| 				refresh(); | ||||
| 			} | ||||
| 		}, | ||||
| 		tokenRefreshInterval, | ||||
| 		true, | ||||
| 	); | ||||
|  | ||||
| 	const value = { authenticated, login, logout }; | ||||
|  | ||||
| 	return <AuthContext.Provider value={value}>{children}</AuthContext.Provider>; | ||||
| } | ||||
|  | ||||
| function useAuthState() { | ||||
| 	const context = useContext(AuthContext); | ||||
| 	if (!context) { | ||||
| 		throw new Error("useAuthState must be used within a AuthProvider"); | ||||
| 	} | ||||
| 	return context; | ||||
| } | ||||
|  | ||||
| export { AuthProvider, useAuthState }; | ||||
| export default AuthContext; | ||||
							
								
								
									
										38
									
								
								frontend/src/context/LocaleContext.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										38
									
								
								frontend/src/context/LocaleContext.tsx
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,38 @@ | ||||
| import { createContext, type ReactNode, useContext, useState } from "react"; | ||||
| import { getLocale } from "src/locale"; | ||||
|  | ||||
| // Context | ||||
| export interface LocaleContextType { | ||||
| 	setLocale: (locale: string) => void; | ||||
| 	locale?: string; | ||||
| } | ||||
|  | ||||
| const initalValue = null; | ||||
| const LocaleContext = createContext<LocaleContextType | null>(initalValue); | ||||
|  | ||||
| // Provider | ||||
| interface Props { | ||||
| 	children?: ReactNode; | ||||
| } | ||||
| function LocaleProvider({ children }: Props) { | ||||
| 	const [locale, setLocaleValue] = useState(getLocale()); | ||||
|  | ||||
| 	const setLocale = async (locale: string) => { | ||||
| 		setLocaleValue(locale); | ||||
| 	}; | ||||
|  | ||||
| 	const value = { locale, setLocale }; | ||||
|  | ||||
| 	return <LocaleContext.Provider value={value}>{children}</LocaleContext.Provider>; | ||||
| } | ||||
|  | ||||
| function useLocaleState() { | ||||
| 	const context = useContext(LocaleContext); | ||||
| 	if (!context) { | ||||
| 		throw new Error("useLocaleState must be used within a LocaleProvider"); | ||||
| 	} | ||||
| 	return context; | ||||
| } | ||||
|  | ||||
| export { LocaleProvider, useLocaleState }; | ||||
| export default LocaleContext; | ||||
							
								
								
									
										68
									
								
								frontend/src/context/ThemeContext.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										68
									
								
								frontend/src/context/ThemeContext.tsx
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,68 @@ | ||||
| import type React from "react"; | ||||
| import { createContext, type ReactNode, useContext, useEffect, useState } from "react"; | ||||
|  | ||||
| const StorageKey = "tabler-theme"; | ||||
| export const Light = "light"; | ||||
| export const Dark = "dark"; | ||||
|  | ||||
| // Define theme types | ||||
| export type Theme = "light" | "dark"; | ||||
|  | ||||
| interface ThemeContextType { | ||||
| 	theme: Theme; | ||||
| 	toggleTheme: () => void; | ||||
| 	setTheme: (theme: Theme) => void; | ||||
| 	getTheme: () => Theme; | ||||
| } | ||||
|  | ||||
| const ThemeContext = createContext<ThemeContextType | undefined>(undefined); | ||||
|  | ||||
| interface ThemeProviderProps { | ||||
| 	children: ReactNode; | ||||
| } | ||||
|  | ||||
| const getBrowserDefault = (): Theme => { | ||||
| 	if (window.matchMedia("(prefers-color-scheme: dark)").matches) { | ||||
| 		return Dark; | ||||
| 	} | ||||
| 	return Light; | ||||
| }; | ||||
|  | ||||
| export const ThemeProvider: React.FC<ThemeProviderProps> = ({ children }) => { | ||||
| 	const [theme, setThemeState] = useState<Theme>(() => { | ||||
| 		// Try to read theme from localStorage or use 'light' as default | ||||
| 		if (typeof window !== "undefined") { | ||||
| 			const stored = localStorage.getItem(StorageKey) as Theme | null; | ||||
| 			return stored || getBrowserDefault(); | ||||
| 		} | ||||
| 		return getBrowserDefault(); | ||||
| 	}); | ||||
|  | ||||
| 	useEffect(() => { | ||||
| 		document.body.dataset.theme = theme; | ||||
| 		localStorage.setItem(StorageKey, theme); | ||||
| 	}, [theme]); | ||||
|  | ||||
| 	const toggleTheme = () => { | ||||
| 		setThemeState((prev) => (prev === Light ? Dark : Light)); | ||||
| 	}; | ||||
|  | ||||
| 	const setTheme = (newTheme: Theme) => { | ||||
| 		setThemeState(newTheme); | ||||
| 	}; | ||||
|  | ||||
| 	const getTheme = () => { | ||||
| 		return theme; | ||||
| 	} | ||||
|  | ||||
| 	document.documentElement.setAttribute("data-bs-theme", theme); | ||||
| 	return <ThemeContext.Provider value={{ theme, toggleTheme, setTheme, getTheme }}>{children}</ThemeContext.Provider>; | ||||
| }; | ||||
|  | ||||
| export function useTheme(): ThemeContextType { | ||||
| 	const context = useContext(ThemeContext); | ||||
| 	if (!context) { | ||||
| 		throw new Error("useTheme must be used within a ThemeProvider"); | ||||
| 	} | ||||
| 	return context; | ||||
| } | ||||
							
								
								
									
										3
									
								
								frontend/src/context/index.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										3
									
								
								frontend/src/context/index.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,3 @@ | ||||
| export * from "./AuthContext"; | ||||
| export * from "./LocaleContext"; | ||||
| export * from "./ThemeContext"; | ||||
							
								
								
									
										1
									
								
								frontend/src/declarations.d.ts
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										1
									
								
								frontend/src/declarations.d.ts
									
									
									
									
										vendored
									
									
										Normal file
									
								
							| @@ -0,0 +1 @@ | ||||
| declare module "*.md"; | ||||
							
								
								
									
										10
									
								
								frontend/src/hooks/index.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										10
									
								
								frontend/src/hooks/index.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,10 @@ | ||||
| export * from "./useAccessLists"; | ||||
| export * from "./useDeadHosts"; | ||||
| export * from "./useHealth"; | ||||
| export * from "./useHostReport"; | ||||
| export * from "./useProxyHosts"; | ||||
| export * from "./useRedirectionHosts"; | ||||
| export * from "./useStreams"; | ||||
| export * from "./useTheme"; | ||||
| export * from "./useUser"; | ||||
| export * from "./useUsers"; | ||||
							
								
								
									
										17
									
								
								frontend/src/hooks/useAccessLists.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										17
									
								
								frontend/src/hooks/useAccessLists.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,17 @@ | ||||
| import { useQuery } from "@tanstack/react-query"; | ||||
| import { type AccessList, type AccessListExpansion, getAccessLists } from "src/api/backend"; | ||||
|  | ||||
| const fetchAccessLists = (expand?: AccessListExpansion[]) => { | ||||
| 	return getAccessLists(expand); | ||||
| }; | ||||
|  | ||||
| const useAccessLists = (expand?: AccessListExpansion[], options = {}) => { | ||||
| 	return useQuery<AccessList[], Error>({ | ||||
| 		queryKey: ["access-lists", { expand }], | ||||
| 		queryFn: () => fetchAccessLists(expand), | ||||
| 		staleTime: 60 * 1000, | ||||
| 		...options, | ||||
| 	}); | ||||
| }; | ||||
|  | ||||
| export { fetchAccessLists, useAccessLists }; | ||||
							
								
								
									
										17
									
								
								frontend/src/hooks/useDeadHosts.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										17
									
								
								frontend/src/hooks/useDeadHosts.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,17 @@ | ||||
| import { useQuery } from "@tanstack/react-query"; | ||||
| import { type DeadHost, type DeadHostExpansion, getDeadHosts } from "src/api/backend"; | ||||
|  | ||||
| const fetchDeadHosts = (expand?: DeadHostExpansion[]) => { | ||||
| 	return getDeadHosts(expand); | ||||
| }; | ||||
|  | ||||
| const useDeadHosts = (expand?: DeadHostExpansion[], options = {}) => { | ||||
| 	return useQuery<DeadHost[], Error>({ | ||||
| 		queryKey: ["dead-hosts", { expand }], | ||||
| 		queryFn: () => fetchDeadHosts(expand), | ||||
| 		staleTime: 60 * 1000, | ||||
| 		...options, | ||||
| 	}); | ||||
| }; | ||||
|  | ||||
| export { fetchDeadHosts, useDeadHosts }; | ||||
Some files were not shown because too many files have changed in this diff Show More
		Reference in New Issue
	
	Block a user