mirror of
https://github.com/fosrl/pangolin.git
synced 2026-03-02 16:56:39 +00:00
Compare commits
145 Commits
1.8.0
...
site-targe
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
200a7fcd40 | ||
|
|
5c04b1e14a | ||
|
|
67ba225003 | ||
|
|
74d2527af5 | ||
|
|
eeb1d4954d | ||
|
|
4c463de45f | ||
|
|
1f4a7a7f6f | ||
|
|
e7df29104e | ||
|
|
9987b35b60 | ||
|
|
16e876ab68 | ||
|
|
50fc2fc74e | ||
|
|
c244dc9c0c | ||
|
|
0f50981573 | ||
|
|
0c1cb20936 | ||
|
|
192617a884 | ||
|
|
297991ef5f | ||
|
|
75f97c4a31 | ||
|
|
40f520086c | ||
|
|
c8dda4f90d | ||
|
|
5f09f97032 | ||
|
|
168056d595 | ||
|
|
c70eaa0096 | ||
|
|
5f36b13408 | ||
|
|
9dc73efa3a | ||
|
|
e9c2868998 | ||
|
|
0a13b04c55 | ||
|
|
cf12d3ee56 | ||
|
|
cea7190453 | ||
|
|
c6d78680fb | ||
|
|
0bf302e013 | ||
|
|
1351fb6689 | ||
|
|
af638d666c | ||
|
|
e4fe601d9d | ||
|
|
4f3cd71e1e | ||
|
|
9c0295db9f | ||
|
|
3fc2d1df80 | ||
|
|
4a6747dcc7 | ||
|
|
54b3c92953 | ||
|
|
a4d460e850 | ||
|
|
ae52fcc757 | ||
|
|
03c8d82471 | ||
|
|
14d7a138a5 | ||
|
|
a829eb949b | ||
|
|
fd605d9c81 | ||
|
|
55b4a9eddb | ||
|
|
9ccf77b99c | ||
|
|
ea27075bab | ||
|
|
c3723d0fce | ||
|
|
0edb3cd316 | ||
|
|
e9e6b0bc4f | ||
|
|
4701da201d | ||
|
|
d6d2e052dd | ||
|
|
d3d1dcfe1d | ||
|
|
918ebf5e65 | ||
|
|
67184b88a8 | ||
|
|
fb0f4c3939 | ||
|
|
aae5343543 | ||
|
|
51e9762ca8 | ||
|
|
330dafc652 | ||
|
|
7ddf9fa54e | ||
|
|
f2ca09eedd | ||
|
|
f0e2c8416d | ||
|
|
338b7a8c13 | ||
|
|
b4284f82f3 | ||
|
|
0ce430cab5 | ||
|
|
95c0f6c093 | ||
|
|
387dbc360e | ||
|
|
a88be89c2f | ||
|
|
8bc353442f | ||
|
|
b3502bd627 | ||
|
|
56da7c242d | ||
|
|
fa8f49e87d | ||
|
|
6e08a70afc | ||
|
|
bd4be2b05c | ||
|
|
6b6ff0a95e | ||
|
|
4755cae5cb | ||
|
|
b2384ccc06 | ||
|
|
e6589308dd | ||
|
|
879b25be9f | ||
|
|
d3ad941b30 | ||
|
|
f077fbc3f5 | ||
|
|
4679ce968b | ||
|
|
101e462649 | ||
|
|
5d93ab9b9e | ||
|
|
d557832509 | ||
|
|
fe5c91db29 | ||
|
|
b2947193ec | ||
|
|
f6440753b6 | ||
|
|
17cf903804 | ||
|
|
dcf530d237 | ||
|
|
6b1808dab1 | ||
|
|
5889efd74a | ||
|
|
1a9de1e5c5 | ||
|
|
d1404a2b07 | ||
|
|
664dbf3f4c | ||
|
|
f32a8e26b6 | ||
|
|
b1a92fd4e0 | ||
|
|
1ea9fd2d49 | ||
|
|
f31e4e3176 | ||
|
|
e3287a7e9f | ||
|
|
ec21153d4b | ||
|
|
917e7a8c1d | ||
|
|
8e0a8dc272 | ||
|
|
91bac29ea3 | ||
|
|
3e333769bb | ||
|
|
b4bde6660a | ||
|
|
917f752081 | ||
|
|
915d561286 | ||
|
|
01ef809fd3 | ||
|
|
19902092ce | ||
|
|
39603b6e53 | ||
|
|
9c85a09d3e | ||
|
|
69baa6785f | ||
|
|
bb84d01e14 | ||
|
|
616dae2d8b | ||
|
|
3fbfe50e09 | ||
|
|
c0c8edb9d1 | ||
|
|
84268e484d | ||
|
|
c473c2fa81 | ||
|
|
7402590f49 | ||
|
|
529d1c9f66 | ||
|
|
e85b772ca5 | ||
|
|
f75169fc26 | ||
|
|
07b86521a5 | ||
|
|
961008bbe1 | ||
|
|
6d359b6bb9 | ||
|
|
ea6f803e78 | ||
|
|
0151f8a6a9 | ||
|
|
39c5101957 | ||
|
|
9b1cd5f79c | ||
|
|
36d0b83ed3 | ||
|
|
f0138fad4f | ||
|
|
69802e78f8 | ||
|
|
92e69f561f | ||
|
|
b351520e92 | ||
|
|
481714f095 | ||
|
|
d38656e026 | ||
|
|
27ac204bb6 | ||
|
|
a2526ea244 | ||
|
|
39c43c0c09 | ||
|
|
350485612e | ||
|
|
df31c13912 | ||
|
|
2259879595 | ||
|
|
4f5091ed7f | ||
|
|
b5afd73024 |
@@ -28,3 +28,4 @@ LICENSE
|
|||||||
CONTRIBUTING.md
|
CONTRIBUTING.md
|
||||||
dist
|
dist
|
||||||
.git
|
.git
|
||||||
|
config/
|
||||||
22
.github/dependabot.yml
vendored
22
.github/dependabot.yml
vendored
@@ -38,3 +38,25 @@ updates:
|
|||||||
directory: "/"
|
directory: "/"
|
||||||
schedule:
|
schedule:
|
||||||
interval: "weekly"
|
interval: "weekly"
|
||||||
|
|
||||||
|
- package-ecosystem: "gomod"
|
||||||
|
directory: "/install"
|
||||||
|
schedule:
|
||||||
|
interval: "daily"
|
||||||
|
groups:
|
||||||
|
dev-patch-updates:
|
||||||
|
dependency-type: "development"
|
||||||
|
update-types:
|
||||||
|
- "patch"
|
||||||
|
dev-minor-updates:
|
||||||
|
dependency-type: "development"
|
||||||
|
update-types:
|
||||||
|
- "minor"
|
||||||
|
prod-patch-updates:
|
||||||
|
dependency-type: "production"
|
||||||
|
update-types:
|
||||||
|
- "patch"
|
||||||
|
prod-minor-updates:
|
||||||
|
dependency-type: "production"
|
||||||
|
update-types:
|
||||||
|
- "minor"
|
||||||
2
.github/workflows/cicd.yml
vendored
2
.github/workflows/cicd.yml
vendored
@@ -30,7 +30,7 @@ jobs:
|
|||||||
- name: Install Go
|
- name: Install Go
|
||||||
uses: actions/setup-go@v5
|
uses: actions/setup-go@v5
|
||||||
with:
|
with:
|
||||||
go-version: 1.23.0
|
go-version: 1.24
|
||||||
|
|
||||||
- name: Update version in package.json
|
- name: Update version in package.json
|
||||||
run: |
|
run: |
|
||||||
|
|||||||
2
.github/workflows/linting.yml
vendored
2
.github/workflows/linting.yml
vendored
@@ -23,7 +23,7 @@ jobs:
|
|||||||
- name: Set up Node.js
|
- name: Set up Node.js
|
||||||
uses: actions/setup-node@v4
|
uses: actions/setup-node@v4
|
||||||
with:
|
with:
|
||||||
node-version: '20'
|
node-version: '22'
|
||||||
|
|
||||||
- name: Install dependencies
|
- name: Install dependencies
|
||||||
run: |
|
run: |
|
||||||
|
|||||||
2
.github/workflows/test.yml
vendored
2
.github/workflows/test.yml
vendored
@@ -15,7 +15,7 @@ jobs:
|
|||||||
|
|
||||||
- uses: actions/setup-node@v4
|
- uses: actions/setup-node@v4
|
||||||
with:
|
with:
|
||||||
node-version: '20'
|
node-version: '22'
|
||||||
|
|
||||||
- name: Copy config file
|
- name: Copy config file
|
||||||
run: cp config/config.example.yml config/config.yml
|
run: cp config/config.example.yml config/config.yml
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ Contributions are welcome!
|
|||||||
|
|
||||||
Please see the contribution and local development guide on the docs page before getting started:
|
Please see the contribution and local development guide on the docs page before getting started:
|
||||||
|
|
||||||
https://docs.fossorial.io/development
|
https://docs.digpangolin.com/development/contributing
|
||||||
|
|
||||||
### Licensing Considerations
|
### Licensing Considerations
|
||||||
|
|
||||||
@@ -17,4 +17,4 @@ By creating this pull request, I grant the project maintainers an unlimited,
|
|||||||
perpetual license to use, modify, and redistribute these contributions under any terms they
|
perpetual license to use, modify, and redistribute these contributions under any terms they
|
||||||
choose, including both the AGPLv3 and the Fossorial Commercial license terms. I
|
choose, including both the AGPLv3 and the Fossorial Commercial license terms. I
|
||||||
represent that I have the right to grant this license for all contributed content.
|
represent that I have the right to grant this license for all contributed content.
|
||||||
```
|
```
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
FROM node:20-alpine
|
FROM node:22-alpine
|
||||||
|
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
FROM node:20-alpine AS builder
|
FROM node:22-alpine AS builder
|
||||||
|
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
|
|
||||||
@@ -15,7 +15,7 @@ RUN npx drizzle-kit generate --dialect postgresql --schema ./server/db/pg/schema
|
|||||||
RUN npm run build:pg
|
RUN npm run build:pg
|
||||||
RUN npm run build:cli
|
RUN npm run build:cli
|
||||||
|
|
||||||
FROM node:20-alpine AS runner
|
FROM node:22-alpine AS runner
|
||||||
|
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
FROM node:20-alpine AS builder
|
FROM node:22-alpine AS builder
|
||||||
|
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
|
|
||||||
@@ -15,7 +15,7 @@ RUN npx drizzle-kit generate --dialect sqlite --schema ./server/db/sqlite/schema
|
|||||||
RUN npm run build:sqlite
|
RUN npm run build:sqlite
|
||||||
RUN npm run build:cli
|
RUN npm run build:cli
|
||||||
|
|
||||||
FROM node:20-alpine AS runner
|
FROM node:22-alpine AS runner
|
||||||
|
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
|
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ _Pangolin tunnels your services to the internet so you can access anything from
|
|||||||
Website
|
Website
|
||||||
</a>
|
</a>
|
||||||
<span> | </span>
|
<span> | </span>
|
||||||
<a href="https://docs.fossorial.io/Getting%20Started/quick-install">
|
<a href="https://docs.digpangolin.com/self-host/quick-install">
|
||||||
Install Guide
|
Install Guide
|
||||||
</a>
|
</a>
|
||||||
<span> | </span>
|
<span> | </span>
|
||||||
@@ -104,7 +104,7 @@ Pangolin is a self-hosted tunneled reverse proxy server with identity and access
|
|||||||
|
|
||||||
### Fully Self Hosted
|
### Fully Self Hosted
|
||||||
|
|
||||||
Host the full application on your own server or on the cloud with a VPS. Take a look at the [documentation](https://docs.fossorial.io/Getting%20Started/quick-install) to get started.
|
Host the full application on your own server or on the cloud with a VPS. Take a look at the [documentation](https://docs.digpangolin.com/self-host/quick-install) to get started.
|
||||||
|
|
||||||
> Many of our users have had a great experience with [RackNerd](https://my.racknerd.com/aff.php?aff=13788). Depending on promotions, you can get a [**VPS with 1 vCPU, 1GB RAM, and ~20GB SSD for just around $12/year**](https://my.racknerd.com/aff.php?aff=13788&pid=912). That's a great deal!
|
> Many of our users have had a great experience with [RackNerd](https://my.racknerd.com/aff.php?aff=13788). Depending on promotions, you can get a [**VPS with 1 vCPU, 1GB RAM, and ~20GB SSD for just around $12/year**](https://my.racknerd.com/aff.php?aff=13788&pid=912). That's a great deal!
|
||||||
|
|
||||||
@@ -139,7 +139,7 @@ Pangolin is dual licensed under the AGPL-3 and the Fossorial Commercial license.
|
|||||||
|
|
||||||
## Contributions
|
## Contributions
|
||||||
|
|
||||||
Looking for something to contribute? Take a look at issues marked with [help wanted](https://github.com/fosrl/pangolin/issues?q=is%3Aissue%20state%3Aopen%20label%3A%22help%20wanted%22).
|
Looking for something to contribute? Take a look at issues marked with [help wanted](https://github.com/fosrl/pangolin/issues?q=is%3Aissue%20state%3Aopen%20label%3A%22help%20wanted%22). Also take a look through the freature requests in Discussions - any are available and some are marked as a good first issue.
|
||||||
|
|
||||||
Please see [CONTRIBUTING](./CONTRIBUTING.md) in the repository for guidelines and best practices.
|
Please see [CONTRIBUTING](./CONTRIBUTING.md) in the repository for guidelines and best practices.
|
||||||
|
|
||||||
|
|||||||
72
cli/commands/resetUserSecurityKeys.ts
Normal file
72
cli/commands/resetUserSecurityKeys.ts
Normal file
@@ -0,0 +1,72 @@
|
|||||||
|
import { CommandModule } from "yargs";
|
||||||
|
import { db, users, securityKeys } from "@server/db";
|
||||||
|
import { eq } from "drizzle-orm";
|
||||||
|
|
||||||
|
type ResetUserSecurityKeysArgs = {
|
||||||
|
email: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const resetUserSecurityKeys: CommandModule<
|
||||||
|
{},
|
||||||
|
ResetUserSecurityKeysArgs
|
||||||
|
> = {
|
||||||
|
command: "reset-user-security-keys",
|
||||||
|
describe:
|
||||||
|
"Reset a user's security keys (passkeys) by deleting all their webauthn credentials",
|
||||||
|
builder: (yargs) => {
|
||||||
|
return yargs.option("email", {
|
||||||
|
type: "string",
|
||||||
|
demandOption: true,
|
||||||
|
describe: "User email address"
|
||||||
|
});
|
||||||
|
},
|
||||||
|
handler: async (argv: { email: string }) => {
|
||||||
|
try {
|
||||||
|
const { email } = argv;
|
||||||
|
|
||||||
|
console.log(`Looking for user with email: ${email}`);
|
||||||
|
|
||||||
|
// Find the user by email
|
||||||
|
const [user] = await db
|
||||||
|
.select()
|
||||||
|
.from(users)
|
||||||
|
.where(eq(users.email, email))
|
||||||
|
.limit(1);
|
||||||
|
|
||||||
|
if (!user) {
|
||||||
|
console.error(`User with email '${email}' not found`);
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(`Found user: ${user.email} (ID: ${user.userId})`);
|
||||||
|
|
||||||
|
// Check if user has any security keys
|
||||||
|
const userSecurityKeys = await db
|
||||||
|
.select()
|
||||||
|
.from(securityKeys)
|
||||||
|
.where(eq(securityKeys.userId, user.userId));
|
||||||
|
|
||||||
|
if (userSecurityKeys.length === 0) {
|
||||||
|
console.log(`User '${email}' has no security keys to reset`);
|
||||||
|
process.exit(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(
|
||||||
|
`Found ${userSecurityKeys.length} security key(s) for user '${email}'`
|
||||||
|
);
|
||||||
|
|
||||||
|
// Delete all security keys for the user
|
||||||
|
await db
|
||||||
|
.delete(securityKeys)
|
||||||
|
.where(eq(securityKeys.userId, user.userId));
|
||||||
|
|
||||||
|
console.log(`Successfully reset security keys for user '${email}'`);
|
||||||
|
console.log(`Deleted ${userSecurityKeys.length} security key(s)`);
|
||||||
|
|
||||||
|
process.exit(0);
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error:", error);
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -32,7 +32,8 @@ export const setAdminCredentials: CommandModule<{}, SetAdminCredentialsArgs> = {
|
|||||||
},
|
},
|
||||||
handler: async (argv: { email: string; password: string }) => {
|
handler: async (argv: { email: string; password: string }) => {
|
||||||
try {
|
try {
|
||||||
const { email, password } = argv;
|
let { email, password } = argv;
|
||||||
|
email = email.trim().toLowerCase();
|
||||||
|
|
||||||
const parsed = passwordSchema.safeParse(password);
|
const parsed = passwordSchema.safeParse(password);
|
||||||
|
|
||||||
|
|||||||
@@ -3,9 +3,11 @@
|
|||||||
import yargs from "yargs";
|
import yargs from "yargs";
|
||||||
import { hideBin } from "yargs/helpers";
|
import { hideBin } from "yargs/helpers";
|
||||||
import { setAdminCredentials } from "@cli/commands/setAdminCredentials";
|
import { setAdminCredentials } from "@cli/commands/setAdminCredentials";
|
||||||
|
import { resetUserSecurityKeys } from "@cli/commands/resetUserSecurityKeys";
|
||||||
|
|
||||||
yargs(hideBin(process.argv))
|
yargs(hideBin(process.argv))
|
||||||
.scriptName("pangctl")
|
.scriptName("pangctl")
|
||||||
.command(setAdminCredentials)
|
.command(setAdminCredentials)
|
||||||
|
.command(resetUserSecurityKeys)
|
||||||
.demandCommand()
|
.demandCommand()
|
||||||
.help().argv;
|
.help().argv;
|
||||||
|
|||||||
@@ -1,48 +1,28 @@
|
|||||||
# To see all available options, please visit the docs:
|
# To see all available options, please visit the docs:
|
||||||
# https://docs.fossorial.io/Pangolin/Configuration/config
|
# https://docs.digpangolin.com/self-host/advanced/config-file
|
||||||
|
|
||||||
app:
|
app:
|
||||||
dashboard_url: "http://localhost:3002"
|
dashboard_url: http://localhost:3002
|
||||||
log_level: "info"
|
log_level: debug
|
||||||
save_logs: false
|
|
||||||
|
|
||||||
domains:
|
domains:
|
||||||
domain1:
|
domain1:
|
||||||
base_domain: "example.com"
|
base_domain: example.com
|
||||||
cert_resolver: "letsencrypt"
|
|
||||||
|
|
||||||
server:
|
server:
|
||||||
external_port: 3000
|
secret: my_secret_key
|
||||||
internal_port: 3001
|
|
||||||
next_port: 3002
|
|
||||||
internal_hostname: "pangolin"
|
|
||||||
session_cookie_name: "p_session_token"
|
|
||||||
resource_access_token_param: "p_token"
|
|
||||||
secret: "your_secret_key_here"
|
|
||||||
resource_access_token_headers:
|
|
||||||
id: "P-Access-Token-Id"
|
|
||||||
token: "P-Access-Token"
|
|
||||||
resource_session_request_param: "p_session_request"
|
|
||||||
|
|
||||||
traefik:
|
|
||||||
http_entrypoint: "web"
|
|
||||||
https_entrypoint: "websecure"
|
|
||||||
|
|
||||||
gerbil:
|
gerbil:
|
||||||
start_port: 51820
|
base_endpoint: example.com
|
||||||
base_endpoint: "localhost"
|
|
||||||
block_size: 24
|
|
||||||
site_block_size: 30
|
|
||||||
subnet_group: 100.89.137.0/20
|
|
||||||
use_subdomain: true
|
|
||||||
|
|
||||||
rate_limits:
|
orgs:
|
||||||
global:
|
block_size: 24
|
||||||
window_minutes: 1
|
subnet_group: 100.90.137.0/20
|
||||||
max_requests: 500
|
|
||||||
|
|
||||||
flags:
|
flags:
|
||||||
require_email_verification: false
|
require_email_verification: false
|
||||||
disable_signup_without_invite: true
|
disable_signup_without_invite: true
|
||||||
disable_user_create_org: true
|
disable_user_create_org: true
|
||||||
allow_raw_resources: true
|
allow_raw_resources: true
|
||||||
|
enable_integration_api: true
|
||||||
|
enable_clients: true
|
||||||
|
|||||||
@@ -36,7 +36,7 @@ services:
|
|||||||
- 80:80 # Port for traefik because of the network_mode
|
- 80:80 # Port for traefik because of the network_mode
|
||||||
|
|
||||||
traefik:
|
traefik:
|
||||||
image: traefik:v3.4.0
|
image: traefik:v3.5
|
||||||
container_name: traefik
|
container_name: traefik
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
network_mode: service:gerbil # Ports appear on the gerbil service
|
network_mode: service:gerbil # Ports appear on the gerbil service
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ services:
|
|||||||
- "3000:3000"
|
- "3000:3000"
|
||||||
- "3001:3001"
|
- "3001:3001"
|
||||||
- "3002:3002"
|
- "3002:3002"
|
||||||
|
- "3003:3003"
|
||||||
environment:
|
environment:
|
||||||
- NODE_ENV=development
|
- NODE_ENV=development
|
||||||
- ENVIRONMENT=dev
|
- ENVIRONMENT=dev
|
||||||
@@ -26,4 +27,4 @@ services:
|
|||||||
- ./postcss.config.mjs:/app/postcss.config.mjs
|
- ./postcss.config.mjs:/app/postcss.config.mjs
|
||||||
- ./eslint.config.js:/app/eslint.config.js
|
- ./eslint.config.js:/app/eslint.config.js
|
||||||
- ./config:/app/config
|
- ./config:/app/config
|
||||||
restart: no
|
restart: no
|
||||||
|
|||||||
@@ -64,7 +64,7 @@ esbuild
|
|||||||
}),
|
}),
|
||||||
],
|
],
|
||||||
sourcemap: true,
|
sourcemap: true,
|
||||||
target: "node20",
|
target: "node22",
|
||||||
})
|
})
|
||||||
.then(() => {
|
.then(() => {
|
||||||
console.log("Build completed successfully");
|
console.log("Build completed successfully");
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
# To see all available options, please visit the docs:
|
# To see all available options, please visit the docs:
|
||||||
# https://docs.fossorial.io/Pangolin/Configuration/config
|
# https://docs.digpangolin.com/self-host/dns-and-networking
|
||||||
|
|
||||||
app:
|
app:
|
||||||
dashboard_url: "https://{{.DashboardDomain}}"
|
dashboard_url: "https://{{.DashboardDomain}}"
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ experimental:
|
|||||||
version: "{{.BadgerVersion}}"
|
version: "{{.BadgerVersion}}"
|
||||||
crowdsec: # CrowdSec plugin configuration added
|
crowdsec: # CrowdSec plugin configuration added
|
||||||
moduleName: "github.com/maxlerebourg/crowdsec-bouncer-traefik-plugin"
|
moduleName: "github.com/maxlerebourg/crowdsec-bouncer-traefik-plugin"
|
||||||
version: "v1.4.2"
|
version: "v1.4.4"
|
||||||
|
|
||||||
log:
|
log:
|
||||||
level: "INFO"
|
level: "INFO"
|
||||||
|
|||||||
@@ -36,7 +36,7 @@ services:
|
|||||||
- 80:80 # Port for traefik because of the network_mode
|
- 80:80 # Port for traefik because of the network_mode
|
||||||
{{end}}
|
{{end}}
|
||||||
traefik:
|
traefik:
|
||||||
image: docker.io/traefik:v3.4.1
|
image: docker.io/traefik:v3.5
|
||||||
container_name: traefik
|
container_name: traefik
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
{{if .InstallGerbil}}
|
{{if .InstallGerbil}}
|
||||||
@@ -60,4 +60,4 @@ networks:
|
|||||||
default:
|
default:
|
||||||
driver: bridge
|
driver: bridge
|
||||||
name: pangolin
|
name: pangolin
|
||||||
enable_ipv6: true
|
{{if .EnableIPv6}} enable_ipv6: true{{end}}
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
module installer
|
module installer
|
||||||
|
|
||||||
go 1.23.0
|
go 1.24
|
||||||
|
|
||||||
require (
|
require (
|
||||||
golang.org/x/term v0.28.0
|
golang.org/x/term v0.33.0
|
||||||
gopkg.in/yaml.v3 v3.0.1
|
gopkg.in/yaml.v3 v3.0.1
|
||||||
)
|
)
|
||||||
|
|
||||||
require golang.org/x/sys v0.29.0 // indirect
|
require golang.org/x/sys v0.34.0 // indirect
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
golang.org/x/sys v0.29.0 h1:TPYlXGxvx1MGTn2GiZDhnjPA9wZzZeGKHHmKhHYvgaU=
|
golang.org/x/sys v0.34.0 h1:H5Y5sJ2L2JRdyv7ROF1he/lPdvFsd0mJHFw2ThKHxLA=
|
||||||
golang.org/x/sys v0.29.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
golang.org/x/sys v0.34.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
|
||||||
golang.org/x/term v0.28.0 h1:/Ts8HFuMR2E6IP/jlo7QVLZHggjKQbhu/7H0LJFr3Gg=
|
golang.org/x/term v0.33.0 h1:NuFncQrRcaRvVmgRkvM3j/F00gWIAlcmlB8ACEKmGIg=
|
||||||
golang.org/x/term v0.28.0/go.mod h1:Sw/lC2IAUZ92udQNf3WodGtn4k/XoLyZoh8v/8uiwek=
|
golang.org/x/term v0.33.0/go.mod h1:s18+ql9tYWp1IfpV9DmCtQDDSRBUjKaw9M1eAv5UeF0=
|
||||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
|
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/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
docker
|
docker
|
||||||
example.com
|
example.com
|
||||||
pangolin.example.com
|
pangolin.example.com
|
||||||
|
yes
|
||||||
admin@example.com
|
admin@example.com
|
||||||
yes
|
yes
|
||||||
admin@example.com
|
admin@example.com
|
||||||
|
|||||||
170
install/main.go
170
install/main.go
@@ -18,6 +18,7 @@ import (
|
|||||||
"syscall"
|
"syscall"
|
||||||
"text/template"
|
"text/template"
|
||||||
"time"
|
"time"
|
||||||
|
"net"
|
||||||
|
|
||||||
"golang.org/x/term"
|
"golang.org/x/term"
|
||||||
)
|
)
|
||||||
@@ -39,6 +40,7 @@ type Config struct {
|
|||||||
BadgerVersion string
|
BadgerVersion string
|
||||||
BaseDomain string
|
BaseDomain string
|
||||||
DashboardDomain string
|
DashboardDomain string
|
||||||
|
EnableIPv6 bool
|
||||||
LetsEncryptEmail string
|
LetsEncryptEmail string
|
||||||
EnableEmail bool
|
EnableEmail bool
|
||||||
EmailSMTPHost string
|
EmailSMTPHost string
|
||||||
@@ -70,11 +72,22 @@ func main() {
|
|||||||
fmt.Println("- Open TCP ports 80 and 443 and UDP ports 51820 and 21820 on your VPS and firewall.")
|
fmt.Println("- Open TCP ports 80 and 443 and UDP ports 51820 and 21820 on your VPS and firewall.")
|
||||||
fmt.Println("- Point your domain to the VPS IP with A records.")
|
fmt.Println("- Point your domain to the VPS IP with A records.")
|
||||||
fmt.Println("")
|
fmt.Println("")
|
||||||
fmt.Println("http://docs.fossorial.io/Getting%20Started/dns-networking")
|
fmt.Println("https://docs.digpangolin.com/self-host/dns-and-networking")
|
||||||
fmt.Println("")
|
fmt.Println("")
|
||||||
fmt.Println("Lets get started!")
|
fmt.Println("Lets get started!")
|
||||||
fmt.Println("")
|
fmt.Println("")
|
||||||
|
|
||||||
|
if os.Geteuid() == 0 { // WE NEED TO BE SUDO TO CHECK THIS
|
||||||
|
for _, p := range []int{80, 443} {
|
||||||
|
if err := checkPortsAvailable(p); err != nil {
|
||||||
|
fmt.Fprintln(os.Stderr, err)
|
||||||
|
|
||||||
|
fmt.Printf("Please close any services on ports 80/443 in order to run the installation smoothly")
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
reader := bufio.NewReader(os.Stdin)
|
reader := bufio.NewReader(os.Stdin)
|
||||||
inputContainer := readString(reader, "Would you like to run Pangolin as Docker or Podman containers?", "docker")
|
inputContainer := readString(reader, "Would you like to run Pangolin as Docker or Podman containers?", "docker")
|
||||||
|
|
||||||
@@ -202,6 +215,28 @@ func main() {
|
|||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
fmt.Println("Looks like you already installed, so I am going to do the setup...")
|
fmt.Println("Looks like you already installed, so I am going to do the setup...")
|
||||||
|
|
||||||
|
// Read existing config to get DashboardDomain
|
||||||
|
traefikConfig, err := ReadTraefikConfig("config/traefik/traefik_config.yml", "config/traefik/dynamic_config.yml")
|
||||||
|
if err != nil {
|
||||||
|
fmt.Printf("Warning: Could not read existing config: %v\n", err)
|
||||||
|
fmt.Println("You may need to manually enter your domain information.")
|
||||||
|
config = collectUserInput(reader)
|
||||||
|
} else {
|
||||||
|
config.DashboardDomain = traefikConfig.DashboardDomain
|
||||||
|
config.LetsEncryptEmail = traefikConfig.LetsEncryptEmail
|
||||||
|
config.BadgerVersion = traefikConfig.BadgerVersion
|
||||||
|
|
||||||
|
// Show detected values and allow user to confirm or re-enter
|
||||||
|
fmt.Println("Detected existing configuration:")
|
||||||
|
fmt.Printf("Dashboard Domain: %s\n", config.DashboardDomain)
|
||||||
|
fmt.Printf("Let's Encrypt Email: %s\n", config.LetsEncryptEmail)
|
||||||
|
fmt.Printf("Badger Version: %s\n", config.BadgerVersion)
|
||||||
|
|
||||||
|
if !readBool(reader, "Are these values correct?", true) {
|
||||||
|
config = collectUserInput(reader)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if !checkIsCrowdsecInstalledInCompose() {
|
if !checkIsCrowdsecInstalledInCompose() {
|
||||||
@@ -239,6 +274,23 @@ func main() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Setup Token Section
|
||||||
|
fmt.Println("\n=== Setup Token ===")
|
||||||
|
|
||||||
|
// Check if containers were started during this installation
|
||||||
|
containersStarted := false
|
||||||
|
if (isDockerInstalled() && chosenContainer == Docker) ||
|
||||||
|
(isPodmanInstalled() && chosenContainer == Podman) {
|
||||||
|
// Try to fetch and display the token if containers are running
|
||||||
|
containersStarted = true
|
||||||
|
printSetupToken(chosenContainer, config.DashboardDomain)
|
||||||
|
}
|
||||||
|
|
||||||
|
// If containers weren't started or token wasn't found, show instructions
|
||||||
|
if !containersStarted {
|
||||||
|
showSetupTokenInstructions(chosenContainer, config.DashboardDomain)
|
||||||
|
}
|
||||||
|
|
||||||
fmt.Println("Installation complete!")
|
fmt.Println("Installation complete!")
|
||||||
fmt.Printf("\nTo complete the initial setup, please visit:\nhttps://%s/auth/initial-setup\n", config.DashboardDomain)
|
fmt.Printf("\nTo complete the initial setup, please visit:\nhttps://%s/auth/initial-setup\n", config.DashboardDomain)
|
||||||
}
|
}
|
||||||
@@ -302,9 +354,16 @@ func collectUserInput(reader *bufio.Reader) Config {
|
|||||||
// Basic configuration
|
// Basic configuration
|
||||||
fmt.Println("\n=== Basic Configuration ===")
|
fmt.Println("\n=== Basic Configuration ===")
|
||||||
config.BaseDomain = readString(reader, "Enter your base domain (no subdomain e.g. example.com)", "")
|
config.BaseDomain = readString(reader, "Enter your base domain (no subdomain e.g. example.com)", "")
|
||||||
config.DashboardDomain = readString(reader, "Enter the domain for the Pangolin dashboard", "pangolin."+config.BaseDomain)
|
|
||||||
|
// Set default dashboard domain after base domain is collected
|
||||||
|
defaultDashboardDomain := ""
|
||||||
|
if config.BaseDomain != "" {
|
||||||
|
defaultDashboardDomain = "pangolin." + config.BaseDomain
|
||||||
|
}
|
||||||
|
config.DashboardDomain = readString(reader, "Enter the domain for the Pangolin dashboard", defaultDashboardDomain)
|
||||||
config.LetsEncryptEmail = readString(reader, "Enter email for Let's Encrypt certificates", "")
|
config.LetsEncryptEmail = readString(reader, "Enter email for Let's Encrypt certificates", "")
|
||||||
config.InstallGerbil = readBool(reader, "Do you want to use Gerbil to allow tunneled connections", true)
|
config.InstallGerbil = readBool(reader, "Do you want to use Gerbil to allow tunneled connections", true)
|
||||||
|
config.EnableIPv6 = readBool(reader, "Is your server IPv6 capable?", true)
|
||||||
|
|
||||||
// Email configuration
|
// Email configuration
|
||||||
fmt.Println("\n=== Email Configuration ===")
|
fmt.Println("\n=== Email Configuration ===")
|
||||||
@@ -625,8 +684,8 @@ func pullContainers(containerType SupportedContainer) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if containerType == Docker {
|
if containerType == Docker {
|
||||||
if err := executeDockerComposeCommandWithArgs("-f", "docker-compose.yml", "pull", "--policy", "always"); err != nil {
|
if err := executeDockerComposeCommandWithArgs("-f", "docker-compose.yml", "pull", "--policy", "always"); err != nil {
|
||||||
return fmt.Errorf("failed to pull the containers: %v", err)
|
return fmt.Errorf("failed to pull the containers: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
@@ -755,6 +814,91 @@ func waitForContainer(containerName string, containerType SupportedContainer) er
|
|||||||
return fmt.Errorf("container %s did not start within %v seconds", containerName, maxAttempts*int(retryInterval.Seconds()))
|
return fmt.Errorf("container %s did not start within %v seconds", containerName, maxAttempts*int(retryInterval.Seconds()))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func printSetupToken(containerType SupportedContainer, dashboardDomain string) {
|
||||||
|
fmt.Println("Waiting for Pangolin to generate setup token...")
|
||||||
|
|
||||||
|
// Wait for Pangolin to be healthy
|
||||||
|
if err := waitForContainer("pangolin", containerType); err != nil {
|
||||||
|
fmt.Println("Warning: Pangolin container did not become healthy in time.")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Give a moment for the setup token to be generated
|
||||||
|
time.Sleep(2 * time.Second)
|
||||||
|
|
||||||
|
// Fetch logs
|
||||||
|
var cmd *exec.Cmd
|
||||||
|
if containerType == Docker {
|
||||||
|
cmd = exec.Command("docker", "logs", "pangolin")
|
||||||
|
} else {
|
||||||
|
cmd = exec.Command("podman", "logs", "pangolin")
|
||||||
|
}
|
||||||
|
output, err := cmd.Output()
|
||||||
|
if err != nil {
|
||||||
|
fmt.Println("Warning: Could not fetch Pangolin logs to find setup token.")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse for setup token
|
||||||
|
lines := strings.Split(string(output), "\n")
|
||||||
|
for i, line := range lines {
|
||||||
|
if strings.Contains(line, "=== SETUP TOKEN GENERATED ===") || strings.Contains(line, "=== SETUP TOKEN EXISTS ===") {
|
||||||
|
// Look for "Token: ..." in the next few lines
|
||||||
|
for j := i + 1; j < i+5 && j < len(lines); j++ {
|
||||||
|
trimmedLine := strings.TrimSpace(lines[j])
|
||||||
|
if strings.Contains(trimmedLine, "Token:") {
|
||||||
|
// Extract token after "Token:"
|
||||||
|
tokenStart := strings.Index(trimmedLine, "Token:")
|
||||||
|
if tokenStart != -1 {
|
||||||
|
token := strings.TrimSpace(trimmedLine[tokenStart+6:])
|
||||||
|
fmt.Printf("Setup token: %s\n", token)
|
||||||
|
fmt.Println("")
|
||||||
|
fmt.Println("This token is required to register the first admin account in the web UI at:")
|
||||||
|
fmt.Printf("https://%s/auth/initial-setup\n", dashboardDomain)
|
||||||
|
fmt.Println("")
|
||||||
|
fmt.Println("Save this token securely. It will be invalid after the first admin is created.")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
fmt.Println("Warning: Could not find a setup token in Pangolin logs.")
|
||||||
|
}
|
||||||
|
|
||||||
|
func showSetupTokenInstructions(containerType SupportedContainer, dashboardDomain string) {
|
||||||
|
fmt.Println("\n=== Setup Token Instructions ===")
|
||||||
|
fmt.Println("To get your setup token, you need to:")
|
||||||
|
fmt.Println("")
|
||||||
|
fmt.Println("1. Start the containers:")
|
||||||
|
if containerType == Docker {
|
||||||
|
fmt.Println(" docker-compose up -d")
|
||||||
|
} else {
|
||||||
|
fmt.Println(" podman-compose up -d")
|
||||||
|
}
|
||||||
|
fmt.Println("")
|
||||||
|
fmt.Println("2. Wait for the Pangolin container to start and generate the token")
|
||||||
|
fmt.Println("")
|
||||||
|
fmt.Println("3. Check the container logs for the setup token:")
|
||||||
|
if containerType == Docker {
|
||||||
|
fmt.Println(" docker logs pangolin | grep -A 2 -B 2 'SETUP TOKEN'")
|
||||||
|
} else {
|
||||||
|
fmt.Println(" podman logs pangolin | grep -A 2 -B 2 'SETUP TOKEN'")
|
||||||
|
}
|
||||||
|
fmt.Println("")
|
||||||
|
fmt.Println("4. Look for output like:")
|
||||||
|
fmt.Println(" === SETUP TOKEN GENERATED ===")
|
||||||
|
fmt.Println(" Token: [your-token-here]")
|
||||||
|
fmt.Println(" Use this token on the initial setup page")
|
||||||
|
fmt.Println("")
|
||||||
|
fmt.Println("5. Use the token to complete initial setup at:")
|
||||||
|
fmt.Printf(" https://%s/auth/initial-setup\n", dashboardDomain)
|
||||||
|
fmt.Println("")
|
||||||
|
fmt.Println("The setup token is required to register the first admin account.")
|
||||||
|
fmt.Println("Save it securely - it will be invalid after the first admin is created.")
|
||||||
|
fmt.Println("================================")
|
||||||
|
}
|
||||||
|
|
||||||
func generateRandomSecretKey() string {
|
func generateRandomSecretKey() string {
|
||||||
const charset = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"
|
const charset = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"
|
||||||
const length = 32
|
const length = 32
|
||||||
@@ -776,3 +920,21 @@ func run(name string, args ...string) error {
|
|||||||
cmd.Stderr = os.Stderr
|
cmd.Stderr = os.Stderr
|
||||||
return cmd.Run()
|
return cmd.Run()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func checkPortsAvailable(port int) error {
|
||||||
|
addr := fmt.Sprintf(":%d", port)
|
||||||
|
ln, err := net.Listen("tcp", addr)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf(
|
||||||
|
"ERROR: port %d is occupied or cannot be bound: %w\n\n",
|
||||||
|
port, err,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
if closeErr := ln.Close(); closeErr != nil {
|
||||||
|
fmt.Fprintf(os.Stderr,
|
||||||
|
"WARNING: failed to close test listener on port %d: %v\n",
|
||||||
|
port, closeErr,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|||||||
1348
messages/bg-BG.json
Normal file
1348
messages/bg-BG.json
Normal file
File diff suppressed because it is too large
Load Diff
@@ -833,6 +833,24 @@
|
|||||||
"pincodeRequirementsLength": "PIN must be exactly 6 digits",
|
"pincodeRequirementsLength": "PIN must be exactly 6 digits",
|
||||||
"pincodeRequirementsChars": "PIN must only contain numbers",
|
"pincodeRequirementsChars": "PIN must only contain numbers",
|
||||||
"passwordRequirementsLength": "Password must be at least 1 character long",
|
"passwordRequirementsLength": "Password must be at least 1 character long",
|
||||||
|
"passwordRequirementsTitle": "Password requirements:",
|
||||||
|
"passwordRequirementLength": "At least 8 characters long",
|
||||||
|
"passwordRequirementUppercase": "At least one uppercase letter",
|
||||||
|
"passwordRequirementLowercase": "At least one lowercase letter",
|
||||||
|
"passwordRequirementNumber": "At least one number",
|
||||||
|
"passwordRequirementSpecial": "At least one special character",
|
||||||
|
"passwordRequirementsMet": "✓ Password meets all requirements",
|
||||||
|
"passwordStrength": "Password strength",
|
||||||
|
"passwordStrengthWeak": "Weak",
|
||||||
|
"passwordStrengthMedium": "Medium",
|
||||||
|
"passwordStrengthStrong": "Strong",
|
||||||
|
"passwordRequirements": "Requirements:",
|
||||||
|
"passwordRequirementLengthText": "8+ characters",
|
||||||
|
"passwordRequirementUppercaseText": "Uppercase letter (A-Z)",
|
||||||
|
"passwordRequirementLowercaseText": "Lowercase letter (a-z)",
|
||||||
|
"passwordRequirementNumberText": "Number (0-9)",
|
||||||
|
"passwordRequirementSpecialText": "Special character (!@#$%...)",
|
||||||
|
"passwordsDoNotMatch": "Passwords do not match",
|
||||||
"otpEmailRequirementsLength": "OTP must be at least 1 character long",
|
"otpEmailRequirementsLength": "OTP must be at least 1 character long",
|
||||||
"otpEmailSent": "OTP Sent",
|
"otpEmailSent": "OTP Sent",
|
||||||
"otpEmailSentDescription": "An OTP has been sent to your email",
|
"otpEmailSentDescription": "An OTP has been sent to your email",
|
||||||
@@ -967,6 +985,9 @@
|
|||||||
"actionDeleteSite": "Delete Site",
|
"actionDeleteSite": "Delete Site",
|
||||||
"actionGetSite": "Get Site",
|
"actionGetSite": "Get Site",
|
||||||
"actionListSites": "List Sites",
|
"actionListSites": "List Sites",
|
||||||
|
"setupToken": "Setup Token",
|
||||||
|
"setupTokenPlaceholder": "Enter the setup token from the server console",
|
||||||
|
"setupTokenRequired": "Setup token is required",
|
||||||
"actionUpdateSite": "Update Site",
|
"actionUpdateSite": "Update Site",
|
||||||
"actionListSiteRoles": "List Allowed Site Roles",
|
"actionListSiteRoles": "List Allowed Site Roles",
|
||||||
"actionCreateResource": "Create Resource",
|
"actionCreateResource": "Create Resource",
|
||||||
@@ -1022,6 +1043,11 @@
|
|||||||
"actionDeleteIdpOrg": "Delete IDP Org Policy",
|
"actionDeleteIdpOrg": "Delete IDP Org Policy",
|
||||||
"actionListIdpOrgs": "List IDP Orgs",
|
"actionListIdpOrgs": "List IDP Orgs",
|
||||||
"actionUpdateIdpOrg": "Update IDP Org",
|
"actionUpdateIdpOrg": "Update IDP Org",
|
||||||
|
"actionCreateClient": "Create Client",
|
||||||
|
"actionDeleteClient": "Delete Client",
|
||||||
|
"actionUpdateClient": "Update Client",
|
||||||
|
"actionListClients": "List Clients",
|
||||||
|
"actionGetClient": "Get Client",
|
||||||
"noneSelected": "None selected",
|
"noneSelected": "None selected",
|
||||||
"orgNotFound2": "No organizations found.",
|
"orgNotFound2": "No organizations found.",
|
||||||
"searchProgress": "Search...",
|
"searchProgress": "Search...",
|
||||||
@@ -1315,8 +1341,8 @@
|
|||||||
"olmErrorFetchLatest": "An error occurred while fetching the latest Olm release.",
|
"olmErrorFetchLatest": "An error occurred while fetching the latest Olm release.",
|
||||||
"remoteSubnets": "Remote Subnets",
|
"remoteSubnets": "Remote Subnets",
|
||||||
"enterCidrRange": "Enter CIDR range",
|
"enterCidrRange": "Enter CIDR range",
|
||||||
"remoteSubnetsDescription": "Add CIDR ranges that can access this site remotely. Use format like 10.0.0.0/24 or 192.168.1.0/24.",
|
"remoteSubnetsDescription": "Add CIDR ranges that can be accessed from this site remotely using clients. Use format like 10.0.0.0/24. This ONLY applies to VPN client connectivity.",
|
||||||
"resourceEnableProxy": "Enable Public Proxy",
|
"resourceEnableProxy": "Enable Public Proxy",
|
||||||
"resourceEnableProxyDescription": "Enable public proxying to this resource. This allows access to the resource from outside the network through the cloud on an open port. Requires Traefik config.",
|
"resourceEnableProxyDescription": "Enable public proxying to this resource. This allows access to the resource from outside the network through the cloud on an open port. Requires Traefik config.",
|
||||||
"externalProxyEnabled": "External Proxy Enabled"
|
"externalProxyEnabled": "External Proxy Enabled"
|
||||||
}
|
}
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
{
|
{
|
||||||
"setupCreate": "Erstelle eine Organisation, Site und Ressourcen",
|
"setupCreate": "Erstelle eine Organisation, einen Standort und Ressourcen",
|
||||||
"setupNewOrg": "Neue Organisation",
|
"setupNewOrg": "Neue Organisation",
|
||||||
"setupCreateOrg": "Organisation erstellen",
|
"setupCreateOrg": "Organisation erstellen",
|
||||||
"setupCreateResources": "Ressource erstellen",
|
"setupCreateResources": "Ressource erstellen",
|
||||||
@@ -16,7 +16,7 @@
|
|||||||
"componentsMember": "Du bist Mitglied von {count, plural, =0 {keiner Organisation} one {einer Organisation} other {# Organisationen}}.",
|
"componentsMember": "Du bist Mitglied von {count, plural, =0 {keiner Organisation} one {einer Organisation} other {# Organisationen}}.",
|
||||||
"componentsInvalidKey": "Ungültige oder abgelaufene Lizenzschlüssel erkannt. Beachte die Lizenzbedingungen, um alle Funktionen weiterhin zu nutzen.",
|
"componentsInvalidKey": "Ungültige oder abgelaufene Lizenzschlüssel erkannt. Beachte die Lizenzbedingungen, um alle Funktionen weiterhin zu nutzen.",
|
||||||
"dismiss": "Verwerfen",
|
"dismiss": "Verwerfen",
|
||||||
"componentsLicenseViolation": "Lizenzverstoß: Dieser Server benutzt {usedSites} Sites, die das Lizenzlimit der {maxSites} Sites überschreiten. Beachte die Lizenzbedingungen, um alle Funktionen weiterhin zu nutzen.",
|
"componentsLicenseViolation": "Lizenzverstoß: Dieser Server benutzt {usedSites} Standorte, was das Lizenzlimit von {maxSites} Standorten überschreitet. Beachte die Lizenzbedingungen, um alle Funktionen weiterhin zu nutzen.",
|
||||||
"componentsSupporterMessage": "Vielen Dank für die Unterstützung von Pangolin als {tier}!",
|
"componentsSupporterMessage": "Vielen Dank für die Unterstützung von Pangolin als {tier}!",
|
||||||
"inviteErrorNotValid": "Es tut uns leid, aber es sieht so aus, als wäre die Einladung, auf die du zugreifen möchtest, entweder nicht angenommen worden oder nicht mehr gültig.",
|
"inviteErrorNotValid": "Es tut uns leid, aber es sieht so aus, als wäre die Einladung, auf die du zugreifen möchtest, entweder nicht angenommen worden oder nicht mehr gültig.",
|
||||||
"inviteErrorUser": "Es tut uns leid, aber es scheint, als sei die Einladung, auf die du zugreifen möchtest, nicht für diesen Benutzer bestimmt.",
|
"inviteErrorUser": "Es tut uns leid, aber es scheint, als sei die Einladung, auf die du zugreifen möchtest, nicht für diesen Benutzer bestimmt.",
|
||||||
@@ -38,25 +38,25 @@
|
|||||||
"name": "Name",
|
"name": "Name",
|
||||||
"online": "Online",
|
"online": "Online",
|
||||||
"offline": "Offline",
|
"offline": "Offline",
|
||||||
"site": "Seite",
|
"site": "Standort",
|
||||||
"dataIn": "Daten eingehend",
|
"dataIn": "Daten eingehend",
|
||||||
"dataOut": "Daten ausgehend",
|
"dataOut": "Daten ausgehend",
|
||||||
"connectionType": "Verbindungstyp",
|
"connectionType": "Verbindungstyp",
|
||||||
"tunnelType": "Tunneltyp",
|
"tunnelType": "Tunneltyp",
|
||||||
"local": "Lokal",
|
"local": "Lokal",
|
||||||
"edit": "Bearbeiten",
|
"edit": "Bearbeiten",
|
||||||
"siteConfirmDelete": "Site löschen bestätigen",
|
"siteConfirmDelete": "Standort löschen bestätigen",
|
||||||
"siteDelete": "Site löschen",
|
"siteDelete": "Standort löschen",
|
||||||
"siteMessageRemove": "Sobald diese Seite entfernt ist, wird sie nicht mehr zugänglich sein. Alle Ressourcen und Ziele, die mit der Site verbunden sind, werden ebenfalls entfernt.",
|
"siteMessageRemove": "Sobald dieser Standort entfernt ist, wird er nicht mehr zugänglich sein. Alle Ressourcen und Ziele, die mit diesem Standort verbunden sind, werden ebenfalls entfernt.",
|
||||||
"siteMessageConfirm": "Um zu bestätigen, gib den Namen der Site ein.",
|
"siteMessageConfirm": "Um zu bestätigen, gib den Namen des Standortes unten ein.",
|
||||||
"siteQuestionRemove": "Bist du sicher, dass Sie die Site {selectedSite} aus der Organisation entfernt werden soll?",
|
"siteQuestionRemove": "Bist du sicher, dass der Standort {selectedSite} aus der Organisation entfernt werden soll?",
|
||||||
"siteManageSites": "Sites verwalten",
|
"siteManageSites": "Standorte verwalten",
|
||||||
"siteDescription": "Verbindung zum Netzwerk durch sichere Tunnel erlauben",
|
"siteDescription": "Verbindung zum Netzwerk durch sichere Tunnel erlauben",
|
||||||
"siteCreate": "Site erstellen",
|
"siteCreate": "Standort erstellen",
|
||||||
"siteCreateDescription2": "Folge den nachfolgenden Schritten, um eine neue Site zu erstellen und zu verbinden",
|
"siteCreateDescription2": "Folge den nachfolgenden Schritten, um einen neuen Standort zu erstellen und zu verbinden",
|
||||||
"siteCreateDescription": "Erstelle eine neue Site, um Ressourcen zu verbinden",
|
"siteCreateDescription": "Erstelle einen neuen Standort, um Ressourcen zu verbinden",
|
||||||
"close": "Schließen",
|
"close": "Schließen",
|
||||||
"siteErrorCreate": "Fehler beim Erstellen der Site",
|
"siteErrorCreate": "Fehler beim Erstellen des Standortes",
|
||||||
"siteErrorCreateKeyPair": "Schlüsselpaar oder Standardwerte nicht gefunden",
|
"siteErrorCreateKeyPair": "Schlüsselpaar oder Standardwerte nicht gefunden",
|
||||||
"siteErrorCreateDefaults": "Standardwerte der Site nicht gefunden",
|
"siteErrorCreateDefaults": "Standardwerte der Site nicht gefunden",
|
||||||
"method": "Methode",
|
"method": "Methode",
|
||||||
@@ -70,8 +70,8 @@
|
|||||||
"dockerRun": "Docker Run",
|
"dockerRun": "Docker Run",
|
||||||
"siteLearnLocal": "Mehr Infos zu lokalen Sites",
|
"siteLearnLocal": "Mehr Infos zu lokalen Sites",
|
||||||
"siteConfirmCopy": "Ich habe die Konfiguration kopiert",
|
"siteConfirmCopy": "Ich habe die Konfiguration kopiert",
|
||||||
"searchSitesProgress": "Sites durchsuchen...",
|
"searchSitesProgress": "Standorte durchsuchen...",
|
||||||
"siteAdd": "Site hinzufügen",
|
"siteAdd": "Standort hinzufügen",
|
||||||
"siteInstallNewt": "Newt installieren",
|
"siteInstallNewt": "Newt installieren",
|
||||||
"siteInstallNewtDescription": "Installiere Newt auf deinem System.",
|
"siteInstallNewtDescription": "Installiere Newt auf deinem System.",
|
||||||
"WgConfiguration": "WireGuard Konfiguration",
|
"WgConfiguration": "WireGuard Konfiguration",
|
||||||
@@ -82,26 +82,26 @@
|
|||||||
"siteNewtDescription": "Nutze Newt für die beste Benutzererfahrung. Newt verwendet WireGuard as Basis und erlaubt Ihnen, Ihre privaten Ressourcen über ihre LAN-Adresse in Ihrem privaten Netzwerk aus dem Pangolin-Dashboard heraus zu adressieren.",
|
"siteNewtDescription": "Nutze Newt für die beste Benutzererfahrung. Newt verwendet WireGuard as Basis und erlaubt Ihnen, Ihre privaten Ressourcen über ihre LAN-Adresse in Ihrem privaten Netzwerk aus dem Pangolin-Dashboard heraus zu adressieren.",
|
||||||
"siteRunsInDocker": "Läuft in Docker",
|
"siteRunsInDocker": "Läuft in Docker",
|
||||||
"siteRunsInShell": "Läuft in der Konsole auf macOS, Linux und Windows",
|
"siteRunsInShell": "Läuft in der Konsole auf macOS, Linux und Windows",
|
||||||
"siteErrorDelete": "Fehler beim Löschen der Site",
|
"siteErrorDelete": "Fehler beim Löschen des Standortes",
|
||||||
"siteErrorUpdate": "Fehler beim Aktualisieren der Site",
|
"siteErrorUpdate": "Fehler beim Aktualisieren des Standortes",
|
||||||
"siteErrorUpdateDescription": "Beim Aktualisieren der Site ist ein Fehler aufgetreten.",
|
"siteErrorUpdateDescription": "Beim Aktualisieren des Standortes ist ein Fehler aufgetreten.",
|
||||||
"siteUpdated": "Site aktualisiert",
|
"siteUpdated": "Standort aktualisiert",
|
||||||
"siteUpdatedDescription": "Die Site wurde aktualisiert.",
|
"siteUpdatedDescription": "Der Standort wurde aktualisiert.",
|
||||||
"siteGeneralDescription": "Allgemeine Einstellungen für diese Site konfigurieren",
|
"siteGeneralDescription": "Allgemeine Einstellungen für diesen Standort konfigurieren",
|
||||||
"siteSettingDescription": "Konfigurieren der Site Einstellungen",
|
"siteSettingDescription": "Konfigurieren der Standort Einstellungen",
|
||||||
"siteSetting": "{siteName} Einstellungen",
|
"siteSetting": "{siteName} Einstellungen",
|
||||||
"siteNewtTunnel": "Newt-Tunnel (empfohlen)",
|
"siteNewtTunnel": "Newt-Tunnel (empfohlen)",
|
||||||
"siteNewtTunnelDescription": "Einfachster Weg, einen Zugriffspunkt zu deinem Netzwerk zu erstellen. Keine zusätzliche Einrichtung erforderlich.",
|
"siteNewtTunnelDescription": "Einfachster Weg, einen Zugriffspunkt zu deinem Netzwerk zu erstellen. Keine zusätzliche Einrichtung erforderlich.",
|
||||||
"siteWg": "Einfacher WireGuard Tunnel",
|
"siteWg": "Einfacher WireGuard Tunnel",
|
||||||
"siteWgDescription": "Verwende jeden WireGuard-Client, um einen Tunnel einzurichten. Manuelles NAT-Setup erforderlich.",
|
"siteWgDescription": "Verwende jeden WireGuard-Client, um einen Tunnel einzurichten. Manuelles NAT-Setup erforderlich.",
|
||||||
"siteLocalDescription": "Nur lokale Ressourcen. Kein Tunneling.",
|
"siteLocalDescription": "Nur lokale Ressourcen. Kein Tunneling.",
|
||||||
"siteSeeAll": "Alle Sites anzeigen",
|
"siteSeeAll": "Alle Standorte anzeigen",
|
||||||
"siteTunnelDescription": "Lege fest, wie du dich mit deiner Site verbinden möchtest",
|
"siteTunnelDescription": "Lege fest, wie du dich mit deinem Standort verbinden möchtest",
|
||||||
"siteNewtCredentials": "Neue Newt Zugangsdaten",
|
"siteNewtCredentials": "Neue Newt Zugangsdaten",
|
||||||
"siteNewtCredentialsDescription": "So wird sich Newt mit dem Server authentifizieren",
|
"siteNewtCredentialsDescription": "So wird sich Newt mit dem Server authentifizieren",
|
||||||
"siteCredentialsSave": "Ihre Zugangsdaten speichern",
|
"siteCredentialsSave": "Ihre Zugangsdaten speichern",
|
||||||
"siteCredentialsSaveDescription": "Du kannst das nur einmal sehen. Stelle sicher, dass du es an einen sicheren Ort kopierst.",
|
"siteCredentialsSaveDescription": "Du kannst das nur einmal sehen. Stelle sicher, dass du es an einen sicheren Ort kopierst.",
|
||||||
"siteInfo": "Site-Informationen",
|
"siteInfo": "Standort-Informationen",
|
||||||
"status": "Status",
|
"status": "Status",
|
||||||
"shareTitle": "Links zum Teilen verwalten",
|
"shareTitle": "Links zum Teilen verwalten",
|
||||||
"shareDescription": "Erstellen Sie teilbare Links, um temporären oder permanenten Zugriff auf Ihre Ressourcen zu gewähren",
|
"shareDescription": "Erstellen Sie teilbare Links, um temporären oder permanenten Zugriff auf Ihre Ressourcen zu gewähren",
|
||||||
@@ -163,10 +163,10 @@
|
|||||||
"resourceSeeAll": "Alle Ressourcen anzeigen",
|
"resourceSeeAll": "Alle Ressourcen anzeigen",
|
||||||
"resourceInfo": "Ressourcen-Informationen",
|
"resourceInfo": "Ressourcen-Informationen",
|
||||||
"resourceNameDescription": "Dies ist der Anzeigename für die Ressource.",
|
"resourceNameDescription": "Dies ist der Anzeigename für die Ressource.",
|
||||||
"siteSelect": "Site auswählen",
|
"siteSelect": "Standort auswählen",
|
||||||
"siteSearch": "Website durchsuchen",
|
"siteSearch": "Standorte durchsuchen",
|
||||||
"siteNotFound": "Keine Site gefunden.",
|
"siteNotFound": "Keinen Standort gefunden.",
|
||||||
"siteSelectionDescription": "Diese Seite wird die Verbindung zu der Ressource herstellen.",
|
"siteSelectionDescription": "Dieser Standort wird die Verbindung zu der Ressource herstellen.",
|
||||||
"resourceType": "Ressourcentyp",
|
"resourceType": "Ressourcentyp",
|
||||||
"resourceTypeDescription": "Legen Sie fest, wie Sie auf Ihre Ressource zugreifen möchten",
|
"resourceTypeDescription": "Legen Sie fest, wie Sie auf Ihre Ressource zugreifen möchten",
|
||||||
"resourceHTTPSSettings": "HTTPS-Einstellungen",
|
"resourceHTTPSSettings": "HTTPS-Einstellungen",
|
||||||
@@ -302,7 +302,7 @@
|
|||||||
"userQuestionRemove": "Sind Sie sicher, dass Sie {selectedUser} dauerhaft vom Server löschen möchten?",
|
"userQuestionRemove": "Sind Sie sicher, dass Sie {selectedUser} dauerhaft vom Server löschen möchten?",
|
||||||
"licenseKey": "Lizenzschlüssel",
|
"licenseKey": "Lizenzschlüssel",
|
||||||
"valid": "Gültig",
|
"valid": "Gültig",
|
||||||
"numberOfSites": "Anzahl der Sites",
|
"numberOfSites": "Anzahl der Standorte",
|
||||||
"licenseKeySearch": "Lizenzschlüssel suchen...",
|
"licenseKeySearch": "Lizenzschlüssel suchen...",
|
||||||
"licenseKeyAdd": "Lizenzschlüssel hinzufügen",
|
"licenseKeyAdd": "Lizenzschlüssel hinzufügen",
|
||||||
"type": "Typ",
|
"type": "Typ",
|
||||||
@@ -342,16 +342,16 @@
|
|||||||
"licensedNot": "Nicht lizenziert",
|
"licensedNot": "Nicht lizenziert",
|
||||||
"hostId": "Host-ID",
|
"hostId": "Host-ID",
|
||||||
"licenseReckeckAll": "Überprüfe alle Schlüssel",
|
"licenseReckeckAll": "Überprüfe alle Schlüssel",
|
||||||
"licenseSiteUsage": "Website-Nutzung",
|
"licenseSiteUsage": "Standort-Nutzung",
|
||||||
"licenseSiteUsageDecsription": "Sehen Sie sich die Anzahl der Sites an, die diese Lizenz verwenden.",
|
"licenseSiteUsageDecsription": "Sehen Sie sich die Anzahl der Standorte an, die diese Lizenz verwenden.",
|
||||||
"licenseNoSiteLimit": "Die Anzahl der Sites, die einen nicht lizenzierten Host verwenden, ist unbegrenzt.",
|
"licenseNoSiteLimit": "Die Anzahl der Standorte, die einen nicht lizenzierten Host verwenden, ist unbegrenzt.",
|
||||||
"licensePurchase": "Lizenz kaufen",
|
"licensePurchase": "Lizenz kaufen",
|
||||||
"licensePurchaseSites": "Zusätzliche Seiten kaufen",
|
"licensePurchaseSites": "Zusätzliche Standorte kaufen\n",
|
||||||
"licenseSitesUsedMax": "{usedSites} der {maxSites} Seiten verwendet",
|
"licenseSitesUsedMax": "{usedSites} von {maxSites} Standorten verwendet",
|
||||||
"licenseSitesUsed": "{count, plural, =0 {# Seiten} one {# Seite} other {# Seiten}} im System.",
|
"licenseSitesUsed": "{count, plural, =0 {# Standorte} one {# Standort} other {# Standorte}} im System.",
|
||||||
"licensePurchaseDescription": "Wähle aus, für wieviele Seiten du möchtest {selectedMode, select, license {kaufe eine Lizenz. Du kannst später immer weitere Seiten hinzufügen.} other {Füge zu deiner bestehenden Lizenz hinzu.}}",
|
"licensePurchaseDescription": "Wähle aus, für wieviele Seiten du möchtest {selectedMode, select, license {kaufe eine Lizenz. Du kannst später immer weitere Seiten hinzufügen.} other {Füge zu deiner bestehenden Lizenz hinzu.}}",
|
||||||
"licenseFee": "Lizenzgebühr",
|
"licenseFee": "Lizenzgebühr",
|
||||||
"licensePriceSite": "Preis pro Seite",
|
"licensePriceSite": "Preis pro Standort",
|
||||||
"total": "Gesamt",
|
"total": "Gesamt",
|
||||||
"licenseContinuePayment": "Weiter zur Zahlung",
|
"licenseContinuePayment": "Weiter zur Zahlung",
|
||||||
"pricingPage": "Preisseite",
|
"pricingPage": "Preisseite",
|
||||||
@@ -467,7 +467,7 @@
|
|||||||
"targetErrorDuplicate": "Doppeltes Ziel",
|
"targetErrorDuplicate": "Doppeltes Ziel",
|
||||||
"targetErrorDuplicateDescription": "Ein Ziel mit diesen Einstellungen existiert bereits",
|
"targetErrorDuplicateDescription": "Ein Ziel mit diesen Einstellungen existiert bereits",
|
||||||
"targetWireGuardErrorInvalidIp": "Ungültige Ziel-IP",
|
"targetWireGuardErrorInvalidIp": "Ungültige Ziel-IP",
|
||||||
"targetWireGuardErrorInvalidIpDescription": "Die Ziel-IP muss innerhalb des Site-Subnets liegen",
|
"targetWireGuardErrorInvalidIpDescription": "Die Ziel-IP muss innerhalb des Standort-Subnets liegen",
|
||||||
"targetsUpdated": "Ziele aktualisiert",
|
"targetsUpdated": "Ziele aktualisiert",
|
||||||
"targetsUpdatedDescription": "Ziele und Einstellungen erfolgreich aktualisiert",
|
"targetsUpdatedDescription": "Ziele und Einstellungen erfolgreich aktualisiert",
|
||||||
"targetsErrorUpdate": "Fehler beim Aktualisieren der Ziele",
|
"targetsErrorUpdate": "Fehler beim Aktualisieren der Ziele",
|
||||||
@@ -558,8 +558,8 @@
|
|||||||
"resourceErrorCreateDescription": "Beim Erstellen der Ressource ist ein Fehler aufgetreten",
|
"resourceErrorCreateDescription": "Beim Erstellen der Ressource ist ein Fehler aufgetreten",
|
||||||
"resourceErrorCreateMessage": "Fehler beim Erstellen der Ressource:",
|
"resourceErrorCreateMessage": "Fehler beim Erstellen der Ressource:",
|
||||||
"resourceErrorCreateMessageDescription": "Ein unerwarteter Fehler ist aufgetreten",
|
"resourceErrorCreateMessageDescription": "Ein unerwarteter Fehler ist aufgetreten",
|
||||||
"sitesErrorFetch": "Fehler beim Abrufen der Sites",
|
"sitesErrorFetch": "Fehler beim Abrufen der Standorte",
|
||||||
"sitesErrorFetchDescription": "Beim Abrufen der Sites ist ein Fehler aufgetreten",
|
"sitesErrorFetchDescription": "Beim Abrufen der Standorte ist ein Fehler aufgetreten",
|
||||||
"domainsErrorFetch": "Fehler beim Abrufen der Domains",
|
"domainsErrorFetch": "Fehler beim Abrufen der Domains",
|
||||||
"domainsErrorFetchDescription": "Beim Abrufen der Domains ist ein Fehler aufgetreten",
|
"domainsErrorFetchDescription": "Beim Abrufen der Domains ist ein Fehler aufgetreten",
|
||||||
"none": "Keine",
|
"none": "Keine",
|
||||||
@@ -677,10 +677,10 @@
|
|||||||
"resourceGeneralDescription": "Konfigurieren Sie die allgemeinen Einstellungen für diese Ressource",
|
"resourceGeneralDescription": "Konfigurieren Sie die allgemeinen Einstellungen für diese Ressource",
|
||||||
"resourceEnable": "Ressource aktivieren",
|
"resourceEnable": "Ressource aktivieren",
|
||||||
"resourceTransfer": "Ressource übertragen",
|
"resourceTransfer": "Ressource übertragen",
|
||||||
"resourceTransferDescription": "Diese Ressource auf eine andere Site übertragen",
|
"resourceTransferDescription": "Diese Ressource auf einen anderen Standort übertragen",
|
||||||
"resourceTransferSubmit": "Ressource übertragen",
|
"resourceTransferSubmit": "Ressource übertragen",
|
||||||
"siteDestination": "Zielsite",
|
"siteDestination": "Zielort",
|
||||||
"searchSites": "Sites durchsuchen",
|
"searchSites": "Standorte durchsuchen",
|
||||||
"accessRoleCreate": "Rolle erstellen",
|
"accessRoleCreate": "Rolle erstellen",
|
||||||
"accessRoleCreateDescription": "Erstellen Sie eine neue Rolle, um Benutzer zu gruppieren und ihre Berechtigungen zu verwalten.",
|
"accessRoleCreateDescription": "Erstellen Sie eine neue Rolle, um Benutzer zu gruppieren und ihre Berechtigungen zu verwalten.",
|
||||||
"accessRoleCreateSubmit": "Rolle erstellen",
|
"accessRoleCreateSubmit": "Rolle erstellen",
|
||||||
@@ -700,7 +700,7 @@
|
|||||||
"accessRoleRemovedDescription": "Die Rolle wurde erfolgreich entfernt.",
|
"accessRoleRemovedDescription": "Die Rolle wurde erfolgreich entfernt.",
|
||||||
"accessRoleRequiredRemove": "Bevor Sie diese Rolle löschen, wählen Sie bitte eine neue Rolle aus, zu der die bestehenden Mitglieder übertragen werden sollen.",
|
"accessRoleRequiredRemove": "Bevor Sie diese Rolle löschen, wählen Sie bitte eine neue Rolle aus, zu der die bestehenden Mitglieder übertragen werden sollen.",
|
||||||
"manage": "Verwalten",
|
"manage": "Verwalten",
|
||||||
"sitesNotFound": "Keine Sites gefunden.",
|
"sitesNotFound": "Keine Standorte gefunden.",
|
||||||
"pangolinServerAdmin": "Server-Admin - Pangolin",
|
"pangolinServerAdmin": "Server-Admin - Pangolin",
|
||||||
"licenseTierProfessional": "Professional Lizenz",
|
"licenseTierProfessional": "Professional Lizenz",
|
||||||
"licenseTierEnterprise": "Enterprise Lizenz",
|
"licenseTierEnterprise": "Enterprise Lizenz",
|
||||||
@@ -708,10 +708,10 @@
|
|||||||
"licensed": "Lizenziert",
|
"licensed": "Lizenziert",
|
||||||
"yes": "Ja",
|
"yes": "Ja",
|
||||||
"no": "Nein",
|
"no": "Nein",
|
||||||
"sitesAdditional": "Zusätzliche Sites",
|
"sitesAdditional": "Zusätzliche Standorte",
|
||||||
"licenseKeys": "Lizenzschlüssel",
|
"licenseKeys": "Lizenzschlüssel",
|
||||||
"sitestCountDecrease": "Anzahl der Sites verringern",
|
"sitestCountDecrease": "Anzahl der Standorte verringern",
|
||||||
"sitestCountIncrease": "Anzahl der Sites erhöhen",
|
"sitestCountIncrease": "Anzahl der Standorte erhöhen",
|
||||||
"idpManage": "Identitätsanbieter verwalten",
|
"idpManage": "Identitätsanbieter verwalten",
|
||||||
"idpManageDescription": "Identitätsanbieter im System anzeigen und verwalten",
|
"idpManageDescription": "Identitätsanbieter im System anzeigen und verwalten",
|
||||||
"idpDeletedDescription": "Identitätsanbieter erfolgreich gelöscht",
|
"idpDeletedDescription": "Identitätsanbieter erfolgreich gelöscht",
|
||||||
@@ -833,6 +833,24 @@
|
|||||||
"pincodeRequirementsLength": "PIN muss genau 6 Ziffern lang sein",
|
"pincodeRequirementsLength": "PIN muss genau 6 Ziffern lang sein",
|
||||||
"pincodeRequirementsChars": "PIN darf nur Zahlen enthalten",
|
"pincodeRequirementsChars": "PIN darf nur Zahlen enthalten",
|
||||||
"passwordRequirementsLength": "Passwort muss mindestens 1 Zeichen lang sein",
|
"passwordRequirementsLength": "Passwort muss mindestens 1 Zeichen lang sein",
|
||||||
|
"passwordRequirementsTitle": "Password requirements:",
|
||||||
|
"passwordRequirementLength": "At least 8 characters long",
|
||||||
|
"passwordRequirementUppercase": "At least one uppercase letter",
|
||||||
|
"passwordRequirementLowercase": "At least one lowercase letter",
|
||||||
|
"passwordRequirementNumber": "At least one number",
|
||||||
|
"passwordRequirementSpecial": "At least one special character",
|
||||||
|
"passwordRequirementsMet": "✓ Password meets all requirements",
|
||||||
|
"passwordStrength": "Password strength",
|
||||||
|
"passwordStrengthWeak": "Weak",
|
||||||
|
"passwordStrengthMedium": "Medium",
|
||||||
|
"passwordStrengthStrong": "Strong",
|
||||||
|
"passwordRequirements": "Requirements:",
|
||||||
|
"passwordRequirementLengthText": "8+ characters",
|
||||||
|
"passwordRequirementUppercaseText": "Uppercase letter (A-Z)",
|
||||||
|
"passwordRequirementLowercaseText": "Lowercase letter (a-z)",
|
||||||
|
"passwordRequirementNumberText": "Number (0-9)",
|
||||||
|
"passwordRequirementSpecialText": "Special character (!@#$%...)",
|
||||||
|
"passwordsDoNotMatch": "Passwords do not match",
|
||||||
"otpEmailRequirementsLength": "OTP muss mindestens 1 Zeichen lang sein",
|
"otpEmailRequirementsLength": "OTP muss mindestens 1 Zeichen lang sein",
|
||||||
"otpEmailSent": "OTP gesendet",
|
"otpEmailSent": "OTP gesendet",
|
||||||
"otpEmailSentDescription": "Ein OTP wurde an Ihre E-Mail gesendet",
|
"otpEmailSentDescription": "Ein OTP wurde an Ihre E-Mail gesendet",
|
||||||
@@ -963,12 +981,15 @@
|
|||||||
"actionGetUser": "Benutzer abrufen",
|
"actionGetUser": "Benutzer abrufen",
|
||||||
"actionGetOrgUser": "Organisationsbenutzer abrufen",
|
"actionGetOrgUser": "Organisationsbenutzer abrufen",
|
||||||
"actionListOrgDomains": "Organisationsdomänen auflisten",
|
"actionListOrgDomains": "Organisationsdomänen auflisten",
|
||||||
"actionCreateSite": "Site erstellen",
|
"actionCreateSite": "Standort erstellen",
|
||||||
"actionDeleteSite": "Site löschen",
|
"actionDeleteSite": "Standort löschen",
|
||||||
"actionGetSite": "Site abrufen",
|
"actionGetSite": "Standort abrufen",
|
||||||
"actionListSites": "Sites auflisten",
|
"actionListSites": "Standorte auflisten",
|
||||||
"actionUpdateSite": "Site aktualisieren",
|
"setupToken": "Setup Token",
|
||||||
"actionListSiteRoles": "Erlaubte Site-Rollen auflisten",
|
"setupTokenPlaceholder": "Enter the setup token from the server console",
|
||||||
|
"setupTokenRequired": "Setup token is required",
|
||||||
|
"actionUpdateSite": "Standorte aktualisieren",
|
||||||
|
"actionListSiteRoles": "Erlaubte Standort-Rollen auflisten",
|
||||||
"actionCreateResource": "Ressource erstellen",
|
"actionCreateResource": "Ressource erstellen",
|
||||||
"actionDeleteResource": "Ressource löschen",
|
"actionDeleteResource": "Ressource löschen",
|
||||||
"actionGetResource": "Ressource abrufen",
|
"actionGetResource": "Ressource abrufen",
|
||||||
@@ -1022,6 +1043,11 @@
|
|||||||
"actionDeleteIdpOrg": "IDP-Organisationsrichtlinie löschen",
|
"actionDeleteIdpOrg": "IDP-Organisationsrichtlinie löschen",
|
||||||
"actionListIdpOrgs": "IDP-Organisationen auflisten",
|
"actionListIdpOrgs": "IDP-Organisationen auflisten",
|
||||||
"actionUpdateIdpOrg": "IDP-Organisation aktualisieren",
|
"actionUpdateIdpOrg": "IDP-Organisation aktualisieren",
|
||||||
|
"actionCreateClient": "Kunde erstellen",
|
||||||
|
"actionDeleteClient": "Kunde löschen",
|
||||||
|
"actionUpdateClient": "Kunde aktualisieren",
|
||||||
|
"actionListClients": "Kunden auflisten",
|
||||||
|
"actionGetClient": "Kunde holen",
|
||||||
"noneSelected": "Keine ausgewählt",
|
"noneSelected": "Keine ausgewählt",
|
||||||
"orgNotFound2": "Keine Organisationen gefunden.",
|
"orgNotFound2": "Keine Organisationen gefunden.",
|
||||||
"searchProgress": "Suche...",
|
"searchProgress": "Suche...",
|
||||||
@@ -1073,7 +1099,7 @@
|
|||||||
"language": "Sprache",
|
"language": "Sprache",
|
||||||
"verificationCodeRequired": "Code ist erforderlich",
|
"verificationCodeRequired": "Code ist erforderlich",
|
||||||
"userErrorNoUpdate": "Kein Benutzer zum Aktualisieren",
|
"userErrorNoUpdate": "Kein Benutzer zum Aktualisieren",
|
||||||
"siteErrorNoUpdate": "Keine Site zum Aktualisieren",
|
"siteErrorNoUpdate": "Keine Standorte zum Aktualisieren",
|
||||||
"resourceErrorNoUpdate": "Keine Ressource zum Aktualisieren",
|
"resourceErrorNoUpdate": "Keine Ressource zum Aktualisieren",
|
||||||
"authErrorNoUpdate": "Keine Auth-Informationen zum Aktualisieren",
|
"authErrorNoUpdate": "Keine Auth-Informationen zum Aktualisieren",
|
||||||
"orgErrorNoUpdate": "Keine Organisation zum Aktualisieren",
|
"orgErrorNoUpdate": "Keine Organisation zum Aktualisieren",
|
||||||
@@ -1081,7 +1107,7 @@
|
|||||||
"apiKeysErrorNoUpdate": "Kein API-Schlüssel zum Aktualisieren",
|
"apiKeysErrorNoUpdate": "Kein API-Schlüssel zum Aktualisieren",
|
||||||
"sidebarOverview": "Übersicht",
|
"sidebarOverview": "Übersicht",
|
||||||
"sidebarHome": "Zuhause",
|
"sidebarHome": "Zuhause",
|
||||||
"sidebarSites": "Seiten",
|
"sidebarSites": "Standorte",
|
||||||
"sidebarResources": "Ressourcen",
|
"sidebarResources": "Ressourcen",
|
||||||
"sidebarAccessControl": "Zugriffskontrolle",
|
"sidebarAccessControl": "Zugriffskontrolle",
|
||||||
"sidebarUsers": "Benutzer",
|
"sidebarUsers": "Benutzer",
|
||||||
@@ -1280,21 +1306,21 @@
|
|||||||
"and": "und",
|
"and": "und",
|
||||||
"privacyPolicy": "Datenschutzrichtlinie"
|
"privacyPolicy": "Datenschutzrichtlinie"
|
||||||
},
|
},
|
||||||
"siteRequired": "Site ist erforderlich.",
|
"siteRequired": "Standort ist erforderlich.",
|
||||||
"olmTunnel": "Olm Tunnel",
|
"olmTunnel": "Olm Tunnel",
|
||||||
"olmTunnelDescription": "Nutzen Sie Olm für die Kundenverbindung",
|
"olmTunnelDescription": "Nutzen Sie Olm für die Kundenverbindung",
|
||||||
"errorCreatingClient": "Fehler beim Erstellen des Clients",
|
"errorCreatingClient": "Fehler beim Erstellen des Clients",
|
||||||
"clientDefaultsNotFound": "Kundenvorgaben nicht gefunden",
|
"clientDefaultsNotFound": "Kundenvorgaben nicht gefunden",
|
||||||
"createClient": "Client erstellen",
|
"createClient": "Client erstellen",
|
||||||
"createClientDescription": "Erstellen Sie einen neuen Client für die Verbindung zu Ihren Sites.",
|
"createClientDescription": "Erstellen Sie einen neuen Client für die Verbindung zu Ihren Standorten.",
|
||||||
"seeAllClients": "Alle Clients anzeigen",
|
"seeAllClients": "Alle Clients anzeigen",
|
||||||
"clientInformation": "Kundeninformationen",
|
"clientInformation": "Kundeninformationen",
|
||||||
"clientNamePlaceholder": "Kundenname",
|
"clientNamePlaceholder": "Kundenname",
|
||||||
"address": "Adresse",
|
"address": "Adresse",
|
||||||
"subnetPlaceholder": "Subnetz",
|
"subnetPlaceholder": "Subnetz",
|
||||||
"addressDescription": "Die Adresse, die dieser Client für die Verbindung verwenden wird.",
|
"addressDescription": "Die Adresse, die dieser Client für die Verbindung verwenden wird.",
|
||||||
"selectSites": "Sites auswählen",
|
"selectSites": "Standorte auswählen",
|
||||||
"sitesDescription": "Der Client wird zu den ausgewählten Sites eine Verbindung haben.",
|
"sitesDescription": "Der Client wird zu den ausgewählten Standorten eine Verbindung haben.",
|
||||||
"clientInstallOlm": "Olm installieren",
|
"clientInstallOlm": "Olm installieren",
|
||||||
"clientInstallOlmDescription": "Olm auf Ihrem System zum Laufen bringen",
|
"clientInstallOlmDescription": "Olm auf Ihrem System zum Laufen bringen",
|
||||||
"clientOlmCredentials": "Olm-Zugangsdaten",
|
"clientOlmCredentials": "Olm-Zugangsdaten",
|
||||||
@@ -1309,14 +1335,14 @@
|
|||||||
"clientUpdatedDescription": "Der Client wurde aktualisiert.",
|
"clientUpdatedDescription": "Der Client wurde aktualisiert.",
|
||||||
"clientUpdateFailed": "Fehler beim Aktualisieren des Clients",
|
"clientUpdateFailed": "Fehler beim Aktualisieren des Clients",
|
||||||
"clientUpdateError": "Beim Aktualisieren des Clients ist ein Fehler aufgetreten.",
|
"clientUpdateError": "Beim Aktualisieren des Clients ist ein Fehler aufgetreten.",
|
||||||
"sitesFetchFailed": "Fehler beim Abrufen von Sites",
|
"sitesFetchFailed": "Fehler beim Abrufen von Standorten",
|
||||||
"sitesFetchError": "Beim Abrufen von Sites ist ein Fehler aufgetreten.",
|
"sitesFetchError": "Beim Abrufen von Standorten ist ein Fehler aufgetreten.",
|
||||||
"olmErrorFetchReleases": "Beim Abrufen von Olm-Veröffentlichungen ist ein Fehler aufgetreten.",
|
"olmErrorFetchReleases": "Beim Abrufen von Olm-Veröffentlichungen ist ein Fehler aufgetreten.",
|
||||||
"olmErrorFetchLatest": "Beim Abrufen der neuesten Olm-Veröffentlichung ist ein Fehler aufgetreten.",
|
"olmErrorFetchLatest": "Beim Abrufen der neuesten Olm-Veröffentlichung ist ein Fehler aufgetreten.",
|
||||||
"remoteSubnets": "Remote-Subnetze",
|
"remoteSubnets": "Remote-Subnetze",
|
||||||
"enterCidrRange": "Geben Sie den CIDR-Bereich ein",
|
"enterCidrRange": "Geben Sie den CIDR-Bereich ein",
|
||||||
"remoteSubnetsDescription": "Fügen Sie CIDR-Bereiche hinzu, die aus der Ferne auf diese Site zugreifen können. Verwenden Sie das Format wie 10.0.0.0/24 oder 192.168.1.0/24.",
|
"remoteSubnetsDescription": "Add CIDR ranges that can be accessed from this site remotely using clients. Use format like 10.0.0.0/24. This ONLY applies to VPN client connectivity.",
|
||||||
"resourceEnableProxy": "Öffentlichen Proxy aktivieren",
|
"resourceEnableProxy": "Öffentlichen Proxy aktivieren",
|
||||||
"resourceEnableProxyDescription": "Ermöglichen Sie öffentliches Proxieren zu dieser Ressource. Dies ermöglicht den Zugriff auf die Ressource von außerhalb des Netzwerks durch die Cloud über einen offenen Port. Erfordert Traefik-Config.",
|
"resourceEnableProxyDescription": "Ermöglichen Sie öffentliches Proxieren zu dieser Ressource. Dies ermöglicht den Zugriff auf die Ressource von außerhalb des Netzwerks durch die Cloud über einen offenen Port. Erfordert Traefik-Config.",
|
||||||
"externalProxyEnabled": "Externer Proxy aktiviert"
|
"externalProxyEnabled": "Externer Proxy aktiviert"
|
||||||
}
|
}
|
||||||
@@ -166,7 +166,7 @@
|
|||||||
"siteSelect": "Select site",
|
"siteSelect": "Select site",
|
||||||
"siteSearch": "Search site",
|
"siteSearch": "Search site",
|
||||||
"siteNotFound": "No site found.",
|
"siteNotFound": "No site found.",
|
||||||
"siteSelectionDescription": "This site will provide connectivity to the resource.",
|
"siteSelectionDescription": "This site will provide connectivity to the target.",
|
||||||
"resourceType": "Resource Type",
|
"resourceType": "Resource Type",
|
||||||
"resourceTypeDescription": "Determine how you want to access your resource",
|
"resourceTypeDescription": "Determine how you want to access your resource",
|
||||||
"resourceHTTPSSettings": "HTTPS Settings",
|
"resourceHTTPSSettings": "HTTPS Settings",
|
||||||
@@ -197,6 +197,7 @@
|
|||||||
"general": "General",
|
"general": "General",
|
||||||
"generalSettings": "General Settings",
|
"generalSettings": "General Settings",
|
||||||
"proxy": "Proxy",
|
"proxy": "Proxy",
|
||||||
|
"internal": "Internal",
|
||||||
"rules": "Rules",
|
"rules": "Rules",
|
||||||
"resourceSettingDescription": "Configure the settings on your resource",
|
"resourceSettingDescription": "Configure the settings on your resource",
|
||||||
"resourceSetting": "{resourceName} Settings",
|
"resourceSetting": "{resourceName} Settings",
|
||||||
@@ -490,7 +491,7 @@
|
|||||||
"targetTlsSniDescription": "The TLS Server Name to use for SNI. Leave empty to use the default.",
|
"targetTlsSniDescription": "The TLS Server Name to use for SNI. Leave empty to use the default.",
|
||||||
"targetTlsSubmit": "Save Settings",
|
"targetTlsSubmit": "Save Settings",
|
||||||
"targets": "Targets Configuration",
|
"targets": "Targets Configuration",
|
||||||
"targetsDescription": "Set up targets to route traffic to your services",
|
"targetsDescription": "Set up targets to route traffic to your backend services",
|
||||||
"targetStickySessions": "Enable Sticky Sessions",
|
"targetStickySessions": "Enable Sticky Sessions",
|
||||||
"targetStickySessionsDescription": "Keep connections on the same backend target for their entire session.",
|
"targetStickySessionsDescription": "Keep connections on the same backend target for their entire session.",
|
||||||
"methodSelect": "Select method",
|
"methodSelect": "Select method",
|
||||||
@@ -833,6 +834,24 @@
|
|||||||
"pincodeRequirementsLength": "PIN must be exactly 6 digits",
|
"pincodeRequirementsLength": "PIN must be exactly 6 digits",
|
||||||
"pincodeRequirementsChars": "PIN must only contain numbers",
|
"pincodeRequirementsChars": "PIN must only contain numbers",
|
||||||
"passwordRequirementsLength": "Password must be at least 1 character long",
|
"passwordRequirementsLength": "Password must be at least 1 character long",
|
||||||
|
"passwordRequirementsTitle": "Password requirements:",
|
||||||
|
"passwordRequirementLength": "At least 8 characters long",
|
||||||
|
"passwordRequirementUppercase": "At least one uppercase letter",
|
||||||
|
"passwordRequirementLowercase": "At least one lowercase letter",
|
||||||
|
"passwordRequirementNumber": "At least one number",
|
||||||
|
"passwordRequirementSpecial": "At least one special character",
|
||||||
|
"passwordRequirementsMet": "✓ Password meets all requirements",
|
||||||
|
"passwordStrength": "Password strength",
|
||||||
|
"passwordStrengthWeak": "Weak",
|
||||||
|
"passwordStrengthMedium": "Medium",
|
||||||
|
"passwordStrengthStrong": "Strong",
|
||||||
|
"passwordRequirements": "Requirements:",
|
||||||
|
"passwordRequirementLengthText": "8+ characters",
|
||||||
|
"passwordRequirementUppercaseText": "Uppercase letter (A-Z)",
|
||||||
|
"passwordRequirementLowercaseText": "Lowercase letter (a-z)",
|
||||||
|
"passwordRequirementNumberText": "Number (0-9)",
|
||||||
|
"passwordRequirementSpecialText": "Special character (!@#$%...)",
|
||||||
|
"passwordsDoNotMatch": "Passwords do not match",
|
||||||
"otpEmailRequirementsLength": "OTP must be at least 1 character long",
|
"otpEmailRequirementsLength": "OTP must be at least 1 character long",
|
||||||
"otpEmailSent": "OTP Sent",
|
"otpEmailSent": "OTP Sent",
|
||||||
"otpEmailSentDescription": "An OTP has been sent to your email",
|
"otpEmailSentDescription": "An OTP has been sent to your email",
|
||||||
@@ -967,6 +986,9 @@
|
|||||||
"actionDeleteSite": "Delete Site",
|
"actionDeleteSite": "Delete Site",
|
||||||
"actionGetSite": "Get Site",
|
"actionGetSite": "Get Site",
|
||||||
"actionListSites": "List Sites",
|
"actionListSites": "List Sites",
|
||||||
|
"setupToken": "Setup Token",
|
||||||
|
"setupTokenDescription": "Enter the setup token from the server console.",
|
||||||
|
"setupTokenRequired": "Setup token is required",
|
||||||
"actionUpdateSite": "Update Site",
|
"actionUpdateSite": "Update Site",
|
||||||
"actionListSiteRoles": "List Allowed Site Roles",
|
"actionListSiteRoles": "List Allowed Site Roles",
|
||||||
"actionCreateResource": "Create Resource",
|
"actionCreateResource": "Create Resource",
|
||||||
@@ -1022,6 +1044,11 @@
|
|||||||
"actionDeleteIdpOrg": "Delete IDP Org Policy",
|
"actionDeleteIdpOrg": "Delete IDP Org Policy",
|
||||||
"actionListIdpOrgs": "List IDP Orgs",
|
"actionListIdpOrgs": "List IDP Orgs",
|
||||||
"actionUpdateIdpOrg": "Update IDP Org",
|
"actionUpdateIdpOrg": "Update IDP Org",
|
||||||
|
"actionCreateClient": "Create Client",
|
||||||
|
"actionDeleteClient": "Delete Client",
|
||||||
|
"actionUpdateClient": "Update Client",
|
||||||
|
"actionListClients": "List Clients",
|
||||||
|
"actionGetClient": "Get Client",
|
||||||
"noneSelected": "None selected",
|
"noneSelected": "None selected",
|
||||||
"orgNotFound2": "No organizations found.",
|
"orgNotFound2": "No organizations found.",
|
||||||
"searchProgress": "Search...",
|
"searchProgress": "Search...",
|
||||||
@@ -1315,8 +1342,110 @@
|
|||||||
"olmErrorFetchLatest": "An error occurred while fetching the latest Olm release.",
|
"olmErrorFetchLatest": "An error occurred while fetching the latest Olm release.",
|
||||||
"remoteSubnets": "Remote Subnets",
|
"remoteSubnets": "Remote Subnets",
|
||||||
"enterCidrRange": "Enter CIDR range",
|
"enterCidrRange": "Enter CIDR range",
|
||||||
"remoteSubnetsDescription": "Add CIDR ranges that can access this site remotely. Use format like 10.0.0.0/24 or 192.168.1.0/24.",
|
"remoteSubnetsDescription": "Add CIDR ranges that can be accessed from this site remotely using clients. Use format like 10.0.0.0/24. This ONLY applies to VPN client connectivity.",
|
||||||
"resourceEnableProxy": "Enable Public Proxy",
|
"resourceEnableProxy": "Enable Public Proxy",
|
||||||
"resourceEnableProxyDescription": "Enable public proxying to this resource. This allows access to the resource from outside the network through the cloud on an open port. Requires Traefik config.",
|
"resourceEnableProxyDescription": "Enable public proxying to this resource. This allows access to the resource from outside the network through the cloud on an open port. Requires Traefik config.",
|
||||||
"externalProxyEnabled": "External Proxy Enabled"
|
"externalProxyEnabled": "External Proxy Enabled",
|
||||||
|
"addNewTarget": "Add New Target",
|
||||||
|
"targetsList": "Targets List",
|
||||||
|
"targetErrorDuplicateTargetFound": "Duplicate target found",
|
||||||
|
"httpMethod": "HTTP Method",
|
||||||
|
"selectHttpMethod": "Select HTTP method",
|
||||||
|
"domainPickerSubdomainLabel": "Subdomain",
|
||||||
|
"domainPickerBaseDomainLabel": "Base Domain",
|
||||||
|
"domainPickerSearchDomains": "Search domains...",
|
||||||
|
"domainPickerNoDomainsFound": "No domains found",
|
||||||
|
"domainPickerLoadingDomains": "Loading domains...",
|
||||||
|
"domainPickerSelectBaseDomain": "Select base domain...",
|
||||||
|
"domainPickerNotAvailableForCname": "Not available for CNAME domains",
|
||||||
|
"domainPickerEnterSubdomainOrLeaveBlank": "Enter subdomain or leave blank to use base domain.",
|
||||||
|
"domainPickerEnterSubdomainToSearch": "Enter a subdomain to search and select from available free domains.",
|
||||||
|
"domainPickerFreeDomains": "Free Domains",
|
||||||
|
"domainPickerSearchForAvailableDomains": "Search for available domains",
|
||||||
|
"resourceDomain": "Domain",
|
||||||
|
"resourceEditDomain": "Edit Domain",
|
||||||
|
"siteName": "Site Name",
|
||||||
|
"proxyPort": "Port",
|
||||||
|
"resourcesTableProxyResources": "Proxy Resources",
|
||||||
|
"resourcesTableClientResources": "Client Resources",
|
||||||
|
"resourcesTableNoProxyResourcesFound": "No proxy resources found.",
|
||||||
|
"resourcesTableNoInternalResourcesFound": "No internal resources found.",
|
||||||
|
"resourcesTableDestination": "Destination",
|
||||||
|
"resourcesTableTheseResourcesForUseWith": "These resources are for use with",
|
||||||
|
"resourcesTableClients": "Clients",
|
||||||
|
"resourcesTableAndOnlyAccessibleInternally": "and are only accessible internally when connected with a client.",
|
||||||
|
"editInternalResourceDialogEditClientResource": "Edit Client Resource",
|
||||||
|
"editInternalResourceDialogUpdateResourceProperties": "Update the resource properties and target configuration for {resourceName}.",
|
||||||
|
"editInternalResourceDialogResourceProperties": "Resource Properties",
|
||||||
|
"editInternalResourceDialogName": "Name",
|
||||||
|
"editInternalResourceDialogProtocol": "Protocol",
|
||||||
|
"editInternalResourceDialogSitePort": "Site Port",
|
||||||
|
"editInternalResourceDialogTargetConfiguration": "Target Configuration",
|
||||||
|
"editInternalResourceDialogDestinationIP": "Destination IP",
|
||||||
|
"editInternalResourceDialogDestinationPort": "Destination Port",
|
||||||
|
"editInternalResourceDialogCancel": "Cancel",
|
||||||
|
"editInternalResourceDialogSaveResource": "Save Resource",
|
||||||
|
"editInternalResourceDialogSuccess": "Success",
|
||||||
|
"editInternalResourceDialogInternalResourceUpdatedSuccessfully": "Internal resource updated successfully",
|
||||||
|
"editInternalResourceDialogError": "Error",
|
||||||
|
"editInternalResourceDialogFailedToUpdateInternalResource": "Failed to update internal resource",
|
||||||
|
"editInternalResourceDialogNameRequired": "Name is required",
|
||||||
|
"editInternalResourceDialogNameMaxLength": "Name must be less than 255 characters",
|
||||||
|
"editInternalResourceDialogProxyPortMin": "Proxy port must be at least 1",
|
||||||
|
"editInternalResourceDialogProxyPortMax": "Proxy port must be less than 65536",
|
||||||
|
"editInternalResourceDialogInvalidIPAddressFormat": "Invalid IP address format",
|
||||||
|
"editInternalResourceDialogDestinationPortMin": "Destination port must be at least 1",
|
||||||
|
"editInternalResourceDialogDestinationPortMax": "Destination port must be less than 65536",
|
||||||
|
"createInternalResourceDialogNoSitesAvailable": "No Sites Available",
|
||||||
|
"createInternalResourceDialogNoSitesAvailableDescription": "You need to have at least one Newt site with a subnet configured to create internal resources.",
|
||||||
|
"createInternalResourceDialogClose": "Close",
|
||||||
|
"createInternalResourceDialogCreateClientResource": "Create Client Resource",
|
||||||
|
"createInternalResourceDialogCreateClientResourceDescription": "Create a new resource that will be accessible to clients connected to the selected site.",
|
||||||
|
"createInternalResourceDialogResourceProperties": "Resource Properties",
|
||||||
|
"createInternalResourceDialogName": "Name",
|
||||||
|
"createInternalResourceDialogSite": "Site",
|
||||||
|
"createInternalResourceDialogSelectSite": "Select site...",
|
||||||
|
"createInternalResourceDialogSearchSites": "Search sites...",
|
||||||
|
"createInternalResourceDialogNoSitesFound": "No sites found.",
|
||||||
|
"createInternalResourceDialogProtocol": "Protocol",
|
||||||
|
"createInternalResourceDialogTcp": "TCP",
|
||||||
|
"createInternalResourceDialogUdp": "UDP",
|
||||||
|
"createInternalResourceDialogSitePort": "Site Port",
|
||||||
|
"createInternalResourceDialogSitePortDescription": "Use this port to access the resource on the site when connected with a client.",
|
||||||
|
"createInternalResourceDialogTargetConfiguration": "Target Configuration",
|
||||||
|
"createInternalResourceDialogDestinationIP": "Destination IP",
|
||||||
|
"createInternalResourceDialogDestinationIPDescription": "The IP address of the resource on the site's network.",
|
||||||
|
"createInternalResourceDialogDestinationPort": "Destination Port",
|
||||||
|
"createInternalResourceDialogDestinationPortDescription": "The port on the destination IP where the resource is accessible.",
|
||||||
|
"createInternalResourceDialogCancel": "Cancel",
|
||||||
|
"createInternalResourceDialogCreateResource": "Create Resource",
|
||||||
|
"createInternalResourceDialogSuccess": "Success",
|
||||||
|
"createInternalResourceDialogInternalResourceCreatedSuccessfully": "Internal resource created successfully",
|
||||||
|
"createInternalResourceDialogError": "Error",
|
||||||
|
"createInternalResourceDialogFailedToCreateInternalResource": "Failed to create internal resource",
|
||||||
|
"createInternalResourceDialogNameRequired": "Name is required",
|
||||||
|
"createInternalResourceDialogNameMaxLength": "Name must be less than 255 characters",
|
||||||
|
"createInternalResourceDialogPleaseSelectSite": "Please select a site",
|
||||||
|
"createInternalResourceDialogProxyPortMin": "Proxy port must be at least 1",
|
||||||
|
"createInternalResourceDialogProxyPortMax": "Proxy port must be less than 65536",
|
||||||
|
"createInternalResourceDialogInvalidIPAddressFormat": "Invalid IP address format",
|
||||||
|
"createInternalResourceDialogDestinationPortMin": "Destination port must be at least 1",
|
||||||
|
"createInternalResourceDialogDestinationPortMax": "Destination port must be less than 65536",
|
||||||
|
"siteConfiguration": "Configuration",
|
||||||
|
"siteAcceptClientConnections": "Accept Client Connections",
|
||||||
|
"siteAcceptClientConnectionsDescription": "Allow other devices to connect through this Newt instance as a gateway using clients.",
|
||||||
|
"siteAddress": "Site Address",
|
||||||
|
"siteAddressDescription": "Specify the IP address of the host for clients to connect to.",
|
||||||
|
"autoLoginExternalIdp": "Auto Login with External IDP",
|
||||||
|
"autoLoginExternalIdpDescription": "Immediately redirect the user to the external IDP for authentication.",
|
||||||
|
"selectIdp": "Select IDP",
|
||||||
|
"selectIdpPlaceholder": "Choose an IDP...",
|
||||||
|
"selectIdpRequired": "Please select an IDP when auto login is enabled.",
|
||||||
|
"autoLoginTitle": "Redirecting",
|
||||||
|
"autoLoginDescription": "Redirecting you to the external identity provider for authentication.",
|
||||||
|
"autoLoginProcessing": "Preparing authentication...",
|
||||||
|
"autoLoginRedirecting": "Redirecting to login...",
|
||||||
|
"autoLoginError": "Auto Login Error",
|
||||||
|
"autoLoginErrorNoRedirectUrl": "No redirect URL received from the identity provider.",
|
||||||
|
"autoLoginErrorGeneratingUrl": "Failed to generate authentication URL."
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -833,6 +833,24 @@
|
|||||||
"pincodeRequirementsLength": "El PIN debe tener exactamente 6 dígitos",
|
"pincodeRequirementsLength": "El PIN debe tener exactamente 6 dígitos",
|
||||||
"pincodeRequirementsChars": "El PIN sólo debe contener números",
|
"pincodeRequirementsChars": "El PIN sólo debe contener números",
|
||||||
"passwordRequirementsLength": "La contraseña debe tener al menos 1 carácter",
|
"passwordRequirementsLength": "La contraseña debe tener al menos 1 carácter",
|
||||||
|
"passwordRequirementsTitle": "Password requirements:",
|
||||||
|
"passwordRequirementLength": "At least 8 characters long",
|
||||||
|
"passwordRequirementUppercase": "At least one uppercase letter",
|
||||||
|
"passwordRequirementLowercase": "At least one lowercase letter",
|
||||||
|
"passwordRequirementNumber": "At least one number",
|
||||||
|
"passwordRequirementSpecial": "At least one special character",
|
||||||
|
"passwordRequirementsMet": "✓ Password meets all requirements",
|
||||||
|
"passwordStrength": "Password strength",
|
||||||
|
"passwordStrengthWeak": "Weak",
|
||||||
|
"passwordStrengthMedium": "Medium",
|
||||||
|
"passwordStrengthStrong": "Strong",
|
||||||
|
"passwordRequirements": "Requirements:",
|
||||||
|
"passwordRequirementLengthText": "8+ characters",
|
||||||
|
"passwordRequirementUppercaseText": "Uppercase letter (A-Z)",
|
||||||
|
"passwordRequirementLowercaseText": "Lowercase letter (a-z)",
|
||||||
|
"passwordRequirementNumberText": "Number (0-9)",
|
||||||
|
"passwordRequirementSpecialText": "Special character (!@#$%...)",
|
||||||
|
"passwordsDoNotMatch": "Passwords do not match",
|
||||||
"otpEmailRequirementsLength": "OTP debe tener al menos 1 carácter",
|
"otpEmailRequirementsLength": "OTP debe tener al menos 1 carácter",
|
||||||
"otpEmailSent": "OTP enviado",
|
"otpEmailSent": "OTP enviado",
|
||||||
"otpEmailSentDescription": "Un OTP ha sido enviado a tu correo electrónico",
|
"otpEmailSentDescription": "Un OTP ha sido enviado a tu correo electrónico",
|
||||||
@@ -967,6 +985,9 @@
|
|||||||
"actionDeleteSite": "Eliminar sitio",
|
"actionDeleteSite": "Eliminar sitio",
|
||||||
"actionGetSite": "Obtener sitio",
|
"actionGetSite": "Obtener sitio",
|
||||||
"actionListSites": "Listar sitios",
|
"actionListSites": "Listar sitios",
|
||||||
|
"setupToken": "Setup Token",
|
||||||
|
"setupTokenPlaceholder": "Enter the setup token from the server console",
|
||||||
|
"setupTokenRequired": "Setup token is required",
|
||||||
"actionUpdateSite": "Actualizar sitio",
|
"actionUpdateSite": "Actualizar sitio",
|
||||||
"actionListSiteRoles": "Lista de roles permitidos del sitio",
|
"actionListSiteRoles": "Lista de roles permitidos del sitio",
|
||||||
"actionCreateResource": "Crear Recurso",
|
"actionCreateResource": "Crear Recurso",
|
||||||
@@ -1022,6 +1043,11 @@
|
|||||||
"actionDeleteIdpOrg": "Eliminar política de IDP Org",
|
"actionDeleteIdpOrg": "Eliminar política de IDP Org",
|
||||||
"actionListIdpOrgs": "Listar Orgs IDP",
|
"actionListIdpOrgs": "Listar Orgs IDP",
|
||||||
"actionUpdateIdpOrg": "Actualizar IDP Org",
|
"actionUpdateIdpOrg": "Actualizar IDP Org",
|
||||||
|
"actionCreateClient": "Crear cliente",
|
||||||
|
"actionDeleteClient": "Eliminar cliente",
|
||||||
|
"actionUpdateClient": "Actualizar cliente",
|
||||||
|
"actionListClients": "Listar clientes",
|
||||||
|
"actionGetClient": "Obtener cliente",
|
||||||
"noneSelected": "Ninguno seleccionado",
|
"noneSelected": "Ninguno seleccionado",
|
||||||
"orgNotFound2": "No se encontraron organizaciones.",
|
"orgNotFound2": "No se encontraron organizaciones.",
|
||||||
"searchProgress": "Buscar...",
|
"searchProgress": "Buscar...",
|
||||||
@@ -1315,8 +1341,8 @@
|
|||||||
"olmErrorFetchLatest": "Se ha producido un error al recuperar la última versión de Olm.",
|
"olmErrorFetchLatest": "Se ha producido un error al recuperar la última versión de Olm.",
|
||||||
"remoteSubnets": "Subredes remotas",
|
"remoteSubnets": "Subredes remotas",
|
||||||
"enterCidrRange": "Ingresa el rango CIDR",
|
"enterCidrRange": "Ingresa el rango CIDR",
|
||||||
"remoteSubnetsDescription": "Agregue rangos CIDR que puedan acceder a este sitio de forma remota. Use un formato como 10.0.0.0/24 o 192.168.1.0/24.",
|
"remoteSubnetsDescription": "Add CIDR ranges that can be accessed from this site remotely using clients. Use format like 10.0.0.0/24. This ONLY applies to VPN client connectivity.",
|
||||||
"resourceEnableProxy": "Habilitar proxy público",
|
"resourceEnableProxy": "Habilitar proxy público",
|
||||||
"resourceEnableProxyDescription": "Habilite el proxy público para este recurso. Esto permite el acceso al recurso desde fuera de la red a través de la nube en un puerto abierto. Requiere configuración de Traefik.",
|
"resourceEnableProxyDescription": "Habilite el proxy público para este recurso. Esto permite el acceso al recurso desde fuera de la red a través de la nube en un puerto abierto. Requiere configuración de Traefik.",
|
||||||
"externalProxyEnabled": "Proxy externo habilitado"
|
"externalProxyEnabled": "Proxy externo habilitado"
|
||||||
}
|
}
|
||||||
@@ -833,6 +833,24 @@
|
|||||||
"pincodeRequirementsLength": "Le code PIN doit comporter exactement 6 chiffres",
|
"pincodeRequirementsLength": "Le code PIN doit comporter exactement 6 chiffres",
|
||||||
"pincodeRequirementsChars": "Le code PIN ne doit contenir que des chiffres",
|
"pincodeRequirementsChars": "Le code PIN ne doit contenir que des chiffres",
|
||||||
"passwordRequirementsLength": "Le mot de passe doit comporter au moins 1 caractère",
|
"passwordRequirementsLength": "Le mot de passe doit comporter au moins 1 caractère",
|
||||||
|
"passwordRequirementsTitle": "Password requirements:",
|
||||||
|
"passwordRequirementLength": "At least 8 characters long",
|
||||||
|
"passwordRequirementUppercase": "At least one uppercase letter",
|
||||||
|
"passwordRequirementLowercase": "At least one lowercase letter",
|
||||||
|
"passwordRequirementNumber": "At least one number",
|
||||||
|
"passwordRequirementSpecial": "At least one special character",
|
||||||
|
"passwordRequirementsMet": "✓ Password meets all requirements",
|
||||||
|
"passwordStrength": "Password strength",
|
||||||
|
"passwordStrengthWeak": "Weak",
|
||||||
|
"passwordStrengthMedium": "Medium",
|
||||||
|
"passwordStrengthStrong": "Strong",
|
||||||
|
"passwordRequirements": "Requirements:",
|
||||||
|
"passwordRequirementLengthText": "8+ characters",
|
||||||
|
"passwordRequirementUppercaseText": "Uppercase letter (A-Z)",
|
||||||
|
"passwordRequirementLowercaseText": "Lowercase letter (a-z)",
|
||||||
|
"passwordRequirementNumberText": "Number (0-9)",
|
||||||
|
"passwordRequirementSpecialText": "Special character (!@#$%...)",
|
||||||
|
"passwordsDoNotMatch": "Passwords do not match",
|
||||||
"otpEmailRequirementsLength": "L'OTP doit comporter au moins 1 caractère",
|
"otpEmailRequirementsLength": "L'OTP doit comporter au moins 1 caractère",
|
||||||
"otpEmailSent": "OTP envoyé",
|
"otpEmailSent": "OTP envoyé",
|
||||||
"otpEmailSentDescription": "Un OTP a été envoyé à votre e-mail",
|
"otpEmailSentDescription": "Un OTP a été envoyé à votre e-mail",
|
||||||
@@ -967,6 +985,9 @@
|
|||||||
"actionDeleteSite": "Supprimer un site",
|
"actionDeleteSite": "Supprimer un site",
|
||||||
"actionGetSite": "Obtenir un site",
|
"actionGetSite": "Obtenir un site",
|
||||||
"actionListSites": "Lister les sites",
|
"actionListSites": "Lister les sites",
|
||||||
|
"setupToken": "Setup Token",
|
||||||
|
"setupTokenPlaceholder": "Enter the setup token from the server console",
|
||||||
|
"setupTokenRequired": "Setup token is required",
|
||||||
"actionUpdateSite": "Mettre à jour un site",
|
"actionUpdateSite": "Mettre à jour un site",
|
||||||
"actionListSiteRoles": "Lister les rôles autorisés du site",
|
"actionListSiteRoles": "Lister les rôles autorisés du site",
|
||||||
"actionCreateResource": "Créer une ressource",
|
"actionCreateResource": "Créer une ressource",
|
||||||
@@ -1022,6 +1043,11 @@
|
|||||||
"actionDeleteIdpOrg": "Supprimer une politique d'organisation IDP",
|
"actionDeleteIdpOrg": "Supprimer une politique d'organisation IDP",
|
||||||
"actionListIdpOrgs": "Lister les organisations IDP",
|
"actionListIdpOrgs": "Lister les organisations IDP",
|
||||||
"actionUpdateIdpOrg": "Mettre à jour une organisation IDP",
|
"actionUpdateIdpOrg": "Mettre à jour une organisation IDP",
|
||||||
|
"actionCreateClient": "Créer un client",
|
||||||
|
"actionDeleteClient": "Supprimer le client",
|
||||||
|
"actionUpdateClient": "Mettre à jour le client",
|
||||||
|
"actionListClients": "Liste des clients",
|
||||||
|
"actionGetClient": "Obtenir le client",
|
||||||
"noneSelected": "Aucune sélection",
|
"noneSelected": "Aucune sélection",
|
||||||
"orgNotFound2": "Aucune organisation trouvée.",
|
"orgNotFound2": "Aucune organisation trouvée.",
|
||||||
"searchProgress": "Rechercher...",
|
"searchProgress": "Rechercher...",
|
||||||
@@ -1315,8 +1341,8 @@
|
|||||||
"olmErrorFetchLatest": "Une erreur s'est produite lors de la récupération de la dernière version d'Olm.",
|
"olmErrorFetchLatest": "Une erreur s'est produite lors de la récupération de la dernière version d'Olm.",
|
||||||
"remoteSubnets": "Sous-réseaux distants",
|
"remoteSubnets": "Sous-réseaux distants",
|
||||||
"enterCidrRange": "Entrez la plage CIDR",
|
"enterCidrRange": "Entrez la plage CIDR",
|
||||||
"remoteSubnetsDescription": "Ajoutez des plages CIDR pouvant accéder à ce site à distance. Utilisez le format comme 10.0.0.0/24 ou 192.168.1.0/24.",
|
"remoteSubnetsDescription": "Add CIDR ranges that can be accessed from this site remotely using clients. Use format like 10.0.0.0/24. This ONLY applies to VPN client connectivity.",
|
||||||
"resourceEnableProxy": "Activer le proxy public",
|
"resourceEnableProxy": "Activer le proxy public",
|
||||||
"resourceEnableProxyDescription": "Activez le proxy public vers cette ressource. Cela permet d'accéder à la ressource depuis l'extérieur du réseau via le cloud sur un port ouvert. Nécessite la configuration de Traefik.",
|
"resourceEnableProxyDescription": "Activez le proxy public vers cette ressource. Cela permet d'accéder à la ressource depuis l'extérieur du réseau via le cloud sur un port ouvert. Nécessite la configuration de Traefik.",
|
||||||
"externalProxyEnabled": "Proxy externe activé"
|
"externalProxyEnabled": "Proxy externe activé"
|
||||||
}
|
}
|
||||||
@@ -833,6 +833,24 @@
|
|||||||
"pincodeRequirementsLength": "Il PIN deve essere esattamente di 6 cifre",
|
"pincodeRequirementsLength": "Il PIN deve essere esattamente di 6 cifre",
|
||||||
"pincodeRequirementsChars": "Il PIN deve contenere solo numeri",
|
"pincodeRequirementsChars": "Il PIN deve contenere solo numeri",
|
||||||
"passwordRequirementsLength": "La password deve essere lunga almeno 1 carattere",
|
"passwordRequirementsLength": "La password deve essere lunga almeno 1 carattere",
|
||||||
|
"passwordRequirementsTitle": "Password requirements:",
|
||||||
|
"passwordRequirementLength": "At least 8 characters long",
|
||||||
|
"passwordRequirementUppercase": "At least one uppercase letter",
|
||||||
|
"passwordRequirementLowercase": "At least one lowercase letter",
|
||||||
|
"passwordRequirementNumber": "At least one number",
|
||||||
|
"passwordRequirementSpecial": "At least one special character",
|
||||||
|
"passwordRequirementsMet": "✓ Password meets all requirements",
|
||||||
|
"passwordStrength": "Password strength",
|
||||||
|
"passwordStrengthWeak": "Weak",
|
||||||
|
"passwordStrengthMedium": "Medium",
|
||||||
|
"passwordStrengthStrong": "Strong",
|
||||||
|
"passwordRequirements": "Requirements:",
|
||||||
|
"passwordRequirementLengthText": "8+ characters",
|
||||||
|
"passwordRequirementUppercaseText": "Uppercase letter (A-Z)",
|
||||||
|
"passwordRequirementLowercaseText": "Lowercase letter (a-z)",
|
||||||
|
"passwordRequirementNumberText": "Number (0-9)",
|
||||||
|
"passwordRequirementSpecialText": "Special character (!@#$%...)",
|
||||||
|
"passwordsDoNotMatch": "Passwords do not match",
|
||||||
"otpEmailRequirementsLength": "L'OTP deve essere lungo almeno 1 carattere",
|
"otpEmailRequirementsLength": "L'OTP deve essere lungo almeno 1 carattere",
|
||||||
"otpEmailSent": "OTP Inviato",
|
"otpEmailSent": "OTP Inviato",
|
||||||
"otpEmailSentDescription": "Un OTP è stato inviato alla tua email",
|
"otpEmailSentDescription": "Un OTP è stato inviato alla tua email",
|
||||||
@@ -967,6 +985,9 @@
|
|||||||
"actionDeleteSite": "Elimina Sito",
|
"actionDeleteSite": "Elimina Sito",
|
||||||
"actionGetSite": "Ottieni Sito",
|
"actionGetSite": "Ottieni Sito",
|
||||||
"actionListSites": "Elenca Siti",
|
"actionListSites": "Elenca Siti",
|
||||||
|
"setupToken": "Setup Token",
|
||||||
|
"setupTokenPlaceholder": "Enter the setup token from the server console",
|
||||||
|
"setupTokenRequired": "Setup token is required",
|
||||||
"actionUpdateSite": "Aggiorna Sito",
|
"actionUpdateSite": "Aggiorna Sito",
|
||||||
"actionListSiteRoles": "Elenca Ruoli Sito Consentiti",
|
"actionListSiteRoles": "Elenca Ruoli Sito Consentiti",
|
||||||
"actionCreateResource": "Crea Risorsa",
|
"actionCreateResource": "Crea Risorsa",
|
||||||
@@ -1022,6 +1043,11 @@
|
|||||||
"actionDeleteIdpOrg": "Elimina Politica Org IDP",
|
"actionDeleteIdpOrg": "Elimina Politica Org IDP",
|
||||||
"actionListIdpOrgs": "Elenca Org IDP",
|
"actionListIdpOrgs": "Elenca Org IDP",
|
||||||
"actionUpdateIdpOrg": "Aggiorna Org IDP",
|
"actionUpdateIdpOrg": "Aggiorna Org IDP",
|
||||||
|
"actionCreateClient": "Crea Client",
|
||||||
|
"actionDeleteClient": "Elimina Client",
|
||||||
|
"actionUpdateClient": "Aggiorna Client",
|
||||||
|
"actionListClients": "Elenco Clienti",
|
||||||
|
"actionGetClient": "Ottieni Client",
|
||||||
"noneSelected": "Nessuna selezione",
|
"noneSelected": "Nessuna selezione",
|
||||||
"orgNotFound2": "Nessuna organizzazione trovata.",
|
"orgNotFound2": "Nessuna organizzazione trovata.",
|
||||||
"searchProgress": "Ricerca...",
|
"searchProgress": "Ricerca...",
|
||||||
@@ -1315,8 +1341,8 @@
|
|||||||
"olmErrorFetchLatest": "Si è verificato un errore durante il recupero dell'ultima versione di Olm.",
|
"olmErrorFetchLatest": "Si è verificato un errore durante il recupero dell'ultima versione di Olm.",
|
||||||
"remoteSubnets": "Sottoreti Remote",
|
"remoteSubnets": "Sottoreti Remote",
|
||||||
"enterCidrRange": "Inserisci l'intervallo CIDR",
|
"enterCidrRange": "Inserisci l'intervallo CIDR",
|
||||||
"remoteSubnetsDescription": "Aggiungi intervalli CIDR che possono accedere a questo sito da remoto. Usa il formato come 10.0.0.0/24 o 192.168.1.0/24.",
|
"remoteSubnetsDescription": "Add CIDR ranges that can be accessed from this site remotely using clients. Use format like 10.0.0.0/24. This ONLY applies to VPN client connectivity.",
|
||||||
"resourceEnableProxy": "Abilita Proxy Pubblico",
|
"resourceEnableProxy": "Abilita Proxy Pubblico",
|
||||||
"resourceEnableProxyDescription": "Abilita il proxy pubblico a questa risorsa. Consente l'accesso alla risorsa dall'esterno della rete tramite il cloud su una porta aperta. Richiede la configurazione di Traefik.",
|
"resourceEnableProxyDescription": "Abilita il proxy pubblico a questa risorsa. Consente l'accesso alla risorsa dall'esterno della rete tramite il cloud su una porta aperta. Richiede la configurazione di Traefik.",
|
||||||
"externalProxyEnabled": "Proxy Esterno Abilitato"
|
"externalProxyEnabled": "Proxy Esterno Abilitato"
|
||||||
}
|
}
|
||||||
@@ -833,6 +833,24 @@
|
|||||||
"pincodeRequirementsLength": "PIN은 정확히 6자리여야 합니다",
|
"pincodeRequirementsLength": "PIN은 정확히 6자리여야 합니다",
|
||||||
"pincodeRequirementsChars": "PIN은 숫자만 포함해야 합니다.",
|
"pincodeRequirementsChars": "PIN은 숫자만 포함해야 합니다.",
|
||||||
"passwordRequirementsLength": "비밀번호는 최소 1자 이상이어야 합니다",
|
"passwordRequirementsLength": "비밀번호는 최소 1자 이상이어야 합니다",
|
||||||
|
"passwordRequirementsTitle": "Password requirements:",
|
||||||
|
"passwordRequirementLength": "At least 8 characters long",
|
||||||
|
"passwordRequirementUppercase": "At least one uppercase letter",
|
||||||
|
"passwordRequirementLowercase": "At least one lowercase letter",
|
||||||
|
"passwordRequirementNumber": "At least one number",
|
||||||
|
"passwordRequirementSpecial": "At least one special character",
|
||||||
|
"passwordRequirementsMet": "✓ Password meets all requirements",
|
||||||
|
"passwordStrength": "Password strength",
|
||||||
|
"passwordStrengthWeak": "Weak",
|
||||||
|
"passwordStrengthMedium": "Medium",
|
||||||
|
"passwordStrengthStrong": "Strong",
|
||||||
|
"passwordRequirements": "Requirements:",
|
||||||
|
"passwordRequirementLengthText": "8+ characters",
|
||||||
|
"passwordRequirementUppercaseText": "Uppercase letter (A-Z)",
|
||||||
|
"passwordRequirementLowercaseText": "Lowercase letter (a-z)",
|
||||||
|
"passwordRequirementNumberText": "Number (0-9)",
|
||||||
|
"passwordRequirementSpecialText": "Special character (!@#$%...)",
|
||||||
|
"passwordsDoNotMatch": "Passwords do not match",
|
||||||
"otpEmailRequirementsLength": "OTP는 최소 1자 이상이어야 합니다",
|
"otpEmailRequirementsLength": "OTP는 최소 1자 이상이어야 합니다",
|
||||||
"otpEmailSent": "OTP 전송됨",
|
"otpEmailSent": "OTP 전송됨",
|
||||||
"otpEmailSentDescription": "OTP가 귀하의 이메일로 전송되었습니다.",
|
"otpEmailSentDescription": "OTP가 귀하의 이메일로 전송되었습니다.",
|
||||||
@@ -967,6 +985,9 @@
|
|||||||
"actionDeleteSite": "사이트 삭제",
|
"actionDeleteSite": "사이트 삭제",
|
||||||
"actionGetSite": "사이트 가져오기",
|
"actionGetSite": "사이트 가져오기",
|
||||||
"actionListSites": "사이트 목록",
|
"actionListSites": "사이트 목록",
|
||||||
|
"setupToken": "Setup Token",
|
||||||
|
"setupTokenPlaceholder": "Enter the setup token from the server console",
|
||||||
|
"setupTokenRequired": "Setup token is required",
|
||||||
"actionUpdateSite": "사이트 업데이트",
|
"actionUpdateSite": "사이트 업데이트",
|
||||||
"actionListSiteRoles": "허용된 사이트 역할 목록",
|
"actionListSiteRoles": "허용된 사이트 역할 목록",
|
||||||
"actionCreateResource": "리소스 생성",
|
"actionCreateResource": "리소스 생성",
|
||||||
@@ -1022,6 +1043,11 @@
|
|||||||
"actionDeleteIdpOrg": "IDP 조직 정책 삭제",
|
"actionDeleteIdpOrg": "IDP 조직 정책 삭제",
|
||||||
"actionListIdpOrgs": "IDP 조직 목록",
|
"actionListIdpOrgs": "IDP 조직 목록",
|
||||||
"actionUpdateIdpOrg": "IDP 조직 업데이트",
|
"actionUpdateIdpOrg": "IDP 조직 업데이트",
|
||||||
|
"actionCreateClient": "Create Client",
|
||||||
|
"actionDeleteClient": "Delete Client",
|
||||||
|
"actionUpdateClient": "Update Client",
|
||||||
|
"actionListClients": "List Clients",
|
||||||
|
"actionGetClient": "Get Client",
|
||||||
"noneSelected": "선택된 항목 없음",
|
"noneSelected": "선택된 항목 없음",
|
||||||
"orgNotFound2": "조직이 없습니다.",
|
"orgNotFound2": "조직이 없습니다.",
|
||||||
"searchProgress": "검색...",
|
"searchProgress": "검색...",
|
||||||
@@ -1315,8 +1341,8 @@
|
|||||||
"olmErrorFetchLatest": "An error occurred while fetching the latest Olm release.",
|
"olmErrorFetchLatest": "An error occurred while fetching the latest Olm release.",
|
||||||
"remoteSubnets": "Remote Subnets",
|
"remoteSubnets": "Remote Subnets",
|
||||||
"enterCidrRange": "Enter CIDR range",
|
"enterCidrRange": "Enter CIDR range",
|
||||||
"remoteSubnetsDescription": "Add CIDR ranges that can access this site remotely. Use format like 10.0.0.0/24 or 192.168.1.0/24.",
|
"remoteSubnetsDescription": "Add CIDR ranges that can be accessed from this site remotely using clients. Use format like 10.0.0.0/24. This ONLY applies to VPN client connectivity.",
|
||||||
"resourceEnableProxy": "Enable Public Proxy",
|
"resourceEnableProxy": "Enable Public Proxy",
|
||||||
"resourceEnableProxyDescription": "Enable public proxying to this resource. This allows access to the resource from outside the network through the cloud on an open port. Requires Traefik config.",
|
"resourceEnableProxyDescription": "Enable public proxying to this resource. This allows access to the resource from outside the network through the cloud on an open port. Requires Traefik config.",
|
||||||
"externalProxyEnabled": "External Proxy Enabled"
|
"externalProxyEnabled": "External Proxy Enabled"
|
||||||
}
|
}
|
||||||
1348
messages/nb-NO.json
Normal file
1348
messages/nb-NO.json
Normal file
File diff suppressed because it is too large
Load Diff
@@ -833,6 +833,24 @@
|
|||||||
"pincodeRequirementsLength": "Pincode moet precies 6 cijfers zijn",
|
"pincodeRequirementsLength": "Pincode moet precies 6 cijfers zijn",
|
||||||
"pincodeRequirementsChars": "Pincode mag alleen cijfers bevatten",
|
"pincodeRequirementsChars": "Pincode mag alleen cijfers bevatten",
|
||||||
"passwordRequirementsLength": "Wachtwoord moet ten minste 1 teken lang zijn",
|
"passwordRequirementsLength": "Wachtwoord moet ten minste 1 teken lang zijn",
|
||||||
|
"passwordRequirementsTitle": "Password requirements:",
|
||||||
|
"passwordRequirementLength": "At least 8 characters long",
|
||||||
|
"passwordRequirementUppercase": "At least one uppercase letter",
|
||||||
|
"passwordRequirementLowercase": "At least one lowercase letter",
|
||||||
|
"passwordRequirementNumber": "At least one number",
|
||||||
|
"passwordRequirementSpecial": "At least one special character",
|
||||||
|
"passwordRequirementsMet": "✓ Password meets all requirements",
|
||||||
|
"passwordStrength": "Password strength",
|
||||||
|
"passwordStrengthWeak": "Weak",
|
||||||
|
"passwordStrengthMedium": "Medium",
|
||||||
|
"passwordStrengthStrong": "Strong",
|
||||||
|
"passwordRequirements": "Requirements:",
|
||||||
|
"passwordRequirementLengthText": "8+ characters",
|
||||||
|
"passwordRequirementUppercaseText": "Uppercase letter (A-Z)",
|
||||||
|
"passwordRequirementLowercaseText": "Lowercase letter (a-z)",
|
||||||
|
"passwordRequirementNumberText": "Number (0-9)",
|
||||||
|
"passwordRequirementSpecialText": "Special character (!@#$%...)",
|
||||||
|
"passwordsDoNotMatch": "Passwords do not match",
|
||||||
"otpEmailRequirementsLength": "OTP moet minstens 1 teken lang zijn",
|
"otpEmailRequirementsLength": "OTP moet minstens 1 teken lang zijn",
|
||||||
"otpEmailSent": "OTP verzonden",
|
"otpEmailSent": "OTP verzonden",
|
||||||
"otpEmailSentDescription": "Een OTP is naar uw e-mail verzonden",
|
"otpEmailSentDescription": "Een OTP is naar uw e-mail verzonden",
|
||||||
@@ -967,6 +985,9 @@
|
|||||||
"actionDeleteSite": "Site verwijderen",
|
"actionDeleteSite": "Site verwijderen",
|
||||||
"actionGetSite": "Site ophalen",
|
"actionGetSite": "Site ophalen",
|
||||||
"actionListSites": "Sites weergeven",
|
"actionListSites": "Sites weergeven",
|
||||||
|
"setupToken": "Setup Token",
|
||||||
|
"setupTokenPlaceholder": "Enter the setup token from the server console",
|
||||||
|
"setupTokenRequired": "Setup token is required",
|
||||||
"actionUpdateSite": "Site bijwerken",
|
"actionUpdateSite": "Site bijwerken",
|
||||||
"actionListSiteRoles": "Toon toegestane sitenollen",
|
"actionListSiteRoles": "Toon toegestane sitenollen",
|
||||||
"actionCreateResource": "Bron maken",
|
"actionCreateResource": "Bron maken",
|
||||||
@@ -1022,6 +1043,11 @@
|
|||||||
"actionDeleteIdpOrg": "Verwijder IDP Org Beleid",
|
"actionDeleteIdpOrg": "Verwijder IDP Org Beleid",
|
||||||
"actionListIdpOrgs": "Toon IDP Orgs",
|
"actionListIdpOrgs": "Toon IDP Orgs",
|
||||||
"actionUpdateIdpOrg": "IDP-org bijwerken",
|
"actionUpdateIdpOrg": "IDP-org bijwerken",
|
||||||
|
"actionCreateClient": "Client aanmaken",
|
||||||
|
"actionDeleteClient": "Verwijder klant",
|
||||||
|
"actionUpdateClient": "Klant bijwerken",
|
||||||
|
"actionListClients": "Lijst klanten",
|
||||||
|
"actionGetClient": "Client ophalen",
|
||||||
"noneSelected": "Niet geselecteerd",
|
"noneSelected": "Niet geselecteerd",
|
||||||
"orgNotFound2": "Geen organisaties gevonden.",
|
"orgNotFound2": "Geen organisaties gevonden.",
|
||||||
"searchProgress": "Zoeken...",
|
"searchProgress": "Zoeken...",
|
||||||
@@ -1315,8 +1341,8 @@
|
|||||||
"olmErrorFetchLatest": "Er is een fout opgetreden bij het ophalen van de nieuwste Olm release.",
|
"olmErrorFetchLatest": "Er is een fout opgetreden bij het ophalen van de nieuwste Olm release.",
|
||||||
"remoteSubnets": "Externe Subnets",
|
"remoteSubnets": "Externe Subnets",
|
||||||
"enterCidrRange": "Voer CIDR-bereik in",
|
"enterCidrRange": "Voer CIDR-bereik in",
|
||||||
"remoteSubnetsDescription": "Voeg CIDR-bereiken toe die deze site op afstand kunnen openen. Gebruik een format zoals 10.0.0.0/24 of 192.168.1.0/24.",
|
"remoteSubnetsDescription": "Add CIDR ranges that can be accessed from this site remotely using clients. Use format like 10.0.0.0/24. This ONLY applies to VPN client connectivity.",
|
||||||
"resourceEnableProxy": "Openbare proxy inschakelen",
|
"resourceEnableProxy": "Openbare proxy inschakelen",
|
||||||
"resourceEnableProxyDescription": "Schakel publieke proxy in voor deze resource. Dit maakt toegang tot de resource mogelijk vanuit het netwerk via de cloud met een open poort. Vereist Traefik-configuratie.",
|
"resourceEnableProxyDescription": "Schakel publieke proxy in voor deze resource. Dit maakt toegang tot de resource mogelijk vanuit het netwerk via de cloud met een open poort. Vereist Traefik-configuratie.",
|
||||||
"externalProxyEnabled": "Externe Proxy Ingeschakeld"
|
"externalProxyEnabled": "Externe Proxy Ingeschakeld"
|
||||||
}
|
}
|
||||||
@@ -833,6 +833,24 @@
|
|||||||
"pincodeRequirementsLength": "PIN musi składać się dokładnie z 6 cyfr",
|
"pincodeRequirementsLength": "PIN musi składać się dokładnie z 6 cyfr",
|
||||||
"pincodeRequirementsChars": "PIN może zawierać tylko cyfry",
|
"pincodeRequirementsChars": "PIN może zawierać tylko cyfry",
|
||||||
"passwordRequirementsLength": "Hasło musi mieć co najmniej 1 znak",
|
"passwordRequirementsLength": "Hasło musi mieć co najmniej 1 znak",
|
||||||
|
"passwordRequirementsTitle": "Password requirements:",
|
||||||
|
"passwordRequirementLength": "At least 8 characters long",
|
||||||
|
"passwordRequirementUppercase": "At least one uppercase letter",
|
||||||
|
"passwordRequirementLowercase": "At least one lowercase letter",
|
||||||
|
"passwordRequirementNumber": "At least one number",
|
||||||
|
"passwordRequirementSpecial": "At least one special character",
|
||||||
|
"passwordRequirementsMet": "✓ Password meets all requirements",
|
||||||
|
"passwordStrength": "Password strength",
|
||||||
|
"passwordStrengthWeak": "Weak",
|
||||||
|
"passwordStrengthMedium": "Medium",
|
||||||
|
"passwordStrengthStrong": "Strong",
|
||||||
|
"passwordRequirements": "Requirements:",
|
||||||
|
"passwordRequirementLengthText": "8+ characters",
|
||||||
|
"passwordRequirementUppercaseText": "Uppercase letter (A-Z)",
|
||||||
|
"passwordRequirementLowercaseText": "Lowercase letter (a-z)",
|
||||||
|
"passwordRequirementNumberText": "Number (0-9)",
|
||||||
|
"passwordRequirementSpecialText": "Special character (!@#$%...)",
|
||||||
|
"passwordsDoNotMatch": "Passwords do not match",
|
||||||
"otpEmailRequirementsLength": "Kod jednorazowy musi mieć co najmniej 1 znak",
|
"otpEmailRequirementsLength": "Kod jednorazowy musi mieć co najmniej 1 znak",
|
||||||
"otpEmailSent": "Kod jednorazowy wysłany",
|
"otpEmailSent": "Kod jednorazowy wysłany",
|
||||||
"otpEmailSentDescription": "Kod jednorazowy został wysłany na Twój e-mail",
|
"otpEmailSentDescription": "Kod jednorazowy został wysłany na Twój e-mail",
|
||||||
@@ -967,6 +985,9 @@
|
|||||||
"actionDeleteSite": "Usuń witrynę",
|
"actionDeleteSite": "Usuń witrynę",
|
||||||
"actionGetSite": "Pobierz witrynę",
|
"actionGetSite": "Pobierz witrynę",
|
||||||
"actionListSites": "Lista witryn",
|
"actionListSites": "Lista witryn",
|
||||||
|
"setupToken": "Setup Token",
|
||||||
|
"setupTokenPlaceholder": "Enter the setup token from the server console",
|
||||||
|
"setupTokenRequired": "Setup token is required",
|
||||||
"actionUpdateSite": "Aktualizuj witrynę",
|
"actionUpdateSite": "Aktualizuj witrynę",
|
||||||
"actionListSiteRoles": "Lista dozwolonych ról witryny",
|
"actionListSiteRoles": "Lista dozwolonych ról witryny",
|
||||||
"actionCreateResource": "Utwórz zasób",
|
"actionCreateResource": "Utwórz zasób",
|
||||||
@@ -1022,6 +1043,11 @@
|
|||||||
"actionDeleteIdpOrg": "Usuń politykę organizacji IDP",
|
"actionDeleteIdpOrg": "Usuń politykę organizacji IDP",
|
||||||
"actionListIdpOrgs": "Lista organizacji IDP",
|
"actionListIdpOrgs": "Lista organizacji IDP",
|
||||||
"actionUpdateIdpOrg": "Aktualizuj organizację IDP",
|
"actionUpdateIdpOrg": "Aktualizuj organizację IDP",
|
||||||
|
"actionCreateClient": "Utwórz klienta",
|
||||||
|
"actionDeleteClient": "Usuń klienta",
|
||||||
|
"actionUpdateClient": "Aktualizuj klienta",
|
||||||
|
"actionListClients": "Lista klientów",
|
||||||
|
"actionGetClient": "Pobierz klienta",
|
||||||
"noneSelected": "Nie wybrano",
|
"noneSelected": "Nie wybrano",
|
||||||
"orgNotFound2": "Nie znaleziono organizacji.",
|
"orgNotFound2": "Nie znaleziono organizacji.",
|
||||||
"searchProgress": "Szukaj...",
|
"searchProgress": "Szukaj...",
|
||||||
@@ -1315,8 +1341,8 @@
|
|||||||
"olmErrorFetchLatest": "Wystąpił błąd podczas pobierania najnowszego wydania Olm.",
|
"olmErrorFetchLatest": "Wystąpił błąd podczas pobierania najnowszego wydania Olm.",
|
||||||
"remoteSubnets": "Zdalne Podsieci",
|
"remoteSubnets": "Zdalne Podsieci",
|
||||||
"enterCidrRange": "Wprowadź zakres CIDR",
|
"enterCidrRange": "Wprowadź zakres CIDR",
|
||||||
"remoteSubnetsDescription": "Dodaj zakresy CIDR, które mogą uzyskać zdalny dostęp do tej witryny. Użyj formatu takiego jak 10.0.0.0/24 lub 192.168.1.0/24.",
|
"remoteSubnetsDescription": "Add CIDR ranges that can be accessed from this site remotely using clients. Use format like 10.0.0.0/24. This ONLY applies to VPN client connectivity.",
|
||||||
"resourceEnableProxy": "Włącz publiczny proxy",
|
"resourceEnableProxy": "Włącz publiczny proxy",
|
||||||
"resourceEnableProxyDescription": "Włącz publiczne proxy dla tego zasobu. To umożliwia dostęp do zasobu spoza sieci przez chmurę na otwartym porcie. Wymaga konfiguracji Traefik.",
|
"resourceEnableProxyDescription": "Włącz publiczne proxy dla tego zasobu. To umożliwia dostęp do zasobu spoza sieci przez chmurę na otwartym porcie. Wymaga konfiguracji Traefik.",
|
||||||
"externalProxyEnabled": "Zewnętrzny Proxy Włączony"
|
"externalProxyEnabled": "Zewnętrzny Proxy Włączony"
|
||||||
}
|
}
|
||||||
@@ -833,6 +833,24 @@
|
|||||||
"pincodeRequirementsLength": "O PIN deve ter exatamente 6 dígitos",
|
"pincodeRequirementsLength": "O PIN deve ter exatamente 6 dígitos",
|
||||||
"pincodeRequirementsChars": "O PIN deve conter apenas números",
|
"pincodeRequirementsChars": "O PIN deve conter apenas números",
|
||||||
"passwordRequirementsLength": "A palavra-passe deve ter pelo menos 1 caractere",
|
"passwordRequirementsLength": "A palavra-passe deve ter pelo menos 1 caractere",
|
||||||
|
"passwordRequirementsTitle": "Password requirements:",
|
||||||
|
"passwordRequirementLength": "At least 8 characters long",
|
||||||
|
"passwordRequirementUppercase": "At least one uppercase letter",
|
||||||
|
"passwordRequirementLowercase": "At least one lowercase letter",
|
||||||
|
"passwordRequirementNumber": "At least one number",
|
||||||
|
"passwordRequirementSpecial": "At least one special character",
|
||||||
|
"passwordRequirementsMet": "✓ Password meets all requirements",
|
||||||
|
"passwordStrength": "Password strength",
|
||||||
|
"passwordStrengthWeak": "Weak",
|
||||||
|
"passwordStrengthMedium": "Medium",
|
||||||
|
"passwordStrengthStrong": "Strong",
|
||||||
|
"passwordRequirements": "Requirements:",
|
||||||
|
"passwordRequirementLengthText": "8+ characters",
|
||||||
|
"passwordRequirementUppercaseText": "Uppercase letter (A-Z)",
|
||||||
|
"passwordRequirementLowercaseText": "Lowercase letter (a-z)",
|
||||||
|
"passwordRequirementNumberText": "Number (0-9)",
|
||||||
|
"passwordRequirementSpecialText": "Special character (!@#$%...)",
|
||||||
|
"passwordsDoNotMatch": "Passwords do not match",
|
||||||
"otpEmailRequirementsLength": "O OTP deve ter pelo menos 1 caractere",
|
"otpEmailRequirementsLength": "O OTP deve ter pelo menos 1 caractere",
|
||||||
"otpEmailSent": "OTP Enviado",
|
"otpEmailSent": "OTP Enviado",
|
||||||
"otpEmailSentDescription": "Um OTP foi enviado para o seu email",
|
"otpEmailSentDescription": "Um OTP foi enviado para o seu email",
|
||||||
@@ -967,6 +985,9 @@
|
|||||||
"actionDeleteSite": "Eliminar Site",
|
"actionDeleteSite": "Eliminar Site",
|
||||||
"actionGetSite": "Obter Site",
|
"actionGetSite": "Obter Site",
|
||||||
"actionListSites": "Listar Sites",
|
"actionListSites": "Listar Sites",
|
||||||
|
"setupToken": "Setup Token",
|
||||||
|
"setupTokenPlaceholder": "Enter the setup token from the server console",
|
||||||
|
"setupTokenRequired": "Setup token is required",
|
||||||
"actionUpdateSite": "Atualizar Site",
|
"actionUpdateSite": "Atualizar Site",
|
||||||
"actionListSiteRoles": "Listar Funções Permitidas do Site",
|
"actionListSiteRoles": "Listar Funções Permitidas do Site",
|
||||||
"actionCreateResource": "Criar Recurso",
|
"actionCreateResource": "Criar Recurso",
|
||||||
@@ -1022,6 +1043,11 @@
|
|||||||
"actionDeleteIdpOrg": "Eliminar Política de Organização IDP",
|
"actionDeleteIdpOrg": "Eliminar Política de Organização IDP",
|
||||||
"actionListIdpOrgs": "Listar Organizações IDP",
|
"actionListIdpOrgs": "Listar Organizações IDP",
|
||||||
"actionUpdateIdpOrg": "Atualizar Organização IDP",
|
"actionUpdateIdpOrg": "Atualizar Organização IDP",
|
||||||
|
"actionCreateClient": "Criar Cliente",
|
||||||
|
"actionDeleteClient": "Excluir Cliente",
|
||||||
|
"actionUpdateClient": "Atualizar Cliente",
|
||||||
|
"actionListClients": "Listar Clientes",
|
||||||
|
"actionGetClient": "Obter Cliente",
|
||||||
"noneSelected": "Nenhum selecionado",
|
"noneSelected": "Nenhum selecionado",
|
||||||
"orgNotFound2": "Nenhuma organização encontrada.",
|
"orgNotFound2": "Nenhuma organização encontrada.",
|
||||||
"searchProgress": "Pesquisar...",
|
"searchProgress": "Pesquisar...",
|
||||||
@@ -1315,8 +1341,8 @@
|
|||||||
"olmErrorFetchLatest": "Ocorreu um erro ao buscar o lançamento mais recente do Olm.",
|
"olmErrorFetchLatest": "Ocorreu um erro ao buscar o lançamento mais recente do Olm.",
|
||||||
"remoteSubnets": "Sub-redes Remotas",
|
"remoteSubnets": "Sub-redes Remotas",
|
||||||
"enterCidrRange": "Insira o intervalo CIDR",
|
"enterCidrRange": "Insira o intervalo CIDR",
|
||||||
"remoteSubnetsDescription": "Adicione intervalos CIDR que podem acessar este site remotamente. Use o formato como 10.0.0.0/24 ou 192.168.1.0/24.",
|
"remoteSubnetsDescription": "Add CIDR ranges that can be accessed from this site remotely using clients. Use format like 10.0.0.0/24. This ONLY applies to VPN client connectivity.",
|
||||||
"resourceEnableProxy": "Ativar Proxy Público",
|
"resourceEnableProxy": "Ativar Proxy Público",
|
||||||
"resourceEnableProxyDescription": "Permite proxy público para este recurso. Isso permite o acesso ao recurso de fora da rede através da nuvem em uma porta aberta. Requer configuração do Traefik.",
|
"resourceEnableProxyDescription": "Permite proxy público para este recurso. Isso permite o acesso ao recurso de fora da rede através da nuvem em uma porta aberta. Requer configuração do Traefik.",
|
||||||
"externalProxyEnabled": "Proxy Externo Habilitado"
|
"externalProxyEnabled": "Proxy Externo Habilitado"
|
||||||
}
|
}
|
||||||
@@ -833,6 +833,24 @@
|
|||||||
"pincodeRequirementsLength": "PIN должен состоять ровно из 6 цифр",
|
"pincodeRequirementsLength": "PIN должен состоять ровно из 6 цифр",
|
||||||
"pincodeRequirementsChars": "PIN должен содержать только цифры",
|
"pincodeRequirementsChars": "PIN должен содержать только цифры",
|
||||||
"passwordRequirementsLength": "Пароль должен быть не менее 1 символа",
|
"passwordRequirementsLength": "Пароль должен быть не менее 1 символа",
|
||||||
|
"passwordRequirementsTitle": "Password requirements:",
|
||||||
|
"passwordRequirementLength": "At least 8 characters long",
|
||||||
|
"passwordRequirementUppercase": "At least one uppercase letter",
|
||||||
|
"passwordRequirementLowercase": "At least one lowercase letter",
|
||||||
|
"passwordRequirementNumber": "At least one number",
|
||||||
|
"passwordRequirementSpecial": "At least one special character",
|
||||||
|
"passwordRequirementsMet": "✓ Password meets all requirements",
|
||||||
|
"passwordStrength": "Password strength",
|
||||||
|
"passwordStrengthWeak": "Weak",
|
||||||
|
"passwordStrengthMedium": "Medium",
|
||||||
|
"passwordStrengthStrong": "Strong",
|
||||||
|
"passwordRequirements": "Requirements:",
|
||||||
|
"passwordRequirementLengthText": "8+ characters",
|
||||||
|
"passwordRequirementUppercaseText": "Uppercase letter (A-Z)",
|
||||||
|
"passwordRequirementLowercaseText": "Lowercase letter (a-z)",
|
||||||
|
"passwordRequirementNumberText": "Number (0-9)",
|
||||||
|
"passwordRequirementSpecialText": "Special character (!@#$%...)",
|
||||||
|
"passwordsDoNotMatch": "Passwords do not match",
|
||||||
"otpEmailRequirementsLength": "OTP должен быть не менее 1 символа",
|
"otpEmailRequirementsLength": "OTP должен быть не менее 1 символа",
|
||||||
"otpEmailSent": "OTP отправлен",
|
"otpEmailSent": "OTP отправлен",
|
||||||
"otpEmailSentDescription": "OTP был отправлен на ваш email",
|
"otpEmailSentDescription": "OTP был отправлен на ваш email",
|
||||||
@@ -925,74 +943,77 @@
|
|||||||
"supportKeyInvalid": "Недействительный ключ",
|
"supportKeyInvalid": "Недействительный ключ",
|
||||||
"supportKeyInvalidDescription": "Ваш ключ поддержки недействителен.",
|
"supportKeyInvalidDescription": "Ваш ключ поддержки недействителен.",
|
||||||
"supportKeyValid": "Действительный ключ",
|
"supportKeyValid": "Действительный ключ",
|
||||||
"supportKeyValidDescription": "Your supporter key has been validated. Thank you for your support!",
|
"supportKeyValidDescription": "Ваш ключ поддержки был проверен. Спасибо за поддержку!",
|
||||||
"supportKeyErrorValidationDescription": "Failed to validate supporter key.",
|
"supportKeyErrorValidationDescription": "Не удалось проверить ключ поддержки.",
|
||||||
"supportKey": "Support Development and Adopt a Pangolin!",
|
"supportKey": "Поддержите разработку и усыновите Панголина!",
|
||||||
"supportKeyDescription": "Приобретите ключ поддержки, чтобы помочь нам продолжать разработку Pangolin для сообщества. Ваш вклад позволяет нам уделять больше времени поддержке и добавлению новых функций в приложение для всех. Мы никогда не будем использовать это для платного доступа к функциям. Это отдельно от любой коммерческой версии.",
|
"supportKeyDescription": "Приобретите ключ поддержки, чтобы помочь нам продолжать разработку Pangolin для сообщества. Ваш вклад позволяет нам уделять больше времени поддержке и добавлению новых функций в приложение для всех. Мы никогда не будем использовать это для платного доступа к функциям. Это отдельно от любой коммерческой версии.",
|
||||||
"supportKeyPet": "You will also get to adopt and meet your very own pet Pangolin!",
|
"supportKeyPet": "Вы также сможете усыновить и встретить вашего собственного питомца Панголина!",
|
||||||
"supportKeyPurchase": "Payments are processed via GitHub. Afterward, you can retrieve your key on",
|
"supportKeyPurchase": "Платежи обрабатываются через GitHub. После этого вы сможете получить свой ключ на",
|
||||||
"supportKeyPurchaseLink": "our website",
|
"supportKeyPurchaseLink": "нашем сайте",
|
||||||
"supportKeyPurchase2": "and redeem it here.",
|
"supportKeyPurchase2": "и активировать его здесь.",
|
||||||
"supportKeyLearnMore": "Learn more.",
|
"supportKeyLearnMore": "Узнать больше.",
|
||||||
"supportKeyOptions": "Please select the option that best suits you.",
|
"supportKeyOptions": "Пожалуйста, выберите подходящий вам вариант.",
|
||||||
"supportKetOptionFull": "Full Supporter",
|
"supportKetOptionFull": "Полная поддержка",
|
||||||
"forWholeServer": "For the whole server",
|
"forWholeServer": "За весь сервер",
|
||||||
"lifetimePurchase": "Lifetime purchase",
|
"lifetimePurchase": "Пожизненная покупка",
|
||||||
"supporterStatus": "Supporter status",
|
"supporterStatus": "Статус поддержки",
|
||||||
"buy": "Buy",
|
"buy": "Купить",
|
||||||
"supportKeyOptionLimited": "Limited Supporter",
|
"supportKeyOptionLimited": "Лимитированная поддержка",
|
||||||
"forFiveUsers": "For 5 or less users",
|
"forFiveUsers": "За 5 или меньше пользователей",
|
||||||
"supportKeyRedeem": "Redeem Supporter Key",
|
"supportKeyRedeem": "Использовать ключ Поддержки",
|
||||||
"supportKeyHideSevenDays": "Hide for 7 days",
|
"supportKeyHideSevenDays": "Скрыть на 7 дней",
|
||||||
"supportKeyEnter": "Enter Supporter Key",
|
"supportKeyEnter": "Введите ключ поддержки",
|
||||||
"supportKeyEnterDescription": "Meet your very own pet Pangolin!",
|
"supportKeyEnterDescription": "Встречайте своего питомца Панголина!",
|
||||||
"githubUsername": "GitHub Username",
|
"githubUsername": "Имя пользователя Github",
|
||||||
"supportKeyInput": "Supporter Key",
|
"supportKeyInput": "Ключ поддержки",
|
||||||
"supportKeyBuy": "Buy Supporter Key",
|
"supportKeyBuy": "Ключ поддержки",
|
||||||
"logoutError": "Error logging out",
|
"logoutError": "Ошибка при выходе",
|
||||||
"signingAs": "Signed in as",
|
"signingAs": "Вы вошли как",
|
||||||
"serverAdmin": "Server Admin",
|
"serverAdmin": "Администратор сервера",
|
||||||
"otpEnable": "Enable Two-factor",
|
"otpEnable": "Включить Двухфакторную Аутентификацию",
|
||||||
"otpDisable": "Disable Two-factor",
|
"otpDisable": "Отключить двухфакторную аутентификацию",
|
||||||
"logout": "Log Out",
|
"logout": "Выйти",
|
||||||
"licenseTierProfessionalRequired": "Professional Edition Required",
|
"licenseTierProfessionalRequired": "Требуется профессиональная версия",
|
||||||
"licenseTierProfessionalRequiredDescription": "Эта функция доступна только в профессиональной версии.",
|
"licenseTierProfessionalRequiredDescription": "Эта функция доступна только в профессиональной версии.",
|
||||||
"actionGetOrg": "Get Organization",
|
"actionGetOrg": "Получить организацию",
|
||||||
"actionUpdateOrg": "Update Organization",
|
"actionUpdateOrg": "Обновить организацию",
|
||||||
"actionUpdateUser": "Update User",
|
"actionUpdateUser": "Обновить пользователя",
|
||||||
"actionGetUser": "Get User",
|
"actionGetUser": "Получить пользователя",
|
||||||
"actionGetOrgUser": "Get Organization User",
|
"actionGetOrgUser": "Получить пользователя организации",
|
||||||
"actionListOrgDomains": "List Organization Domains",
|
"actionListOrgDomains": "Список доменов организации",
|
||||||
"actionCreateSite": "Create Site",
|
"actionCreateSite": "Создать сайт",
|
||||||
"actionDeleteSite": "Delete Site",
|
"actionDeleteSite": "Удалить сайт",
|
||||||
"actionGetSite": "Get Site",
|
"actionGetSite": "Получить сайт",
|
||||||
"actionListSites": "List Sites",
|
"actionListSites": "Список сайтов",
|
||||||
"actionUpdateSite": "Update Site",
|
"setupToken": "Setup Token",
|
||||||
"actionListSiteRoles": "List Allowed Site Roles",
|
"setupTokenPlaceholder": "Enter the setup token from the server console",
|
||||||
"actionCreateResource": "Create Resource",
|
"setupTokenRequired": "Setup token is required",
|
||||||
"actionDeleteResource": "Delete Resource",
|
"actionUpdateSite": "Обновить сайт",
|
||||||
"actionGetResource": "Get Resource",
|
"actionListSiteRoles": "Список разрешенных ролей сайта",
|
||||||
"actionListResource": "List Resources",
|
"actionCreateResource": "Создать ресурс",
|
||||||
"actionUpdateResource": "Update Resource",
|
"actionDeleteResource": "Удалить ресурс",
|
||||||
"actionListResourceUsers": "List Resource Users",
|
"actionGetResource": "Получить ресурсы",
|
||||||
"actionSetResourceUsers": "Set Resource Users",
|
"actionListResource": "Список ресурсов",
|
||||||
"actionSetAllowedResourceRoles": "Set Allowed Resource Roles",
|
"actionUpdateResource": "Обновить ресурс",
|
||||||
"actionListAllowedResourceRoles": "List Allowed Resource Roles",
|
"actionListResourceUsers": "Список пользователей ресурсов",
|
||||||
"actionSetResourcePassword": "Set Resource Password",
|
"actionSetResourceUsers": "Список пользователей ресурсов",
|
||||||
"actionSetResourcePincode": "Set Resource Pincode",
|
"actionSetAllowedResourceRoles": "Набор разрешенных ролей ресурсов",
|
||||||
|
"actionListAllowedResourceRoles": "Список разрешенных ролей сайта",
|
||||||
|
"actionSetResourcePassword": "Задать пароль ресурса",
|
||||||
|
"actionSetResourcePincode": "Установить ПИН-код ресурса",
|
||||||
"actionSetResourceEmailWhitelist": "Set Resource Email Whitelist",
|
"actionSetResourceEmailWhitelist": "Set Resource Email Whitelist",
|
||||||
"actionGetResourceEmailWhitelist": "Get Resource Email Whitelist",
|
"actionGetResourceEmailWhitelist": "Get Resource Email Whitelist",
|
||||||
"actionCreateTarget": "Create Target",
|
"actionCreateTarget": "Создать цель",
|
||||||
"actionDeleteTarget": "Delete Target",
|
"actionDeleteTarget": "Удалить цель",
|
||||||
"actionGetTarget": "Get Target",
|
"actionGetTarget": "Получить цель",
|
||||||
"actionListTargets": "List Targets",
|
"actionListTargets": "Список целей",
|
||||||
"actionUpdateTarget": "Update Target",
|
"actionUpdateTarget": "Обновить цель",
|
||||||
"actionCreateRole": "Create Role",
|
"actionCreateRole": "Создать роль",
|
||||||
"actionDeleteRole": "Delete Role",
|
"actionDeleteRole": "Удалить роль",
|
||||||
"actionGetRole": "Get Role",
|
"actionGetRole": "Получить Роль",
|
||||||
"actionListRole": "List Roles",
|
"actionListRole": "Список ролей",
|
||||||
"actionUpdateRole": "Update Role",
|
"actionUpdateRole": "Обновить роль",
|
||||||
"actionListAllowedRoleResources": "List Allowed Role Resources",
|
"actionListAllowedRoleResources": "Список разрешенных ролей сайта",
|
||||||
"actionInviteUser": "Пригласить пользователя",
|
"actionInviteUser": "Пригласить пользователя",
|
||||||
"actionRemoveUser": "Удалить пользователя",
|
"actionRemoveUser": "Удалить пользователя",
|
||||||
"actionListUsers": "Список пользователей",
|
"actionListUsers": "Список пользователей",
|
||||||
@@ -1022,6 +1043,11 @@
|
|||||||
"actionDeleteIdpOrg": "Удалить политику IDP организации",
|
"actionDeleteIdpOrg": "Удалить политику IDP организации",
|
||||||
"actionListIdpOrgs": "Список организаций IDP",
|
"actionListIdpOrgs": "Список организаций IDP",
|
||||||
"actionUpdateIdpOrg": "Обновить организацию IDP",
|
"actionUpdateIdpOrg": "Обновить организацию IDP",
|
||||||
|
"actionCreateClient": "Создать Клиента",
|
||||||
|
"actionDeleteClient": "Удалить Клиента",
|
||||||
|
"actionUpdateClient": "Обновить Клиента",
|
||||||
|
"actionListClients": "Список Клиентов",
|
||||||
|
"actionGetClient": "Получить Клиента",
|
||||||
"noneSelected": "Ничего не выбрано",
|
"noneSelected": "Ничего не выбрано",
|
||||||
"orgNotFound2": "Организации не найдены.",
|
"orgNotFound2": "Организации не найдены.",
|
||||||
"searchProgress": "Поиск...",
|
"searchProgress": "Поиск...",
|
||||||
@@ -1093,8 +1119,8 @@
|
|||||||
"sidebarAllUsers": "Все пользователи",
|
"sidebarAllUsers": "Все пользователи",
|
||||||
"sidebarIdentityProviders": "Поставщики удостоверений",
|
"sidebarIdentityProviders": "Поставщики удостоверений",
|
||||||
"sidebarLicense": "Лицензия",
|
"sidebarLicense": "Лицензия",
|
||||||
"sidebarClients": "Clients (Beta)",
|
"sidebarClients": "Клиенты (бета)",
|
||||||
"sidebarDomains": "Domains",
|
"sidebarDomains": "Домены",
|
||||||
"enableDockerSocket": "Включить Docker Socket",
|
"enableDockerSocket": "Включить Docker Socket",
|
||||||
"enableDockerSocketDescription": "Включить обнаружение Docker Socket для заполнения информации о контейнерах. Путь к сокету должен быть предоставлен Newt.",
|
"enableDockerSocketDescription": "Включить обнаружение Docker Socket для заполнения информации о контейнерах. Путь к сокету должен быть предоставлен Newt.",
|
||||||
"enableDockerSocketLink": "Узнать больше",
|
"enableDockerSocketLink": "Узнать больше",
|
||||||
@@ -1134,36 +1160,36 @@
|
|||||||
"dark": "тёмная",
|
"dark": "тёмная",
|
||||||
"system": "системная",
|
"system": "системная",
|
||||||
"theme": "Тема",
|
"theme": "Тема",
|
||||||
"subnetRequired": "Subnet is required",
|
"subnetRequired": "Требуется подсеть",
|
||||||
"initialSetupTitle": "Начальная настройка сервера",
|
"initialSetupTitle": "Начальная настройка сервера",
|
||||||
"initialSetupDescription": "Создайте первоначальную учётную запись администратора сервера. Может существовать только один администратор сервера. Вы всегда можете изменить эти учётные данные позже.",
|
"initialSetupDescription": "Создайте первоначальную учётную запись администратора сервера. Может существовать только один администратор сервера. Вы всегда можете изменить эти учётные данные позже.",
|
||||||
"createAdminAccount": "Создать учётную запись администратора",
|
"createAdminAccount": "Создать учётную запись администратора",
|
||||||
"setupErrorCreateAdmin": "Произошла ошибка при создании учётной записи администратора сервера.",
|
"setupErrorCreateAdmin": "Произошла ошибка при создании учётной записи администратора сервера.",
|
||||||
"certificateStatus": "Certificate Status",
|
"certificateStatus": "Статус сертификата",
|
||||||
"loading": "Loading",
|
"loading": "Загрузка",
|
||||||
"restart": "Restart",
|
"restart": "Перезагрузка",
|
||||||
"domains": "Domains",
|
"domains": "Домены",
|
||||||
"domainsDescription": "Manage domains for your organization",
|
"domainsDescription": "Управление доменами для вашей организации",
|
||||||
"domainsSearch": "Search domains...",
|
"domainsSearch": "Поиск доменов...",
|
||||||
"domainAdd": "Add Domain",
|
"domainAdd": "Добавить Домен",
|
||||||
"domainAddDescription": "Register a new domain with your organization",
|
"domainAddDescription": "Зарегистрировать новый домен в вашей организации",
|
||||||
"domainCreate": "Create Domain",
|
"domainCreate": "Создать Домен",
|
||||||
"domainCreatedDescription": "Domain created successfully",
|
"domainCreatedDescription": "Домен успешно создан",
|
||||||
"domainDeletedDescription": "Domain deleted successfully",
|
"domainDeletedDescription": "Домен успешно удален",
|
||||||
"domainQuestionRemove": "Are you sure you want to remove the domain {domain} from your account?",
|
"domainQuestionRemove": "Вы уверены, что хотите удалить домен {domain} из вашего аккаунта?",
|
||||||
"domainMessageRemove": "Once removed, the domain will no longer be associated with your account.",
|
"domainMessageRemove": "После удаления домен больше не будет связан с вашей учетной записью.",
|
||||||
"domainMessageConfirm": "To confirm, please type the domain name below.",
|
"domainMessageConfirm": "Для подтверждения введите ниже имя домена.",
|
||||||
"domainConfirmDelete": "Confirm Delete Domain",
|
"domainConfirmDelete": "Подтвердить удаление домена",
|
||||||
"domainDelete": "Delete Domain",
|
"domainDelete": "Удалить Домен",
|
||||||
"domain": "Domain",
|
"domain": "Домен",
|
||||||
"selectDomainTypeNsName": "Domain Delegation (NS)",
|
"selectDomainTypeNsName": "Делегация домена (NS)",
|
||||||
"selectDomainTypeNsDescription": "This domain and all its subdomains. Use this when you want to control an entire domain zone.",
|
"selectDomainTypeNsDescription": "Этот домен и все его субдомены. Используйте это, когда вы хотите управлять всей доменной зоной.",
|
||||||
"selectDomainTypeCnameName": "Single Domain (CNAME)",
|
"selectDomainTypeCnameName": "Одиночный домен (CNAME)",
|
||||||
"selectDomainTypeCnameDescription": "Just this specific domain. Use this for individual subdomains or specific domain entries.",
|
"selectDomainTypeCnameDescription": "Только этот конкретный домен. Используйте это для отдельных субдоменов или отдельных записей домена.",
|
||||||
"selectDomainTypeWildcardName": "Wildcard Domain",
|
"selectDomainTypeWildcardName": "Wildcard Domain",
|
||||||
"selectDomainTypeWildcardDescription": "This domain and its subdomains.",
|
"selectDomainTypeWildcardDescription": "Этот домен и его субдомены.",
|
||||||
"domainDelegation": "Single Domain",
|
"domainDelegation": "Единый домен",
|
||||||
"selectType": "Select a type",
|
"selectType": "Выберите тип",
|
||||||
"actions": "Actions",
|
"actions": "Actions",
|
||||||
"refresh": "Refresh",
|
"refresh": "Refresh",
|
||||||
"refreshError": "Failed to refresh data",
|
"refreshError": "Failed to refresh data",
|
||||||
@@ -1268,19 +1294,19 @@
|
|||||||
"createDomainARecords": "A Records",
|
"createDomainARecords": "A Records",
|
||||||
"createDomainRecordNumber": "Record {number}",
|
"createDomainRecordNumber": "Record {number}",
|
||||||
"createDomainTxtRecords": "TXT Records",
|
"createDomainTxtRecords": "TXT Records",
|
||||||
"createDomainSaveTheseRecords": "Save These Records",
|
"createDomainSaveTheseRecords": "Сохранить эти записи",
|
||||||
"createDomainSaveTheseRecordsDescription": "Make sure to save these DNS records as you will not see them again.",
|
"createDomainSaveTheseRecordsDescription": "Обязательно сохраните эти DNS записи, так как вы их больше не увидите.",
|
||||||
"createDomainDnsPropagation": "DNS Propagation",
|
"createDomainDnsPropagation": "Распространение DNS",
|
||||||
"createDomainDnsPropagationDescription": "DNS changes may take some time to propagate across the internet. This can take anywhere from a few minutes to 48 hours, depending on your DNS provider and TTL settings.",
|
"createDomainDnsPropagationDescription": "Изменения DNS могут занять некоторое время для распространения через интернет. Это может занять от нескольких минут до 48 часов в зависимости от вашего DNS провайдера и настроек TTL.",
|
||||||
"resourcePortRequired": "Port number is required for non-HTTP resources",
|
"resourcePortRequired": "Номер порта необходим для не-HTTP ресурсов",
|
||||||
"resourcePortNotAllowed": "Port number should not be set for HTTP resources",
|
"resourcePortNotAllowed": "Номер порта не должен быть установлен для HTTP ресурсов",
|
||||||
"signUpTerms": {
|
"signUpTerms": {
|
||||||
"IAgreeToThe": "I agree to the",
|
"IAgreeToThe": "Я согласен с",
|
||||||
"termsOfService": "terms of service",
|
"termsOfService": "условия использования",
|
||||||
"and": "and",
|
"and": "и",
|
||||||
"privacyPolicy": "privacy policy"
|
"privacyPolicy": "политика конфиденциальности"
|
||||||
},
|
},
|
||||||
"siteRequired": "Site is required.",
|
"siteRequired": "Необходимо указать сайт.",
|
||||||
"olmTunnel": "Olm Tunnel",
|
"olmTunnel": "Olm Tunnel",
|
||||||
"olmTunnelDescription": "Use Olm for client connectivity",
|
"olmTunnelDescription": "Use Olm for client connectivity",
|
||||||
"errorCreatingClient": "Error creating client",
|
"errorCreatingClient": "Error creating client",
|
||||||
@@ -1315,8 +1341,8 @@
|
|||||||
"olmErrorFetchLatest": "An error occurred while fetching the latest Olm release.",
|
"olmErrorFetchLatest": "An error occurred while fetching the latest Olm release.",
|
||||||
"remoteSubnets": "Remote Subnets",
|
"remoteSubnets": "Remote Subnets",
|
||||||
"enterCidrRange": "Enter CIDR range",
|
"enterCidrRange": "Enter CIDR range",
|
||||||
"remoteSubnetsDescription": "Add CIDR ranges that can access this site remotely. Use format like 10.0.0.0/24 or 192.168.1.0/24.",
|
"remoteSubnetsDescription": "Add CIDR ranges that can be accessed from this site remotely using clients. Use format like 10.0.0.0/24. This ONLY applies to VPN client connectivity.",
|
||||||
"resourceEnableProxy": "Enable Public Proxy",
|
"resourceEnableProxy": "Enable Public Proxy",
|
||||||
"resourceEnableProxyDescription": "Enable public proxying to this resource. This allows access to the resource from outside the network through the cloud on an open port. Requires Traefik config.",
|
"resourceEnableProxyDescription": "Enable public proxying to this resource. This allows access to the resource from outside the network through the cloud on an open port. Requires Traefik config.",
|
||||||
"externalProxyEnabled": "External Proxy Enabled"
|
"externalProxyEnabled": "External Proxy Enabled"
|
||||||
}
|
}
|
||||||
@@ -833,6 +833,24 @@
|
|||||||
"pincodeRequirementsLength": "PIN kesinlikle 6 haneli olmalıdır",
|
"pincodeRequirementsLength": "PIN kesinlikle 6 haneli olmalıdır",
|
||||||
"pincodeRequirementsChars": "PIN sadece numaralardan oluşmalıdır",
|
"pincodeRequirementsChars": "PIN sadece numaralardan oluşmalıdır",
|
||||||
"passwordRequirementsLength": "Şifre en az 1 karakter uzunluğunda olmalıdır",
|
"passwordRequirementsLength": "Şifre en az 1 karakter uzunluğunda olmalıdır",
|
||||||
|
"passwordRequirementsTitle": "Password requirements:",
|
||||||
|
"passwordRequirementLength": "At least 8 characters long",
|
||||||
|
"passwordRequirementUppercase": "At least one uppercase letter",
|
||||||
|
"passwordRequirementLowercase": "At least one lowercase letter",
|
||||||
|
"passwordRequirementNumber": "At least one number",
|
||||||
|
"passwordRequirementSpecial": "At least one special character",
|
||||||
|
"passwordRequirementsMet": "✓ Password meets all requirements",
|
||||||
|
"passwordStrength": "Password strength",
|
||||||
|
"passwordStrengthWeak": "Weak",
|
||||||
|
"passwordStrengthMedium": "Medium",
|
||||||
|
"passwordStrengthStrong": "Strong",
|
||||||
|
"passwordRequirements": "Requirements:",
|
||||||
|
"passwordRequirementLengthText": "8+ characters",
|
||||||
|
"passwordRequirementUppercaseText": "Uppercase letter (A-Z)",
|
||||||
|
"passwordRequirementLowercaseText": "Lowercase letter (a-z)",
|
||||||
|
"passwordRequirementNumberText": "Number (0-9)",
|
||||||
|
"passwordRequirementSpecialText": "Special character (!@#$%...)",
|
||||||
|
"passwordsDoNotMatch": "Passwords do not match",
|
||||||
"otpEmailRequirementsLength": "OTP en az 1 karakter uzunluğunda olmalıdır",
|
"otpEmailRequirementsLength": "OTP en az 1 karakter uzunluğunda olmalıdır",
|
||||||
"otpEmailSent": "OTP Gönderildi",
|
"otpEmailSent": "OTP Gönderildi",
|
||||||
"otpEmailSentDescription": "E-posta adresinize bir OTP gönderildi",
|
"otpEmailSentDescription": "E-posta adresinize bir OTP gönderildi",
|
||||||
@@ -967,6 +985,9 @@
|
|||||||
"actionDeleteSite": "Siteyi Sil",
|
"actionDeleteSite": "Siteyi Sil",
|
||||||
"actionGetSite": "Siteyi Al",
|
"actionGetSite": "Siteyi Al",
|
||||||
"actionListSites": "Siteleri Listele",
|
"actionListSites": "Siteleri Listele",
|
||||||
|
"setupToken": "Setup Token",
|
||||||
|
"setupTokenPlaceholder": "Enter the setup token from the server console",
|
||||||
|
"setupTokenRequired": "Setup token is required",
|
||||||
"actionUpdateSite": "Siteyi Güncelle",
|
"actionUpdateSite": "Siteyi Güncelle",
|
||||||
"actionListSiteRoles": "İzin Verilen Site Rolleri Listele",
|
"actionListSiteRoles": "İzin Verilen Site Rolleri Listele",
|
||||||
"actionCreateResource": "Kaynak Oluştur",
|
"actionCreateResource": "Kaynak Oluştur",
|
||||||
@@ -1022,6 +1043,11 @@
|
|||||||
"actionDeleteIdpOrg": "Kimlik Sağlayıcı Organizasyon Politikasını Sil",
|
"actionDeleteIdpOrg": "Kimlik Sağlayıcı Organizasyon Politikasını Sil",
|
||||||
"actionListIdpOrgs": "Kimlik Sağlayıcı Organizasyonları Listele",
|
"actionListIdpOrgs": "Kimlik Sağlayıcı Organizasyonları Listele",
|
||||||
"actionUpdateIdpOrg": "Kimlik Sağlayıcı Organizasyonu Güncelle",
|
"actionUpdateIdpOrg": "Kimlik Sağlayıcı Organizasyonu Güncelle",
|
||||||
|
"actionCreateClient": "Müşteri Oluştur",
|
||||||
|
"actionDeleteClient": "Müşteri Sil",
|
||||||
|
"actionUpdateClient": "Müşteri Güncelle",
|
||||||
|
"actionListClients": "Müşterileri Listele",
|
||||||
|
"actionGetClient": "Müşteriyi Al",
|
||||||
"noneSelected": "Hiçbiri seçili değil",
|
"noneSelected": "Hiçbiri seçili değil",
|
||||||
"orgNotFound2": "Hiçbir organizasyon bulunamadı.",
|
"orgNotFound2": "Hiçbir organizasyon bulunamadı.",
|
||||||
"searchProgress": "Ara...",
|
"searchProgress": "Ara...",
|
||||||
@@ -1315,8 +1341,8 @@
|
|||||||
"olmErrorFetchLatest": "En son Olm yayını alınırken bir hata oluştu.",
|
"olmErrorFetchLatest": "En son Olm yayını alınırken bir hata oluştu.",
|
||||||
"remoteSubnets": "Uzak Alt Ağlar",
|
"remoteSubnets": "Uzak Alt Ağlar",
|
||||||
"enterCidrRange": "CIDR aralığını girin",
|
"enterCidrRange": "CIDR aralığını girin",
|
||||||
"remoteSubnetsDescription": "Bu siteye uzaktan erişebilecek CIDR aralıklarını ekleyin. 10.0.0.0/24 veya 192.168.1.0/24 gibi formatlar kullanın.",
|
"remoteSubnetsDescription": "Add CIDR ranges that can be accessed from this site remotely using clients. Use format like 10.0.0.0/24. This ONLY applies to VPN client connectivity.",
|
||||||
"resourceEnableProxy": "Genel Proxy'i Etkinleştir",
|
"resourceEnableProxy": "Genel Proxy'i Etkinleştir",
|
||||||
"resourceEnableProxyDescription": "Bu kaynağa genel proxy erişimini etkinleştirin. Bu sayede ağ dışından açık bir port üzerinden kaynağa bulut aracılığıyla erişim sağlanır. Traefik yapılandırması gereklidir.",
|
"resourceEnableProxyDescription": "Bu kaynağa genel proxy erişimini etkinleştirin. Bu sayede ağ dışından açık bir port üzerinden kaynağa bulut aracılığıyla erişim sağlanır. Traefik yapılandırması gereklidir.",
|
||||||
"externalProxyEnabled": "Dış Proxy Etkinleştirildi"
|
"externalProxyEnabled": "Dış Proxy Etkinleştirildi"
|
||||||
}
|
}
|
||||||
@@ -833,6 +833,24 @@
|
|||||||
"pincodeRequirementsLength": "PIN码必须是6位数字",
|
"pincodeRequirementsLength": "PIN码必须是6位数字",
|
||||||
"pincodeRequirementsChars": "PIN 必须只包含数字",
|
"pincodeRequirementsChars": "PIN 必须只包含数字",
|
||||||
"passwordRequirementsLength": "密码必须至少 1 个字符长",
|
"passwordRequirementsLength": "密码必须至少 1 个字符长",
|
||||||
|
"passwordRequirementsTitle": "Password requirements:",
|
||||||
|
"passwordRequirementLength": "At least 8 characters long",
|
||||||
|
"passwordRequirementUppercase": "At least one uppercase letter",
|
||||||
|
"passwordRequirementLowercase": "At least one lowercase letter",
|
||||||
|
"passwordRequirementNumber": "At least one number",
|
||||||
|
"passwordRequirementSpecial": "At least one special character",
|
||||||
|
"passwordRequirementsMet": "✓ Password meets all requirements",
|
||||||
|
"passwordStrength": "Password strength",
|
||||||
|
"passwordStrengthWeak": "Weak",
|
||||||
|
"passwordStrengthMedium": "Medium",
|
||||||
|
"passwordStrengthStrong": "Strong",
|
||||||
|
"passwordRequirements": "Requirements:",
|
||||||
|
"passwordRequirementLengthText": "8+ characters",
|
||||||
|
"passwordRequirementUppercaseText": "Uppercase letter (A-Z)",
|
||||||
|
"passwordRequirementLowercaseText": "Lowercase letter (a-z)",
|
||||||
|
"passwordRequirementNumberText": "Number (0-9)",
|
||||||
|
"passwordRequirementSpecialText": "Special character (!@#$%...)",
|
||||||
|
"passwordsDoNotMatch": "Passwords do not match",
|
||||||
"otpEmailRequirementsLength": "OTP 必须至少 1 个字符长",
|
"otpEmailRequirementsLength": "OTP 必须至少 1 个字符长",
|
||||||
"otpEmailSent": "OTP 已发送",
|
"otpEmailSent": "OTP 已发送",
|
||||||
"otpEmailSentDescription": "OTP 已经发送到您的电子邮件",
|
"otpEmailSentDescription": "OTP 已经发送到您的电子邮件",
|
||||||
@@ -967,6 +985,9 @@
|
|||||||
"actionDeleteSite": "删除站点",
|
"actionDeleteSite": "删除站点",
|
||||||
"actionGetSite": "获取站点",
|
"actionGetSite": "获取站点",
|
||||||
"actionListSites": "站点列表",
|
"actionListSites": "站点列表",
|
||||||
|
"setupToken": "Setup Token",
|
||||||
|
"setupTokenPlaceholder": "Enter the setup token from the server console",
|
||||||
|
"setupTokenRequired": "Setup token is required",
|
||||||
"actionUpdateSite": "更新站点",
|
"actionUpdateSite": "更新站点",
|
||||||
"actionListSiteRoles": "允许站点角色列表",
|
"actionListSiteRoles": "允许站点角色列表",
|
||||||
"actionCreateResource": "创建资源",
|
"actionCreateResource": "创建资源",
|
||||||
@@ -1022,6 +1043,11 @@
|
|||||||
"actionDeleteIdpOrg": "删除 IDP组织策略",
|
"actionDeleteIdpOrg": "删除 IDP组织策略",
|
||||||
"actionListIdpOrgs": "列出 IDP组织",
|
"actionListIdpOrgs": "列出 IDP组织",
|
||||||
"actionUpdateIdpOrg": "更新 IDP组织",
|
"actionUpdateIdpOrg": "更新 IDP组织",
|
||||||
|
"actionCreateClient": "创建客户端",
|
||||||
|
"actionDeleteClient": "删除客户端",
|
||||||
|
"actionUpdateClient": "更新客户端",
|
||||||
|
"actionListClients": "列出客户端",
|
||||||
|
"actionGetClient": "获取客户端",
|
||||||
"noneSelected": "未选择",
|
"noneSelected": "未选择",
|
||||||
"orgNotFound2": "未找到组织。",
|
"orgNotFound2": "未找到组织。",
|
||||||
"searchProgress": "搜索中...",
|
"searchProgress": "搜索中...",
|
||||||
@@ -1315,8 +1341,8 @@
|
|||||||
"olmErrorFetchLatest": "获取最新 Olm 发布版本时出错。",
|
"olmErrorFetchLatest": "获取最新 Olm 发布版本时出错。",
|
||||||
"remoteSubnets": "远程子网",
|
"remoteSubnets": "远程子网",
|
||||||
"enterCidrRange": "输入 CIDR 范围",
|
"enterCidrRange": "输入 CIDR 范围",
|
||||||
"remoteSubnetsDescription": "添加能远程访问此站点的 CIDR 范围。使用格式如 10.0.0.0/24 或 192.168.1.0/24。",
|
"remoteSubnetsDescription": "Add CIDR ranges that can be accessed from this site remotely using clients. Use format like 10.0.0.0/24. This ONLY applies to VPN client connectivity.",
|
||||||
"resourceEnableProxy": "启用公共代理",
|
"resourceEnableProxy": "启用公共代理",
|
||||||
"resourceEnableProxyDescription": "启用到此资源的公共代理。这允许外部网络通过开放端口访问资源。需要 Traefik 配置。",
|
"resourceEnableProxyDescription": "启用到此资源的公共代理。这允许外部网络通过开放端口访问资源。需要 Traefik 配置。",
|
||||||
"externalProxyEnabled": "外部代理已启用"
|
"externalProxyEnabled": "外部代理已启用"
|
||||||
}
|
}
|
||||||
1687
package-lock.json
generated
1687
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
53
package.json
53
package.json
@@ -50,15 +50,15 @@
|
|||||||
"@radix-ui/react-tabs": "1.1.12",
|
"@radix-ui/react-tabs": "1.1.12",
|
||||||
"@radix-ui/react-toast": "1.2.14",
|
"@radix-ui/react-toast": "1.2.14",
|
||||||
"@radix-ui/react-tooltip": "^1.2.7",
|
"@radix-ui/react-tooltip": "^1.2.7",
|
||||||
"@react-email/components": "0.3.1",
|
"@react-email/components": "0.5.0",
|
||||||
"@react-email/render": "^1.1.2",
|
"@react-email/render": "^1.2.0",
|
||||||
|
"@react-email/tailwind": "1.2.2",
|
||||||
"@simplewebauthn/browser": "^13.1.0",
|
"@simplewebauthn/browser": "^13.1.0",
|
||||||
"@simplewebauthn/server": "^9.0.3",
|
"@simplewebauthn/server": "^9.0.3",
|
||||||
"@react-email/tailwind": "1.2.1",
|
|
||||||
"@tailwindcss/forms": "^0.5.10",
|
"@tailwindcss/forms": "^0.5.10",
|
||||||
"@tanstack/react-table": "8.21.3",
|
"@tanstack/react-table": "8.21.3",
|
||||||
"arctic": "^3.7.0",
|
"arctic": "^3.7.0",
|
||||||
"axios": "1.10.0",
|
"axios": "1.11.0",
|
||||||
"better-sqlite3": "11.7.0",
|
"better-sqlite3": "11.7.0",
|
||||||
"canvas-confetti": "1.9.3",
|
"canvas-confetti": "1.9.3",
|
||||||
"class-variance-authority": "^0.7.1",
|
"class-variance-authority": "^0.7.1",
|
||||||
@@ -69,10 +69,10 @@
|
|||||||
"cookies": "^0.9.1",
|
"cookies": "^0.9.1",
|
||||||
"cors": "2.8.5",
|
"cors": "2.8.5",
|
||||||
"crypto-js": "^4.2.0",
|
"crypto-js": "^4.2.0",
|
||||||
"drizzle-orm": "0.44.2",
|
"drizzle-orm": "0.44.4",
|
||||||
"eslint": "9.31.0",
|
"eslint": "9.33.0",
|
||||||
"eslint-config-next": "15.3.5",
|
"eslint-config-next": "15.4.6",
|
||||||
"express": "4.21.2",
|
"express": "5.1.0",
|
||||||
"express-rate-limit": "7.5.1",
|
"express-rate-limit": "7.5.1",
|
||||||
"glob": "11.0.3",
|
"glob": "11.0.3",
|
||||||
"helmet": "8.1.0",
|
"helmet": "8.1.0",
|
||||||
@@ -82,69 +82,70 @@
|
|||||||
"jmespath": "^0.16.0",
|
"jmespath": "^0.16.0",
|
||||||
"js-yaml": "4.1.0",
|
"js-yaml": "4.1.0",
|
||||||
"jsonwebtoken": "^9.0.2",
|
"jsonwebtoken": "^9.0.2",
|
||||||
"lucide-react": "0.525.0",
|
"lucide-react": "0.539.0",
|
||||||
"moment": "2.30.1",
|
"moment": "2.30.1",
|
||||||
"next": "15.3.5",
|
"next": "15.4.6",
|
||||||
"next-intl": "^4.3.4",
|
"next-intl": "^4.3.4",
|
||||||
"next-themes": "0.4.6",
|
"next-themes": "0.4.6",
|
||||||
"node-cache": "5.1.2",
|
"node-cache": "5.1.2",
|
||||||
"node-fetch": "3.3.2",
|
"node-fetch": "3.3.2",
|
||||||
"nodemailer": "7.0.5",
|
"nodemailer": "7.0.5",
|
||||||
"npm": "^11.4.2",
|
"npm": "^11.5.2",
|
||||||
"oslo": "1.2.1",
|
"oslo": "1.2.1",
|
||||||
"pg": "^8.16.2",
|
"pg": "^8.16.2",
|
||||||
|
"posthog-node": "^5.7.0",
|
||||||
"qrcode.react": "4.2.0",
|
"qrcode.react": "4.2.0",
|
||||||
"react": "19.1.0",
|
"react": "19.1.1",
|
||||||
"react-dom": "19.1.0",
|
"react-dom": "19.1.1",
|
||||||
"react-easy-sort": "^1.6.0",
|
"react-easy-sort": "^1.6.0",
|
||||||
"react-hook-form": "7.60.0",
|
"react-hook-form": "7.62.0",
|
||||||
"react-icons": "^5.5.0",
|
"react-icons": "^5.5.0",
|
||||||
"rebuild": "0.1.2",
|
"rebuild": "0.1.2",
|
||||||
"semver": "^7.7.2",
|
"semver": "^7.7.2",
|
||||||
"swagger-ui-express": "^5.0.1",
|
"swagger-ui-express": "^5.0.1",
|
||||||
"tailwind-merge": "3.3.1",
|
"tailwind-merge": "3.3.1",
|
||||||
"tw-animate-css": "^1.3.5",
|
"tw-animate-css": "^1.3.6",
|
||||||
"uuid": "^11.1.0",
|
"uuid": "^11.1.0",
|
||||||
"vaul": "1.1.2",
|
"vaul": "1.1.2",
|
||||||
"winston": "3.17.0",
|
"winston": "3.17.0",
|
||||||
"winston-daily-rotate-file": "5.0.0",
|
"winston-daily-rotate-file": "5.0.0",
|
||||||
"ws": "8.18.3",
|
"ws": "8.18.3",
|
||||||
|
"yargs": "18.0.0",
|
||||||
"zod": "3.25.76",
|
"zod": "3.25.76",
|
||||||
"zod-validation-error": "3.5.2",
|
"zod-validation-error": "3.5.2"
|
||||||
"yargs": "18.0.0"
|
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@dotenvx/dotenvx": "1.47.6",
|
"@dotenvx/dotenvx": "1.48.4",
|
||||||
"@esbuild-plugins/tsconfig-paths": "0.1.2",
|
"@esbuild-plugins/tsconfig-paths": "0.1.2",
|
||||||
"@tailwindcss/postcss": "^4.1.10",
|
"@tailwindcss/postcss": "^4.1.10",
|
||||||
"@types/better-sqlite3": "7.6.12",
|
"@types/better-sqlite3": "7.6.12",
|
||||||
"@types/cookie-parser": "1.4.9",
|
"@types/cookie-parser": "1.4.9",
|
||||||
"@types/cors": "2.8.19",
|
"@types/cors": "2.8.19",
|
||||||
"@types/crypto-js": "^4.2.2",
|
"@types/crypto-js": "^4.2.2",
|
||||||
"@types/express": "5.0.0",
|
"@types/express": "5.0.3",
|
||||||
"@types/express-session": "^1.18.2",
|
"@types/express-session": "^1.18.2",
|
||||||
"@types/jmespath": "^0.15.2",
|
"@types/jmespath": "^0.15.2",
|
||||||
"@types/js-yaml": "4.0.9",
|
"@types/js-yaml": "4.0.9",
|
||||||
"@types/jsonwebtoken": "^9.0.10",
|
"@types/jsonwebtoken": "^9.0.10",
|
||||||
"@types/node": "^24",
|
"@types/node": "^24",
|
||||||
"@types/nodemailer": "6.4.17",
|
"@types/nodemailer": "6.4.17",
|
||||||
"@types/pg": "8.15.4",
|
"@types/pg": "8.15.5",
|
||||||
"@types/react": "19.1.8",
|
"@types/react": "19.1.10",
|
||||||
"@types/react-dom": "19.1.6",
|
"@types/react-dom": "19.1.7",
|
||||||
"@types/semver": "^7.7.0",
|
"@types/semver": "^7.7.0",
|
||||||
"@types/swagger-ui-express": "^4.1.8",
|
"@types/swagger-ui-express": "^4.1.8",
|
||||||
"@types/ws": "8.18.1",
|
"@types/ws": "8.18.1",
|
||||||
"@types/yargs": "17.0.33",
|
"@types/yargs": "17.0.33",
|
||||||
"drizzle-kit": "0.31.4",
|
"drizzle-kit": "0.31.4",
|
||||||
"esbuild": "0.25.6",
|
"esbuild": "0.25.9",
|
||||||
"esbuild-node-externals": "1.18.0",
|
"esbuild-node-externals": "1.18.0",
|
||||||
"postcss": "^8",
|
"postcss": "^8",
|
||||||
"react-email": "4.1.0",
|
"react-email": "4.2.8",
|
||||||
"tailwindcss": "^4.1.4",
|
"tailwindcss": "^4.1.4",
|
||||||
"tsc-alias": "1.8.16",
|
"tsc-alias": "1.8.16",
|
||||||
"tsx": "4.20.3",
|
"tsx": "4.20.4",
|
||||||
"typescript": "^5",
|
"typescript": "^5",
|
||||||
"typescript-eslint": "^8.36.0"
|
"typescript-eslint": "^8.39.1"
|
||||||
},
|
},
|
||||||
"overrides": {
|
"overrides": {
|
||||||
"emblor": {
|
"emblor": {
|
||||||
|
|||||||
@@ -69,6 +69,11 @@ export enum ActionsEnum {
|
|||||||
deleteResourceRule = "deleteResourceRule",
|
deleteResourceRule = "deleteResourceRule",
|
||||||
listResourceRules = "listResourceRules",
|
listResourceRules = "listResourceRules",
|
||||||
updateResourceRule = "updateResourceRule",
|
updateResourceRule = "updateResourceRule",
|
||||||
|
createSiteResource = "createSiteResource",
|
||||||
|
deleteSiteResource = "deleteSiteResource",
|
||||||
|
getSiteResource = "getSiteResource",
|
||||||
|
listSiteResources = "listSiteResources",
|
||||||
|
updateSiteResource = "updateSiteResource",
|
||||||
createClient = "createClient",
|
createClient = "createClient",
|
||||||
deleteClient = "deleteClient",
|
deleteClient = "deleteClient",
|
||||||
updateClient = "updateClient",
|
updateClient = "updateClient",
|
||||||
|
|||||||
@@ -23,7 +23,8 @@ export const domains = pgTable("domains", {
|
|||||||
export const orgs = pgTable("orgs", {
|
export const orgs = pgTable("orgs", {
|
||||||
orgId: varchar("orgId").primaryKey(),
|
orgId: varchar("orgId").primaryKey(),
|
||||||
name: varchar("name").notNull(),
|
name: varchar("name").notNull(),
|
||||||
subnet: varchar("subnet")
|
subnet: varchar("subnet"),
|
||||||
|
createdAt: text("createdAt")
|
||||||
});
|
});
|
||||||
|
|
||||||
export const orgDomains = pgTable("orgDomains", {
|
export const orgDomains = pgTable("orgDomains", {
|
||||||
@@ -65,11 +66,6 @@ export const sites = pgTable("sites", {
|
|||||||
|
|
||||||
export const resources = pgTable("resources", {
|
export const resources = pgTable("resources", {
|
||||||
resourceId: serial("resourceId").primaryKey(),
|
resourceId: serial("resourceId").primaryKey(),
|
||||||
siteId: integer("siteId")
|
|
||||||
.references(() => sites.siteId, {
|
|
||||||
onDelete: "cascade"
|
|
||||||
})
|
|
||||||
.notNull(),
|
|
||||||
orgId: varchar("orgId")
|
orgId: varchar("orgId")
|
||||||
.references(() => orgs.orgId, {
|
.references(() => orgs.orgId, {
|
||||||
onDelete: "cascade"
|
onDelete: "cascade"
|
||||||
@@ -96,6 +92,9 @@ export const resources = pgTable("resources", {
|
|||||||
tlsServerName: varchar("tlsServerName"),
|
tlsServerName: varchar("tlsServerName"),
|
||||||
setHostHeader: varchar("setHostHeader"),
|
setHostHeader: varchar("setHostHeader"),
|
||||||
enableProxy: boolean("enableProxy").default(true),
|
enableProxy: boolean("enableProxy").default(true),
|
||||||
|
skipToIdpId: integer("skipToIdpId").references(() => idp.idpId, {
|
||||||
|
onDelete: "cascade"
|
||||||
|
}),
|
||||||
});
|
});
|
||||||
|
|
||||||
export const targets = pgTable("targets", {
|
export const targets = pgTable("targets", {
|
||||||
@@ -105,6 +104,11 @@ export const targets = pgTable("targets", {
|
|||||||
onDelete: "cascade"
|
onDelete: "cascade"
|
||||||
})
|
})
|
||||||
.notNull(),
|
.notNull(),
|
||||||
|
siteId: integer("siteId")
|
||||||
|
.references(() => sites.siteId, {
|
||||||
|
onDelete: "cascade"
|
||||||
|
})
|
||||||
|
.notNull(),
|
||||||
ip: varchar("ip").notNull(),
|
ip: varchar("ip").notNull(),
|
||||||
method: varchar("method"),
|
method: varchar("method"),
|
||||||
port: integer("port").notNull(),
|
port: integer("port").notNull(),
|
||||||
@@ -123,6 +127,22 @@ export const exitNodes = pgTable("exitNodes", {
|
|||||||
maxConnections: integer("maxConnections")
|
maxConnections: integer("maxConnections")
|
||||||
});
|
});
|
||||||
|
|
||||||
|
export const siteResources = pgTable("siteResources", { // this is for the clients
|
||||||
|
siteResourceId: serial("siteResourceId").primaryKey(),
|
||||||
|
siteId: integer("siteId")
|
||||||
|
.notNull()
|
||||||
|
.references(() => sites.siteId, { onDelete: "cascade" }),
|
||||||
|
orgId: varchar("orgId")
|
||||||
|
.notNull()
|
||||||
|
.references(() => orgs.orgId, { onDelete: "cascade" }),
|
||||||
|
name: varchar("name").notNull(),
|
||||||
|
protocol: varchar("protocol").notNull(),
|
||||||
|
proxyPort: integer("proxyPort").notNull(),
|
||||||
|
destinationPort: integer("destinationPort").notNull(),
|
||||||
|
destinationIp: varchar("destinationIp").notNull(),
|
||||||
|
enabled: boolean("enabled").notNull().default(true),
|
||||||
|
});
|
||||||
|
|
||||||
export const users = pgTable("user", {
|
export const users = pgTable("user", {
|
||||||
userId: varchar("id").primaryKey(),
|
userId: varchar("id").primaryKey(),
|
||||||
email: varchar("email"),
|
email: varchar("email"),
|
||||||
@@ -512,10 +532,10 @@ export const clients = pgTable("clients", {
|
|||||||
megabytesIn: real("bytesIn"),
|
megabytesIn: real("bytesIn"),
|
||||||
megabytesOut: real("bytesOut"),
|
megabytesOut: real("bytesOut"),
|
||||||
lastBandwidthUpdate: varchar("lastBandwidthUpdate"),
|
lastBandwidthUpdate: varchar("lastBandwidthUpdate"),
|
||||||
lastPing: varchar("lastPing"),
|
lastPing: integer("lastPing"),
|
||||||
type: varchar("type").notNull(), // "olm"
|
type: varchar("type").notNull(), // "olm"
|
||||||
online: boolean("online").notNull().default(false),
|
online: boolean("online").notNull().default(false),
|
||||||
endpoint: varchar("endpoint"),
|
// endpoint: varchar("endpoint"),
|
||||||
lastHolePunch: integer("lastHolePunch"),
|
lastHolePunch: integer("lastHolePunch"),
|
||||||
maxConnections: integer("maxConnections")
|
maxConnections: integer("maxConnections")
|
||||||
});
|
});
|
||||||
@@ -527,13 +547,15 @@ export const clientSites = pgTable("clientSites", {
|
|||||||
siteId: integer("siteId")
|
siteId: integer("siteId")
|
||||||
.notNull()
|
.notNull()
|
||||||
.references(() => sites.siteId, { onDelete: "cascade" }),
|
.references(() => sites.siteId, { onDelete: "cascade" }),
|
||||||
isRelayed: boolean("isRelayed").notNull().default(false)
|
isRelayed: boolean("isRelayed").notNull().default(false),
|
||||||
|
endpoint: varchar("endpoint")
|
||||||
});
|
});
|
||||||
|
|
||||||
export const olms = pgTable("olms", {
|
export const olms = pgTable("olms", {
|
||||||
olmId: varchar("id").primaryKey(),
|
olmId: varchar("id").primaryKey(),
|
||||||
secretHash: varchar("secretHash").notNull(),
|
secretHash: varchar("secretHash").notNull(),
|
||||||
dateCreated: varchar("dateCreated").notNull(),
|
dateCreated: varchar("dateCreated").notNull(),
|
||||||
|
version: text("version"),
|
||||||
clientId: integer("clientId").references(() => clients.clientId, {
|
clientId: integer("clientId").references(() => clients.clientId, {
|
||||||
onDelete: "cascade"
|
onDelete: "cascade"
|
||||||
})
|
})
|
||||||
@@ -591,6 +613,14 @@ export const webauthnChallenge = pgTable("webauthnChallenge", {
|
|||||||
expiresAt: bigint("expiresAt", { mode: "number" }).notNull() // Unix timestamp
|
expiresAt: bigint("expiresAt", { mode: "number" }).notNull() // Unix timestamp
|
||||||
});
|
});
|
||||||
|
|
||||||
|
export const setupTokens = pgTable("setupTokens", {
|
||||||
|
tokenId: varchar("tokenId").primaryKey(),
|
||||||
|
token: varchar("token").notNull(),
|
||||||
|
used: boolean("used").notNull().default(false),
|
||||||
|
dateCreated: varchar("dateCreated").notNull(),
|
||||||
|
dateUsed: varchar("dateUsed")
|
||||||
|
});
|
||||||
|
|
||||||
export type Org = InferSelectModel<typeof orgs>;
|
export type Org = InferSelectModel<typeof orgs>;
|
||||||
export type User = InferSelectModel<typeof users>;
|
export type User = InferSelectModel<typeof users>;
|
||||||
export type Site = InferSelectModel<typeof sites>;
|
export type Site = InferSelectModel<typeof sites>;
|
||||||
@@ -636,3 +666,5 @@ export type OlmSession = InferSelectModel<typeof olmSessions>;
|
|||||||
export type UserClient = InferSelectModel<typeof userClients>;
|
export type UserClient = InferSelectModel<typeof userClients>;
|
||||||
export type RoleClient = InferSelectModel<typeof roleClients>;
|
export type RoleClient = InferSelectModel<typeof roleClients>;
|
||||||
export type OrgDomains = InferSelectModel<typeof orgDomains>;
|
export type OrgDomains = InferSelectModel<typeof orgDomains>;
|
||||||
|
export type SiteResource = InferSelectModel<typeof siteResources>;
|
||||||
|
export type SetupToken = InferSelectModel<typeof setupTokens>;
|
||||||
|
|||||||
@@ -16,7 +16,8 @@ export const domains = sqliteTable("domains", {
|
|||||||
export const orgs = sqliteTable("orgs", {
|
export const orgs = sqliteTable("orgs", {
|
||||||
orgId: text("orgId").primaryKey(),
|
orgId: text("orgId").primaryKey(),
|
||||||
name: text("name").notNull(),
|
name: text("name").notNull(),
|
||||||
subnet: text("subnet")
|
subnet: text("subnet"),
|
||||||
|
createdAt: text("createdAt")
|
||||||
});
|
});
|
||||||
|
|
||||||
export const userDomains = sqliteTable("userDomains", {
|
export const userDomains = sqliteTable("userDomains", {
|
||||||
@@ -66,16 +67,11 @@ export const sites = sqliteTable("sites", {
|
|||||||
dockerSocketEnabled: integer("dockerSocketEnabled", { mode: "boolean" })
|
dockerSocketEnabled: integer("dockerSocketEnabled", { mode: "boolean" })
|
||||||
.notNull()
|
.notNull()
|
||||||
.default(true),
|
.default(true),
|
||||||
remoteSubnets: text("remoteSubnets"), // comma-separated list of subnets that this site can access
|
remoteSubnets: text("remoteSubnets") // comma-separated list of subnets that this site can access
|
||||||
});
|
});
|
||||||
|
|
||||||
export const resources = sqliteTable("resources", {
|
export const resources = sqliteTable("resources", {
|
||||||
resourceId: integer("resourceId").primaryKey({ autoIncrement: true }),
|
resourceId: integer("resourceId").primaryKey({ autoIncrement: true }),
|
||||||
siteId: integer("siteId")
|
|
||||||
.references(() => sites.siteId, {
|
|
||||||
onDelete: "cascade"
|
|
||||||
})
|
|
||||||
.notNull(),
|
|
||||||
orgId: text("orgId")
|
orgId: text("orgId")
|
||||||
.references(() => orgs.orgId, {
|
.references(() => orgs.orgId, {
|
||||||
onDelete: "cascade"
|
onDelete: "cascade"
|
||||||
@@ -108,6 +104,9 @@ export const resources = sqliteTable("resources", {
|
|||||||
tlsServerName: text("tlsServerName"),
|
tlsServerName: text("tlsServerName"),
|
||||||
setHostHeader: text("setHostHeader"),
|
setHostHeader: text("setHostHeader"),
|
||||||
enableProxy: integer("enableProxy", { mode: "boolean" }).default(true),
|
enableProxy: integer("enableProxy", { mode: "boolean" }).default(true),
|
||||||
|
skipToIdpId: integer("skipToIdpId").references(() => idp.idpId, {
|
||||||
|
onDelete: "cascade"
|
||||||
|
}),
|
||||||
});
|
});
|
||||||
|
|
||||||
export const targets = sqliteTable("targets", {
|
export const targets = sqliteTable("targets", {
|
||||||
@@ -117,6 +116,11 @@ export const targets = sqliteTable("targets", {
|
|||||||
onDelete: "cascade"
|
onDelete: "cascade"
|
||||||
})
|
})
|
||||||
.notNull(),
|
.notNull(),
|
||||||
|
siteId: integer("siteId")
|
||||||
|
.references(() => sites.siteId, {
|
||||||
|
onDelete: "cascade"
|
||||||
|
})
|
||||||
|
.notNull(),
|
||||||
ip: text("ip").notNull(),
|
ip: text("ip").notNull(),
|
||||||
method: text("method"),
|
method: text("method"),
|
||||||
port: integer("port").notNull(),
|
port: integer("port").notNull(),
|
||||||
@@ -135,6 +139,22 @@ export const exitNodes = sqliteTable("exitNodes", {
|
|||||||
maxConnections: integer("maxConnections")
|
maxConnections: integer("maxConnections")
|
||||||
});
|
});
|
||||||
|
|
||||||
|
export const siteResources = sqliteTable("siteResources", { // this is for the clients
|
||||||
|
siteResourceId: integer("siteResourceId").primaryKey({ autoIncrement: true }),
|
||||||
|
siteId: integer("siteId")
|
||||||
|
.notNull()
|
||||||
|
.references(() => sites.siteId, { onDelete: "cascade" }),
|
||||||
|
orgId: text("orgId")
|
||||||
|
.notNull()
|
||||||
|
.references(() => orgs.orgId, { onDelete: "cascade" }),
|
||||||
|
name: text("name").notNull(),
|
||||||
|
protocol: text("protocol").notNull(),
|
||||||
|
proxyPort: integer("proxyPort").notNull(),
|
||||||
|
destinationPort: integer("destinationPort").notNull(),
|
||||||
|
destinationIp: text("destinationIp").notNull(),
|
||||||
|
enabled: integer("enabled", { mode: "boolean" }).notNull().default(true),
|
||||||
|
});
|
||||||
|
|
||||||
export const users = sqliteTable("user", {
|
export const users = sqliteTable("user", {
|
||||||
userId: text("id").primaryKey(),
|
userId: text("id").primaryKey(),
|
||||||
email: text("email"),
|
email: text("email"),
|
||||||
@@ -165,9 +185,11 @@ export const users = sqliteTable("user", {
|
|||||||
|
|
||||||
export const securityKeys = sqliteTable("webauthnCredentials", {
|
export const securityKeys = sqliteTable("webauthnCredentials", {
|
||||||
credentialId: text("credentialId").primaryKey(),
|
credentialId: text("credentialId").primaryKey(),
|
||||||
userId: text("userId").notNull().references(() => users.userId, {
|
userId: text("userId")
|
||||||
onDelete: "cascade"
|
.notNull()
|
||||||
}),
|
.references(() => users.userId, {
|
||||||
|
onDelete: "cascade"
|
||||||
|
}),
|
||||||
publicKey: text("publicKey").notNull(),
|
publicKey: text("publicKey").notNull(),
|
||||||
signCount: integer("signCount").notNull(),
|
signCount: integer("signCount").notNull(),
|
||||||
transports: text("transports"),
|
transports: text("transports"),
|
||||||
@@ -186,6 +208,14 @@ export const webauthnChallenge = sqliteTable("webauthnChallenge", {
|
|||||||
expiresAt: integer("expiresAt").notNull() // Unix timestamp
|
expiresAt: integer("expiresAt").notNull() // Unix timestamp
|
||||||
});
|
});
|
||||||
|
|
||||||
|
export const setupTokens = sqliteTable("setupTokens", {
|
||||||
|
tokenId: text("tokenId").primaryKey(),
|
||||||
|
token: text("token").notNull(),
|
||||||
|
used: integer("used", { mode: "boolean" }).notNull().default(false),
|
||||||
|
dateCreated: text("dateCreated").notNull(),
|
||||||
|
dateUsed: text("dateUsed")
|
||||||
|
});
|
||||||
|
|
||||||
export const newts = sqliteTable("newt", {
|
export const newts = sqliteTable("newt", {
|
||||||
newtId: text("id").primaryKey(),
|
newtId: text("id").primaryKey(),
|
||||||
secretHash: text("secretHash").notNull(),
|
secretHash: text("secretHash").notNull(),
|
||||||
@@ -212,10 +242,10 @@ export const clients = sqliteTable("clients", {
|
|||||||
megabytesIn: integer("bytesIn"),
|
megabytesIn: integer("bytesIn"),
|
||||||
megabytesOut: integer("bytesOut"),
|
megabytesOut: integer("bytesOut"),
|
||||||
lastBandwidthUpdate: text("lastBandwidthUpdate"),
|
lastBandwidthUpdate: text("lastBandwidthUpdate"),
|
||||||
lastPing: text("lastPing"),
|
lastPing: integer("lastPing"),
|
||||||
type: text("type").notNull(), // "olm"
|
type: text("type").notNull(), // "olm"
|
||||||
online: integer("online", { mode: "boolean" }).notNull().default(false),
|
online: integer("online", { mode: "boolean" }).notNull().default(false),
|
||||||
endpoint: text("endpoint"),
|
// endpoint: text("endpoint"),
|
||||||
lastHolePunch: integer("lastHolePunch")
|
lastHolePunch: integer("lastHolePunch")
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -226,13 +256,15 @@ export const clientSites = sqliteTable("clientSites", {
|
|||||||
siteId: integer("siteId")
|
siteId: integer("siteId")
|
||||||
.notNull()
|
.notNull()
|
||||||
.references(() => sites.siteId, { onDelete: "cascade" }),
|
.references(() => sites.siteId, { onDelete: "cascade" }),
|
||||||
isRelayed: integer("isRelayed", { mode: "boolean" }).notNull().default(false)
|
isRelayed: integer("isRelayed", { mode: "boolean" }).notNull().default(false),
|
||||||
|
endpoint: text("endpoint")
|
||||||
});
|
});
|
||||||
|
|
||||||
export const olms = sqliteTable("olms", {
|
export const olms = sqliteTable("olms", {
|
||||||
olmId: text("id").primaryKey(),
|
olmId: text("id").primaryKey(),
|
||||||
secretHash: text("secretHash").notNull(),
|
secretHash: text("secretHash").notNull(),
|
||||||
dateCreated: text("dateCreated").notNull(),
|
dateCreated: text("dateCreated").notNull(),
|
||||||
|
version: text("version"),
|
||||||
clientId: integer("clientId").references(() => clients.clientId, {
|
clientId: integer("clientId").references(() => clients.clientId, {
|
||||||
onDelete: "cascade"
|
onDelete: "cascade"
|
||||||
})
|
})
|
||||||
@@ -677,4 +709,7 @@ export type Idp = InferSelectModel<typeof idp>;
|
|||||||
export type ApiKey = InferSelectModel<typeof apiKeys>;
|
export type ApiKey = InferSelectModel<typeof apiKeys>;
|
||||||
export type ApiKeyAction = InferSelectModel<typeof apiKeyActions>;
|
export type ApiKeyAction = InferSelectModel<typeof apiKeyActions>;
|
||||||
export type ApiKeyOrg = InferSelectModel<typeof apiKeyOrg>;
|
export type ApiKeyOrg = InferSelectModel<typeof apiKeyOrg>;
|
||||||
|
export type SiteResource = InferSelectModel<typeof siteResources>;
|
||||||
export type OrgDomains = InferSelectModel<typeof orgDomains>;
|
export type OrgDomains = InferSelectModel<typeof orgDomains>;
|
||||||
|
export type SetupToken = InferSelectModel<typeof setupTokens>;
|
||||||
|
export type HostMeta = InferSelectModel<typeof hostMeta>;
|
||||||
|
|||||||
@@ -88,7 +88,7 @@ export const WelcomeQuickStart = ({
|
|||||||
To learn how to use Newt, including more
|
To learn how to use Newt, including more
|
||||||
installation methods, visit the{" "}
|
installation methods, visit the{" "}
|
||||||
<a
|
<a
|
||||||
href="https://docs.fossorial.io"
|
href="https://docs.digpangolin.com/manage/sites/install-site"
|
||||||
className="underline"
|
className="underline"
|
||||||
>
|
>
|
||||||
docs
|
docs
|
||||||
|
|||||||
@@ -8,11 +8,17 @@ import { createInternalServer } from "./internalServer";
|
|||||||
import { ApiKey, ApiKeyOrg, Session, User, UserOrg } from "@server/db";
|
import { ApiKey, ApiKeyOrg, Session, User, UserOrg } from "@server/db";
|
||||||
import { createIntegrationApiServer } from "./integrationApiServer";
|
import { createIntegrationApiServer } from "./integrationApiServer";
|
||||||
import config from "@server/lib/config";
|
import config from "@server/lib/config";
|
||||||
|
import { setHostMeta } from "@server/lib/hostMeta";
|
||||||
|
import { initTelemetryClient } from "./lib/telemetry.js";
|
||||||
|
|
||||||
async function startServers() {
|
async function startServers() {
|
||||||
|
await setHostMeta();
|
||||||
|
|
||||||
await config.initServer();
|
await config.initServer();
|
||||||
await runSetupFunctions();
|
await runSetupFunctions();
|
||||||
|
|
||||||
|
initTelemetryClient();
|
||||||
|
|
||||||
// Start all servers
|
// Start all servers
|
||||||
const apiServer = createApiServer();
|
const apiServer = createApiServer();
|
||||||
const internalServer = createInternalServer();
|
const internalServer = createInternalServer();
|
||||||
|
|||||||
@@ -30,12 +30,6 @@ export class Config {
|
|||||||
throw new Error(`Invalid configuration file: ${errors}`);
|
throw new Error(`Invalid configuration file: ${errors}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (process.env.APP_BASE_DOMAIN) {
|
|
||||||
console.log(
|
|
||||||
"WARNING: You're using deprecated environment variables. Transition to the configuration file. https://docs.fossorial.io/"
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (
|
if (
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
parsedConfig.users ||
|
parsedConfig.users ||
|
||||||
|
|||||||
@@ -1,7 +1,9 @@
|
|||||||
import { db } from "@server/db";
|
import { db, HostMeta } from "@server/db";
|
||||||
import { hostMeta } from "@server/db";
|
import { hostMeta } from "@server/db";
|
||||||
import { v4 as uuidv4 } from "uuid";
|
import { v4 as uuidv4 } from "uuid";
|
||||||
|
|
||||||
|
let gotHostMeta: HostMeta | undefined;
|
||||||
|
|
||||||
export async function setHostMeta() {
|
export async function setHostMeta() {
|
||||||
const [existing] = await db.select().from(hostMeta).limit(1);
|
const [existing] = await db.select().from(hostMeta).limit(1);
|
||||||
|
|
||||||
@@ -15,3 +17,12 @@ export async function setHostMeta() {
|
|||||||
.insert(hostMeta)
|
.insert(hostMeta)
|
||||||
.values({ hostMetaId: id, createdAt: new Date().getTime() });
|
.values({ hostMetaId: id, createdAt: new Date().getTime() });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function getHostMeta() {
|
||||||
|
if (gotHostMeta) {
|
||||||
|
return gotHostMeta;
|
||||||
|
}
|
||||||
|
const [meta] = await db.select().from(hostMeta).limit(1);
|
||||||
|
gotHostMeta = meta;
|
||||||
|
return meta;
|
||||||
|
}
|
||||||
@@ -271,7 +271,7 @@ export async function getNextAvailableClientSubnet(
|
|||||||
)
|
)
|
||||||
].filter((address) => address !== null) as string[];
|
].filter((address) => address !== null) as string[];
|
||||||
|
|
||||||
let subnet = findNextAvailableCidr(addresses, 32, org.subnet); // pick the sites address in the org
|
const subnet = findNextAvailableCidr(addresses, 32, org.subnet); // pick the sites address in the org
|
||||||
if (!subnet) {
|
if (!subnet) {
|
||||||
throw new Error("No available subnets remaining in space");
|
throw new Error("No available subnets remaining in space");
|
||||||
}
|
}
|
||||||
@@ -289,7 +289,7 @@ export async function getNextAvailableOrgSubnet(): Promise<string> {
|
|||||||
|
|
||||||
const addresses = existingAddresses.map((org) => org.subnet!);
|
const addresses = existingAddresses.map((org) => org.subnet!);
|
||||||
|
|
||||||
let subnet = findNextAvailableCidr(
|
const subnet = findNextAvailableCidr(
|
||||||
addresses,
|
addresses,
|
||||||
config.getRawConfig().orgs.block_size,
|
config.getRawConfig().orgs.block_size,
|
||||||
config.getRawConfig().orgs.subnet_group
|
config.getRawConfig().orgs.subnet_group
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { MemoryStore, Store } from "express-rate-limit";
|
import { MemoryStore, Store } from "express-rate-limit";
|
||||||
|
|
||||||
export function createStore(): Store {
|
export function createStore(): Store {
|
||||||
let rateLimitStore: Store = new MemoryStore();
|
const rateLimitStore: Store = new MemoryStore();
|
||||||
return rateLimitStore;
|
return rateLimitStore;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,7 +3,6 @@ import yaml from "js-yaml";
|
|||||||
import { configFilePath1, configFilePath2 } from "./consts";
|
import { configFilePath1, configFilePath2 } from "./consts";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
import stoi from "./stoi";
|
import stoi from "./stoi";
|
||||||
import { build } from "@server/build";
|
|
||||||
|
|
||||||
const portSchema = z.number().positive().gt(0).lte(65535);
|
const portSchema = z.number().positive().gt(0).lte(65535);
|
||||||
|
|
||||||
@@ -25,7 +24,13 @@ export const configSchema = z
|
|||||||
.optional()
|
.optional()
|
||||||
.default("info"),
|
.default("info"),
|
||||||
save_logs: z.boolean().optional().default(false),
|
save_logs: z.boolean().optional().default(false),
|
||||||
log_failed_attempts: z.boolean().optional().default(false)
|
log_failed_attempts: z.boolean().optional().default(false),
|
||||||
|
telmetry: z
|
||||||
|
.object({
|
||||||
|
anonymous_usage: z.boolean().optional().default(true)
|
||||||
|
})
|
||||||
|
.optional()
|
||||||
|
.default({})
|
||||||
}),
|
}),
|
||||||
domains: z
|
domains: z
|
||||||
.record(
|
.record(
|
||||||
@@ -213,7 +218,10 @@ export const configSchema = z
|
|||||||
smtp_host: z.string().optional(),
|
smtp_host: z.string().optional(),
|
||||||
smtp_port: portSchema.optional(),
|
smtp_port: portSchema.optional(),
|
||||||
smtp_user: z.string().optional(),
|
smtp_user: z.string().optional(),
|
||||||
smtp_pass: z.string().optional().transform(getEnvOrYaml("EMAIL_SMTP_PASS")),
|
smtp_pass: z
|
||||||
|
.string()
|
||||||
|
.optional()
|
||||||
|
.transform(getEnvOrYaml("EMAIL_SMTP_PASS")),
|
||||||
smtp_secure: z.boolean().optional(),
|
smtp_secure: z.boolean().optional(),
|
||||||
smtp_tls_reject_unauthorized: z.boolean().optional(),
|
smtp_tls_reject_unauthorized: z.boolean().optional(),
|
||||||
no_reply: z.string().email().optional()
|
no_reply: z.string().email().optional()
|
||||||
@@ -229,7 +237,7 @@ export const configSchema = z
|
|||||||
disable_local_sites: z.boolean().optional(),
|
disable_local_sites: z.boolean().optional(),
|
||||||
disable_basic_wireguard_sites: z.boolean().optional(),
|
disable_basic_wireguard_sites: z.boolean().optional(),
|
||||||
disable_config_managed_domains: z.boolean().optional(),
|
disable_config_managed_domains: z.boolean().optional(),
|
||||||
enable_clients: z.boolean().optional().default(true),
|
enable_clients: z.boolean().optional().default(true)
|
||||||
})
|
})
|
||||||
.optional(),
|
.optional(),
|
||||||
dns: z
|
dns: z
|
||||||
@@ -287,7 +295,7 @@ export function readConfigFile() {
|
|||||||
|
|
||||||
if (!environment) {
|
if (!environment) {
|
||||||
throw new Error(
|
throw new Error(
|
||||||
"No configuration file found. Please create one. https://docs.fossorial.io/"
|
"No configuration file found. Please create one. https://docs.digpangolin.com/self-host/advanced/config-file"
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
295
server/lib/telemetry.ts
Normal file
295
server/lib/telemetry.ts
Normal file
@@ -0,0 +1,295 @@
|
|||||||
|
import { PostHog } from "posthog-node";
|
||||||
|
import config from "./config";
|
||||||
|
import { getHostMeta } from "./hostMeta";
|
||||||
|
import logger from "@server/logger";
|
||||||
|
import { apiKeys, db, roles } from "@server/db";
|
||||||
|
import { sites, users, orgs, resources, clients, idp } from "@server/db";
|
||||||
|
import { eq, count, notInArray } from "drizzle-orm";
|
||||||
|
import { APP_VERSION } from "./consts";
|
||||||
|
import crypto from "crypto";
|
||||||
|
import { UserType } from "@server/types/UserTypes";
|
||||||
|
|
||||||
|
class TelemetryClient {
|
||||||
|
private client: PostHog | null = null;
|
||||||
|
private enabled: boolean;
|
||||||
|
private intervalId: NodeJS.Timeout | null = null;
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
const enabled = config.getRawConfig().app.telmetry.anonymous_usage;
|
||||||
|
this.enabled = enabled;
|
||||||
|
const dev = process.env.ENVIRONMENT !== "prod";
|
||||||
|
|
||||||
|
if (this.enabled && !dev) {
|
||||||
|
this.client = new PostHog(
|
||||||
|
"phc_QYuATSSZt6onzssWcYJbXLzQwnunIpdGGDTYhzK3VjX",
|
||||||
|
{
|
||||||
|
host: "https://digpangolin.com/relay-O7yI"
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
process.on("exit", () => {
|
||||||
|
this.client?.shutdown();
|
||||||
|
});
|
||||||
|
|
||||||
|
this.sendStartupEvents().catch((err) => {
|
||||||
|
logger.error("Failed to send startup telemetry:", err);
|
||||||
|
});
|
||||||
|
|
||||||
|
this.startAnalyticsInterval();
|
||||||
|
|
||||||
|
logger.info(
|
||||||
|
"Pangolin now gathers anonymous usage data to help us better understand how the software is used and guide future improvements and feature development. You can find more details, including instructions for opting out of this anonymous data collection, at: https://docs.digpangolin.com/telemetry"
|
||||||
|
);
|
||||||
|
} else if (!this.enabled && !dev) {
|
||||||
|
logger.info(
|
||||||
|
"Analytics usage statistics collection is disabled. If you enable this, you can help us make Pangolin better for everyone. Learn more at: https://docs.digpangolin.com/telemetry"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private startAnalyticsInterval() {
|
||||||
|
this.intervalId = setInterval(
|
||||||
|
() => {
|
||||||
|
this.collectAndSendAnalytics().catch((err) => {
|
||||||
|
logger.error("Failed to collect analytics:", err);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
6 * 60 * 60 * 1000
|
||||||
|
);
|
||||||
|
|
||||||
|
this.collectAndSendAnalytics().catch((err) => {
|
||||||
|
logger.error("Failed to collect initial analytics:", err);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private anon(value: string): string {
|
||||||
|
return crypto
|
||||||
|
.createHash("sha256")
|
||||||
|
.update(value.toLowerCase())
|
||||||
|
.digest("hex");
|
||||||
|
}
|
||||||
|
|
||||||
|
private async getSystemStats() {
|
||||||
|
try {
|
||||||
|
const [sitesCount] = await db
|
||||||
|
.select({ count: count() })
|
||||||
|
.from(sites);
|
||||||
|
const [usersCount] = await db
|
||||||
|
.select({ count: count() })
|
||||||
|
.from(users);
|
||||||
|
const [usersInternalCount] = await db
|
||||||
|
.select({ count: count() })
|
||||||
|
.from(users)
|
||||||
|
.where(eq(users.type, UserType.Internal));
|
||||||
|
const [usersOidcCount] = await db
|
||||||
|
.select({ count: count() })
|
||||||
|
.from(users)
|
||||||
|
.where(eq(users.type, UserType.OIDC));
|
||||||
|
const [orgsCount] = await db.select({ count: count() }).from(orgs);
|
||||||
|
const [resourcesCount] = await db
|
||||||
|
.select({ count: count() })
|
||||||
|
.from(resources);
|
||||||
|
const [clientsCount] = await db
|
||||||
|
.select({ count: count() })
|
||||||
|
.from(clients);
|
||||||
|
const [idpCount] = await db.select({ count: count() }).from(idp);
|
||||||
|
const [onlineSitesCount] = await db
|
||||||
|
.select({ count: count() })
|
||||||
|
.from(sites)
|
||||||
|
.where(eq(sites.online, true));
|
||||||
|
const [numApiKeys] = await db
|
||||||
|
.select({ count: count() })
|
||||||
|
.from(apiKeys);
|
||||||
|
const [customRoles] = await db
|
||||||
|
.select({ count: count() })
|
||||||
|
.from(roles)
|
||||||
|
.where(notInArray(roles.name, ["Admin", "Member"]));
|
||||||
|
|
||||||
|
const adminUsers = await db
|
||||||
|
.select({ email: users.email })
|
||||||
|
.from(users)
|
||||||
|
.where(eq(users.serverAdmin, true));
|
||||||
|
|
||||||
|
const resourceDetails = await db
|
||||||
|
.select({
|
||||||
|
name: resources.name,
|
||||||
|
sso: resources.sso,
|
||||||
|
protocol: resources.protocol,
|
||||||
|
http: resources.http
|
||||||
|
})
|
||||||
|
.from(resources);
|
||||||
|
|
||||||
|
const siteDetails = await db
|
||||||
|
.select({
|
||||||
|
siteName: sites.name,
|
||||||
|
megabytesIn: sites.megabytesIn,
|
||||||
|
megabytesOut: sites.megabytesOut,
|
||||||
|
type: sites.type,
|
||||||
|
online: sites.online
|
||||||
|
})
|
||||||
|
.from(sites);
|
||||||
|
|
||||||
|
const supporterKey = config.getSupporterData();
|
||||||
|
|
||||||
|
return {
|
||||||
|
numSites: sitesCount.count,
|
||||||
|
numUsers: usersCount.count,
|
||||||
|
numUsersInternal: usersInternalCount.count,
|
||||||
|
numUsersOidc: usersOidcCount.count,
|
||||||
|
numOrganizations: orgsCount.count,
|
||||||
|
numResources: resourcesCount.count,
|
||||||
|
numClients: clientsCount.count,
|
||||||
|
numIdentityProviders: idpCount.count,
|
||||||
|
numSitesOnline: onlineSitesCount.count,
|
||||||
|
resources: resourceDetails,
|
||||||
|
adminUsers: adminUsers.map((u) => u.email),
|
||||||
|
sites: siteDetails,
|
||||||
|
appVersion: APP_VERSION,
|
||||||
|
numApiKeys: numApiKeys.count,
|
||||||
|
numCustomRoles: customRoles.count,
|
||||||
|
supporterStatus: {
|
||||||
|
valid: supporterKey?.valid || false,
|
||||||
|
tier: supporterKey?.tier || "None",
|
||||||
|
githubUsername: supporterKey?.githubUsername || null
|
||||||
|
}
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
logger.error("Failed to collect system stats:", error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async sendStartupEvents() {
|
||||||
|
if (!this.enabled || !this.client) return;
|
||||||
|
|
||||||
|
const hostMeta = await getHostMeta();
|
||||||
|
if (!hostMeta) return;
|
||||||
|
|
||||||
|
const stats = await this.getSystemStats();
|
||||||
|
|
||||||
|
this.client.capture({
|
||||||
|
distinctId: hostMeta.hostMetaId,
|
||||||
|
event: "supporter_status",
|
||||||
|
properties: {
|
||||||
|
valid: stats.supporterStatus.valid,
|
||||||
|
tier: stats.supporterStatus.tier,
|
||||||
|
github_username: stats.supporterStatus.githubUsername
|
||||||
|
? this.anon(stats.supporterStatus.githubUsername)
|
||||||
|
: "None"
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
this.client.capture({
|
||||||
|
distinctId: hostMeta.hostMetaId,
|
||||||
|
event: "host_startup",
|
||||||
|
properties: {
|
||||||
|
host_id: hostMeta.hostMetaId,
|
||||||
|
app_version: stats.appVersion,
|
||||||
|
install_timestamp: hostMeta.createdAt
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
for (const email of stats.adminUsers) {
|
||||||
|
// There should only be on admin user, but just in case
|
||||||
|
if (email) {
|
||||||
|
this.client.capture({
|
||||||
|
distinctId: this.anon(email),
|
||||||
|
event: "admin_user",
|
||||||
|
properties: {
|
||||||
|
host_id: hostMeta.hostMetaId,
|
||||||
|
app_version: stats.appVersion,
|
||||||
|
hashed_email: this.anon(email)
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async collectAndSendAnalytics() {
|
||||||
|
if (!this.enabled || !this.client) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const hostMeta = await getHostMeta();
|
||||||
|
if (!hostMeta) {
|
||||||
|
logger.warn(
|
||||||
|
"Telemetry: Host meta not found, skipping analytics"
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const stats = await this.getSystemStats();
|
||||||
|
|
||||||
|
this.client.capture({
|
||||||
|
distinctId: hostMeta.hostMetaId,
|
||||||
|
event: "system_analytics",
|
||||||
|
properties: {
|
||||||
|
app_version: stats.appVersion,
|
||||||
|
num_sites: stats.numSites,
|
||||||
|
num_users: stats.numUsers,
|
||||||
|
num_users_internal: stats.numUsersInternal,
|
||||||
|
num_users_oidc: stats.numUsersOidc,
|
||||||
|
num_organizations: stats.numOrganizations,
|
||||||
|
num_resources: stats.numResources,
|
||||||
|
num_clients: stats.numClients,
|
||||||
|
num_identity_providers: stats.numIdentityProviders,
|
||||||
|
num_sites_online: stats.numSitesOnline,
|
||||||
|
resources: stats.resources.map((r) => ({
|
||||||
|
name: this.anon(r.name),
|
||||||
|
sso_enabled: r.sso,
|
||||||
|
protocol: r.protocol,
|
||||||
|
http_enabled: r.http
|
||||||
|
})),
|
||||||
|
sites: stats.sites.map((s) => ({
|
||||||
|
site_name: this.anon(s.siteName),
|
||||||
|
megabytes_in: s.megabytesIn,
|
||||||
|
megabytes_out: s.megabytesOut,
|
||||||
|
type: s.type,
|
||||||
|
online: s.online
|
||||||
|
})),
|
||||||
|
num_api_keys: stats.numApiKeys,
|
||||||
|
num_custom_roles: stats.numCustomRoles
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
logger.error("Failed to send analytics:", error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async sendTelemetry(eventName: string, properties: Record<string, any>) {
|
||||||
|
if (!this.enabled || !this.client) return;
|
||||||
|
|
||||||
|
const hostMeta = await getHostMeta();
|
||||||
|
if (!hostMeta) {
|
||||||
|
logger.warn("Telemetry: Host meta not found, skipping telemetry");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.client.groupIdentify({
|
||||||
|
groupType: "host_id",
|
||||||
|
groupKey: hostMeta.hostMetaId,
|
||||||
|
properties
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
shutdown() {
|
||||||
|
if (this.intervalId) {
|
||||||
|
clearInterval(this.intervalId);
|
||||||
|
this.intervalId = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.enabled && this.client) {
|
||||||
|
this.client.shutdown();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let telemetryClient!: TelemetryClient;
|
||||||
|
|
||||||
|
export function initTelemetryClient() {
|
||||||
|
if (!telemetryClient) {
|
||||||
|
telemetryClient = new TelemetryClient();
|
||||||
|
}
|
||||||
|
return telemetryClient;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default telemetryClient;
|
||||||
@@ -5,7 +5,7 @@ import NodeCache from "node-cache";
|
|||||||
import { validateJWT } from "./licenseJwt";
|
import { validateJWT } from "./licenseJwt";
|
||||||
import { count, eq } from "drizzle-orm";
|
import { count, eq } from "drizzle-orm";
|
||||||
import moment from "moment";
|
import moment from "moment";
|
||||||
import { setHostMeta } from "@server/setup/setHostMeta";
|
import { setHostMeta } from "@server/lib/hostMeta";
|
||||||
import { encrypt, decrypt } from "@server/lib/crypto";
|
import { encrypt, decrypt } from "@server/lib/crypto";
|
||||||
|
|
||||||
const keyTypes = ["HOST", "SITES"] as const;
|
const keyTypes = ["HOST", "SITES"] as const;
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import config from "@server/lib/config";
|
|||||||
import * as winston from "winston";
|
import * as winston from "winston";
|
||||||
import path from "path";
|
import path from "path";
|
||||||
import { APP_PATH } from "./lib/consts";
|
import { APP_PATH } from "./lib/consts";
|
||||||
|
import telemetryClient from "./lib/telemetry";
|
||||||
|
|
||||||
const hformat = winston.format.printf(
|
const hformat = winston.format.printf(
|
||||||
({ level, label, message, timestamp, stack, ...metadata }) => {
|
({ level, label, message, timestamp, stack, ...metadata }) => {
|
||||||
|
|||||||
@@ -27,3 +27,4 @@ export * from "./verifyApiKeyAccess";
|
|||||||
export * from "./verifyDomainAccess";
|
export * from "./verifyDomainAccess";
|
||||||
export * from "./verifyClientsEnabled";
|
export * from "./verifyClientsEnabled";
|
||||||
export * from "./verifyUserIsOrgOwner";
|
export * from "./verifyUserIsOrgOwner";
|
||||||
|
export * from "./verifySiteResourceAccess";
|
||||||
|
|||||||
@@ -10,3 +10,4 @@ export * from "./verifyApiKeySetResourceUsers";
|
|||||||
export * from "./verifyAccessTokenAccess";
|
export * from "./verifyAccessTokenAccess";
|
||||||
export * from "./verifyApiKeyIsRoot";
|
export * from "./verifyApiKeyIsRoot";
|
||||||
export * from "./verifyApiKeyApiKeyAccess";
|
export * from "./verifyApiKeyApiKeyAccess";
|
||||||
|
export * from "./verifyApiKeyClientAccess";
|
||||||
|
|||||||
@@ -35,6 +35,11 @@ export async function verifyApiKeyApiKeyAccess(
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (callerApiKey.isRoot) {
|
||||||
|
// Root keys can access any key in any org
|
||||||
|
return next();
|
||||||
|
}
|
||||||
|
|
||||||
const [callerApiKeyOrg] = await db
|
const [callerApiKeyOrg] = await db
|
||||||
.select()
|
.select()
|
||||||
.from(apiKeyOrg)
|
.from(apiKeyOrg)
|
||||||
|
|||||||
91
server/middlewares/integration/verifyApiKeyClientAccess.ts
Normal file
91
server/middlewares/integration/verifyApiKeyClientAccess.ts
Normal file
@@ -0,0 +1,91 @@
|
|||||||
|
import { Request, Response, NextFunction } from "express";
|
||||||
|
import { clients, db } from "@server/db";
|
||||||
|
import { apiKeyOrg } from "@server/db";
|
||||||
|
import { and, eq } from "drizzle-orm";
|
||||||
|
import createHttpError from "http-errors";
|
||||||
|
import HttpCode from "@server/types/HttpCode";
|
||||||
|
|
||||||
|
export async function verifyApiKeyClientAccess(
|
||||||
|
req: Request,
|
||||||
|
res: Response,
|
||||||
|
next: NextFunction
|
||||||
|
) {
|
||||||
|
try {
|
||||||
|
const apiKey = req.apiKey;
|
||||||
|
const clientId = parseInt(
|
||||||
|
req.params.clientId || req.body.clientId || req.query.clientId
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!apiKey) {
|
||||||
|
return next(
|
||||||
|
createHttpError(HttpCode.UNAUTHORIZED, "Key not authenticated")
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isNaN(clientId)) {
|
||||||
|
return next(
|
||||||
|
createHttpError(HttpCode.BAD_REQUEST, "Invalid client ID")
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (apiKey.isRoot) {
|
||||||
|
// Root keys can access any key in any org
|
||||||
|
return next();
|
||||||
|
}
|
||||||
|
|
||||||
|
const client = await db
|
||||||
|
.select()
|
||||||
|
.from(clients)
|
||||||
|
.where(eq(clients.clientId, clientId))
|
||||||
|
.limit(1);
|
||||||
|
|
||||||
|
if (client.length === 0) {
|
||||||
|
return next(
|
||||||
|
createHttpError(
|
||||||
|
HttpCode.NOT_FOUND,
|
||||||
|
`Client with ID ${clientId} not found`
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!client[0].orgId) {
|
||||||
|
return next(
|
||||||
|
createHttpError(
|
||||||
|
HttpCode.INTERNAL_SERVER_ERROR,
|
||||||
|
`Client with ID ${clientId} does not have an organization ID`
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!req.apiKeyOrg) {
|
||||||
|
const apiKeyOrgRes = await db
|
||||||
|
.select()
|
||||||
|
.from(apiKeyOrg)
|
||||||
|
.where(
|
||||||
|
and(
|
||||||
|
eq(apiKeyOrg.apiKeyId, apiKey.apiKeyId),
|
||||||
|
eq(apiKeyOrg.orgId, client[0].orgId)
|
||||||
|
)
|
||||||
|
);
|
||||||
|
req.apiKeyOrg = apiKeyOrgRes[0];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!req.apiKeyOrg) {
|
||||||
|
return next(
|
||||||
|
createHttpError(
|
||||||
|
HttpCode.FORBIDDEN,
|
||||||
|
"Key does not have access to this organization"
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return next();
|
||||||
|
} catch (error) {
|
||||||
|
return next(
|
||||||
|
createHttpError(
|
||||||
|
HttpCode.INTERNAL_SERVER_ERROR,
|
||||||
|
"Error verifying site access"
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -27,6 +27,11 @@ export async function verifyApiKeyOrgAccess(
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (req.apiKey?.isRoot) {
|
||||||
|
// Root keys can access any key in any org
|
||||||
|
return next();
|
||||||
|
}
|
||||||
|
|
||||||
if (!req.apiKeyOrg) {
|
if (!req.apiKeyOrg) {
|
||||||
const apiKeyOrgRes = await db
|
const apiKeyOrgRes = await db
|
||||||
.select()
|
.select()
|
||||||
|
|||||||
@@ -37,6 +37,11 @@ export async function verifyApiKeyResourceAccess(
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (apiKey.isRoot) {
|
||||||
|
// Root keys can access any key in any org
|
||||||
|
return next();
|
||||||
|
}
|
||||||
|
|
||||||
if (!resource.orgId) {
|
if (!resource.orgId) {
|
||||||
return next(
|
return next(
|
||||||
createHttpError(
|
createHttpError(
|
||||||
|
|||||||
@@ -45,6 +45,11 @@ export async function verifyApiKeyRoleAccess(
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (apiKey.isRoot) {
|
||||||
|
// Root keys can access any key in any org
|
||||||
|
return next();
|
||||||
|
}
|
||||||
|
|
||||||
const orgIds = new Set(rolesData.map((role) => role.orgId));
|
const orgIds = new Set(rolesData.map((role) => role.orgId));
|
||||||
|
|
||||||
for (const role of rolesData) {
|
for (const role of rolesData) {
|
||||||
|
|||||||
@@ -32,6 +32,11 @@ export async function verifyApiKeySetResourceUsers(
|
|||||||
return next(createHttpError(HttpCode.BAD_REQUEST, "Invalid user IDs"));
|
return next(createHttpError(HttpCode.BAD_REQUEST, "Invalid user IDs"));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (apiKey.isRoot) {
|
||||||
|
// Root keys can access any key in any org
|
||||||
|
return next();
|
||||||
|
}
|
||||||
|
|
||||||
if (userIds.length === 0) {
|
if (userIds.length === 0) {
|
||||||
return next();
|
return next();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,9 +1,6 @@
|
|||||||
import { Request, Response, NextFunction } from "express";
|
import { Request, Response, NextFunction } from "express";
|
||||||
import { db } from "@server/db";
|
import { db } from "@server/db";
|
||||||
import {
|
import { sites, apiKeyOrg } from "@server/db";
|
||||||
sites,
|
|
||||||
apiKeyOrg
|
|
||||||
} from "@server/db";
|
|
||||||
import { and, eq, or } from "drizzle-orm";
|
import { and, eq, or } from "drizzle-orm";
|
||||||
import createHttpError from "http-errors";
|
import createHttpError from "http-errors";
|
||||||
import HttpCode from "@server/types/HttpCode";
|
import HttpCode from "@server/types/HttpCode";
|
||||||
@@ -31,6 +28,11 @@ export async function verifyApiKeySiteAccess(
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (apiKey.isRoot) {
|
||||||
|
// Root keys can access any key in any org
|
||||||
|
return next();
|
||||||
|
}
|
||||||
|
|
||||||
const site = await db
|
const site = await db
|
||||||
.select()
|
.select()
|
||||||
.from(sites)
|
.from(sites)
|
||||||
|
|||||||
@@ -66,6 +66,11 @@ export async function verifyApiKeyTargetAccess(
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (apiKey.isRoot) {
|
||||||
|
// Root keys can access any key in any org
|
||||||
|
return next();
|
||||||
|
}
|
||||||
|
|
||||||
if (!resource.orgId) {
|
if (!resource.orgId) {
|
||||||
return next(
|
return next(
|
||||||
createHttpError(
|
createHttpError(
|
||||||
|
|||||||
@@ -27,6 +27,11 @@ export async function verifyApiKeyUserAccess(
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (apiKey.isRoot) {
|
||||||
|
// Root keys can access any key in any org
|
||||||
|
return next();
|
||||||
|
}
|
||||||
|
|
||||||
if (!req.apiKeyOrg || !req.apiKeyOrg.orgId) {
|
if (!req.apiKeyOrg || !req.apiKeyOrg.orgId) {
|
||||||
return next(
|
return next(
|
||||||
createHttpError(
|
createHttpError(
|
||||||
|
|||||||
62
server/middlewares/verifySiteResourceAccess.ts
Normal file
62
server/middlewares/verifySiteResourceAccess.ts
Normal file
@@ -0,0 +1,62 @@
|
|||||||
|
import { Request, Response, NextFunction } from "express";
|
||||||
|
import { db } from "@server/db";
|
||||||
|
import { siteResources } from "@server/db";
|
||||||
|
import { eq, and } from "drizzle-orm";
|
||||||
|
import createHttpError from "http-errors";
|
||||||
|
import HttpCode from "@server/types/HttpCode";
|
||||||
|
import logger from "@server/logger";
|
||||||
|
|
||||||
|
export async function verifySiteResourceAccess(
|
||||||
|
req: Request,
|
||||||
|
res: Response,
|
||||||
|
next: NextFunction
|
||||||
|
): Promise<any> {
|
||||||
|
try {
|
||||||
|
const siteResourceId = parseInt(req.params.siteResourceId);
|
||||||
|
const siteId = parseInt(req.params.siteId);
|
||||||
|
const orgId = req.params.orgId;
|
||||||
|
|
||||||
|
if (!siteResourceId || !siteId || !orgId) {
|
||||||
|
return next(
|
||||||
|
createHttpError(
|
||||||
|
HttpCode.BAD_REQUEST,
|
||||||
|
"Missing required parameters"
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if the site resource exists and belongs to the specified site and org
|
||||||
|
const [siteResource] = await db
|
||||||
|
.select()
|
||||||
|
.from(siteResources)
|
||||||
|
.where(and(
|
||||||
|
eq(siteResources.siteResourceId, siteResourceId),
|
||||||
|
eq(siteResources.siteId, siteId),
|
||||||
|
eq(siteResources.orgId, orgId)
|
||||||
|
))
|
||||||
|
.limit(1);
|
||||||
|
|
||||||
|
if (!siteResource) {
|
||||||
|
return next(
|
||||||
|
createHttpError(
|
||||||
|
HttpCode.NOT_FOUND,
|
||||||
|
"Site resource not found"
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Attach the siteResource to the request for use in the next middleware/route
|
||||||
|
// @ts-ignore - Extending Request type
|
||||||
|
req.siteResource = siteResource;
|
||||||
|
|
||||||
|
next();
|
||||||
|
} catch (error) {
|
||||||
|
logger.error("Error verifying site resource access:", error);
|
||||||
|
return next(
|
||||||
|
createHttpError(
|
||||||
|
HttpCode.INTERNAL_SERVER_ERROR,
|
||||||
|
"Error verifying site resource access"
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -15,7 +15,7 @@ export async function createNextServer() {
|
|||||||
|
|
||||||
const nextServer = express();
|
const nextServer = express();
|
||||||
|
|
||||||
nextServer.all("*", (req, res) => {
|
nextServer.all("/{*splat}", (req, res) => {
|
||||||
const parsedUrl = parse(req.url!, true);
|
const parsedUrl = parse(req.url!, true);
|
||||||
return handle(req, res, parsedUrl);
|
return handle(req, res, parsedUrl);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -222,7 +222,7 @@ export async function listAccessTokens(
|
|||||||
(resource) => resource.resourceId
|
(resource) => resource.resourceId
|
||||||
);
|
);
|
||||||
|
|
||||||
let countQuery: any = db
|
const countQuery: any = db
|
||||||
.select({ count: count() })
|
.select({ count: count() })
|
||||||
.from(resources)
|
.from(resources)
|
||||||
.where(inArray(resources.resourceId, accessibleResourceIds));
|
.where(inArray(resources.resourceId, accessibleResourceIds));
|
||||||
|
|||||||
@@ -63,15 +63,6 @@ export async function createRootApiKey(
|
|||||||
lastChars,
|
lastChars,
|
||||||
isRoot: true
|
isRoot: true
|
||||||
});
|
});
|
||||||
|
|
||||||
const allOrgs = await trx.select().from(orgs);
|
|
||||||
|
|
||||||
for (const org of allOrgs) {
|
|
||||||
await trx.insert(apiKeyOrg).values({
|
|
||||||
apiKeyId,
|
|
||||||
orgId: org.orgId
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ export * from "./resetPassword";
|
|||||||
export * from "./requestPasswordReset";
|
export * from "./requestPasswordReset";
|
||||||
export * from "./setServerAdmin";
|
export * from "./setServerAdmin";
|
||||||
export * from "./initialSetupComplete";
|
export * from "./initialSetupComplete";
|
||||||
|
export * from "./validateSetupToken";
|
||||||
export * from "./changePassword";
|
export * from "./changePassword";
|
||||||
export * from "./checkResourceSession";
|
export * from "./checkResourceSession";
|
||||||
export * from "./securityKey";
|
export * from "./securityKey";
|
||||||
|
|||||||
@@ -8,14 +8,15 @@ import logger from "@server/logger";
|
|||||||
import { hashPassword } from "@server/auth/password";
|
import { hashPassword } from "@server/auth/password";
|
||||||
import { passwordSchema } from "@server/auth/passwordSchema";
|
import { passwordSchema } from "@server/auth/passwordSchema";
|
||||||
import { response } from "@server/lib";
|
import { response } from "@server/lib";
|
||||||
import { db, users } from "@server/db";
|
import { db, users, setupTokens } from "@server/db";
|
||||||
import { eq } from "drizzle-orm";
|
import { eq, and } from "drizzle-orm";
|
||||||
import { UserType } from "@server/types/UserTypes";
|
import { UserType } from "@server/types/UserTypes";
|
||||||
import moment from "moment";
|
import moment from "moment";
|
||||||
|
|
||||||
export const bodySchema = z.object({
|
export const bodySchema = z.object({
|
||||||
email: z.string().toLowerCase().email(),
|
email: z.string().toLowerCase().email(),
|
||||||
password: passwordSchema
|
password: passwordSchema,
|
||||||
|
setupToken: z.string().min(1, "Setup token is required")
|
||||||
});
|
});
|
||||||
|
|
||||||
export type SetServerAdminBody = z.infer<typeof bodySchema>;
|
export type SetServerAdminBody = z.infer<typeof bodySchema>;
|
||||||
@@ -39,7 +40,27 @@ export async function setServerAdmin(
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const { email, password } = parsedBody.data;
|
const { email, password, setupToken } = parsedBody.data;
|
||||||
|
|
||||||
|
// Validate setup token
|
||||||
|
const [validToken] = await db
|
||||||
|
.select()
|
||||||
|
.from(setupTokens)
|
||||||
|
.where(
|
||||||
|
and(
|
||||||
|
eq(setupTokens.token, setupToken),
|
||||||
|
eq(setupTokens.used, false)
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!validToken) {
|
||||||
|
return next(
|
||||||
|
createHttpError(
|
||||||
|
HttpCode.BAD_REQUEST,
|
||||||
|
"Invalid or expired setup token"
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
const [existing] = await db
|
const [existing] = await db
|
||||||
.select()
|
.select()
|
||||||
@@ -58,15 +79,27 @@ export async function setServerAdmin(
|
|||||||
const passwordHash = await hashPassword(password);
|
const passwordHash = await hashPassword(password);
|
||||||
const userId = generateId(15);
|
const userId = generateId(15);
|
||||||
|
|
||||||
await db.insert(users).values({
|
await db.transaction(async (trx) => {
|
||||||
userId: userId,
|
// Mark the token as used
|
||||||
email: email,
|
await trx
|
||||||
type: UserType.Internal,
|
.update(setupTokens)
|
||||||
username: email,
|
.set({
|
||||||
passwordHash,
|
used: true,
|
||||||
dateCreated: moment().toISOString(),
|
dateUsed: moment().toISOString()
|
||||||
serverAdmin: true,
|
})
|
||||||
emailVerified: true
|
.where(eq(setupTokens.tokenId, validToken.tokenId));
|
||||||
|
|
||||||
|
// Create the server admin user
|
||||||
|
await trx.insert(users).values({
|
||||||
|
userId: userId,
|
||||||
|
email: email,
|
||||||
|
type: UserType.Internal,
|
||||||
|
username: email,
|
||||||
|
passwordHash,
|
||||||
|
dateCreated: moment().toISOString(),
|
||||||
|
serverAdmin: true,
|
||||||
|
emailVerified: true
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
return response<SetServerAdminResponse>(res, {
|
return response<SetServerAdminResponse>(res, {
|
||||||
|
|||||||
84
server/routers/auth/validateSetupToken.ts
Normal file
84
server/routers/auth/validateSetupToken.ts
Normal file
@@ -0,0 +1,84 @@
|
|||||||
|
import { Request, Response, NextFunction } from "express";
|
||||||
|
import { z } from "zod";
|
||||||
|
import { db, setupTokens } from "@server/db";
|
||||||
|
import { eq, and } from "drizzle-orm";
|
||||||
|
import response from "@server/lib/response";
|
||||||
|
import HttpCode from "@server/types/HttpCode";
|
||||||
|
import createHttpError from "http-errors";
|
||||||
|
import logger from "@server/logger";
|
||||||
|
import { fromError } from "zod-validation-error";
|
||||||
|
|
||||||
|
const validateSetupTokenSchema = z
|
||||||
|
.object({
|
||||||
|
token: z.string().min(1, "Token is required")
|
||||||
|
})
|
||||||
|
.strict();
|
||||||
|
|
||||||
|
export type ValidateSetupTokenResponse = {
|
||||||
|
valid: boolean;
|
||||||
|
message: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export async function validateSetupToken(
|
||||||
|
req: Request,
|
||||||
|
res: Response,
|
||||||
|
next: NextFunction
|
||||||
|
): Promise<any> {
|
||||||
|
try {
|
||||||
|
const parsedBody = validateSetupTokenSchema.safeParse(req.body);
|
||||||
|
|
||||||
|
if (!parsedBody.success) {
|
||||||
|
return next(
|
||||||
|
createHttpError(
|
||||||
|
HttpCode.BAD_REQUEST,
|
||||||
|
fromError(parsedBody.error).toString()
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const { token } = parsedBody.data;
|
||||||
|
|
||||||
|
// Find the token in the database
|
||||||
|
const [setupToken] = await db
|
||||||
|
.select()
|
||||||
|
.from(setupTokens)
|
||||||
|
.where(
|
||||||
|
and(
|
||||||
|
eq(setupTokens.token, token),
|
||||||
|
eq(setupTokens.used, false)
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!setupToken) {
|
||||||
|
return response<ValidateSetupTokenResponse>(res, {
|
||||||
|
data: {
|
||||||
|
valid: false,
|
||||||
|
message: "Invalid or expired setup token"
|
||||||
|
},
|
||||||
|
success: true,
|
||||||
|
error: false,
|
||||||
|
message: "Token validation completed",
|
||||||
|
status: HttpCode.OK
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return response<ValidateSetupTokenResponse>(res, {
|
||||||
|
data: {
|
||||||
|
valid: true,
|
||||||
|
message: "Setup token is valid"
|
||||||
|
},
|
||||||
|
success: true,
|
||||||
|
error: false,
|
||||||
|
message: "Token validation completed",
|
||||||
|
status: HttpCode.OK
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
logger.error(error);
|
||||||
|
return next(
|
||||||
|
createHttpError(
|
||||||
|
HttpCode.INTERNAL_SERVER_ERROR,
|
||||||
|
"Failed to validate setup token"
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -52,20 +52,26 @@ export async function exchangeSession(
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
const { requestToken, host, requestIp } = parsedBody.data;
|
const { requestToken, host, requestIp } = parsedBody.data;
|
||||||
|
let cleanHost = host;
|
||||||
|
// if the host ends with :port
|
||||||
|
if (cleanHost.match(/:[0-9]{1,5}$/)) {
|
||||||
|
let matched = ''+cleanHost.match(/:[0-9]{1,5}$/);
|
||||||
|
cleanHost = cleanHost.slice(0, -1*matched.length);
|
||||||
|
}
|
||||||
|
|
||||||
const clientIp = requestIp?.split(":")[0];
|
const clientIp = requestIp?.split(":")[0];
|
||||||
|
|
||||||
const [resource] = await db
|
const [resource] = await db
|
||||||
.select()
|
.select()
|
||||||
.from(resources)
|
.from(resources)
|
||||||
.where(eq(resources.fullDomain, host))
|
.where(eq(resources.fullDomain, cleanHost))
|
||||||
.limit(1);
|
.limit(1);
|
||||||
|
|
||||||
if (!resource) {
|
if (!resource) {
|
||||||
return next(
|
return next(
|
||||||
createHttpError(
|
createHttpError(
|
||||||
HttpCode.NOT_FOUND,
|
HttpCode.NOT_FOUND,
|
||||||
`Resource with host ${host} not found`
|
`Resource with host ${cleanHost} not found`
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -121,11 +121,10 @@ export async function verifyResourceSession(
|
|||||||
logger.debug("Client IP:", { clientIp });
|
logger.debug("Client IP:", { clientIp });
|
||||||
|
|
||||||
let cleanHost = host;
|
let cleanHost = host;
|
||||||
// if the host ends with :443 or :80 remove it
|
// if the host ends with :port, strip it
|
||||||
if (cleanHost.endsWith(":443")) {
|
if (cleanHost.match(/:[0-9]{1,5}$/)) {
|
||||||
cleanHost = cleanHost.slice(0, -4);
|
let matched = ''+cleanHost.match(/:[0-9]{1,5}$/);
|
||||||
} else if (cleanHost.endsWith(":80")) {
|
cleanHost = cleanHost.slice(0, -1*matched.length);
|
||||||
cleanHost = cleanHost.slice(0, -3);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const resourceCacheKey = `resource:${cleanHost}`;
|
const resourceCacheKey = `resource:${cleanHost}`;
|
||||||
|
|||||||
@@ -14,32 +14,32 @@ import { OpenAPITags, registry } from "@server/openApi";
|
|||||||
const getClientSchema = z
|
const getClientSchema = z
|
||||||
.object({
|
.object({
|
||||||
clientId: z.string().transform(stoi).pipe(z.number().int().positive()),
|
clientId: z.string().transform(stoi).pipe(z.number().int().positive()),
|
||||||
orgId: z.string().optional()
|
orgId: z.string()
|
||||||
})
|
})
|
||||||
.strict();
|
.strict();
|
||||||
|
|
||||||
async function query(clientId: number) {
|
async function query(clientId: number, orgId: string) {
|
||||||
// Get the client
|
// Get the client
|
||||||
const [client] = await db
|
const [client] = await db
|
||||||
.select()
|
.select()
|
||||||
.from(clients)
|
.from(clients)
|
||||||
.where(eq(clients.clientId, clientId))
|
.where(and(eq(clients.clientId, clientId), eq(clients.orgId, orgId)))
|
||||||
.limit(1);
|
.limit(1);
|
||||||
|
|
||||||
if (!client) {
|
if (!client) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get the siteIds associated with this client
|
// Get the siteIds associated with this client
|
||||||
const sites = await db
|
const sites = await db
|
||||||
.select({ siteId: clientSites.siteId })
|
.select({ siteId: clientSites.siteId })
|
||||||
.from(clientSites)
|
.from(clientSites)
|
||||||
.where(eq(clientSites.clientId, clientId));
|
.where(eq(clientSites.clientId, clientId));
|
||||||
|
|
||||||
// Add the siteIds to the client object
|
// Add the siteIds to the client object
|
||||||
return {
|
return {
|
||||||
...client,
|
...client,
|
||||||
siteIds: sites.map(site => site.siteId)
|
siteIds: sites.map((site) => site.siteId)
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -75,9 +75,9 @@ export async function getClient(
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const { clientId } = parsedParams.data;
|
const { clientId, orgId } = parsedParams.data;
|
||||||
|
|
||||||
const client = await query(clientId);
|
const client = await query(clientId, orgId);
|
||||||
|
|
||||||
if (!client) {
|
if (!client) {
|
||||||
return next(
|
return next(
|
||||||
@@ -98,4 +98,4 @@ export async function getClient(
|
|||||||
createHttpError(HttpCode.INTERNAL_SERVER_ERROR, "An error occurred")
|
createHttpError(HttpCode.INTERNAL_SERVER_ERROR, "An error occurred")
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ const pickClientDefaultsSchema = z
|
|||||||
|
|
||||||
registry.registerPath({
|
registry.registerPath({
|
||||||
method: "get",
|
method: "get",
|
||||||
path: "/site/{siteId}/pick-client-defaults",
|
path: "/org/{orgId}/pick-client-defaults",
|
||||||
description: "Return pre-requisite data for creating a client.",
|
description: "Return pre-requisite data for creating a client.",
|
||||||
tags: [OpenAPITags.Client, OpenAPITags.Site],
|
tags: [OpenAPITags.Client, OpenAPITags.Site],
|
||||||
request: {
|
request: {
|
||||||
|
|||||||
39
server/routers/client/targets.ts
Normal file
39
server/routers/client/targets.ts
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
import { sendToClient } from "../ws";
|
||||||
|
|
||||||
|
export async function addTargets(
|
||||||
|
newtId: string,
|
||||||
|
destinationIp: string,
|
||||||
|
destinationPort: number,
|
||||||
|
protocol: string,
|
||||||
|
port: number | null = null
|
||||||
|
) {
|
||||||
|
const target = `${port ? port + ":" : ""}${
|
||||||
|
destinationIp
|
||||||
|
}:${destinationPort}`;
|
||||||
|
|
||||||
|
await sendToClient(newtId, {
|
||||||
|
type: `newt/wg/${protocol}/add`,
|
||||||
|
data: {
|
||||||
|
targets: [target] // We can only use one target for WireGuard right now
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function removeTargets(
|
||||||
|
newtId: string,
|
||||||
|
destinationIp: string,
|
||||||
|
destinationPort: number,
|
||||||
|
protocol: string,
|
||||||
|
port: number | null = null
|
||||||
|
) {
|
||||||
|
const target = `${port ? port + ":" : ""}${
|
||||||
|
destinationIp
|
||||||
|
}:${destinationPort}`;
|
||||||
|
|
||||||
|
await sendToClient(newtId, {
|
||||||
|
type: `newt/wg/${protocol}/remove`,
|
||||||
|
data: {
|
||||||
|
targets: [target] // We can only use one target for WireGuard right now
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -129,7 +129,7 @@ export async function updateClient(
|
|||||||
`Adding ${sitesAdded.length} new sites to client ${client.clientId}`
|
`Adding ${sitesAdded.length} new sites to client ${client.clientId}`
|
||||||
);
|
);
|
||||||
for (const siteId of sitesAdded) {
|
for (const siteId of sitesAdded) {
|
||||||
if (!client.subnet || !client.pubKey || !client.endpoint) {
|
if (!client.subnet || !client.pubKey) {
|
||||||
logger.debug(
|
logger.debug(
|
||||||
"Client subnet, pubKey or endpoint is not set"
|
"Client subnet, pubKey or endpoint is not set"
|
||||||
);
|
);
|
||||||
@@ -140,10 +140,25 @@ export async function updateClient(
|
|||||||
// BUT REALLY WE NEED TO TRACK THE USERS PREFERENCE THAT THEY CHOSE IN THE CLIENTS
|
// BUT REALLY WE NEED TO TRACK THE USERS PREFERENCE THAT THEY CHOSE IN THE CLIENTS
|
||||||
const isRelayed = true;
|
const isRelayed = true;
|
||||||
|
|
||||||
|
// get the clientsite
|
||||||
|
const [clientSite] = await db
|
||||||
|
.select()
|
||||||
|
.from(clientSites)
|
||||||
|
.where(and(
|
||||||
|
eq(clientSites.clientId, client.clientId),
|
||||||
|
eq(clientSites.siteId, siteId)
|
||||||
|
))
|
||||||
|
.limit(1);
|
||||||
|
|
||||||
|
if (!clientSite || !clientSite.endpoint) {
|
||||||
|
logger.debug("Client site is missing or has no endpoint");
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
const site = await newtAddPeer(siteId, {
|
const site = await newtAddPeer(siteId, {
|
||||||
publicKey: client.pubKey,
|
publicKey: client.pubKey,
|
||||||
allowedIps: [`${client.subnet.split("/")[0]}/32`], // we want to only allow from that client
|
allowedIps: [`${client.subnet.split("/")[0]}/32`], // we want to only allow from that client
|
||||||
endpoint: isRelayed ? "" : client.endpoint
|
endpoint: isRelayed ? "" : clientSite.endpoint
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!site) {
|
if (!site) {
|
||||||
@@ -255,7 +270,6 @@ export async function updateClient(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (client.endpoint) {
|
|
||||||
// get all sites for this client and join with exit nodes with site.exitNodeId
|
// get all sites for this client and join with exit nodes with site.exitNodeId
|
||||||
const sitesData = await db
|
const sitesData = await db
|
||||||
.select()
|
.select()
|
||||||
@@ -272,6 +286,8 @@ export async function updateClient(
|
|||||||
|
|
||||||
let exitNodeDestinations: {
|
let exitNodeDestinations: {
|
||||||
reachableAt: string;
|
reachableAt: string;
|
||||||
|
sourceIp: string;
|
||||||
|
sourcePort: number;
|
||||||
destinations: PeerDestination[];
|
destinations: PeerDestination[];
|
||||||
}[] = [];
|
}[] = [];
|
||||||
|
|
||||||
@@ -282,6 +298,14 @@ export async function updateClient(
|
|||||||
);
|
);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!site.clientSites.endpoint) {
|
||||||
|
logger.warn(
|
||||||
|
`Site ${site.sites.siteId} has no endpoint, skipping`
|
||||||
|
);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
// find the destinations in the array
|
// find the destinations in the array
|
||||||
let destinations = exitNodeDestinations.find(
|
let destinations = exitNodeDestinations.find(
|
||||||
(d) => d.reachableAt === site.exitNodes?.reachableAt
|
(d) => d.reachableAt === site.exitNodes?.reachableAt
|
||||||
@@ -290,6 +314,8 @@ export async function updateClient(
|
|||||||
if (!destinations) {
|
if (!destinations) {
|
||||||
destinations = {
|
destinations = {
|
||||||
reachableAt: site.exitNodes?.reachableAt || "",
|
reachableAt: site.exitNodes?.reachableAt || "",
|
||||||
|
sourceIp: site.clientSites.endpoint.split(":")[0] || "",
|
||||||
|
sourcePort: parseInt(site.clientSites.endpoint.split(":")[1]) || 0,
|
||||||
destinations: [
|
destinations: [
|
||||||
{
|
{
|
||||||
destinationIP:
|
destinationIP:
|
||||||
@@ -319,8 +345,8 @@ export async function updateClient(
|
|||||||
`Updating destinations for exit node at ${destination.reachableAt}`
|
`Updating destinations for exit node at ${destination.reachableAt}`
|
||||||
);
|
);
|
||||||
const payload = {
|
const payload = {
|
||||||
sourceIp: client.endpoint?.split(":")[0] || "",
|
sourceIp: destination.sourceIp,
|
||||||
sourcePort: parseInt(client.endpoint?.split(":")[1]) || 0,
|
sourcePort: destination.sourcePort,
|
||||||
destinations: destination.destinations
|
destinations: destination.destinations
|
||||||
};
|
};
|
||||||
logger.info(
|
logger.info(
|
||||||
@@ -341,14 +367,16 @@ export async function updateClient(
|
|||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (axios.isAxiosError(error)) {
|
if (axios.isAxiosError(error)) {
|
||||||
throw new Error(
|
logger.error(
|
||||||
`Error communicating with Gerbil. Make sure Pangolin can reach the Gerbil HTTP API: ${error.response?.status}`
|
`Error updating destinations (can Pangolin see Gerbil HTTP API?) for exit node at ${destination.reachableAt} (status: ${error.response?.status}): ${JSON.stringify(error.response?.data, null, 2)}`
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
logger.error(
|
||||||
|
`Error updating destinations for exit node at ${destination.reachableAt}: ${error}`
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
throw error;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
// Fetch the updated client
|
// Fetch the updated client
|
||||||
const [updatedClient] = await trx
|
const [updatedClient] = await trx
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import * as user from "./user";
|
|||||||
import * as auth from "./auth";
|
import * as auth from "./auth";
|
||||||
import * as role from "./role";
|
import * as role from "./role";
|
||||||
import * as client from "./client";
|
import * as client from "./client";
|
||||||
|
import * as siteResource from "./siteResource";
|
||||||
import * as supporterKey from "./supporterKey";
|
import * as supporterKey from "./supporterKey";
|
||||||
import * as accessToken from "./accessToken";
|
import * as accessToken from "./accessToken";
|
||||||
import * as idp from "./idp";
|
import * as idp from "./idp";
|
||||||
@@ -34,7 +35,8 @@ import {
|
|||||||
verifyDomainAccess,
|
verifyDomainAccess,
|
||||||
verifyClientsEnabled,
|
verifyClientsEnabled,
|
||||||
verifyUserHasAction,
|
verifyUserHasAction,
|
||||||
verifyUserIsOrgOwner
|
verifyUserIsOrgOwner,
|
||||||
|
verifySiteResourceAccess
|
||||||
} from "@server/middlewares";
|
} from "@server/middlewares";
|
||||||
import { createStore } from "@server/lib/rateLimitStore";
|
import { createStore } from "@server/lib/rateLimitStore";
|
||||||
import { ActionsEnum } from "@server/auth/actions";
|
import { ActionsEnum } from "@server/auth/actions";
|
||||||
@@ -213,9 +215,60 @@ authenticated.get(
|
|||||||
site.listContainers
|
site.listContainers
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// Site Resource endpoints
|
||||||
authenticated.put(
|
authenticated.put(
|
||||||
"/org/:orgId/site/:siteId/resource",
|
"/org/:orgId/site/:siteId/resource",
|
||||||
verifyOrgAccess,
|
verifyOrgAccess,
|
||||||
|
verifySiteAccess,
|
||||||
|
verifyUserHasAction(ActionsEnum.createSiteResource),
|
||||||
|
siteResource.createSiteResource
|
||||||
|
);
|
||||||
|
|
||||||
|
authenticated.get(
|
||||||
|
"/org/:orgId/site/:siteId/resources",
|
||||||
|
verifyOrgAccess,
|
||||||
|
verifySiteAccess,
|
||||||
|
verifyUserHasAction(ActionsEnum.listSiteResources),
|
||||||
|
siteResource.listSiteResources
|
||||||
|
);
|
||||||
|
|
||||||
|
authenticated.get(
|
||||||
|
"/org/:orgId/site-resources",
|
||||||
|
verifyOrgAccess,
|
||||||
|
verifyUserHasAction(ActionsEnum.listSiteResources),
|
||||||
|
siteResource.listAllSiteResourcesByOrg
|
||||||
|
);
|
||||||
|
|
||||||
|
authenticated.get(
|
||||||
|
"/org/:orgId/site/:siteId/resource/:siteResourceId",
|
||||||
|
verifyOrgAccess,
|
||||||
|
verifySiteAccess,
|
||||||
|
verifySiteResourceAccess,
|
||||||
|
verifyUserHasAction(ActionsEnum.getSiteResource),
|
||||||
|
siteResource.getSiteResource
|
||||||
|
);
|
||||||
|
|
||||||
|
authenticated.post(
|
||||||
|
"/org/:orgId/site/:siteId/resource/:siteResourceId",
|
||||||
|
verifyOrgAccess,
|
||||||
|
verifySiteAccess,
|
||||||
|
verifySiteResourceAccess,
|
||||||
|
verifyUserHasAction(ActionsEnum.updateSiteResource),
|
||||||
|
siteResource.updateSiteResource
|
||||||
|
);
|
||||||
|
|
||||||
|
authenticated.delete(
|
||||||
|
"/org/:orgId/site/:siteId/resource/:siteResourceId",
|
||||||
|
verifyOrgAccess,
|
||||||
|
verifySiteAccess,
|
||||||
|
verifySiteResourceAccess,
|
||||||
|
verifyUserHasAction(ActionsEnum.deleteSiteResource),
|
||||||
|
siteResource.deleteSiteResource
|
||||||
|
);
|
||||||
|
|
||||||
|
authenticated.put(
|
||||||
|
"/org/:orgId/resource",
|
||||||
|
verifyOrgAccess,
|
||||||
verifyUserHasAction(ActionsEnum.createResource),
|
verifyUserHasAction(ActionsEnum.createResource),
|
||||||
resource.createResource
|
resource.createResource
|
||||||
);
|
);
|
||||||
@@ -397,28 +450,6 @@ authenticated.post(
|
|||||||
user.addUserRole
|
user.addUserRole
|
||||||
);
|
);
|
||||||
|
|
||||||
// authenticated.put(
|
|
||||||
// "/role/:roleId/site",
|
|
||||||
// verifyRoleAccess,
|
|
||||||
// verifyUserInRole,
|
|
||||||
// verifyUserHasAction(ActionsEnum.addRoleSite),
|
|
||||||
// role.addRoleSite
|
|
||||||
// );
|
|
||||||
// authenticated.delete(
|
|
||||||
// "/role/:roleId/site",
|
|
||||||
// verifyRoleAccess,
|
|
||||||
// verifyUserInRole,
|
|
||||||
// verifyUserHasAction(ActionsEnum.removeRoleSite),
|
|
||||||
// role.removeRoleSite
|
|
||||||
// );
|
|
||||||
// authenticated.get(
|
|
||||||
// "/role/:roleId/sites",
|
|
||||||
// verifyRoleAccess,
|
|
||||||
// verifyUserInRole,
|
|
||||||
// verifyUserHasAction(ActionsEnum.listRoleSites),
|
|
||||||
// role.listRoleSites
|
|
||||||
// );
|
|
||||||
|
|
||||||
authenticated.post(
|
authenticated.post(
|
||||||
"/resource/:resourceId/roles",
|
"/resource/:resourceId/roles",
|
||||||
verifyResourceAccess,
|
verifyResourceAccess,
|
||||||
@@ -463,13 +494,6 @@ authenticated.get(
|
|||||||
resource.getResourceWhitelist
|
resource.getResourceWhitelist
|
||||||
);
|
);
|
||||||
|
|
||||||
authenticated.post(
|
|
||||||
`/resource/:resourceId/transfer`,
|
|
||||||
verifyResourceAccess,
|
|
||||||
verifyUserHasAction(ActionsEnum.updateResource),
|
|
||||||
resource.transferResource
|
|
||||||
);
|
|
||||||
|
|
||||||
authenticated.post(
|
authenticated.post(
|
||||||
`/resource/:resourceId/access-token`,
|
`/resource/:resourceId/access-token`,
|
||||||
verifyResourceAccess,
|
verifyResourceAccess,
|
||||||
@@ -1033,6 +1057,7 @@ authRouter.post("/idp/:idpId/oidc/validate-callback", idp.validateOidcCallback);
|
|||||||
|
|
||||||
authRouter.put("/set-server-admin", auth.setServerAdmin);
|
authRouter.put("/set-server-admin", auth.setServerAdmin);
|
||||||
authRouter.get("/initial-setup-complete", auth.initialSetupComplete);
|
authRouter.get("/initial-setup-complete", auth.initialSetupComplete);
|
||||||
|
authRouter.post("/validate-setup-token", auth.validateSetupToken);
|
||||||
|
|
||||||
// Security Key routes
|
// Security Key routes
|
||||||
authRouter.post(
|
authRouter.post(
|
||||||
|
|||||||
@@ -48,7 +48,7 @@ export async function getAllRelays(
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Fetch exit node
|
// Fetch exit node
|
||||||
let [exitNode] = await db.select().from(exitNodes).where(eq(exitNodes.publicKey, publicKey));
|
const [exitNode] = await db.select().from(exitNodes).where(eq(exitNodes.publicKey, publicKey));
|
||||||
if (!exitNode) {
|
if (!exitNode) {
|
||||||
return next(createHttpError(HttpCode.NOT_FOUND, "Exit node not found"));
|
return next(createHttpError(HttpCode.NOT_FOUND, "Exit node not found"));
|
||||||
}
|
}
|
||||||
@@ -63,7 +63,7 @@ export async function getAllRelays(
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Initialize mappings object for multi-peer support
|
// Initialize mappings object for multi-peer support
|
||||||
let mappings: { [key: string]: ProxyMapping } = {};
|
const mappings: { [key: string]: ProxyMapping } = {};
|
||||||
|
|
||||||
// Process each site
|
// Process each site
|
||||||
for (const site of sitesRes) {
|
for (const site of sitesRes) {
|
||||||
@@ -78,19 +78,13 @@ export async function getAllRelays(
|
|||||||
.where(eq(clientSites.siteId, site.siteId));
|
.where(eq(clientSites.siteId, site.siteId));
|
||||||
|
|
||||||
for (const clientSite of clientSitesRes) {
|
for (const clientSite of clientSitesRes) {
|
||||||
// Get client information
|
if (!clientSite.endpoint) {
|
||||||
const [client] = await db
|
|
||||||
.select()
|
|
||||||
.from(clients)
|
|
||||||
.where(eq(clients.clientId, clientSite.clientId));
|
|
||||||
|
|
||||||
if (!client || !client.endpoint) {
|
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add this site as a destination for the client
|
// Add this site as a destination for the client
|
||||||
if (!mappings[client.endpoint]) {
|
if (!mappings[clientSite.endpoint]) {
|
||||||
mappings[client.endpoint] = { destinations: [] };
|
mappings[clientSite.endpoint] = { destinations: [] };
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add site as a destination for this client
|
// Add site as a destination for this client
|
||||||
@@ -100,13 +94,13 @@ export async function getAllRelays(
|
|||||||
};
|
};
|
||||||
|
|
||||||
// Check if this destination is already in the array to avoid duplicates
|
// Check if this destination is already in the array to avoid duplicates
|
||||||
const isDuplicate = mappings[client.endpoint].destinations.some(
|
const isDuplicate = mappings[clientSite.endpoint].destinations.some(
|
||||||
dest => dest.destinationIP === destination.destinationIP &&
|
dest => dest.destinationIP === destination.destinationIP &&
|
||||||
dest.destinationPort === destination.destinationPort
|
dest.destinationPort === destination.destinationPort
|
||||||
);
|
);
|
||||||
|
|
||||||
if (!isDuplicate) {
|
if (!isDuplicate) {
|
||||||
mappings[client.endpoint].destinations.push(destination);
|
mappings[clientSite.endpoint].destinations.push(destination);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -112,7 +112,7 @@ export async function getConfig(
|
|||||||
)
|
)
|
||||||
);
|
);
|
||||||
|
|
||||||
let peers = await Promise.all(
|
const peers = await Promise.all(
|
||||||
sitesRes.map(async (site) => {
|
sitesRes.map(async (site) => {
|
||||||
if (site.type === "wireguard") {
|
if (site.type === "wireguard") {
|
||||||
return {
|
return {
|
||||||
|
|||||||
@@ -1,15 +1,24 @@
|
|||||||
import axios from 'axios';
|
import axios from "axios";
|
||||||
import logger from '@server/logger';
|
import logger from "@server/logger";
|
||||||
import { db } from "@server/db";
|
import { db } from "@server/db";
|
||||||
import { exitNodes } from '@server/db';
|
import { exitNodes } from "@server/db";
|
||||||
import { eq } from 'drizzle-orm';
|
import { eq } from "drizzle-orm";
|
||||||
|
|
||||||
export async function addPeer(exitNodeId: number, peer: {
|
export async function addPeer(
|
||||||
publicKey: string;
|
exitNodeId: number,
|
||||||
allowedIps: string[];
|
peer: {
|
||||||
}) {
|
publicKey: string;
|
||||||
logger.info(`Adding peer with public key ${peer.publicKey} to exit node ${exitNodeId}`);
|
allowedIps: string[];
|
||||||
const [exitNode] = await db.select().from(exitNodes).where(eq(exitNodes.exitNodeId, exitNodeId)).limit(1);
|
}
|
||||||
|
) {
|
||||||
|
logger.info(
|
||||||
|
`Adding peer with public key ${peer.publicKey} to exit node ${exitNodeId}`
|
||||||
|
);
|
||||||
|
const [exitNode] = await db
|
||||||
|
.select()
|
||||||
|
.from(exitNodes)
|
||||||
|
.where(eq(exitNodes.exitNodeId, exitNodeId))
|
||||||
|
.limit(1);
|
||||||
if (!exitNode) {
|
if (!exitNode) {
|
||||||
throw new Error(`Exit node with ID ${exitNodeId} not found`);
|
throw new Error(`Exit node with ID ${exitNodeId} not found`);
|
||||||
}
|
}
|
||||||
@@ -18,25 +27,40 @@ export async function addPeer(exitNodeId: number, peer: {
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const response = await axios.post(`${exitNode.reachableAt}/peer`, peer, {
|
const response = await axios.post(
|
||||||
headers: {
|
`${exitNode.reachableAt}/peer`,
|
||||||
'Content-Type': 'application/json',
|
peer,
|
||||||
|
{
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
});
|
);
|
||||||
|
|
||||||
logger.info('Peer added successfully:', { peer: response.data.status });
|
logger.info("Peer added successfully:", { peer: response.data.status });
|
||||||
return response.data;
|
return response.data;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (axios.isAxiosError(error)) {
|
if (axios.isAxiosError(error)) {
|
||||||
throw new Error(`Error communicating with Gerbil. Make sure Pangolin can reach the Gerbil HTTP API: ${error.response?.status}`);
|
logger.error(
|
||||||
|
`Error adding peer (can Pangolin see Gerbil HTTP API?) for exit node at ${exitNode.reachableAt} (status: ${error.response?.status}): ${error.message}`
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
logger.error(
|
||||||
|
`Error adding peer for exit node at ${exitNode.reachableAt}: ${error}`
|
||||||
|
);
|
||||||
}
|
}
|
||||||
throw error;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function deletePeer(exitNodeId: number, publicKey: string) {
|
export async function deletePeer(exitNodeId: number, publicKey: string) {
|
||||||
logger.info(`Deleting peer with public key ${publicKey} from exit node ${exitNodeId}`);
|
logger.info(
|
||||||
const [exitNode] = await db.select().from(exitNodes).where(eq(exitNodes.exitNodeId, exitNodeId)).limit(1);
|
`Deleting peer with public key ${publicKey} from exit node ${exitNodeId}`
|
||||||
|
);
|
||||||
|
const [exitNode] = await db
|
||||||
|
.select()
|
||||||
|
.from(exitNodes)
|
||||||
|
.where(eq(exitNodes.exitNodeId, exitNodeId))
|
||||||
|
.limit(1);
|
||||||
if (!exitNode) {
|
if (!exitNode) {
|
||||||
throw new Error(`Exit node with ID ${exitNodeId} not found`);
|
throw new Error(`Exit node with ID ${exitNodeId} not found`);
|
||||||
}
|
}
|
||||||
@@ -44,13 +68,20 @@ export async function deletePeer(exitNodeId: number, publicKey: string) {
|
|||||||
throw new Error(`Exit node with ID ${exitNodeId} is not reachable`);
|
throw new Error(`Exit node with ID ${exitNodeId} is not reachable`);
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
const response = await axios.delete(`${exitNode.reachableAt}/peer?public_key=${encodeURIComponent(publicKey)}`);
|
const response = await axios.delete(
|
||||||
logger.info('Peer deleted successfully:', response.data.status);
|
`${exitNode.reachableAt}/peer?public_key=${encodeURIComponent(publicKey)}`
|
||||||
|
);
|
||||||
|
logger.info("Peer deleted successfully:", response.data.status);
|
||||||
return response.data;
|
return response.data;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (axios.isAxiosError(error)) {
|
if (axios.isAxiosError(error)) {
|
||||||
throw new Error(`Error communicating with Gerbil. Make sure Pangolin can reach the Gerbil HTTP API: ${error.response?.status}`);
|
logger.error(
|
||||||
|
`Error deleting peer (can Pangolin see Gerbil HTTP API?) for exit node at ${exitNode.reachableAt} (status: ${error.response?.status}): ${error.message}`
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
logger.error(
|
||||||
|
`Error deleting peer for exit node at ${exitNode.reachableAt}: ${error}`
|
||||||
|
);
|
||||||
}
|
}
|
||||||
throw error;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -31,7 +31,7 @@ export const receiveBandwidth = async (
|
|||||||
const currentTime = new Date();
|
const currentTime = new Date();
|
||||||
const oneMinuteAgo = new Date(currentTime.getTime() - 60000); // 1 minute ago
|
const oneMinuteAgo = new Date(currentTime.getTime() - 60000); // 1 minute ago
|
||||||
|
|
||||||
logger.debug(`Received data: ${JSON.stringify(bandwidthData)}`);
|
// logger.debug(`Received data: ${JSON.stringify(bandwidthData)}`);
|
||||||
|
|
||||||
await db.transaction(async (trx) => {
|
await db.transaction(async (trx) => {
|
||||||
// First, handle sites that are actively reporting bandwidth
|
// First, handle sites that are actively reporting bandwidth
|
||||||
|
|||||||
@@ -1,8 +1,17 @@
|
|||||||
import { Request, Response, NextFunction } from "express";
|
import { Request, Response, NextFunction } from "express";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
import { clients, newts, olms, Site, sites, clientSites, exitNodes } from "@server/db";
|
import {
|
||||||
|
clients,
|
||||||
|
newts,
|
||||||
|
olms,
|
||||||
|
Site,
|
||||||
|
sites,
|
||||||
|
clientSites,
|
||||||
|
exitNodes,
|
||||||
|
ExitNode
|
||||||
|
} from "@server/db";
|
||||||
import { db } from "@server/db";
|
import { db } from "@server/db";
|
||||||
import { eq } from "drizzle-orm";
|
import { eq, and } from "drizzle-orm";
|
||||||
import HttpCode from "@server/types/HttpCode";
|
import HttpCode from "@server/types/HttpCode";
|
||||||
import createHttpError from "http-errors";
|
import createHttpError from "http-errors";
|
||||||
import logger from "@server/logger";
|
import logger from "@server/logger";
|
||||||
@@ -19,7 +28,8 @@ const updateHolePunchSchema = z.object({
|
|||||||
ip: z.string(),
|
ip: z.string(),
|
||||||
port: z.number(),
|
port: z.number(),
|
||||||
timestamp: z.number(),
|
timestamp: z.number(),
|
||||||
reachableAt: z.string().optional()
|
reachableAt: z.string().optional(),
|
||||||
|
publicKey: z.string().optional()
|
||||||
});
|
});
|
||||||
|
|
||||||
// New response type with multi-peer destination support
|
// New response type with multi-peer destination support
|
||||||
@@ -45,13 +55,24 @@ export async function updateHolePunch(
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const { olmId, newtId, ip, port, timestamp, token, reachableAt } = parsedParams.data;
|
const {
|
||||||
|
olmId,
|
||||||
|
newtId,
|
||||||
|
ip,
|
||||||
|
port,
|
||||||
|
timestamp,
|
||||||
|
token,
|
||||||
|
reachableAt,
|
||||||
|
publicKey
|
||||||
|
} = parsedParams.data;
|
||||||
|
|
||||||
let currentSiteId: number | undefined;
|
let currentSiteId: number | undefined;
|
||||||
let destinations: PeerDestination[] = [];
|
let destinations: PeerDestination[] = [];
|
||||||
|
|
||||||
if (olmId) {
|
if (olmId) {
|
||||||
logger.debug(`Got hole punch with ip: ${ip}, port: ${port} for olmId: ${olmId}`);
|
logger.debug(
|
||||||
|
`Got hole punch with ip: ${ip}, port: ${port} for olmId: ${olmId}${publicKey ? ` with exit node publicKey: ${publicKey}` : ""}`
|
||||||
|
);
|
||||||
|
|
||||||
const { session, olm: olmSession } =
|
const { session, olm: olmSession } =
|
||||||
await validateOlmSessionToken(token);
|
await validateOlmSessionToken(token);
|
||||||
@@ -62,7 +83,9 @@ export async function updateHolePunch(
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (olmId !== olmSession.olmId) {
|
if (olmId !== olmSession.olmId) {
|
||||||
logger.warn(`Olm ID mismatch: ${olmId} !== ${olmSession.olmId}`);
|
logger.warn(
|
||||||
|
`Olm ID mismatch: ${olmId} !== ${olmSession.olmId}`
|
||||||
|
);
|
||||||
return next(
|
return next(
|
||||||
createHttpError(HttpCode.UNAUTHORIZED, "Unauthorized")
|
createHttpError(HttpCode.UNAUTHORIZED, "Unauthorized")
|
||||||
);
|
);
|
||||||
@@ -83,12 +106,64 @@ export async function updateHolePunch(
|
|||||||
const [client] = await db
|
const [client] = await db
|
||||||
.update(clients)
|
.update(clients)
|
||||||
.set({
|
.set({
|
||||||
endpoint: `${ip}:${port}`,
|
|
||||||
lastHolePunch: timestamp
|
lastHolePunch: timestamp
|
||||||
})
|
})
|
||||||
.where(eq(clients.clientId, olm.clientId))
|
.where(eq(clients.clientId, olm.clientId))
|
||||||
.returning();
|
.returning();
|
||||||
|
|
||||||
|
let exitNode: ExitNode | undefined;
|
||||||
|
if (publicKey) {
|
||||||
|
// Get the exit node by public key
|
||||||
|
[exitNode] = await db
|
||||||
|
.select()
|
||||||
|
.from(exitNodes)
|
||||||
|
.where(eq(exitNodes.publicKey, publicKey));
|
||||||
|
} else {
|
||||||
|
// FOR BACKWARDS COMPATIBILITY IF GERBIL IS STILL =<1.1.0
|
||||||
|
[exitNode] = await db.select().from(exitNodes).limit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!exitNode) {
|
||||||
|
logger.warn(`Exit node not found for publicKey: ${publicKey}`);
|
||||||
|
return next(
|
||||||
|
createHttpError(HttpCode.NOT_FOUND, "Exit node not found")
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get sites that are on this specific exit node and connected to this client
|
||||||
|
const sitesOnExitNode = await db
|
||||||
|
.select({ siteId: sites.siteId, subnet: sites.subnet, listenPort: sites.listenPort })
|
||||||
|
.from(sites)
|
||||||
|
.innerJoin(clientSites, eq(sites.siteId, clientSites.siteId))
|
||||||
|
.where(
|
||||||
|
and(
|
||||||
|
eq(sites.exitNodeId, exitNode.exitNodeId),
|
||||||
|
eq(clientSites.clientId, olm.clientId)
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
// Update clientSites for each site on this exit node
|
||||||
|
for (const site of sitesOnExitNode) {
|
||||||
|
logger.debug(
|
||||||
|
`Updating site ${site.siteId} on exit node with publicKey: ${publicKey}`
|
||||||
|
);
|
||||||
|
|
||||||
|
await db
|
||||||
|
.update(clientSites)
|
||||||
|
.set({
|
||||||
|
endpoint: `${ip}:${port}`
|
||||||
|
})
|
||||||
|
.where(
|
||||||
|
and(
|
||||||
|
eq(clientSites.clientId, olm.clientId),
|
||||||
|
eq(clientSites.siteId, site.siteId)
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.debug(
|
||||||
|
`Updated ${sitesOnExitNode.length} sites on exit node with publicKey: ${publicKey}`
|
||||||
|
);
|
||||||
if (!client) {
|
if (!client) {
|
||||||
logger.warn(`Client not found for olm: ${olmId}`);
|
logger.warn(`Client not found for olm: ${olmId}`);
|
||||||
return next(
|
return next(
|
||||||
@@ -96,128 +171,20 @@ export async function updateHolePunch(
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// // Get all sites that this client is connected to
|
// Create a list of the destinations from the sites
|
||||||
// const clientSitePairs = await db
|
for (const site of sitesOnExitNode) {
|
||||||
// .select()
|
if (site.subnet && site.listenPort) {
|
||||||
// .from(clientSites)
|
destinations.push({
|
||||||
// .where(eq(clientSites.clientId, client.clientId));
|
destinationIP: site.subnet.split("/")[0],
|
||||||
|
destinationPort: site.listenPort
|
||||||
// if (clientSitePairs.length === 0) {
|
|
||||||
// logger.warn(`No sites found for client: ${client.clientId}`);
|
|
||||||
// return next(
|
|
||||||
// createHttpError(HttpCode.NOT_FOUND, "No sites found for client")
|
|
||||||
// );
|
|
||||||
// }
|
|
||||||
|
|
||||||
// // Get all sites details
|
|
||||||
// const siteIds = clientSitePairs.map(pair => pair.siteId);
|
|
||||||
|
|
||||||
// for (const siteId of siteIds) {
|
|
||||||
// const [site] = await db
|
|
||||||
// .select()
|
|
||||||
// .from(sites)
|
|
||||||
// .where(eq(sites.siteId, siteId));
|
|
||||||
|
|
||||||
// if (site && site.subnet && site.listenPort) {
|
|
||||||
// destinations.push({
|
|
||||||
// destinationIP: site.subnet.split("/")[0],
|
|
||||||
// destinationPort: site.listenPort
|
|
||||||
// });
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
|
|
||||||
// get all sites for this client and join with exit nodes with site.exitNodeId
|
|
||||||
const sitesData = await db
|
|
||||||
.select()
|
|
||||||
.from(sites)
|
|
||||||
.innerJoin(clientSites, eq(sites.siteId, clientSites.siteId))
|
|
||||||
.leftJoin(exitNodes, eq(sites.exitNodeId, exitNodes.exitNodeId))
|
|
||||||
.where(eq(clientSites.clientId, client.clientId));
|
|
||||||
|
|
||||||
let exitNodeDestinations: {
|
|
||||||
reachableAt: string;
|
|
||||||
destinations: PeerDestination[];
|
|
||||||
}[] = [];
|
|
||||||
|
|
||||||
for (const site of sitesData) {
|
|
||||||
if (!site.sites.subnet) {
|
|
||||||
logger.warn(`Site ${site.sites.siteId} has no subnet, skipping`);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
// find the destinations in the array
|
|
||||||
let destinations = exitNodeDestinations.find(
|
|
||||||
(d) => d.reachableAt === site.exitNodes?.reachableAt
|
|
||||||
);
|
|
||||||
|
|
||||||
if (!destinations) {
|
|
||||||
destinations = {
|
|
||||||
reachableAt: site.exitNodes?.reachableAt || "",
|
|
||||||
destinations: [
|
|
||||||
{
|
|
||||||
destinationIP: site.sites.subnet.split("/")[0],
|
|
||||||
destinationPort: site.sites.listenPort || 0
|
|
||||||
}
|
|
||||||
]
|
|
||||||
};
|
|
||||||
} else {
|
|
||||||
// add to the existing destinations
|
|
||||||
destinations.destinations.push({
|
|
||||||
destinationIP: site.sites.subnet.split("/")[0],
|
|
||||||
destinationPort: site.sites.listenPort || 0
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// update it in the array
|
|
||||||
exitNodeDestinations = exitNodeDestinations.filter(
|
|
||||||
(d) => d.reachableAt !== site.exitNodes?.reachableAt
|
|
||||||
);
|
|
||||||
exitNodeDestinations.push(destinations);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
logger.debug(JSON.stringify(exitNodeDestinations, null, 2));
|
|
||||||
|
|
||||||
for (const destination of exitNodeDestinations) {
|
|
||||||
// if its the current exit node skip it because it is replying with the same data
|
|
||||||
if (reachableAt && destination.reachableAt == reachableAt) {
|
|
||||||
logger.debug(`Skipping update for reachableAt: ${reachableAt}`);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
const response = await axios.post(
|
|
||||||
`${destination.reachableAt}/update-destinations`,
|
|
||||||
{
|
|
||||||
sourceIp: client.endpoint?.split(":")[0] || "",
|
|
||||||
sourcePort: client.endpoint?.split(":")[1] || 0,
|
|
||||||
destinations: destination.destinations
|
|
||||||
},
|
|
||||||
{
|
|
||||||
headers: {
|
|
||||||
"Content-Type": "application/json"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
logger.info("Destinations updated:", {
|
|
||||||
peer: response.data.status
|
|
||||||
});
|
|
||||||
} catch (error) {
|
|
||||||
if (axios.isAxiosError(error)) {
|
|
||||||
throw new Error(
|
|
||||||
`Error communicating with Gerbil. Make sure Pangolin can reach the Gerbil HTTP API: ${error.response?.status}`
|
|
||||||
);
|
|
||||||
}
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Send the desinations back to the origin
|
|
||||||
destinations = exitNodeDestinations.find(
|
|
||||||
(d) => d.reachableAt === reachableAt
|
|
||||||
)?.destinations || [];
|
|
||||||
|
|
||||||
} else if (newtId) {
|
} else if (newtId) {
|
||||||
logger.debug(`Got hole punch with ip: ${ip}, port: ${port} for olmId: ${olmId}`);
|
logger.debug(
|
||||||
|
`Got hole punch with ip: ${ip}, port: ${port} for newtId: ${newtId}`
|
||||||
|
);
|
||||||
|
|
||||||
const { session, newt: newtSession } =
|
const { session, newt: newtSession } =
|
||||||
await validateNewtSessionToken(token);
|
await validateNewtSessionToken(token);
|
||||||
@@ -229,7 +196,9 @@ export async function updateHolePunch(
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (newtId !== newtSession.newtId) {
|
if (newtId !== newtSession.newtId) {
|
||||||
logger.warn(`Newt ID mismatch: ${newtId} !== ${newtSession.newtId}`);
|
logger.warn(
|
||||||
|
`Newt ID mismatch: ${newtId} !== ${newtSession.newtId}`
|
||||||
|
);
|
||||||
return next(
|
return next(
|
||||||
createHttpError(HttpCode.UNAUTHORIZED, "Unauthorized")
|
createHttpError(HttpCode.UNAUTHORIZED, "Unauthorized")
|
||||||
);
|
);
|
||||||
@@ -258,7 +227,7 @@ export async function updateHolePunch(
|
|||||||
})
|
})
|
||||||
.where(eq(sites.siteId, newt.siteId))
|
.where(eq(sites.siteId, newt.siteId))
|
||||||
.returning();
|
.returning();
|
||||||
|
|
||||||
if (!updatedSite || !updatedSite.subnet) {
|
if (!updatedSite || !updatedSite.subnet) {
|
||||||
logger.warn(`Site not found: ${newt.siteId}`);
|
logger.warn(`Site not found: ${newt.siteId}`);
|
||||||
return next(
|
return next(
|
||||||
@@ -271,7 +240,7 @@ export async function updateHolePunch(
|
|||||||
// .select()
|
// .select()
|
||||||
// .from(clientSites)
|
// .from(clientSites)
|
||||||
// .where(eq(clientSites.siteId, newt.siteId));
|
// .where(eq(clientSites.siteId, newt.siteId));
|
||||||
|
|
||||||
// THE NEWT IS NOT SENDING RAW WG TO THE GERBIL SO IDK IF WE REALLY NEED THIS - REMOVING
|
// THE NEWT IS NOT SENDING RAW WG TO THE GERBIL SO IDK IF WE REALLY NEED THIS - REMOVING
|
||||||
// Get client details for each client
|
// Get client details for each client
|
||||||
// for (const pair of sitesClientPairs) {
|
// for (const pair of sitesClientPairs) {
|
||||||
@@ -279,7 +248,7 @@ export async function updateHolePunch(
|
|||||||
// .select()
|
// .select()
|
||||||
// .from(clients)
|
// .from(clients)
|
||||||
// .where(eq(clients.clientId, pair.clientId));
|
// .where(eq(clients.clientId, pair.clientId));
|
||||||
|
|
||||||
// if (client && client.endpoint) {
|
// if (client && client.endpoint) {
|
||||||
// const [host, portStr] = client.endpoint.split(':');
|
// const [host, portStr] = client.endpoint.split(':');
|
||||||
// if (host && portStr) {
|
// if (host && portStr) {
|
||||||
@@ -290,27 +259,27 @@ export async function updateHolePunch(
|
|||||||
// }
|
// }
|
||||||
// }
|
// }
|
||||||
// }
|
// }
|
||||||
|
|
||||||
// If this is a newt/site, also add other sites in the same org
|
// If this is a newt/site, also add other sites in the same org
|
||||||
// if (updatedSite.orgId) {
|
// if (updatedSite.orgId) {
|
||||||
// const orgSites = await db
|
// const orgSites = await db
|
||||||
// .select()
|
// .select()
|
||||||
// .from(sites)
|
// .from(sites)
|
||||||
// .where(eq(sites.orgId, updatedSite.orgId));
|
// .where(eq(sites.orgId, updatedSite.orgId));
|
||||||
|
|
||||||
// for (const site of orgSites) {
|
// for (const site of orgSites) {
|
||||||
// // Don't add the current site to the destinations
|
// // Don't add the current site to the destinations
|
||||||
// if (site.siteId !== currentSiteId && site.subnet && site.endpoint && site.listenPort) {
|
// if (site.siteId !== currentSiteId && site.subnet && site.endpoint && site.listenPort) {
|
||||||
// const [host, portStr] = site.endpoint.split(':');
|
// const [host, portStr] = site.endpoint.split(':');
|
||||||
// if (host && portStr) {
|
// if (host && portStr) {
|
||||||
// destinations.push({
|
// destinations.push({
|
||||||
// destinationIP: host,
|
// destinationIP: host,
|
||||||
// destinationPort: site.listenPort
|
// destinationPort: site.listenPort
|
||||||
// });
|
// });
|
||||||
// }
|
// }
|
||||||
// }
|
// }
|
||||||
// }
|
// }
|
||||||
// }
|
// }
|
||||||
}
|
}
|
||||||
|
|
||||||
// if (destinations.length === 0) {
|
// if (destinations.length === 0) {
|
||||||
@@ -320,6 +289,10 @@ export async function updateHolePunch(
|
|||||||
// return next(createHttpError(HttpCode.NOT_FOUND, "No peer destinations found"));
|
// return next(createHttpError(HttpCode.NOT_FOUND, "No peer destinations found"));
|
||||||
// }
|
// }
|
||||||
|
|
||||||
|
logger.debug(
|
||||||
|
`Returning ${destinations.length} peer destinations for olmId: ${olmId} or newtId: ${newtId}: ${JSON.stringify(destinations, null, 2)}`
|
||||||
|
);
|
||||||
|
|
||||||
// Return the new multi-peer structure
|
// Return the new multi-peer structure
|
||||||
return res.status(HttpCode.OK).send({
|
return res.status(HttpCode.OK).send({
|
||||||
destinations: destinations
|
destinations: destinations
|
||||||
@@ -333,4 +306,4 @@ export async function updateHolePunch(
|
|||||||
)
|
)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -68,7 +68,7 @@ export async function createOidcIdp(
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
let {
|
const {
|
||||||
clientId,
|
clientId,
|
||||||
clientSecret,
|
clientSecret,
|
||||||
authUrl,
|
authUrl,
|
||||||
|
|||||||
@@ -85,7 +85,7 @@ export async function updateOidcIdp(
|
|||||||
}
|
}
|
||||||
|
|
||||||
const { idpId } = parsedParams.data;
|
const { idpId } = parsedParams.data;
|
||||||
let {
|
const {
|
||||||
clientId,
|
clientId,
|
||||||
clientSecret,
|
clientSecret,
|
||||||
authUrl,
|
authUrl,
|
||||||
|
|||||||
@@ -238,7 +238,7 @@ export async function validateOidcCallback(
|
|||||||
const defaultRoleMapping = existingIdp.idp.defaultRoleMapping;
|
const defaultRoleMapping = existingIdp.idp.defaultRoleMapping;
|
||||||
const defaultOrgMapping = existingIdp.idp.defaultOrgMapping;
|
const defaultOrgMapping = existingIdp.idp.defaultOrgMapping;
|
||||||
|
|
||||||
let userOrgInfo: { orgId: string; roleId: number }[] = [];
|
const userOrgInfo: { orgId: string; roleId: number }[] = [];
|
||||||
for (const org of allOrgs) {
|
for (const org of allOrgs) {
|
||||||
const [idpOrgRes] = await db
|
const [idpOrgRes] = await db
|
||||||
.select()
|
.select()
|
||||||
@@ -314,7 +314,7 @@ export async function validateOidcCallback(
|
|||||||
|
|
||||||
let existingUserId = existingUser?.userId;
|
let existingUserId = existingUser?.userId;
|
||||||
|
|
||||||
let orgUserCounts: { orgId: string; userCount: number }[] = [];
|
const orgUserCounts: { orgId: string; userCount: number }[] = [];
|
||||||
|
|
||||||
// sync the user with the orgs and roles
|
// sync the user with the orgs and roles
|
||||||
await db.transaction(async (trx) => {
|
await db.transaction(async (trx) => {
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import * as domain from "./domain";
|
|||||||
import * as target from "./target";
|
import * as target from "./target";
|
||||||
import * as user from "./user";
|
import * as user from "./user";
|
||||||
import * as role from "./role";
|
import * as role from "./role";
|
||||||
// import * as client from "./client";
|
import * as client from "./client";
|
||||||
import * as accessToken from "./accessToken";
|
import * as accessToken from "./accessToken";
|
||||||
import * as apiKeys from "./apiKeys";
|
import * as apiKeys from "./apiKeys";
|
||||||
import * as idp from "./idp";
|
import * as idp from "./idp";
|
||||||
@@ -20,7 +20,9 @@ import {
|
|||||||
verifyApiKeyUserAccess,
|
verifyApiKeyUserAccess,
|
||||||
verifyApiKeySetResourceUsers,
|
verifyApiKeySetResourceUsers,
|
||||||
verifyApiKeyAccessTokenAccess,
|
verifyApiKeyAccessTokenAccess,
|
||||||
verifyApiKeyIsRoot
|
verifyApiKeyIsRoot,
|
||||||
|
verifyApiKeyClientAccess,
|
||||||
|
verifyClientsEnabled
|
||||||
} from "@server/middlewares";
|
} from "@server/middlewares";
|
||||||
import HttpCode from "@server/types/HttpCode";
|
import HttpCode from "@server/types/HttpCode";
|
||||||
import { Router } from "express";
|
import { Router } from "express";
|
||||||
@@ -339,13 +341,6 @@ authenticated.get(
|
|||||||
resource.getResourceWhitelist
|
resource.getResourceWhitelist
|
||||||
);
|
);
|
||||||
|
|
||||||
authenticated.post(
|
|
||||||
`/resource/:resourceId/transfer`,
|
|
||||||
verifyApiKeyResourceAccess,
|
|
||||||
verifyApiKeyHasAction(ActionsEnum.updateResource),
|
|
||||||
resource.transferResource
|
|
||||||
);
|
|
||||||
|
|
||||||
authenticated.post(
|
authenticated.post(
|
||||||
`/resource/:resourceId/access-token`,
|
`/resource/:resourceId/access-token`,
|
||||||
verifyApiKeyResourceAccess,
|
verifyApiKeyResourceAccess,
|
||||||
@@ -513,3 +508,51 @@ authenticated.get(
|
|||||||
verifyApiKeyHasAction(ActionsEnum.listIdpOrgs),
|
verifyApiKeyHasAction(ActionsEnum.listIdpOrgs),
|
||||||
idp.listIdpOrgPolicies
|
idp.listIdpOrgPolicies
|
||||||
);
|
);
|
||||||
|
|
||||||
|
authenticated.get(
|
||||||
|
"/org/:orgId/pick-client-defaults",
|
||||||
|
verifyClientsEnabled,
|
||||||
|
verifyApiKeyOrgAccess,
|
||||||
|
verifyApiKeyHasAction(ActionsEnum.createClient),
|
||||||
|
client.pickClientDefaults
|
||||||
|
);
|
||||||
|
|
||||||
|
authenticated.get(
|
||||||
|
"/org/:orgId/clients",
|
||||||
|
verifyClientsEnabled,
|
||||||
|
verifyApiKeyOrgAccess,
|
||||||
|
verifyApiKeyHasAction(ActionsEnum.listClients),
|
||||||
|
client.listClients
|
||||||
|
);
|
||||||
|
|
||||||
|
authenticated.get(
|
||||||
|
"/org/:orgId/client/:clientId",
|
||||||
|
verifyClientsEnabled,
|
||||||
|
verifyApiKeyOrgAccess,
|
||||||
|
verifyApiKeyHasAction(ActionsEnum.getClient),
|
||||||
|
client.getClient
|
||||||
|
);
|
||||||
|
|
||||||
|
authenticated.put(
|
||||||
|
"/org/:orgId/client",
|
||||||
|
verifyClientsEnabled,
|
||||||
|
verifyApiKeyOrgAccess,
|
||||||
|
verifyApiKeyHasAction(ActionsEnum.createClient),
|
||||||
|
client.createClient
|
||||||
|
);
|
||||||
|
|
||||||
|
authenticated.delete(
|
||||||
|
"/client/:clientId",
|
||||||
|
verifyClientsEnabled,
|
||||||
|
verifyApiKeyClientAccess,
|
||||||
|
verifyApiKeyHasAction(ActionsEnum.deleteClient),
|
||||||
|
client.deleteClient
|
||||||
|
);
|
||||||
|
|
||||||
|
authenticated.post(
|
||||||
|
"/client/:clientId",
|
||||||
|
verifyClientsEnabled,
|
||||||
|
verifyApiKeyClientAccess,
|
||||||
|
verifyApiKeyHasAction(ActionsEnum.updateClient),
|
||||||
|
client.updateClient
|
||||||
|
);
|
||||||
|
|||||||
@@ -102,7 +102,7 @@ export const handleGetConfigMessage: MessageHandler = async (context) => {
|
|||||||
.from(exitNodes)
|
.from(exitNodes)
|
||||||
.where(eq(exitNodes.exitNodeId, site.exitNodeId))
|
.where(eq(exitNodes.exitNodeId, site.exitNodeId))
|
||||||
.limit(1);
|
.limit(1);
|
||||||
if (exitNode.reachableAt) {
|
if (exitNode.reachableAt && existingSite.subnet && existingSite.listenPort) {
|
||||||
try {
|
try {
|
||||||
const response = await axios.post(
|
const response = await axios.post(
|
||||||
`${exitNode.reachableAt}/update-proxy-mapping`,
|
`${exitNode.reachableAt}/update-proxy-mapping`,
|
||||||
@@ -128,11 +128,14 @@ export const handleGetConfigMessage: MessageHandler = async (context) => {
|
|||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (axios.isAxiosError(error)) {
|
if (axios.isAxiosError(error)) {
|
||||||
throw new Error(
|
logger.error(
|
||||||
`Error communicating with Gerbil. Make sure Pangolin can reach the Gerbil HTTP API: ${error.response?.status}`
|
`Error updating proxy mapping (can Pangolin see Gerbil HTTP API?) for exit node at ${exitNode.reachableAt} (status: ${error.response?.status}): ${error.message}`
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
logger.error(
|
||||||
|
`Error updating proxy mapping for exit node at ${exitNode.reachableAt}: ${error}`
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
throw error;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -154,9 +157,6 @@ export const handleGetConfigMessage: MessageHandler = async (context) => {
|
|||||||
if (!client.clients.subnet) {
|
if (!client.clients.subnet) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
if (!client.clients.endpoint) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
return true;
|
return true;
|
||||||
})
|
})
|
||||||
.map(async (client) => {
|
.map(async (client) => {
|
||||||
@@ -212,7 +212,7 @@ export const handleGetConfigMessage: MessageHandler = async (context) => {
|
|||||||
allowedIps: [`${client.clients.subnet.split("/")[0]}/32`], // we want to only allow from that client
|
allowedIps: [`${client.clients.subnet.split("/")[0]}/32`], // we want to only allow from that client
|
||||||
endpoint: client.clientSites.isRelayed
|
endpoint: client.clientSites.isRelayed
|
||||||
? ""
|
? ""
|
||||||
: client.clients.endpoint! // if its relayed it should be localhost
|
: client.clientSites.endpoint! // if its relayed it should be localhost
|
||||||
};
|
};
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
@@ -220,78 +220,37 @@ export const handleGetConfigMessage: MessageHandler = async (context) => {
|
|||||||
// Filter out any null values from peers that didn't have an olm
|
// Filter out any null values from peers that didn't have an olm
|
||||||
const validPeers = peers.filter((peer) => peer !== null);
|
const validPeers = peers.filter((peer) => peer !== null);
|
||||||
|
|
||||||
// Improved version
|
// Get all enabled targets with their resource protocol information
|
||||||
const allResources = await db.transaction(async (tx) => {
|
const allTargets = await db
|
||||||
// First get all resources for the site
|
.select({
|
||||||
const resourcesList = await tx
|
resourceId: targets.resourceId,
|
||||||
.select({
|
targetId: targets.targetId,
|
||||||
resourceId: resources.resourceId,
|
ip: targets.ip,
|
||||||
subdomain: resources.subdomain,
|
method: targets.method,
|
||||||
fullDomain: resources.fullDomain,
|
port: targets.port,
|
||||||
ssl: resources.ssl,
|
internalPort: targets.internalPort,
|
||||||
blockAccess: resources.blockAccess,
|
enabled: targets.enabled,
|
||||||
sso: resources.sso,
|
protocol: resources.protocol
|
||||||
emailWhitelistEnabled: resources.emailWhitelistEnabled,
|
})
|
||||||
http: resources.http,
|
.from(targets)
|
||||||
proxyPort: resources.proxyPort,
|
.innerJoin(resources, eq(targets.resourceId, resources.resourceId))
|
||||||
protocol: resources.protocol
|
.where(and(eq(targets.siteId, siteId), eq(targets.enabled, true)));
|
||||||
})
|
|
||||||
.from(resources)
|
|
||||||
.where(and(eq(resources.siteId, siteId), eq(resources.http, false)));
|
|
||||||
|
|
||||||
// Get all enabled targets for these resources in a single query
|
const { tcpTargets, udpTargets } = allTargets.reduce(
|
||||||
const resourceIds = resourcesList.map((r) => r.resourceId);
|
(acc, target) => {
|
||||||
const allTargets =
|
// Filter out invalid targets
|
||||||
resourceIds.length > 0
|
if (!target.internalPort || !target.ip || !target.port) {
|
||||||
? await tx
|
return acc;
|
||||||
.select({
|
}
|
||||||
resourceId: targets.resourceId,
|
|
||||||
targetId: targets.targetId,
|
|
||||||
ip: targets.ip,
|
|
||||||
method: targets.method,
|
|
||||||
port: targets.port,
|
|
||||||
internalPort: targets.internalPort,
|
|
||||||
enabled: targets.enabled,
|
|
||||||
})
|
|
||||||
.from(targets)
|
|
||||||
.where(
|
|
||||||
and(
|
|
||||||
inArray(targets.resourceId, resourceIds),
|
|
||||||
eq(targets.enabled, true)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
: [];
|
|
||||||
|
|
||||||
// Combine the data in JS instead of using SQL for the JSON
|
// Format target into string
|
||||||
return resourcesList.map((resource) => ({
|
const formattedTarget = `${target.internalPort}:${target.ip}:${target.port}`;
|
||||||
...resource,
|
|
||||||
targets: allTargets.filter(
|
|
||||||
(target) => target.resourceId === resource.resourceId
|
|
||||||
)
|
|
||||||
}));
|
|
||||||
});
|
|
||||||
|
|
||||||
const { tcpTargets, udpTargets } = allResources.reduce(
|
|
||||||
(acc, resource) => {
|
|
||||||
// Skip resources with no targets
|
|
||||||
if (!resource.targets?.length) return acc;
|
|
||||||
|
|
||||||
// Format valid targets into strings
|
|
||||||
const formattedTargets = resource.targets
|
|
||||||
.filter(
|
|
||||||
(target: Target) =>
|
|
||||||
resource.proxyPort && target?.ip && target?.port
|
|
||||||
)
|
|
||||||
.map(
|
|
||||||
(target: Target) =>
|
|
||||||
`${resource.proxyPort}:${target.ip}:${target.port}`
|
|
||||||
);
|
|
||||||
|
|
||||||
// Add to the appropriate protocol array
|
// Add to the appropriate protocol array
|
||||||
if (resource.protocol === "tcp") {
|
if (target.protocol === "tcp") {
|
||||||
acc.tcpTargets.push(...formattedTargets);
|
acc.tcpTargets.push(formattedTarget);
|
||||||
} else {
|
} else {
|
||||||
acc.udpTargets.push(...formattedTargets);
|
acc.udpTargets.push(formattedTarget);
|
||||||
}
|
}
|
||||||
|
|
||||||
return acc;
|
return acc;
|
||||||
|
|||||||
@@ -55,7 +55,7 @@ export const handleNewtPingRequestMessage: MessageHandler = async (context) => {
|
|||||||
);
|
);
|
||||||
|
|
||||||
if (currentConnections.count >= maxConnections) {
|
if (currentConnections.count >= maxConnections) {
|
||||||
return null
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
weight =
|
weight =
|
||||||
|
|||||||
@@ -105,7 +105,9 @@ export const handleNewtRegisterMessage: MessageHandler = async (context) => {
|
|||||||
.limit(1);
|
.limit(1);
|
||||||
|
|
||||||
const blockSize = config.getRawConfig().gerbil.site_block_size;
|
const blockSize = config.getRawConfig().gerbil.site_block_size;
|
||||||
const subnets = sitesQuery.map((site) => site.subnet).filter((subnet) => subnet !== null);
|
const subnets = sitesQuery
|
||||||
|
.map((site) => site.subnet)
|
||||||
|
.filter((subnet) => subnet !== null);
|
||||||
subnets.push(exitNode.address.replace(/\/\d+$/, `/${blockSize}`));
|
subnets.push(exitNode.address.replace(/\/\d+$/, `/${blockSize}`));
|
||||||
const newSubnet = findNextAvailableCidr(
|
const newSubnet = findNextAvailableCidr(
|
||||||
subnets,
|
subnets,
|
||||||
@@ -160,78 +162,37 @@ export const handleNewtRegisterMessage: MessageHandler = async (context) => {
|
|||||||
allowedIps: [siteSubnet]
|
allowedIps: [siteSubnet]
|
||||||
});
|
});
|
||||||
|
|
||||||
// Improved version
|
// Get all enabled targets with their resource protocol information
|
||||||
const allResources = await db.transaction(async (tx) => {
|
const allTargets = await db
|
||||||
// First get all resources for the site
|
.select({
|
||||||
const resourcesList = await tx
|
resourceId: targets.resourceId,
|
||||||
.select({
|
targetId: targets.targetId,
|
||||||
resourceId: resources.resourceId,
|
ip: targets.ip,
|
||||||
subdomain: resources.subdomain,
|
method: targets.method,
|
||||||
fullDomain: resources.fullDomain,
|
port: targets.port,
|
||||||
ssl: resources.ssl,
|
internalPort: targets.internalPort,
|
||||||
blockAccess: resources.blockAccess,
|
enabled: targets.enabled,
|
||||||
sso: resources.sso,
|
protocol: resources.protocol
|
||||||
emailWhitelistEnabled: resources.emailWhitelistEnabled,
|
})
|
||||||
http: resources.http,
|
.from(targets)
|
||||||
proxyPort: resources.proxyPort,
|
.innerJoin(resources, eq(targets.resourceId, resources.resourceId))
|
||||||
protocol: resources.protocol
|
.where(and(eq(targets.siteId, siteId), eq(targets.enabled, true)));
|
||||||
})
|
|
||||||
.from(resources)
|
|
||||||
.where(eq(resources.siteId, siteId));
|
|
||||||
|
|
||||||
// Get all enabled targets for these resources in a single query
|
const { tcpTargets, udpTargets } = allTargets.reduce(
|
||||||
const resourceIds = resourcesList.map((r) => r.resourceId);
|
(acc, target) => {
|
||||||
const allTargets =
|
// Filter out invalid targets
|
||||||
resourceIds.length > 0
|
if (!target.internalPort || !target.ip || !target.port) {
|
||||||
? await tx
|
return acc;
|
||||||
.select({
|
}
|
||||||
resourceId: targets.resourceId,
|
|
||||||
targetId: targets.targetId,
|
|
||||||
ip: targets.ip,
|
|
||||||
method: targets.method,
|
|
||||||
port: targets.port,
|
|
||||||
internalPort: targets.internalPort,
|
|
||||||
enabled: targets.enabled
|
|
||||||
})
|
|
||||||
.from(targets)
|
|
||||||
.where(
|
|
||||||
and(
|
|
||||||
inArray(targets.resourceId, resourceIds),
|
|
||||||
eq(targets.enabled, true)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
: [];
|
|
||||||
|
|
||||||
// Combine the data in JS instead of using SQL for the JSON
|
// Format target into string
|
||||||
return resourcesList.map((resource) => ({
|
const formattedTarget = `${target.internalPort}:${target.ip}:${target.port}`;
|
||||||
...resource,
|
|
||||||
targets: allTargets.filter(
|
|
||||||
(target) => target.resourceId === resource.resourceId
|
|
||||||
)
|
|
||||||
}));
|
|
||||||
});
|
|
||||||
|
|
||||||
const { tcpTargets, udpTargets } = allResources.reduce(
|
|
||||||
(acc, resource) => {
|
|
||||||
// Skip resources with no targets
|
|
||||||
if (!resource.targets?.length) return acc;
|
|
||||||
|
|
||||||
// Format valid targets into strings
|
|
||||||
const formattedTargets = resource.targets
|
|
||||||
.filter(
|
|
||||||
(target: Target) =>
|
|
||||||
target?.internalPort && target?.ip && target?.port
|
|
||||||
)
|
|
||||||
.map(
|
|
||||||
(target: Target) =>
|
|
||||||
`${target.internalPort}:${target.ip}:${target.port}`
|
|
||||||
);
|
|
||||||
|
|
||||||
// Add to the appropriate protocol array
|
// Add to the appropriate protocol array
|
||||||
if (resource.protocol === "tcp") {
|
if (target.protocol === "tcp") {
|
||||||
acc.tcpTargets.push(...formattedTargets);
|
acc.tcpTargets.push(formattedTarget);
|
||||||
} else {
|
} else {
|
||||||
acc.udpTargets.push(...formattedTargets);
|
acc.udpTargets.push(formattedTarget);
|
||||||
}
|
}
|
||||||
|
|
||||||
return acc;
|
return acc;
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
import { Target } from "@server/db";
|
import { Target } from "@server/db";
|
||||||
import { sendToClient } from "../ws";
|
import { sendToClient } from "../ws";
|
||||||
|
import logger from "@server/logger";
|
||||||
|
|
||||||
export function addTargets(
|
export async function addTargets(
|
||||||
newtId: string,
|
newtId: string,
|
||||||
targets: Target[],
|
targets: Target[],
|
||||||
protocol: string,
|
protocol: string,
|
||||||
@@ -20,22 +21,9 @@ export function addTargets(
|
|||||||
targets: payloadTargets
|
targets: payloadTargets
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
const payloadTargetsResources = targets.map((target) => {
|
|
||||||
return `${port ? port + ":" : ""}${
|
|
||||||
target.ip
|
|
||||||
}:${target.port}`;
|
|
||||||
});
|
|
||||||
|
|
||||||
sendToClient(newtId, {
|
|
||||||
type: `newt/wg/${protocol}/add`,
|
|
||||||
data: {
|
|
||||||
targets: [payloadTargetsResources[0]] // We can only use one target for WireGuard right now
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function removeTargets(
|
export async function removeTargets(
|
||||||
newtId: string,
|
newtId: string,
|
||||||
targets: Target[],
|
targets: Target[],
|
||||||
protocol: string,
|
protocol: string,
|
||||||
@@ -48,23 +36,10 @@ export function removeTargets(
|
|||||||
}:${target.port}`;
|
}:${target.port}`;
|
||||||
});
|
});
|
||||||
|
|
||||||
sendToClient(newtId, {
|
await sendToClient(newtId, {
|
||||||
type: `newt/${protocol}/remove`,
|
type: `newt/${protocol}/remove`,
|
||||||
data: {
|
data: {
|
||||||
targets: payloadTargets
|
targets: payloadTargets
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
const payloadTargetsResources = targets.map((target) => {
|
|
||||||
return `${port ? port + ":" : ""}${
|
|
||||||
target.ip
|
|
||||||
}:${target.port}`;
|
|
||||||
});
|
|
||||||
|
|
||||||
sendToClient(newtId, {
|
|
||||||
type: `newt/wg/${protocol}/remove`,
|
|
||||||
data: {
|
|
||||||
targets: [payloadTargetsResources[0]] // We can only use one target for WireGuard right now
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -28,7 +28,7 @@ export const startOfflineChecker = (): void => {
|
|||||||
.set({ online: false })
|
.set({ online: false })
|
||||||
.where(
|
.where(
|
||||||
eq(clients.online, true) &&
|
eq(clients.online, true) &&
|
||||||
(lt(clients.lastPing, twoMinutesAgo.toISOString()) || isNull(clients.lastPing))
|
(lt(clients.lastPing, twoMinutesAgo.getTime() / 1000) || isNull(clients.lastPing))
|
||||||
);
|
);
|
||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -37,7 +37,7 @@ export const startOfflineChecker = (): void => {
|
|||||||
}, OFFLINE_CHECK_INTERVAL);
|
}, OFFLINE_CHECK_INTERVAL);
|
||||||
|
|
||||||
logger.info("Started offline checker interval");
|
logger.info("Started offline checker interval");
|
||||||
}
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Stops the background interval that checks for offline clients
|
* Stops the background interval that checks for offline clients
|
||||||
@@ -48,7 +48,7 @@ export const stopOfflineChecker = (): void => {
|
|||||||
offlineCheckerInterval = null;
|
offlineCheckerInterval = null;
|
||||||
logger.info("Stopped offline checker interval");
|
logger.info("Stopped offline checker interval");
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Handles ping messages from clients and responds with pong
|
* Handles ping messages from clients and responds with pong
|
||||||
@@ -72,7 +72,7 @@ export const handleOlmPingMessage: MessageHandler = async (context) => {
|
|||||||
await db
|
await db
|
||||||
.update(clients)
|
.update(clients)
|
||||||
.set({
|
.set({
|
||||||
lastPing: new Date().toISOString(),
|
lastPing: new Date().getTime() / 1000,
|
||||||
online: true,
|
online: true,
|
||||||
})
|
})
|
||||||
.where(eq(clients.clientId, olm.clientId));
|
.where(eq(clients.clientId, olm.clientId));
|
||||||
|
|||||||
@@ -1,14 +1,7 @@
|
|||||||
import { db, ExitNode } from "@server/db";
|
import { db, ExitNode } from "@server/db";
|
||||||
import { MessageHandler } from "../ws";
|
import { MessageHandler } from "../ws";
|
||||||
import {
|
import { clients, clientSites, exitNodes, Olm, olms, sites } from "@server/db";
|
||||||
clients,
|
import { and, eq, inArray } from "drizzle-orm";
|
||||||
clientSites,
|
|
||||||
exitNodes,
|
|
||||||
Olm,
|
|
||||||
olms,
|
|
||||||
sites
|
|
||||||
} from "@server/db";
|
|
||||||
import { eq, inArray } from "drizzle-orm";
|
|
||||||
import { addPeer, deletePeer } from "../newt/peers";
|
import { addPeer, deletePeer } from "../newt/peers";
|
||||||
import logger from "@server/logger";
|
import logger from "@server/logger";
|
||||||
|
|
||||||
@@ -28,9 +21,11 @@ export const handleOlmRegisterMessage: MessageHandler = async (context) => {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const clientId = olm.clientId;
|
const clientId = olm.clientId;
|
||||||
const { publicKey, relay } = message.data;
|
const { publicKey, relay, olmVersion } = message.data;
|
||||||
|
|
||||||
logger.debug(`Olm client ID: ${clientId}, Public Key: ${publicKey}, Relay: ${relay}`);
|
logger.debug(
|
||||||
|
`Olm client ID: ${clientId}, Public Key: ${publicKey}, Relay: ${relay}`
|
||||||
|
);
|
||||||
|
|
||||||
if (!publicKey) {
|
if (!publicKey) {
|
||||||
logger.warn("Public key not provided");
|
logger.warn("Public key not provided");
|
||||||
@@ -50,22 +45,46 @@ export const handleOlmRegisterMessage: MessageHandler = async (context) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (client.exitNodeId) {
|
if (client.exitNodeId) {
|
||||||
// Get the exit node for this site
|
// TODO: FOR NOW WE ARE JUST HOLEPUNCHING ALL EXIT NODES BUT IN THE FUTURE WE SHOULD HANDLE THIS BETTER
|
||||||
const [exitNode] = await db
|
|
||||||
.select()
|
|
||||||
.from(exitNodes)
|
|
||||||
.where(eq(exitNodes.exitNodeId, client.exitNodeId))
|
|
||||||
.limit(1);
|
|
||||||
|
|
||||||
// Send holepunch message for each site
|
// Get the exit node
|
||||||
sendToClient(olm.olmId, {
|
const allExitNodes = await db.select().from(exitNodes);
|
||||||
type: "olm/wg/holepunch",
|
|
||||||
|
const exitNodesHpData = allExitNodes.map((exitNode: ExitNode) => {
|
||||||
|
return {
|
||||||
|
publicKey: exitNode.publicKey,
|
||||||
|
endpoint: exitNode.endpoint
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
// Send holepunch message
|
||||||
|
await sendToClient(olm.olmId, {
|
||||||
|
type: "olm/wg/holepunch/all",
|
||||||
data: {
|
data: {
|
||||||
serverPubKey: exitNode.publicKey,
|
exitNodes: exitNodesHpData
|
||||||
endpoint: exitNode.endpoint,
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
if (!olmVersion) {
|
||||||
|
// THIS IS FOR BACKWARDS COMPATIBILITY
|
||||||
|
// THE OLDER CLIENTS DID NOT SEND THE VERSION
|
||||||
|
await sendToClient(olm.olmId, {
|
||||||
|
type: "olm/wg/holepunch",
|
||||||
|
data: {
|
||||||
|
serverPubKey: allExitNodes[0].publicKey,
|
||||||
|
endpoint: allExitNodes[0].endpoint
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (olmVersion) {
|
||||||
|
await db
|
||||||
|
.update(olms)
|
||||||
|
.set({
|
||||||
|
version: olmVersion
|
||||||
|
})
|
||||||
|
.where(eq(olms.olmId, olm.olmId));
|
||||||
}
|
}
|
||||||
|
|
||||||
if (now - (client.lastHolePunch || 0) > 6) {
|
if (now - (client.lastHolePunch || 0) > 6) {
|
||||||
@@ -103,7 +122,9 @@ export const handleOlmRegisterMessage: MessageHandler = async (context) => {
|
|||||||
|
|
||||||
// Prepare an array to store site configurations
|
// Prepare an array to store site configurations
|
||||||
let siteConfigurations = [];
|
let siteConfigurations = [];
|
||||||
logger.debug(`Found ${sitesData.length} sites for client ${client.clientId}`);
|
logger.debug(
|
||||||
|
`Found ${sitesData.length} sites for client ${client.clientId}`
|
||||||
|
);
|
||||||
|
|
||||||
if (sitesData.length === 0) {
|
if (sitesData.length === 0) {
|
||||||
sendToClient(olm.olmId, {
|
sendToClient(olm.olmId, {
|
||||||
@@ -147,15 +168,26 @@ export const handleOlmRegisterMessage: MessageHandler = async (context) => {
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const [clientSite] = await db
|
||||||
|
.select()
|
||||||
|
.from(clientSites)
|
||||||
|
.where(
|
||||||
|
and(
|
||||||
|
eq(clientSites.clientId, client.clientId),
|
||||||
|
eq(clientSites.siteId, site.siteId)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
.limit(1);
|
||||||
|
|
||||||
// Add the peer to the exit node for this site
|
// Add the peer to the exit node for this site
|
||||||
if (client.endpoint) {
|
if (clientSite.endpoint) {
|
||||||
logger.info(
|
logger.info(
|
||||||
`Adding peer ${publicKey} to site ${site.siteId} with endpoint ${client.endpoint}`
|
`Adding peer ${publicKey} to site ${site.siteId} with endpoint ${clientSite.endpoint}`
|
||||||
);
|
);
|
||||||
await addPeer(site.siteId, {
|
await addPeer(site.siteId, {
|
||||||
publicKey: publicKey,
|
publicKey: publicKey,
|
||||||
allowedIps: [`${client.subnet.split('/')[0]}/32`], // we want to only allow from that client
|
allowedIps: [`${client.subnet.split("/")[0]}/32`], // we want to only allow from that client
|
||||||
endpoint: relay ? "" : client.endpoint
|
endpoint: relay ? "" : clientSite.endpoint
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
logger.warn(
|
logger.warn(
|
||||||
@@ -188,7 +220,7 @@ export const handleOlmRegisterMessage: MessageHandler = async (context) => {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// REMOVED THIS SO IT CREATES THE INTERFACE AND JUST WAITS FOR THE SITES
|
// REMOVED THIS SO IT CREATES THE INTERFACE AND JUST WAITS FOR THE SITES
|
||||||
// if (siteConfigurations.length === 0) {
|
// if (siteConfigurations.length === 0) {
|
||||||
// logger.warn("No valid site configurations found");
|
// logger.warn("No valid site configurations found");
|
||||||
// return;
|
// return;
|
||||||
|
|||||||
@@ -136,7 +136,8 @@ export async function createOrg(
|
|||||||
.values({
|
.values({
|
||||||
orgId,
|
orgId,
|
||||||
name,
|
name,
|
||||||
subnet
|
subnet,
|
||||||
|
createdAt: new Date().toISOString()
|
||||||
})
|
})
|
||||||
.returning();
|
.returning();
|
||||||
|
|
||||||
@@ -214,7 +215,7 @@ export async function createOrg(
|
|||||||
orgId: newOrg[0].orgId,
|
orgId: newOrg[0].orgId,
|
||||||
roleId: roleId,
|
roleId: roleId,
|
||||||
isOwner: true
|
isOwner: true
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
const memberRole = await trx
|
const memberRole = await trx
|
||||||
@@ -233,18 +234,6 @@ export async function createOrg(
|
|||||||
orgId
|
orgId
|
||||||
}))
|
}))
|
||||||
);
|
);
|
||||||
|
|
||||||
const rootApiKeys = await trx
|
|
||||||
.select()
|
|
||||||
.from(apiKeys)
|
|
||||||
.where(eq(apiKeys.isRoot, true));
|
|
||||||
|
|
||||||
for (const apiKey of rootApiKeys) {
|
|
||||||
await trx.insert(apiKeyOrg).values({
|
|
||||||
apiKeyId: apiKey.apiKeyId,
|
|
||||||
orgId: newOrg[0].orgId
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!org) {
|
if (!org) {
|
||||||
|
|||||||
@@ -15,7 +15,6 @@ import response from "@server/lib/response";
|
|||||||
import HttpCode from "@server/types/HttpCode";
|
import HttpCode from "@server/types/HttpCode";
|
||||||
import createHttpError from "http-errors";
|
import createHttpError from "http-errors";
|
||||||
import { eq, and } from "drizzle-orm";
|
import { eq, and } from "drizzle-orm";
|
||||||
import stoi from "@server/lib/stoi";
|
|
||||||
import { fromError } from "zod-validation-error";
|
import { fromError } from "zod-validation-error";
|
||||||
import logger from "@server/logger";
|
import logger from "@server/logger";
|
||||||
import { subdomainSchema } from "@server/lib/schemas";
|
import { subdomainSchema } from "@server/lib/schemas";
|
||||||
@@ -25,7 +24,6 @@ import { build } from "@server/build";
|
|||||||
|
|
||||||
const createResourceParamsSchema = z
|
const createResourceParamsSchema = z
|
||||||
.object({
|
.object({
|
||||||
siteId: z.string().transform(stoi).pipe(z.number().int().positive()),
|
|
||||||
orgId: z.string()
|
orgId: z.string()
|
||||||
})
|
})
|
||||||
.strict();
|
.strict();
|
||||||
@@ -34,7 +32,6 @@ const createHttpResourceSchema = z
|
|||||||
.object({
|
.object({
|
||||||
name: z.string().min(1).max(255),
|
name: z.string().min(1).max(255),
|
||||||
subdomain: z.string().nullable().optional(),
|
subdomain: z.string().nullable().optional(),
|
||||||
siteId: z.number(),
|
|
||||||
http: z.boolean(),
|
http: z.boolean(),
|
||||||
protocol: z.enum(["tcp", "udp"]),
|
protocol: z.enum(["tcp", "udp"]),
|
||||||
domainId: z.string()
|
domainId: z.string()
|
||||||
@@ -53,11 +50,10 @@ const createHttpResourceSchema = z
|
|||||||
const createRawResourceSchema = z
|
const createRawResourceSchema = z
|
||||||
.object({
|
.object({
|
||||||
name: z.string().min(1).max(255),
|
name: z.string().min(1).max(255),
|
||||||
siteId: z.number(),
|
|
||||||
http: z.boolean(),
|
http: z.boolean(),
|
||||||
protocol: z.enum(["tcp", "udp"]),
|
protocol: z.enum(["tcp", "udp"]),
|
||||||
proxyPort: z.number().int().min(1).max(65535),
|
proxyPort: z.number().int().min(1).max(65535),
|
||||||
enableProxy: z.boolean().default(true)
|
// enableProxy: z.boolean().default(true) // always true now
|
||||||
})
|
})
|
||||||
.strict()
|
.strict()
|
||||||
.refine(
|
.refine(
|
||||||
@@ -78,7 +74,7 @@ export type CreateResourceResponse = Resource;
|
|||||||
|
|
||||||
registry.registerPath({
|
registry.registerPath({
|
||||||
method: "put",
|
method: "put",
|
||||||
path: "/org/{orgId}/site/{siteId}/resource",
|
path: "/org/{orgId}/resource",
|
||||||
description: "Create a resource.",
|
description: "Create a resource.",
|
||||||
tags: [OpenAPITags.Org, OpenAPITags.Resource],
|
tags: [OpenAPITags.Org, OpenAPITags.Resource],
|
||||||
request: {
|
request: {
|
||||||
@@ -111,7 +107,7 @@ export async function createResource(
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const { siteId, orgId } = parsedParams.data;
|
const { orgId } = parsedParams.data;
|
||||||
|
|
||||||
if (req.user && !req.userOrgRoleId) {
|
if (req.user && !req.userOrgRoleId) {
|
||||||
return next(
|
return next(
|
||||||
@@ -146,7 +142,7 @@ export async function createResource(
|
|||||||
if (http) {
|
if (http) {
|
||||||
return await createHttpResource(
|
return await createHttpResource(
|
||||||
{ req, res, next },
|
{ req, res, next },
|
||||||
{ siteId, orgId }
|
{ orgId }
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
if (
|
if (
|
||||||
@@ -162,7 +158,7 @@ export async function createResource(
|
|||||||
}
|
}
|
||||||
return await createRawResource(
|
return await createRawResource(
|
||||||
{ req, res, next },
|
{ req, res, next },
|
||||||
{ siteId, orgId }
|
{ orgId }
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -180,12 +176,11 @@ async function createHttpResource(
|
|||||||
next: NextFunction;
|
next: NextFunction;
|
||||||
},
|
},
|
||||||
meta: {
|
meta: {
|
||||||
siteId: number;
|
|
||||||
orgId: string;
|
orgId: string;
|
||||||
}
|
}
|
||||||
) {
|
) {
|
||||||
const { req, res, next } = route;
|
const { req, res, next } = route;
|
||||||
const { siteId, orgId } = meta;
|
const { orgId } = meta;
|
||||||
|
|
||||||
const parsedBody = createHttpResourceSchema.safeParse(req.body);
|
const parsedBody = createHttpResourceSchema.safeParse(req.body);
|
||||||
if (!parsedBody.success) {
|
if (!parsedBody.success) {
|
||||||
@@ -292,7 +287,6 @@ async function createHttpResource(
|
|||||||
const newResource = await trx
|
const newResource = await trx
|
||||||
.insert(resources)
|
.insert(resources)
|
||||||
.values({
|
.values({
|
||||||
siteId,
|
|
||||||
fullDomain,
|
fullDomain,
|
||||||
domainId,
|
domainId,
|
||||||
orgId,
|
orgId,
|
||||||
@@ -357,12 +351,11 @@ async function createRawResource(
|
|||||||
next: NextFunction;
|
next: NextFunction;
|
||||||
},
|
},
|
||||||
meta: {
|
meta: {
|
||||||
siteId: number;
|
|
||||||
orgId: string;
|
orgId: string;
|
||||||
}
|
}
|
||||||
) {
|
) {
|
||||||
const { req, res, next } = route;
|
const { req, res, next } = route;
|
||||||
const { siteId, orgId } = meta;
|
const { orgId } = meta;
|
||||||
|
|
||||||
const parsedBody = createRawResourceSchema.safeParse(req.body);
|
const parsedBody = createRawResourceSchema.safeParse(req.body);
|
||||||
if (!parsedBody.success) {
|
if (!parsedBody.success) {
|
||||||
@@ -374,7 +367,7 @@ async function createRawResource(
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const { name, http, protocol, proxyPort, enableProxy } = parsedBody.data;
|
const { name, http, protocol, proxyPort } = parsedBody.data;
|
||||||
|
|
||||||
// if http is false check to see if there is already a resource with the same port and protocol
|
// if http is false check to see if there is already a resource with the same port and protocol
|
||||||
const existingResource = await db
|
const existingResource = await db
|
||||||
@@ -402,13 +395,12 @@ async function createRawResource(
|
|||||||
const newResource = await trx
|
const newResource = await trx
|
||||||
.insert(resources)
|
.insert(resources)
|
||||||
.values({
|
.values({
|
||||||
siteId,
|
|
||||||
orgId,
|
orgId,
|
||||||
name,
|
name,
|
||||||
http,
|
http,
|
||||||
protocol,
|
protocol,
|
||||||
proxyPort,
|
proxyPort,
|
||||||
enableProxy
|
// enableProxy
|
||||||
})
|
})
|
||||||
.returning();
|
.returning();
|
||||||
|
|
||||||
|
|||||||
@@ -71,44 +71,44 @@ export async function deleteResource(
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const [site] = await db
|
// const [site] = await db
|
||||||
.select()
|
// .select()
|
||||||
.from(sites)
|
// .from(sites)
|
||||||
.where(eq(sites.siteId, deletedResource.siteId!))
|
// .where(eq(sites.siteId, deletedResource.siteId!))
|
||||||
.limit(1);
|
// .limit(1);
|
||||||
|
//
|
||||||
if (!site) {
|
// if (!site) {
|
||||||
return next(
|
// return next(
|
||||||
createHttpError(
|
// createHttpError(
|
||||||
HttpCode.NOT_FOUND,
|
// HttpCode.NOT_FOUND,
|
||||||
`Site with ID ${deletedResource.siteId} not found`
|
// `Site with ID ${deletedResource.siteId} not found`
|
||||||
)
|
// )
|
||||||
);
|
// );
|
||||||
}
|
// }
|
||||||
|
//
|
||||||
if (site.pubKey) {
|
// if (site.pubKey) {
|
||||||
if (site.type == "wireguard") {
|
// if (site.type == "wireguard") {
|
||||||
await addPeer(site.exitNodeId!, {
|
// await addPeer(site.exitNodeId!, {
|
||||||
publicKey: site.pubKey,
|
// publicKey: site.pubKey,
|
||||||
allowedIps: await getAllowedIps(site.siteId)
|
// allowedIps: await getAllowedIps(site.siteId)
|
||||||
});
|
// });
|
||||||
} else if (site.type == "newt") {
|
// } else if (site.type == "newt") {
|
||||||
// get the newt on the site by querying the newt table for siteId
|
// // get the newt on the site by querying the newt table for siteId
|
||||||
const [newt] = await db
|
// const [newt] = await db
|
||||||
.select()
|
// .select()
|
||||||
.from(newts)
|
// .from(newts)
|
||||||
.where(eq(newts.siteId, site.siteId))
|
// .where(eq(newts.siteId, site.siteId))
|
||||||
.limit(1);
|
// .limit(1);
|
||||||
|
//
|
||||||
removeTargets(
|
// removeTargets(
|
||||||
newt.newtId,
|
// newt.newtId,
|
||||||
targetsToBeRemoved,
|
// targetsToBeRemoved,
|
||||||
deletedResource.protocol,
|
// deletedResource.protocol,
|
||||||
deletedResource.proxyPort
|
// deletedResource.proxyPort
|
||||||
);
|
// );
|
||||||
}
|
// }
|
||||||
}
|
// }
|
||||||
|
//
|
||||||
return response(res, {
|
return response(res, {
|
||||||
data: null,
|
data: null,
|
||||||
success: true,
|
success: true,
|
||||||
|
|||||||
@@ -19,9 +19,7 @@ const getResourceSchema = z
|
|||||||
})
|
})
|
||||||
.strict();
|
.strict();
|
||||||
|
|
||||||
export type GetResourceResponse = Resource & {
|
export type GetResourceResponse = Resource;
|
||||||
siteName: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
registry.registerPath({
|
registry.registerPath({
|
||||||
method: "get",
|
method: "get",
|
||||||
@@ -56,11 +54,9 @@ export async function getResource(
|
|||||||
.select()
|
.select()
|
||||||
.from(resources)
|
.from(resources)
|
||||||
.where(eq(resources.resourceId, resourceId))
|
.where(eq(resources.resourceId, resourceId))
|
||||||
.leftJoin(sites, eq(sites.siteId, resources.siteId))
|
|
||||||
.limit(1);
|
.limit(1);
|
||||||
|
|
||||||
const resource = resp.resources;
|
const resource = resp;
|
||||||
const site = resp.sites;
|
|
||||||
|
|
||||||
if (!resource) {
|
if (!resource) {
|
||||||
return next(
|
return next(
|
||||||
@@ -73,8 +69,7 @@ export async function getResource(
|
|||||||
|
|
||||||
return response(res, {
|
return response(res, {
|
||||||
data: {
|
data: {
|
||||||
...resource,
|
...resource
|
||||||
siteName: site?.name
|
|
||||||
},
|
},
|
||||||
success: true,
|
success: true,
|
||||||
error: false,
|
error: false,
|
||||||
|
|||||||
@@ -31,6 +31,7 @@ export type GetResourceAuthInfoResponse = {
|
|||||||
blockAccess: boolean;
|
blockAccess: boolean;
|
||||||
url: string;
|
url: string;
|
||||||
whitelist: boolean;
|
whitelist: boolean;
|
||||||
|
skipToIdpId: number | null;
|
||||||
};
|
};
|
||||||
|
|
||||||
export async function getResourceAuthInfo(
|
export async function getResourceAuthInfo(
|
||||||
@@ -86,7 +87,8 @@ export async function getResourceAuthInfo(
|
|||||||
sso: resource.sso,
|
sso: resource.sso,
|
||||||
blockAccess: resource.blockAccess,
|
blockAccess: resource.blockAccess,
|
||||||
url,
|
url,
|
||||||
whitelist: resource.emailWhitelistEnabled
|
whitelist: resource.emailWhitelistEnabled,
|
||||||
|
skipToIdpId: resource.skipToIdpId
|
||||||
},
|
},
|
||||||
success: true,
|
success: true,
|
||||||
error: false,
|
error: false,
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user