Compare commits
5 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
01330e0f58 | ||
|
|
e9ac1a1a23 | ||
|
|
b53802a5c5 | ||
|
|
9addc18956 | ||
|
|
9701e6503b |
12
.github/pull_request_template.md
vendored
Normal file
12
.github/pull_request_template.md
vendored
Normal file
@@ -0,0 +1,12 @@
|
||||
## Issue ticket number and link
|
||||
|
||||
## Documentation
|
||||
Select exactly one:
|
||||
|
||||
- [ ] I added/updated documentation for this change
|
||||
- [ ] Documentation is **not needed** for this change (explain why)
|
||||
|
||||
### Docs PR URL (required if "docs added" is checked)
|
||||
Paste the PR link from https://github.com/netbirdio/docs here:
|
||||
|
||||
https://github.com/netbirdio/docs/pull/__
|
||||
105
.github/workflows/docs-ack.yml
vendored
Normal file
105
.github/workflows/docs-ack.yml
vendored
Normal file
@@ -0,0 +1,105 @@
|
||||
name: Docs Acknowledgement
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
types: [opened, edited, synchronize]
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
pull-requests: read
|
||||
|
||||
jobs:
|
||||
docs-ack:
|
||||
name: Require docs PR URL or explicit "not needed"
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Read PR body
|
||||
id: body
|
||||
shell: bash
|
||||
run: |
|
||||
set -euo pipefail
|
||||
BODY_B64=$(jq -r '.pull_request.body // "" | @base64' "$GITHUB_EVENT_PATH")
|
||||
{
|
||||
echo "body_b64=$BODY_B64"
|
||||
} >> "$GITHUB_OUTPUT"
|
||||
|
||||
- name: Validate checkbox selection
|
||||
id: validate
|
||||
shell: bash
|
||||
env:
|
||||
BODY_B64: ${{ steps.body.outputs.body_b64 }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
if ! body="$(printf '%s' "$BODY_B64" | base64 -d)"; then
|
||||
echo "::error::Failed to decode PR body from base64. Data may be corrupted or missing."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
added_checked=$(printf '%s' "$body" | grep -Ei '^[[:space:]]*-\s*\[x\]\s*I added/updated documentation' | wc -l | tr -d '[:space:]' || true)
|
||||
noneed_checked=$(printf '%s' "$body" | grep -Ei '^[[:space:]]*-\s*\[x\]\s*Documentation is \*\*not needed\*\*' | wc -l | tr -d '[:space:]' || true)
|
||||
|
||||
total=$((added_checked + noneed_checked))
|
||||
if [ "$total" -ne 1 ]; then
|
||||
echo "::error::You must check exactly one docs option in the PR template (either 'docs added' OR 'not needed')."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [ "$added_checked" -eq 1 ]; then
|
||||
echo "mode=added" >> "$GITHUB_OUTPUT"
|
||||
else
|
||||
echo "mode=noneed" >> "$GITHUB_OUTPUT"
|
||||
fi
|
||||
|
||||
- name: Extract docs PR URL (when 'docs added')
|
||||
if: steps.validate.outputs.mode == 'added'
|
||||
id: extract
|
||||
shell: bash
|
||||
env:
|
||||
BODY_B64: ${{ steps.body.outputs.body_b64 }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
body="$(printf '%s' "$BODY_B64" | base64 -d)"
|
||||
|
||||
# Strictly require HTTPS and that it's a PR in netbirdio/docs
|
||||
# e.g., https://github.com/netbirdio/docs/pull/1234
|
||||
url="$(printf '%s' "$body" | grep -Eo 'https://github\.com/netbirdio/docs/pull/[0-9]+' | head -n1 || true)"
|
||||
|
||||
if [ -z "${url:-}" ]; then
|
||||
echo "::error::You checked 'docs added' but didn't include a valid HTTPS PR link to netbirdio/docs (e.g., https://github.com/netbirdio/docs/pull/1234)."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
pr_number="$(printf '%s' "$url" | sed -E 's#.*/pull/([0-9]+)$#\1#')"
|
||||
{
|
||||
echo "url=$url"
|
||||
echo "pr_number=$pr_number"
|
||||
} >> "$GITHUB_OUTPUT"
|
||||
|
||||
- name: Verify docs PR exists (and is open or merged)
|
||||
if: steps.validate.outputs.mode == 'added'
|
||||
uses: actions/github-script@v7
|
||||
id: verify
|
||||
env:
|
||||
PR_NUMBER: ${{ steps.extract.outputs.pr_number }}
|
||||
with:
|
||||
script: |
|
||||
const prNumber = parseInt(process.env.PR_NUMBER, 10);
|
||||
const { data } = await github.rest.pulls.get({
|
||||
owner: 'netbirdio',
|
||||
repo: 'docs',
|
||||
pull_number: prNumber
|
||||
});
|
||||
|
||||
// Allow open or merged PRs
|
||||
const ok = data.state === 'open' || data.merged === true;
|
||||
core.setOutput('state', data.state);
|
||||
core.setOutput('merged', String(!!data.merged));
|
||||
if (!ok) {
|
||||
core.setFailed(`Docs PR #${prNumber} exists but is neither open nor merged (state=${data.state}, merged=${data.merged}).`);
|
||||
}
|
||||
result-encoding: string
|
||||
github-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: All good
|
||||
run: echo "Documentation requirement satisfied ✅"
|
||||
90
package-lock.json
generated
90
package-lock.json
generated
@@ -60,7 +60,7 @@
|
||||
"js-cookie": "^3.0.5",
|
||||
"lodash": "^4.17.23",
|
||||
"lucide-react": "^0.566.0",
|
||||
"next": "^16.1.7",
|
||||
"next": "16.1.7",
|
||||
"next-themes": "^0.2.1",
|
||||
"punycode": "^2.3.1",
|
||||
"react": "^19.2.4",
|
||||
@@ -3244,32 +3244,6 @@
|
||||
"typescript": ">=4.8.4 <6.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/typescript-estree/node_modules/brace-expansion": {
|
||||
"version": "2.0.2",
|
||||
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz",
|
||||
"integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"balanced-match": "^1.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/typescript-estree/node_modules/minimatch": {
|
||||
"version": "9.0.9",
|
||||
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.9.tgz",
|
||||
"integrity": "sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg==",
|
||||
"dev": true,
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"brace-expansion": "^2.0.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=16 || 14 >=14.17"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/isaacs"
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/typescript-estree/node_modules/semver": {
|
||||
"version": "7.7.3",
|
||||
"resolved": "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz",
|
||||
@@ -3998,10 +3972,13 @@
|
||||
}
|
||||
},
|
||||
"node_modules/balanced-match": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
|
||||
"integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==",
|
||||
"license": "MIT"
|
||||
"version": "4.0.4",
|
||||
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz",
|
||||
"integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": "18 || 20 || >=22"
|
||||
}
|
||||
},
|
||||
"node_modules/baseline-browser-mapping": {
|
||||
"version": "2.9.19",
|
||||
@@ -4025,13 +4002,15 @@
|
||||
}
|
||||
},
|
||||
"node_modules/brace-expansion": {
|
||||
"version": "1.1.12",
|
||||
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz",
|
||||
"integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==",
|
||||
"version": "5.0.5",
|
||||
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.5.tgz",
|
||||
"integrity": "sha512-VZznLgtwhn+Mact9tfiwx64fA9erHH/MCXEUfB/0bX/6Fz6ny5EGTXYltMocqg4xFAQZtnO3DHWWXi8RiuN7cQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"balanced-match": "^1.0.0",
|
||||
"concat-map": "0.0.1"
|
||||
"balanced-match": "^4.0.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": "18 || 20 || >=22"
|
||||
}
|
||||
},
|
||||
"node_modules/braces": {
|
||||
@@ -4320,12 +4299,6 @@
|
||||
"node": ">= 10"
|
||||
}
|
||||
},
|
||||
"node_modules/concat-map": {
|
||||
"version": "0.0.1",
|
||||
"resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
|
||||
"integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/convert-source-map": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz",
|
||||
@@ -6889,9 +6862,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/lodash": {
|
||||
"version": "4.17.23",
|
||||
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.23.tgz",
|
||||
"integrity": "sha512-LgVTMpQtIopCi79SJeDiP0TfWi5CNEc/L/aRdTh3yIvmZXTnheWpKjSZhnvMl8iXbC1tFg9gdHHDMLoV7CnG+w==",
|
||||
"version": "4.18.1",
|
||||
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.18.1.tgz",
|
||||
"integrity": "sha512-dMInicTPVE8d1e5otfwmmjlxkZoUpiVLwyeTdUsi/Caj/gfzzblBcCE5sRHV/AsjuCmxWrte2TNGSYuCeCq+0Q==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/lodash.merge": {
|
||||
@@ -6973,15 +6946,18 @@
|
||||
}
|
||||
},
|
||||
"node_modules/minimatch": {
|
||||
"version": "3.1.5",
|
||||
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz",
|
||||
"integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==",
|
||||
"license": "ISC",
|
||||
"version": "10.2.5",
|
||||
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.5.tgz",
|
||||
"integrity": "sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg==",
|
||||
"license": "BlueOak-1.0.0",
|
||||
"dependencies": {
|
||||
"brace-expansion": "^1.1.7"
|
||||
"brace-expansion": "^5.0.5"
|
||||
},
|
||||
"engines": {
|
||||
"node": "*"
|
||||
"node": "18 || 20 || >=22"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/isaacs"
|
||||
}
|
||||
},
|
||||
"node_modules/minimist": {
|
||||
@@ -7412,9 +7388,9 @@
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/picomatch": {
|
||||
"version": "2.3.1",
|
||||
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz",
|
||||
"integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==",
|
||||
"version": "2.3.2",
|
||||
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.2.tgz",
|
||||
"integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=8.6"
|
||||
@@ -8777,9 +8753,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/tinyglobby/node_modules/picomatch": {
|
||||
"version": "4.0.3",
|
||||
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz",
|
||||
"integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
|
||||
"version": "4.0.4",
|
||||
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz",
|
||||
"integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"engines": {
|
||||
|
||||
@@ -68,7 +68,7 @@
|
||||
"js-cookie": "^3.0.5",
|
||||
"lodash": "^4.17.23",
|
||||
"lucide-react": "^0.566.0",
|
||||
"next": "^16.1.7",
|
||||
"next": "16.1.7",
|
||||
"next-themes": "^0.2.1",
|
||||
"punycode": "^2.3.1",
|
||||
"react": "^19.2.4",
|
||||
@@ -89,6 +89,9 @@
|
||||
"timescape": "^0.7.1",
|
||||
"typescript": "^5"
|
||||
},
|
||||
"overrides": {
|
||||
"minimatch": ">=10.2.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@faker-js/faker": "^9.5.1",
|
||||
"@types/chroma-js": "^3.1.1",
|
||||
|
||||
BIN
src/assets/integrations/crowdsec.png
Normal file
BIN
src/assets/integrations/crowdsec.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 18 KiB |
@@ -23,6 +23,7 @@ const variants = cva("", {
|
||||
purple: ["bg-purple-950/50 border-purple-500 border text-purple-500"],
|
||||
yellow: ["bg-yellow-950 border-yellow-500 border text-yellow-400"],
|
||||
gray: ["bg-nb-gray-930/60 border-nb-gray-800/40 text-nb-gray-300 border"],
|
||||
lightGray: ["bg-nb-gray-910 text-nb-gray-200 border border-nb-gray-900"],
|
||||
grayer: [
|
||||
"bg-nb-gray-900/40 border-nb-gray-800/40 text-nb-gray-300 border",
|
||||
],
|
||||
@@ -45,6 +46,7 @@ const variants = cva("", {
|
||||
"blue-darker": ["hover:bg-sky-800"],
|
||||
red: ["hover:bg-red-950/40"],
|
||||
gray: ["hover:bg-nb-gray-900"],
|
||||
lightGray: ["hover:bg-nb-gray-900"],
|
||||
grayer: ["hover:bg-nb-gray-900"],
|
||||
"gray-ghost": ["hover:bg-nb-gray-800 cursor-pointer"],
|
||||
green: ["hover:bg-green-950/50"],
|
||||
|
||||
@@ -8,7 +8,7 @@ import React from "react";
|
||||
export const fancyToggleSwitchVariants = cva([], {
|
||||
variants: {
|
||||
variant: {
|
||||
default: ["px-6 py-4 border rounded-md"],
|
||||
default: ["px-5 py-4 border rounded-md"],
|
||||
blank: null,
|
||||
},
|
||||
state: {
|
||||
@@ -45,6 +45,8 @@ interface Props extends FancyToggleSwitchVariants {
|
||||
disabled?: boolean;
|
||||
dataCy?: string;
|
||||
className?: string;
|
||||
labelClassName?: string;
|
||||
textWrapperClassName?: string;
|
||||
}
|
||||
|
||||
export default function FancyToggleSwitch({
|
||||
@@ -57,6 +59,8 @@ export default function FancyToggleSwitch({
|
||||
dataCy,
|
||||
className,
|
||||
variant = "default",
|
||||
labelClassName,
|
||||
textWrapperClassName = "max-w-sm",
|
||||
}: Readonly<Props>) {
|
||||
const handleToggle = () => {
|
||||
if (disabled) return;
|
||||
@@ -87,8 +91,8 @@ export default function FancyToggleSwitch({
|
||||
)}
|
||||
>
|
||||
<div className={"flex justify-between gap-10"}>
|
||||
<div className={"max-w-sm"}>
|
||||
<Label>{label}</Label>
|
||||
<div className={cn(textWrapperClassName)}>
|
||||
<Label className={labelClassName}>{label}</Label>
|
||||
<HelpText margin={false}>{helpText}</HelpText>
|
||||
</div>
|
||||
<div className={"mt-2 pr-1"}>
|
||||
|
||||
17
src/hooks/useEmbeddedIdentityProviders.ts
Normal file
17
src/hooks/useEmbeddedIdentityProviders.ts
Normal file
@@ -0,0 +1,17 @@
|
||||
import { useAccount } from "@/modules/account/useAccount";
|
||||
import { SSOIdentityProvider } from "@/interfaces/IdentityProvider";
|
||||
import useFetchApi from "@utils/api";
|
||||
|
||||
export function useEmbeddedIdentityProviders() {
|
||||
const account = useAccount();
|
||||
const isEmbeddedIdPEnabled = !!account?.settings?.embedded_idp_enabled;
|
||||
|
||||
const { data: providers } = useFetchApi<SSOIdentityProvider[]>(
|
||||
"/identity-providers",
|
||||
true,
|
||||
true,
|
||||
isEmbeddedIdPEnabled,
|
||||
);
|
||||
|
||||
return { providers, isEmbeddedIdPEnabled };
|
||||
}
|
||||
@@ -1,37 +1,33 @@
|
||||
import { useSearchParams } from "next/navigation";
|
||||
import { useCallback, useEffect, useState } from "react";
|
||||
import { useRouter, useSearchParams } from "next/navigation";
|
||||
import { useCallback, useMemo } from "react";
|
||||
|
||||
export default function useUrlTab(
|
||||
validTabs: string[],
|
||||
defaultTab: string,
|
||||
paramName: string = "tab",
|
||||
): [string, (value: string) => void] {
|
||||
const searchParams = useSearchParams();
|
||||
const router = useRouter();
|
||||
|
||||
const getTab = useCallback(
|
||||
(params: URLSearchParams) => {
|
||||
const tabParam = params.get("tab");
|
||||
const tabParam = params.get(paramName);
|
||||
if (tabParam && validTabs.includes(tabParam)) return tabParam;
|
||||
return defaultTab;
|
||||
},
|
||||
[validTabs, defaultTab],
|
||||
[validTabs, defaultTab, paramName],
|
||||
);
|
||||
|
||||
const [tab, setTabState] = useState(() => getTab(searchParams));
|
||||
|
||||
useEffect(() => {
|
||||
const newTab = getTab(searchParams);
|
||||
setTabState(newTab);
|
||||
}, [searchParams, getTab]);
|
||||
const tab = useMemo(() => getTab(searchParams), [searchParams, getTab]);
|
||||
|
||||
const setTab = useCallback(
|
||||
(value: string) => {
|
||||
const nextTab = validTabs.includes(value) ? value : defaultTab;
|
||||
setTabState(nextTab);
|
||||
const params = new URLSearchParams(window.location.search);
|
||||
params.set("tab", nextTab);
|
||||
window.history.replaceState(null, "", `?${params.toString()}`);
|
||||
const params = new URLSearchParams(searchParams.toString());
|
||||
params.set(paramName, nextTab);
|
||||
router.replace(`?${params.toString()}`, { scroll: false });
|
||||
},
|
||||
[validTabs, defaultTab],
|
||||
[searchParams, router, validTabs, defaultTab, paramName],
|
||||
);
|
||||
|
||||
return [tab, setTab];
|
||||
|
||||
@@ -1,20 +1,22 @@
|
||||
export interface GoogleWorkspaceIntegration {
|
||||
id: string;
|
||||
customerId: string;
|
||||
syncInterval: number;
|
||||
customer_id: string;
|
||||
sync_interval: number;
|
||||
enabled: boolean;
|
||||
group_prefixes: string[];
|
||||
user_group_prefixes: string[];
|
||||
connector_id?: string;
|
||||
}
|
||||
|
||||
export interface AzureADIntegration {
|
||||
id: string;
|
||||
clientId: string;
|
||||
tenantId: string;
|
||||
syncInterval: number;
|
||||
client_id: string;
|
||||
tenant_id: string;
|
||||
sync_interval: number;
|
||||
enabled: boolean;
|
||||
group_prefixes: string[];
|
||||
user_group_prefixes: string[];
|
||||
connector_id?: string;
|
||||
}
|
||||
|
||||
export interface OktaIntegration {
|
||||
@@ -23,6 +25,8 @@ export interface OktaIntegration {
|
||||
group_prefixes: string[];
|
||||
user_group_prefixes: string[];
|
||||
auth_token: string;
|
||||
connection_name?: string;
|
||||
connector_id?: string;
|
||||
}
|
||||
|
||||
export interface IdentityProviderLog {
|
||||
|
||||
@@ -22,11 +22,20 @@ export interface ReverseProxy {
|
||||
meta?: ReverseProxyMeta;
|
||||
}
|
||||
|
||||
export const CrowdSecMode = {
|
||||
OFF: "off",
|
||||
ENFORCE: "enforce",
|
||||
OBSERVE: "observe",
|
||||
} as const;
|
||||
|
||||
export type CrowdSecMode = (typeof CrowdSecMode)[keyof typeof CrowdSecMode];
|
||||
|
||||
export interface AccessRestrictions {
|
||||
allowed_cidrs?: string[];
|
||||
blocked_cidrs?: string[];
|
||||
allowed_countries?: string[];
|
||||
blocked_countries?: string[];
|
||||
crowdsec_mode?: CrowdSecMode;
|
||||
}
|
||||
|
||||
export interface ReverseProxyMeta {
|
||||
@@ -102,6 +111,7 @@ export interface ReverseProxyDomain {
|
||||
target_cluster?: string;
|
||||
supports_custom_ports?: boolean;
|
||||
require_subdomain?: boolean;
|
||||
supports_crowdsec?: boolean;
|
||||
}
|
||||
|
||||
export enum ReverseProxyDomainType {
|
||||
@@ -149,6 +159,7 @@ export interface ReverseProxyEvent {
|
||||
bytes_upload: number;
|
||||
bytes_download: number;
|
||||
protocol?: EventProtocol;
|
||||
metadata?: Record<string, string>;
|
||||
}
|
||||
|
||||
export function isL4Event(event: ReverseProxyEvent): boolean {
|
||||
|
||||
@@ -821,6 +821,36 @@ export default function ActivityDescription({ event }: Props) {
|
||||
</div>
|
||||
);
|
||||
|
||||
/**
|
||||
* Reverse Proxy
|
||||
*/
|
||||
|
||||
if (event.activity_code == "service.create")
|
||||
return (
|
||||
<div className={"inline"}>
|
||||
Service <Value>{m.domain}</Value> in cluster{" "}
|
||||
<Value>{m.proxy_cluster}</Value> was created with authentication{" "}
|
||||
<Value>{m.auth ? "Enabled" : "Disabled"}</Value>
|
||||
</div>
|
||||
);
|
||||
|
||||
if (event.activity_code == "service.update")
|
||||
return (
|
||||
<div className={"inline"}>
|
||||
Service <Value>{m.domain}</Value> in cluster{" "}
|
||||
<Value>{m.proxy_cluster}</Value> was updated with authentication{" "}
|
||||
<Value>{m.auth === "true" ? "Enabled" : "Disabled"}</Value>
|
||||
</div>
|
||||
);
|
||||
|
||||
if (event.activity_code == "service.delete")
|
||||
return (
|
||||
<div className={"inline"}>
|
||||
Service <Value>{m.domain}</Value> in cluster{" "}
|
||||
<Value>{m.proxy_cluster}</Value> was deleted
|
||||
</div>
|
||||
);
|
||||
|
||||
return (
|
||||
<div className={"flex gap-2.5 items-center"}>
|
||||
<span className={"mb-[1px]"}>{event.activity}</span>
|
||||
|
||||
@@ -49,7 +49,9 @@ export function ActivityEventCodeSelector({
|
||||
return {
|
||||
activity_code: event.activity_code,
|
||||
activity: event.activity,
|
||||
group: event.activity_code.split(".")[0],
|
||||
group: event.activity_code.startsWith("service.user")
|
||||
? "Service User"
|
||||
: event.activity_code.split(".")[0],
|
||||
};
|
||||
});
|
||||
return items.reduce((acc, item) => {
|
||||
|
||||
@@ -11,7 +11,6 @@ import {
|
||||
KeyRound,
|
||||
Layers3Icon,
|
||||
LogIn,
|
||||
type LucideIcon,
|
||||
MonitorSmartphoneIcon,
|
||||
NetworkIcon,
|
||||
RefreshCcw,
|
||||
@@ -21,6 +20,7 @@ import {
|
||||
User,
|
||||
} from "lucide-react";
|
||||
import React from "react";
|
||||
import ReverseProxyIcon from "@/assets/icons/ReverseProxyIcon";
|
||||
|
||||
type Props = {
|
||||
code: string;
|
||||
@@ -46,7 +46,7 @@ const ActivityTypeMappings = {
|
||||
dashboard: LogIn,
|
||||
integration: Blocks,
|
||||
personal: User,
|
||||
service: Cog,
|
||||
"service.user": Cog,
|
||||
billing: CreditCardIcon,
|
||||
integrated: ShieldCheck,
|
||||
posture: ShieldCheck,
|
||||
@@ -54,21 +54,24 @@ const ActivityTypeMappings = {
|
||||
resource: Layers3Icon,
|
||||
network: NetworkIcon,
|
||||
identityprovider: FingerprintIcon,
|
||||
} as const satisfies Record<string, LucideIcon>;
|
||||
service: ReverseProxyIcon,
|
||||
} as const;
|
||||
|
||||
export default function ActivityTypeIcon({
|
||||
code,
|
||||
size = 18,
|
||||
className,
|
||||
}: Props) {
|
||||
const prefixParts = code?.split(".") || [];
|
||||
const prefix = (prefixParts[0] || "").toLowerCase();
|
||||
const parts = code?.split(".") || [];
|
||||
const twoPartKey = parts.slice(0, 2).join(".").toLowerCase();
|
||||
const onePartKey = (parts[0] || "").toLowerCase();
|
||||
|
||||
const key = (
|
||||
twoPartKey in ActivityTypeMappings ? twoPartKey : onePartKey
|
||||
) as ActivityTypeKey;
|
||||
|
||||
// Check if prefix is a valid key, otherwise use fallback
|
||||
const Icon =
|
||||
prefix in ActivityTypeMappings
|
||||
? ActivityTypeMappings[prefix as ActivityTypeKey]
|
||||
: HelpCircleIcon;
|
||||
key in ActivityTypeMappings ? ActivityTypeMappings[key] : HelpCircleIcon;
|
||||
|
||||
return <Icon size={size} className={cn(DEFAULT_CLASSES, className)} />;
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import React from "react";
|
||||
import { useGroupContext } from "@/contexts/GroupProvider";
|
||||
import PeersProvider from "@/contexts/PeersProvider";
|
||||
import { Route } from "@/interfaces/Route";
|
||||
import { GroupDetailsTableContainer } from "@/modules/groups/details/GroupDetailsTableContainer";
|
||||
import NetworkRoutesTable from "@/modules/route-group/NetworkRoutesTable";
|
||||
@@ -18,14 +19,16 @@ export const GroupNetworkRoutesSection = ({
|
||||
const { group } = useGroupContext();
|
||||
|
||||
return (
|
||||
<GroupDetailsTableContainer>
|
||||
<NetworkRoutesTable
|
||||
isGroupPage={true}
|
||||
isLoading={isLoading}
|
||||
groupedRoutes={groupedRoutes}
|
||||
routes={routes}
|
||||
distributionGroups={[group]}
|
||||
/>
|
||||
</GroupDetailsTableContainer>
|
||||
<PeersProvider>
|
||||
<GroupDetailsTableContainer>
|
||||
<NetworkRoutesTable
|
||||
isGroupPage={true}
|
||||
isLoading={isLoading}
|
||||
groupedRoutes={groupedRoutes}
|
||||
routes={routes}
|
||||
distributionGroups={[group]}
|
||||
/>
|
||||
</GroupDetailsTableContainer>
|
||||
</PeersProvider>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { useEffect, useMemo, useReducer, useRef } from "react";
|
||||
import { useEffect, useMemo, useReducer, useRef, useState } from "react";
|
||||
import { Label } from "@components/Label";
|
||||
import HelpText from "@components/HelpText";
|
||||
import Button from "@components/Button";
|
||||
@@ -18,7 +18,8 @@ import {
|
||||
SelectOption,
|
||||
} from "@components/select/SelectDropdown";
|
||||
import { CountrySelector } from "@/components/ui/CountrySelector";
|
||||
import { AccessRestrictions } from "@/interfaces/ReverseProxy";
|
||||
import { AccessRestrictions, CrowdSecMode } from "@/interfaces/ReverseProxy";
|
||||
import { ReverseProxyCrowdSecIPReputation } from "@/modules/reverse-proxy/ReverseProxyCrowdSecIPReputation";
|
||||
|
||||
type AccessAction = "allow" | "block";
|
||||
type AccessRuleType = "country" | "ip" | "cidr";
|
||||
@@ -93,30 +94,41 @@ function rulesReducer(state: AccessRule[], action: RulesAction): AccessRule[] {
|
||||
}
|
||||
}
|
||||
|
||||
function pushCidrRules(
|
||||
rules: AccessRule[],
|
||||
values: string[] | undefined,
|
||||
action: AccessAction,
|
||||
) {
|
||||
values?.forEach((v) => {
|
||||
const isIp = v.includes(":") ? v.endsWith("/128") : v.endsWith("/32");
|
||||
rules.push({
|
||||
id: nextId(),
|
||||
action,
|
||||
type: isIp ? "ip" : "cidr",
|
||||
value: isIp ? v.replace(/\/(32|128)$/, "") : v,
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function restrictionsToRules(
|
||||
restrictions: AccessRestrictions | undefined,
|
||||
): AccessRule[] {
|
||||
if (!restrictions) return [];
|
||||
const rules: AccessRule[] = [];
|
||||
restrictions.allowed_countries?.forEach((v) =>
|
||||
rules.push({ id: nextId(), action: "allow", type: "country", value: v }),
|
||||
);
|
||||
pushCidrRules(rules, restrictions.blocked_cidrs, "block");
|
||||
restrictions.blocked_countries?.forEach((v) =>
|
||||
rules.push({ id: nextId(), action: "block", type: "country", value: v }),
|
||||
);
|
||||
restrictions.allowed_cidrs?.forEach((v) => {
|
||||
const isIp = v.endsWith("/32");
|
||||
rules.push({ id: nextId(), action: "allow", type: isIp ? "ip" : "cidr", value: isIp ? v.replace(/\/32$/, "") : v });
|
||||
});
|
||||
restrictions.blocked_cidrs?.forEach((v) => {
|
||||
const isIp = v.endsWith("/32");
|
||||
rules.push({ id: nextId(), action: "block", type: isIp ? "ip" : "cidr", value: isIp ? v.replace(/\/32$/, "") : v });
|
||||
});
|
||||
pushCidrRules(rules, restrictions.allowed_cidrs, "allow");
|
||||
restrictions.allowed_countries?.forEach((v) =>
|
||||
rules.push({ id: nextId(), action: "allow", type: "country", value: v }),
|
||||
);
|
||||
return rules;
|
||||
}
|
||||
|
||||
function rulesToRestrictions(
|
||||
rules: AccessRule[],
|
||||
crowdsecMode?: CrowdSecMode,
|
||||
): AccessRestrictions | undefined {
|
||||
const allowed_countries: string[] = [];
|
||||
const blocked_countries: string[] = [];
|
||||
@@ -129,17 +141,23 @@ function rulesToRestrictions(
|
||||
if (rule.action === "allow") allowed_countries.push(rule.value);
|
||||
else blocked_countries.push(rule.value);
|
||||
} else {
|
||||
const value = rule.type === "ip" && !rule.value.includes("/") ? `${rule.value}/32` : rule.value;
|
||||
const suffix = rule.value.includes(":") ? "/128" : "/32";
|
||||
const value =
|
||||
rule.type === "ip" && !rule.value.includes("/")
|
||||
? `${rule.value}${suffix}`
|
||||
: rule.value;
|
||||
if (rule.action === "allow") allowed_cidrs.push(value);
|
||||
else blocked_cidrs.push(value);
|
||||
}
|
||||
}
|
||||
|
||||
const hasCrowdSec = crowdsecMode != null && crowdsecMode !== CrowdSecMode.OFF;
|
||||
const hasAny =
|
||||
allowed_countries.length > 0 ||
|
||||
blocked_countries.length > 0 ||
|
||||
allowed_cidrs.length > 0 ||
|
||||
blocked_cidrs.length > 0;
|
||||
blocked_cidrs.length > 0 ||
|
||||
hasCrowdSec;
|
||||
|
||||
if (!hasAny) return undefined;
|
||||
|
||||
@@ -148,6 +166,7 @@ function rulesToRestrictions(
|
||||
...(blocked_countries.length > 0 && { blocked_countries }),
|
||||
...(allowed_cidrs.length > 0 && { allowed_cidrs }),
|
||||
...(blocked_cidrs.length > 0 && { blocked_cidrs }),
|
||||
...(hasCrowdSec && { crowdsec_mode: crowdsecMode }),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -155,6 +174,7 @@ type Props = {
|
||||
value: AccessRestrictions | undefined;
|
||||
onChange: (value: AccessRestrictions | undefined) => void;
|
||||
onValidationChange?: (hasErrors: boolean) => void;
|
||||
supportsCrowdSec?: boolean;
|
||||
};
|
||||
|
||||
function validateRule(rule: AccessRule): string {
|
||||
@@ -172,13 +192,22 @@ function validateRule(rule: AccessRule): string {
|
||||
return "";
|
||||
}
|
||||
|
||||
export const ReverseProxyAccessControlRules = ({ value, onChange, onValidationChange }: Props) => {
|
||||
export const ReverseProxyAccessControlRules = ({
|
||||
value,
|
||||
onChange,
|
||||
onValidationChange,
|
||||
supportsCrowdSec,
|
||||
}: Props) => {
|
||||
const [rules, dispatch] = useReducer(
|
||||
rulesReducer,
|
||||
value,
|
||||
restrictionsToRules,
|
||||
);
|
||||
|
||||
const [crowdsecMode, setCrowdsecMode] = useState<CrowdSecMode>(
|
||||
value?.crowdsec_mode ?? CrowdSecMode.OFF,
|
||||
);
|
||||
|
||||
const errors = useMemo(
|
||||
() => Object.fromEntries(rules.map((r) => [r.id, validateRule(r)])),
|
||||
[rules],
|
||||
@@ -196,8 +225,14 @@ export const ReverseProxyAccessControlRules = ({ value, onChange, onValidationCh
|
||||
onValidationChangeRef.current = onValidationChange;
|
||||
|
||||
useEffect(() => {
|
||||
onChangeRef.current(rulesToRestrictions(rules));
|
||||
}, [rules]);
|
||||
if (!supportsCrowdSec) {
|
||||
setCrowdsecMode(CrowdSecMode.OFF);
|
||||
}
|
||||
}, [supportsCrowdSec]);
|
||||
|
||||
useEffect(() => {
|
||||
onChangeRef.current(rulesToRestrictions(rules, crowdsecMode));
|
||||
}, [rules, crowdsecMode]);
|
||||
|
||||
useEffect(() => {
|
||||
onValidationChangeRef.current?.(hasErrors);
|
||||
@@ -205,6 +240,13 @@ export const ReverseProxyAccessControlRules = ({ value, onChange, onValidationCh
|
||||
|
||||
return (
|
||||
<div className={"flex-col flex"}>
|
||||
{supportsCrowdSec && (
|
||||
<ReverseProxyCrowdSecIPReputation
|
||||
value={crowdsecMode}
|
||||
onChange={setCrowdsecMode}
|
||||
/>
|
||||
)}
|
||||
|
||||
<div>
|
||||
<Label>Access Control Rules</Label>
|
||||
<HelpText>
|
||||
@@ -301,15 +343,17 @@ export const ReverseProxyAccessControlRules = ({ value, onChange, onValidationCh
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
<Button
|
||||
variant="dotted"
|
||||
className="w-full"
|
||||
size="sm"
|
||||
onClick={() => dispatch({ type: "add" })}
|
||||
>
|
||||
<PlusIcon size={14} />
|
||||
Add Rule
|
||||
</Button>
|
||||
<div className="flex gap-2">
|
||||
<Button
|
||||
variant="dotted"
|
||||
className="flex-1"
|
||||
size="sm"
|
||||
onClick={() => dispatch({ type: "add" })}
|
||||
>
|
||||
<PlusIcon size={14} />
|
||||
Add Rule
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
109
src/modules/reverse-proxy/ReverseProxyCrowdSecIPReputation.tsx
Normal file
109
src/modules/reverse-proxy/ReverseProxyCrowdSecIPReputation.tsx
Normal file
@@ -0,0 +1,109 @@
|
||||
import * as React from "react";
|
||||
import { ReactNode } from "react";
|
||||
import { Label } from "@components/Label";
|
||||
import HelpText from "@components/HelpText";
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from "@components/Select";
|
||||
import { EyeIcon, PowerOffIcon, ShieldCheckIcon } from "lucide-react";
|
||||
import { HelpTooltip } from "@components/HelpTooltip";
|
||||
import { CrowdSecMode } from "@/interfaces/ReverseProxy";
|
||||
import Image from "next/image";
|
||||
import CrowdSecIconImage from "@/assets/integrations/crowdsec.png";
|
||||
|
||||
type Props = {
|
||||
value: CrowdSecMode;
|
||||
onChange: (value: CrowdSecMode) => void;
|
||||
};
|
||||
|
||||
type CrowdSecOption = {
|
||||
label: string;
|
||||
description?: string;
|
||||
icon: ReactNode;
|
||||
};
|
||||
|
||||
const CROWDSEC_OPTIONS: Record<CrowdSecMode, CrowdSecOption> = {
|
||||
[CrowdSecMode.OFF]: {
|
||||
label: "Disabled",
|
||||
icon: <PowerOffIcon size={14} />,
|
||||
},
|
||||
[CrowdSecMode.ENFORCE]: {
|
||||
label: "Enforce",
|
||||
description:
|
||||
"Blocked IPs are denied immediately. If the bouncer is not yet synced, connections are denied (fail-closed).",
|
||||
icon: <ShieldCheckIcon size={14} />,
|
||||
},
|
||||
[CrowdSecMode.OBSERVE]: {
|
||||
label: "Observe",
|
||||
description:
|
||||
"Blocked IPs are logged but not denied. Use this to evaluate CrowdSec before enforcing.",
|
||||
icon: <EyeIcon size={14} />,
|
||||
},
|
||||
};
|
||||
|
||||
export const ReverseProxyCrowdSecIPReputation = ({
|
||||
value,
|
||||
onChange,
|
||||
}: Props) => {
|
||||
const selected = CROWDSEC_OPTIONS[value];
|
||||
|
||||
return (
|
||||
<div className="flex items-center gap-0 justify-between mb-6">
|
||||
<div className="flex gap-4">
|
||||
<div
|
||||
className={
|
||||
"h-12 w-12 flex items-center justify-center rounded-md bg-nb-gray-900/70 p-2 border border-nb-gray-900/70 shrink-0 relative"
|
||||
}
|
||||
>
|
||||
<Image
|
||||
src={CrowdSecIconImage}
|
||||
alt={"CrowdSec"}
|
||||
className={"rounded-[4px]"}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<Label>CrowdSec IP Reputation</Label>
|
||||
<HelpText>
|
||||
Detect malicious IPs with CrowdSec.{" "}
|
||||
<b className={"text-white"}>Enforce</b> to block them or{" "}
|
||||
<b className={"text-white"}>Observe</b> to only log without
|
||||
blocking.
|
||||
</HelpText>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Select value={value} onValueChange={(v) => onChange(v as CrowdSecMode)}>
|
||||
<SelectTrigger className="w-[260px]">
|
||||
<div className="flex items-center gap-2 whitespace-nowrap">
|
||||
{selected.icon}
|
||||
<SelectValue />
|
||||
</div>
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{Object.entries(CROWDSEC_OPTIONS).map(([mode, config]) => (
|
||||
<SelectItem
|
||||
key={mode}
|
||||
value={mode}
|
||||
extra={
|
||||
config.description ? (
|
||||
<HelpTooltip
|
||||
triggerClassName="ml-[0.01rem]"
|
||||
align="center"
|
||||
side="right"
|
||||
content={<>{config.description}</>}
|
||||
/>
|
||||
) : undefined
|
||||
}
|
||||
>
|
||||
<span className="whitespace-nowrap">{config.label}</span>
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -46,7 +46,7 @@ export default function ReverseProxyHTTPTargets({
|
||||
}: Readonly<Props>) {
|
||||
return (
|
||||
<div>
|
||||
<Label>HTTP/S Targets</Label>
|
||||
<Label>HTTPS Targets</Label>
|
||||
<HelpText>
|
||||
Add one or more devices running your service or resources to make it
|
||||
publicly accessible.
|
||||
@@ -93,10 +93,7 @@ export default function ReverseProxyHTTPTargets({
|
||||
/>
|
||||
<DropdownMenu modal={false}>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button
|
||||
variant="default-outline"
|
||||
className="!px-3"
|
||||
>
|
||||
<Button variant="default-outline" className="!px-3">
|
||||
<MoreVertical size={16} className="shrink-0" />
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
@@ -104,9 +101,7 @@ export default function ReverseProxyHTTPTargets({
|
||||
className="w-auto min-w-[200px]"
|
||||
align="end"
|
||||
>
|
||||
<DropdownMenuItem
|
||||
onClick={() => onEditTarget(index)}
|
||||
>
|
||||
<DropdownMenuItem onClick={() => onEditTarget(index)}>
|
||||
<div className="flex gap-3 items-center">
|
||||
<Edit size={14} className="shrink-0" />
|
||||
Edit Target
|
||||
@@ -117,10 +112,7 @@ export default function ReverseProxyHTTPTargets({
|
||||
onClick={() => onRemoveTarget(index)}
|
||||
>
|
||||
<div className="flex gap-3 items-center">
|
||||
<MinusCircleIcon
|
||||
size={14}
|
||||
className="shrink-0"
|
||||
/>
|
||||
<MinusCircleIcon size={14} className="shrink-0" />
|
||||
Remove Target
|
||||
</div>
|
||||
</DropdownMenuItem>
|
||||
@@ -151,10 +143,7 @@ export default function ReverseProxyHTTPTargets({
|
||||
variant="warning"
|
||||
className="mt-3"
|
||||
icon={
|
||||
<AlertTriangle
|
||||
size={14}
|
||||
className="shrink-0 relative top-[3px]"
|
||||
/>
|
||||
<AlertTriangle size={14} className="shrink-0 relative top-[3px]" />
|
||||
}
|
||||
>
|
||||
There are currently no resources in your network{" "}
|
||||
|
||||
@@ -66,7 +66,7 @@ export default function ReverseProxyLayer4Content({
|
||||
<Label>
|
||||
Listen Port
|
||||
<HelpTooltip
|
||||
className={"max-w-sm"}
|
||||
className={isListenPortSupported ? "max-w-sm" : "max-w-xs"}
|
||||
content={
|
||||
isListenPortSupported
|
||||
? "Enter the public listen port this service will be reachable on."
|
||||
|
||||
@@ -590,6 +590,7 @@ export default function ReverseProxyModal({
|
||||
value={accessRestrictions}
|
||||
onChange={setAccessRestrictions}
|
||||
onValidationChange={setAccessControlHasErrors}
|
||||
supportsCrowdSec={selectedDomain?.supports_crowdsec}
|
||||
/>
|
||||
</div>
|
||||
</TabsContent>
|
||||
|
||||
@@ -31,7 +31,7 @@ type ServiceModeConfig = {
|
||||
|
||||
export const SERVICE_MODES: Record<ServiceMode, ServiceModeConfig> = {
|
||||
[ServiceMode.HTTP]: {
|
||||
label: "HTTP/S Service",
|
||||
label: "HTTPS Service",
|
||||
description:
|
||||
"Reverse proxy with path routing and built-in authentication (SSO, PIN, password). Typically used for web applications and APIs.",
|
||||
icon: <Globe size={14} />,
|
||||
@@ -64,7 +64,7 @@ export const ReverseProxyServiceModeSelector = ({
|
||||
}: Props) => {
|
||||
const selected = value ?? ServiceMode.HTTP;
|
||||
const selectedMode = SERVICE_MODES[selected];
|
||||
const isL4Supported = domain?.supports_custom_ports === true;
|
||||
const isL4Supported = domain?.supports_custom_ports !== undefined;
|
||||
|
||||
// Reset to HTTP if the current L4 mode becomes unsupported (e.g. domain changed)
|
||||
useEffect(() => {
|
||||
|
||||
@@ -7,6 +7,8 @@ import {
|
||||
Mail,
|
||||
Network,
|
||||
RectangleEllipsis,
|
||||
ShieldAlert,
|
||||
ShieldOff,
|
||||
Users,
|
||||
} from "lucide-react";
|
||||
import * as React from "react";
|
||||
@@ -69,6 +71,26 @@ export const ReverseProxyEventsAuthMethodCell = ({ event }: Props) => {
|
||||
icon: <GlobeOff size={12} />,
|
||||
label: "Geo Unavailable",
|
||||
};
|
||||
case "crowdsec_ban":
|
||||
return {
|
||||
icon: <ShieldAlert size={12} />,
|
||||
label: "CrowdSec Ban",
|
||||
};
|
||||
case "crowdsec_captcha":
|
||||
return {
|
||||
icon: <ShieldAlert size={12} />,
|
||||
label: "CrowdSec Captcha",
|
||||
};
|
||||
case "crowdsec_throttle":
|
||||
return {
|
||||
icon: <ShieldAlert size={12} />,
|
||||
label: "CrowdSec Throttle",
|
||||
};
|
||||
case "crowdsec_unavailable":
|
||||
return {
|
||||
icon: <ShieldOff size={12} />,
|
||||
label: "CrowdSec Unavailable",
|
||||
};
|
||||
default:
|
||||
return {
|
||||
icon: null,
|
||||
|
||||
@@ -1,11 +1,61 @@
|
||||
import Badge from "@components/Badge";
|
||||
import FullTooltip from "@components/FullTooltip";
|
||||
import { ListItem } from "@components/ListItem";
|
||||
import { Info, ShieldAlert } from "lucide-react";
|
||||
import * as React from "react";
|
||||
import { ReverseProxyEvent } from "@/interfaces/ReverseProxy";
|
||||
|
||||
const VERDICT_LABELS: Record<string, string> = {
|
||||
crowdsec_ban: "Ban",
|
||||
crowdsec_captcha: "Captcha",
|
||||
crowdsec_throttle: "Throttle",
|
||||
};
|
||||
|
||||
type Props = {
|
||||
event: ReverseProxyEvent;
|
||||
};
|
||||
|
||||
export const ReverseProxyEventsReasonCell = ({ event }: Props) => {
|
||||
const metadata = event.metadata;
|
||||
const verdict = metadata?.crowdsec_verdict;
|
||||
|
||||
if (verdict && !event.auth_method_used?.startsWith("crowdsec_")) {
|
||||
const verdictLabel = VERDICT_LABELS[verdict] ?? verdict;
|
||||
const metaEntries = Object.entries(metadata!).filter(
|
||||
([k]) => k !== "crowdsec_verdict",
|
||||
);
|
||||
|
||||
return (
|
||||
<FullTooltip
|
||||
side="top"
|
||||
interactive
|
||||
delayDuration={250}
|
||||
skipDelayDuration={100}
|
||||
disabled={metaEntries.length === 0}
|
||||
contentClassName="p-0"
|
||||
content={
|
||||
<div className="text-xs flex flex-col">
|
||||
{metaEntries.map(([key, val]) => (
|
||||
<ListItem
|
||||
key={key}
|
||||
icon={<Info size={14} />}
|
||||
label={key.replaceAll("_", " ")}
|
||||
value={<span className="text-nb-gray-200">{val}</span>}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<div className="px-3 py-2">
|
||||
<Badge variant="gray" className="gap-1.5">
|
||||
<ShieldAlert size={12} className="text-yellow-500" />
|
||||
CrowdSec Observe: {verdictLabel}
|
||||
</Badge>
|
||||
</div>
|
||||
</FullTooltip>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<span className="text-nb-gray-300 text-[0.82rem] py-2 text-left">
|
||||
{event.reason || "-"}
|
||||
|
||||
@@ -10,6 +10,7 @@ import {
|
||||
LucideIcon,
|
||||
NetworkIcon,
|
||||
Settings,
|
||||
ShieldAlert,
|
||||
ShieldCheck,
|
||||
ShieldOff,
|
||||
WorkflowIcon,
|
||||
@@ -19,7 +20,7 @@ import { useMemo } from "react";
|
||||
import { useCountries } from "@/contexts/CountryProvider";
|
||||
import { usePermissions } from "@/contexts/PermissionsProvider";
|
||||
import { useReverseProxies } from "@/contexts/ReverseProxiesProvider";
|
||||
import { ReverseProxy } from "@/interfaces/ReverseProxy";
|
||||
import { CrowdSecMode, ReverseProxy } from "@/interfaces/ReverseProxy";
|
||||
|
||||
type RuleEntry = {
|
||||
key: string;
|
||||
@@ -37,17 +38,27 @@ export default function ReverseProxyAccessControlCell({
|
||||
reverseProxy,
|
||||
}: Readonly<Props>) {
|
||||
const { permission } = usePermissions();
|
||||
const { openModal } = useReverseProxies();
|
||||
const { openModal, domains } = useReverseProxies();
|
||||
const { countries } = useCountries();
|
||||
|
||||
const canConfigure = !!permission?.services?.update;
|
||||
const restrictions = reverseProxy.access_restrictions;
|
||||
|
||||
const supportsCrowdSec = domains?.find(
|
||||
(d) => d.domain === reverseProxy.proxy_cluster,
|
||||
)?.supports_crowdsec;
|
||||
|
||||
const hasCrowdSec =
|
||||
supportsCrowdSec &&
|
||||
restrictions?.crowdsec_mode != null &&
|
||||
restrictions.crowdsec_mode !== CrowdSecMode.OFF;
|
||||
|
||||
const ruleCount =
|
||||
(restrictions?.allowed_cidrs?.length ?? 0) +
|
||||
(restrictions?.blocked_cidrs?.length ?? 0) +
|
||||
(restrictions?.allowed_countries?.length ?? 0) +
|
||||
(restrictions?.blocked_countries?.length ?? 0);
|
||||
(restrictions?.blocked_countries?.length ?? 0) +
|
||||
(hasCrowdSec ? 1 : 0);
|
||||
|
||||
const rulesBadge =
|
||||
ruleCount > 0 ? (
|
||||
@@ -92,21 +103,23 @@ export default function ReverseProxyAccessControlCell({
|
||||
});
|
||||
}
|
||||
|
||||
const isHostCidr = (c: string) =>
|
||||
c.includes(":") ? c.endsWith("/128") : c.endsWith("/32");
|
||||
const allowedIps =
|
||||
restrictions?.allowed_cidrs?.filter((c) => c.endsWith("/32")) ?? [];
|
||||
restrictions?.allowed_cidrs?.filter(isHostCidr) ?? [];
|
||||
const allowedCidrs =
|
||||
restrictions?.allowed_cidrs?.filter((c) => !c.endsWith("/32")) ?? [];
|
||||
restrictions?.allowed_cidrs?.filter((c) => !isHostCidr(c)) ?? [];
|
||||
const blockedIps =
|
||||
restrictions?.blocked_cidrs?.filter((c) => c.endsWith("/32")) ?? [];
|
||||
restrictions?.blocked_cidrs?.filter(isHostCidr) ?? [];
|
||||
const blockedCidrs =
|
||||
restrictions?.blocked_cidrs?.filter((c) => !c.endsWith("/32")) ?? [];
|
||||
restrictions?.blocked_cidrs?.filter((c) => !isHostCidr(c)) ?? [];
|
||||
|
||||
if (allowedIps.length) {
|
||||
entries.push({
|
||||
key: "allowed-ips",
|
||||
label: allowedIps.length === 1 ? "Allowed IP" : "Allowed IPs",
|
||||
Icon: WorkflowIcon,
|
||||
value: allowedIps.map((c) => c.replace(/\/32$/, "")).join(", "),
|
||||
value: allowedIps.map((c) => c.replace(/\/(32|128)$/, "")).join(", "),
|
||||
});
|
||||
}
|
||||
|
||||
@@ -124,7 +137,7 @@ export default function ReverseProxyAccessControlCell({
|
||||
key: "blocked-ips",
|
||||
label: blockedIps.length === 1 ? "Blocked IP" : "Blocked IPs",
|
||||
Icon: WorkflowIcon,
|
||||
value: blockedIps.map((c) => c.replace(/\/32$/, "")).join(", "),
|
||||
value: blockedIps.map((c) => c.replace(/\/(32|128)$/, "")).join(", "),
|
||||
blocked: true,
|
||||
});
|
||||
}
|
||||
@@ -139,8 +152,20 @@ export default function ReverseProxyAccessControlCell({
|
||||
});
|
||||
}
|
||||
|
||||
if (hasCrowdSec) {
|
||||
entries.push({
|
||||
key: "crowdsec",
|
||||
label: "CrowdSec",
|
||||
Icon: ShieldAlert,
|
||||
value:
|
||||
restrictions?.crowdsec_mode === CrowdSecMode.ENFORCE
|
||||
? "Enforce"
|
||||
: "Observe",
|
||||
});
|
||||
}
|
||||
|
||||
return entries;
|
||||
}, [restrictions, countries]);
|
||||
}, [restrictions, countries, hasCrowdSec]);
|
||||
|
||||
const showRulesHover = ruleGroups.length > 0;
|
||||
|
||||
|
||||
@@ -37,6 +37,7 @@ export default function ReverseProxyActionCell({
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent className="w-auto" align="end">
|
||||
<DropdownMenuItem
|
||||
data-proxy-edit-action={reverseProxy.id}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
openModal({ proxy: reverseProxy });
|
||||
|
||||
@@ -15,7 +15,7 @@ export default function ReverseProxyActiveCell({
|
||||
const { handleToggle } = useReverseProxies();
|
||||
|
||||
return (
|
||||
<div className={"flex min-w-[0px]"}>
|
||||
<div className={"flex min-w-[0px]"} data-active-cell>
|
||||
<ToggleSwitch
|
||||
disabled={!permission?.services?.update}
|
||||
checked={reverseProxy.enabled}
|
||||
|
||||
@@ -112,7 +112,9 @@ export default function ReverseProxyAuthCell({
|
||||
variant={"gray"}
|
||||
useHover={false}
|
||||
disabled={!canConfigure}
|
||||
className={"cursor-pointer !rounded-r-none !border-r-0 !h-[34px] min-w-[100px] !justify-start hover:bg-nb-gray-930 transition-all"}
|
||||
className={
|
||||
"cursor-pointer !rounded-r-none !border-r-0 !h-[34px] min-w-[100px] !justify-start hover:bg-nb-gray-930 transition-all"
|
||||
}
|
||||
>
|
||||
<SingleAuthIcon size={12} className="text-green-500" />
|
||||
<span className={"font-medium text-xs"}>{singleAuth!.label}</span>
|
||||
@@ -122,7 +124,9 @@ export default function ReverseProxyAuthCell({
|
||||
variant={"gray"}
|
||||
useHover={false}
|
||||
disabled={!canConfigure}
|
||||
className={"cursor-pointer !rounded-r-none !border-r-0 !h-[34px] min-w-[100px] !justify-start hover:bg-nb-gray-930 transition-all"}
|
||||
className={
|
||||
"cursor-pointer !rounded-r-none !border-r-0 !h-[34px] min-w-[100px] !justify-start hover:bg-nb-gray-930 transition-all"
|
||||
}
|
||||
>
|
||||
<LockKeyhole size={12} className="text-green-500" />
|
||||
<span className={"font-medium text-xs"}>{authCount} Enabled</span>
|
||||
@@ -130,15 +134,20 @@ export default function ReverseProxyAuthCell({
|
||||
) : null;
|
||||
|
||||
const showAuthHover =
|
||||
authCount > 1 || (authCount === 1 && (auth?.bearer_auth?.enabled || hasHeaderAuths));
|
||||
authCount > 1 ||
|
||||
(authCount === 1 && (auth?.bearer_auth?.enabled || hasHeaderAuths));
|
||||
|
||||
return (
|
||||
<div className={"flex"} onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
if (permission?.services?.update) {
|
||||
openModal({ proxy: reverseProxy, initialTab: "auth" });
|
||||
}
|
||||
}}>
|
||||
<div
|
||||
className={"flex"}
|
||||
data-auth-cell
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
if (permission?.services?.update) {
|
||||
openModal({ proxy: reverseProxy, initialTab: "auth" });
|
||||
}
|
||||
}}
|
||||
>
|
||||
<div className={"flex items-center"}>
|
||||
{authBadge ? (
|
||||
<HoverCard openDelay={200} closeDelay={100}>
|
||||
@@ -189,7 +198,15 @@ export default function ReverseProxyAuthCell({
|
||||
label={HEADER_AUTH_METHOD.hoverLabel}
|
||||
value={
|
||||
<div className={"text-green-500"}>
|
||||
{(auth?.header_auths ?? []).filter((h) => h.enabled).length} Header{(auth?.header_auths ?? []).filter((h) => h.enabled).length !== 1 ? "s" : ""}
|
||||
{
|
||||
(auth?.header_auths ?? []).filter((h) => h.enabled)
|
||||
.length
|
||||
}{" "}
|
||||
Header
|
||||
{(auth?.header_auths ?? []).filter((h) => h.enabled)
|
||||
.length !== 1
|
||||
? "s"
|
||||
: ""}
|
||||
</div>
|
||||
}
|
||||
/>
|
||||
@@ -202,7 +219,9 @@ export default function ReverseProxyAuthCell({
|
||||
<Badge
|
||||
variant={"gray"}
|
||||
disabled={!canConfigure}
|
||||
className={"cursor-pointer !rounded-r-none !border-r-0 !h-[34px] min-w-[100px] !justify-start hover:bg-nb-gray-930 transition-all"}
|
||||
className={
|
||||
"cursor-pointer !rounded-r-none !border-r-0 !h-[34px] min-w-[100px] !justify-start hover:bg-nb-gray-930 transition-all"
|
||||
}
|
||||
>
|
||||
<LockOpenIcon size={12} className="text-red-500" />
|
||||
<span className={"font-medium text-xs"}>No Auth</span>
|
||||
|
||||
@@ -31,7 +31,7 @@ export default function ReverseProxyClusterCell({
|
||||
|
||||
if (!hasCluster) {
|
||||
return (
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="flex items-center gap-2" data-cluster-cell>
|
||||
<Badge variant="gray" className="font-normal">
|
||||
<Globe size={12} />
|
||||
All
|
||||
@@ -42,7 +42,7 @@ export default function ReverseProxyClusterCell({
|
||||
|
||||
if (isConnected) {
|
||||
return (
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="flex items-center gap-2" data-cluster-cell>
|
||||
<Badge variant={"gray"} className={cn("font-normal")}>
|
||||
<Server size={11} className={cn("text-green-500")} />
|
||||
{reverseProxy.proxy_cluster}
|
||||
@@ -76,7 +76,7 @@ export default function ReverseProxyClusterCell({
|
||||
align={"center"}
|
||||
alignOffset={0}
|
||||
>
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="flex items-center gap-2" data-cluster-cell>
|
||||
<Badge variant={"red"} className={cn("font-normal")}>
|
||||
<AlertTriangle size={11} />
|
||||
{reverseProxy.proxy_cluster}
|
||||
|
||||
@@ -36,6 +36,7 @@ export default function ReverseProxyNameCell({
|
||||
? "gap-6 min-w-[270px] max-w-[270px]"
|
||||
: "gap-2.5 min-w-[200px]",
|
||||
)}
|
||||
data-name-cell
|
||||
>
|
||||
{showChevron && (
|
||||
<>
|
||||
|
||||
@@ -63,56 +63,60 @@ export default function ReverseProxyStatusCell({
|
||||
if (isActive) return null;
|
||||
if (hasError) {
|
||||
return (
|
||||
<FullTooltip
|
||||
content={
|
||||
<div className={"text-xs max-w-xs"}>
|
||||
Something went wrong while setting up this service. See our{" "}
|
||||
<InlineLink
|
||||
href={REVERSE_PROXY_TROUBLESHOOTING_DOCS_LINK}
|
||||
target={"_blank"}
|
||||
>
|
||||
Troubleshooting Docs
|
||||
</InlineLink>{" "}
|
||||
for more details.
|
||||
<div className={"flex"} data-status-cell>
|
||||
<FullTooltip
|
||||
content={
|
||||
<div className={"text-xs max-w-xs"}>
|
||||
Something went wrong while setting up this service. See our{" "}
|
||||
<InlineLink
|
||||
href={REVERSE_PROXY_TROUBLESHOOTING_DOCS_LINK}
|
||||
target={"_blank"}
|
||||
>
|
||||
Troubleshooting Docs
|
||||
</InlineLink>{" "}
|
||||
for more details.
|
||||
</div>
|
||||
}
|
||||
align={"center"}
|
||||
alignOffset={0}
|
||||
>
|
||||
<div className={"flex"}>
|
||||
<Badge variant={"red"}>
|
||||
<CircleAlert size={11} />
|
||||
Error
|
||||
</Badge>
|
||||
</div>
|
||||
}
|
||||
align={"center"}
|
||||
alignOffset={0}
|
||||
>
|
||||
<div className={"flex"}>
|
||||
<Badge variant={"red"}>
|
||||
<CircleAlert size={11} />
|
||||
Error
|
||||
</Badge>
|
||||
</div>
|
||||
</FullTooltip>
|
||||
</FullTooltip>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
if (isTunnelNotCreated) {
|
||||
return (
|
||||
<FullTooltip
|
||||
content={
|
||||
<div className={"text-xs max-w-xs"}>
|
||||
The tunnel to the target peer could not be established. See our{" "}
|
||||
<InlineLink
|
||||
href={REVERSE_PROXY_TROUBLESHOOTING_DOCS_LINK}
|
||||
target={"_blank"}
|
||||
>
|
||||
Troubleshooting Docs
|
||||
</InlineLink>{" "}
|
||||
for more details.
|
||||
<div className={"flex"} data-status-cell>
|
||||
<FullTooltip
|
||||
content={
|
||||
<div className={"text-xs max-w-xs"}>
|
||||
The tunnel to the target peer could not be established. See our{" "}
|
||||
<InlineLink
|
||||
href={REVERSE_PROXY_TROUBLESHOOTING_DOCS_LINK}
|
||||
target={"_blank"}
|
||||
>
|
||||
Troubleshooting Docs
|
||||
</InlineLink>{" "}
|
||||
for more details.
|
||||
</div>
|
||||
}
|
||||
align={"center"}
|
||||
alignOffset={0}
|
||||
>
|
||||
<div className={"flex"}>
|
||||
<Badge variant={"red"}>
|
||||
<CircleAlert size={11} />
|
||||
Tunnel not created
|
||||
</Badge>
|
||||
</div>
|
||||
}
|
||||
align={"center"}
|
||||
alignOffset={0}
|
||||
>
|
||||
<div className={"flex"}>
|
||||
<Badge variant={"red"}>
|
||||
<CircleAlert size={11} />
|
||||
Tunnel not created
|
||||
</Badge>
|
||||
</div>
|
||||
</FullTooltip>
|
||||
</FullTooltip>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
return <SettingUpService />;
|
||||
@@ -120,12 +124,12 @@ export default function ReverseProxyStatusCell({
|
||||
|
||||
// HTTP services: hide once active with certificate issued
|
||||
if (isActive && certificateIssued) {
|
||||
return null;
|
||||
return <div data-status-cell />;
|
||||
}
|
||||
|
||||
if (!certificateIssued) {
|
||||
return (
|
||||
<div className={"flex"}>
|
||||
<div className={"flex"} data-status-cell>
|
||||
<Badge variant={"yellow"}>
|
||||
<Loader2 size={12} className={"animate-spin"} />
|
||||
Issuing certificate...
|
||||
@@ -139,7 +143,7 @@ export default function ReverseProxyStatusCell({
|
||||
|
||||
const SettingUpService = () => {
|
||||
return (
|
||||
<div className={"flex"}>
|
||||
<div className={"flex"} data-status-cell>
|
||||
<Badge variant={"yellow"}>
|
||||
<Loader2 size={14} className={"animate-spin"} />
|
||||
Setting up service...
|
||||
|
||||
@@ -36,7 +36,7 @@ export default function ReverseProxyTargetsCell({
|
||||
const targetsCount = reverseProxy?.targets?.length ?? 0;
|
||||
|
||||
return (
|
||||
<div className={"flex gap-3"}>
|
||||
<div className={"flex gap-3"} data-targets-cell>
|
||||
{targetsCount > 0 && (
|
||||
<Badge
|
||||
variant={"gray"}
|
||||
|
||||
@@ -13,9 +13,9 @@ type Props = {
|
||||
|
||||
export const ReverseProxyTypeCell = ({ reverseProxy }: Props) => {
|
||||
const serviceModeLabel = useMemo(() => {
|
||||
if (!reverseProxy?.mode) return "HTTP/S";
|
||||
if (!reverseProxy?.mode) return "HTTPS";
|
||||
const mode = SERVICE_MODES[reverseProxy.mode];
|
||||
if (!mode) return "HTTP/S";
|
||||
if (!mode) return "HTTPS";
|
||||
return trim(mode.label.replace("Service", ""));
|
||||
}, [reverseProxy]);
|
||||
|
||||
|
||||
@@ -38,6 +38,7 @@ export default function ReverseProxyFlatTargetActionCell({
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent className="w-auto" align="end">
|
||||
<DropdownMenuItem
|
||||
data-proxy-edit-action={target.proxy.id}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
if (isL4Mode(target.proxy.mode)) {
|
||||
@@ -55,6 +56,7 @@ export default function ReverseProxyFlatTargetActionCell({
|
||||
</DropdownMenuItem>
|
||||
|
||||
<DropdownMenuItem
|
||||
data-proxy-settings-action={target.proxy.id}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
openModal({ proxy: target.proxy, initialTab: "settings" });
|
||||
|
||||
@@ -49,13 +49,19 @@ const FlatTargetsTableColumns: ColumnDef<ReverseProxyFlatTarget>[] = [
|
||||
const isEnabled = target.proxy.enabled && target.enabled;
|
||||
|
||||
return (
|
||||
<div className={disabled ? "opacity-40" : ""}>
|
||||
<ReverseProxyNameCell
|
||||
domain={fullUrl}
|
||||
enabled={isEnabled}
|
||||
reverseProxy={row.original.proxy}
|
||||
showChevron={false}
|
||||
/>
|
||||
<div className="flex items-center gap-2">
|
||||
<div
|
||||
className={disabled ? "opacity-40" : ""}
|
||||
data-proxy-id={target.proxy.id}
|
||||
>
|
||||
<ReverseProxyNameCell
|
||||
domain={fullUrl}
|
||||
enabled={isEnabled}
|
||||
reverseProxy={row.original.proxy}
|
||||
showChevron={false}
|
||||
/>
|
||||
</div>
|
||||
<div data-status-cell />
|
||||
</div>
|
||||
);
|
||||
},
|
||||
@@ -64,7 +70,9 @@ const FlatTargetsTableColumns: ColumnDef<ReverseProxyFlatTarget>[] = [
|
||||
accessorKey: "arrow",
|
||||
header: "",
|
||||
cell: ({ row }) => (
|
||||
<ReverseProxyArrowCell disabled={!row.original.enabled} />
|
||||
<div data-proxy-id={row.original.proxy.id}>
|
||||
<ReverseProxyArrowCell disabled={row.original.enabled === false} />
|
||||
</div>
|
||||
),
|
||||
},
|
||||
{
|
||||
@@ -72,7 +80,11 @@ const FlatTargetsTableColumns: ColumnDef<ReverseProxyFlatTarget>[] = [
|
||||
header: ({ column }) => (
|
||||
<DataTableHeader column={column}>Destination</DataTableHeader>
|
||||
),
|
||||
cell: ({ row }) => <ReverseProxyDestinationCell target={row.original} />,
|
||||
cell: ({ row }) => (
|
||||
<div data-proxy-id={row.original.proxy.id}>
|
||||
<ReverseProxyDestinationCell target={row.original} />
|
||||
</div>
|
||||
),
|
||||
},
|
||||
{
|
||||
accessorKey: "enabled",
|
||||
@@ -80,9 +92,11 @@ const FlatTargetsTableColumns: ColumnDef<ReverseProxyFlatTarget>[] = [
|
||||
<DataTableHeader column={column}>Active</DataTableHeader>
|
||||
),
|
||||
cell: ({ row }) => (
|
||||
<ReverseProxyTargetProvider value={row.original.proxy}>
|
||||
<ReverseProxyTargetActiveCell target={row.original} />
|
||||
</ReverseProxyTargetProvider>
|
||||
<div data-proxy-id={row.original.proxy.id}>
|
||||
<ReverseProxyTargetProvider value={row.original.proxy}>
|
||||
<ReverseProxyTargetActiveCell target={row.original} />
|
||||
</ReverseProxyTargetProvider>
|
||||
</div>
|
||||
),
|
||||
},
|
||||
{
|
||||
@@ -91,7 +105,9 @@ const FlatTargetsTableColumns: ColumnDef<ReverseProxyFlatTarget>[] = [
|
||||
<DataTableHeader column={column}>Cluster</DataTableHeader>
|
||||
),
|
||||
cell: ({ row }) => (
|
||||
<ReverseProxyClusterCell reverseProxy={row.original.proxy} />
|
||||
<div data-proxy-id={row.original.proxy.id}>
|
||||
<ReverseProxyClusterCell reverseProxy={row.original.proxy} />
|
||||
</div>
|
||||
),
|
||||
},
|
||||
{
|
||||
@@ -100,7 +116,9 @@ const FlatTargetsTableColumns: ColumnDef<ReverseProxyFlatTarget>[] = [
|
||||
<DataTableHeader column={column}>Resource</DataTableHeader>
|
||||
),
|
||||
cell: ({ row }) => (
|
||||
<ReverseProxyTargetDevice target={row.original} showDescription />
|
||||
<div data-proxy-id={row.original.proxy.id}>
|
||||
<ReverseProxyTargetDevice target={row.original} showDescription />
|
||||
</div>
|
||||
),
|
||||
},
|
||||
{
|
||||
@@ -109,7 +127,9 @@ const FlatTargetsTableColumns: ColumnDef<ReverseProxyFlatTarget>[] = [
|
||||
<DataTableHeader column={column}>Auth Methods</DataTableHeader>
|
||||
),
|
||||
cell: ({ row }) => (
|
||||
<ReverseProxyAuthCell reverseProxy={row.original.proxy} />
|
||||
<div data-proxy-id={row.original.proxy.id}>
|
||||
<ReverseProxyAuthCell reverseProxy={row.original.proxy} />
|
||||
</div>
|
||||
),
|
||||
},
|
||||
{
|
||||
@@ -118,7 +138,9 @@ const FlatTargetsTableColumns: ColumnDef<ReverseProxyFlatTarget>[] = [
|
||||
<DataTableHeader column={column}>Access Control</DataTableHeader>
|
||||
),
|
||||
cell: ({ row }) => (
|
||||
<ReverseProxyAccessControlCell reverseProxy={row.original.proxy} />
|
||||
<div data-proxy-id={row.original.proxy.id}>
|
||||
<ReverseProxyAccessControlCell reverseProxy={row.original.proxy} />
|
||||
</div>
|
||||
),
|
||||
},
|
||||
{
|
||||
|
||||
@@ -174,7 +174,7 @@ export default function NetworkRoutesTable({
|
||||
wrapperComponent={isGroupPage ? Card : undefined}
|
||||
wrapperProps={isGroupPage ? { className: "mt-6 w-full" } : undefined}
|
||||
paginationPaddingClassName={isGroupPage ? "px-0 pt-8" : undefined}
|
||||
tableClassName={isGroupPage ? "mt-0 mb-2" : undefined}
|
||||
tableClassName={isGroupPage ? "mt-0" : undefined}
|
||||
inset={false}
|
||||
minimal={isGroupPage}
|
||||
keepStateInLocalStorage={!isGroupPage}
|
||||
|
||||
@@ -26,9 +26,8 @@ export default function DangerZoneTab({ account }: Props) {
|
||||
const deletePromise = new Promise<void>((resolve, reject) => {
|
||||
return deleteRequest
|
||||
.del()
|
||||
.catch((error) => reject(error))
|
||||
.then(() => {
|
||||
// Clear browser storage after account deletion
|
||||
// Clear browser storage only after confirmed account deletion
|
||||
if (typeof window !== "undefined") {
|
||||
localStorage.clear();
|
||||
sessionStorage.clear();
|
||||
@@ -37,7 +36,8 @@ export default function DangerZoneTab({ account }: Props) {
|
||||
}
|
||||
logout().then();
|
||||
resolve();
|
||||
});
|
||||
})
|
||||
.catch((error) => reject(error));
|
||||
});
|
||||
|
||||
notify({
|
||||
|
||||
@@ -6,13 +6,22 @@ type Props = {
|
||||
name?: string;
|
||||
email?: string;
|
||||
id?: string;
|
||||
size?: "default" | "sm";
|
||||
className?: string;
|
||||
};
|
||||
export const SmallUserAvatar = ({ name, id, email, className }: Props) => {
|
||||
export const SmallUserAvatar = ({
|
||||
name,
|
||||
id,
|
||||
email,
|
||||
size = "default",
|
||||
className,
|
||||
}: Props) => {
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
"w-7 h-7 rounded-full shrink-0 flex items-center justify-center text-white uppercase text-[12px] font-medium bg-nb-gray-850",
|
||||
"rounded-full shrink-0 flex items-center justify-center text-white uppercase font-medium bg-nb-gray-850",
|
||||
size === "default" && "w-7 h-7 text-[12px]",
|
||||
size === "sm" && "w-5 h-5 text-[9px] leading-[0]",
|
||||
className,
|
||||
)}
|
||||
style={{
|
||||
|
||||
@@ -18,7 +18,9 @@ export const getInstallUrl = () => {
|
||||
export const isNetBirdHosted = () => {
|
||||
const hostname = window.location.hostname;
|
||||
if (hostname.includes("selfhosted")) return false;
|
||||
return hostname.endsWith(".netbird.io") || hostname.endsWith(".wiretrustee.com");
|
||||
return (
|
||||
hostname.endsWith(".netbird.io") || hostname.endsWith(".wiretrustee.com")
|
||||
);
|
||||
};
|
||||
|
||||
export const isLocalDev = () => {
|
||||
|
||||
Reference in New Issue
Block a user