mirror of
https://github.com/NginxProxyManager/nginx-proxy-manager.git
synced 2025-09-16 20:00:35 +00: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