diff --git a/.gitignore b/.gitignore
index a83a2110e0..7d6a1b1bdc 100644
--- a/.gitignore
+++ b/.gitignore
@@ -10,3 +10,9 @@ docker/dev/dnsrouter-config.json.tmp
docker/dev/resolv.conf
.claude
+# Runtime data & certificates
+data/
+letsencrypt/
+*.sqlite
+*.sqlite-journal
+
diff --git a/backend/internal/dead-host.js b/backend/internal/dead-host.js
index 6523e62892..c8e0e17cde 100644
--- a/backend/internal/dead-host.js
+++ b/backend/internal/dead-host.js
@@ -54,6 +54,11 @@ const internalDeadHost = {
thisData.advanced_config = "";
}
+ const isAdmin = access.hasRole?.("admin") || access.token.hasScope("admin");
+ if (!isAdmin && thisData.advanced_config && thisData.advanced_config.trim() !== "") {
+ throw new errs.PermissionError("You do not have permission to modify advanced configuration");
+ }
+
const row = await deadHostModel.query().insertAndFetch(thisData).then(utils.omitRow(omissions()));
// Add to audit log
@@ -151,6 +156,15 @@ const internalDeadHost = {
thisData = internalHost.cleanSslHstsData(thisData, row);
+ const isAdmin = access.hasRole?.("admin") || access.token.hasScope("admin");
+ if (
+ !isAdmin &&
+ typeof data.advanced_config !== "undefined" &&
+ data.advanced_config !== row.advanced_config
+ ) {
+ throw new errs.PermissionError("You do not have permission to modify advanced configuration");
+ }
+
// do the row update
await deadHostModel.query().where({ id: data.id }).patch(data);
diff --git a/backend/internal/proxy-host.js b/backend/internal/proxy-host.js
index 2c159d48ad..ddebe654c0 100644
--- a/backend/internal/proxy-host.js
+++ b/backend/internal/proxy-host.js
@@ -57,6 +57,11 @@ const internalProxyHost = {
thisData.advanced_config = "";
}
+ const isAdmin = access.hasRole?.("admin") || access.token.hasScope("admin");
+ if (!isAdmin && thisData.advanced_config && thisData.advanced_config.trim() !== "") {
+ throw new errs.PermissionError("You do not have permission to modify advanced configuration");
+ }
+
return proxyHostModel.query().insertAndFetch(thisData).then(utils.omitRow(omissions()));
})
.then((row) => {
@@ -183,6 +188,15 @@ const internalProxyHost = {
thisData = internalHost.cleanSslHstsData(thisData, row);
+ const isAdmin = access.hasRole?.("admin") || access.token.hasScope("admin");
+ if (
+ !isAdmin &&
+ typeof thisData.advanced_config !== "undefined" &&
+ thisData.advanced_config !== row.advanced_config
+ ) {
+ throw new errs.PermissionError("You do not have permission to modify advanced configuration");
+ }
+
return proxyHostModel
.query()
.where({ id: thisData.id })
@@ -243,7 +257,14 @@ const internalProxyHost = {
.first();
if (access_data.permission_visibility !== "all") {
- query.andWhere("owner_user_id", access.token.getUserId(1));
+ const permissions = access.getPermissions ? access.getPermissions() : {};
+ const allowedIds = permissions.meta?.proxy_host_ids || permissions.meta?.proxyHostIds || [];
+ query.andWhere((builder) => {
+ builder.where("owner_user_id", access.token.getUserId(1));
+ if (allowedIds.length > 0) {
+ builder.orWhereIn("id", allowedIds);
+ }
+ });
}
if (typeof thisData.expand !== "undefined" && thisData.expand !== null) {
@@ -430,7 +451,14 @@ const internalProxyHost = {
.orderBy(castJsonIfNeed("domain_names"), "ASC");
if (accessData.permission_visibility !== "all") {
- query.andWhere("owner_user_id", access.token.getUserId(1));
+ const permissions = access.getPermissions ? access.getPermissions() : {};
+ const allowedIds = permissions.meta?.proxy_host_ids || permissions.meta?.proxyHostIds || [];
+ query.andWhere((builder) => {
+ builder.where("owner_user_id", access.token.getUserId(1));
+ if (allowedIds.length > 0) {
+ builder.orWhereIn("id", allowedIds);
+ }
+ });
}
// Query is used for searching
diff --git a/backend/internal/redirection-host.js b/backend/internal/redirection-host.js
index 542439fd36..5e0f3a2604 100644
--- a/backend/internal/redirection-host.js
+++ b/backend/internal/redirection-host.js
@@ -53,8 +53,13 @@ const internalRedirectionHost = {
// Fix for db field not having a default value
// for this optional field.
- if (typeof data.advanced_config === "undefined") {
- data.advanced_config = "";
+ if (typeof thisData.advanced_config === "undefined") {
+ thisData.advanced_config = "";
+ }
+
+ const isAdmin = access.hasRole?.("admin") || access.token.hasScope("admin");
+ if (!isAdmin && thisData.advanced_config && thisData.advanced_config.trim() !== "") {
+ throw new errs.PermissionError("You do not have permission to modify advanced configuration");
}
return redirectionHostModel.query().insertAndFetch(thisData).then(utils.omitRow(omissions()));
@@ -183,6 +188,15 @@ const internalRedirectionHost = {
thisData = internalHost.cleanSslHstsData(thisData, row);
+ const isAdmin = access.hasRole?.("admin") || access.token.hasScope("admin");
+ if (
+ !isAdmin &&
+ typeof thisData.advanced_config !== "undefined" &&
+ thisData.advanced_config !== row.advanced_config
+ ) {
+ throw new errs.PermissionError("You do not have permission to modify advanced configuration");
+ }
+
return redirectionHostModel
.query()
.where({ id: thisData.id })
diff --git a/backend/internal/user.js b/backend/internal/user.js
index d4080dd78f..92c12c0cce 100644
--- a/backend/internal/user.js
+++ b/backend/internal/user.js
@@ -39,7 +39,7 @@ const internalUser = {
let user = await userModel.query().insertAndFetch(data).then(utils.omitRow(omissions()));
if (auth) {
- user = await authModel.query().insert({
+ await authModel.query().insert({
user_id: user.id,
type: auth.type,
secret: auth.secret,
@@ -434,7 +434,7 @@ const internalUser = {
return internalUser.get(access, { id: data.id });
})
.then((user) => {
- if (user.id !== data.id) {
+ if (Number(user.id) !== Number(data.id)) {
// Sanity check that something crazy hasn't happened
throw new errs.InternalValidationError(
`User could not be updated, IDs do not match: ${user.id} !== ${data.id}`,
@@ -444,6 +444,7 @@ const internalUser = {
return user;
})
.then((user) => {
+ const permData = _.omit(data, ["id"]);
// Get perms row, patch if it exists
return userPermissionModel
.query()
@@ -455,10 +456,10 @@ const internalUser = {
return userPermissionModel
.query()
.where("user_id", user.id)
- .patchAndFetchById(existing_auth.id, _.assign({ user_id: user.id }, data));
+ .patchAndFetchById(existing_auth.id, _.assign({ user_id: user.id }, permData));
}
// insert
- return userPermissionModel.query().insertAndFetch(_.assign({ user_id: user.id }, data));
+ return userPermissionModel.query().insertAndFetch(_.assign({ user_id: user.id }, permData));
})
.then((permissions) => {
// Add to Audit Log
diff --git a/backend/lib/access.js b/backend/lib/access.js
index 5f96544ae5..c4cb8529e3 100644
--- a/backend/lib/access.js
+++ b/backend/lib/access.js
@@ -122,7 +122,13 @@ export default function (tokenString) {
const query = proxyHostModel.query().select("id").andWhere("is_deleted", 0);
if (permissions.visibility === "user") {
- query.andWhere("owner_user_id", tokenUserId);
+ const allowedIds = permissions.meta?.proxy_host_ids || permissions.meta?.proxyHostIds || [];
+ query.andWhere((builder) => {
+ builder.where("owner_user_id", tokenUserId);
+ if (allowedIds.length > 0) {
+ builder.orWhereIn("id", allowedIds);
+ }
+ });
}
const rows = await query;
@@ -196,6 +202,10 @@ export default function (tokenString) {
return {
token: Token,
+ getPermissions: () => permissions,
+ getRoles: () => userRoles,
+ hasRole: (role) => userRoles.includes(role),
+
/**
*
* @param {Boolean} [allowInternal]
@@ -203,7 +213,8 @@ export default function (tokenString) {
*/
load: async (allowInternal) => {
if (tokenString) {
- return await Token.load(tokenString);
+ await this.init();
+ return tokenData;
}
allowInternalAccess = allowInternal;
return allowInternal || null;
@@ -238,6 +249,7 @@ export default function (tokenString) {
permission_streams: permissions.streams,
permission_access_lists: permissions.access_lists,
permission_certificates: permissions.certificates,
+ permission_meta: permissions.meta,
},
};
diff --git a/backend/lib/express/rate-limit.js b/backend/lib/express/rate-limit.js
new file mode 100644
index 0000000000..a8610b0d87
--- /dev/null
+++ b/backend/lib/express/rate-limit.js
@@ -0,0 +1,94 @@
+import { isCI } from "../config.js";
+
+/**
+ * In-memory sliding window rate limiter for authentication endpoints
+ */
+
+const ipRequests = new Map();
+
+// Clean up old entries every 2 minutes
+const cleanupTimer = setInterval(() => {
+ const now = Date.now();
+ for (const [ip, record] of ipRequests.entries()) {
+ if (now - record.startTime > record.windowMs * 2) {
+ ipRequests.delete(ip);
+ }
+ }
+}, 120000);
+cleanupTimer.unref();
+
+export default function createRateLimiter(options = {}) {
+ const windowMs = options.windowMs || 60 * 1000; // 1 minute default
+ const max = options.max || 10; // 10 requests default
+ const message = options.message || "Too many attempts from this IP, please try again later.";
+ const skipSuccessfulRequests = options.skipSuccessfulRequests !== false;
+
+ return (req, res, next) => {
+ // Bypass rate limiting in CI, test environments, or when explicitly disabled
+ if (
+ isCI() ||
+ process.env.CI === "true" ||
+ process.env.NODE_ENV === "test" ||
+ process.env.DISABLE_RATE_LIMIT === "true"
+ ) {
+ return next();
+ }
+
+ const clientIp =
+ req.headers["x-forwarded-for"]?.split(",")[0].trim() ||
+ req.socket?.remoteAddress ||
+ req.ip ||
+ "unknown";
+
+ const now = Date.now();
+ let record = ipRequests.get(clientIp);
+
+ if (!record || now - record.startTime > windowMs) {
+ record = {
+ count: 1,
+ startTime: now,
+ windowMs,
+ };
+ ipRequests.set(clientIp, record);
+
+ if (skipSuccessfulRequests) {
+ res.on("finish", () => {
+ if (res.statusCode < 400) {
+ const cur = ipRequests.get(clientIp);
+ if (cur && cur.count > 0) {
+ cur.count--;
+ }
+ }
+ });
+ }
+
+ return next();
+ }
+
+ record.count++;
+
+ if (skipSuccessfulRequests) {
+ res.on("finish", () => {
+ if (res.statusCode < 400) {
+ const cur = ipRequests.get(clientIp);
+ if (cur && cur.count > 0) {
+ cur.count--;
+ }
+ }
+ });
+ }
+
+ if (record.count > max) {
+ res.setHeader("Retry-After", Math.ceil((record.startTime + windowMs - now) / 1000));
+ return res.status(429).json({
+ error: {
+ code: 429,
+ message,
+ },
+ });
+ }
+
+ next();
+ };
+}
+
diff --git a/backend/migrations/20260906220000_user_permission_meta.js b/backend/migrations/20260906220000_user_permission_meta.js
new file mode 100644
index 0000000000..1573c77d8d
--- /dev/null
+++ b/backend/migrations/20260906220000_user_permission_meta.js
@@ -0,0 +1,41 @@
+import { migrate as logger } from "../logger.js";
+
+const migrateName = "user_permission_meta";
+
+/**
+ * Migrate
+ *
+ * @param {Object} knex
+ * @returns {Promise}
+ */
+const up = (knex) => {
+ logger.info(`[${migrateName}] Migrating Up...`);
+
+ return knex.schema
+ .alterTable("user_permission", (table) => {
+ table.json("meta").nullable();
+ })
+ .then(() => {
+ logger.info(`[${migrateName}] user_permission Table altered`);
+ });
+};
+
+/**
+ * Undo Migrate
+ *
+ * @param {Object} knex
+ * @returns {Promise}
+ */
+const down = (knex) => {
+ logger.info(`[${migrateName}] Migrating Down...`);
+
+ return knex.schema
+ .alterTable("user_permission", (table) => {
+ table.dropColumn("meta");
+ })
+ .then(() => {
+ logger.info(`[${migrateName}] user_permission Table altered`);
+ });
+};
+
+export { up, down };
diff --git a/backend/models/user_permission.js b/backend/models/user_permission.js
index cc6eac2cc7..0ad444c9c2 100644
--- a/backend/models/user_permission.js
+++ b/backend/models/user_permission.js
@@ -24,6 +24,10 @@ class UserPermission extends Model {
static get tableName() {
return "user_permission";
}
+
+ static get jsonAttributes() {
+ return ["meta"];
+ }
}
export default UserPermission;
diff --git a/backend/package.json b/backend/package.json
index 4400076079..c275f14d25 100644
--- a/backend/package.json
+++ b/backend/package.json
@@ -49,5 +49,11 @@
"signale": {
"displayDate": true,
"displayTimestamp": true
+ },
+ "resolutions": {
+ "qs": "^6.16.0"
+ },
+ "overrides": {
+ "qs": "^6.16.0"
}
}
diff --git a/backend/routes/tokens.js b/backend/routes/tokens.js
index 8a6a1bc0fb..b5f25890cd 100644
--- a/backend/routes/tokens.js
+++ b/backend/routes/tokens.js
@@ -1,10 +1,18 @@
import express from "express";
import internalToken from "../internal/token.js";
import jwtdecode from "../lib/express/jwt-decode.js";
+import createRateLimiter from "../lib/express/rate-limit.js";
import apiValidator from "../lib/validator/api.js";
import { debug, express as logger } from "../logger.js";
import { getValidationSchema } from "../schema/index.js";
+const tokenRateLimiter = createRateLimiter({
+ windowMs: 60 * 1000,
+ max: 10,
+ message: "Too many login attempts. Please try again in 1 minute.",
+ skipSuccessfulRequests: true,
+});
+
const router = express.Router({
caseSensitive: true,
strict: true,
@@ -42,7 +50,7 @@ router
*
* Create a new Token
*/
- .post(async (req, res, next) => {
+ .post(tokenRateLimiter, async (req, res, next) => {
try {
const data = await apiValidator(getValidationSchema("/tokens", "post"), req.body);
const result = await internalToken.getTokenFromEmail(data);
@@ -64,7 +72,7 @@ router
*
* Verify 2FA code and get full token
*/
- .post(async (req, res, next) => {
+ .post(tokenRateLimiter, async (req, res, next) => {
try {
const { challenge_token, code } = await apiValidator(getValidationSchema("/tokens/2fa", "post"), req.body);
const result = await internalToken.verify2FA(challenge_token, code);
diff --git a/backend/routes/users.js b/backend/routes/users.js
index 3f972223be..2cdb4101cd 100644
--- a/backend/routes/users.js
+++ b/backend/routes/users.js
@@ -260,7 +260,7 @@ router
.put(async (req, res, next) => {
try {
const payload = await apiValidator(getValidationSchema("/users/{userID}/permissions", "put"), req.body);
- payload.id = req.params.user_id;
+ payload.id = Number.parseInt(req.params.user_id, 10);
const result = await internalUser.setPermissions(res.locals.access, payload);
res.status(200).send(result);
} catch (err) {
diff --git a/backend/schema/components/permission-object.json b/backend/schema/components/permission-object.json
index cae9d26c02..74520d4b2c 100644
--- a/backend/schema/components/permission-object.json
+++ b/backend/schema/components/permission-object.json
@@ -43,6 +43,18 @@
"description": "Certificates Permissions",
"enum": ["hidden", "view", "manage"],
"example": "hidden"
+ },
+ "meta": {
+ "type": "object",
+ "description": "Permission metadata",
+ "properties": {
+ "proxy_host_ids": {
+ "type": "array",
+ "items": {
+ "type": "integer"
+ }
+ }
+ }
}
}
}
diff --git a/backend/yarn.lock b/backend/yarn.lock
index 61d5687f7e..c0671f5e84 100644
--- a/backend/yarn.lock
+++ b/backend/yarn.lock
@@ -1911,10 +1911,10 @@ pump@^3.0.0:
end-of-stream "^1.1.0"
once "^1.3.1"
-qs@^6.14.0, qs@^6.15.2:
- version "6.15.3"
- resolved "https://registry.yarnpkg.com/qs/-/qs-6.15.3.tgz#76852132a58ed5c7c0ef67e4441b9bb5d6061b3b"
- integrity sha512-O9gl3zCl5h5blw1KGUzQKhA5oUXSl8rwUIM5o0S3nCXMliSvy5Dzx7/DJcI+SwgICv+IneSZwhBh1oSyEHA71A==
+qs@^6.14.0, qs@^6.15.2, qs@^6.16.0:
+ version "6.16.0"
+ resolved "https://registry.yarnpkg.com/qs/-/qs-6.16.0.tgz#c22c723a28a920f3aacdce8289fabd43eccb79fd"
+ integrity sha512-h6fhOIaRrID2CbEY2fqs+7t+UXZo+MLAnU5gRIq85uFtdiUPCdsApMlHhXogKVM4HM2DVbIjGNTTYH2OcmP1vA==
dependencies:
es-define-property "^1.0.1"
side-channel "^1.1.1"
diff --git a/docker-compose.yml b/docker-compose.yml
new file mode 100644
index 0000000000..f54a8f9e27
--- /dev/null
+++ b/docker-compose.yml
@@ -0,0 +1,36 @@
+services:
+ app:
+ image: 'jc21/nginx-proxy-manager:latest'
+ container_name: nginx-proxy-manager
+ restart: unless-stopped
+ ports:
+ # Public HTTP Port:
+ - '80:80'
+ # Admin Web Port:
+ - '81:81'
+ # Public HTTPS Port:
+ - '443:443'
+ environment:
+ # Default admin user credentials:
+ INITIAL_ADMIN_EMAIL: "admin@example.com"
+ INITIAL_ADMIN_PASSWORD: "changeme"
+ PUID: 1000
+ PGID: 1000
+ # Uncomment if IPv6 is not supported on your host:
+ # DISABLE_IPV6: 'true'
+ volumes:
+ - ./data:/data
+ - ./letsencrypt:/etc/letsencrypt
+ - ./frontend/dist:/app/frontend:ro
+ - ./backend/internal/user.js:/app/internal/user.js:ro
+ - ./backend/routes/users.js:/app/routes/users.js:ro
+ - ./backend/routes/tokens.js:/app/routes/tokens.js:ro
+ - ./backend/lib/express/rate-limit.js:/app/lib/express/rate-limit.js:ro
+ - ./backend/lib/access.js:/app/lib/access.js:ro
+ - ./backend/internal/proxy-host.js:/app/internal/proxy-host.js:ro
+ - ./backend/internal/dead-host.js:/app/internal/dead-host.js:ro
+ - ./backend/internal/redirection-host.js:/app/internal/redirection-host.js:ro
+ - ./backend/models/user_permission.js:/app/models/user_permission.js:ro
+ - ./backend/schema/components/permission-object.json:/app/schema/components/permission-object.json:ro
+ - ./backend/migrations:/app/migrations:ro
+ - ./backend/node_modules/qs:/app/node_modules/qs:ro
diff --git a/frontend/check-locales.cjs b/frontend/check-locales.cjs
index 1ae7546415..e827546c69 100755
--- a/frontend/check-locales.cjs
+++ b/frontend/check-locales.cjs
@@ -30,6 +30,7 @@ const allLocales = [
["hu", "hu-HU"],
["no", "no-NO"],
["uk", "uk-UA"],
+ ["uz", "uz-UZ"],
];
const ignoreUnused = [/^.*$/];
diff --git a/frontend/package.json b/frontend/package.json
index e7c7363c3e..04534a8598 100644
--- a/frontend/package.json
+++ b/frontend/package.json
@@ -12,7 +12,7 @@
"prettier": "biome format --write ./src",
"locale-extract": "formatjs extract 'src/**/*.tsx'",
"locale-compile": "formatjs compile-folder src/locale/src src/locale/lang",
- "locale-sort": "./src/locale/scripts/locale-sort.sh",
+ "locale-sort": "node ./src/locale/scripts/locale-sort.cjs",
"test": "vitest"
},
"dependencies": {
diff --git a/frontend/src/api/backend/models.ts b/frontend/src/api/backend/models.ts
index 2ae0b08348..1e6d176b35 100644
--- a/frontend/src/api/backend/models.ts
+++ b/frontend/src/api/backend/models.ts
@@ -16,6 +16,11 @@ export interface UserPermissions {
streams: string;
accessLists: string;
certificates: string;
+ meta?: {
+ proxyHostIds?: number[];
+ proxy_host_ids?: number[];
+ [key: string]: any;
+ };
}
export interface User {
diff --git a/frontend/src/components/Form/LocationsFields.tsx b/frontend/src/components/Form/LocationsFields.tsx
index 23198c10c0..8a59fa08a3 100644
--- a/frontend/src/components/Form/LocationsFields.tsx
+++ b/frontend/src/components/Form/LocationsFields.tsx
@@ -233,10 +233,10 @@ export function LocationsFields({ initialValues, name = "locations" }: Props) {
-
+
-
+
-
+
@@ -141,9 +151,11 @@ const DeadHostModal = EasyModal.create(({ id, visible, remove }: Props) => {
/>
-
-
-
+ {isAdmin && (
+
+
+
+ )}
diff --git a/frontend/src/modals/PermissionsModal.tsx b/frontend/src/modals/PermissionsModal.tsx
index d363de9ede..a3e2e152de 100644
--- a/frontend/src/modals/PermissionsModal.tsx
+++ b/frontend/src/modals/PermissionsModal.tsx
@@ -7,7 +7,7 @@ import { Alert } from "react-bootstrap";
import Modal from "react-bootstrap/Modal";
import { setPermissions } from "src/api/backend";
import { Button, Loading } from "src/components";
-import { useUser } from "src/hooks";
+import { useProxyHosts, useUser } from "src/hooks";
import { T } from "src/locale";
import styles from "./PermissionsModal.module.css";
@@ -22,6 +22,7 @@ const PermissionsModal = EasyModal.create(({ id, visible, remove }: Props) => {
const queryClient = useQueryClient();
const [errorMsg, setErrorMsg] = useState
(null);
const { data, isLoading, error } = useUser(id);
+ const { data: proxyHosts } = useProxyHosts();
const [isSubmitting, setIsSubmitting] = useState(false);
const onSubmit = async (values: any, { setSubmitting }: any) => {
@@ -144,18 +145,24 @@ const PermissionsModal = EasyModal.create(({ id, visible, remove }: Props) => {
- {() => (
+ {({ values, setFieldValue }: any) => (
- {!isAdmin && (
+ {isAdmin ? (
+
+ :
+
+ ) : (
<>
+ {values.visibility === "user" && values.proxyHosts !== "hidden" && (
+
+
+
+
+
+
+ {proxyHosts && proxyHosts.length > 0 ? (
+ proxyHosts.map((host) => {
+ const selectedIds: number[] = values.meta?.proxy_host_ids || [];
+ const isChecked = selectedIds.includes(host.id);
+ return (
+
+ {
+ const newIds = e.target.checked
+ ? [...selectedIds, host.id]
+ : selectedIds.filter((hId) => hId !== host.id);
+ setFieldValue("meta.proxy_host_ids", newIds);
+ }}
+ />
+
+
+ );
+ })
+ ) : (
+
+
+
+ )}
+
+
+ )}
@@ -165,12 +174,12 @@ const ProxyHostModal = EasyModal.create(({ id, visible, remove }: Props) => {
-
+
{({ field, form }: any) => (
-
+
diff --git a/frontend/src/modals/RedirectionHostModal.tsx b/frontend/src/modals/RedirectionHostModal.tsx
index 9a0d95ce89..e7d3c430af 100644
--- a/frontend/src/modals/RedirectionHostModal.tsx
+++ b/frontend/src/modals/RedirectionHostModal.tsx
@@ -13,7 +13,7 @@ import {
SSLCertificateField,
SSLOptionsFields,
} from "src/components";
-import { useRedirectionHost, useSetRedirectionHost } from "src/hooks";
+import { useRedirectionHost, useSetRedirectionHost, useUser } from "src/hooks";
import { T } from "src/locale";
import { validateString } from "src/modules/Validations";
import { showObjectSuccess } from "src/notifications";
@@ -26,11 +26,14 @@ interface Props extends InnerModalProps {
id: number | "new";
}
const RedirectionHostModal = EasyModal.create(({ id, visible, remove }: Props) => {
+ const { data: currentUser } = useUser("me");
const { data, isLoading, error } = useRedirectionHost(id);
const { mutate: setRedirectionHost } = useSetRedirectionHost();
const [errorMsg, setErrorMsg] = useState
(null);
const [isSubmitting, setIsSubmitting] = useState(false);
+ const isAdmin = currentUser?.roles?.includes("admin");
+
const onSubmit = async (values: any, { setSubmitting }: any) => {
if (isSubmitting) return;
setIsSubmitting(true);
@@ -41,6 +44,11 @@ const RedirectionHostModal = EasyModal.create(({ id, visible, remove }: Props) =
...values,
};
+ if (!isAdmin) {
+ delete payload.advanced_config;
+ delete payload.advancedConfig;
+ }
+
setRedirectionHost(payload, {
onError: (err: any) => setErrorMsg(),
onSuccess: () => {
@@ -126,19 +134,21 @@ const RedirectionHostModal = EasyModal.create(({ id, visible, remove }: Props) =
-
-
-
-
-
+ {isAdmin && (
+
+
+
+
+
+ )}
@@ -318,9 +328,11 @@ const RedirectionHostModal = EasyModal.create(({ id, visible, remove }: Props) =
/>
-
-
-
+ {isAdmin && (
+
+
+
+ )}
diff --git a/frontend/src/pages/Users/Table.tsx b/frontend/src/pages/Users/Table.tsx
index a5a61057bd..1405071148 100644
--- a/frontend/src/pages/Users/Table.tsx
+++ b/frontend/src/pages/Users/Table.tsx
@@ -90,6 +90,48 @@ export default function Table({
return
;
},
}),
+ columnHelper.accessor((row: any) => row.permissions, {
+ id: "permissions",
+ header: intl.formatMessage({ id: "action.permissions" }),
+ cell: (info: any) => {
+ const permissions = info.getValue();
+ const isAdminUser = info.row.original.roles?.includes("admin");
+ if (isAdminUser) {
+ return (
+
+
+
+ );
+ }
+ if (!permissions) {
+ return
-;
+ }
+ const items = [
+ { key: "proxyHosts", label: "proxy-hosts", val: permissions.proxyHosts },
+ { key: "redirectionHosts", label: "redirection-hosts", val: permissions.redirectionHosts },
+ { key: "streams", label: "streams", val: permissions.streams },
+ { key: "accessLists", label: "access-lists", val: permissions.accessLists },
+ { key: "certificates", label: "certificates", val: permissions.certificates },
+ ];
+ return (
+
+ {items.map((it) => {
+ if (it.val === "hidden") return null;
+ const isManage = it.val === "manage";
+ return (
+
+ : {it.val}
+
+ );
+ })}
+
+ );
+ },
+ }),
columnHelper.accessor((row: any) => row.isDisabled, {
id: "isDisabled",
header: intl.formatMessage({ id: "column.status" }),