mirror of
https://github.com/NginxProxyManager/nginx-proxy-manager.git
synced 2025-09-14 19:02:35 +00:00
React
This commit is contained in:
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,
|
||||
);
|
||||
}
|
Reference in New Issue
Block a user