mirror of
https://github.com/fosrl/pangolin.git
synced 2026-07-06 04:10:13 +00:00
Compare commits
77 Commits
3eac80e666
...
9ef93df54f
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9ef93df54f | ||
|
|
fd9fdf6399 | ||
|
|
8fa1701e06 | ||
|
|
4abe83f8a9 | ||
|
|
db0f7cfbae | ||
|
|
1724885371 | ||
|
|
a97e9ea8b1 | ||
|
|
9d30e97526 | ||
|
|
b91330a27a | ||
|
|
744bc9ebe9 | ||
|
|
89ed9e6d7f | ||
|
|
b007e7f54a | ||
|
|
6651a6df42 | ||
|
|
3f29b165aa | ||
|
|
b13b91face | ||
|
|
63c14fe2d5 | ||
|
|
14e74ed02d | ||
|
|
7e30750618 | ||
|
|
4d1dd16be5 | ||
|
|
fa49cf5eba | ||
|
|
26b39fc1c6 | ||
|
|
0d36e368ea | ||
|
|
859f265c68 | ||
|
|
3219f520ba | ||
|
|
97e27b6caf | ||
|
|
09da83a72b | ||
|
|
d13b210e2f | ||
|
|
09fb672718 | ||
|
|
9797ad0e17 | ||
|
|
8b3d61ac36 | ||
|
|
7161c9547a | ||
|
|
60d4362a87 | ||
|
|
1836e0c8fc | ||
|
|
d3344aeb34 | ||
|
|
cfeb093fa6 | ||
|
|
a469b3ffcc | ||
|
|
14b3a3fdd8 | ||
|
|
94367ce387 | ||
|
|
5be518aa50 | ||
|
|
d059a8da9e | ||
|
|
1dcacbef7a | ||
|
|
a25edeccf7 | ||
|
|
315f73c77d | ||
|
|
666288fccc | ||
|
|
0ccf61c2a9 | ||
|
|
c16b1b27a3 | ||
|
|
ed9ba60be6 | ||
|
|
24d047e3d8 | ||
|
|
9671079ffb | ||
|
|
688892523c | ||
|
|
b02c341f62 | ||
|
|
3e9bcada1e | ||
|
|
93d4bd6438 | ||
|
|
5146498b33 | ||
|
|
72da4f39a8 | ||
|
|
a2b2fb804b | ||
|
|
718d2122a4 | ||
|
|
310c6c90a3 | ||
|
|
9d80f62d58 | ||
|
|
fe729ec762 | ||
|
|
d6a829abc2 | ||
|
|
75005ccf81 | ||
|
|
fd6c600531 | ||
|
|
6996c2501e | ||
|
|
efbd9bdb56 | ||
|
|
0d34213647 | ||
|
|
870b85d71b | ||
|
|
86ba6b6f86 | ||
|
|
02be3cd0c4 | ||
|
|
1b756ef9a0 | ||
|
|
ceda06f9ae | ||
|
|
068eba015b | ||
|
|
90f66baf85 | ||
|
|
5743c0bb72 | ||
|
|
ec10c37468 | ||
|
|
5ee3e140ed | ||
|
|
8e1fd4474f |
6
.github/workflows/cicd.yml
vendored
6
.github/workflows/cicd.yml
vendored
@@ -29,7 +29,7 @@ jobs:
|
||||
permissions: write-all
|
||||
steps:
|
||||
- name: Configure AWS credentials
|
||||
uses: aws-actions/configure-aws-credentials@v2
|
||||
uses: aws-actions/configure-aws-credentials@v5
|
||||
with:
|
||||
role-to-assume: arn:aws:iam::${{ secrets.AWS_ACCOUNT_ID }}:role/${{ secrets.AWS_ROLE_NAME }}
|
||||
role-duration-seconds: 3600
|
||||
@@ -264,7 +264,7 @@ jobs:
|
||||
shell: bash
|
||||
|
||||
- name: Install Go
|
||||
uses: actions/setup-go@4dc6199c7b1a012772edbd06daecab0f50c9053c # v6.1.0
|
||||
uses: actions/setup-go@7a3fe6cf4cb3a834922a1244abfce67bcef6a0c5 # v6.2.0
|
||||
with:
|
||||
go-version: 1.24
|
||||
|
||||
@@ -513,7 +513,7 @@ jobs:
|
||||
permissions: write-all
|
||||
steps:
|
||||
- name: Configure AWS credentials
|
||||
uses: aws-actions/configure-aws-credentials@v2
|
||||
uses: aws-actions/configure-aws-credentials@v5
|
||||
with:
|
||||
role-to-assume: arn:aws:iam::${{ secrets.AWS_ACCOUNT_ID }}:role/${{ secrets.AWS_ROLE_NAME }}
|
||||
role-duration-seconds: 3600
|
||||
|
||||
2
.github/workflows/linting.yml
vendored
2
.github/workflows/linting.yml
vendored
@@ -24,7 +24,7 @@ jobs:
|
||||
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
||||
|
||||
- name: Set up Node.js
|
||||
uses: actions/setup-node@395ad3262231945c25e8478fd5baf05154b1d79f # v6.1.0
|
||||
uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v6.2.0
|
||||
with:
|
||||
node-version: '22'
|
||||
|
||||
|
||||
6
.github/workflows/saas.yml
vendored
6
.github/workflows/saas.yml
vendored
@@ -23,7 +23,7 @@ jobs:
|
||||
permissions: write-all
|
||||
steps:
|
||||
- name: Configure AWS credentials
|
||||
uses: aws-actions/configure-aws-credentials@v2
|
||||
uses: aws-actions/configure-aws-credentials@v5
|
||||
with:
|
||||
role-to-assume: arn:aws:iam::${{ secrets.AWS_ACCOUNT_ID }}:role/${{ secrets.AWS_ROLE_NAME }}
|
||||
role-duration-seconds: 3600
|
||||
@@ -69,7 +69,7 @@ jobs:
|
||||
fi
|
||||
|
||||
- name: Configure AWS credentials
|
||||
uses: aws-actions/configure-aws-credentials@v2
|
||||
uses: aws-actions/configure-aws-credentials@v5
|
||||
with:
|
||||
role-to-assume: arn:aws:iam::${{ secrets.aws_account_id }}:role/${{ secrets.AWS_ROLE_NAME }}
|
||||
role-duration-seconds: 3600
|
||||
@@ -110,7 +110,7 @@ jobs:
|
||||
permissions: write-all
|
||||
steps:
|
||||
- name: Configure AWS credentials
|
||||
uses: aws-actions/configure-aws-credentials@v2
|
||||
uses: aws-actions/configure-aws-credentials@v5
|
||||
with:
|
||||
role-to-assume: arn:aws:iam::${{ secrets.AWS_ACCOUNT_ID }}:role/${{ secrets.AWS_ROLE_NAME }}
|
||||
role-duration-seconds: 3600
|
||||
|
||||
2
.github/workflows/test.yml
vendored
2
.github/workflows/test.yml
vendored
@@ -17,7 +17,7 @@ jobs:
|
||||
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
||||
|
||||
- name: Install Node
|
||||
uses: actions/setup-node@395ad3262231945c25e8478fd5baf05154b1d79f # v6.1.0
|
||||
uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v6.2.0
|
||||
with:
|
||||
node-version: '22'
|
||||
|
||||
|
||||
123
cli/commands/deleteClient.ts
Normal file
123
cli/commands/deleteClient.ts
Normal file
@@ -0,0 +1,123 @@
|
||||
import { CommandModule } from "yargs";
|
||||
import { db, clients, olms, currentFingerprint, userClients, approvals } from "@server/db";
|
||||
import { eq, and, inArray } from "drizzle-orm";
|
||||
|
||||
type DeleteClientArgs = {
|
||||
orgId: string;
|
||||
niceId: string;
|
||||
};
|
||||
|
||||
export const deleteClient: CommandModule<{}, DeleteClientArgs> = {
|
||||
command: "delete-client",
|
||||
describe:
|
||||
"Delete a client and all associated data (OLMs, current fingerprint, userClients, approvals). Snapshots are preserved.",
|
||||
builder: (yargs) => {
|
||||
return yargs
|
||||
.option("orgId", {
|
||||
type: "string",
|
||||
demandOption: true,
|
||||
describe: "The organization ID"
|
||||
})
|
||||
.option("niceId", {
|
||||
type: "string",
|
||||
demandOption: true,
|
||||
describe: "The client niceId (identifier)"
|
||||
});
|
||||
},
|
||||
handler: async (argv: { orgId: string; niceId: string }) => {
|
||||
try {
|
||||
const { orgId, niceId } = argv;
|
||||
|
||||
console.log(
|
||||
`Deleting client with orgId: ${orgId}, niceId: ${niceId}...`
|
||||
);
|
||||
|
||||
// Find the client
|
||||
const [client] = await db
|
||||
.select()
|
||||
.from(clients)
|
||||
.where(and(eq(clients.orgId, orgId), eq(clients.niceId, niceId)))
|
||||
.limit(1);
|
||||
|
||||
if (!client) {
|
||||
console.error(
|
||||
`Error: Client with orgId "${orgId}" and niceId "${niceId}" not found.`
|
||||
);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const clientId = client.clientId;
|
||||
console.log(`Found client with clientId: ${clientId}`);
|
||||
|
||||
// Find all OLMs associated with this client
|
||||
const associatedOlms = await db
|
||||
.select()
|
||||
.from(olms)
|
||||
.where(eq(olms.clientId, clientId));
|
||||
|
||||
console.log(`Found ${associatedOlms.length} OLM(s) associated with this client`);
|
||||
|
||||
// Delete in a transaction to ensure atomicity
|
||||
await db.transaction(async (trx) => {
|
||||
// Delete currentFingerprint entries for the associated OLMs
|
||||
// Note: We delete these explicitly before deleting OLMs to ensure
|
||||
// we have control, even though cascade would handle it
|
||||
let fingerprintCount = 0;
|
||||
if (associatedOlms.length > 0) {
|
||||
const olmIds = associatedOlms.map((olm) => olm.olmId);
|
||||
const deletedFingerprints = await trx
|
||||
.delete(currentFingerprint)
|
||||
.where(inArray(currentFingerprint.olmId, olmIds))
|
||||
.returning();
|
||||
fingerprintCount = deletedFingerprints.length;
|
||||
}
|
||||
console.log(`Deleted ${fingerprintCount} current fingerprint(s)`);
|
||||
|
||||
// Delete OLMs
|
||||
// Note: OLMs have onDelete: "set null" for clientId, so we need to delete them explicitly
|
||||
const deletedOlms = await trx
|
||||
.delete(olms)
|
||||
.where(eq(olms.clientId, clientId))
|
||||
.returning();
|
||||
console.log(`Deleted ${deletedOlms.length} OLM(s)`);
|
||||
|
||||
// Delete approvals
|
||||
// Note: Approvals have onDelete: "cascade" but we delete explicitly for clarity
|
||||
const deletedApprovals = await trx
|
||||
.delete(approvals)
|
||||
.where(eq(approvals.clientId, clientId))
|
||||
.returning();
|
||||
console.log(`Deleted ${deletedApprovals.length} approval(s)`);
|
||||
|
||||
// Delete userClients
|
||||
// Note: userClients have onDelete: "cascade" but we delete explicitly for clarity
|
||||
const deletedUserClients = await trx
|
||||
.delete(userClients)
|
||||
.where(eq(userClients.clientId, clientId))
|
||||
.returning();
|
||||
console.log(`Deleted ${deletedUserClients.length} userClient association(s)`);
|
||||
|
||||
// Finally, delete the client itself
|
||||
const deletedClients = await trx
|
||||
.delete(clients)
|
||||
.where(eq(clients.clientId, clientId))
|
||||
.returning();
|
||||
console.log(`Deleted client: ${deletedClients[0]?.name || niceId}`);
|
||||
});
|
||||
|
||||
console.log("\nClient deletion completed successfully!");
|
||||
console.log("\nSummary:");
|
||||
console.log(` - Client: ${niceId} (clientId: ${clientId})`);
|
||||
console.log(` - Olm(s): ${associatedOlms.length}`);
|
||||
console.log(` - Current fingerprints: deleted`);
|
||||
console.log(` - Approvals: deleted`);
|
||||
console.log(` - UserClients: deleted`);
|
||||
console.log(` - Snapshots: preserved (not deleted)`);
|
||||
|
||||
process.exit(0);
|
||||
} catch (error) {
|
||||
console.error("Error deleting client:", error);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -7,6 +7,7 @@ import { resetUserSecurityKeys } from "@cli/commands/resetUserSecurityKeys";
|
||||
import { clearExitNodes } from "./commands/clearExitNodes";
|
||||
import { rotateServerSecret } from "./commands/rotateServerSecret";
|
||||
import { clearLicenseKeys } from "./commands/clearLicenseKeys";
|
||||
import { deleteClient } from "./commands/deleteClient";
|
||||
|
||||
yargs(hideBin(process.argv))
|
||||
.scriptName("pangctl")
|
||||
@@ -15,5 +16,6 @@ yargs(hideBin(process.argv))
|
||||
.command(clearExitNodes)
|
||||
.command(rotateServerSecret)
|
||||
.command(clearLicenseKeys)
|
||||
.command(deleteClient)
|
||||
.demandCommand()
|
||||
.help().argv;
|
||||
|
||||
@@ -1,27 +1,30 @@
|
||||
# To see all available options, please visit the docs:
|
||||
# https://docs.pangolin.net/self-host/advanced/config-file
|
||||
|
||||
app:
|
||||
dashboard_url: http://localhost:3002
|
||||
log_level: debug
|
||||
|
||||
domains:
|
||||
domain1:
|
||||
base_domain: example.com
|
||||
|
||||
server:
|
||||
secret: my_secret_key
|
||||
# https://docs.pangolin.net/
|
||||
|
||||
gerbil:
|
||||
base_endpoint: example.com
|
||||
start_port: 51820
|
||||
base_endpoint: "{{.DashboardDomain}}"
|
||||
|
||||
orgs:
|
||||
block_size: 24
|
||||
subnet_group: 100.90.137.0/20
|
||||
app:
|
||||
dashboard_url: "https://{{.DashboardDomain}}"
|
||||
log_level: "info"
|
||||
telemetry:
|
||||
anonymous_usage: true
|
||||
|
||||
domains:
|
||||
domain1:
|
||||
base_domain: "{{.BaseDomain}}"
|
||||
|
||||
server:
|
||||
secret: "{{.Secret}}"
|
||||
cors:
|
||||
origins: ["https://{{.DashboardDomain}}"]
|
||||
methods: ["GET", "POST", "PUT", "DELETE", "PATCH"]
|
||||
allowed_headers: ["X-CSRF-Token", "Content-Type"]
|
||||
credentials: false
|
||||
|
||||
flags:
|
||||
require_email_verification: false
|
||||
disable_signup_without_invite: true
|
||||
disable_user_create_org: true
|
||||
allow_raw_resources: true
|
||||
enable_integration_api: true
|
||||
require_email_verification: false
|
||||
disable_signup_without_invite: true
|
||||
disable_user_create_org: false
|
||||
allow_raw_resources: true
|
||||
|
||||
@@ -21,9 +21,8 @@ http:
|
||||
|
||||
# Next.js router (handles everything except API and WebSocket paths)
|
||||
next-router:
|
||||
rule: "Host(`{{.DashboardDomain}}`)"
|
||||
rule: "Host(`{{.DashboardDomain}}`) && !PathPrefix(`/api/v1`)"
|
||||
service: next-service
|
||||
priority: 10
|
||||
entryPoints:
|
||||
- websecure
|
||||
middlewares:
|
||||
@@ -35,7 +34,6 @@ http:
|
||||
api-router:
|
||||
rule: "Host(`{{.DashboardDomain}}`) && PathPrefix(`/api/v1`)"
|
||||
service: api-service
|
||||
priority: 100
|
||||
entryPoints:
|
||||
- websecure
|
||||
middlewares:
|
||||
@@ -53,3 +51,12 @@ http:
|
||||
loadBalancer:
|
||||
servers:
|
||||
- url: "http://pangolin:3000" # API/WebSocket server
|
||||
|
||||
tcp:
|
||||
serversTransports:
|
||||
pp-transport-v1:
|
||||
proxyProtocol:
|
||||
version: 1
|
||||
pp-transport-v2:
|
||||
proxyProtocol:
|
||||
version: 2
|
||||
|
||||
@@ -3,32 +3,52 @@ api:
|
||||
dashboard: true
|
||||
|
||||
providers:
|
||||
http:
|
||||
endpoint: "http://pangolin:3001/api/v1/traefik-config"
|
||||
pollInterval: "5s"
|
||||
file:
|
||||
directory: "/var/dynamic"
|
||||
watch: true
|
||||
filename: "/etc/traefik/dynamic_config.yml"
|
||||
|
||||
experimental:
|
||||
plugins:
|
||||
badger:
|
||||
moduleName: "github.com/fosrl/badger"
|
||||
version: "v1.3.0"
|
||||
version: "{{.BadgerVersion}}"
|
||||
|
||||
log:
|
||||
level: "DEBUG"
|
||||
level: "INFO"
|
||||
format: "common"
|
||||
maxSize: 100
|
||||
maxBackups: 3
|
||||
maxAge: 3
|
||||
compress: true
|
||||
|
||||
certificatesResolvers:
|
||||
letsencrypt:
|
||||
acme:
|
||||
httpChallenge:
|
||||
entryPoint: web
|
||||
email: "{{.LetsEncryptEmail}}"
|
||||
storage: "/letsencrypt/acme.json"
|
||||
caServer: "https://acme-v02.api.letsencrypt.org/directory"
|
||||
|
||||
entryPoints:
|
||||
web:
|
||||
address: ":80"
|
||||
websecure:
|
||||
address: ":9443"
|
||||
address: ":443"
|
||||
transport:
|
||||
respondingTimeouts:
|
||||
readTimeout: "30m"
|
||||
http:
|
||||
tls:
|
||||
certResolver: "letsencrypt"
|
||||
encodedCharacters:
|
||||
allowEncodedSlash: true
|
||||
allowEncodedQuestionMark: true
|
||||
|
||||
serversTransport:
|
||||
insecureSkipVerify: true
|
||||
|
||||
ping:
|
||||
entryPoint: "web"
|
||||
|
||||
@@ -3,8 +3,8 @@ module installer
|
||||
go 1.24.0
|
||||
|
||||
require (
|
||||
golang.org/x/term v0.38.0
|
||||
golang.org/x/term v0.39.0
|
||||
gopkg.in/yaml.v3 v3.0.1
|
||||
)
|
||||
|
||||
require golang.org/x/sys v0.39.0 // indirect
|
||||
require golang.org/x/sys v0.40.0 // indirect
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
golang.org/x/sys v0.39.0 h1:CvCKL8MeisomCi6qNZ+wbb0DN9E5AATixKsvNtMoMFk=
|
||||
golang.org/x/sys v0.39.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
|
||||
golang.org/x/term v0.38.0 h1:PQ5pkm/rLO6HnxFR7N2lJHOZX6Kez5Y1gDSJla6jo7Q=
|
||||
golang.org/x/term v0.38.0/go.mod h1:bSEAKrOT1W+VSu9TSCMtoGEOUcKxOKgl3LE5QEF/xVg=
|
||||
golang.org/x/sys v0.40.0 h1:DBZZqJ2Rkml6QMQsZywtnjnnGvHza6BTfYFWY9kjEWQ=
|
||||
golang.org/x/sys v0.40.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
|
||||
golang.org/x/term v0.39.0 h1:RclSuaJf32jOqZz74CkPA9qFuVTX7vhLlpfj/IGWlqY=
|
||||
golang.org/x/term v0.39.0/go.mod h1:yxzUCTP/U+FzoxfdKmLaA0RV1WgE0VY7hXBwKtY/4ww=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
|
||||
@@ -1625,6 +1625,8 @@
|
||||
"resourcesTableNoInternalResourcesFound": "Не са намерени вътрешни ресурси.",
|
||||
"resourcesTableDestination": "Дестинация",
|
||||
"resourcesTableAlias": "Псевдоним",
|
||||
"resourcesTableAliasAddress": "Адрес на псевдоним.",
|
||||
"resourcesTableAliasAddressInfo": "Този адрес е част от подсистемата на организацията. Използва се за разрешаване на псевдонимни записи чрез вътрешно DNS разрешаване.",
|
||||
"resourcesTableClients": "Клиенти",
|
||||
"resourcesTableAndOnlyAccessibleInternally": "и са достъпни само вътрешно при свързване с клиент.",
|
||||
"resourcesTableNoTargets": "Без цели",
|
||||
@@ -2489,6 +2491,8 @@
|
||||
"logIn": "Вход",
|
||||
"deviceInformation": "Информация за устройството",
|
||||
"deviceInformationDescription": "Информация за устройството и агента",
|
||||
"deviceSecurity": "Защита на устройството.",
|
||||
"deviceSecurityDescription": "Информация за състоянието на защитата на устройството.",
|
||||
"platform": "Платформа",
|
||||
"macosVersion": "Версия на macOS",
|
||||
"windowsVersion": "Версия на Windows",
|
||||
@@ -2501,6 +2505,16 @@
|
||||
"hostname": "Име на хост",
|
||||
"firstSeen": "Видян за първи път",
|
||||
"lastSeen": "Последно видян",
|
||||
"biometricsEnabled": "Активирани биометрични данни.",
|
||||
"diskEncrypted": "Криптиран диск.",
|
||||
"firewallEnabled": "Активирана защитна стена.",
|
||||
"autoUpdatesEnabled": "Активирани автоматични актуализации.",
|
||||
"tpmAvailable": "TPM е на разположение.",
|
||||
"macosSipEnabled": "Protection на системната цялост (SIP).",
|
||||
"macosGatekeeperEnabled": "Gatekeeper.",
|
||||
"macosFirewallStealthMode": "Скрит режим на защитната стена.",
|
||||
"linuxAppArmorEnabled": "AppArmor.",
|
||||
"linuxSELinuxEnabled": "SELinux.",
|
||||
"deviceSettingsDescription": "Разгледайте информация и настройки на устройството",
|
||||
"devicePendingApprovalDescription": "Това устройство чака одобрение",
|
||||
"deviceBlockedDescription": "Това устройство е в момента блокирано. Няма да може да се свърже с никакви ресурси, освен ако не бъде деблокирано.",
|
||||
|
||||
@@ -1625,6 +1625,8 @@
|
||||
"resourcesTableNoInternalResourcesFound": "Nebyly nalezeny žádné vnitřní zdroje.",
|
||||
"resourcesTableDestination": "Místo určení",
|
||||
"resourcesTableAlias": "Alias",
|
||||
"resourcesTableAliasAddress": "Adresa aliasu",
|
||||
"resourcesTableAliasAddressInfo": "Tato adresa je součástí subsítě veřejných služeb organizace. Používá se k řešení záznamů aliasů pomocí interního rozlišení DNS.",
|
||||
"resourcesTableClients": "Klienti",
|
||||
"resourcesTableAndOnlyAccessibleInternally": "a jsou interně přístupné pouze v případě, že jsou propojeni s klientem.",
|
||||
"resourcesTableNoTargets": "Žádné cíle",
|
||||
@@ -2489,6 +2491,8 @@
|
||||
"logIn": "Přihlásit se",
|
||||
"deviceInformation": "Informace o zařízení",
|
||||
"deviceInformationDescription": "Informace o zařízení a agentovi",
|
||||
"deviceSecurity": "Zabezpečení zařízení",
|
||||
"deviceSecurityDescription": "Informace o bezpečnostní pozici zařízení",
|
||||
"platform": "Platforma",
|
||||
"macosVersion": "macOS verze",
|
||||
"windowsVersion": "Verze Windows",
|
||||
@@ -2501,6 +2505,16 @@
|
||||
"hostname": "Hostname",
|
||||
"firstSeen": "První vidění",
|
||||
"lastSeen": "Naposledy viděno",
|
||||
"biometricsEnabled": "Biometrie povolena",
|
||||
"diskEncrypted": "Šifrovaný disk",
|
||||
"firewallEnabled": "Firewall povolen",
|
||||
"autoUpdatesEnabled": "Automatické aktualizace povoleny",
|
||||
"tpmAvailable": "TPM k dispozici",
|
||||
"macosSipEnabled": "Ochrana systémové integrity (SIP)",
|
||||
"macosGatekeeperEnabled": "Gatekeeper",
|
||||
"macosFirewallStealthMode": "Režim neviditelnosti firewallu",
|
||||
"linuxAppArmorEnabled": "Pancíř aplikace",
|
||||
"linuxSELinuxEnabled": "SELinux",
|
||||
"deviceSettingsDescription": "Zobrazit informace o zařízení a nastavení",
|
||||
"devicePendingApprovalDescription": "Toto zařízení čeká na schválení",
|
||||
"deviceBlockedDescription": "Toto zařízení je momentálně blokováno. Nebude se moci připojit k žádným zdrojům, dokud nebude odblokováno.",
|
||||
|
||||
@@ -1625,6 +1625,8 @@
|
||||
"resourcesTableNoInternalResourcesFound": "Keine internen Ressourcen gefunden.",
|
||||
"resourcesTableDestination": "Ziel",
|
||||
"resourcesTableAlias": "Alias",
|
||||
"resourcesTableAliasAddress": "Alias-Adresse",
|
||||
"resourcesTableAliasAddressInfo": "Diese Adresse ist Teil des Utility-Subnetzes der Organisation. Sie wird verwendet, um Alias-Einträge mit interner DNS-Auflösung aufzulösen.",
|
||||
"resourcesTableClients": "Clients",
|
||||
"resourcesTableAndOnlyAccessibleInternally": "und sind nur intern zugänglich, wenn mit einem Client verbunden.",
|
||||
"resourcesTableNoTargets": "Keine Ziele",
|
||||
@@ -2489,6 +2491,8 @@
|
||||
"logIn": "Anmelden",
|
||||
"deviceInformation": "Geräteinformationen",
|
||||
"deviceInformationDescription": "Informationen über das Gerät und den Agent",
|
||||
"deviceSecurity": "Gerätesicherheit",
|
||||
"deviceSecurityDescription": "Informationen zur Gerätesicherheit",
|
||||
"platform": "Plattform",
|
||||
"macosVersion": "macOS-Version",
|
||||
"windowsVersion": "Windows-Version",
|
||||
@@ -2501,6 +2505,16 @@
|
||||
"hostname": "Hostname",
|
||||
"firstSeen": "Erster Blick",
|
||||
"lastSeen": "Zuletzt gesehen",
|
||||
"biometricsEnabled": "Biometrie aktiviert",
|
||||
"diskEncrypted": "Festplatte verschlüsselt",
|
||||
"firewallEnabled": "Firewall aktiviert",
|
||||
"autoUpdatesEnabled": "Automatische Updates aktiviert",
|
||||
"tpmAvailable": "TPM verfügbar",
|
||||
"macosSipEnabled": "Schutz der Systemintegrität (SIP)",
|
||||
"macosGatekeeperEnabled": "Gatekeeper",
|
||||
"macosFirewallStealthMode": "Firewall Stealth-Modus",
|
||||
"linuxAppArmorEnabled": "AppRüstung",
|
||||
"linuxSELinuxEnabled": "SELinux",
|
||||
"deviceSettingsDescription": "Geräteinformationen und -einstellungen anzeigen",
|
||||
"devicePendingApprovalDescription": "Dieses Gerät wartet auf Freigabe",
|
||||
"deviceBlockedDescription": "Dieses Gerät ist derzeit gesperrt. Es kann keine Verbindung zu anderen Ressourcen herstellen, es sei denn, es entsperrt.",
|
||||
|
||||
@@ -1625,6 +1625,8 @@
|
||||
"resourcesTableNoInternalResourcesFound": "No internal resources found.",
|
||||
"resourcesTableDestination": "Destination",
|
||||
"resourcesTableAlias": "Alias",
|
||||
"resourcesTableAliasAddress": "Alias Address",
|
||||
"resourcesTableAliasAddressInfo": "This address is part of the organization's utility subnet. It's used to resolve alias records using internal DNS resolution.",
|
||||
"resourcesTableClients": "Clients",
|
||||
"resourcesTableAndOnlyAccessibleInternally": "and are only accessible internally when connected with a client.",
|
||||
"resourcesTableNoTargets": "No targets",
|
||||
@@ -2508,7 +2510,7 @@
|
||||
"firewallEnabled": "Firewall Enabled",
|
||||
"autoUpdatesEnabled": "Auto Updates Enabled",
|
||||
"tpmAvailable": "TPM Available",
|
||||
"windowsDefenderEnabled": "Windows Defender Enabled",
|
||||
"windowsAntivirusEnabled": "Antivirus Enabled",
|
||||
"macosSipEnabled": "System Integrity Protection (SIP)",
|
||||
"macosGatekeeperEnabled": "Gatekeeper",
|
||||
"macosFirewallStealthMode": "Firewall Stealth Mode",
|
||||
|
||||
@@ -1625,6 +1625,8 @@
|
||||
"resourcesTableNoInternalResourcesFound": "No se encontraron recursos internos.",
|
||||
"resourcesTableDestination": "Destino",
|
||||
"resourcesTableAlias": "Alias",
|
||||
"resourcesTableAliasAddress": "Dirección del alias",
|
||||
"resourcesTableAliasAddressInfo": "Esta dirección es parte de la subred de utilidad de la organización. Se utiliza para resolver registros de alias usando resolución DNS interna.",
|
||||
"resourcesTableClients": "Clientes",
|
||||
"resourcesTableAndOnlyAccessibleInternally": "y solo son accesibles internamente cuando se conectan con un cliente.",
|
||||
"resourcesTableNoTargets": "Sin objetivos",
|
||||
@@ -2489,6 +2491,8 @@
|
||||
"logIn": "Iniciar sesión",
|
||||
"deviceInformation": "Información del dispositivo",
|
||||
"deviceInformationDescription": "Información sobre el dispositivo y el agente",
|
||||
"deviceSecurity": "Seguridad del dispositivo",
|
||||
"deviceSecurityDescription": "Información de postura de seguridad del dispositivo",
|
||||
"platform": "Plataforma",
|
||||
"macosVersion": "versión macOS",
|
||||
"windowsVersion": "Versión de Windows",
|
||||
@@ -2501,6 +2505,16 @@
|
||||
"hostname": "Hostname",
|
||||
"firstSeen": "Primer detectado",
|
||||
"lastSeen": "Último Visto",
|
||||
"biometricsEnabled": "Biometría habilitada",
|
||||
"diskEncrypted": "Disco cifrado",
|
||||
"firewallEnabled": "Cortafuegos activado",
|
||||
"autoUpdatesEnabled": "Actualizaciones automáticas habilitadas",
|
||||
"tpmAvailable": "TPM disponible",
|
||||
"macosSipEnabled": "Protección de integridad del sistema (SIP)",
|
||||
"macosGatekeeperEnabled": "Gatekeeper",
|
||||
"macosFirewallStealthMode": "Modo Sigilo Firewall",
|
||||
"linuxAppArmorEnabled": "AppArmor",
|
||||
"linuxSELinuxEnabled": "SELinux",
|
||||
"deviceSettingsDescription": "Ver información y ajustes del dispositivo",
|
||||
"devicePendingApprovalDescription": "Este dispositivo está esperando su aprobación",
|
||||
"deviceBlockedDescription": "Este dispositivo está actualmente bloqueado. No podrá conectarse a ningún recurso a menos que sea desbloqueado.",
|
||||
|
||||
@@ -1625,6 +1625,8 @@
|
||||
"resourcesTableNoInternalResourcesFound": "Aucune ressource interne trouvée.",
|
||||
"resourcesTableDestination": "Destination",
|
||||
"resourcesTableAlias": "Alias",
|
||||
"resourcesTableAliasAddress": "Adresse de l'alias",
|
||||
"resourcesTableAliasAddressInfo": "Cette adresse fait partie du sous-réseau utilitaire de l'organisation. Elle est utilisée pour résoudre les enregistrements d'alias en utilisant une résolution DNS interne.",
|
||||
"resourcesTableClients": "Clients",
|
||||
"resourcesTableAndOnlyAccessibleInternally": "et sont uniquement accessibles en interne lorsqu'elles sont connectées avec un client.",
|
||||
"resourcesTableNoTargets": "Aucune cible",
|
||||
@@ -2489,6 +2491,8 @@
|
||||
"logIn": "Se connecter",
|
||||
"deviceInformation": "Informations sur l'appareil",
|
||||
"deviceInformationDescription": "Informations sur l'appareil et l'agent",
|
||||
"deviceSecurity": "Sécurité de l'appareil",
|
||||
"deviceSecurityDescription": "Informations sur la posture de sécurité de l'appareil",
|
||||
"platform": "Plateforme",
|
||||
"macosVersion": "Version macOS",
|
||||
"windowsVersion": "Version de Windows",
|
||||
@@ -2501,6 +2505,16 @@
|
||||
"hostname": "Hostname",
|
||||
"firstSeen": "Première vue",
|
||||
"lastSeen": "Dernière vue",
|
||||
"biometricsEnabled": "biométrique activée",
|
||||
"diskEncrypted": "Disque chiffré",
|
||||
"firewallEnabled": "Pare-feu activé",
|
||||
"autoUpdatesEnabled": "Mises à jour automatiques activées",
|
||||
"tpmAvailable": "TPM disponible",
|
||||
"macosSipEnabled": "Protection contre l'intégrité du système (SIP)",
|
||||
"macosGatekeeperEnabled": "Gatekeeper",
|
||||
"macosFirewallStealthMode": "Mode furtif du pare-feu",
|
||||
"linuxAppArmorEnabled": "Armure d'application",
|
||||
"linuxSELinuxEnabled": "SELinux",
|
||||
"deviceSettingsDescription": "Afficher les informations et les paramètres de l'appareil",
|
||||
"devicePendingApprovalDescription": "Cet appareil est en attente d'approbation",
|
||||
"deviceBlockedDescription": "Cet appareil est actuellement bloqué. Il ne pourra se connecter à aucune ressource à moins d'être débloqué.",
|
||||
|
||||
@@ -1625,6 +1625,8 @@
|
||||
"resourcesTableNoInternalResourcesFound": "Nessuna risorsa interna trovata.",
|
||||
"resourcesTableDestination": "Destinazione",
|
||||
"resourcesTableAlias": "Alias",
|
||||
"resourcesTableAliasAddress": "Indirizzo Alias",
|
||||
"resourcesTableAliasAddressInfo": "Questo indirizzo fa parte della subnet di utilità dell'organizzazione. È usato per risolvere i record alias usando la risoluzione DNS interna.",
|
||||
"resourcesTableClients": "Client",
|
||||
"resourcesTableAndOnlyAccessibleInternally": "e sono accessibili solo internamente quando connessi con un client.",
|
||||
"resourcesTableNoTargets": "Nessun obiettivo",
|
||||
@@ -2489,6 +2491,8 @@
|
||||
"logIn": "Log In",
|
||||
"deviceInformation": "Informazioni Sul Dispositivo",
|
||||
"deviceInformationDescription": "Informazioni sul dispositivo e sull'agente",
|
||||
"deviceSecurity": "Sicurezza Del Dispositivo",
|
||||
"deviceSecurityDescription": "Informazioni postura sicurezza dispositivo",
|
||||
"platform": "Piattaforma",
|
||||
"macosVersion": "versione macOS",
|
||||
"windowsVersion": "Versione Windows",
|
||||
@@ -2501,6 +2505,16 @@
|
||||
"hostname": "Hostname",
|
||||
"firstSeen": "Prima Visto",
|
||||
"lastSeen": "Visto L'Ultima",
|
||||
"biometricsEnabled": "Biometria Abilitata",
|
||||
"diskEncrypted": "Cifratura Del Disco",
|
||||
"firewallEnabled": "Firewall Abilitato",
|
||||
"autoUpdatesEnabled": "Aggiornamenti Automatici Abilitati",
|
||||
"tpmAvailable": "TPM Disponibile",
|
||||
"macosSipEnabled": "Protezione Dell'Integrità Del Sistema (Sip)",
|
||||
"macosGatekeeperEnabled": "Gatekeeper",
|
||||
"macosFirewallStealthMode": "Modo Furtivo Del Firewall",
|
||||
"linuxAppArmorEnabled": "AppArmor",
|
||||
"linuxSELinuxEnabled": "SELinux",
|
||||
"deviceSettingsDescription": "Visualizza informazioni e impostazioni del dispositivo",
|
||||
"devicePendingApprovalDescription": "Questo dispositivo è in attesa di approvazione",
|
||||
"deviceBlockedDescription": "Questo dispositivo è attualmente bloccato. Non sarà in grado di connettersi a nessuna risorsa a meno che non sia sbloccato.",
|
||||
|
||||
@@ -1625,6 +1625,8 @@
|
||||
"resourcesTableNoInternalResourcesFound": "내부 리소스를 찾을 수 없습니다.",
|
||||
"resourcesTableDestination": "대상지",
|
||||
"resourcesTableAlias": "별칭",
|
||||
"resourcesTableAliasAddress": "별칭 주소",
|
||||
"resourcesTableAliasAddressInfo": "이 주소는 조직의 유틸리티 서브넷의 일부로, 내부 DNS 해석을 사용하여 별칭 레코드를 해석하는 데 사용됩니다.",
|
||||
"resourcesTableClients": "클라이언트",
|
||||
"resourcesTableAndOnlyAccessibleInternally": "클라이언트와 연결되었을 때만 내부적으로 접근 가능합니다.",
|
||||
"resourcesTableNoTargets": "대상 없음",
|
||||
@@ -2489,6 +2491,8 @@
|
||||
"logIn": "로그인",
|
||||
"deviceInformation": "장치 정보",
|
||||
"deviceInformationDescription": "장치와 에이전트 정보",
|
||||
"deviceSecurity": "디바이스 보안",
|
||||
"deviceSecurityDescription": "디바이스 보안 상태 정보",
|
||||
"platform": "플랫폼",
|
||||
"macosVersion": "macOS 버전",
|
||||
"windowsVersion": "Windows 버전",
|
||||
@@ -2501,6 +2505,16 @@
|
||||
"hostname": "호스트 이름",
|
||||
"firstSeen": "처음 발견됨",
|
||||
"lastSeen": "마지막으로 발견됨",
|
||||
"biometricsEnabled": "생체 인식 활성화",
|
||||
"diskEncrypted": "디스크 암호화됨",
|
||||
"firewallEnabled": "방화벽 활성화",
|
||||
"autoUpdatesEnabled": "자동 업데이트 활성화",
|
||||
"tpmAvailable": "TPM 사용 가능",
|
||||
"macosSipEnabled": "시스템 무결성 보호 (SIP)",
|
||||
"macosGatekeeperEnabled": "Gatekeeper",
|
||||
"macosFirewallStealthMode": "방화벽 스텔스 모드",
|
||||
"linuxAppArmorEnabled": "AppArmor",
|
||||
"linuxSELinuxEnabled": "SELinux",
|
||||
"deviceSettingsDescription": "장치 정보 및 설정 보기",
|
||||
"devicePendingApprovalDescription": "이 장치는 승인을 기다리고 있습니다.",
|
||||
"deviceBlockedDescription": "이 장치는 현재 차단되었습니다. 차단이 해제되지 않으면 리소스에 연결할 수 없습니다.",
|
||||
|
||||
@@ -1625,6 +1625,8 @@
|
||||
"resourcesTableNoInternalResourcesFound": "Ingen interne ressurser funnet.",
|
||||
"resourcesTableDestination": "Destinasjon",
|
||||
"resourcesTableAlias": "Alias",
|
||||
"resourcesTableAliasAddress": "Alias adresse",
|
||||
"resourcesTableAliasAddressInfo": "Denne adressen er en del av organisasjonens undernettverk. Den brukes til å løse aliasposter ved hjelp av intern DNS-oppløsning.",
|
||||
"resourcesTableClients": "Klienter",
|
||||
"resourcesTableAndOnlyAccessibleInternally": "og er kun tilgjengelig internt når de er koblet til med en klient.",
|
||||
"resourcesTableNoTargets": "Ingen mål",
|
||||
@@ -2489,6 +2491,8 @@
|
||||
"logIn": "Logg inn",
|
||||
"deviceInformation": "Enhetens informasjon",
|
||||
"deviceInformationDescription": "Informasjon om enheten og agenten",
|
||||
"deviceSecurity": "Enhetens sikkerhet",
|
||||
"deviceSecurityDescription": "Sikkerhetsstillings informasjon om utstyr",
|
||||
"platform": "Plattform",
|
||||
"macosVersion": "macOS versjon",
|
||||
"windowsVersion": "Windows versjon",
|
||||
@@ -2501,6 +2505,16 @@
|
||||
"hostname": "Hostname",
|
||||
"firstSeen": "Først sett",
|
||||
"lastSeen": "Sist sett",
|
||||
"biometricsEnabled": "Biometri aktivert",
|
||||
"diskEncrypted": "Disk kryptert",
|
||||
"firewallEnabled": "Brannmur aktivert",
|
||||
"autoUpdatesEnabled": "Automatiske oppdateringer aktivert",
|
||||
"tpmAvailable": "TPM tilgjengelig",
|
||||
"macosSipEnabled": "System Integritetsbeskyttelse (SIP)",
|
||||
"macosGatekeeperEnabled": "Gatekeeper",
|
||||
"macosFirewallStealthMode": "Brannmur Usynlig Modus",
|
||||
"linuxAppArmorEnabled": "Rustning",
|
||||
"linuxSELinuxEnabled": "SELinux",
|
||||
"deviceSettingsDescription": "Vis enhetsinformasjon og innstillinger",
|
||||
"devicePendingApprovalDescription": "Denne enheten venter på godkjenning",
|
||||
"deviceBlockedDescription": "Denne enheten er blokkert. Det kan ikke kobles til noen ressurser med mindre de ikke blir blokkert.",
|
||||
|
||||
@@ -1625,6 +1625,8 @@
|
||||
"resourcesTableNoInternalResourcesFound": "Geen interne bronnen gevonden.",
|
||||
"resourcesTableDestination": "Bestemming",
|
||||
"resourcesTableAlias": "Alias",
|
||||
"resourcesTableAliasAddress": "Alias adres",
|
||||
"resourcesTableAliasAddressInfo": "Dit adres is onderdeel van het hulpprogramma subnet van de organisatie. Het wordt gebruikt om aliasrecords op te lossen met behulp van interne DNS-resolutie.",
|
||||
"resourcesTableClients": "Clienten",
|
||||
"resourcesTableAndOnlyAccessibleInternally": "en zijn alleen intern toegankelijk wanneer verbonden met een client.",
|
||||
"resourcesTableNoTargets": "Geen doelen",
|
||||
@@ -2489,6 +2491,8 @@
|
||||
"logIn": "Log in",
|
||||
"deviceInformation": "Apparaat informatie",
|
||||
"deviceInformationDescription": "Informatie over het apparaat en de agent",
|
||||
"deviceSecurity": "Apparaat beveiliging",
|
||||
"deviceSecurityDescription": "Apparaat beveiligingsinformatie",
|
||||
"platform": "Platform",
|
||||
"macosVersion": "macOS versie",
|
||||
"windowsVersion": "Windows versie",
|
||||
@@ -2501,6 +2505,16 @@
|
||||
"hostname": "Hostname",
|
||||
"firstSeen": "Eerst gezien",
|
||||
"lastSeen": "Laatst gezien op",
|
||||
"biometricsEnabled": "Biometrie ingeschakeld",
|
||||
"diskEncrypted": "Schijf versleuteld",
|
||||
"firewallEnabled": "Firewall ingeschakeld",
|
||||
"autoUpdatesEnabled": "Auto Updates Ingeschakeld",
|
||||
"tpmAvailable": "TPM beschikbaar",
|
||||
"macosSipEnabled": "Systeemintegriteitsbescherming (SIP)",
|
||||
"macosGatekeeperEnabled": "Gatekeeper",
|
||||
"macosFirewallStealthMode": "Firewall Verberg Modus",
|
||||
"linuxAppArmorEnabled": "Appharnas",
|
||||
"linuxSELinuxEnabled": "SELinux",
|
||||
"deviceSettingsDescription": "Apparaatinformatie en -instellingen bekijken",
|
||||
"devicePendingApprovalDescription": "Dit apparaat wacht op goedkeuring",
|
||||
"deviceBlockedDescription": "Dit apparaat is momenteel geblokkeerd. Het kan geen verbinding maken met bronnen tenzij het wordt gedeblokkeerd.",
|
||||
|
||||
@@ -1625,6 +1625,8 @@
|
||||
"resourcesTableNoInternalResourcesFound": "Nie znaleziono wewnętrznych zasobów.",
|
||||
"resourcesTableDestination": "Miejsce docelowe",
|
||||
"resourcesTableAlias": "Alias",
|
||||
"resourcesTableAliasAddress": "Adres aliasu",
|
||||
"resourcesTableAliasAddressInfo": "Ten adres jest częścią podsieci użyteczności organizacji. Jest używany do rozwiązywania rekordów aliasu przy użyciu wewnętrznej rozdzielczości DNS.",
|
||||
"resourcesTableClients": "Klientami",
|
||||
"resourcesTableAndOnlyAccessibleInternally": "i są dostępne tylko wewnętrznie po połączeniu z klientem.",
|
||||
"resourcesTableNoTargets": "Brak celów",
|
||||
@@ -2489,6 +2491,8 @@
|
||||
"logIn": "Zaloguj się",
|
||||
"deviceInformation": "Informacje o urządzeniu",
|
||||
"deviceInformationDescription": "Informacje o urządzeniu i agentach",
|
||||
"deviceSecurity": "Bezpieczeństwo urządzenia",
|
||||
"deviceSecurityDescription": "Informacje o bezpieczeństwie urządzenia",
|
||||
"platform": "Platforma",
|
||||
"macosVersion": "Wersja macOS",
|
||||
"windowsVersion": "Wersja Windows",
|
||||
@@ -2501,6 +2505,16 @@
|
||||
"hostname": "Hostname",
|
||||
"firstSeen": "Widziany po raz pierwszy",
|
||||
"lastSeen": "Ostatnio widziane",
|
||||
"biometricsEnabled": "Biometria włączona",
|
||||
"diskEncrypted": "Dysk zaszyfrowany",
|
||||
"firewallEnabled": "Zapora włączona",
|
||||
"autoUpdatesEnabled": "Automatyczne aktualizacje włączone",
|
||||
"tpmAvailable": "TPM dostępne",
|
||||
"macosSipEnabled": "Ochrona integralności systemu (SIP)",
|
||||
"macosGatekeeperEnabled": "Gatekeeper",
|
||||
"macosFirewallStealthMode": "Tryb Stealth zapory",
|
||||
"linuxAppArmorEnabled": "Zbroja aplikacji",
|
||||
"linuxSELinuxEnabled": "SELinux",
|
||||
"deviceSettingsDescription": "Wyświetl informacje o urządzeniu i ustawienia",
|
||||
"devicePendingApprovalDescription": "To urządzenie czeka na zatwierdzenie",
|
||||
"deviceBlockedDescription": "To urządzenie jest obecnie zablokowane. Nie będzie można połączyć się z żadnymi zasobami, chyba że zostanie odblokowane.",
|
||||
|
||||
@@ -1625,6 +1625,8 @@
|
||||
"resourcesTableNoInternalResourcesFound": "Nenhum recurso interno encontrado.",
|
||||
"resourcesTableDestination": "Destino",
|
||||
"resourcesTableAlias": "Alias",
|
||||
"resourcesTableAliasAddress": "Endereço do Pseudônimo",
|
||||
"resourcesTableAliasAddressInfo": "Este endereço faz parte da sub-rede de utilitários da organização. É usado para resolver registros de alias usando resolução de DNS interno.",
|
||||
"resourcesTableClients": "Clientes",
|
||||
"resourcesTableAndOnlyAccessibleInternally": "e são acessíveis apenas internamente quando conectados com um cliente.",
|
||||
"resourcesTableNoTargets": "Nenhum alvo",
|
||||
@@ -2489,6 +2491,8 @@
|
||||
"logIn": "Iniciar sessão",
|
||||
"deviceInformation": "Informações do dispositivo",
|
||||
"deviceInformationDescription": "Informações sobre o dispositivo e o agente",
|
||||
"deviceSecurity": "Segurança do dispositivo",
|
||||
"deviceSecurityDescription": "Informações sobre postagem de segurança",
|
||||
"platform": "Plataforma",
|
||||
"macosVersion": "Versão do macOS",
|
||||
"windowsVersion": "Versão do Windows",
|
||||
@@ -2501,6 +2505,16 @@
|
||||
"hostname": "Hostname",
|
||||
"firstSeen": "Visto primeiro",
|
||||
"lastSeen": "Visto por último",
|
||||
"biometricsEnabled": "Biometria habilitada",
|
||||
"diskEncrypted": "Disco criptografado",
|
||||
"firewallEnabled": "Firewall habilitado",
|
||||
"autoUpdatesEnabled": "Atualizações Automáticas Habilitadas",
|
||||
"tpmAvailable": "TPM disponível",
|
||||
"macosSipEnabled": "Proteção da Integridade do Sistema (SIP)",
|
||||
"macosGatekeeperEnabled": "Gatekeeper",
|
||||
"macosFirewallStealthMode": "Modo Furtivo do Firewall",
|
||||
"linuxAppArmorEnabled": "AppArmor",
|
||||
"linuxSELinuxEnabled": "SELinux",
|
||||
"deviceSettingsDescription": "Ver informações e configurações do dispositivo",
|
||||
"devicePendingApprovalDescription": "Este dispositivo está aguardando aprovação",
|
||||
"deviceBlockedDescription": "Este dispositivo está bloqueado no momento. Ele não será capaz de se conectar a qualquer recurso a menos que seja desbloqueado.",
|
||||
|
||||
@@ -1625,6 +1625,8 @@
|
||||
"resourcesTableNoInternalResourcesFound": "Внутренних ресурсов не найдено.",
|
||||
"resourcesTableDestination": "Пункт назначения",
|
||||
"resourcesTableAlias": "Alias",
|
||||
"resourcesTableAliasAddress": "Псевдоним адреса",
|
||||
"resourcesTableAliasAddressInfo": "Этот адрес является частью вспомогательной подсети организации. Он используется для разрешения псевдонимов с использованием внутреннего разрешения DNS.",
|
||||
"resourcesTableClients": "Клиенты",
|
||||
"resourcesTableAndOnlyAccessibleInternally": "и доступны только внутренне при подключении с клиентом.",
|
||||
"resourcesTableNoTargets": "Нет ярлыков",
|
||||
@@ -2489,6 +2491,8 @@
|
||||
"logIn": "Войти",
|
||||
"deviceInformation": "Информация об устройстве",
|
||||
"deviceInformationDescription": "Информация о устройстве и агенте",
|
||||
"deviceSecurity": "Безопасность устройства",
|
||||
"deviceSecurityDescription": "Информация о позе безопасности устройства",
|
||||
"platform": "Платформа",
|
||||
"macosVersion": "Версия macOS",
|
||||
"windowsVersion": "Версия Windows",
|
||||
@@ -2501,6 +2505,16 @@
|
||||
"hostname": "Hostname",
|
||||
"firstSeen": "Первый раз виден",
|
||||
"lastSeen": "Последнее посещение",
|
||||
"biometricsEnabled": "Включены биометрические данные",
|
||||
"diskEncrypted": "Диск зашифрован",
|
||||
"firewallEnabled": "Брандмауэр включен",
|
||||
"autoUpdatesEnabled": "Автоматические обновления включены",
|
||||
"tpmAvailable": "Доступно TPM",
|
||||
"macosSipEnabled": "Защита целостности системы (SIP)",
|
||||
"macosGatekeeperEnabled": "Gatekeeper",
|
||||
"macosFirewallStealthMode": "Стилс-режим брандмауэра",
|
||||
"linuxAppArmorEnabled": "Броня",
|
||||
"linuxSELinuxEnabled": "SELinux",
|
||||
"deviceSettingsDescription": "Просмотр информации и настроек устройства",
|
||||
"devicePendingApprovalDescription": "Это устройство ожидает одобрения",
|
||||
"deviceBlockedDescription": "Это устройство заблокировано. Оно не сможет подключаться к ресурсам, если не разблокировано.",
|
||||
|
||||
@@ -1625,6 +1625,8 @@
|
||||
"resourcesTableNoInternalResourcesFound": "Hiçbir dahili kaynak bulunamadı.",
|
||||
"resourcesTableDestination": "Hedef",
|
||||
"resourcesTableAlias": "Takma Ad",
|
||||
"resourcesTableAliasAddress": "Alias Adresi",
|
||||
"resourcesTableAliasAddressInfo": "Bu adres, kuruluşun yardımcı ağ alt bantının bir parçasıdır. Alias kayıtlarını çözümlemek için dahili DNS çözümlemesi kullanılır.",
|
||||
"resourcesTableClients": "İstemciler",
|
||||
"resourcesTableAndOnlyAccessibleInternally": "veyalnızca bir istemci ile bağlandığında dahili olarak erişilebilir.",
|
||||
"resourcesTableNoTargets": "Hedef yok",
|
||||
@@ -2489,6 +2491,8 @@
|
||||
"logIn": "Giriş Yap",
|
||||
"deviceInformation": "Cihaz Bilgisi",
|
||||
"deviceInformationDescription": "Cihaz ve temsilci hakkında bilgi",
|
||||
"deviceSecurity": "Cihaz Güvenliği",
|
||||
"deviceSecurityDescription": "Cihaz güvenliği durumu bilgisi",
|
||||
"platform": "Platform",
|
||||
"macosVersion": "macOS Sürümü",
|
||||
"windowsVersion": "Windows Sürümü",
|
||||
@@ -2501,6 +2505,16 @@
|
||||
"hostname": "Ana Makine Adı",
|
||||
"firstSeen": "İlk Görüldü",
|
||||
"lastSeen": "Son Görüldü",
|
||||
"biometricsEnabled": "Biyometri Etkin",
|
||||
"diskEncrypted": "Disk Şifrelenmiş",
|
||||
"firewallEnabled": "Güvenlik Duvarı Etkin",
|
||||
"autoUpdatesEnabled": "Otomatik Güncellemeler Etkin",
|
||||
"tpmAvailable": "TPM Mevcut",
|
||||
"macosSipEnabled": "Sistem Bütünlüğü Koruması (SIP)",
|
||||
"macosGatekeeperEnabled": "Gatekeeper",
|
||||
"macosFirewallStealthMode": "Güvenlik Duvarı Gizlilik Modu",
|
||||
"linuxAppArmorEnabled": "AppArmor",
|
||||
"linuxSELinuxEnabled": "SELinux",
|
||||
"deviceSettingsDescription": "Cihaz bilgilerini ve ayarlarını görüntüleyin",
|
||||
"devicePendingApprovalDescription": "Bu cihaz onay bekliyor",
|
||||
"deviceBlockedDescription": "Bu cihaz şu anda engellidir. Engeli kaldırılmadığı sürece hiçbir kaynağa bağlanamayacaktır.",
|
||||
|
||||
@@ -1625,6 +1625,8 @@
|
||||
"resourcesTableNoInternalResourcesFound": "未找到内部资源。",
|
||||
"resourcesTableDestination": "目标",
|
||||
"resourcesTableAlias": "Alias",
|
||||
"resourcesTableAliasAddress": "别名地址",
|
||||
"resourcesTableAliasAddressInfo": "此地址是组织实用子网的一部分。它用来使用内部DNS解析来解析别名记录。",
|
||||
"resourcesTableClients": "客户端",
|
||||
"resourcesTableAndOnlyAccessibleInternally": "且仅在与客户端连接时可内部访问。",
|
||||
"resourcesTableNoTargets": "没有目标",
|
||||
@@ -2489,6 +2491,8 @@
|
||||
"logIn": "登录",
|
||||
"deviceInformation": "设备信息",
|
||||
"deviceInformationDescription": "关于设备和代理的信息",
|
||||
"deviceSecurity": "设备安全",
|
||||
"deviceSecurityDescription": "设备安全态势信息",
|
||||
"platform": "平台",
|
||||
"macosVersion": "macOS 版本",
|
||||
"windowsVersion": "Windows 版本",
|
||||
@@ -2501,6 +2505,16 @@
|
||||
"hostname": "Hostname",
|
||||
"firstSeen": "第一次查看",
|
||||
"lastSeen": "上次查看时间",
|
||||
"biometricsEnabled": "生物计已启用",
|
||||
"diskEncrypted": "磁盘加密",
|
||||
"firewallEnabled": "防火墙已启用",
|
||||
"autoUpdatesEnabled": "启用自动更新",
|
||||
"tpmAvailable": "TPM 可用",
|
||||
"macosSipEnabled": "系统完整性保护 (SIP)",
|
||||
"macosGatekeeperEnabled": "Gatekeeper",
|
||||
"macosFirewallStealthMode": "防火墙隐形模式",
|
||||
"linuxAppArmorEnabled": "AppArmor",
|
||||
"linuxSELinuxEnabled": "SELinux",
|
||||
"deviceSettingsDescription": "查看设备信息和设置",
|
||||
"devicePendingApprovalDescription": "此设备正在等待批准",
|
||||
"deviceBlockedDescription": "此设备目前已被屏蔽。除非解除屏蔽,否则无法连接到任何资源。",
|
||||
|
||||
4487
messages/zh-TW.json
4487
messages/zh-TW.json
File diff suppressed because it is too large
Load Diff
1776
package-lock.json
generated
1776
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
30
package.json
30
package.json
@@ -33,9 +33,9 @@
|
||||
"format": "prettier --write ."
|
||||
},
|
||||
"dependencies": {
|
||||
"@asteasolutions/zod-to-openapi": "8.2.0",
|
||||
"@aws-sdk/client-s3": "3.955.0",
|
||||
"@faker-js/faker": "10.1.0",
|
||||
"@asteasolutions/zod-to-openapi": "8.4.0",
|
||||
"@aws-sdk/client-s3": "3.971.0",
|
||||
"@faker-js/faker": "10.2.0",
|
||||
"@headlessui/react": "2.2.9",
|
||||
"@hookform/resolvers": "5.2.2",
|
||||
"@monaco-editor/react": "4.7.0",
|
||||
@@ -92,7 +92,7 @@
|
||||
"http-errors": "2.0.1",
|
||||
"i": "0.3.7",
|
||||
"input-otp": "1.4.2",
|
||||
"ioredis": "5.8.2",
|
||||
"ioredis": "5.9.2",
|
||||
"jmespath": "0.16.0",
|
||||
"js-yaml": "4.1.1",
|
||||
"jsonwebtoken": "9.0.3",
|
||||
@@ -100,7 +100,7 @@
|
||||
"maxmind": "5.0.1",
|
||||
"moment": "2.30.1",
|
||||
"next": "15.5.9",
|
||||
"next-intl": "4.6.1",
|
||||
"next-intl": "4.7.0",
|
||||
"next-themes": "0.4.6",
|
||||
"nextjs-toploader": "3.9.17",
|
||||
"node-cache": "5.1.2",
|
||||
@@ -109,21 +109,21 @@
|
||||
"npm": "11.7.0",
|
||||
"nprogress": "0.2.0",
|
||||
"oslo": "1.2.1",
|
||||
"pg": "8.16.3",
|
||||
"posthog-node": "5.17.4",
|
||||
"pg": "8.17.1",
|
||||
"posthog-node": "5.23.0",
|
||||
"qrcode.react": "4.2.0",
|
||||
"react": "19.2.3",
|
||||
"react-day-picker": "9.13.0",
|
||||
"react-dom": "19.2.3",
|
||||
"react-easy-sort": "1.8.0",
|
||||
"react-hook-form": "7.68.0",
|
||||
"react-hook-form": "7.71.1",
|
||||
"react-icons": "5.5.0",
|
||||
"rebuild": "0.1.2",
|
||||
"recharts": "2.15.4",
|
||||
"reodotdev": "1.0.0",
|
||||
"resend": "6.6.0",
|
||||
"resend": "6.8.0",
|
||||
"semver": "7.7.3",
|
||||
"stripe": "20.1.0",
|
||||
"stripe": "20.2.0",
|
||||
"swagger-ui-express": "5.0.1",
|
||||
"tailwind-merge": "3.4.0",
|
||||
"topojson-client": "3.1.0",
|
||||
@@ -133,10 +133,10 @@
|
||||
"visionscarto-world-atlas": "1.0.0",
|
||||
"winston": "3.19.0",
|
||||
"winston-daily-rotate-file": "5.0.0",
|
||||
"ws": "8.18.3",
|
||||
"ws": "8.19.0",
|
||||
"yaml": "2.8.2",
|
||||
"yargs": "18.0.0",
|
||||
"zod": "4.2.1",
|
||||
"zod": "4.3.5",
|
||||
"zod-validation-error": "5.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
@@ -170,12 +170,12 @@
|
||||
"esbuild": "0.27.2",
|
||||
"esbuild-node-externals": "1.20.1",
|
||||
"postcss": "8.5.6",
|
||||
"prettier": "3.7.4",
|
||||
"react-email": "5.0.7",
|
||||
"prettier": "3.8.0",
|
||||
"react-email": "5.2.5",
|
||||
"tailwindcss": "4.1.18",
|
||||
"tsc-alias": "1.8.16",
|
||||
"tsx": "4.21.0",
|
||||
"typescript": "5.9.3",
|
||||
"typescript-eslint": "8.49.0"
|
||||
"typescript-eslint": "8.53.1"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -778,7 +778,7 @@ export const currentFingerprint = pgTable("currentFingerprint", {
|
||||
|
||||
// Windows-specific posture check information
|
||||
|
||||
windowsDefenderEnabled: boolean("windowsDefenderEnabled")
|
||||
windowsAntivirusEnabled: boolean("windowsAntivirusEnabled")
|
||||
.notNull()
|
||||
.default(false),
|
||||
|
||||
@@ -830,7 +830,7 @@ export const fingerprintSnapshots = pgTable("fingerprintSnapshots", {
|
||||
|
||||
// Windows-specific posture check information
|
||||
|
||||
windowsDefenderEnabled: boolean("windowsDefenderEnabled")
|
||||
windowsAntivirusEnabled: boolean("windowsAntivirusEnabled")
|
||||
.notNull()
|
||||
.default(false),
|
||||
|
||||
|
||||
@@ -475,7 +475,7 @@ export const currentFingerprint = sqliteTable("currentFingerprint", {
|
||||
|
||||
// Windows-specific posture check information
|
||||
|
||||
windowsDefenderEnabled: integer("windowsDefenderEnabled", {
|
||||
windowsAntivirusEnabled: integer("windowsAntivirusEnabled", {
|
||||
mode: "boolean"
|
||||
})
|
||||
.notNull()
|
||||
@@ -549,7 +549,7 @@ export const fingerprintSnapshots = sqliteTable("fingerprintSnapshots", {
|
||||
|
||||
// Windows-specific posture check information
|
||||
|
||||
windowsDefenderEnabled: integer("windowsDefenderEnabled", {
|
||||
windowsAntivirusEnabled: integer("windowsAntivirusEnabled", {
|
||||
mode: "boolean"
|
||||
})
|
||||
.notNull()
|
||||
|
||||
@@ -2,7 +2,7 @@ import path from "path";
|
||||
import { fileURLToPath } from "url";
|
||||
|
||||
// This is a placeholder value replaced by the build process
|
||||
export const APP_VERSION = "1.14.0";
|
||||
export const APP_VERSION = "1.15.0";
|
||||
|
||||
export const __FILENAME = fileURLToPath(import.meta.url);
|
||||
export const __DIRNAME = path.dirname(__FILENAME);
|
||||
|
||||
@@ -58,7 +58,7 @@ type PostureData = {
|
||||
firewallEnabled?: boolean | null;
|
||||
autoUpdatesEnabled?: boolean | null;
|
||||
tpmAvailable?: boolean | null;
|
||||
windowsDefenderEnabled?: boolean | null;
|
||||
windowsAntivirusEnabled?: boolean | null;
|
||||
macosSipEnabled?: boolean | null;
|
||||
macosGatekeeperEnabled?: boolean | null;
|
||||
macosFirewallStealthMode?: boolean | null;
|
||||
@@ -75,7 +75,7 @@ function getPlatformPostureData(
|
||||
const normalizedPlatform = platform?.toLowerCase() || "unknown";
|
||||
const posture: PostureData = {};
|
||||
|
||||
// Windows: Hard drive encryption, Firewall, Auto updates, TPM availability, Windows Defender
|
||||
// Windows: Hard drive encryption, Firewall, Auto updates, TPM availability, Windows Antivirus status
|
||||
if (normalizedPlatform === "windows") {
|
||||
if (fingerprint.diskEncrypted !== null && fingerprint.diskEncrypted !== undefined) {
|
||||
posture.diskEncrypted = fingerprint.diskEncrypted;
|
||||
@@ -83,14 +83,11 @@ function getPlatformPostureData(
|
||||
if (fingerprint.firewallEnabled !== null && fingerprint.firewallEnabled !== undefined) {
|
||||
posture.firewallEnabled = fingerprint.firewallEnabled;
|
||||
}
|
||||
if (fingerprint.autoUpdatesEnabled !== null && fingerprint.autoUpdatesEnabled !== undefined) {
|
||||
posture.autoUpdatesEnabled = fingerprint.autoUpdatesEnabled;
|
||||
}
|
||||
if (fingerprint.tpmAvailable !== null && fingerprint.tpmAvailable !== undefined) {
|
||||
posture.tpmAvailable = fingerprint.tpmAvailable;
|
||||
}
|
||||
if (fingerprint.windowsDefenderEnabled !== null && fingerprint.windowsDefenderEnabled !== undefined) {
|
||||
posture.windowsDefenderEnabled = fingerprint.windowsDefenderEnabled;
|
||||
if (fingerprint.windowsAntivirusEnabled !== null && fingerprint.windowsAntivirusEnabled !== undefined) {
|
||||
posture.windowsAntivirusEnabled = fingerprint.windowsAntivirusEnabled;
|
||||
}
|
||||
}
|
||||
// macOS: Hard drive encryption, Biometric configuration, Firewall, System Integrity Protection (SIP), Gatekeeper, Firewall stealth mode
|
||||
@@ -113,6 +110,9 @@ function getPlatformPostureData(
|
||||
if (fingerprint.macosFirewallStealthMode !== null && fingerprint.macosFirewallStealthMode !== undefined) {
|
||||
posture.macosFirewallStealthMode = fingerprint.macosFirewallStealthMode;
|
||||
}
|
||||
if (fingerprint.autoUpdatesEnabled !== null && fingerprint.autoUpdatesEnabled !== undefined) {
|
||||
posture.autoUpdatesEnabled = fingerprint.autoUpdatesEnabled;
|
||||
}
|
||||
}
|
||||
// Linux: Hard drive encryption, Firewall, AppArmor, SELinux, TPM availability
|
||||
else if (normalizedPlatform === "linux") {
|
||||
@@ -134,15 +134,10 @@ function getPlatformPostureData(
|
||||
}
|
||||
// iOS: Biometric configuration
|
||||
else if (normalizedPlatform === "ios") {
|
||||
if (fingerprint.biometricsEnabled !== null && fingerprint.biometricsEnabled !== undefined) {
|
||||
posture.biometricsEnabled = fingerprint.biometricsEnabled;
|
||||
}
|
||||
// none supported yet
|
||||
}
|
||||
// Android: Screen lock, Biometric configuration, Hard drive encryption
|
||||
else if (normalizedPlatform === "android") {
|
||||
if (fingerprint.biometricsEnabled !== null && fingerprint.biometricsEnabled !== undefined) {
|
||||
posture.biometricsEnabled = fingerprint.biometricsEnabled;
|
||||
}
|
||||
if (fingerprint.diskEncrypted !== null && fingerprint.diskEncrypted !== undefined) {
|
||||
posture.diskEncrypted = fingerprint.diskEncrypted;
|
||||
}
|
||||
@@ -260,7 +255,7 @@ export async function getClient(
|
||||
postureData = getPlatformPostureData(
|
||||
client.currentFingerprint?.platform || null,
|
||||
client.currentFingerprint
|
||||
);
|
||||
);
|
||||
}
|
||||
|
||||
const data: GetClientResponse = {
|
||||
|
||||
@@ -175,10 +175,7 @@ async function getSiteAssociations(clientIds: number[]) {
|
||||
.where(inArray(clientSitesAssociationsCache.clientId, clientIds));
|
||||
}
|
||||
|
||||
type ClientWithSites = Omit<
|
||||
Awaited<ReturnType<typeof queryClients>>[0],
|
||||
"deviceModel"
|
||||
> & {
|
||||
type ClientWithSites = Awaited<ReturnType<typeof queryClients>>[0] & {
|
||||
sites: Array<{
|
||||
siteId: number;
|
||||
siteName: string | null;
|
||||
@@ -324,9 +321,8 @@ export async function listClients(
|
||||
const clientsWithSites = clientsList.map((client) => {
|
||||
const model = client.deviceModel || null;
|
||||
const newName = getUserDeviceName(model, client.name);
|
||||
const { deviceModel, ...clientWithoutDeviceModel } = client;
|
||||
return {
|
||||
...clientWithoutDeviceModel,
|
||||
...client,
|
||||
name: newName,
|
||||
sites: sitesByClient[client.clientId] || []
|
||||
};
|
||||
|
||||
@@ -22,7 +22,7 @@ function fingerprintSnapshotHash(fingerprint: any, postures: any): string {
|
||||
autoUpdatesEnabled: postures.autoUpdatesEnabled ?? false,
|
||||
tpmAvailable: postures.tpmAvailable ?? false,
|
||||
|
||||
windowsDefenderEnabled: postures.windowsDefenderEnabled ?? false,
|
||||
windowsAntivirusEnabled: postures.windowsAntivirusEnabled ?? false,
|
||||
|
||||
macosSipEnabled: postures.macosSipEnabled ?? false,
|
||||
macosGatekeeperEnabled: postures.macosGatekeeperEnabled ?? false,
|
||||
@@ -87,7 +87,7 @@ export async function handleFingerprintInsertion(
|
||||
autoUpdatesEnabled: postures.autoUpdatesEnabled,
|
||||
tpmAvailable: postures.tpmAvailable,
|
||||
|
||||
windowsDefenderEnabled: postures.windowsDefenderEnabled,
|
||||
windowsAntivirusEnabled: postures.windowsAntivirusEnabled,
|
||||
|
||||
macosSipEnabled: postures.macosSipEnabled,
|
||||
macosGatekeeperEnabled: postures.macosGatekeeperEnabled,
|
||||
@@ -117,7 +117,7 @@ export async function handleFingerprintInsertion(
|
||||
autoUpdatesEnabled: postures.autoUpdatesEnabled,
|
||||
tpmAvailable: postures.tpmAvailable,
|
||||
|
||||
windowsDefenderEnabled: postures.windowsDefenderEnabled,
|
||||
windowsAntivirusEnabled: postures.windowsAntivirusEnabled,
|
||||
|
||||
macosSipEnabled: postures.macosSipEnabled,
|
||||
macosGatekeeperEnabled: postures.macosGatekeeperEnabled,
|
||||
@@ -162,7 +162,7 @@ export async function handleFingerprintInsertion(
|
||||
autoUpdatesEnabled: postures.autoUpdatesEnabled,
|
||||
tpmAvailable: postures.tpmAvailable,
|
||||
|
||||
windowsDefenderEnabled: postures.windowsDefenderEnabled,
|
||||
windowsAntivirusEnabled: postures.windowsAntivirusEnabled,
|
||||
|
||||
macosSipEnabled: postures.macosSipEnabled,
|
||||
macosGatekeeperEnabled: postures.macosGatekeeperEnabled,
|
||||
@@ -197,7 +197,7 @@ export async function handleFingerprintInsertion(
|
||||
autoUpdatesEnabled: postures.autoUpdatesEnabled,
|
||||
tpmAvailable: postures.tpmAvailable,
|
||||
|
||||
windowsDefenderEnabled: postures.windowsDefenderEnabled,
|
||||
windowsAntivirusEnabled: postures.windowsAntivirusEnabled,
|
||||
|
||||
macosSipEnabled: postures.macosSipEnabled,
|
||||
macosGatekeeperEnabled: postures.macosGatekeeperEnabled,
|
||||
|
||||
@@ -46,6 +46,12 @@ export const handleOlmRegisterMessage: MessageHandler = async (context) => {
|
||||
return;
|
||||
}
|
||||
|
||||
logger.debug("Handling fingerprint insertion for olm register...", {
|
||||
olmId: olm.olmId,
|
||||
fingerprint,
|
||||
postures
|
||||
});
|
||||
|
||||
await handleFingerprintInsertion(olm, fingerprint, postures);
|
||||
|
||||
if (
|
||||
|
||||
@@ -97,6 +97,7 @@ export async function listAllSiteResourcesByOrg(
|
||||
destination: siteResources.destination,
|
||||
enabled: siteResources.enabled,
|
||||
alias: siteResources.alias,
|
||||
aliasAddress: siteResources.aliasAddress,
|
||||
tcpPortRangeString: siteResources.tcpPortRangeString,
|
||||
udpPortRangeString: siteResources.udpPortRangeString,
|
||||
disableIcmp: siteResources.disableIcmp,
|
||||
|
||||
@@ -49,7 +49,7 @@ export default async function migration() {
|
||||
"firewallEnabled" boolean DEFAULT false NOT NULL,
|
||||
"autoUpdatesEnabled" boolean DEFAULT false NOT NULL,
|
||||
"tpmAvailable" boolean DEFAULT false NOT NULL,
|
||||
"windowsDefenderEnabled" boolean DEFAULT false NOT NULL,
|
||||
"windowsAntivirusEnabled" boolean DEFAULT false NOT NULL,
|
||||
"macosSipEnabled" boolean DEFAULT false NOT NULL,
|
||||
"macosGatekeeperEnabled" boolean DEFAULT false NOT NULL,
|
||||
"macosFirewallStealthMode" boolean DEFAULT false NOT NULL,
|
||||
@@ -75,7 +75,7 @@ export default async function migration() {
|
||||
"firewallEnabled" boolean DEFAULT false NOT NULL,
|
||||
"autoUpdatesEnabled" boolean DEFAULT false NOT NULL,
|
||||
"tpmAvailable" boolean DEFAULT false NOT NULL,
|
||||
"windowsDefenderEnabled" boolean DEFAULT false NOT NULL,
|
||||
"windowsAntivirusEnabled" boolean DEFAULT false NOT NULL,
|
||||
"macosSipEnabled" boolean DEFAULT false NOT NULL,
|
||||
"macosGatekeeperEnabled" boolean DEFAULT false NOT NULL,
|
||||
"macosFirewallStealthMode" boolean DEFAULT false NOT NULL,
|
||||
|
||||
@@ -53,7 +53,7 @@ CREATE TABLE 'currentFingerprint' (
|
||||
'firewallEnabled' integer DEFAULT false NOT NULL,
|
||||
'autoUpdatesEnabled' integer DEFAULT false NOT NULL,
|
||||
'tpmAvailable' integer DEFAULT false NOT NULL,
|
||||
'windowsDefenderEnabled' integer DEFAULT false NOT NULL,
|
||||
'windowsAntivirusEnabled' integer DEFAULT false NOT NULL,
|
||||
'macosSipEnabled' integer DEFAULT false NOT NULL,
|
||||
'macosGatekeeperEnabled' integer DEFAULT false NOT NULL,
|
||||
'macosFirewallStealthMode' integer DEFAULT false NOT NULL,
|
||||
@@ -83,7 +83,7 @@ CREATE TABLE 'fingerprintSnapshots' (
|
||||
'firewallEnabled' integer DEFAULT false NOT NULL,
|
||||
'autoUpdatesEnabled' integer DEFAULT false NOT NULL,
|
||||
'tpmAvailable' integer DEFAULT false NOT NULL,
|
||||
'windowsDefenderEnabled' integer DEFAULT false NOT NULL,
|
||||
'windowsAntivirusEnabled' integer DEFAULT false NOT NULL,
|
||||
'macosSipEnabled' integer DEFAULT false NOT NULL,
|
||||
'macosGatekeeperEnabled' integer DEFAULT false NOT NULL,
|
||||
'macosFirewallStealthMode' integer DEFAULT false NOT NULL,
|
||||
|
||||
@@ -32,6 +32,7 @@ import CopyToClipboard from "@app/components/CopyToClipboard";
|
||||
import { Alert, AlertDescription, AlertTitle } from "@app/components/ui/alert";
|
||||
import { InfoIcon } from "lucide-react";
|
||||
import { PaidFeaturesAlert } from "@app/components/PaidFeaturesAlert";
|
||||
import { OlmInstallCommands } from "@app/components/olm-install-commands";
|
||||
|
||||
export default function CredentialsPage() {
|
||||
const { env } = useEnvContext();
|
||||
@@ -204,6 +205,12 @@ export default function CredentialsPage() {
|
||||
</SettingsSectionFooter>
|
||||
)}
|
||||
</SettingsSection>
|
||||
|
||||
<OlmInstallCommands
|
||||
id={displayOlmId ?? "********"}
|
||||
endpoint={env.app.dashboardUrl}
|
||||
secret={displaySecret ?? "********"}
|
||||
/>
|
||||
</SettingsContainer>
|
||||
|
||||
<ConfirmDeleteDialog
|
||||
|
||||
@@ -1,15 +1,22 @@
|
||||
"use client";
|
||||
|
||||
import CopyToClipboard from "@app/components/CopyToClipboard";
|
||||
import {
|
||||
InfoSection,
|
||||
InfoSectionContent,
|
||||
InfoSections,
|
||||
InfoSectionTitle
|
||||
} from "@app/components/InfoSection";
|
||||
import {
|
||||
SettingsContainer,
|
||||
SettingsSection,
|
||||
SettingsSectionBody,
|
||||
SettingsSectionDescription,
|
||||
SettingsSectionForm,
|
||||
SettingsSectionHeader,
|
||||
SettingsSectionTitle
|
||||
} from "@app/components/Settings";
|
||||
import { StrategySelect } from "@app/components/StrategySelect";
|
||||
import HeaderTitle from "@app/components/SettingsSectionTitle";
|
||||
import { Button } from "@app/components/ui/button";
|
||||
import {
|
||||
Form,
|
||||
FormControl,
|
||||
@@ -19,44 +26,24 @@ import {
|
||||
FormLabel,
|
||||
FormMessage
|
||||
} from "@app/components/ui/form";
|
||||
import HeaderTitle from "@app/components/SettingsSectionTitle";
|
||||
import { z } from "zod";
|
||||
import { createElement, useEffect, useState } from "react";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { Input } from "@app/components/ui/input";
|
||||
import { ChevronDown, ChevronUp, InfoIcon, Terminal } from "lucide-react";
|
||||
import { Button } from "@app/components/ui/button";
|
||||
import CopyTextBox from "@app/components/CopyTextBox";
|
||||
import CopyToClipboard from "@app/components/CopyToClipboard";
|
||||
import {
|
||||
InfoSection,
|
||||
InfoSectionContent,
|
||||
InfoSections,
|
||||
InfoSectionTitle
|
||||
} from "@app/components/InfoSection";
|
||||
import {
|
||||
FaApple,
|
||||
FaCubes,
|
||||
FaDocker,
|
||||
FaFreebsd,
|
||||
FaWindows
|
||||
} from "react-icons/fa";
|
||||
import { SiNixos, SiKubernetes } from "react-icons/si";
|
||||
import { Alert, AlertDescription, AlertTitle } from "@app/components/ui/alert";
|
||||
import { createApiClient, formatAxiosError } from "@app/lib/api";
|
||||
import { useEnvContext } from "@app/hooks/useEnvContext";
|
||||
import { toast } from "@app/hooks/useToast";
|
||||
import { createApiClient, formatAxiosError } from "@app/lib/api";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import {
|
||||
CreateClientBody,
|
||||
CreateClientResponse,
|
||||
PickClientDefaultsResponse
|
||||
} from "@server/routers/client";
|
||||
import { ListSitesResponse } from "@server/routers/site";
|
||||
import { toast } from "@app/hooks/useToast";
|
||||
import { AxiosResponse } from "axios";
|
||||
import { ChevronDown, ChevronUp } from "lucide-react";
|
||||
import { useParams, useRouter } from "next/navigation";
|
||||
import { Tag, TagInput } from "@app/components/tags/tag-input";
|
||||
import { useEffect, useState } from "react";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { z } from "zod";
|
||||
|
||||
import { OlmInstallCommands } from "@app/components/olm-install-commands";
|
||||
import { useTranslations } from "next-intl";
|
||||
|
||||
type ClientType = "olm";
|
||||
@@ -68,18 +55,6 @@ interface TunnelTypeOption {
|
||||
disabled?: boolean;
|
||||
}
|
||||
|
||||
type CommandItem = string | { title: string; command: string };
|
||||
|
||||
type Commands = {
|
||||
unix: Record<string, CommandItem[]>;
|
||||
windows: Record<string, CommandItem[]>;
|
||||
docker: Record<string, CommandItem[]>;
|
||||
};
|
||||
|
||||
const platforms = ["unix", "docker", "windows"] as const;
|
||||
|
||||
type Platform = (typeof platforms)[number];
|
||||
|
||||
export default function Page() {
|
||||
const { env } = useEnvContext();
|
||||
const api = createApiClient({ env });
|
||||
@@ -113,13 +88,9 @@ export default function Page() {
|
||||
|
||||
const [loadingPage, setLoadingPage] = useState(true);
|
||||
|
||||
const [platform, setPlatform] = useState<Platform>("unix");
|
||||
const [architecture, setArchitecture] = useState("All");
|
||||
const [commands, setCommands] = useState<Commands | null>(null);
|
||||
|
||||
const [olmId, setOlmId] = useState("");
|
||||
const [olmSecret, setOlmSecret] = useState("");
|
||||
const [olmCommand, setOlmCommand] = useState("");
|
||||
const [olmVersion, setOlmVersion] = useState("latest");
|
||||
|
||||
const [createLoading, setCreateLoading] = useState(false);
|
||||
const [showAdvancedSettings, setShowAdvancedSettings] = useState(false);
|
||||
@@ -127,136 +98,6 @@ export default function Page() {
|
||||
const [clientDefaults, setClientDefaults] =
|
||||
useState<PickClientDefaultsResponse | null>(null);
|
||||
|
||||
const hydrateCommands = (
|
||||
id: string,
|
||||
secret: string,
|
||||
endpoint: string,
|
||||
version: string
|
||||
) => {
|
||||
const commands = {
|
||||
unix: {
|
||||
All: [
|
||||
{
|
||||
title: t("install"),
|
||||
command: `curl -fsSL https://static.pangolin.net/get-olm.sh | bash`
|
||||
},
|
||||
{
|
||||
title: t("run"),
|
||||
command: `sudo olm --id ${id} --secret ${secret} --endpoint ${endpoint}`
|
||||
}
|
||||
]
|
||||
},
|
||||
windows: {
|
||||
x64: [
|
||||
{
|
||||
title: t("install"),
|
||||
command: `curl -o olm.exe -L "https://github.com/fosrl/olm/releases/download/${version}/olm_windows_installer.exe"`
|
||||
},
|
||||
{
|
||||
title: t("run"),
|
||||
command: `olm.exe --id ${id} --secret ${secret} --endpoint ${endpoint}`
|
||||
}
|
||||
]
|
||||
},
|
||||
docker: {
|
||||
"Docker Compose": [
|
||||
`services:
|
||||
olm:
|
||||
image: fosrl/olm
|
||||
container_name: olm
|
||||
restart: unless-stopped
|
||||
network_mode: host
|
||||
cap_add:
|
||||
- NET_ADMIN
|
||||
devices:
|
||||
- /dev/net/tun:/dev/net/tun
|
||||
environment:
|
||||
- PANGOLIN_ENDPOINT=${endpoint}
|
||||
- OLM_ID=${id}
|
||||
- OLM_SECRET=${secret}`
|
||||
],
|
||||
"Docker Run": [
|
||||
`docker run -dit --network host --cap-add NET_ADMIN --device /dev/net/tun:/dev/net/tun fosrl/olm --id ${id} --secret ${secret} --endpoint ${endpoint}`
|
||||
]
|
||||
}
|
||||
};
|
||||
setCommands(commands);
|
||||
};
|
||||
|
||||
const getArchitectures = () => {
|
||||
switch (platform) {
|
||||
case "unix":
|
||||
return ["All"];
|
||||
case "windows":
|
||||
return ["x64"];
|
||||
case "docker":
|
||||
return ["Docker Compose", "Docker Run"];
|
||||
default:
|
||||
return ["x64"];
|
||||
}
|
||||
};
|
||||
|
||||
const getPlatformName = (platformName: string) => {
|
||||
switch (platformName) {
|
||||
case "windows":
|
||||
return "Windows";
|
||||
case "unix":
|
||||
return "Unix & macOS";
|
||||
case "docker":
|
||||
return "Docker";
|
||||
default:
|
||||
return "Unix & macOS";
|
||||
}
|
||||
};
|
||||
|
||||
const getCommand = (): CommandItem[] => {
|
||||
const placeholder: CommandItem[] = [t("unknownCommand")];
|
||||
if (!commands) {
|
||||
return placeholder;
|
||||
}
|
||||
let platformCommands = commands[platform as keyof Commands];
|
||||
|
||||
if (!platformCommands) {
|
||||
// get first key
|
||||
const firstPlatform = Object.keys(commands)[0] as Platform;
|
||||
platformCommands = commands[firstPlatform as keyof Commands];
|
||||
|
||||
setPlatform(firstPlatform);
|
||||
}
|
||||
|
||||
let architectureCommands = platformCommands[architecture];
|
||||
if (!architectureCommands) {
|
||||
// get first key
|
||||
const firstArchitecture = Object.keys(platformCommands)[0];
|
||||
architectureCommands = platformCommands[firstArchitecture];
|
||||
|
||||
setArchitecture(firstArchitecture);
|
||||
}
|
||||
|
||||
return architectureCommands || placeholder;
|
||||
};
|
||||
|
||||
const getPlatformIcon = (platformName: string) => {
|
||||
switch (platformName) {
|
||||
case "windows":
|
||||
return <FaWindows className="h-4 w-4 mr-2" />;
|
||||
case "unix":
|
||||
return <Terminal className="h-4 w-4 mr-2" />;
|
||||
case "docker":
|
||||
return <FaDocker className="h-4 w-4 mr-2" />;
|
||||
case "kubernetes":
|
||||
return <SiKubernetes className="h-4 w-4 mr-2" />;
|
||||
case "podman":
|
||||
return <FaCubes className="h-4 w-4 mr-2" />;
|
||||
case "freebsd":
|
||||
return <FaFreebsd className="h-4 w-4 mr-2" />;
|
||||
case "nixos":
|
||||
return <SiNixos className="h-4 w-4 mr-2" />;
|
||||
default:
|
||||
return <Terminal className="h-4 w-4 mr-2" />;
|
||||
}
|
||||
};
|
||||
|
||||
const form = useForm<CreateClientFormValues>({
|
||||
resolver: zodResolver(createClientFormSchema),
|
||||
defaultValues: {
|
||||
@@ -311,23 +152,6 @@ export default function Page() {
|
||||
const load = async () => {
|
||||
setLoadingPage(true);
|
||||
|
||||
// Fetch available sites
|
||||
|
||||
// const res = await api.get<AxiosResponse<ListSitesResponse>>(
|
||||
// `/org/${orgId}/sites/`
|
||||
// );
|
||||
// const sites = res.data.data.sites.filter(
|
||||
// (s) => s.type === "newt" && s.subnet
|
||||
// );
|
||||
// setSites(
|
||||
// sites.map((site) => ({
|
||||
// id: site.siteId.toString(),
|
||||
// text: site.name
|
||||
// }))
|
||||
// );
|
||||
|
||||
let olmVersion = "latest";
|
||||
|
||||
try {
|
||||
const controller = new AbortController();
|
||||
const timeoutId = setTimeout(() => controller.abort(), 3000);
|
||||
@@ -348,7 +172,7 @@ export default function Page() {
|
||||
}
|
||||
const data = await response.json();
|
||||
const latestVersion = data.tag_name;
|
||||
olmVersion = latestVersion;
|
||||
setOlmVersion(latestVersion);
|
||||
} catch (error) {
|
||||
if (error instanceof Error && error.name === "AbortError") {
|
||||
console.error(t("olmErrorFetchTimeout"));
|
||||
@@ -377,18 +201,9 @@ export default function Page() {
|
||||
|
||||
const olmId = data.olmId;
|
||||
const olmSecret = data.olmSecret;
|
||||
const olmCommand = `olm --id ${olmId} --secret ${olmSecret} --endpoint ${env.app.dashboardUrl}`;
|
||||
|
||||
setOlmId(olmId);
|
||||
setOlmSecret(olmSecret);
|
||||
setOlmCommand(olmCommand);
|
||||
|
||||
hydrateCommands(
|
||||
olmId,
|
||||
olmSecret,
|
||||
env.app.dashboardUrl,
|
||||
olmVersion
|
||||
);
|
||||
|
||||
if (data.subnet) {
|
||||
form.setValue("subnet", data.subnet);
|
||||
@@ -571,118 +386,12 @@ export default function Page() {
|
||||
</InfoSections>
|
||||
</SettingsSectionBody>
|
||||
</SettingsSection>
|
||||
<SettingsSection>
|
||||
<SettingsSectionHeader>
|
||||
<SettingsSectionTitle>
|
||||
{t("clientInstallOlm")}
|
||||
</SettingsSectionTitle>
|
||||
<SettingsSectionDescription>
|
||||
{t("clientInstallOlmDescription")}
|
||||
</SettingsSectionDescription>
|
||||
</SettingsSectionHeader>
|
||||
<SettingsSectionBody>
|
||||
<div>
|
||||
<p className="font-bold mb-3">
|
||||
{t("operatingSystem")}
|
||||
</p>
|
||||
<div className="grid grid-cols-2 md:grid-cols-5 gap-2">
|
||||
{platforms.map((os) => (
|
||||
<Button
|
||||
key={os}
|
||||
variant={
|
||||
platform === os
|
||||
? "squareOutlinePrimary"
|
||||
: "squareOutline"
|
||||
}
|
||||
className={`flex-1 min-w-[120px] ${platform === os ? "bg-primary/10" : ""} shadow-none`}
|
||||
onClick={() => {
|
||||
setPlatform(os);
|
||||
}}
|
||||
>
|
||||
{getPlatformIcon(os)}
|
||||
{getPlatformName(os)}
|
||||
</Button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<p className="font-bold mb-3">
|
||||
{["docker", "podman"].includes(
|
||||
platform
|
||||
)
|
||||
? t("method")
|
||||
: t("architecture")}
|
||||
</p>
|
||||
<div className="grid grid-cols-2 md:grid-cols-5 gap-2">
|
||||
{getArchitectures().map(
|
||||
(arch) => (
|
||||
<Button
|
||||
key={arch}
|
||||
variant={
|
||||
architecture ===
|
||||
arch
|
||||
? "squareOutlinePrimary"
|
||||
: "squareOutline"
|
||||
}
|
||||
className={`flex-1 min-w-[120px] ${architecture === arch ? "bg-primary/10" : ""} shadow-none`}
|
||||
onClick={() =>
|
||||
setArchitecture(
|
||||
arch
|
||||
)
|
||||
}
|
||||
>
|
||||
{arch}
|
||||
</Button>
|
||||
)
|
||||
)}
|
||||
</div>
|
||||
<div className="pt-4">
|
||||
<p className="font-bold mb-3">
|
||||
{t("commands")}
|
||||
</p>
|
||||
<div className="mt-2 space-y-3">
|
||||
{getCommand().map(
|
||||
(item, index) => {
|
||||
const commandText =
|
||||
typeof item ===
|
||||
"string"
|
||||
? item
|
||||
: item.command;
|
||||
const title =
|
||||
typeof item ===
|
||||
"string"
|
||||
? undefined
|
||||
: item.title;
|
||||
|
||||
return (
|
||||
<div
|
||||
key={index}
|
||||
>
|
||||
{title && (
|
||||
<p className="text-sm font-medium mb-1.5">
|
||||
{
|
||||
title
|
||||
}
|
||||
</p>
|
||||
)}
|
||||
<CopyTextBox
|
||||
text={
|
||||
commandText
|
||||
}
|
||||
outline={
|
||||
true
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</SettingsSectionBody>
|
||||
</SettingsSection>
|
||||
<OlmInstallCommands
|
||||
id={olmId}
|
||||
endpoint={env.app.dashboardUrl}
|
||||
secret={olmSecret}
|
||||
version={olmVersion}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</SettingsContainer>
|
||||
|
||||
@@ -656,17 +656,17 @@ export default function GeneralPage() {
|
||||
</InfoSection>
|
||||
)}
|
||||
|
||||
{client.posture.windowsDefenderEnabled !== null &&
|
||||
client.posture.windowsDefenderEnabled !== undefined && (
|
||||
{client.posture.windowsAntivirusEnabled !== null &&
|
||||
client.posture.windowsAntivirusEnabled !== undefined && (
|
||||
<InfoSection>
|
||||
<InfoSectionTitle>
|
||||
{t("windowsDefenderEnabled")}
|
||||
{t("windowsAntivirusEnabled")}
|
||||
</InfoSectionTitle>
|
||||
<InfoSectionContent>
|
||||
{isPaidUser
|
||||
? formatPostureValue(
|
||||
client.posture
|
||||
.windowsDefenderEnabled
|
||||
.windowsAntivirusEnabled
|
||||
)
|
||||
: "-"}
|
||||
</InfoSectionContent>
|
||||
|
||||
@@ -265,4 +265,3 @@ function GeneralSectionForm({ org }: SectionFormProps) {
|
||||
</SettingsSection>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -67,6 +67,7 @@ export default async function ClientResourcesPage(
|
||||
destination: siteResource.destination,
|
||||
// destinationPort: siteResource.destinationPort,
|
||||
alias: siteResource.alias || null,
|
||||
aliasAddress: siteResource.aliasAddress || null,
|
||||
siteNiceId: siteResource.siteNiceId,
|
||||
niceId: siteResource.niceId,
|
||||
tcpPortRangeString: siteResource.tcpPortRangeString || null,
|
||||
|
||||
@@ -39,6 +39,7 @@ import {
|
||||
} from "@app/lib/wireguard";
|
||||
import { QRCodeCanvas } from "qrcode.react";
|
||||
import { PaidFeaturesAlert } from "@app/components/PaidFeaturesAlert";
|
||||
import { NewtSiteInstallCommands } from "@app/components/newt-install-commands";
|
||||
|
||||
export default function CredentialsPage() {
|
||||
const { env } = useEnvContext();
|
||||
@@ -186,107 +187,119 @@ export default function CredentialsPage() {
|
||||
return site?.name || site?.niceId || "My site";
|
||||
};
|
||||
|
||||
const displayNewtId = currentNewtId || siteDefaults?.newtId || null;
|
||||
const displaySecret = regeneratedSecret || null;
|
||||
const displayNewtId = currentNewtId ?? siteDefaults?.newtId ?? null;
|
||||
const displaySecret = regeneratedSecret ?? null;
|
||||
|
||||
return (
|
||||
<>
|
||||
<SettingsContainer>
|
||||
{site?.type === "newt" && (
|
||||
<SettingsSection>
|
||||
<SettingsSectionHeader>
|
||||
<SettingsSectionTitle>
|
||||
{t("siteNewtCredentials")}
|
||||
</SettingsSectionTitle>
|
||||
<SettingsSectionDescription>
|
||||
{t("siteNewtCredentialsDescription")}
|
||||
</SettingsSectionDescription>
|
||||
</SettingsSectionHeader>
|
||||
<>
|
||||
<SettingsSection>
|
||||
<SettingsSectionHeader>
|
||||
<SettingsSectionTitle>
|
||||
{t("siteNewtCredentials")}
|
||||
</SettingsSectionTitle>
|
||||
<SettingsSectionDescription>
|
||||
{t("siteNewtCredentialsDescription")}
|
||||
</SettingsSectionDescription>
|
||||
</SettingsSectionHeader>
|
||||
|
||||
<PaidFeaturesAlert />
|
||||
<PaidFeaturesAlert />
|
||||
|
||||
<SettingsSectionBody>
|
||||
<InfoSections cols={3}>
|
||||
<InfoSection>
|
||||
<InfoSectionTitle>
|
||||
{t("newtEndpoint")}
|
||||
</InfoSectionTitle>
|
||||
<InfoSectionContent>
|
||||
<CopyToClipboard
|
||||
text={env.app.dashboardUrl}
|
||||
/>
|
||||
</InfoSectionContent>
|
||||
</InfoSection>
|
||||
<InfoSection>
|
||||
<InfoSectionTitle>
|
||||
{t("newtId")}
|
||||
</InfoSectionTitle>
|
||||
<InfoSectionContent>
|
||||
{displayNewtId ? (
|
||||
<SettingsSectionBody>
|
||||
<InfoSections cols={3}>
|
||||
<InfoSection>
|
||||
<InfoSectionTitle>
|
||||
{t("newtEndpoint")}
|
||||
</InfoSectionTitle>
|
||||
<InfoSectionContent>
|
||||
<CopyToClipboard
|
||||
text={displayNewtId}
|
||||
text={env.app.dashboardUrl}
|
||||
/>
|
||||
) : (
|
||||
<span>{"••••••••••••••••"}</span>
|
||||
)}
|
||||
</InfoSectionContent>
|
||||
</InfoSection>
|
||||
<InfoSection>
|
||||
<InfoSectionTitle>
|
||||
{t("newtSecretKey")}
|
||||
</InfoSectionTitle>
|
||||
<InfoSectionContent>
|
||||
{displaySecret ? (
|
||||
<CopyToClipboard
|
||||
text={displaySecret}
|
||||
/>
|
||||
) : (
|
||||
<span>
|
||||
{
|
||||
"••••••••••••••••••••••••••••••••"
|
||||
}
|
||||
</span>
|
||||
)}
|
||||
</InfoSectionContent>
|
||||
</InfoSection>
|
||||
</InfoSections>
|
||||
</InfoSectionContent>
|
||||
</InfoSection>
|
||||
<InfoSection>
|
||||
<InfoSectionTitle>
|
||||
{t("newtId")}
|
||||
</InfoSectionTitle>
|
||||
<InfoSectionContent>
|
||||
{displayNewtId ? (
|
||||
<CopyToClipboard
|
||||
text={displayNewtId}
|
||||
/>
|
||||
) : (
|
||||
<span>
|
||||
{"••••••••••••••••"}
|
||||
</span>
|
||||
)}
|
||||
</InfoSectionContent>
|
||||
</InfoSection>
|
||||
<InfoSection>
|
||||
<InfoSectionTitle>
|
||||
{t("newtSecretKey")}
|
||||
</InfoSectionTitle>
|
||||
<InfoSectionContent>
|
||||
{displaySecret ? (
|
||||
<CopyToClipboard
|
||||
text={displaySecret}
|
||||
/>
|
||||
) : (
|
||||
<span>
|
||||
{
|
||||
"••••••••••••••••••••••••••••••••"
|
||||
}
|
||||
</span>
|
||||
)}
|
||||
</InfoSectionContent>
|
||||
</InfoSection>
|
||||
</InfoSections>
|
||||
|
||||
{showCredentialsAlert && displaySecret && (
|
||||
<Alert variant="neutral" className="mt-4">
|
||||
<InfoIcon className="h-4 w-4" />
|
||||
<AlertTitle className="font-semibold">
|
||||
{t("siteCredentialsSave")}
|
||||
</AlertTitle>
|
||||
<AlertDescription>
|
||||
{t("siteCredentialsSaveDescription")}
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
{showCredentialsAlert && displaySecret && (
|
||||
<Alert variant="neutral" className="mt-4">
|
||||
<InfoIcon className="h-4 w-4" />
|
||||
<AlertTitle className="font-semibold">
|
||||
{t("siteCredentialsSave")}
|
||||
</AlertTitle>
|
||||
<AlertDescription>
|
||||
{t(
|
||||
"siteCredentialsSaveDescription"
|
||||
)}
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
)}
|
||||
</SettingsSectionBody>
|
||||
{build !== "oss" && (
|
||||
<SettingsSectionFooter>
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={() => {
|
||||
setShouldDisconnect(false);
|
||||
setModalOpen(true);
|
||||
}}
|
||||
disabled={isSecurityFeatureDisabled()}
|
||||
>
|
||||
{t("regenerateCredentialsButton")}
|
||||
</Button>
|
||||
<Button
|
||||
onClick={() => {
|
||||
setShouldDisconnect(true);
|
||||
setModalOpen(true);
|
||||
}}
|
||||
disabled={isSecurityFeatureDisabled()}
|
||||
>
|
||||
{t("siteRegenerateAndDisconnect")}
|
||||
</Button>
|
||||
</SettingsSectionFooter>
|
||||
)}
|
||||
</SettingsSectionBody>
|
||||
{build !== "oss" && (
|
||||
<SettingsSectionFooter>
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={() => {
|
||||
setShouldDisconnect(false);
|
||||
setModalOpen(true);
|
||||
}}
|
||||
disabled={isSecurityFeatureDisabled()}
|
||||
>
|
||||
{t("regenerateCredentialsButton")}
|
||||
</Button>
|
||||
<Button
|
||||
onClick={() => {
|
||||
setShouldDisconnect(true);
|
||||
setModalOpen(true);
|
||||
}}
|
||||
disabled={isSecurityFeatureDisabled()}
|
||||
>
|
||||
{t("siteRegenerateAndDisconnect")}
|
||||
</Button>
|
||||
</SettingsSectionFooter>
|
||||
)}
|
||||
</SettingsSection>
|
||||
</SettingsSection>
|
||||
|
||||
<NewtSiteInstallCommands
|
||||
id={displayNewtId ?? "**********"}
|
||||
secret={displaySecret ?? "**************"}
|
||||
endpoint={env.app.dashboardUrl}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
|
||||
{site?.type === "wireguard" && (
|
||||
@@ -306,7 +319,7 @@ export default function CredentialsPage() {
|
||||
{!loadingDefaults && (
|
||||
<>
|
||||
{wgConfig ? (
|
||||
<div className="flex items-center gap-4">
|
||||
<div className="flex flex-col sm:flex-row items-center gap-4">
|
||||
<CopyTextBox
|
||||
text={wgConfig}
|
||||
outline={true}
|
||||
|
||||
@@ -62,6 +62,8 @@ import { QRCodeCanvas } from "qrcode.react";
|
||||
|
||||
import { useTranslations } from "next-intl";
|
||||
import { build } from "@server/build";
|
||||
import { NewtSiteInstallCommands } from "@app/components/newt-install-commands";
|
||||
import { id } from "date-fns/locale";
|
||||
|
||||
type SiteType = "newt" | "wireguard" | "local";
|
||||
|
||||
@@ -189,10 +191,6 @@ export default function Page() {
|
||||
|
||||
const [loadingPage, setLoadingPage] = useState(true);
|
||||
|
||||
const [platform, setPlatform] = useState<Platform>("unix");
|
||||
const [architecture, setArchitecture] = useState("amd64");
|
||||
const [commands, setCommands] = useState<Commands | null>(null);
|
||||
|
||||
const [newtId, setNewtId] = useState("");
|
||||
const [newtSecret, setNewtSecret] = useState("");
|
||||
const [newtEndpoint, setNewtEndpoint] = useState("");
|
||||
@@ -202,7 +200,6 @@ export default function Page() {
|
||||
const [wgConfig, setWgConfig] = useState("");
|
||||
|
||||
const [createLoading, setCreateLoading] = useState(false);
|
||||
const [acceptClients, setAcceptClients] = useState(true);
|
||||
const [newtVersion, setNewtVersion] = useState("latest");
|
||||
const [showAdvancedSettings, setShowAdvancedSettings] = useState(false);
|
||||
|
||||
@@ -216,187 +213,6 @@ export default function Page() {
|
||||
string | undefined
|
||||
>();
|
||||
|
||||
const hydrateCommands = (
|
||||
id: string,
|
||||
secret: string,
|
||||
endpoint: string,
|
||||
version: string,
|
||||
acceptClients: boolean = true
|
||||
) => {
|
||||
const acceptClientsFlag = !acceptClients ? " --disable-clients" : "";
|
||||
const acceptClientsEnv = !acceptClients
|
||||
? "\n - DISABLE_CLIENTS=true"
|
||||
: "";
|
||||
|
||||
const commands = {
|
||||
unix: {
|
||||
All: [
|
||||
{
|
||||
title: t("install"),
|
||||
command: `curl -fsSL https://static.pangolin.net/get-newt.sh | bash`
|
||||
},
|
||||
{
|
||||
title: t("run"),
|
||||
command: `newt --id ${id} --secret ${secret} --endpoint ${endpoint}${acceptClientsFlag}`
|
||||
}
|
||||
]
|
||||
},
|
||||
windows: {
|
||||
x64: [
|
||||
{
|
||||
title: t("install"),
|
||||
command: `curl -o newt.exe -L "https://github.com/fosrl/newt/releases/download/${version}/newt_windows_amd64.exe"`
|
||||
},
|
||||
{
|
||||
title: t("run"),
|
||||
command: `newt.exe --id ${id} --secret ${secret} --endpoint ${endpoint}${acceptClientsFlag}`
|
||||
}
|
||||
]
|
||||
},
|
||||
docker: {
|
||||
"Docker Compose": [
|
||||
`services:
|
||||
newt:
|
||||
image: fosrl/newt
|
||||
container_name: newt
|
||||
restart: unless-stopped
|
||||
environment:
|
||||
- PANGOLIN_ENDPOINT=${endpoint}
|
||||
- NEWT_ID=${id}
|
||||
- NEWT_SECRET=${secret}${acceptClientsEnv}`
|
||||
],
|
||||
"Docker Run": [
|
||||
`docker run -dit fosrl/newt --id ${id} --secret ${secret} --endpoint ${endpoint}${acceptClientsFlag}`
|
||||
]
|
||||
},
|
||||
kubernetes: {
|
||||
"Helm Chart": [
|
||||
`helm repo add fossorial https://charts.fossorial.io`,
|
||||
`helm repo update fossorial`,
|
||||
`helm install newt fossorial/newt \\
|
||||
--create-namespace \\
|
||||
--set newtInstances[0].name="main-tunnel" \\
|
||||
--set-string newtInstances[0].auth.keys.endpointKey="${endpoint}" \\
|
||||
--set-string newtInstances[0].auth.keys.idKey="${id}" \\
|
||||
--set-string newtInstances[0].auth.keys.secretKey="${secret}"`
|
||||
]
|
||||
},
|
||||
podman: {
|
||||
"Podman Quadlet": [
|
||||
`[Unit]
|
||||
Description=Newt container
|
||||
|
||||
[Container]
|
||||
ContainerName=newt
|
||||
Image=docker.io/fosrl/newt
|
||||
Environment=PANGOLIN_ENDPOINT=${endpoint}
|
||||
Environment=NEWT_ID=${id}
|
||||
Environment=NEWT_SECRET=${secret}${!acceptClients ? "\nEnvironment=DISABLE_CLIENTS=true" : ""}
|
||||
# Secret=newt-secret,type=env,target=NEWT_SECRET
|
||||
|
||||
[Service]
|
||||
Restart=always
|
||||
|
||||
[Install]
|
||||
WantedBy=default.target`
|
||||
],
|
||||
"Podman Run": [
|
||||
`podman run -dit docker.io/fosrl/newt --id ${id} --secret ${secret} --endpoint ${endpoint}${acceptClientsFlag}`
|
||||
]
|
||||
},
|
||||
nixos: {
|
||||
All: [
|
||||
`nix run 'nixpkgs#fosrl-newt' -- --id ${id} --secret ${secret} --endpoint ${endpoint}${acceptClientsFlag}`
|
||||
]
|
||||
}
|
||||
};
|
||||
setCommands(commands);
|
||||
};
|
||||
|
||||
const getArchitectures = () => {
|
||||
switch (platform) {
|
||||
case "unix":
|
||||
return ["All"];
|
||||
case "windows":
|
||||
return ["x64"];
|
||||
case "docker":
|
||||
return ["Docker Compose", "Docker Run"];
|
||||
case "kubernetes":
|
||||
return ["Helm Chart"];
|
||||
case "podman":
|
||||
return ["Podman Quadlet", "Podman Run"];
|
||||
case "nixos":
|
||||
return ["All"];
|
||||
default:
|
||||
return ["x64"];
|
||||
}
|
||||
};
|
||||
|
||||
const getPlatformName = (platformName: string) => {
|
||||
switch (platformName) {
|
||||
case "windows":
|
||||
return "Windows";
|
||||
case "unix":
|
||||
return "Unix & macOS";
|
||||
case "docker":
|
||||
return "Docker";
|
||||
case "kubernetes":
|
||||
return "Kubernetes";
|
||||
case "podman":
|
||||
return "Podman";
|
||||
case "nixos":
|
||||
return "NixOS";
|
||||
default:
|
||||
return "Unix / macOS";
|
||||
}
|
||||
};
|
||||
|
||||
const getCommand = (): CommandItem[] => {
|
||||
const placeholder: CommandItem[] = [t("unknownCommand")];
|
||||
if (!commands) {
|
||||
return placeholder;
|
||||
}
|
||||
let platformCommands = commands[platform as keyof Commands];
|
||||
|
||||
if (!platformCommands) {
|
||||
// get first key
|
||||
const firstPlatform = Object.keys(commands)[0] as Platform;
|
||||
platformCommands = commands[firstPlatform as keyof Commands];
|
||||
|
||||
setPlatform(firstPlatform);
|
||||
}
|
||||
|
||||
let architectureCommands = platformCommands[architecture];
|
||||
if (!architectureCommands) {
|
||||
// get first key
|
||||
const firstArchitecture = Object.keys(platformCommands)[0];
|
||||
architectureCommands = platformCommands[firstArchitecture];
|
||||
|
||||
setArchitecture(firstArchitecture);
|
||||
}
|
||||
|
||||
return architectureCommands || placeholder;
|
||||
};
|
||||
|
||||
const getPlatformIcon = (platformName: string) => {
|
||||
switch (platformName) {
|
||||
case "windows":
|
||||
return <FaWindows className="h-4 w-4 mr-2" />;
|
||||
case "unix":
|
||||
return <Terminal className="h-4 w-4 mr-2" />;
|
||||
case "docker":
|
||||
return <FaDocker className="h-4 w-4 mr-2" />;
|
||||
case "kubernetes":
|
||||
return <SiKubernetes className="h-4 w-4 mr-2" />;
|
||||
case "podman":
|
||||
return <FaCubes className="h-4 w-4 mr-2" />;
|
||||
case "nixos":
|
||||
return <SiNixos className="h-4 w-4 mr-2" />;
|
||||
default:
|
||||
return <Terminal className="h-4 w-4 mr-2" />;
|
||||
}
|
||||
};
|
||||
|
||||
const form = useForm({
|
||||
resolver: zodResolver(createSiteFormSchema),
|
||||
defaultValues: {
|
||||
@@ -414,7 +230,7 @@ WantedBy=default.target`
|
||||
|
||||
let payload: CreateSiteBody = {
|
||||
name: data.name,
|
||||
type: data.method as "newt" | "wireguard" | "local"
|
||||
type: data.method
|
||||
};
|
||||
|
||||
if (data.method == "wireguard") {
|
||||
@@ -568,14 +384,6 @@ WantedBy=default.target`
|
||||
setNewtEndpoint(newtEndpoint);
|
||||
setClientAddress(clientAddress);
|
||||
|
||||
hydrateCommands(
|
||||
newtId,
|
||||
newtSecret,
|
||||
env.app.dashboardUrl,
|
||||
currentNewtVersion,
|
||||
acceptClients
|
||||
);
|
||||
|
||||
const wgConfig = generateWireGuardConfig(
|
||||
privateKey,
|
||||
data.publicKey,
|
||||
@@ -631,11 +439,6 @@ WantedBy=default.target`
|
||||
load();
|
||||
}, []);
|
||||
|
||||
// Sync form acceptClients value with local state
|
||||
useEffect(() => {
|
||||
form.setValue("acceptClients", acceptClients);
|
||||
}, [acceptClients, form]);
|
||||
|
||||
// Sync form exitNodeId value with local state
|
||||
useEffect(() => {
|
||||
if (build !== "saas") {
|
||||
@@ -847,214 +650,15 @@ WantedBy=default.target`
|
||||
</InfoSectionContent>
|
||||
</InfoSection>
|
||||
</InfoSections>
|
||||
|
||||
{/* <Form {...form}> */}
|
||||
{/* <form */}
|
||||
{/* className="space-y-4" */}
|
||||
{/* id="create-site-form" */}
|
||||
{/* > */}
|
||||
{/* <FormField */}
|
||||
{/* control={form.control} */}
|
||||
{/* name="copied" */}
|
||||
{/* render={({ field }) => ( */}
|
||||
{/* <FormItem> */}
|
||||
{/* <div className="flex items-center space-x-2"> */}
|
||||
{/* <Checkbox */}
|
||||
{/* id="terms" */}
|
||||
{/* defaultChecked={ */}
|
||||
{/* form.getValues( */}
|
||||
{/* "copied" */}
|
||||
{/* ) as boolean */}
|
||||
{/* } */}
|
||||
{/* onCheckedChange={( */}
|
||||
{/* e */}
|
||||
{/* ) => { */}
|
||||
{/* form.setValue( */}
|
||||
{/* "copied", */}
|
||||
{/* e as boolean */}
|
||||
{/* ); */}
|
||||
{/* }} */}
|
||||
{/* /> */}
|
||||
{/* <label */}
|
||||
{/* htmlFor="terms" */}
|
||||
{/* className="text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70" */}
|
||||
{/* > */}
|
||||
{/* {t('siteConfirmCopy')} */}
|
||||
{/* </label> */}
|
||||
{/* </div> */}
|
||||
{/* <FormMessage /> */}
|
||||
{/* </FormItem> */}
|
||||
{/* )} */}
|
||||
{/* /> */}
|
||||
{/* </form> */}
|
||||
{/* </Form> */}
|
||||
</SettingsSectionBody>
|
||||
</SettingsSection>
|
||||
<SettingsSection>
|
||||
<SettingsSectionHeader>
|
||||
<SettingsSectionTitle>
|
||||
{t("siteInstallNewt")}
|
||||
</SettingsSectionTitle>
|
||||
<SettingsSectionDescription>
|
||||
{t("siteInstallNewtDescription")}
|
||||
</SettingsSectionDescription>
|
||||
</SettingsSectionHeader>
|
||||
<SettingsSectionBody>
|
||||
<div>
|
||||
<p className="font-bold mb-3">
|
||||
{t("operatingSystem")}
|
||||
</p>
|
||||
<div className="grid grid-cols-2 md:grid-cols-5 gap-2">
|
||||
{platforms.map((os) => (
|
||||
<Button
|
||||
key={os}
|
||||
variant={
|
||||
platform === os
|
||||
? "squareOutlinePrimary"
|
||||
: "squareOutline"
|
||||
}
|
||||
className={`flex-1 min-w-30 ${platform === os ? "bg-primary/10" : ""} shadow-none`}
|
||||
onClick={() => {
|
||||
setPlatform(os);
|
||||
}}
|
||||
>
|
||||
{getPlatformIcon(os)}
|
||||
{getPlatformName(os)}
|
||||
</Button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<p className="font-bold mb-3">
|
||||
{["docker", "podman"].includes(
|
||||
platform
|
||||
)
|
||||
? t("method")
|
||||
: t("architecture")}
|
||||
</p>
|
||||
<div className="grid grid-cols-2 md:grid-cols-5 gap-2">
|
||||
{getArchitectures().map(
|
||||
(arch) => (
|
||||
<Button
|
||||
key={arch}
|
||||
variant={
|
||||
architecture ===
|
||||
arch
|
||||
? "squareOutlinePrimary"
|
||||
: "squareOutline"
|
||||
}
|
||||
className={`flex-1 min-w-30 ${architecture === arch ? "bg-primary/10" : ""} shadow-none`}
|
||||
onClick={() =>
|
||||
setArchitecture(
|
||||
arch
|
||||
)
|
||||
}
|
||||
>
|
||||
{arch}
|
||||
</Button>
|
||||
)
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="pt-4">
|
||||
<p className="font-bold mb-3">
|
||||
{t("siteConfiguration")}
|
||||
</p>
|
||||
<div className="flex items-center space-x-2 mb-2">
|
||||
<CheckboxWithLabel
|
||||
id="acceptClients"
|
||||
aria-describedby="acceptClients-desc"
|
||||
checked={acceptClients}
|
||||
onCheckedChange={(
|
||||
checked
|
||||
) => {
|
||||
const value =
|
||||
checked as boolean;
|
||||
setAcceptClients(
|
||||
value
|
||||
);
|
||||
form.setValue(
|
||||
"acceptClients",
|
||||
value
|
||||
);
|
||||
// Re-hydrate commands with new acceptClients value
|
||||
if (
|
||||
newtId &&
|
||||
newtSecret &&
|
||||
newtVersion
|
||||
) {
|
||||
hydrateCommands(
|
||||
newtId,
|
||||
newtSecret,
|
||||
env.app
|
||||
.dashboardUrl,
|
||||
newtVersion,
|
||||
value
|
||||
);
|
||||
}
|
||||
}}
|
||||
label={t(
|
||||
"siteAcceptClientConnections"
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
<p
|
||||
id="acceptClients-desc"
|
||||
className="text-sm text-muted-foreground"
|
||||
>
|
||||
{t(
|
||||
"siteAcceptClientConnectionsDescription"
|
||||
)}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="pt-4">
|
||||
<p className="font-bold mb-3">
|
||||
{t("commands")}
|
||||
</p>
|
||||
<div className="mt-2 space-y-3">
|
||||
{getCommand().map(
|
||||
(item, index) => {
|
||||
const commandText =
|
||||
typeof item ===
|
||||
"string"
|
||||
? item
|
||||
: item.command;
|
||||
const title =
|
||||
typeof item ===
|
||||
"string"
|
||||
? undefined
|
||||
: item.title;
|
||||
|
||||
return (
|
||||
<div
|
||||
key={index}
|
||||
>
|
||||
{title && (
|
||||
<p className="text-sm font-medium mb-1.5">
|
||||
{
|
||||
title
|
||||
}
|
||||
</p>
|
||||
)}
|
||||
<CopyTextBox
|
||||
text={
|
||||
commandText
|
||||
}
|
||||
outline={
|
||||
true
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</SettingsSectionBody>
|
||||
</SettingsSection>
|
||||
<NewtSiteInstallCommands
|
||||
id={newtId}
|
||||
secret={newtSecret}
|
||||
endpoint={env.app.dashboardUrl}
|
||||
version={newtVersion}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
|
||||
@@ -1069,7 +673,7 @@ WantedBy=default.target`
|
||||
</SettingsSectionDescription>
|
||||
</SettingsSectionHeader>
|
||||
<SettingsSectionBody>
|
||||
<div className="flex items-center gap-4">
|
||||
<div className="flex flex-col sm:flex-row items-center gap-4">
|
||||
<CopyTextBox text={wgConfig} />
|
||||
<div
|
||||
className={`relative w-fit border rounded-md`}
|
||||
@@ -1087,7 +691,7 @@ WantedBy=default.target`
|
||||
</SettingsSection>
|
||||
)}
|
||||
|
||||
{build == "saas" &&
|
||||
{build === "saas" &&
|
||||
form.watch("method") === "local" && (
|
||||
<SettingsSection>
|
||||
<SettingsSectionHeader>
|
||||
|
||||
@@ -4,6 +4,7 @@ import React from "react";
|
||||
import { Button } from "@app/components/ui/button";
|
||||
import { Download } from "lucide-react";
|
||||
import { FaApple, FaWindows, FaLinux } from "react-icons/fa";
|
||||
import { SiAndroid } from "react-icons/si";
|
||||
import { useTranslations } from "next-intl";
|
||||
import Link from "next/link";
|
||||
import DismissableBanner from "./DismissableBanner";
|
||||
@@ -61,6 +62,34 @@ export const ClientDownloadBanner = () => {
|
||||
Linux
|
||||
</Button>
|
||||
</Link>
|
||||
<Link
|
||||
href="https://pangolin.net/downloads/ios"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
className="gap-2 hover:bg-primary/10 hover:border-primary/50 transition-colors"
|
||||
>
|
||||
<FaApple className="w-4 h-4" />
|
||||
iOS
|
||||
</Button>
|
||||
</Link>
|
||||
<Link
|
||||
href="https://pangolin.net/downloads/android"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
className="gap-2 hover:bg-primary/10 hover:border-primary/50 transition-colors"
|
||||
>
|
||||
<SiAndroid className="w-4 h-4" />
|
||||
Android
|
||||
</Button>
|
||||
</Link>
|
||||
</DismissableBanner>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -11,6 +11,7 @@ import {
|
||||
DropdownMenuItem,
|
||||
DropdownMenuTrigger
|
||||
} from "@app/components/ui/dropdown-menu";
|
||||
import { InfoPopup } from "@app/components/ui/info-popup";
|
||||
import { useEnvContext } from "@app/hooks/useEnvContext";
|
||||
import { toast } from "@app/hooks/useToast";
|
||||
import { createApiClient, formatAxiosError } from "@app/lib/api";
|
||||
@@ -40,6 +41,7 @@ export type InternalResourceRow = {
|
||||
destination: string;
|
||||
// destinationPort: number | null;
|
||||
alias: string | null;
|
||||
aliasAddress: string | null;
|
||||
niceId: string;
|
||||
tcpPortRangeString: string | null;
|
||||
udpPortRangeString: string | null;
|
||||
@@ -228,6 +230,29 @@ export default function ClientResourcesTable({
|
||||
);
|
||||
}
|
||||
},
|
||||
{
|
||||
accessorKey: "aliasAddress",
|
||||
friendlyName: t("resourcesTableAliasAddress"),
|
||||
enableHiding: true,
|
||||
header: () => (
|
||||
<div className="flex items-center gap-2 p-3">
|
||||
<span>{t("resourcesTableAliasAddress")}</span>
|
||||
<InfoPopup info={t("resourcesTableAliasAddressInfo")} />
|
||||
</div>
|
||||
),
|
||||
cell: ({ row }) => {
|
||||
const resourceRow = row.original;
|
||||
return resourceRow.aliasAddress ? (
|
||||
<CopyToClipboard
|
||||
text={resourceRow.aliasAddress}
|
||||
isLink={false}
|
||||
displayText={resourceRow.aliasAddress}
|
||||
/>
|
||||
) : (
|
||||
<span>-</span>
|
||||
);
|
||||
}
|
||||
},
|
||||
{
|
||||
id: "actions",
|
||||
enableHiding: false,
|
||||
@@ -316,7 +341,8 @@ export default function ClientResourcesTable({
|
||||
enableColumnVisibility={true}
|
||||
persistColumnVisibility="internal-resources"
|
||||
columnVisibility={{
|
||||
niceId: false
|
||||
niceId: false,
|
||||
aliasAddress: false
|
||||
}}
|
||||
stickyLeftColumn="name"
|
||||
stickyRightColumn="actions"
|
||||
|
||||
@@ -60,6 +60,18 @@ export default function LocaleSwitcher() {
|
||||
{
|
||||
value: "zh-TW",
|
||||
label: "繁體中文"
|
||||
},
|
||||
{
|
||||
value: "ru-RU",
|
||||
label: "Русский"
|
||||
},
|
||||
{
|
||||
value: "bg-BG",
|
||||
label: "Български"
|
||||
},
|
||||
{
|
||||
value: "cs-CZ",
|
||||
label: "Čeština"
|
||||
}
|
||||
]}
|
||||
/>
|
||||
|
||||
@@ -68,7 +68,6 @@ export function LogAnalyticsData(props: AnalyticsContentProps) {
|
||||
|
||||
const router = useRouter();
|
||||
|
||||
console.log({ filters });
|
||||
const dateRange = {
|
||||
startDate: filters.timeStart
|
||||
? new Date(filters.timeStart)
|
||||
|
||||
@@ -548,7 +548,7 @@ export default function UserDevicesTable({ userClients }: ClientTableProps) {
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="end">
|
||||
{clientRow.approvalState === "pending" && (
|
||||
{clientRow.approvalState === "pending" && build !== "oss" && (
|
||||
<>
|
||||
<DropdownMenuItem
|
||||
onClick={() => approveDevice(clientRow)}
|
||||
@@ -653,7 +653,7 @@ export default function UserDevicesTable({ userClients }: ClientTableProps) {
|
||||
];
|
||||
|
||||
if (build === "oss") {
|
||||
return allOptions.filter((option) => option.value !== "pending");
|
||||
return allOptions.filter((option) => option.value !== "pending" && option.value !== "denied");
|
||||
}
|
||||
|
||||
return allOptions;
|
||||
|
||||
314
src/components/newt-install-commands.tsx
Normal file
314
src/components/newt-install-commands.tsx
Normal file
@@ -0,0 +1,314 @@
|
||||
import { useTranslations } from "next-intl";
|
||||
import CopyTextBox from "./CopyTextBox";
|
||||
import {
|
||||
SettingsSection,
|
||||
SettingsSectionBody,
|
||||
SettingsSectionDescription,
|
||||
SettingsSectionHeader,
|
||||
SettingsSectionTitle
|
||||
} from "./Settings";
|
||||
import { CheckboxWithLabel } from "./ui/checkbox";
|
||||
import { Button } from "./ui/button";
|
||||
import { useState } from "react";
|
||||
import { FaCubes, FaDocker, FaWindows } from "react-icons/fa";
|
||||
import { Terminal } from "lucide-react";
|
||||
import { SiKubernetes, SiNixos } from "react-icons/si";
|
||||
|
||||
export type CommandItem = string | { title: string; command: string };
|
||||
|
||||
const PLATFORMS = [
|
||||
"unix",
|
||||
"windows",
|
||||
"docker",
|
||||
"kubernetes",
|
||||
"podman",
|
||||
"nixos"
|
||||
] as const;
|
||||
|
||||
type Platform = (typeof PLATFORMS)[number];
|
||||
|
||||
export type NewtSiteInstallCommandsProps = {
|
||||
id: string;
|
||||
secret: string;
|
||||
endpoint: string;
|
||||
version?: string;
|
||||
};
|
||||
|
||||
export function NewtSiteInstallCommands({
|
||||
id,
|
||||
secret,
|
||||
endpoint,
|
||||
version = "latest"
|
||||
}: NewtSiteInstallCommandsProps) {
|
||||
const t = useTranslations();
|
||||
|
||||
const [acceptClients, setAcceptClients] = useState(true);
|
||||
const [platform, setPlatform] = useState<Platform>("unix");
|
||||
const [architecture, setArchitecture] = useState(
|
||||
() => getArchitectures(platform)[0]
|
||||
);
|
||||
|
||||
const acceptClientsFlag = !acceptClients ? " --disable-clients" : "";
|
||||
const acceptClientsEnv = !acceptClients
|
||||
? "\n - DISABLE_CLIENTS=true"
|
||||
: "";
|
||||
|
||||
const commandList: Record<Platform, Record<string, CommandItem[]>> = {
|
||||
unix: {
|
||||
All: [
|
||||
{
|
||||
title: t("install"),
|
||||
command: `curl -fsSL https://static.pangolin.net/get-newt.sh | bash`
|
||||
},
|
||||
{
|
||||
title: t("run"),
|
||||
command: `newt --id ${id} --secret ${secret} --endpoint ${endpoint}${acceptClientsFlag}`
|
||||
}
|
||||
]
|
||||
},
|
||||
windows: {
|
||||
x64: [
|
||||
{
|
||||
title: t("install"),
|
||||
command: `curl -o newt.exe -L "https://github.com/fosrl/newt/releases/download/${version}/newt_windows_amd64.exe"`
|
||||
},
|
||||
{
|
||||
title: t("run"),
|
||||
command: `newt.exe --id ${id} --secret ${secret} --endpoint ${endpoint}${acceptClientsFlag}`
|
||||
}
|
||||
]
|
||||
},
|
||||
docker: {
|
||||
"Docker Compose": [
|
||||
`services:
|
||||
newt:
|
||||
image: fosrl/newt
|
||||
container_name: newt
|
||||
restart: unless-stopped
|
||||
environment:
|
||||
- PANGOLIN_ENDPOINT=${endpoint}
|
||||
- NEWT_ID=${id}
|
||||
- NEWT_SECRET=${secret}${acceptClientsEnv}`
|
||||
],
|
||||
"Docker Run": [
|
||||
`docker run -dit fosrl/newt --id ${id} --secret ${secret} --endpoint ${endpoint}${acceptClientsFlag}`
|
||||
]
|
||||
},
|
||||
kubernetes: {
|
||||
"Helm Chart": [
|
||||
`helm repo add fossorial https://charts.fossorial.io`,
|
||||
`helm repo update fossorial`,
|
||||
`helm install newt fossorial/newt \\
|
||||
--create-namespace \\
|
||||
--set newtInstances[0].name="main-tunnel" \\
|
||||
--set-string newtInstances[0].auth.keys.endpointKey="${endpoint}" \\
|
||||
--set-string newtInstances[0].auth.keys.idKey="${id}" \\
|
||||
--set-string newtInstances[0].auth.keys.secretKey="${secret}"`
|
||||
]
|
||||
},
|
||||
podman: {
|
||||
"Podman Quadlet": [
|
||||
`[Unit]
|
||||
Description=Newt container
|
||||
|
||||
[Container]
|
||||
ContainerName=newt
|
||||
Image=docker.io/fosrl/newt
|
||||
Environment=PANGOLIN_ENDPOINT=${endpoint}
|
||||
Environment=NEWT_ID=${id}
|
||||
Environment=NEWT_SECRET=${secret}${!acceptClients ? "\nEnvironment=DISABLE_CLIENTS=true" : ""}
|
||||
# Secret=newt-secret,type=env,target=NEWT_SECRET
|
||||
|
||||
[Service]
|
||||
Restart=always
|
||||
|
||||
[Install]
|
||||
WantedBy=default.target`
|
||||
],
|
||||
"Podman Run": [
|
||||
`podman run -dit docker.io/fosrl/newt --id ${id} --secret ${secret} --endpoint ${endpoint}${acceptClientsFlag}`
|
||||
]
|
||||
},
|
||||
nixos: {
|
||||
All: [
|
||||
`nix run 'nixpkgs#fosrl-newt' -- --id ${id} --secret ${secret} --endpoint ${endpoint}${acceptClientsFlag}`
|
||||
]
|
||||
}
|
||||
};
|
||||
|
||||
const commands = commandList[platform][architecture];
|
||||
|
||||
return (
|
||||
<SettingsSection>
|
||||
<SettingsSectionHeader>
|
||||
<SettingsSectionTitle>
|
||||
{t("siteInstallNewt")}
|
||||
</SettingsSectionTitle>
|
||||
<SettingsSectionDescription>
|
||||
{t("siteInstallNewtDescription")}
|
||||
</SettingsSectionDescription>
|
||||
</SettingsSectionHeader>
|
||||
<SettingsSectionBody>
|
||||
<div>
|
||||
<p className="font-bold mb-3">{t("operatingSystem")}</p>
|
||||
<div className="grid grid-cols-2 md:grid-cols-5 gap-2">
|
||||
{PLATFORMS.map((os) => (
|
||||
<Button
|
||||
key={os}
|
||||
variant={
|
||||
platform === os
|
||||
? "squareOutlinePrimary"
|
||||
: "squareOutline"
|
||||
}
|
||||
className={`flex-1 min-w-30 ${platform === os ? "bg-primary/10" : ""} shadow-none`}
|
||||
onClick={() => {
|
||||
setPlatform(os);
|
||||
const architectures = getArchitectures(os);
|
||||
setArchitecture(architectures[0]);
|
||||
}}
|
||||
>
|
||||
{getPlatformIcon(os)}
|
||||
{getPlatformName(os)}
|
||||
</Button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<p className="font-bold mb-3">
|
||||
{["docker", "podman"].includes(platform)
|
||||
? t("method")
|
||||
: t("architecture")}
|
||||
</p>
|
||||
<div className="grid grid-cols-2 md:grid-cols-5 gap-2">
|
||||
{getArchitectures(platform).map((arch) => (
|
||||
<Button
|
||||
key={arch}
|
||||
variant={
|
||||
architecture === arch
|
||||
? "squareOutlinePrimary"
|
||||
: "squareOutline"
|
||||
}
|
||||
className={`flex-1 min-w-30 ${architecture === arch ? "bg-primary/10" : ""} shadow-none`}
|
||||
onClick={() => setArchitecture(arch)}
|
||||
>
|
||||
{arch}
|
||||
</Button>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div className="pt-4">
|
||||
<p className="font-bold mb-3">
|
||||
{t("siteConfiguration")}
|
||||
</p>
|
||||
<div className="flex items-center space-x-2 mb-2">
|
||||
<CheckboxWithLabel
|
||||
id="acceptClients"
|
||||
aria-describedby="acceptClients-desc"
|
||||
checked={acceptClients}
|
||||
onCheckedChange={(checked) => {
|
||||
const value = checked as boolean;
|
||||
setAcceptClients(value);
|
||||
}}
|
||||
label={t("siteAcceptClientConnections")}
|
||||
/>
|
||||
</div>
|
||||
<p
|
||||
id="acceptClients-desc"
|
||||
className="text-sm text-muted-foreground"
|
||||
>
|
||||
{t("siteAcceptClientConnectionsDescription")}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="pt-4">
|
||||
<p className="font-bold mb-3">{t("commands")}</p>
|
||||
<div className="mt-2 space-y-3">
|
||||
{commands.map((item, index) => {
|
||||
const commandText =
|
||||
typeof item === "string"
|
||||
? item
|
||||
: item.command;
|
||||
const title =
|
||||
typeof item === "string"
|
||||
? undefined
|
||||
: item.title;
|
||||
|
||||
return (
|
||||
<div key={index}>
|
||||
{title && (
|
||||
<p className="text-sm font-medium mb-1.5">
|
||||
{title}
|
||||
</p>
|
||||
)}
|
||||
<CopyTextBox
|
||||
text={commandText}
|
||||
outline={true}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</SettingsSectionBody>
|
||||
</SettingsSection>
|
||||
);
|
||||
}
|
||||
|
||||
function getPlatformIcon(platformName: Platform) {
|
||||
switch (platformName) {
|
||||
case "windows":
|
||||
return <FaWindows className="h-4 w-4 mr-2" />;
|
||||
case "unix":
|
||||
return <Terminal className="h-4 w-4 mr-2" />;
|
||||
case "docker":
|
||||
return <FaDocker className="h-4 w-4 mr-2" />;
|
||||
case "kubernetes":
|
||||
return <SiKubernetes className="h-4 w-4 mr-2" />;
|
||||
case "podman":
|
||||
return <FaCubes className="h-4 w-4 mr-2" />;
|
||||
case "nixos":
|
||||
return <SiNixos className="h-4 w-4 mr-2" />;
|
||||
default:
|
||||
return <Terminal className="h-4 w-4 mr-2" />;
|
||||
}
|
||||
}
|
||||
|
||||
function getPlatformName(platformName: Platform) {
|
||||
switch (platformName) {
|
||||
case "windows":
|
||||
return "Windows";
|
||||
case "unix":
|
||||
return "Unix & macOS";
|
||||
case "docker":
|
||||
return "Docker";
|
||||
case "kubernetes":
|
||||
return "Kubernetes";
|
||||
case "podman":
|
||||
return "Podman";
|
||||
case "nixos":
|
||||
return "NixOS";
|
||||
default:
|
||||
return "Unix / macOS";
|
||||
}
|
||||
}
|
||||
|
||||
function getArchitectures(platform: Platform) {
|
||||
switch (platform) {
|
||||
case "unix":
|
||||
return ["All"];
|
||||
case "windows":
|
||||
return ["x64"];
|
||||
case "docker":
|
||||
return ["Docker Compose", "Docker Run"];
|
||||
case "kubernetes":
|
||||
return ["Helm Chart"];
|
||||
case "podman":
|
||||
return ["Podman Quadlet", "Podman Run"];
|
||||
case "nixos":
|
||||
return ["All"];
|
||||
default:
|
||||
return ["x64"];
|
||||
}
|
||||
}
|
||||
220
src/components/olm-install-commands.tsx
Normal file
220
src/components/olm-install-commands.tsx
Normal file
@@ -0,0 +1,220 @@
|
||||
import { Terminal } from "lucide-react";
|
||||
import { useTranslations } from "next-intl";
|
||||
import { useState } from "react";
|
||||
import { FaDocker, FaWindows } from "react-icons/fa";
|
||||
import CopyTextBox from "./CopyTextBox";
|
||||
import {
|
||||
SettingsSection,
|
||||
SettingsSectionBody,
|
||||
SettingsSectionDescription,
|
||||
SettingsSectionHeader,
|
||||
SettingsSectionTitle
|
||||
} from "./Settings";
|
||||
import { Button } from "./ui/button";
|
||||
|
||||
export type CommandItem = string | { title: string; command: string };
|
||||
|
||||
const PLATFORMS = ["unix", "windows", "docker"] as const;
|
||||
|
||||
type Platform = (typeof PLATFORMS)[number];
|
||||
|
||||
export type OlmInstallCommandsProps = {
|
||||
id: string;
|
||||
secret: string;
|
||||
endpoint: string;
|
||||
version?: string;
|
||||
};
|
||||
|
||||
export function OlmInstallCommands({
|
||||
id,
|
||||
secret,
|
||||
endpoint,
|
||||
version = "latest"
|
||||
}: OlmInstallCommandsProps) {
|
||||
const t = useTranslations();
|
||||
|
||||
const [platform, setPlatform] = useState<Platform>("unix");
|
||||
const [architecture, setArchitecture] = useState(
|
||||
() => getArchitectures(platform)[0]
|
||||
);
|
||||
|
||||
const commandList: Record<Platform, Record<string, CommandItem[]>> = {
|
||||
unix: {
|
||||
All: [
|
||||
{
|
||||
title: t("install"),
|
||||
command: `curl -fsSL https://static.pangolin.net/get-olm.sh | bash`
|
||||
},
|
||||
{
|
||||
title: t("run"),
|
||||
command: `sudo olm --id ${id} --secret ${secret} --endpoint ${endpoint}`
|
||||
}
|
||||
]
|
||||
},
|
||||
windows: {
|
||||
x64: [
|
||||
{
|
||||
title: t("install"),
|
||||
command: `curl -o olm.exe -L "https://github.com/fosrl/olm/releases/download/${version}/olm_windows_installer.exe"`
|
||||
},
|
||||
{
|
||||
title: t("run"),
|
||||
command: `olm.exe --id ${id} --secret ${secret} --endpoint ${endpoint}`
|
||||
}
|
||||
]
|
||||
},
|
||||
docker: {
|
||||
"Docker Compose": [
|
||||
`services:
|
||||
olm:
|
||||
image: fosrl/olm
|
||||
container_name: olm
|
||||
restart: unless-stopped
|
||||
network_mode: host
|
||||
cap_add:
|
||||
- NET_ADMIN
|
||||
devices:
|
||||
- /dev/net/tun:/dev/net/tun
|
||||
environment:
|
||||
- PANGOLIN_ENDPOINT=${endpoint}
|
||||
- OLM_ID=${id}
|
||||
- OLM_SECRET=${secret}`
|
||||
],
|
||||
"Docker Run": [
|
||||
`docker run -dit --network host --cap-add NET_ADMIN --device /dev/net/tun:/dev/net/tun fosrl/olm --id ${id} --secret ${secret} --endpoint ${endpoint}`
|
||||
]
|
||||
}
|
||||
};
|
||||
|
||||
const commands = commandList[platform][architecture];
|
||||
return (
|
||||
<SettingsSection>
|
||||
<SettingsSectionHeader>
|
||||
<SettingsSectionTitle>
|
||||
{t("clientInstallOlm")}
|
||||
</SettingsSectionTitle>
|
||||
<SettingsSectionDescription>
|
||||
{t("clientInstallOlmDescription")}
|
||||
</SettingsSectionDescription>
|
||||
</SettingsSectionHeader>
|
||||
<SettingsSectionBody>
|
||||
<div>
|
||||
<p className="font-bold mb-3">{t("operatingSystem")}</p>
|
||||
<div className="grid grid-cols-2 md:grid-cols-5 gap-2">
|
||||
{PLATFORMS.map((os) => (
|
||||
<Button
|
||||
key={os}
|
||||
variant={
|
||||
platform === os
|
||||
? "squareOutlinePrimary"
|
||||
: "squareOutline"
|
||||
}
|
||||
className={`flex-1 min-w-30 ${platform === os ? "bg-primary/10" : ""} shadow-none`}
|
||||
onClick={() => {
|
||||
setPlatform(os);
|
||||
const architectures = getArchitectures(os);
|
||||
setArchitecture(architectures[0]);
|
||||
}}
|
||||
>
|
||||
{getPlatformIcon(os)}
|
||||
{getPlatformName(os)}
|
||||
</Button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<p className="font-bold mb-3">
|
||||
{["docker", "podman"].includes(platform)
|
||||
? t("method")
|
||||
: t("architecture")}
|
||||
</p>
|
||||
<div className="grid grid-cols-2 md:grid-cols-5 gap-2">
|
||||
{getArchitectures(platform).map((arch) => (
|
||||
<Button
|
||||
key={arch}
|
||||
variant={
|
||||
architecture === arch
|
||||
? "squareOutlinePrimary"
|
||||
: "squareOutline"
|
||||
}
|
||||
className={`flex-1 min-w-30 ${architecture === arch ? "bg-primary/10" : ""} shadow-none`}
|
||||
onClick={() => setArchitecture(arch)}
|
||||
>
|
||||
{arch}
|
||||
</Button>
|
||||
))}
|
||||
</div>
|
||||
<div className="pt-4">
|
||||
<p className="font-bold mb-3">{t("commands")}</p>
|
||||
<div className="mt-2 space-y-3">
|
||||
{commands.map((item, index) => {
|
||||
const commandText =
|
||||
typeof item === "string"
|
||||
? item
|
||||
: item.command;
|
||||
const title =
|
||||
typeof item === "string"
|
||||
? undefined
|
||||
: item.title;
|
||||
|
||||
return (
|
||||
<div key={index}>
|
||||
{title && (
|
||||
<p className="text-sm font-medium mb-1.5">
|
||||
{title}
|
||||
</p>
|
||||
)}
|
||||
<CopyTextBox
|
||||
text={commandText}
|
||||
outline={true}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</SettingsSectionBody>
|
||||
</SettingsSection>
|
||||
);
|
||||
}
|
||||
|
||||
function getArchitectures(platform: Platform) {
|
||||
switch (platform) {
|
||||
case "unix":
|
||||
return ["All"];
|
||||
case "windows":
|
||||
return ["x64"];
|
||||
case "docker":
|
||||
return ["Docker Compose", "Docker Run"];
|
||||
default:
|
||||
return ["x64"];
|
||||
}
|
||||
}
|
||||
|
||||
function getPlatformName(platformName: Platform) {
|
||||
switch (platformName) {
|
||||
case "windows":
|
||||
return "Windows";
|
||||
case "unix":
|
||||
return "Unix & macOS";
|
||||
case "docker":
|
||||
return "Docker";
|
||||
default:
|
||||
return "Unix & macOS";
|
||||
}
|
||||
}
|
||||
|
||||
function getPlatformIcon(platformName: Platform) {
|
||||
switch (platformName) {
|
||||
case "windows":
|
||||
return <FaWindows className="h-4 w-4 mr-2" />;
|
||||
case "unix":
|
||||
return <Terminal className="h-4 w-4 mr-2" />;
|
||||
case "docker":
|
||||
return <FaDocker className="h-4 w-4 mr-2" />;
|
||||
default:
|
||||
return <Terminal className="h-4 w-4 mr-2" />;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user