mirror of
https://github.com/NginxProxyManager/nginx-proxy-manager.git
synced 2025-12-05 16:06:51 +00:00
Compare commits
3 Commits
95957a192c
...
4709f9826c
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4709f9826c | ||
|
|
74a8c5d806 | ||
|
|
82a1a86c3a |
@@ -265,7 +265,7 @@ export default function (tokenString) {
|
||||
schemas: [roleSchema, permsSchema, objectSchema, permissionSchema],
|
||||
});
|
||||
|
||||
const valid = ajv.validate("permissions", dataSchema);
|
||||
const valid = await ajv.validate("permissions", dataSchema);
|
||||
return valid && dataSchema[permission];
|
||||
} catch (err) {
|
||||
err.permission = permission;
|
||||
|
||||
@@ -20,9 +20,9 @@
|
||||
"body-parser": "^1.20.3",
|
||||
"compression": "^1.7.4",
|
||||
"express": "^4.20.0",
|
||||
"express-fileupload": "^1.1.9",
|
||||
"gravatar": "^1.8.0",
|
||||
"jsonwebtoken": "^9.0.0",
|
||||
"express-fileupload": "^1.5.2",
|
||||
"gravatar": "^1.8.2",
|
||||
"jsonwebtoken": "^9.0.2",
|
||||
"knex": "2.4.2",
|
||||
"liquidjs": "10.6.1",
|
||||
"lodash": "^4.17.21",
|
||||
@@ -38,7 +38,7 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"@apidevtools/swagger-parser": "^10.1.0",
|
||||
"@biomejs/biome": "^2.3.1",
|
||||
"@biomejs/biome": "^2.3.2",
|
||||
"chalk": "4.1.2",
|
||||
"nodemon": "^2.0.2"
|
||||
},
|
||||
|
||||
@@ -43,59 +43,59 @@
|
||||
ajv-draft-04 "^1.0.0"
|
||||
call-me-maybe "^1.0.2"
|
||||
|
||||
"@biomejs/biome@^2.3.1":
|
||||
version "2.3.1"
|
||||
resolved "https://registry.yarnpkg.com/@biomejs/biome/-/biome-2.3.1.tgz#d1a9284f52986324f288cdaf450331a0f3fb1da7"
|
||||
integrity sha512-A29evf1R72V5bo4o2EPxYMm5mtyGvzp2g+biZvRFx29nWebGyyeOSsDWGx3tuNNMFRepGwxmA9ZQ15mzfabK2w==
|
||||
"@biomejs/biome@^2.3.2":
|
||||
version "2.3.2"
|
||||
resolved "https://registry.yarnpkg.com/@biomejs/biome/-/biome-2.3.2.tgz#aeeb5f12c39571a18f36a919be63ba7dbc7b290a"
|
||||
integrity sha512-8e9tzamuDycx7fdrcJ/F/GDZ8SYukc5ud6tDicjjFqURKYFSWMl0H0iXNXZEGmcmNUmABgGuHThPykcM41INgg==
|
||||
optionalDependencies:
|
||||
"@biomejs/cli-darwin-arm64" "2.3.1"
|
||||
"@biomejs/cli-darwin-x64" "2.3.1"
|
||||
"@biomejs/cli-linux-arm64" "2.3.1"
|
||||
"@biomejs/cli-linux-arm64-musl" "2.3.1"
|
||||
"@biomejs/cli-linux-x64" "2.3.1"
|
||||
"@biomejs/cli-linux-x64-musl" "2.3.1"
|
||||
"@biomejs/cli-win32-arm64" "2.3.1"
|
||||
"@biomejs/cli-win32-x64" "2.3.1"
|
||||
"@biomejs/cli-darwin-arm64" "2.3.2"
|
||||
"@biomejs/cli-darwin-x64" "2.3.2"
|
||||
"@biomejs/cli-linux-arm64" "2.3.2"
|
||||
"@biomejs/cli-linux-arm64-musl" "2.3.2"
|
||||
"@biomejs/cli-linux-x64" "2.3.2"
|
||||
"@biomejs/cli-linux-x64-musl" "2.3.2"
|
||||
"@biomejs/cli-win32-arm64" "2.3.2"
|
||||
"@biomejs/cli-win32-x64" "2.3.2"
|
||||
|
||||
"@biomejs/cli-darwin-arm64@2.3.1":
|
||||
version "2.3.1"
|
||||
resolved "https://registry.yarnpkg.com/@biomejs/cli-darwin-arm64/-/cli-darwin-arm64-2.3.1.tgz#607835f8ef043e1a80f9ad2a232c9e860941ab60"
|
||||
integrity sha512-ombSf3MnTUueiYGN1SeI9tBCsDUhpWzOwS63Dove42osNh0PfE1cUtHFx6eZ1+MYCCLwXzlFlYFdrJ+U7h6LcA==
|
||||
"@biomejs/cli-darwin-arm64@2.3.2":
|
||||
version "2.3.2"
|
||||
resolved "https://registry.yarnpkg.com/@biomejs/cli-darwin-arm64/-/cli-darwin-arm64-2.3.2.tgz#93f866161abe32e702987ccbddf492c1aabe016f"
|
||||
integrity sha512-4LECm4kc3If0JISai4c3KWQzukoUdpxy4fRzlrPcrdMSRFksR9ZoXK7JBcPuLBmd2SoT4/d7CQS33VnZpgBjew==
|
||||
|
||||
"@biomejs/cli-darwin-x64@2.3.1":
|
||||
version "2.3.1"
|
||||
resolved "https://registry.yarnpkg.com/@biomejs/cli-darwin-x64/-/cli-darwin-x64-2.3.1.tgz#654fe4aaa8ea5d5bde5457db4961ad5d214713ac"
|
||||
integrity sha512-pcOfwyoQkrkbGvXxRvZNe5qgD797IowpJPovPX5biPk2FwMEV+INZqfCaz4G5bVq9hYnjwhRMamg11U4QsRXrQ==
|
||||
"@biomejs/cli-darwin-x64@2.3.2":
|
||||
version "2.3.2"
|
||||
resolved "https://registry.yarnpkg.com/@biomejs/cli-darwin-x64/-/cli-darwin-x64-2.3.2.tgz#9c3dffdac12e4f4d8db7680ca20f58ace1f38c23"
|
||||
integrity sha512-jNMnfwHT4N3wi+ypRfMTjLGnDmKYGzxVr1EYAPBcauRcDnICFXN81wD6wxJcSUrLynoyyYCdfW6vJHS/IAoTDA==
|
||||
|
||||
"@biomejs/cli-linux-arm64-musl@2.3.1":
|
||||
version "2.3.1"
|
||||
resolved "https://registry.yarnpkg.com/@biomejs/cli-linux-arm64-musl/-/cli-linux-arm64-musl-2.3.1.tgz#5fe502082a575c31ef808cf080cbcd4485964167"
|
||||
integrity sha512-+DZYv8l7FlUtTrWs1Tdt1KcNCAmRO87PyOnxKGunbWm5HKg1oZBSbIIPkjrCtDZaeqSG1DiGx7qF+CPsquQRcg==
|
||||
"@biomejs/cli-linux-arm64-musl@2.3.2":
|
||||
version "2.3.2"
|
||||
resolved "https://registry.yarnpkg.com/@biomejs/cli-linux-arm64-musl/-/cli-linux-arm64-musl-2.3.2.tgz#a0424d2fe355cc43c375b3fbf3e42d39b7221d0e"
|
||||
integrity sha512-2Zz4usDG1GTTPQnliIeNx6eVGGP2ry5vE/v39nT73a3cKN6t5H5XxjcEoZZh62uVZvED7hXXikclvI64vZkYqw==
|
||||
|
||||
"@biomejs/cli-linux-arm64@2.3.1":
|
||||
version "2.3.1"
|
||||
resolved "https://registry.yarnpkg.com/@biomejs/cli-linux-arm64/-/cli-linux-arm64-2.3.1.tgz#81c02547905d379dbb312e6ff24b04908c2e320f"
|
||||
integrity sha512-td5O8pFIgLs8H1sAZsD6v+5quODihyEw4nv2R8z7swUfIK1FKk+15e4eiYVLcAE4jUqngvh4j3JCNgg0Y4o4IQ==
|
||||
"@biomejs/cli-linux-arm64@2.3.2":
|
||||
version "2.3.2"
|
||||
resolved "https://registry.yarnpkg.com/@biomejs/cli-linux-arm64/-/cli-linux-arm64-2.3.2.tgz#f85717c04d420ede20523d173a1fc10df60d4d37"
|
||||
integrity sha512-amnqvk+gWybbQleRRq8TMe0rIv7GHss8mFJEaGuEZYWg1Tw14YKOkeo8h6pf1c+d3qR+JU4iT9KXnBKGON4klw==
|
||||
|
||||
"@biomejs/cli-linux-x64-musl@2.3.1":
|
||||
version "2.3.1"
|
||||
resolved "https://registry.yarnpkg.com/@biomejs/cli-linux-x64-musl/-/cli-linux-x64-musl-2.3.1.tgz#c7c00beb5eda1ad25185544897e66eeec6be3b0b"
|
||||
integrity sha512-Y3Ob4nqgv38Mh+6EGHltuN+Cq8aj/gyMTJYzkFZV2AEj+9XzoXB9VNljz9pjfFNHUxvLEV4b55VWyxozQTBaUQ==
|
||||
"@biomejs/cli-linux-x64-musl@2.3.2":
|
||||
version "2.3.2"
|
||||
resolved "https://registry.yarnpkg.com/@biomejs/cli-linux-x64-musl/-/cli-linux-x64-musl-2.3.2.tgz#d3e114c744c32d2c50a77c13476bd941819c92d8"
|
||||
integrity sha512-gzB19MpRdTuOuLtPpFBGrV3Lq424gHyq2lFj8wfX9tvLMLdmA/R9C7k/mqBp/spcbWuHeIEKgEs3RviOPcWGBA==
|
||||
|
||||
"@biomejs/cli-linux-x64@2.3.1":
|
||||
version "2.3.1"
|
||||
resolved "https://registry.yarnpkg.com/@biomejs/cli-linux-x64/-/cli-linux-x64-2.3.1.tgz#7481d2e7be98d4de574df233766a5bdda037c897"
|
||||
integrity sha512-PYWgEO7up7XYwSAArOpzsVCiqxBCXy53gsReAb1kKYIyXaoAlhBaBMvxR/k2Rm9aTuZ662locXUmPk/Aj+Xu+Q==
|
||||
"@biomejs/cli-linux-x64@2.3.2":
|
||||
version "2.3.2"
|
||||
resolved "https://registry.yarnpkg.com/@biomejs/cli-linux-x64/-/cli-linux-x64-2.3.2.tgz#f66ce85d2d757d45e6edecce04753a805bd816f0"
|
||||
integrity sha512-8BG/vRAhFz1pmuyd24FQPhNeueLqPtwvZk6yblABY2gzL2H8fLQAF/Z2OPIc+BPIVPld+8cSiKY/KFh6k81xfA==
|
||||
|
||||
"@biomejs/cli-win32-arm64@2.3.1":
|
||||
version "2.3.1"
|
||||
resolved "https://registry.yarnpkg.com/@biomejs/cli-win32-arm64/-/cli-win32-arm64-2.3.1.tgz#dac8c7c7223e97f86cd0eed7aa95584984761481"
|
||||
integrity sha512-RHIG/zgo+69idUqVvV3n8+j58dKYABRpMyDmfWu2TITC+jwGPiEaT0Q3RKD+kQHiS80mpBrST0iUGeEXT0bU9A==
|
||||
"@biomejs/cli-win32-arm64@2.3.2":
|
||||
version "2.3.2"
|
||||
resolved "https://registry.yarnpkg.com/@biomejs/cli-win32-arm64/-/cli-win32-arm64-2.3.2.tgz#b46f8b47a3d97e766cc5ad5eb67d90eeb230b2cb"
|
||||
integrity sha512-lCruqQlfWjhMlOdyf5pDHOxoNm4WoyY2vZ4YN33/nuZBRstVDuqPPjS0yBkbUlLEte11FbpW+wWSlfnZfSIZvg==
|
||||
|
||||
"@biomejs/cli-win32-x64@2.3.1":
|
||||
version "2.3.1"
|
||||
resolved "https://registry.yarnpkg.com/@biomejs/cli-win32-x64/-/cli-win32-x64-2.3.1.tgz#f8818ab2c1e3a6e2ed8a656935173e5ce4c720be"
|
||||
integrity sha512-izl30JJ5Dp10mi90Eko47zhxE6pYyWPcnX1NQxKpL/yMhXxf95oLTzfpu4q+MDBh/gemNqyJEwjBpe0MT5iWPA==
|
||||
"@biomejs/cli-win32-x64@2.3.2":
|
||||
version "2.3.2"
|
||||
resolved "https://registry.yarnpkg.com/@biomejs/cli-win32-x64/-/cli-win32-x64-2.3.2.tgz#a14f5e220dd496705278315ee3e5e028dd657344"
|
||||
integrity sha512-6Ee9P26DTb4D8sN9nXxgbi9Dw5vSOfH98M7UlmkjKB2vtUbrRqCbZiNfryGiwnPIpd6YUoTl7rLVD2/x1CyEHQ==
|
||||
|
||||
"@gar/promisify@^1.0.1":
|
||||
version "1.1.3"
|
||||
@@ -861,7 +861,7 @@ expand-template@^2.0.3:
|
||||
resolved "https://registry.yarnpkg.com/expand-template/-/expand-template-2.0.3.tgz#6e14b3fcee0f3a6340ecb57d2e8918692052a47c"
|
||||
integrity sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg==
|
||||
|
||||
express-fileupload@^1.1.9:
|
||||
express-fileupload@^1.5.2:
|
||||
version "1.5.2"
|
||||
resolved "https://registry.yarnpkg.com/express-fileupload/-/express-fileupload-1.5.2.tgz#4da70ba6f2ffd4c736eab0776445865a9dbd9bfa"
|
||||
integrity sha512-wxUJn2vTHvj/kZCVmc5/bJO15C7aSMyHeuXYY3geKpeKibaAoQGcEv5+sM6nHS2T7VF+QHS4hTWPiY2mKofEdg==
|
||||
@@ -1108,7 +1108,7 @@ graceful-fs@^4.1.15, graceful-fs@^4.1.2, graceful-fs@^4.2.0, graceful-fs@^4.2.6:
|
||||
resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.11.tgz#4183e4e8bf08bb6e05bbb2f7d2e0c8f712ca40e3"
|
||||
integrity sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==
|
||||
|
||||
gravatar@^1.8.0:
|
||||
gravatar@^1.8.2:
|
||||
version "1.8.2"
|
||||
resolved "https://registry.yarnpkg.com/gravatar/-/gravatar-1.8.2.tgz#f298642b1562ed685af2ae938dbe31ec0c542cc1"
|
||||
integrity sha512-GdRwLM3oYpFQKy47MKuluw9hZ2gaCtiKPbDGdcDEuYDKlc8eNnW27KYL9LVbIDzEsx88WtDWQm2ClBcsgBnj6w==
|
||||
@@ -1352,7 +1352,7 @@ json-schema-traverse@^1.0.0:
|
||||
resolved "https://registry.yarnpkg.com/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz#ae7bcb3656ab77a73ba5c49bf654f38e6b6860e2"
|
||||
integrity sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==
|
||||
|
||||
jsonwebtoken@^9.0.0:
|
||||
jsonwebtoken@^9.0.2:
|
||||
version "9.0.2"
|
||||
resolved "https://registry.yarnpkg.com/jsonwebtoken/-/jsonwebtoken-9.0.2.tgz#65ff91f4abef1784697d40952bb1998c504caaf3"
|
||||
integrity sha512-PRp66vJ865SSqOlgqS8hujT5U4AOgMfhrwYIuIhfKaoSCZcirrmASQr8CX7cUg+RMih+hgznrjp99o+W4pJLHQ==
|
||||
|
||||
@@ -37,6 +37,7 @@ export * from "./getToken";
|
||||
export * from "./getUser";
|
||||
export * from "./getUsers";
|
||||
export * from "./helpers";
|
||||
export * from "./loginAsUser";
|
||||
export * from "./models";
|
||||
export * from "./refreshToken";
|
||||
export * from "./renewCertificate";
|
||||
|
||||
8
frontend/src/api/backend/loginAsUser.ts
Normal file
8
frontend/src/api/backend/loginAsUser.ts
Normal file
@@ -0,0 +1,8 @@
|
||||
import * as api from "./base";
|
||||
import type { LoginAsTokenResponse } from "./responseTypes";
|
||||
|
||||
export async function loginAsUser(id: number): Promise<LoginAsTokenResponse> {
|
||||
return await api.post({
|
||||
url: `/users/${id}/login`,
|
||||
});
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { AppVersion } from "./models";
|
||||
import type { AppVersion, User } from "./models";
|
||||
|
||||
export interface HealthResponse {
|
||||
status: string;
|
||||
@@ -15,3 +15,7 @@ export interface ValidatedCertificateResponse {
|
||||
certificate: Record<string, any>;
|
||||
certificateKey: boolean;
|
||||
}
|
||||
|
||||
export interface LoginAsTokenResponse extends TokenResponse {
|
||||
user: User;
|
||||
}
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
import type { Table as ReactTable } from "@tanstack/react-table";
|
||||
import cn from "classnames";
|
||||
import type { ReactNode } from "react";
|
||||
import { Button } from "src/components";
|
||||
import { Button, HasPermission } from "src/components";
|
||||
import { T } from "src/locale";
|
||||
import { type ADMIN, MANAGE, type Permission, type Section } from "src/modules/Permissions";
|
||||
|
||||
interface Props {
|
||||
tableInstance: ReactTable<any>;
|
||||
@@ -12,8 +13,20 @@ interface Props {
|
||||
objects: string;
|
||||
color?: string;
|
||||
customAddBtn?: ReactNode;
|
||||
permissionSection?: Section | typeof ADMIN;
|
||||
permission?: Permission;
|
||||
}
|
||||
function EmptyData({ tableInstance, onNew, isFiltered, object, objects, color = "primary", customAddBtn }: Props) {
|
||||
function EmptyData({
|
||||
tableInstance,
|
||||
onNew,
|
||||
isFiltered,
|
||||
object,
|
||||
objects,
|
||||
color = "primary",
|
||||
customAddBtn,
|
||||
permissionSection,
|
||||
permission,
|
||||
}: Props) {
|
||||
return (
|
||||
<tr>
|
||||
<td colSpan={tableInstance.getVisibleFlatColumns().length}>
|
||||
@@ -27,16 +40,18 @@ function EmptyData({ tableInstance, onNew, isFiltered, object, objects, color =
|
||||
<h2>
|
||||
<T id="object.empty" tData={{ objects }} />
|
||||
</h2>
|
||||
<p className="text-muted">
|
||||
<T id="empty-subtitle" />
|
||||
</p>
|
||||
{customAddBtn ? (
|
||||
customAddBtn
|
||||
) : (
|
||||
<Button className={cn("my-3", `btn-${color}`)} onClick={onNew}>
|
||||
<T id="object.add" tData={{ object }} />
|
||||
</Button>
|
||||
)}
|
||||
<HasPermission section={permissionSection} permission={permission || MANAGE} hideError>
|
||||
<p className="text-muted">
|
||||
<T id="empty-subtitle" />
|
||||
</p>
|
||||
{customAddBtn ? (
|
||||
customAddBtn
|
||||
) : (
|
||||
<Button className={cn("my-3", `btn-${color}`)} onClick={onNew}>
|
||||
<T id="object.add" tData={{ object }} />
|
||||
</Button>
|
||||
)}
|
||||
</HasPermission>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -3,25 +3,29 @@ import Alert from "react-bootstrap/Alert";
|
||||
import { Loading, LoadingPage } from "src/components";
|
||||
import { useUser } from "src/hooks";
|
||||
import { T } from "src/locale";
|
||||
import { type ADMIN, hasPermission, type Permission, type Section } from "src/modules/Permissions";
|
||||
|
||||
interface Props {
|
||||
permission: string;
|
||||
type: "manage" | "view";
|
||||
section?: Section | typeof ADMIN;
|
||||
permission: Permission;
|
||||
hideError?: boolean;
|
||||
children?: ReactNode;
|
||||
pageLoading?: boolean;
|
||||
loadingNoLogo?: boolean;
|
||||
}
|
||||
function HasPermission({
|
||||
section,
|
||||
permission,
|
||||
type,
|
||||
children,
|
||||
hideError = false,
|
||||
pageLoading = false,
|
||||
loadingNoLogo = false,
|
||||
}: Props) {
|
||||
const { data, isLoading } = useUser("me");
|
||||
const perms = data?.permissions;
|
||||
|
||||
if (!section) {
|
||||
return <>{children}</>;
|
||||
}
|
||||
|
||||
if (isLoading) {
|
||||
if (hideError) {
|
||||
@@ -33,33 +37,7 @@ function HasPermission({
|
||||
return <Loading noLogo={loadingNoLogo} />;
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
const allowed = hasPermission(section, permission, data?.permissions, data?.roles);
|
||||
if (allowed) {
|
||||
return <>{children}</>;
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { IconLock, IconLogout, IconUser } from "@tabler/icons-react";
|
||||
import { LocalePicker, ThemeSwitcher, NavLink } from "src/components";
|
||||
import { LocalePicker, NavLink, ThemeSwitcher } from "src/components";
|
||||
import { useAuthState } from "src/context";
|
||||
import { useUser } from "src/hooks";
|
||||
import { T } from "src/locale";
|
||||
@@ -26,18 +26,18 @@ export function SiteHeader() {
|
||||
<span className="navbar-toggler-icon" />
|
||||
</button>
|
||||
<div className="navbar-brand navbar-brand-autodark d-none-navbar-horizontal pe-0 pe-md-3">
|
||||
<NavLink to="/">
|
||||
<div className={styles.logo}>
|
||||
<img
|
||||
src="/images/logo-no-text.svg"
|
||||
width={40}
|
||||
height={40}
|
||||
className="navbar-brand-image"
|
||||
alt="Logo"
|
||||
/>
|
||||
</div>
|
||||
Nginx Proxy Manager
|
||||
</NavLink>
|
||||
<NavLink to="/">
|
||||
<div className={styles.logo}>
|
||||
<img
|
||||
src="/images/logo-no-text.svg"
|
||||
width={40}
|
||||
height={40}
|
||||
className="navbar-brand-image"
|
||||
alt="Logo"
|
||||
/>
|
||||
</div>
|
||||
Nginx Proxy Manager
|
||||
</NavLink>
|
||||
</div>
|
||||
<div className="navbar-nav flex-row order-md-last">
|
||||
<div className="d-none d-md-flex">
|
||||
|
||||
@@ -11,14 +11,26 @@ import cn from "classnames";
|
||||
import React from "react";
|
||||
import { HasPermission, NavLink } from "src/components";
|
||||
import { T } from "src/locale";
|
||||
import {
|
||||
ACCESS_LISTS,
|
||||
ADMIN,
|
||||
CERTIFICATES,
|
||||
DEAD_HOSTS,
|
||||
type MANAGE,
|
||||
PROXY_HOSTS,
|
||||
REDIRECTION_HOSTS,
|
||||
type Section,
|
||||
STREAMS,
|
||||
VIEW,
|
||||
} from "src/modules/Permissions";
|
||||
|
||||
interface MenuItem {
|
||||
label: string;
|
||||
icon?: React.ElementType;
|
||||
to?: string;
|
||||
items?: MenuItem[];
|
||||
permission?: string;
|
||||
permissionType?: "view" | "manage";
|
||||
permissionSection?: Section | typeof ADMIN;
|
||||
permission?: typeof VIEW | typeof MANAGE;
|
||||
}
|
||||
|
||||
const menuItems: MenuItem[] = [
|
||||
@@ -34,26 +46,26 @@ const menuItems: MenuItem[] = [
|
||||
{
|
||||
to: "/nginx/proxy",
|
||||
label: "proxy-hosts",
|
||||
permission: "proxyHosts",
|
||||
permissionType: "view",
|
||||
permissionSection: PROXY_HOSTS,
|
||||
permission: VIEW,
|
||||
},
|
||||
{
|
||||
to: "/nginx/redirection",
|
||||
label: "redirection-hosts",
|
||||
permission: "redirectionHosts",
|
||||
permissionType: "view",
|
||||
permissionSection: REDIRECTION_HOSTS,
|
||||
permission: VIEW,
|
||||
},
|
||||
{
|
||||
to: "/nginx/stream",
|
||||
label: "streams",
|
||||
permission: "streams",
|
||||
permissionType: "view",
|
||||
permissionSection: STREAMS,
|
||||
permission: VIEW,
|
||||
},
|
||||
{
|
||||
to: "/nginx/404",
|
||||
label: "dead-hosts",
|
||||
permission: "deadHosts",
|
||||
permissionType: "view",
|
||||
permissionSection: DEAD_HOSTS,
|
||||
permission: VIEW,
|
||||
},
|
||||
],
|
||||
},
|
||||
@@ -61,33 +73,33 @@ const menuItems: MenuItem[] = [
|
||||
to: "/access",
|
||||
icon: IconLock,
|
||||
label: "access-lists",
|
||||
permission: "accessLists",
|
||||
permissionType: "view",
|
||||
permissionSection: ACCESS_LISTS,
|
||||
permission: VIEW,
|
||||
},
|
||||
{
|
||||
to: "/certificates",
|
||||
icon: IconShield,
|
||||
label: "certificates",
|
||||
permission: "certificates",
|
||||
permissionType: "view",
|
||||
permissionSection: CERTIFICATES,
|
||||
permission: VIEW,
|
||||
},
|
||||
{
|
||||
to: "/users",
|
||||
icon: IconUser,
|
||||
label: "users",
|
||||
permission: "admin",
|
||||
permissionSection: ADMIN,
|
||||
},
|
||||
{
|
||||
to: "/audit-log",
|
||||
icon: IconBook,
|
||||
label: "auditlogs",
|
||||
permission: "admin",
|
||||
permissionSection: ADMIN,
|
||||
},
|
||||
{
|
||||
to: "/settings",
|
||||
icon: IconSettings,
|
||||
label: "settings",
|
||||
permission: "admin",
|
||||
permissionSection: ADMIN,
|
||||
},
|
||||
];
|
||||
|
||||
@@ -99,8 +111,8 @@ const getMenuItem = (item: MenuItem, onClick?: () => void) => {
|
||||
return (
|
||||
<HasPermission
|
||||
key={`item-${item.label}`}
|
||||
permission={item.permission || ""}
|
||||
type={item.permissionType || "view"}
|
||||
section={item.permissionSection}
|
||||
permission={item.permission || VIEW}
|
||||
hideError
|
||||
>
|
||||
<li className="nav-item">
|
||||
@@ -122,8 +134,8 @@ const getMenuDropown = (item: MenuItem, onClick?: () => void) => {
|
||||
return (
|
||||
<HasPermission
|
||||
key={`item-${item.label}`}
|
||||
permission={item.permission || ""}
|
||||
type={item.permissionType || "view"}
|
||||
section={item.permissionSection}
|
||||
permission={item.permission || VIEW}
|
||||
hideError
|
||||
>
|
||||
<li className={cns}>
|
||||
@@ -147,8 +159,8 @@ const getMenuDropown = (item: MenuItem, onClick?: () => void) => {
|
||||
return (
|
||||
<HasPermission
|
||||
key={`${idx}-${subitem.to}`}
|
||||
permission={subitem.permission || ""}
|
||||
type={subitem.permissionType || "view"}
|
||||
section={subitem.permissionSection}
|
||||
permission={subitem.permission || VIEW}
|
||||
hideError
|
||||
>
|
||||
<NavLink to={subitem.to} isDropdownItem onClick={onClick}>
|
||||
|
||||
@@ -1,13 +1,14 @@
|
||||
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 { getToken, loginAsUser, 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>;
|
||||
loginAs: (id: number) => Promise<void>;
|
||||
logout: () => void;
|
||||
token?: string;
|
||||
}
|
||||
@@ -34,7 +35,20 @@ function AuthProvider({ children, tokenRefreshInterval = 5 * 60 * 1000 }: Props)
|
||||
handleTokenUpdate(response);
|
||||
};
|
||||
|
||||
const loginAs = async (id: number) => {
|
||||
const response = await loginAsUser(id);
|
||||
AuthStore.add(response);
|
||||
queryClient.clear();
|
||||
window.location.reload();
|
||||
};
|
||||
|
||||
const logout = () => {
|
||||
if (AuthStore.count() >= 2) {
|
||||
AuthStore.drop();
|
||||
queryClient.clear();
|
||||
window.location.reload();
|
||||
return;
|
||||
}
|
||||
AuthStore.clear();
|
||||
setAuthenticated(false);
|
||||
queryClient.clear();
|
||||
@@ -55,7 +69,7 @@ function AuthProvider({ children, tokenRefreshInterval = 5 * 60 * 1000 }: Props)
|
||||
true,
|
||||
);
|
||||
|
||||
const value = { authenticated, login, logout };
|
||||
const value = { authenticated, login, logout, loginAs };
|
||||
|
||||
return <AuthContext.Provider value={value}>{children}</AuthContext.Provider>;
|
||||
}
|
||||
|
||||
@@ -201,6 +201,7 @@
|
||||
"user.current-password": "Current Password",
|
||||
"user.edit-profile": "Edit Profile",
|
||||
"user.full-name": "Full Name",
|
||||
"user.login-as": "Sign in as {name}",
|
||||
"user.logout": "Logout",
|
||||
"user.new-password": "New Password",
|
||||
"user.nickname": "Nickname",
|
||||
|
||||
@@ -605,6 +605,9 @@
|
||||
"user.full-name": {
|
||||
"defaultMessage": "Full Name"
|
||||
},
|
||||
"user.login-as": {
|
||||
"defaultMessage": "Sign in as {name}"
|
||||
},
|
||||
"user.logout": {
|
||||
"defaultMessage": "Logout"
|
||||
},
|
||||
|
||||
@@ -47,11 +47,41 @@ const PermissionsModal = EasyModal.create(({ id, visible, remove }: Props) => {
|
||||
});
|
||||
};
|
||||
|
||||
// given the field and clicked permission, intelligently set the value, and
|
||||
// other values that depends on it.
|
||||
const handleChange = (form: any, field: any, perm: string) => {
|
||||
if (field.name === "proxyHosts" && perm !== "hidden" && form.values.accessLists === "hidden") {
|
||||
form.setFieldValue("accessLists", "view");
|
||||
}
|
||||
// certs are required for proxy and redirection hosts, and streams
|
||||
if (
|
||||
["proxyHosts", "redirectionHosts", "deadHosts", "streams"].includes(field.name) &&
|
||||
perm !== "hidden" &&
|
||||
form.values.certificates === "hidden"
|
||||
) {
|
||||
form.setFieldValue("certificates", "view");
|
||||
}
|
||||
|
||||
form.setFieldValue(field.name, perm);
|
||||
};
|
||||
|
||||
const getPermissionButtons = (field: any, form: any) => {
|
||||
const isManage = field.value === "manage";
|
||||
const isView = field.value === "view";
|
||||
const isHidden = field.value === "hidden";
|
||||
|
||||
let hiddenDisabled = false;
|
||||
if (field.name === "accessLists") {
|
||||
hiddenDisabled = form.values.proxyHosts !== "hidden";
|
||||
}
|
||||
if (field.name === "certificates") {
|
||||
hiddenDisabled =
|
||||
form.values.proxyHosts !== "hidden" ||
|
||||
form.values.redirectionHosts !== "hidden" ||
|
||||
form.values.deadHosts !== "hidden" ||
|
||||
form.values.streams !== "hidden";
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className="btn-group w-100" role="group">
|
||||
@@ -63,7 +93,7 @@ const PermissionsModal = EasyModal.create(({ id, visible, remove }: Props) => {
|
||||
autoComplete="off"
|
||||
value="manage"
|
||||
checked={field.value === "manage"}
|
||||
onChange={() => form.setFieldValue(field.name, "manage")}
|
||||
onChange={() => handleChange(form, field, "manage")}
|
||||
/>
|
||||
<label htmlFor={`${field.name}-manage`} className={getClasses(isManage)}>
|
||||
<T id="permissions.manage" />
|
||||
@@ -76,7 +106,7 @@ const PermissionsModal = EasyModal.create(({ id, visible, remove }: Props) => {
|
||||
autoComplete="off"
|
||||
value="view"
|
||||
checked={field.value === "view"}
|
||||
onChange={() => form.setFieldValue(field.name, "view")}
|
||||
onChange={() => handleChange(form, field, "view")}
|
||||
/>
|
||||
<label htmlFor={`${field.name}-view`} className={getClasses(isView)}>
|
||||
<T id="permissions.view" />
|
||||
@@ -89,7 +119,8 @@ const PermissionsModal = EasyModal.create(({ id, visible, remove }: Props) => {
|
||||
autoComplete="off"
|
||||
value="hidden"
|
||||
checked={field.value === "hidden"}
|
||||
onChange={() => form.setFieldValue(field.name, "hidden")}
|
||||
disabled={hiddenDisabled}
|
||||
onChange={() => handleChange(form, field, "hidden")}
|
||||
/>
|
||||
<label htmlFor={`${field.name}-hidden`} className={getClasses(isHidden)}>
|
||||
<T id="permissions.hidden" />
|
||||
|
||||
@@ -9,14 +9,16 @@ import {
|
||||
AccessField,
|
||||
Button,
|
||||
DomainNamesField,
|
||||
HasPermission,
|
||||
Loading,
|
||||
LocationsFields,
|
||||
NginxConfigField,
|
||||
SSLCertificateField,
|
||||
SSLOptionsFields,
|
||||
} from "src/components";
|
||||
import { useProxyHost, useSetProxyHost } from "src/hooks";
|
||||
import { useProxyHost, useSetProxyHost, useUser } from "src/hooks";
|
||||
import { T } from "src/locale";
|
||||
import { MANAGE, PROXY_HOSTS } from "src/modules/Permissions";
|
||||
import { validateNumber, validateString } from "src/modules/Validations";
|
||||
import { showObjectSuccess } from "src/notifications";
|
||||
|
||||
@@ -28,6 +30,7 @@ interface Props extends InnerModalProps {
|
||||
id: number | "new";
|
||||
}
|
||||
const ProxyHostModal = EasyModal.create(({ id, visible, remove }: Props) => {
|
||||
const { data: currentUser, isLoading: userIsLoading, error: userError } = useUser("me");
|
||||
const { data, isLoading, error } = useProxyHost(id);
|
||||
const { mutate: setProxyHost } = useSetProxyHost();
|
||||
const [errorMsg, setErrorMsg] = useState<ReactNode | null>(null);
|
||||
@@ -58,13 +61,13 @@ const ProxyHostModal = EasyModal.create(({ id, visible, remove }: Props) => {
|
||||
|
||||
return (
|
||||
<Modal show={visible} onHide={remove}>
|
||||
{!isLoading && error && (
|
||||
{!isLoading && (error || userError) && (
|
||||
<Alert variant="danger" className="m-3">
|
||||
{error?.message || "Unknown error"}
|
||||
{error?.message || userError?.message || "Unknown error"}
|
||||
</Alert>
|
||||
)}
|
||||
{isLoading && <Loading noLogo />}
|
||||
{!isLoading && data && (
|
||||
{isLoading || (userIsLoading && <Loading noLogo />)}
|
||||
{!isLoading && !userIsLoading && data && currentUser && (
|
||||
<Formik
|
||||
initialValues={
|
||||
{
|
||||
@@ -349,16 +352,18 @@ const ProxyHostModal = EasyModal.create(({ id, visible, remove }: Props) => {
|
||||
<Button data-bs-dismiss="modal" onClick={remove} disabled={isSubmitting}>
|
||||
<T id="cancel" />
|
||||
</Button>
|
||||
<Button
|
||||
type="submit"
|
||||
actionType="primary"
|
||||
className="ms-auto bg-lime"
|
||||
data-bs-dismiss="modal"
|
||||
isLoading={isSubmitting}
|
||||
disabled={isSubmitting}
|
||||
>
|
||||
<T id="save" />
|
||||
</Button>
|
||||
<HasPermission section={PROXY_HOSTS} permission={MANAGE} hideError>
|
||||
<Button
|
||||
type="submit"
|
||||
actionType="primary"
|
||||
className="ms-auto bg-lime"
|
||||
data-bs-dismiss="modal"
|
||||
isLoading={isSubmitting}
|
||||
disabled={isSubmitting}
|
||||
>
|
||||
<T id="save" />
|
||||
</Button>
|
||||
</HasPermission>
|
||||
</Modal.Footer>
|
||||
</Form>
|
||||
)}
|
||||
|
||||
@@ -44,6 +44,7 @@ export class AuthStore {
|
||||
// const t = this.tokens;
|
||||
// return t.length > 0;
|
||||
// }
|
||||
// Start from the END of the stack and work backwards
|
||||
hasActiveToken() {
|
||||
const t = this.tokens;
|
||||
if (!t.length) {
|
||||
@@ -68,22 +69,27 @@ export class AuthStore {
|
||||
localStorage.setItem(TOKEN_KEY, JSON.stringify([{ token, expires }]));
|
||||
}
|
||||
|
||||
// Add a token to the stack
|
||||
// Add a token to the END of the stack
|
||||
add({ token, expires }: TokenResponse) {
|
||||
const t = this.tokens;
|
||||
t.push({ token, expires });
|
||||
localStorage.setItem(TOKEN_KEY, JSON.stringify(t));
|
||||
}
|
||||
|
||||
// Drop a token from the stack
|
||||
// Drop a token from the END of the stack
|
||||
drop() {
|
||||
const t = this.tokens;
|
||||
localStorage.setItem(TOKEN_KEY, JSON.stringify(t.splice(-1, 1)));
|
||||
t.splice(-1, 1);
|
||||
localStorage.setItem(TOKEN_KEY, JSON.stringify(t));
|
||||
}
|
||||
|
||||
clear() {
|
||||
localStorage.removeItem(TOKEN_KEY);
|
||||
}
|
||||
|
||||
count() {
|
||||
return this.tokens.length;
|
||||
}
|
||||
}
|
||||
|
||||
export default new AuthStore();
|
||||
|
||||
49
frontend/src/modules/Permissions.ts
Normal file
49
frontend/src/modules/Permissions.ts
Normal file
@@ -0,0 +1,49 @@
|
||||
import type { UserPermissions } from "src/api/backend";
|
||||
|
||||
export const ADMIN = "admin";
|
||||
export const VISIBILITY = "visibility";
|
||||
export const PROXY_HOSTS = "proxyHosts";
|
||||
export const REDIRECTION_HOSTS = "redirectionHosts";
|
||||
export const DEAD_HOSTS = "deadHosts";
|
||||
export const STREAMS = "streams";
|
||||
export const CERTIFICATES = "certificates";
|
||||
export const ACCESS_LISTS = "accessLists";
|
||||
|
||||
export const MANAGE = "manage";
|
||||
export const VIEW = "view";
|
||||
export const HIDDEN = "hidden";
|
||||
|
||||
export const ALL = "all";
|
||||
export const USER = "user";
|
||||
|
||||
export type Section =
|
||||
| typeof ADMIN
|
||||
| typeof VISIBILITY
|
||||
| typeof PROXY_HOSTS
|
||||
| typeof REDIRECTION_HOSTS
|
||||
| typeof DEAD_HOSTS
|
||||
| typeof STREAMS
|
||||
| typeof CERTIFICATES
|
||||
| typeof ACCESS_LISTS;
|
||||
|
||||
export type Permission = typeof MANAGE | typeof VIEW;
|
||||
|
||||
const hasPermission = (
|
||||
section: Section,
|
||||
perm: Permission,
|
||||
userPerms: UserPermissions | undefined,
|
||||
roles: string[] | undefined,
|
||||
): boolean => {
|
||||
if (!userPerms) return false;
|
||||
if (isAdmin(roles)) return true;
|
||||
const acceptable = [MANAGE, perm];
|
||||
// @ts-expect-error 7053
|
||||
const v = typeof userPerms[section] !== "undefined" ? userPerms[section] : HIDDEN;
|
||||
return acceptable.indexOf(v) !== -1;
|
||||
};
|
||||
|
||||
const isAdmin = (roles: string[] | undefined): boolean => {
|
||||
return roles?.includes("admin") || false;
|
||||
};
|
||||
|
||||
export { hasPermission, isAdmin };
|
||||
@@ -2,9 +2,10 @@ import { IconDotsVertical, IconEdit, IconTrash } from "@tabler/icons-react";
|
||||
import { createColumnHelper, getCoreRowModel, useReactTable } from "@tanstack/react-table";
|
||||
import { useMemo } from "react";
|
||||
import type { AccessList } from "src/api/backend";
|
||||
import { EmptyData, GravatarFormatter, ValueWithDateFormatter } from "src/components";
|
||||
import { EmptyData, GravatarFormatter, HasPermission, ValueWithDateFormatter } from "src/components";
|
||||
import { TableLayout } from "src/components/Table/TableLayout";
|
||||
import { intl, T } from "src/locale";
|
||||
import { ACCESS_LISTS, MANAGE } from "src/modules/Permissions";
|
||||
|
||||
interface Props {
|
||||
data: AccessList[];
|
||||
@@ -84,18 +85,20 @@ export default function Table({ data, isFetching, isFiltered, onEdit, onDelete,
|
||||
<IconEdit size={16} />
|
||||
<T id="action.edit" />
|
||||
</a>
|
||||
<div className="dropdown-divider" />
|
||||
<a
|
||||
className="dropdown-item"
|
||||
href="#"
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
onDelete?.(info.row.original.id);
|
||||
}}
|
||||
>
|
||||
<IconTrash size={16} />
|
||||
<T id="action.delete" />
|
||||
</a>
|
||||
<HasPermission section={ACCESS_LISTS} permission={MANAGE} hideError>
|
||||
<div className="dropdown-divider" />
|
||||
<a
|
||||
className="dropdown-item"
|
||||
href="#"
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
onDelete?.(info.row.original.id);
|
||||
}}
|
||||
>
|
||||
<IconTrash size={16} />
|
||||
<T id="action.delete" />
|
||||
</a>
|
||||
</HasPermission>
|
||||
</div>
|
||||
</span>
|
||||
);
|
||||
@@ -130,6 +133,7 @@ export default function Table({ data, isFetching, isFiltered, onEdit, onDelete,
|
||||
onNew={onNew}
|
||||
isFiltered={isFiltered}
|
||||
color="cyan"
|
||||
permissionSection={ACCESS_LISTS}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
|
||||
@@ -2,10 +2,11 @@ import { IconHelp, IconSearch } from "@tabler/icons-react";
|
||||
import { useState } from "react";
|
||||
import Alert from "react-bootstrap/Alert";
|
||||
import { deleteAccessList } from "src/api/backend";
|
||||
import { Button, LoadingPage } from "src/components";
|
||||
import { Button, HasPermission, LoadingPage } from "src/components";
|
||||
import { useAccessLists } from "src/hooks";
|
||||
import { T } from "src/locale";
|
||||
import { showAccessListModal, showDeleteConfirmModal, showHelpModal } from "src/modals";
|
||||
import { ACCESS_LISTS, MANAGE } from "src/modules/Permissions";
|
||||
import { showObjectSuccess } from "src/notifications";
|
||||
import Table from "./Table";
|
||||
|
||||
@@ -67,11 +68,17 @@ export default function TableWrapper() {
|
||||
<Button size="sm" onClick={() => showHelpModal("AccessLists", "cyan")}>
|
||||
<IconHelp size={20} />
|
||||
</Button>
|
||||
{data?.length ? (
|
||||
<Button size="sm" className="btn-cyan" onClick={() => showAccessListModal("new")}>
|
||||
<T id="object.add" tData={{ object: "access-list" }} />
|
||||
</Button>
|
||||
) : null}
|
||||
<HasPermission section={ACCESS_LISTS} permission={MANAGE} hideError>
|
||||
{data?.length ? (
|
||||
<Button
|
||||
size="sm"
|
||||
className="btn-cyan"
|
||||
onClick={() => showAccessListModal("new")}
|
||||
>
|
||||
<T id="object.add" tData={{ object: "access-list" }} />
|
||||
</Button>
|
||||
) : null}
|
||||
</HasPermission>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
import { HasPermission } from "src/components";
|
||||
import { ACCESS_LISTS, VIEW } from "src/modules/Permissions";
|
||||
import TableWrapper from "./TableWrapper";
|
||||
|
||||
const Access = () => {
|
||||
return (
|
||||
<HasPermission permission="accessLists" type="view" pageLoading loadingNoLogo>
|
||||
<HasPermission section={ACCESS_LISTS} permission={VIEW} pageLoading loadingNoLogo>
|
||||
<TableWrapper />
|
||||
</HasPermission>
|
||||
);
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
import { HasPermission } from "src/components";
|
||||
import { ADMIN, VIEW } from "src/modules/Permissions";
|
||||
import TableWrapper from "./TableWrapper";
|
||||
|
||||
const AuditLog = () => {
|
||||
return (
|
||||
<HasPermission permission="admin" type="manage" pageLoading loadingNoLogo>
|
||||
<HasPermission section={ADMIN} permission={VIEW} pageLoading loadingNoLogo>
|
||||
<TableWrapper />
|
||||
</HasPermission>
|
||||
);
|
||||
|
||||
@@ -8,10 +8,12 @@ import {
|
||||
DomainsFormatter,
|
||||
EmptyData,
|
||||
GravatarFormatter,
|
||||
HasPermission,
|
||||
} from "src/components";
|
||||
import { TableLayout } from "src/components/Table/TableLayout";
|
||||
import { intl, T } from "src/locale";
|
||||
import { showCustomCertificateModal, showDNSCertificateModal, showHTTPCertificateModal } from "src/modals";
|
||||
import { CERTIFICATES, MANAGE } from "src/modules/Permissions";
|
||||
|
||||
interface Props {
|
||||
data: Certificate[];
|
||||
@@ -125,29 +127,31 @@ export default function Table({ data, isFetching, onDelete, onRenew, onDownload,
|
||||
<IconRefresh size={16} />
|
||||
<T id="action.renew" />
|
||||
</a>
|
||||
<a
|
||||
className="dropdown-item"
|
||||
href="#"
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
onDownload?.(info.row.original.id);
|
||||
}}
|
||||
>
|
||||
<IconDownload size={16} />
|
||||
<T id="action.download" />
|
||||
</a>
|
||||
<div className="dropdown-divider" />
|
||||
<a
|
||||
className="dropdown-item"
|
||||
href="#"
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
onDelete?.(info.row.original.id);
|
||||
}}
|
||||
>
|
||||
<IconTrash size={16} />
|
||||
<T id="action.delete" />
|
||||
</a>
|
||||
<HasPermission section={CERTIFICATES} permission={MANAGE} hideError>
|
||||
<a
|
||||
className="dropdown-item"
|
||||
href="#"
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
onDownload?.(info.row.original.id);
|
||||
}}
|
||||
>
|
||||
<IconDownload size={16} />
|
||||
<T id="action.download" />
|
||||
</a>
|
||||
<div className="dropdown-divider" />
|
||||
<a
|
||||
className="dropdown-item"
|
||||
href="#"
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
onDelete?.(info.row.original.id);
|
||||
}}
|
||||
>
|
||||
<IconTrash size={16} />
|
||||
<T id="action.delete" />
|
||||
</a>
|
||||
</HasPermission>
|
||||
</div>
|
||||
</span>
|
||||
);
|
||||
@@ -223,6 +227,7 @@ export default function Table({ data, isFetching, onDelete, onRenew, onDownload,
|
||||
isFiltered={isFiltered}
|
||||
color="pink"
|
||||
customAddBtn={customAddBtn}
|
||||
permissionSection={CERTIFICATES}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
|
||||
@@ -2,7 +2,7 @@ import { IconHelp, IconSearch } from "@tabler/icons-react";
|
||||
import { useState } from "react";
|
||||
import Alert from "react-bootstrap/Alert";
|
||||
import { deleteCertificate, downloadCertificate } from "src/api/backend";
|
||||
import { Button, LoadingPage } from "src/components";
|
||||
import { Button, HasPermission, LoadingPage } from "src/components";
|
||||
import { useCertificates } from "src/hooks";
|
||||
import { T } from "src/locale";
|
||||
import {
|
||||
@@ -13,6 +13,7 @@ import {
|
||||
showHTTPCertificateModal,
|
||||
showRenewCertificateModal,
|
||||
} from "src/modals";
|
||||
import { CERTIFICATES, MANAGE } from "src/modules/Permissions";
|
||||
import { showError, showObjectSuccess } from "src/notifications";
|
||||
import Table from "./Table";
|
||||
|
||||
@@ -70,7 +71,6 @@ export default function TableWrapper() {
|
||||
<T id="certificates" />
|
||||
</h2>
|
||||
</div>
|
||||
|
||||
<div className="col-md-auto col-sm-12">
|
||||
<div className="ms-auto d-flex flex-wrap btn-list">
|
||||
{data?.length ? (
|
||||
@@ -90,50 +90,52 @@ export default function TableWrapper() {
|
||||
<Button size="sm" onClick={() => showHelpModal("Certificates", "pink")}>
|
||||
<IconHelp size={20} />
|
||||
</Button>
|
||||
{data?.length ? (
|
||||
<div className="dropdown">
|
||||
<button
|
||||
type="button"
|
||||
className="btn btn-sm dropdown-toggle btn-pink mt-1"
|
||||
data-bs-toggle="dropdown"
|
||||
>
|
||||
<T id="object.add" tData={{ object: "certificate" }} />
|
||||
</button>
|
||||
<div className="dropdown-menu">
|
||||
<a
|
||||
className="dropdown-item"
|
||||
href="#"
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
showHTTPCertificateModal();
|
||||
}}
|
||||
<HasPermission section={CERTIFICATES} permission={MANAGE} hideError>
|
||||
{data?.length ? (
|
||||
<div className="dropdown">
|
||||
<button
|
||||
type="button"
|
||||
className="btn btn-sm dropdown-toggle btn-pink mt-1"
|
||||
data-bs-toggle="dropdown"
|
||||
>
|
||||
<T id="lets-encrypt-via-http" />
|
||||
</a>
|
||||
<a
|
||||
className="dropdown-item"
|
||||
href="#"
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
showDNSCertificateModal();
|
||||
}}
|
||||
>
|
||||
<T id="lets-encrypt-via-dns" />
|
||||
</a>
|
||||
<div className="dropdown-divider" />
|
||||
<a
|
||||
className="dropdown-item"
|
||||
href="#"
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
showCustomCertificateModal();
|
||||
}}
|
||||
>
|
||||
<T id="certificates.custom" />
|
||||
</a>
|
||||
<T id="object.add" tData={{ object: "certificate" }} />
|
||||
</button>
|
||||
<div className="dropdown-menu">
|
||||
<a
|
||||
className="dropdown-item"
|
||||
href="#"
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
showHTTPCertificateModal();
|
||||
}}
|
||||
>
|
||||
<T id="lets-encrypt-via-http" />
|
||||
</a>
|
||||
<a
|
||||
className="dropdown-item"
|
||||
href="#"
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
showDNSCertificateModal();
|
||||
}}
|
||||
>
|
||||
<T id="lets-encrypt-via-dns" />
|
||||
</a>
|
||||
<div className="dropdown-divider" />
|
||||
<a
|
||||
className="dropdown-item"
|
||||
href="#"
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
showCustomCertificateModal();
|
||||
}}
|
||||
>
|
||||
<T id="certificates.custom" />
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
) : null}
|
||||
) : null}
|
||||
</HasPermission>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
import { HasPermission } from "src/components";
|
||||
import { CERTIFICATES, VIEW } from "src/modules/Permissions";
|
||||
import TableWrapper from "./TableWrapper";
|
||||
|
||||
const Certificates = () => {
|
||||
return (
|
||||
<HasPermission permission="certificates" type="view" pageLoading loadingNoLogo>
|
||||
<HasPermission section={CERTIFICATES} permission={VIEW} pageLoading loadingNoLogo>
|
||||
<TableWrapper />
|
||||
</HasPermission>
|
||||
);
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
import { IconArrowsCross, IconBolt, IconBoltOff, IconDisc } from "@tabler/icons-react";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import { HasPermission } from "src/components";
|
||||
import { useHostReport } from "src/hooks";
|
||||
import { T } from "src/locale";
|
||||
import { DEAD_HOSTS, PROXY_HOSTS, REDIRECTION_HOSTS, STREAMS, VIEW } from "src/modules/Permissions";
|
||||
|
||||
const Dashboard = () => {
|
||||
const { data: hostReport } = useHostReport();
|
||||
@@ -15,100 +17,111 @@ const Dashboard = () => {
|
||||
<div className="row row-deck row-cards">
|
||||
<div className="col-12 my-4">
|
||||
<div className="row row-cards">
|
||||
<div className="col-sm-6 col-lg-3">
|
||||
<a
|
||||
href="/nginx/proxy"
|
||||
className="card card-sm card-link card-link-pop"
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
navigate("/nginx/proxy");
|
||||
}}
|
||||
>
|
||||
<div className="card-body">
|
||||
<div className="row align-items-center">
|
||||
<div className="col-auto">
|
||||
<span className="bg-green text-white avatar">
|
||||
<IconBolt />
|
||||
</span>
|
||||
</div>
|
||||
<div className="col">
|
||||
<div className="font-weight-medium">
|
||||
<T id="proxy-hosts.count" data={{ count: hostReport?.proxy }} />
|
||||
<HasPermission section={PROXY_HOSTS} permission={VIEW} hideError>
|
||||
<div className="col-sm-6 col-lg-3">
|
||||
<a
|
||||
href="/nginx/proxy"
|
||||
className="card card-sm card-link card-link-pop"
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
navigate("/nginx/proxy");
|
||||
}}
|
||||
>
|
||||
<div className="card-body">
|
||||
<div className="row align-items-center">
|
||||
<div className="col-auto">
|
||||
<span className="bg-green text-white avatar">
|
||||
<IconBolt />
|
||||
</span>
|
||||
</div>
|
||||
<div className="col">
|
||||
<div className="font-weight-medium">
|
||||
<T id="proxy-hosts.count" data={{ count: hostReport?.proxy }} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</a>
|
||||
</div>
|
||||
<div className="col-sm-6 col-lg-3">
|
||||
<a
|
||||
href="/nginx/redirection"
|
||||
className="card card-sm card-link card-link-pop"
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
navigate("/nginx/redirection");
|
||||
}}
|
||||
>
|
||||
<div className="card-body">
|
||||
<div className="row align-items-center">
|
||||
<div className="col-auto">
|
||||
<span className="bg-yellow text-white avatar">
|
||||
<IconArrowsCross />
|
||||
</span>
|
||||
</div>
|
||||
<div className="col">
|
||||
<T id="redirection-hosts.count" data={{ count: hostReport?.redirection }} />
|
||||
</a>
|
||||
</div>
|
||||
</HasPermission>
|
||||
<HasPermission section={REDIRECTION_HOSTS} permission={VIEW} hideError>
|
||||
<div className="col-sm-6 col-lg-3">
|
||||
<a
|
||||
href="/nginx/redirection"
|
||||
className="card card-sm card-link card-link-pop"
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
navigate("/nginx/redirection");
|
||||
}}
|
||||
>
|
||||
<div className="card-body">
|
||||
<div className="row align-items-center">
|
||||
<div className="col-auto">
|
||||
<span className="bg-yellow text-white avatar">
|
||||
<IconArrowsCross />
|
||||
</span>
|
||||
</div>
|
||||
<div className="col">
|
||||
<T
|
||||
id="redirection-hosts.count"
|
||||
data={{ count: hostReport?.redirection }}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</a>
|
||||
</div>
|
||||
<div className="col-sm-6 col-lg-3">
|
||||
<a
|
||||
href="/nginx/stream"
|
||||
className="card card-sm card-link card-link-pop"
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
navigate("/nginx/stream");
|
||||
}}
|
||||
>
|
||||
<div className="card-body">
|
||||
<div className="row align-items-center">
|
||||
<div className="col-auto">
|
||||
<span className="bg-blue text-white avatar">
|
||||
<IconDisc />
|
||||
</span>
|
||||
</div>
|
||||
<div className="col">
|
||||
<T id="streams.count" data={{ count: hostReport?.stream }} />
|
||||
</a>
|
||||
</div>
|
||||
</HasPermission>
|
||||
<HasPermission section={STREAMS} permission={VIEW} hideError>
|
||||
<div className="col-sm-6 col-lg-3">
|
||||
<a
|
||||
href="/nginx/stream"
|
||||
className="card card-sm card-link card-link-pop"
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
navigate("/nginx/stream");
|
||||
}}
|
||||
>
|
||||
<div className="card-body">
|
||||
<div className="row align-items-center">
|
||||
<div className="col-auto">
|
||||
<span className="bg-blue text-white avatar">
|
||||
<IconDisc />
|
||||
</span>
|
||||
</div>
|
||||
<div className="col">
|
||||
<T id="streams.count" data={{ count: hostReport?.stream }} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</a>
|
||||
</div>
|
||||
<div className="col-sm-6 col-lg-3">
|
||||
<a
|
||||
href="/nginx/404"
|
||||
className="card card-sm card-link card-link-pop"
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
navigate("/nginx/404");
|
||||
}}
|
||||
>
|
||||
<div className="card-body">
|
||||
<div className="row align-items-center">
|
||||
<div className="col-auto">
|
||||
<span className="bg-red text-white avatar">
|
||||
<IconBoltOff />
|
||||
</span>
|
||||
</div>
|
||||
<div className="col">
|
||||
<T id="dead-hosts.count" data={{ count: hostReport?.dead }} />
|
||||
</a>
|
||||
</div>
|
||||
</HasPermission>
|
||||
<HasPermission section={DEAD_HOSTS} permission={VIEW} hideError>
|
||||
<div className="col-sm-6 col-lg-3">
|
||||
<a
|
||||
href="/nginx/404"
|
||||
className="card card-sm card-link card-link-pop"
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
navigate("/nginx/404");
|
||||
}}
|
||||
>
|
||||
<div className="card-body">
|
||||
<div className="row align-items-center">
|
||||
<div className="col-auto">
|
||||
<span className="bg-red text-white avatar">
|
||||
<IconBoltOff />
|
||||
</span>
|
||||
</div>
|
||||
<div className="col">
|
||||
<T id="dead-hosts.count" data={{ count: hostReport?.dead }} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</a>
|
||||
</div>
|
||||
</a>
|
||||
</div>
|
||||
</HasPermission>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -7,10 +7,12 @@ import {
|
||||
DomainsFormatter,
|
||||
EmptyData,
|
||||
GravatarFormatter,
|
||||
HasPermission,
|
||||
TrueFalseFormatter,
|
||||
} from "src/components";
|
||||
import { TableLayout } from "src/components/Table/TableLayout";
|
||||
import { intl, T } from "src/locale";
|
||||
import { DEAD_HOSTS, MANAGE } from "src/modules/Permissions";
|
||||
|
||||
interface Props {
|
||||
data: DeadHost[];
|
||||
@@ -89,29 +91,31 @@ export default function Table({ data, isFetching, onEdit, onDelete, onDisableTog
|
||||
<IconEdit size={16} />
|
||||
<T id="action.edit" />
|
||||
</a>
|
||||
<a
|
||||
className="dropdown-item"
|
||||
href="#"
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
onDisableToggle?.(info.row.original.id, !info.row.original.enabled);
|
||||
}}
|
||||
>
|
||||
<IconPower size={16} />
|
||||
<T id={info.row.original.enabled ? "action.disable" : "action.enable"} />
|
||||
</a>
|
||||
<div className="dropdown-divider" />
|
||||
<a
|
||||
className="dropdown-item"
|
||||
href="#"
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
onDelete?.(info.row.original.id);
|
||||
}}
|
||||
>
|
||||
<IconTrash size={16} />
|
||||
<T id="action.delete" />
|
||||
</a>
|
||||
<HasPermission section={DEAD_HOSTS} permission={MANAGE} hideError>
|
||||
<a
|
||||
className="dropdown-item"
|
||||
href="#"
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
onDisableToggle?.(info.row.original.id, !info.row.original.enabled);
|
||||
}}
|
||||
>
|
||||
<IconPower size={16} />
|
||||
<T id={info.row.original.enabled ? "action.disable" : "action.enable"} />
|
||||
</a>
|
||||
<div className="dropdown-divider" />
|
||||
<a
|
||||
className="dropdown-item"
|
||||
href="#"
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
onDelete?.(info.row.original.id);
|
||||
}}
|
||||
>
|
||||
<IconTrash size={16} />
|
||||
<T id="action.delete" />
|
||||
</a>
|
||||
</HasPermission>
|
||||
</div>
|
||||
</span>
|
||||
);
|
||||
@@ -146,6 +150,7 @@ export default function Table({ data, isFetching, onEdit, onDelete, onDisableTog
|
||||
onNew={onNew}
|
||||
isFiltered={isFiltered}
|
||||
color="red"
|
||||
permissionSection={DEAD_HOSTS}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
|
||||
@@ -3,10 +3,11 @@ import { useQueryClient } from "@tanstack/react-query";
|
||||
import { useState } from "react";
|
||||
import Alert from "react-bootstrap/Alert";
|
||||
import { deleteDeadHost, toggleDeadHost } from "src/api/backend";
|
||||
import { Button, LoadingPage } from "src/components";
|
||||
import { Button, HasPermission, LoadingPage } from "src/components";
|
||||
import { useDeadHosts } from "src/hooks";
|
||||
import { T } from "src/locale";
|
||||
import { showDeadHostModal, showDeleteConfirmModal, showHelpModal } from "src/modals";
|
||||
import { DEAD_HOSTS, MANAGE } from "src/modules/Permissions";
|
||||
import { showObjectSuccess } from "src/notifications";
|
||||
import Table from "./Table";
|
||||
|
||||
@@ -76,11 +77,13 @@ export default function TableWrapper() {
|
||||
<Button size="sm" onClick={() => showHelpModal("DeadHosts", "red")}>
|
||||
<IconHelp size={20} />
|
||||
</Button>
|
||||
{data?.length ? (
|
||||
<Button size="sm" className="btn-red" onClick={() => showDeadHostModal("new")}>
|
||||
<T id="object.add" tData={{ object: "dead-host" }} />
|
||||
</Button>
|
||||
) : null}
|
||||
<HasPermission section={DEAD_HOSTS} permission={MANAGE} hideError>
|
||||
{data?.length ? (
|
||||
<Button size="sm" className="btn-red" onClick={() => showDeadHostModal("new")}>
|
||||
<T id="object.add" tData={{ object: "dead-host" }} />
|
||||
</Button>
|
||||
) : null}
|
||||
</HasPermission>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
import { HasPermission } from "src/components";
|
||||
import { DEAD_HOSTS, VIEW } from "src/modules/Permissions";
|
||||
import TableWrapper from "./TableWrapper";
|
||||
|
||||
const DeadHosts = () => {
|
||||
return (
|
||||
<HasPermission permission="deadHosts" type="view" pageLoading loadingNoLogo>
|
||||
<HasPermission section={DEAD_HOSTS} permission={VIEW} pageLoading loadingNoLogo>
|
||||
<TableWrapper />
|
||||
</HasPermission>
|
||||
);
|
||||
|
||||
@@ -8,10 +8,12 @@ import {
|
||||
DomainsFormatter,
|
||||
EmptyData,
|
||||
GravatarFormatter,
|
||||
HasPermission,
|
||||
TrueFalseFormatter,
|
||||
} from "src/components";
|
||||
import { TableLayout } from "src/components/Table/TableLayout";
|
||||
import { intl, T } from "src/locale";
|
||||
import { MANAGE, PROXY_HOSTS } from "src/modules/Permissions";
|
||||
|
||||
interface Props {
|
||||
data: ProxyHost[];
|
||||
@@ -105,29 +107,31 @@ export default function Table({ data, isFetching, onEdit, onDelete, onDisableTog
|
||||
<IconEdit size={16} />
|
||||
<T id="action.edit" />
|
||||
</a>
|
||||
<a
|
||||
className="dropdown-item"
|
||||
href="#"
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
onDisableToggle?.(info.row.original.id, !info.row.original.enabled);
|
||||
}}
|
||||
>
|
||||
<IconPower size={16} />
|
||||
<T id={info.row.original.enabled ? "action.disable" : "action.enable"} />
|
||||
</a>
|
||||
<div className="dropdown-divider" />
|
||||
<a
|
||||
className="dropdown-item"
|
||||
href="#"
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
onDelete?.(info.row.original.id);
|
||||
}}
|
||||
>
|
||||
<IconTrash size={16} />
|
||||
<T id="action.delete" />
|
||||
</a>
|
||||
<HasPermission section={PROXY_HOSTS} permission={MANAGE} hideError>
|
||||
<a
|
||||
className="dropdown-item"
|
||||
href="#"
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
onDisableToggle?.(info.row.original.id, !info.row.original.enabled);
|
||||
}}
|
||||
>
|
||||
<IconPower size={16} />
|
||||
<T id={info.row.original.enabled ? "action.disable" : "action.enable"} />
|
||||
</a>
|
||||
<div className="dropdown-divider" />
|
||||
<a
|
||||
className="dropdown-item"
|
||||
href="#"
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
onDelete?.(info.row.original.id);
|
||||
}}
|
||||
>
|
||||
<IconTrash size={16} />
|
||||
<T id="action.delete" />
|
||||
</a>
|
||||
</HasPermission>
|
||||
</div>
|
||||
</span>
|
||||
);
|
||||
@@ -162,6 +166,7 @@ export default function Table({ data, isFetching, onEdit, onDelete, onDisableTog
|
||||
onNew={onNew}
|
||||
isFiltered={isFiltered}
|
||||
color="lime"
|
||||
permissionSection={PROXY_HOSTS}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
|
||||
@@ -3,10 +3,11 @@ import { useQueryClient } from "@tanstack/react-query";
|
||||
import { useState } from "react";
|
||||
import Alert from "react-bootstrap/Alert";
|
||||
import { deleteProxyHost, toggleProxyHost } from "src/api/backend";
|
||||
import { Button, LoadingPage } from "src/components";
|
||||
import { Button, HasPermission, LoadingPage } from "src/components";
|
||||
import { useProxyHosts } from "src/hooks";
|
||||
import { T } from "src/locale";
|
||||
import { showDeleteConfirmModal, showHelpModal, showProxyHostModal } from "src/modals";
|
||||
import { MANAGE, PROXY_HOSTS } from "src/modules/Permissions";
|
||||
import { showObjectSuccess } from "src/notifications";
|
||||
import Table from "./Table";
|
||||
|
||||
@@ -59,7 +60,6 @@ export default function TableWrapper() {
|
||||
<T id="proxy-hosts" />
|
||||
</h2>
|
||||
</div>
|
||||
|
||||
<div className="col-md-auto col-sm-12">
|
||||
<div className="ms-auto d-flex flex-wrap btn-list">
|
||||
{data?.length ? (
|
||||
@@ -79,11 +79,17 @@ export default function TableWrapper() {
|
||||
<Button size="sm" onClick={() => showHelpModal("ProxyHosts", "lime")}>
|
||||
<IconHelp size={20} />
|
||||
</Button>
|
||||
{data?.length ? (
|
||||
<Button size="sm" className="btn-lime" onClick={() => showProxyHostModal("new")}>
|
||||
<T id="object.add" tData={{ object: "proxy-host" }} />
|
||||
</Button>
|
||||
) : null}
|
||||
<HasPermission section={PROXY_HOSTS} permission={MANAGE} hideError>
|
||||
{data?.length ? (
|
||||
<Button
|
||||
size="sm"
|
||||
className="btn-lime"
|
||||
onClick={() => showProxyHostModal("new")}
|
||||
>
|
||||
<T id="object.add" tData={{ object: "proxy-host" }} />
|
||||
</Button>
|
||||
) : null}
|
||||
</HasPermission>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
import { HasPermission } from "src/components";
|
||||
import { PROXY_HOSTS, VIEW } from "src/modules/Permissions";
|
||||
import TableWrapper from "./TableWrapper";
|
||||
|
||||
const ProxyHosts = () => {
|
||||
return (
|
||||
<HasPermission permission="proxyHosts" type="view" pageLoading loadingNoLogo>
|
||||
<HasPermission section={PROXY_HOSTS} permission={VIEW} pageLoading loadingNoLogo>
|
||||
<TableWrapper />
|
||||
</HasPermission>
|
||||
);
|
||||
|
||||
@@ -7,10 +7,12 @@ import {
|
||||
DomainsFormatter,
|
||||
EmptyData,
|
||||
GravatarFormatter,
|
||||
HasPermission,
|
||||
TrueFalseFormatter,
|
||||
} from "src/components";
|
||||
import { TableLayout } from "src/components/Table/TableLayout";
|
||||
import { intl, T } from "src/locale";
|
||||
import { MANAGE, REDIRECTION_HOSTS } from "src/modules/Permissions";
|
||||
|
||||
interface Props {
|
||||
data: RedirectionHost[];
|
||||
@@ -110,29 +112,31 @@ export default function Table({ data, isFetching, onEdit, onDelete, onDisableTog
|
||||
<IconEdit size={16} />
|
||||
<T id="action.edit" />
|
||||
</a>
|
||||
<a
|
||||
className="dropdown-item"
|
||||
href="#"
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
onDisableToggle?.(info.row.original.id, !info.row.original.enabled);
|
||||
}}
|
||||
>
|
||||
<IconPower size={16} />
|
||||
<T id={info.row.original.enabled ? "action.disable" : "action.enable"} />
|
||||
</a>
|
||||
<div className="dropdown-divider" />
|
||||
<a
|
||||
className="dropdown-item"
|
||||
href="#"
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
onDelete?.(info.row.original.id);
|
||||
}}
|
||||
>
|
||||
<IconTrash size={16} />
|
||||
<T id="action.delete" />
|
||||
</a>
|
||||
<HasPermission section={REDIRECTION_HOSTS} permission={MANAGE} hideError>
|
||||
<a
|
||||
className="dropdown-item"
|
||||
href="#"
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
onDisableToggle?.(info.row.original.id, !info.row.original.enabled);
|
||||
}}
|
||||
>
|
||||
<IconPower size={16} />
|
||||
<T id={info.row.original.enabled ? "action.disable" : "action.enable"} />
|
||||
</a>
|
||||
<div className="dropdown-divider" />
|
||||
<a
|
||||
className="dropdown-item"
|
||||
href="#"
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
onDelete?.(info.row.original.id);
|
||||
}}
|
||||
>
|
||||
<IconTrash size={16} />
|
||||
<T id="action.delete" />
|
||||
</a>
|
||||
</HasPermission>
|
||||
</div>
|
||||
</span>
|
||||
);
|
||||
@@ -167,6 +171,7 @@ export default function Table({ data, isFetching, onEdit, onDelete, onDisableTog
|
||||
onNew={onNew}
|
||||
isFiltered={isFiltered}
|
||||
color="yellow"
|
||||
permissionSection={REDIRECTION_HOSTS}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
|
||||
@@ -3,10 +3,11 @@ import { useQueryClient } from "@tanstack/react-query";
|
||||
import { useState } from "react";
|
||||
import Alert from "react-bootstrap/Alert";
|
||||
import { deleteRedirectionHost, toggleRedirectionHost } from "src/api/backend";
|
||||
import { Button, LoadingPage } from "src/components";
|
||||
import { Button, HasPermission, LoadingPage } from "src/components";
|
||||
import { useRedirectionHosts } from "src/hooks";
|
||||
import { T } from "src/locale";
|
||||
import { showDeleteConfirmModal, showHelpModal, showRedirectionHostModal } from "src/modals";
|
||||
import { MANAGE, REDIRECTION_HOSTS } from "src/modules/Permissions";
|
||||
import { showObjectSuccess } from "src/notifications";
|
||||
import Table from "./Table";
|
||||
|
||||
@@ -59,7 +60,6 @@ export default function TableWrapper() {
|
||||
<T id="redirection-hosts" />
|
||||
</h2>
|
||||
</div>
|
||||
|
||||
<div className="col-md-auto col-sm-12">
|
||||
<div className="ms-auto d-flex flex-wrap btn-list">
|
||||
{data?.length ? (
|
||||
@@ -79,15 +79,17 @@ export default function TableWrapper() {
|
||||
<Button size="sm" onClick={() => showHelpModal("RedirectionHosts", "yellow")}>
|
||||
<IconHelp size={20} />
|
||||
</Button>
|
||||
{data?.length ? (
|
||||
<Button
|
||||
size="sm"
|
||||
className="btn-yellow"
|
||||
onClick={() => showRedirectionHostModal("new")}
|
||||
>
|
||||
<T id="object.add" tData={{ object: "redirection-host" }} />
|
||||
</Button>
|
||||
) : null}
|
||||
<HasPermission section={REDIRECTION_HOSTS} permission={MANAGE} hideError>
|
||||
{data?.length ? (
|
||||
<Button
|
||||
size="sm"
|
||||
className="btn-yellow"
|
||||
onClick={() => showRedirectionHostModal("new")}
|
||||
>
|
||||
<T id="object.add" tData={{ object: "redirection-host" }} />
|
||||
</Button>
|
||||
) : null}
|
||||
</HasPermission>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
import { HasPermission } from "src/components";
|
||||
import { REDIRECTION_HOSTS, VIEW } from "src/modules/Permissions";
|
||||
import TableWrapper from "./TableWrapper";
|
||||
|
||||
const RedirectionHosts = () => {
|
||||
return (
|
||||
<HasPermission permission="redirectionHosts" type="view" pageLoading loadingNoLogo>
|
||||
<HasPermission section={REDIRECTION_HOSTS} permission={VIEW} pageLoading loadingNoLogo>
|
||||
<TableWrapper />
|
||||
</HasPermission>
|
||||
);
|
||||
|
||||
@@ -6,11 +6,13 @@ import {
|
||||
CertificateFormatter,
|
||||
EmptyData,
|
||||
GravatarFormatter,
|
||||
HasPermission,
|
||||
TrueFalseFormatter,
|
||||
ValueWithDateFormatter,
|
||||
} from "src/components";
|
||||
import { TableLayout } from "src/components/Table/TableLayout";
|
||||
import { intl, T } from "src/locale";
|
||||
import { MANAGE, STREAMS } from "src/modules/Permissions";
|
||||
|
||||
interface Props {
|
||||
data: Stream[];
|
||||
@@ -118,29 +120,31 @@ export default function Table({ data, isFetching, isFiltered, onEdit, onDelete,
|
||||
<IconEdit size={16} />
|
||||
<T id="action.edit" />
|
||||
</a>
|
||||
<a
|
||||
className="dropdown-item"
|
||||
href="#"
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
onDisableToggle?.(info.row.original.id, !info.row.original.enabled);
|
||||
}}
|
||||
>
|
||||
<IconPower size={16} />
|
||||
<T id="action.disable" />
|
||||
</a>
|
||||
<div className="dropdown-divider" />
|
||||
<a
|
||||
className="dropdown-item"
|
||||
href="#"
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
onDelete?.(info.row.original.id);
|
||||
}}
|
||||
>
|
||||
<IconTrash size={16} />
|
||||
<T id="action.delete" />
|
||||
</a>
|
||||
<HasPermission section={STREAMS} permission={MANAGE} hideError>
|
||||
<a
|
||||
className="dropdown-item"
|
||||
href="#"
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
onDisableToggle?.(info.row.original.id, !info.row.original.enabled);
|
||||
}}
|
||||
>
|
||||
<IconPower size={16} />
|
||||
<T id="action.disable" />
|
||||
</a>
|
||||
<div className="dropdown-divider" />
|
||||
<a
|
||||
className="dropdown-item"
|
||||
href="#"
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
onDelete?.(info.row.original.id);
|
||||
}}
|
||||
>
|
||||
<IconTrash size={16} />
|
||||
<T id="action.delete" />
|
||||
</a>
|
||||
</HasPermission>
|
||||
</div>
|
||||
</span>
|
||||
);
|
||||
@@ -175,6 +179,7 @@ export default function Table({ data, isFetching, isFiltered, onEdit, onDelete,
|
||||
onNew={onNew}
|
||||
isFiltered={isFiltered}
|
||||
color="blue"
|
||||
permissionSection={STREAMS}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
|
||||
@@ -3,10 +3,11 @@ import { useQueryClient } from "@tanstack/react-query";
|
||||
import { useState } from "react";
|
||||
import Alert from "react-bootstrap/Alert";
|
||||
import { deleteStream, toggleStream } from "src/api/backend";
|
||||
import { Button, LoadingPage } from "src/components";
|
||||
import { Button, HasPermission, LoadingPage } from "src/components";
|
||||
import { useStreams } from "src/hooks";
|
||||
import { T } from "src/locale";
|
||||
import { showDeleteConfirmModal, showHelpModal, showStreamModal } from "src/modals";
|
||||
import { MANAGE, STREAMS } from "src/modules/Permissions";
|
||||
import { showObjectSuccess } from "src/notifications";
|
||||
import Table from "./Table";
|
||||
|
||||
@@ -61,7 +62,6 @@ export default function TableWrapper() {
|
||||
<T id="streams" />
|
||||
</h2>
|
||||
</div>
|
||||
|
||||
<div className="col-md-auto col-sm-12">
|
||||
<div className="ms-auto d-flex flex-wrap btn-list">
|
||||
{data?.length ? (
|
||||
@@ -81,11 +81,13 @@ export default function TableWrapper() {
|
||||
<Button size="sm" onClick={() => showHelpModal("Streams", "blue")}>
|
||||
<IconHelp size={20} />
|
||||
</Button>
|
||||
{data?.length ? (
|
||||
<Button size="sm" className="btn-blue" onClick={() => showStreamModal("new")}>
|
||||
<T id="object.add" tData={{ object: "stream" }} />
|
||||
</Button>
|
||||
) : null}
|
||||
<HasPermission section={STREAMS} permission={MANAGE} hideError>
|
||||
{data?.length ? (
|
||||
<Button size="sm" className="btn-blue" onClick={() => showStreamModal("new")}>
|
||||
<T id="object.add" tData={{ object: "stream" }} />
|
||||
</Button>
|
||||
) : null}
|
||||
</HasPermission>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
import { HasPermission } from "src/components";
|
||||
import { STREAMS, VIEW } from "src/modules/Permissions";
|
||||
import TableWrapper from "./TableWrapper";
|
||||
|
||||
const Streams = () => {
|
||||
return (
|
||||
<HasPermission permission="streams" type="view" pageLoading loadingNoLogo>
|
||||
<HasPermission section={STREAMS} permission={VIEW} pageLoading loadingNoLogo>
|
||||
<TableWrapper />
|
||||
</HasPermission>
|
||||
);
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
import { HasPermission } from "src/components";
|
||||
import { ADMIN, VIEW } from "src/modules/Permissions";
|
||||
import Layout from "./Layout";
|
||||
|
||||
const Settings = () => {
|
||||
return (
|
||||
<HasPermission permission="admin" type="manage" pageLoading loadingNoLogo>
|
||||
<HasPermission section={ADMIN} permission={VIEW} pageLoading loadingNoLogo>
|
||||
<Layout />
|
||||
</HasPermission>
|
||||
);
|
||||
|
||||
@@ -1,4 +1,12 @@
|
||||
import { IconDotsVertical, IconEdit, IconLock, IconPower, IconShield, IconTrash } from "@tabler/icons-react";
|
||||
import {
|
||||
IconDotsVertical,
|
||||
IconEdit,
|
||||
IconLock,
|
||||
IconLogin2,
|
||||
IconPower,
|
||||
IconShield,
|
||||
IconTrash,
|
||||
} from "@tabler/icons-react";
|
||||
import { createColumnHelper, getCoreRowModel, useReactTable } from "@tanstack/react-table";
|
||||
import { useMemo } from "react";
|
||||
import type { User } from "src/api/backend";
|
||||
@@ -24,6 +32,7 @@ interface Props {
|
||||
onDeleteUser?: (id: number) => void;
|
||||
onDisableToggle?: (id: number, enabled: boolean) => void;
|
||||
onNewUser?: () => void;
|
||||
onLoginAs?: (id: number) => void;
|
||||
}
|
||||
export default function Table({
|
||||
data,
|
||||
@@ -36,6 +45,7 @@ export default function Table({
|
||||
onDeleteUser,
|
||||
onDisableToggle,
|
||||
onNewUser,
|
||||
onLoginAs,
|
||||
}: Props) {
|
||||
const columnHelper = createColumnHelper<User>();
|
||||
const columns = useMemo(
|
||||
@@ -153,6 +163,24 @@ export default function Table({
|
||||
<IconPower size={16} />
|
||||
<T id={info.row.original.isDisabled ? "action.enable" : "action.disable"} />
|
||||
</a>
|
||||
{info.row.original.isDisabled ? (
|
||||
<div className="dropdown-item text-muted">
|
||||
<IconLogin2 size={16} />
|
||||
<T id="user.login-as" data={{ name: info.row.original.name }} />
|
||||
</div>
|
||||
) : (
|
||||
<a
|
||||
className="dropdown-item"
|
||||
href="#"
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
onLoginAs?.(info.row.original.id);
|
||||
}}
|
||||
>
|
||||
<IconLogin2 size={16} />
|
||||
<T id="user.login-as" data={{ name: info.row.original.name }} />
|
||||
</a>
|
||||
)}
|
||||
<div className="dropdown-divider" />
|
||||
<a
|
||||
className="dropdown-item"
|
||||
@@ -176,7 +204,16 @@ export default function Table({
|
||||
},
|
||||
}),
|
||||
],
|
||||
[columnHelper, currentUserId, onEditUser, onDisableToggle, onDeleteUser, onEditPermissions, onSetPassword],
|
||||
[
|
||||
columnHelper,
|
||||
currentUserId,
|
||||
onEditUser,
|
||||
onDisableToggle,
|
||||
onDeleteUser,
|
||||
onEditPermissions,
|
||||
onSetPassword,
|
||||
onLoginAs,
|
||||
],
|
||||
);
|
||||
|
||||
const tableInstance = useReactTable<User>({
|
||||
|
||||
@@ -4,14 +4,16 @@ import { useState } from "react";
|
||||
import Alert from "react-bootstrap/Alert";
|
||||
import { deleteUser, toggleUser } from "src/api/backend";
|
||||
import { Button, LoadingPage } from "src/components";
|
||||
import { useAuthState } from "src/context";
|
||||
import { useUser, useUsers } from "src/hooks";
|
||||
import { T } from "src/locale";
|
||||
import { showDeleteConfirmModal, showPermissionsModal, showSetPasswordModal, showUserModal } from "src/modals";
|
||||
import { showObjectSuccess } from "src/notifications";
|
||||
import { showError, showObjectSuccess } from "src/notifications";
|
||||
import Table from "./Table";
|
||||
|
||||
export default function TableWrapper() {
|
||||
const queryClient = useQueryClient();
|
||||
const { loginAs } = useAuthState();
|
||||
const [search, setSearch] = useState("");
|
||||
const { isFetching, isLoading, isError, error, data } = useUsers(["permissions"]);
|
||||
const { data: currentUser } = useUser("me");
|
||||
@@ -24,6 +26,16 @@ export default function TableWrapper() {
|
||||
return <Alert variant="danger">{error?.message || "Unknown error"}</Alert>;
|
||||
}
|
||||
|
||||
const handleLoginAs = async (id: number) => {
|
||||
try {
|
||||
await loginAs(id);
|
||||
} catch (err) {
|
||||
if (err instanceof Error) {
|
||||
showError(err.message);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const handleDelete = async (id: number) => {
|
||||
await deleteUser(id);
|
||||
showObjectSuccess("user", "deleted");
|
||||
@@ -103,6 +115,7 @@ export default function TableWrapper() {
|
||||
}
|
||||
onDisableToggle={handleDisableToggle}
|
||||
onNewUser={() => showUserModal("new")}
|
||||
onLoginAs={handleLoginAs}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
import { HasPermission } from "src/components";
|
||||
import { ADMIN, VIEW } from "src/modules/Permissions";
|
||||
import TableWrapper from "./TableWrapper";
|
||||
|
||||
const Users = () => {
|
||||
return (
|
||||
<HasPermission permission="admin" type="manage" pageLoading loadingNoLogo>
|
||||
<HasPermission section={ADMIN} permission={VIEW} pageLoading loadingNoLogo>
|
||||
<TableWrapper />
|
||||
</HasPermission>
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user