Merge branch 'dev' into user-management-and-resources

This commit is contained in:
Adrian Astles 2025-07-18 22:21:55 +08:00 committed by GitHub
commit a140f27d04
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
291 changed files with 21408 additions and 7510 deletions

View file

@ -27,3 +27,4 @@ bruno/
LICENSE LICENSE
CONTRIBUTING.md CONTRIBUTING.md
dist dist
.git

1
.gitignore vendored
View file

@ -18,6 +18,7 @@ yarn-error.log*
next-env.d.ts next-env.d.ts
*.db *.db
*.sqlite *.sqlite
!Dockerfile.sqlite
*.sqlite3 *.sqlite3
*.log *.log
.machinelogs*.json .machinelogs*.json

View file

@ -6,10 +6,6 @@ Please see the contribution and local development guide on the docs page before
https://docs.fossorial.io/development https://docs.fossorial.io/development
For ideas about what features to work on and our future plans, please see the roadmap:
https://docs.fossorial.io/roadmap
### Licensing Considerations ### Licensing Considerations
Please note that your contributions will be distributed under the AGPLv3 and the Fossorial Commercial license. For inquiries about commercial licensing, please contact us. Please note that your contributions will be distributed under the AGPLv3 and the Fossorial Commercial license. For inquiries about commercial licensing, please contact us.
@ -21,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.
``` ```

14
Dockerfile.dev Normal file
View file

@ -0,0 +1,14 @@
FROM node:20-alpine
WORKDIR /app
COPY package*.json ./
# Install dependencies
RUN npm ci
# Copy source code
COPY . .
# Use tsx watch for development with hot reload
CMD ["npm", "run", "dev"]

View file

@ -5,8 +5,8 @@ build-release:
echo "Error: tag is required. Usage: make build-release tag=<tag>"; \ echo "Error: tag is required. Usage: make build-release tag=<tag>"; \
exit 1; \ exit 1; \
fi fi
docker buildx build --platform linux/arm64,linux/amd64 -t fosrl/pangolin:latest -f Dockerfile --push . docker buildx build --platform linux/arm64,linux/amd64 -t fosrl/pangolin:latest -f Dockerfile.sqlite --push .
docker buildx build --platform linux/arm64,linux/amd64 -t fosrl/pangolin:$(tag) -f Dockerfile --push . docker buildx build --platform linux/arm64,linux/amd64 -t fosrl/pangolin:$(tag) -f Dockerfile.sqlite --push .
docker buildx build --platform linux/arm64,linux/amd64 -t fosrl/pangolin:postgresql-latest -f Dockerfile.pg --push . docker buildx build --platform linux/arm64,linux/amd64 -t fosrl/pangolin:postgresql-latest -f Dockerfile.pg --push .
docker buildx build --platform linux/arm64,linux/amd64 -t fosrl/pangolin:postgresql-$(tag) -f Dockerfile.pg --push . docker buildx build --platform linux/arm64,linux/amd64 -t fosrl/pangolin:postgresql-$(tag) -f Dockerfile.pg --push .
@ -16,8 +16,8 @@ build-arm:
build-x86: build-x86:
docker buildx build --platform linux/amd64 -t fosrl/pangolin:latest . docker buildx build --platform linux/amd64 -t fosrl/pangolin:latest .
build: build-sqlite:
docker build -t fosrl/pangolin:latest -f Dockerfile . docker build -t fosrl/pangolin:latest -f Dockerfile.sqlite .
build-pg: build-pg:
docker build -t fosrl/pangolin:postgresql-latest -f Dockerfile.pg . docker build -t fosrl/pangolin:postgresql-latest -f Dockerfile.pg .

106
README.md
View file

@ -7,16 +7,16 @@
</h2> </h2>
</div> </div>
<h4 align="center">Tunneled Reverse Proxy Server with Access Control</h4> <h4 align="center">Secure gateway to your private networks</h4>
<div align="center"> <div align="center">
_Your own self-hosted zero trust tunnel._ _Pangolin tunnels your services to the internet so you can access anything from anywhere._
</div> </div>
<div align="center"> <div align="center">
<h5> <h5>
<a href="https://fossorial.io"> <a href="https://digpangolin.com">
Website Website
</a> </a>
<span> | </span> <span> | </span>
@ -36,22 +36,32 @@ _Your own self-hosted zero trust tunnel._
</div> </div>
<p align="center">
<strong>
Start testing Pangolin at <a href="https://pangolin.fossorial.io/auth/signup">pangolin.fossorial.io</a>
<br/>
</strong>
</p>
Pangolin is a self-hosted tunneled reverse proxy server with identity and access control, designed to securely expose private resources on distributed networks. Acting as a central hub, it connects isolated networks — even those behind restrictive firewalls — through encrypted tunnels, enabling easy access to remote services without opening ports. Pangolin is a self-hosted tunneled reverse proxy server with identity and access control, designed to securely expose private resources on distributed networks. Acting as a central hub, it connects isolated networks — even those behind restrictive firewalls — through encrypted tunnels, enabling easy access to remote services without opening ports.
<img src="public/screenshots/hero.png" alt="Preview"/> <img src="public/screenshots/hero.png" alt="Preview"/>
_Resources page of Pangolin dashboard (dark mode) showing multiple resources available to connect._ ![gif](public/clip.gif)
## Key Features ## Key Features
### Reverse Proxy Through WireGuard Tunnel ### Reverse Proxy Through WireGuard Tunnel
- Expose private resources on your network **without opening ports** (firewall punching). - Expose private resources on your network **without opening ports** (firewall punching).
- Secure and easy to configure site-to-site connectivity via a custom **user space WireGuard client**, [Newt](https://github.com/fosrl/newt). - Secure and easy to configure private connectivity via a custom **user space WireGuard client**, [Newt](https://github.com/fosrl/newt).
- Built-in support for any WireGuard client. - Built-in support for any WireGuard client.
- Automated **SSL certificates** (https) via [LetsEncrypt](https://letsencrypt.org/). - Automated **SSL certificates** (https) via [LetsEncrypt](https://letsencrypt.org/).
- Support for HTTP/HTTPS and **raw TCP/UDP services**. - Support for HTTP/HTTPS and **raw TCP/UDP services**.
- Load balancing. - Load balancing.
- Extend functionality with existing [Traefik](https://github.com/traefik/traefik) plugins, such as [CrowdSec](https://plugins.traefik.io/plugins/6335346ca4caa9ddeffda116/crowdsec-bouncer-traefik-plugin) and [Geoblock](https://github.com/PascalMinder/geoblock).
- **Automatically install and configure Crowdsec via Pangolin's installer script.**
- Attach as many sites to the central server as you wish.
### Identity & Access Management ### Identity & Access Management
@ -65,89 +75,73 @@ _Resources page of Pangolin dashboard (dark mode) showing multiple resources ava
- **Temporary, self-destructing share links.** - **Temporary, self-destructing share links.**
- Resource specific pin codes. - Resource specific pin codes.
- Resource specific passwords. - Resource specific passwords.
- Passkeys
- External identity provider (IdP) support with OAuth2/OIDC, such as Authentik, Keycloak, Okta, and others. - External identity provider (IdP) support with OAuth2/OIDC, such as Authentik, Keycloak, Okta, and others.
- Auto-provision users and roles from your IdP. - Auto-provision users and roles from your IdP.
### Simple Dashboard UI <img src="public/auth-diagram1.png" alt="Auth and diagram"/>
- Manage sites, users, and roles with a clean and intuitive UI. ## Use Cases
- Monitor site usage and connectivity.
- Light and dark mode options.
- Mobile friendly.
### Easy Deployment ### Manage Access to Internal Apps
- Run on any cloud provider or on-premises. - Grant users access to your apps from anywhere using just a web browser. No client software required.
- **Docker Compose based setup** for simplified deployment.
- Future-proof installation script for streamlined setup and feature additions.
- Use any WireGuard client to connect, or use **Newt, our custom user space client** for the best experience.
- Use the API to create custom integrations and scripts.
- Fine-grained access control to the API via scoped API keys.
- Comprehensive Swagger documentation for the API.
### Modular Design ### Developers and DevOps
- Extend functionality with existing [Traefik](https://github.com/traefik/traefik) plugins, such as [CrowdSec](https://plugins.traefik.io/plugins/6335346ca4caa9ddeffda116/crowdsec-bouncer-traefik-plugin) and [Geoblock](https://github.com/PascalMinder/geoblock). - Expose and test internal tools and dashboards like **Grafana**. Bring localhost or private IPs online for easy access.
- **Automatically install and configure Crowdsec via Pangolin's installer script.**
- Attach as many sites to the central server as you wish.
<img src="public/screenshots/collage.png" alt="Collage"/> ### Secure API Gateway
## Deployment and Usage Example - One application load balancer across multiple clouds and on-premises.
1. **Deploy the Central Server**: ### IoT and Edge Devices
- Deploy the Docker Compose stack onto a VPS hosted on a cloud platform like RackNerd, Amazon EC2, DigitalOcean Droplet, or similar. There are many cheap VPS hosting options available to suit your needs. - Easily expose **IoT devices**, **edge servers**, or **Raspberry Pi** to the internet for field equipment monitoring.
<img src="public/screenshots/sites.png" alt="Sites"/>
## Deployment Options
### 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.
> [!TIP]
> 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!
> We are part of the [RackNerd](https://my.racknerd.com/aff.php?aff=13788) affiliate program, so if you purchase through [our link](https://my.racknerd.com/aff.php?aff=13788), we receive a small commission which helps us maintain the project and keep it free for everyone.
1. **Domain Configuration**: ### Pangolin Cloud
- Point your domain name to the VPS and configure Pangolin with your preferred settings. Easy to use with simple [pay as you go pricing](https://digpangolin.io/pricing). [Check it out here](https://pangolin.fossorial.io/auth/signup).
2. **Connect Private Sites**: - Everything you get with self hosted Pangolin, but fully managed for you.
- Install Newt or use another WireGuard client on private sites. ### Hybrid & High Availability
- Automatically establish a connection from these sites to the central server.
3. **Expose Resources**: Managed control plane, your infrastructure
- Add resources to the central server and configure access control rules. - We manage database and control plane.
- Access these resources securely from anywhere. - You self-host lightweight exit-node.
- Traffic flows through your infra.
- We coordinate failover between your nodes or to Cloud when things go bad.
**Use Case Example - Bypassing Port Restrictions in Home Lab**: If interested, [contact us](mailto:numbat@fossorial.io).
Imagine private sites where the ISP restricts port forwarding. By connecting these sites to Pangolin via WireGuard, you can securely expose HTTP and HTTPS resources on the private network without any networking complexity.
**Use Case Example - Deploying Services For Your Business**: ### Full Enterprise On-Premises
You can use Pangolin as an easy way to expose your business applications to your users behind a safe authentication portal you can integrate into your IdP solution. Expose resources on prem and on the cloud.
**Use Case Example - IoT Networks**: [Contact us](mailto:numbat@fossorial.io) for a full distributed and enterprise deployments on your infrastructure controlled by your team.
IoT networks are often fragmented and difficult to manage. By deploying Pangolin on a central server, you can connect all your IoT sites via Newt or another WireGuard client. This creates a simple, secure, and centralized way to access IoT resources without the need for intricate networking setups.
## Similar Projects and Inspirations
**Cloudflare Tunnels**:
A similar approach to proxying private resources securely, but Pangolin is a self-hosted alternative, giving you full control over your infrastructure.
**Authelia**:
This inspired Pangolins centralized authentication system for proxies, enabling robust user and role management.
## Project Development / Roadmap ## Project Development / Roadmap
> [!NOTE] We want to hear your feature requests! Add them to the [discussion board](https://github.com/orgs/fosrl/discussions/categories/feature-requests).
> Pangolin is under heavy development. The roadmap is subject to change as we fix bugs, add new features, and make improvements.
View the [project board](https://github.com/orgs/fosrl/projects/1) for more detailed info.
## Licensing ## Licensing
Pangolin is dual licensed under the AGPL-3 and the Fossorial Commercial license. Please see the [LICENSE](./LICENSE) file in the repository for details. For inquiries about commercial licensing, please contact us at [numbat@fossorial.io](mailto:numbat@fossorial.io). Pangolin is dual licensed under the AGPL-3 and the Fossorial Commercial license. For inquiries about commercial licensing, please contact us at [numbat@fossorial.io](mailto:numbat@fossorial.io).
## 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).
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.
Please post bug reports and other functional issues in the [Issues](https://github.com/fosrl/pangolin/issues) section of the repository. Please post bug reports and other functional issues in the [Issues](https://github.com/fosrl/pangolin/issues) section of the repository.
For all feature requests, or other ideas, please use the [Discussions](https://github.com/orgs/fosrl/discussions) section.

View file

@ -0,0 +1,22 @@
meta {
name: createClient
type: http
seq: 1
}
put {
url: http://localhost:3000/api/v1/site/1/client
body: json
auth: none
}
body:json {
{
"siteId": 1,
"name": "test",
"type": "olm",
"subnet": "100.90.129.4/30",
"olmId": "029yzunhx6nh3y5",
"secret": "l0ymp075y3d4rccb25l6sqpgar52k09etunui970qq5gj7x6"
}
}

View file

@ -0,0 +1,11 @@
meta {
name: pickClientDefaults
type: http
seq: 2
}
get {
url: http://localhost:3000/api/v1/site/1/pick-client-defaults
body: none
auth: none
}

View file

@ -46,4 +46,3 @@ flags:
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
allow_base_domain_resources: true

12
docker-compose.pgr.yml Normal file
View file

@ -0,0 +1,12 @@
services:
# PostgreSQL Service
db:
image: postgres:17 # Use the PostgreSQL 17 image
container_name: dev_postgres # Name your PostgreSQL container
environment:
POSTGRES_DB: postgres # Default database name
POSTGRES_USER: postgres # Default user
POSTGRES_PASSWORD: password # Default password (change for production!)
ports:
- "5432:5432" # Map host port 5432 to container port 5432
restart: no

29
docker-compose.yml Normal file
View file

@ -0,0 +1,29 @@
services:
# Development application service
app:
build:
context: .
dockerfile: Dockerfile.dev
container_name: dev_pangolin
ports:
- "3000:3000"
- "3001:3001"
- "3002:3002"
environment:
- NODE_ENV=development
- ENVIRONMENT=dev
- DB_TYPE=pg
volumes:
# Mount source code for hot reload
- ./src:/app/src
- ./server:/app/server
- ./public:/app/public
- ./messages:/app/messages
- ./components.json:/app/components.json
- ./next.config.mjs:/app/next.config.mjs
- ./tsconfig.json:/app/tsconfig.json
- ./tailwind.config.js:/app/tailwind.config.js
- ./postcss.config.mjs:/app/postcss.config.mjs
- ./eslint.config.js:/app/eslint.config.js
- ./config:/app/config
restart: no

View file

@ -3,7 +3,7 @@ import path from "path";
export default defineConfig({ export default defineConfig({
dialect: "postgresql", dialect: "postgresql",
schema: path.join("server", "db", "pg", "schema.ts"), schema: [path.join("server", "db", "pg", "schema.ts")],
out: path.join("server", "migrations"), out: path.join("server", "migrations"),
verbose: true, verbose: true,
dbCredentials: { dbCredentials: {

View file

@ -22,10 +22,14 @@ gerbil:
start_port: 51820 start_port: 51820
base_endpoint: "{{.DashboardDomain}}" base_endpoint: "{{.DashboardDomain}}"
orgs:
block_size: 24
subnet_group: 100.89.138.0/20
{{if .EnableEmail}} {{if .EnableEmail}}
email: email:
smtp_host: "{{.EmailSMTPHost}}" smtp_host: "{{.EmailSMTPHost}}"
smtp_port: {{.EmailSMTPPort}} smtp_port: "{{.EmailSMTPPort}}"
smtp_user: "{{.EmailSMTPUser}}" smtp_user: "{{.EmailSMTPUser}}"
smtp_pass: "{{.EmailSMTPPass}}" smtp_pass: "{{.EmailSMTPPass}}"
no_reply: "{{.EmailNoReply}}" no_reply: "{{.EmailNoReply}}"
@ -36,4 +40,4 @@ flags:
disable_signup_without_invite: true disable_signup_without_invite: true
disable_user_create_org: false disable_user_create_org: false
allow_raw_resources: true allow_raw_resources: true
allow_base_domain_resources: true allow_base_domain_resources: true

View file

@ -1,4 +1,4 @@
name: captcha_remediation iame: captcha_remediation
filters: filters:
- Alert.Remediation == true && Alert.GetScope() == "Ip" && Alert.GetScenario() contains "http" - Alert.Remediation == true && Alert.GetScope() == "Ip" && Alert.GetScenario() contains "http"
decisions: decisions:

1277
messages/cs-CZ.json Normal file

File diff suppressed because it is too large Load diff

View file

@ -11,8 +11,9 @@
"componentsErrorNoMemberCreate": "Du bist derzeit kein Mitglied einer Organisation. Erstelle eine Organisation, um zu starten.", "componentsErrorNoMemberCreate": "Du bist derzeit kein Mitglied einer Organisation. Erstelle eine Organisation, um zu starten.",
"componentsErrorNoMember": "Du bist aktuell kein Mitglied einer Organisation.", "componentsErrorNoMember": "Du bist aktuell kein Mitglied einer Organisation.",
"welcome": "Willkommen zu Pangolin", "welcome": "Willkommen zu Pangolin",
"welcomeTo": "Willkommen bei",
"componentsCreateOrg": "Erstelle eine Organisation", "componentsCreateOrg": "Erstelle eine Organisation",
"componentsMember": "Du bist Mitglied von {count, plural, =0 {keiner Organisation} =1 {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} Sites, die das Lizenzlimit der {maxSites} Sites überschreiten. Beachte die Lizenzbedingungen, um alle Funktionen weiterhin zu nutzen.",
@ -206,6 +207,7 @@
"orgGeneralSettings": "Organisations-Einstellungen", "orgGeneralSettings": "Organisations-Einstellungen",
"orgGeneralSettingsDescription": "Organisationsdetails und Konfiguration verwalten", "orgGeneralSettingsDescription": "Organisationsdetails und Konfiguration verwalten",
"saveGeneralSettings": "Allgemeine Einstellungen speichern", "saveGeneralSettings": "Allgemeine Einstellungen speichern",
"saveSettings": "Einstellungen speichern",
"orgDangerZone": "Gefahrenzone", "orgDangerZone": "Gefahrenzone",
"orgDangerZoneDescription": "Sobald Sie diesen Org löschen, gibt es kein Zurück mehr. Bitte seien Sie vorsichtig.", "orgDangerZoneDescription": "Sobald Sie diesen Org löschen, gibt es kein Zurück mehr. Bitte seien Sie vorsichtig.",
"orgDelete": "Organisation löschen", "orgDelete": "Organisation löschen",
@ -249,7 +251,7 @@
"weeks": "Wochen", "weeks": "Wochen",
"months": "Monate", "months": "Monate",
"years": "Jahre", "years": "Jahre",
"day": "{count, plural, =1 {# Tag} other {# Tage}}", "day": "{count, plural, one {# Tag} other {# Tage}}",
"apiKeysTitle": "API-Schlüssel Information", "apiKeysTitle": "API-Schlüssel Information",
"apiKeysConfirmCopy2": "Sie müssen bestätigen, dass Sie den API-Schlüssel kopiert haben.", "apiKeysConfirmCopy2": "Sie müssen bestätigen, dass Sie den API-Schlüssel kopiert haben.",
"apiKeysErrorCreate": "Fehler beim Erstellen des API-Schlüssels", "apiKeysErrorCreate": "Fehler beim Erstellen des API-Schlüssels",
@ -347,7 +349,7 @@
"licensePurchase": "Lizenz kaufen", "licensePurchase": "Lizenz kaufen",
"licensePurchaseSites": "Zusätzliche Seiten kaufen", "licensePurchaseSites": "Zusätzliche Seiten kaufen",
"licenseSitesUsedMax": "{usedSites} der {maxSites} Seiten verwendet", "licenseSitesUsedMax": "{usedSites} der {maxSites} Seiten verwendet",
"licenseSitesUsed": "{count, plural, =0 {# Seiten} =1 {# Seite} other {# Seiten}} im System.", "licenseSitesUsed": "{count, plural, =0 {# Seiten} one {# Seite} other {# Seiten}} 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 Seite",
@ -436,7 +438,7 @@
"accessRoleSelect": "Rolle auswählen", "accessRoleSelect": "Rolle auswählen",
"inviteEmailSentDescription": "Eine E-Mail mit dem Zugangslink wurde an den Benutzer gesendet. Er muss den Link aufrufen, um die Einladung anzunehmen.", "inviteEmailSentDescription": "Eine E-Mail mit dem Zugangslink wurde an den Benutzer gesendet. Er muss den Link aufrufen, um die Einladung anzunehmen.",
"inviteSentDescription": "Der Benutzer wurde eingeladen. Er muss den unten stehenden Link aufrufen, um die Einladung anzunehmen.", "inviteSentDescription": "Der Benutzer wurde eingeladen. Er muss den unten stehenden Link aufrufen, um die Einladung anzunehmen.",
"inviteExpiresIn": "Die Einladung läuft in {days, plural, =1 {einem Tag} other {# Tagen}} ab.", "inviteExpiresIn": "Die Einladung läuft in {days, plural, one {einem Tag} other {# Tagen}} ab.",
"idpTitle": "Allgemeine Informationen", "idpTitle": "Allgemeine Informationen",
"idpSelect": "Wählen Sie den Identitätsanbieter für den externen Benutzer", "idpSelect": "Wählen Sie den Identitätsanbieter für den externen Benutzer",
"idpNotConfigured": "Es sind keine Identitätsanbieter konfiguriert. Bitte konfigurieren Sie einen Identitätsanbieter, bevor Sie externe Benutzer erstellen.", "idpNotConfigured": "Es sind keine Identitätsanbieter konfiguriert. Bitte konfigurieren Sie einen Identitätsanbieter, bevor Sie externe Benutzer erstellen.",
@ -958,6 +960,8 @@
"licenseTierProfessionalRequiredDescription": "Diese Funktion ist nur in der Professional Edition verfügbar.", "licenseTierProfessionalRequiredDescription": "Diese Funktion ist nur in der Professional Edition verfügbar.",
"actionGetOrg": "Organisation abrufen", "actionGetOrg": "Organisation abrufen",
"actionUpdateOrg": "Organisation aktualisieren", "actionUpdateOrg": "Organisation aktualisieren",
"actionUpdateUser": "Benutzer aktualisieren",
"actionGetUser": "Benutzer abrufen",
"actionGetOrgUser": "Organisationsbenutzer abrufen", "actionGetOrgUser": "Organisationsbenutzer abrufen",
"actionListOrgDomains": "Organisationsdomänen auflisten", "actionListOrgDomains": "Organisationsdomänen auflisten",
"actionCreateSite": "Site erstellen", "actionCreateSite": "Site erstellen",
@ -1090,6 +1094,8 @@
"sidebarAllUsers": "Alle Benutzer", "sidebarAllUsers": "Alle Benutzer",
"sidebarIdentityProviders": "Identitätsanbieter", "sidebarIdentityProviders": "Identitätsanbieter",
"sidebarLicense": "Lizenz", "sidebarLicense": "Lizenz",
"sidebarClients": "Kunden",
"sidebarDomains": "Domains",
"enableDockerSocket": "Docker Socket aktivieren", "enableDockerSocket": "Docker Socket aktivieren",
"enableDockerSocketDescription": "Docker Socket-Erkennung aktivieren, um Container-Informationen zu befüllen. Socket-Pfad muss Newt bereitgestellt werden.", "enableDockerSocketDescription": "Docker Socket-Erkennung aktivieren, um Container-Informationen zu befüllen. Socket-Pfad muss Newt bereitgestellt werden.",
"enableDockerSocketLink": "Mehr erfahren", "enableDockerSocketLink": "Mehr erfahren",
@ -1102,7 +1108,7 @@
"containerNetworks": "Netzwerke", "containerNetworks": "Netzwerke",
"containerHostnameIp": "Hostname/IP", "containerHostnameIp": "Hostname/IP",
"containerLabels": "Etiketten", "containerLabels": "Etiketten",
"containerLabelsCount": "{count} Label{s,plural,one{} other{s}}", "containerLabelsCount": "{count, plural, one {# Etikett} other {# Etiketten}}",
"containerLabelsTitle": "Container-Labels", "containerLabelsTitle": "Container-Labels",
"containerLabelEmpty": "<leer>", "containerLabelEmpty": "<leer>",
"containerPorts": "Häfen", "containerPorts": "Häfen",
@ -1114,7 +1120,7 @@
"showStoppedContainers": "Stoppte Container anzeigen", "showStoppedContainers": "Stoppte Container anzeigen",
"noContainersFound": "Keine Container gefunden. Stellen Sie sicher, dass Docker Container laufen.", "noContainersFound": "Keine Container gefunden. Stellen Sie sicher, dass Docker Container laufen.",
"searchContainersPlaceholder": "Durchsuche {count} Container...", "searchContainersPlaceholder": "Durchsuche {count} Container...",
"searchResultsCount": "{count} Ergebnis{s,plural,one{} other{s}}", "searchResultsCount": "{count, plural, one {# Ergebnis} other {# Ergebnisse}}",
"filters": "Filter", "filters": "Filter",
"filterOptions": "Filteroptionen", "filterOptions": "Filteroptionen",
"filterPorts": "Häfen", "filterPorts": "Häfen",
@ -1129,10 +1135,89 @@
"dark": "dunkel", "dark": "dunkel",
"system": "System", "system": "System",
"theme": "Design", "theme": "Design",
"subnetRequired": "Subnetz ist erforderlich",
"initialSetupTitle": "Initial Einrichtung des Servers", "initialSetupTitle": "Initial Einrichtung des Servers",
"initialSetupDescription": "Erstellen Sie das initiale Server-Admin-Konto. Es kann nur einen Server-Admin geben. Sie können diese Anmeldedaten später immer ändern.", "initialSetupDescription": "Erstellen Sie das initiale Server-Admin-Konto. Es kann nur einen Server-Admin geben. Sie können diese Anmeldedaten später immer ändern.",
"createAdminAccount": "Admin-Konto erstellen", "createAdminAccount": "Admin-Konto erstellen",
"setupErrorCreateAdmin": "Beim Erstellen des Server-Admin-Kontos ist ein Fehler aufgetreten.", "setupErrorCreateAdmin": "Beim Erstellen des Server-Admin-Kontos ist ein Fehler aufgetreten.",
"certificateStatus": "Zertifikatsstatus",
"loading": "Laden",
"restart": "Neustart",
"domains": "Domains",
"domainsDescription": "Domains für Ihre Organisation verwalten",
"domainsSearch": "Domains durchsuchen...",
"domainAdd": "Domain hinzufügen",
"domainAddDescription": "Eine neue Domain in Ihrer Organisation registrieren",
"domainCreate": "Domain erstellen",
"domainCreatedDescription": "Domain erfolgreich erstellt",
"domainDeletedDescription": "Domain erfolgreich gelöscht",
"domainQuestionRemove": "Möchten Sie die Domain {domain} wirklich aus Ihrem Konto entfernen?",
"domainMessageRemove": "Nach dem Entfernen wird die Domain nicht mehr mit Ihrem Konto verknüpft.",
"domainMessageConfirm": "Um zu bestätigen, geben Sie bitte den Domainnamen unten ein.",
"domainConfirmDelete": "Domain-Löschung bestätigen",
"domainDelete": "Domain löschen",
"domain": "Domain",
"selectDomainTypeNsName": "Domain-Delegation (NS)",
"selectDomainTypeNsDescription": "Diese Domain und alle ihre Subdomains. Verwenden Sie dies, wenn Sie eine gesamte Domainzone kontrollieren möchten.",
"selectDomainTypeCnameName": "Einzelne Domain (CNAME)",
"selectDomainTypeCnameDescription": "Nur diese spezifische Domain. Verwenden Sie dies für einzelne Subdomains oder spezifische Domaineinträge.",
"selectDomainTypeWildcardName": "Wildcard-Domain",
"selectDomainTypeWildcardDescription": "Diese Domain und ihre erste Ebene der Subdomains.",
"domainDelegation": "Einzelne Domain",
"selectType": "Typ auswählen",
"actions": "Aktionen",
"refresh": "Aktualisieren",
"refreshError": "Datenaktualisierung fehlgeschlagen",
"verified": "Verifiziert",
"pending": "Ausstehend",
"sidebarBilling": "Abrechnung",
"billing": "Abrechnung",
"orgBillingDescription": "Verwalten Sie Ihre Rechnungsinformationen und Abonnements",
"github": "GitHub",
"pangolinHosted": "Pangolin Hosted",
"fossorial": "Fossorial",
"completeAccountSetup": "Kontoeinrichtung abschließen",
"completeAccountSetupDescription": "Legen Sie Ihr Passwort fest, um zu beginnen",
"accountSetupSent": "Wir senden einen Code für die Kontoeinrichtung an diese E-Mail-Adresse.",
"accountSetupCode": "Einrichtungscode",
"accountSetupCodeDescription": "Prüfen Sie Ihre E-Mail auf den Einrichtungscode.",
"passwordCreate": "Passwort erstellen",
"passwordCreateConfirm": "Passwort bestätigen",
"accountSetupSubmit": "Einrichtungscode senden",
"completeSetup": "Einrichtung abschließen",
"accountSetupSuccess": "Kontoeinrichtung abgeschlossen! Willkommen bei Pangolin!",
"documentation": "Dokumentation",
"saveAllSettings": "Alle Einstellungen speichern",
"settingsUpdated": "Einstellungen aktualisiert",
"settingsUpdatedDescription": "Alle Einstellungen wurden erfolgreich aktualisiert",
"settingsErrorUpdate": "Einstellungen konnten nicht aktualisiert werden",
"settingsErrorUpdateDescription": "Beim Aktualisieren der Einstellungen ist ein Fehler aufgetreten",
"sidebarCollapse": "Zusammenklappen",
"sidebarExpand": "Erweitern",
"newtUpdateAvailable": "Update verfügbar",
"newtUpdateAvailableInfo": "Eine neue Version von Newt ist verfügbar. Bitte aktualisieren Sie auf die neueste Version für das beste Erlebnis.",
"domainPickerEnterDomain": "Geben Sie Ihre Domain ein",
"domainPickerPlaceholder": "myapp.example.com, api.v1.mydomain.com, oder einfach myapp",
"domainPickerDescription": "Geben Sie die vollständige Domäne der Ressource ein, um verfügbare Optionen zu sehen.",
"domainPickerDescriptionSaas": "Geben Sie eine vollständige Domäne, Subdomäne oder einfach einen Namen ein, um verfügbare Optionen zu sehen",
"domainPickerTabAll": "Alle",
"domainPickerTabOrganization": "Organisation",
"domainPickerTabProvided": "Bereitgestellt",
"domainPickerSortAsc": "A-Z",
"domainPickerSortDesc": "Z-A",
"domainPickerCheckingAvailability": "Verfügbarkeit prüfen...",
"domainPickerNoMatchingDomains": "Keine passenden Domains für \"{userInput}\" gefunden. Versuchen Sie es mit einer anderen Domain oder überprüfen Sie die Domain-Einstellungen Ihrer Organisation.",
"domainPickerOrganizationDomains": "Organisations-Domains",
"domainPickerProvidedDomains": "Bereitgestellte Domains",
"domainPickerSubdomain": "Subdomain: {subdomain}",
"domainPickerNamespace": "Namespace: {namespace}",
"domainPickerShowMore": "Mehr anzeigen",
"domainNotFound": "Domain nicht gefunden",
"domainNotFoundDescription": "Diese Ressource ist deaktiviert, weil die Domain nicht mehr in unserem System existiert. Bitte setzen Sie eine neue Domain für diese Ressource.",
"failed": "Fehlgeschlagen",
"createNewOrgDescription": "Eine neue Organisation erstellen",
"organization": "Organisation",
"port": "Port",
"securityKeyManage": "Sicherheitsschlüssel verwalten", "securityKeyManage": "Sicherheitsschlüssel verwalten",
"securityKeyDescription": "Sicherheitsschlüssel für passwortlose Authentifizierung hinzufügen oder entfernen", "securityKeyDescription": "Sicherheitsschlüssel für passwortlose Authentifizierung hinzufügen oder entfernen",
"securityKeyRegister": "Neuen Sicherheitsschlüssel registrieren", "securityKeyRegister": "Neuen Sicherheitsschlüssel registrieren",
@ -1142,13 +1227,51 @@
"securityKeyRemove": "Entfernen", "securityKeyRemove": "Entfernen",
"securityKeyLastUsed": "Zuletzt verwendet: {date}", "securityKeyLastUsed": "Zuletzt verwendet: {date}",
"securityKeyNameLabel": "Name", "securityKeyNameLabel": "Name",
"securityKeyNamePlaceholder": "Geben Sie einen Namen für diesen Sicherheitsschlüssel ein",
"securityKeyRegisterSuccess": "Sicherheitsschlüssel erfolgreich registriert", "securityKeyRegisterSuccess": "Sicherheitsschlüssel erfolgreich registriert",
"securityKeyRegisterError": "Fehler beim Registrieren des Sicherheitsschlüssels", "securityKeyRegisterError": "Fehler beim Registrieren des Sicherheitsschlüssels",
"securityKeyRemoveSuccess": "Sicherheitsschlüssel erfolgreich entfernt", "securityKeyRemoveSuccess": "Sicherheitsschlüssel erfolgreich entfernt",
"securityKeyRemoveError": "Fehler beim Entfernen des Sicherheitsschlüssels", "securityKeyRemoveError": "Fehler beim Entfernen des Sicherheitsschlüssels",
"securityKeyLoadError": "Fehler beim Laden der Sicherheitsschlüssel", "securityKeyLoadError": "Fehler beim Laden der Sicherheitsschlüssel",
"securityKeyLogin": "Mit Sicherheitsschlüssel anmelden", "securityKeyLogin": "Mit dem Sicherheitsschlüssel fortfahren",
"securityKeyAuthError": "Fehler bei der Authentifizierung mit Sicherheitsschlüssel", "securityKeyAuthError": "Fehler bei der Authentifizierung mit Sicherheitsschlüssel",
"securityKeyRecommendation": "Erwägen Sie die Registrierung eines weiteren Sicherheitsschlüssels auf einem anderen Gerät, um sicherzustellen, dass Sie sich nicht aus Ihrem Konto aussperren." "securityKeyRecommendation": "Erwägen Sie die Registrierung eines weiteren Sicherheitsschlüssels auf einem anderen Gerät, um sicherzustellen, dass Sie sich nicht aus Ihrem Konto aussperren.",
"registering": "Registrierung...",
"securityKeyPrompt": "Bitte bestätigen Sie Ihre Identität mit Ihrem Sicherheitsschlüssel. Stellen Sie sicher, dass Ihr Sicherheitsschlüssel verbunden und einsatzbereit ist.",
"securityKeyBrowserNotSupported": "Ihr Browser unterstützt Sicherheitsschlüssel nicht. Bitte verwenden Sie einen modernen Browser wie Chrome, Firefox oder Safari.",
"securityKeyPermissionDenied": "Bitte erlauben Sie den Zugriff auf Ihren Sicherheitsschlüssel, um sich weiter anzumelden.",
"securityKeyRemovedTooQuickly": "Lassen Sie Ihren Sicherheitsschlüssel verbunden, bis der Anmeldeprozess abgeschlossen ist.",
"securityKeyNotSupported": "Ihr Sicherheitsschlüssel ist möglicherweise nicht kompatibel. Bitte versuchen Sie einen anderen Sicherheitsschlüssel.",
"securityKeyUnknownError": "Es gab ein Problem mit Ihrem Sicherheitsschlüssel. Bitte versuchen Sie es erneut.",
"twoFactorRequired": "Zur Registrierung eines Sicherheitsschlüssels ist eine Zwei-Faktor-Authentifizierung erforderlich.",
"twoFactor": "Zwei-Faktor-Authentifizierung",
"adminEnabled2FaOnYourAccount": "Ihr Administrator hat die Zwei-Faktor-Authentifizierung für {email} aktiviert. Bitte schließen Sie den Einrichtungsprozess ab, um fortzufahren.",
"continueToApplication": "Weiter zur Anwendung",
"securityKeyAdd": "Sicherheitsschlüssel hinzufügen",
"securityKeyRegisterTitle": "Neuen Sicherheitsschlüssel registrieren",
"securityKeyRegisterDescription": "Verbinden Sie Ihren Sicherheitsschlüssel und geben Sie einen Namen ein, um ihn zu identifizieren",
"securityKeyTwoFactorRequired": "Zwei-Faktor-Authentifizierung erforderlich",
"securityKeyTwoFactorDescription": "Bitte geben Sie Ihren Zwei-Faktor-Authentifizierungscode ein, um den Sicherheitsschlüssel zu registrieren",
"securityKeyTwoFactorRemoveDescription": "Bitte geben Sie Ihren Zwei-Faktor-Authentifizierungscode ein, um den Sicherheitsschlüssel zu entfernen",
"securityKeyTwoFactorCode": "Zwei-Faktor-Code",
"securityKeyRemoveTitle": "Sicherheitsschlüssel entfernen",
"securityKeyRemoveDescription": "Geben Sie Ihr Passwort ein, um den Sicherheitsschlüssel \"{name}\" zu entfernen",
"securityKeyNoKeysRegistered": "Keine Sicherheitsschlüssel registriert",
"securityKeyNoKeysDescription": "Fügen Sie einen Sicherheitsschlüssel hinzu, um die Sicherheit Ihres Kontos zu erhöhen",
"createDomainRequired": "Domain ist erforderlich",
"createDomainAddDnsRecords": "DNS-Einträge hinzufügen",
"createDomainAddDnsRecordsDescription": "Fügen Sie die folgenden DNS-Einträge zu Ihrem Domain-Provider hinzu, um die Einrichtung abzuschließen.",
"createDomainNsRecords": "NS-Einträge",
"createDomainRecord": "Eintrag",
"createDomainType": "Typ:",
"createDomainName": "Name:",
"createDomainValue": "Wert:",
"createDomainCnameRecords": "CNAME-Einträge",
"createDomainRecordNumber": "Eintrag {number}",
"createDomainTxtRecords": "TXT-Einträge",
"createDomainSaveTheseRecords": "Diese Einträge speichern",
"createDomainSaveTheseRecordsDescription": "Achten Sie darauf, diese DNS-Einträge zu speichern, da Sie sie nicht erneut sehen werden.",
"createDomainDnsPropagation": "DNS-Verbreitung",
"createDomainDnsPropagationDescription": "Es kann einige Zeit dauern, bis DNS-Änderungen im Internet verbreitet werden. Dies kann je nach Ihrem DNS-Provider und den TTL-Einstellungen von einigen Minuten bis zu 48 Stunden dauern.",
"resourcePortRequired": "Portnummer ist für nicht-HTTP-Ressourcen erforderlich",
"resourcePortNotAllowed": "Portnummer sollte für HTTP-Ressourcen nicht gesetzt werden"
} }

View file

@ -10,7 +10,8 @@
"setupErrorIdentifier": "Organization ID is already taken. Please choose a different one.", "setupErrorIdentifier": "Organization ID is already taken. Please choose a different one.",
"componentsErrorNoMemberCreate": "You are not currently a member of any organizations. Create an organization to get started.", "componentsErrorNoMemberCreate": "You are not currently a member of any organizations. Create an organization to get started.",
"componentsErrorNoMember": "You are not currently a member of any organizations.", "componentsErrorNoMember": "You are not currently a member of any organizations.",
"welcome": "Welcome to Pangolin", "welcome": "Welcome!",
"welcomeTo": "Welcome to",
"componentsCreateOrg": "Create an Organization", "componentsCreateOrg": "Create an Organization",
"componentsMember": "You're a member of {count, plural, =0 {no organization} one {one organization} other {# organizations}}.", "componentsMember": "You're a member of {count, plural, =0 {no organization} one {one organization} other {# organizations}}.",
"componentsInvalidKey": "Invalid or expired license keys detected. Follow license terms to continue using all features.", "componentsInvalidKey": "Invalid or expired license keys detected. Follow license terms to continue using all features.",
@ -206,6 +207,7 @@
"orgGeneralSettings": "Organization Settings", "orgGeneralSettings": "Organization Settings",
"orgGeneralSettingsDescription": "Manage your organization details and configuration", "orgGeneralSettingsDescription": "Manage your organization details and configuration",
"saveGeneralSettings": "Save General Settings", "saveGeneralSettings": "Save General Settings",
"saveSettings": "Save Settings",
"orgDangerZone": "Danger Zone", "orgDangerZone": "Danger Zone",
"orgDangerZoneDescription": "Once you delete this org, there is no going back. Please be certain.", "orgDangerZoneDescription": "Once you delete this org, there is no going back. Please be certain.",
"orgDelete": "Delete Organization", "orgDelete": "Delete Organization",
@ -1107,6 +1109,8 @@
"sidebarAllUsers": "All Users", "sidebarAllUsers": "All Users",
"sidebarIdentityProviders": "Identity Providers", "sidebarIdentityProviders": "Identity Providers",
"sidebarLicense": "License", "sidebarLicense": "License",
"sidebarClients": "Clients",
"sidebarDomains": "Domains",
"enableDockerSocket": "Enable Docker Socket", "enableDockerSocket": "Enable Docker Socket",
"enableDockerSocketDescription": "Enable Docker Socket discovery for populating container information. Socket path must be provided to Newt.", "enableDockerSocketDescription": "Enable Docker Socket discovery for populating container information. Socket path must be provided to Newt.",
"enableDockerSocketLink": "Learn More", "enableDockerSocketLink": "Learn More",
@ -1146,11 +1150,12 @@
"dark": "dark", "dark": "dark",
"system": "system", "system": "system",
"theme": "Theme", "theme": "Theme",
"subnetRequired": "Subnet is required",
"initialSetupTitle": "Initial Server Setup", "initialSetupTitle": "Initial Server Setup",
"initialSetupDescription": "Create the intial server admin account. Only one server admin can exist. You can always change these credentials later.", "initialSetupDescription": "Create the intial server admin account. Only one server admin can exist. You can always change these credentials later.",
"createAdminAccount": "Create Admin Account", "createAdminAccount": "Create Admin Account",
"setupErrorCreateAdmin": "An error occurred while creating the server admin account.", "setupErrorCreateAdmin": "An error occurred while creating the server admin account.",
"passwordReset": "Password Reset", "passwordReset": "Password Reset",
"passwordResetAdminInstructions": "Clicking the button below will send a password reset email to the user. They will be able to set a new password using the link provided.", "passwordResetAdminInstructions": "Clicking the button below will send a password reset email to the user. They will be able to set a new password using the link provided.",
"passwordResetSent": "Password Reset Sent", "passwordResetSent": "Password Reset Sent",
"passwordResetSentDescription": "A password reset email has been sent to {email}.", "passwordResetSentDescription": "A password reset email has been sent to {email}.",
@ -1177,6 +1182,84 @@
"sendEmailNotification": "Send Email Notification", "sendEmailNotification": "Send Email Notification",
"linkCopied": "Link Copied", "linkCopied": "Link Copied",
"linkCopiedDescription": "The reset link has been copied to your clipboard", "linkCopiedDescription": "The reset link has been copied to your clipboard",
"certificateStatus": "Certificate Status",
"loading": "Loading",
"restart": "Restart",
"domains": "Domains",
"domainsDescription": "Manage domains for your organization",
"domainsSearch": "Search domains...",
"domainAdd": "Add Domain",
"domainAddDescription": "Register a new domain with your organization",
"domainCreate": "Create Domain",
"domainCreatedDescription": "Domain created successfully",
"domainDeletedDescription": "Domain deleted successfully",
"domainQuestionRemove": "Are you sure you want to remove the domain {domain} from your account?",
"domainMessageRemove": "Once removed, the domain will no longer be associated with your account.",
"domainMessageConfirm": "To confirm, please type the domain name below.",
"domainConfirmDelete": "Confirm Delete Domain",
"domainDelete": "Delete Domain",
"domain": "Domain",
"selectDomainTypeNsName": "Domain Delegation (NS)",
"selectDomainTypeNsDescription": "This domain and all its subdomains. Use this when you want to control an entire domain zone.",
"selectDomainTypeCnameName": "Single Domain (CNAME)",
"selectDomainTypeCnameDescription": "Just this specific domain. Use this for individual subdomains or specific domain entries.",
"selectDomainTypeWildcardName": "Wildcard Domain",
"selectDomainTypeWildcardDescription": "This domain and its first level of subdomains.",
"domainDelegation": "Single Domain",
"selectType": "Select a type",
"actions": "Actions",
"refresh": "Refresh",
"refreshError": "Failed to refresh data",
"verified": "Verified",
"pending": "Pending",
"sidebarBilling": "Billing",
"billing": "Billing",
"orgBillingDescription": "Manage your billing information and subscriptions",
"github": "GitHub",
"pangolinHosted": "Pangolin Hosted",
"fossorial": "Fossorial",
"completeAccountSetup": "Complete Account Setup",
"completeAccountSetupDescription": "Set your password to get started",
"accountSetupSent": "We'll send an account setup code to this email address.",
"accountSetupCode": "Setup Code",
"accountSetupCodeDescription": "Check your email for the setup code.",
"passwordCreate": "Create Password",
"passwordCreateConfirm": "Confirm Password",
"accountSetupSubmit": "Send Setup Code",
"completeSetup": "Complete Setup",
"accountSetupSuccess": "Account setup completed! Welcome to Pangolin!",
"documentation": "Documentation",
"saveAllSettings": "Save All Settings",
"settingsUpdated": "Settings updated",
"settingsUpdatedDescription": "All settings have been updated successfully",
"settingsErrorUpdate": "Failed to update settings",
"settingsErrorUpdateDescription": "An error occurred while updating settings",
"sidebarCollapse": "Collapse",
"sidebarExpand": "Expand",
"newtUpdateAvailable": "Update Available",
"newtUpdateAvailableInfo": "A new version of Newt is available. Please update to the latest version for the best experience.",
"domainPickerEnterDomain": "Enter your domain",
"domainPickerPlaceholder": "myapp.example.com, api.v1.mydomain.com, or just myapp",
"domainPickerDescription": "Enter the full domain of the resource to see available options.",
"domainPickerDescriptionSaas": "Enter a full domain, subdomain, or just a name to see available options",
"domainPickerTabAll": "All",
"domainPickerTabOrganization": "Organization",
"domainPickerTabProvided": "Provided",
"domainPickerSortAsc": "A-Z",
"domainPickerSortDesc": "Z-A",
"domainPickerCheckingAvailability": "Checking availability...",
"domainPickerNoMatchingDomains": "No matching domains found for \"{userInput}\". Try a different domain or check your organization's domain settings.",
"domainPickerOrganizationDomains": "Organization Domains",
"domainPickerProvidedDomains": "Provided Domains",
"domainPickerSubdomain": "Subdomain: {subdomain}",
"domainPickerNamespace": "Namespace: {namespace}",
"domainPickerShowMore": "Show More",
"domainNotFound": "Domain Not Found",
"domainNotFoundDescription": "This resource is disabled because the domain no longer exists our system. Please set a new domain for this resource.",
"failed": "Failed",
"createNewOrgDescription": "Create a new organization",
"organization": "Organization",
"port": "Port",
"securityKeyManage": "Manage Security Keys", "securityKeyManage": "Manage Security Keys",
"securityKeyDescription": "Add or remove security keys for passwordless authentication", "securityKeyDescription": "Add or remove security keys for passwordless authentication",
"securityKeyRegister": "Register New Security Key", "securityKeyRegister": "Register New Security Key",
@ -1185,16 +1268,15 @@
"securityKeyNameRequired": "Name is required", "securityKeyNameRequired": "Name is required",
"securityKeyRemove": "Remove", "securityKeyRemove": "Remove",
"securityKeyLastUsed": "Last used: {date}", "securityKeyLastUsed": "Last used: {date}",
"securityKeyNameLabel": "Name", "securityKeyNameLabel": "Security Key Name",
"securityKeyNamePlaceholder": "Enter a name for this security key",
"securityKeyRegisterSuccess": "Security key registered successfully", "securityKeyRegisterSuccess": "Security key registered successfully",
"securityKeyRegisterError": "Failed to register security key", "securityKeyRegisterError": "Failed to register security key",
"securityKeyRemoveSuccess": "Security key removed successfully", "securityKeyRemoveSuccess": "Security key removed successfully",
"securityKeyRemoveError": "Failed to remove security key", "securityKeyRemoveError": "Failed to remove security key",
"securityKeyLoadError": "Failed to load security keys", "securityKeyLoadError": "Failed to load security keys",
"securityKeyLogin": "Sign in with security key", "securityKeyLogin": "Continue with security key",
"securityKeyAuthError": "Failed to authenticate with security key", "securityKeyAuthError": "Failed to authenticate with security key",
"securityKeyRecommendation": "Tip: Register a backup security key on another device to ensure you always have access to your account.", "securityKeyRecommendation": "Register a backup security key on another device to ensure you always have access to your account.",
"registering": "Registering...", "registering": "Registering...",
"securityKeyPrompt": "Please verify your identity using your security key. Make sure your security key is connected and ready.", "securityKeyPrompt": "Please verify your identity using your security key. Make sure your security key is connected and ready.",
"securityKeyBrowserNotSupported": "Your browser doesn't support security keys. Please use a modern browser like Chrome, Firefox, or Safari.", "securityKeyBrowserNotSupported": "Your browser doesn't support security keys. Please use a modern browser like Chrome, Firefox, or Safari.",
@ -1205,5 +1287,34 @@
"twoFactorRequired": "Two-factor authentication is required to register a security key.", "twoFactorRequired": "Two-factor authentication is required to register a security key.",
"twoFactor": "Two-Factor Authentication", "twoFactor": "Two-Factor Authentication",
"adminEnabled2FaOnYourAccount": "Your administrator has enabled two-factor authentication for {email}. Please complete the setup process to continue.", "adminEnabled2FaOnYourAccount": "Your administrator has enabled two-factor authentication for {email}. Please complete the setup process to continue.",
"continueToApplication": "Continue to Application" "continueToApplication": "Continue to Application",
"securityKeyAdd": "Add Security Key",
"securityKeyRegisterTitle": "Register New Security Key",
"securityKeyRegisterDescription": "Connect your security key and enter a name to identify it",
"securityKeyTwoFactorRequired": "Two-Factor Authentication Required",
"securityKeyTwoFactorDescription": "Please enter your two-factor authentication code to register the security key",
"securityKeyTwoFactorRemoveDescription": "Please enter your two-factor authentication code to remove the security key",
"securityKeyTwoFactorCode": "Two-Factor Code",
"securityKeyRemoveTitle": "Remove Security Key",
"securityKeyRemoveDescription": "Enter your password to remove the security key \"{name}\"",
"securityKeyNoKeysRegistered": "No security keys registered",
"securityKeyNoKeysDescription": "Add a security key to enhance your account security",
"createDomainRequired": "Domain is required",
"createDomainAddDnsRecords": "Add DNS Records",
"createDomainAddDnsRecordsDescription": "Add the following DNS records to your domain provider to complete the setup.",
"createDomainNsRecords": "NS Records",
"createDomainRecord": "Record",
"createDomainType": "Type:",
"createDomainName": "Name:",
"createDomainValue": "Value:",
"createDomainCnameRecords": "CNAME Records",
"createDomainARecords": "A Records",
"createDomainRecordNumber": "Record {number}",
"createDomainTxtRecords": "TXT Records",
"createDomainSaveTheseRecords": "Save These Records",
"createDomainSaveTheseRecordsDescription": "Make sure to save these DNS records as you will not see them again.",
"createDomainDnsPropagation": "DNS Propagation",
"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.",
"resourcePortRequired": "Port number is required for non-HTTP resources",
"resourcePortNotAllowed": "Port number should not be set for HTTP resources"
} }

View file

@ -11,8 +11,9 @@
"componentsErrorNoMemberCreate": "Actualmente no eres miembro de ninguna organización. Crea una organización para empezar.", "componentsErrorNoMemberCreate": "Actualmente no eres miembro de ninguna organización. Crea una organización para empezar.",
"componentsErrorNoMember": "Actualmente no eres miembro de ninguna organización.", "componentsErrorNoMember": "Actualmente no eres miembro de ninguna organización.",
"welcome": "Bienvenido a Pangolin", "welcome": "Bienvenido a Pangolin",
"welcomeTo": "Bienvenido a",
"componentsCreateOrg": "Crear una organización", "componentsCreateOrg": "Crear una organización",
"componentsMember": "¡Eres un miembro de {count, plural, =0 {¡Ninguna organización} =1 {¡una organización} other {# organizaciones}}.", "componentsMember": "Eres un miembro de {count, plural, =0 {ninguna organización} one {una organización} other {# organizaciones}}.",
"componentsInvalidKey": "Se han detectado claves de licencia inválidas o caducadas. Siga los términos de licencia para seguir usando todas las características.", "componentsInvalidKey": "Se han detectado claves de licencia inválidas o caducadas. Siga los términos de licencia para seguir usando todas las características.",
"dismiss": "Descartar", "dismiss": "Descartar",
"componentsLicenseViolation": "Violación de la Licencia: Este servidor está usando sitios {usedSites} que exceden su límite de licencias de sitios {maxSites} . Siga los términos de licencia para seguir usando todas las características.", "componentsLicenseViolation": "Violación de la Licencia: Este servidor está usando sitios {usedSites} que exceden su límite de licencias de sitios {maxSites} . Siga los términos de licencia para seguir usando todas las características.",
@ -206,6 +207,7 @@
"orgGeneralSettings": "Configuración de la organización", "orgGeneralSettings": "Configuración de la organización",
"orgGeneralSettingsDescription": "Administra los detalles y la configuración de tu organización", "orgGeneralSettingsDescription": "Administra los detalles y la configuración de tu organización",
"saveGeneralSettings": "Guardar ajustes generales", "saveGeneralSettings": "Guardar ajustes generales",
"saveSettings": "Guardar ajustes",
"orgDangerZone": "Zona de peligro", "orgDangerZone": "Zona de peligro",
"orgDangerZoneDescription": "Una vez que elimines este órgano, no hay vuelta atrás. Por favor, asegúrate de ello.", "orgDangerZoneDescription": "Una vez que elimines este órgano, no hay vuelta atrás. Por favor, asegúrate de ello.",
"orgDelete": "Eliminar organización", "orgDelete": "Eliminar organización",
@ -249,7 +251,7 @@
"weeks": "Semanas", "weeks": "Semanas",
"months": "Meses", "months": "Meses",
"years": "Años", "years": "Años",
"day": "{count, plural, =1 {# día} other {# días}}", "day": "{count, plural, one {# día} other {# días}}",
"apiKeysTitle": "Información de Clave API", "apiKeysTitle": "Información de Clave API",
"apiKeysConfirmCopy2": "Debes confirmar que has copiado la clave API.", "apiKeysConfirmCopy2": "Debes confirmar que has copiado la clave API.",
"apiKeysErrorCreate": "Error al crear la clave API", "apiKeysErrorCreate": "Error al crear la clave API",
@ -347,7 +349,7 @@
"licensePurchase": "Comprar Licencia", "licensePurchase": "Comprar Licencia",
"licensePurchaseSites": "Comprar sitios adicionales", "licensePurchaseSites": "Comprar sitios adicionales",
"licenseSitesUsedMax": "{usedSites} de {maxSites} sitios usados", "licenseSitesUsedMax": "{usedSites} de {maxSites} sitios usados",
"licenseSitesUsed": "{count, plural, =0 {# sitios} =1 {# sitio} other {# sitios}} en el sistema.", "licenseSitesUsed": "{count, plural, =0 {# sitios} one {# sitio} other {# sitios}} en el sistema.",
"licensePurchaseDescription": "Elige cuántos sitios quieres {selectedMode, select, license {compra una licencia para. Siempre puedes añadir más sitios más tarde.} other {añadir a tu licencia existente.}}", "licensePurchaseDescription": "Elige cuántos sitios quieres {selectedMode, select, license {compra una licencia para. Siempre puedes añadir más sitios más tarde.} other {añadir a tu licencia existente.}}",
"licenseFee": "Tarifa de licencia", "licenseFee": "Tarifa de licencia",
"licensePriceSite": "Precio por sitio", "licensePriceSite": "Precio por sitio",
@ -436,7 +438,7 @@
"accessRoleSelect": "Seleccionar rol", "accessRoleSelect": "Seleccionar rol",
"inviteEmailSentDescription": "Se ha enviado un correo electrónico al usuario con el siguiente enlace de acceso. Debe acceder al enlace para aceptar la invitación.", "inviteEmailSentDescription": "Se ha enviado un correo electrónico al usuario con el siguiente enlace de acceso. Debe acceder al enlace para aceptar la invitación.",
"inviteSentDescription": "El usuario ha sido invitado. Debe acceder al enlace de abajo para aceptar la invitación.", "inviteSentDescription": "El usuario ha sido invitado. Debe acceder al enlace de abajo para aceptar la invitación.",
"inviteExpiresIn": "La invitación expirará en {days, plural, =1 {# día} other {# días}}.", "inviteExpiresIn": "La invitación expirará en {days, plural, one {# día} other {# días}}.",
"idpTitle": "Proveedor de identidad", "idpTitle": "Proveedor de identidad",
"idpSelect": "Seleccione el proveedor de identidad para el usuario externo", "idpSelect": "Seleccione el proveedor de identidad para el usuario externo",
"idpNotConfigured": "No hay proveedores de identidad configurados. Por favor, configure un proveedor de identidad antes de crear usuarios externos.", "idpNotConfigured": "No hay proveedores de identidad configurados. Por favor, configure un proveedor de identidad antes de crear usuarios externos.",
@ -958,6 +960,8 @@
"licenseTierProfessionalRequiredDescription": "Esta característica sólo está disponible en la Edición Profesional.", "licenseTierProfessionalRequiredDescription": "Esta característica sólo está disponible en la Edición Profesional.",
"actionGetOrg": "Obtener organización", "actionGetOrg": "Obtener organización",
"actionUpdateOrg": "Actualizar organización", "actionUpdateOrg": "Actualizar organización",
"actionUpdateUser": "Actualizar usuario",
"actionGetUser": "Obtener usuario",
"actionGetOrgUser": "Obtener usuario de la organización", "actionGetOrgUser": "Obtener usuario de la organización",
"actionListOrgDomains": "Listar dominios de la organización", "actionListOrgDomains": "Listar dominios de la organización",
"actionCreateSite": "Crear sitio", "actionCreateSite": "Crear sitio",
@ -1090,6 +1094,8 @@
"sidebarAllUsers": "Todos los usuarios", "sidebarAllUsers": "Todos los usuarios",
"sidebarIdentityProviders": "Proveedores de identidad", "sidebarIdentityProviders": "Proveedores de identidad",
"sidebarLicense": "Licencia", "sidebarLicense": "Licencia",
"sidebarClients": "Clientes",
"sidebarDomains": "Dominios",
"enableDockerSocket": "Habilitar conector Docker", "enableDockerSocket": "Habilitar conector Docker",
"enableDockerSocketDescription": "Habilitar el descubrimiento de Docker Socket para completar la información del contenedor. La ruta del socket debe proporcionarse a Newt.", "enableDockerSocketDescription": "Habilitar el descubrimiento de Docker Socket para completar la información del contenedor. La ruta del socket debe proporcionarse a Newt.",
"enableDockerSocketLink": "Saber más", "enableDockerSocketLink": "Saber más",
@ -1102,7 +1108,7 @@
"containerNetworks": "Redes", "containerNetworks": "Redes",
"containerHostnameIp": "Nombre del host/IP", "containerHostnameIp": "Nombre del host/IP",
"containerLabels": "Etiquetas", "containerLabels": "Etiquetas",
"containerLabelsCount": "{count} etiqueta{s,plural,one{} other{s}}", "containerLabelsCount": "{count, plural, one {# etiqueta} other {# etiquetas}}",
"containerLabelsTitle": "Etiquetas de contenedor", "containerLabelsTitle": "Etiquetas de contenedor",
"containerLabelEmpty": "<vacío>", "containerLabelEmpty": "<vacío>",
"containerPorts": "Puertos", "containerPorts": "Puertos",
@ -1114,7 +1120,7 @@
"showStoppedContainers": "Mostrar contenedores parados", "showStoppedContainers": "Mostrar contenedores parados",
"noContainersFound": "No se han encontrado contenedores. Asegúrate de que los contenedores Docker se estén ejecutando.", "noContainersFound": "No se han encontrado contenedores. Asegúrate de que los contenedores Docker se estén ejecutando.",
"searchContainersPlaceholder": "Buscar a través de contenedores {count}...", "searchContainersPlaceholder": "Buscar a través de contenedores {count}...",
"searchResultsCount": "{count} resultado{s,plural,one{} other{s}}", "searchResultsCount": "{count, plural, one {# resultado} other {# resultados}}",
"filters": "Filtros", "filters": "Filtros",
"filterOptions": "Opciones de filtro", "filterOptions": "Opciones de filtro",
"filterPorts": "Puertos", "filterPorts": "Puertos",
@ -1129,10 +1135,89 @@
"dark": "oscuro", "dark": "oscuro",
"system": "sistema", "system": "sistema",
"theme": "Tema", "theme": "Tema",
"subnetRequired": "Se requiere subred",
"initialSetupTitle": "Configuración inicial del servidor", "initialSetupTitle": "Configuración inicial del servidor",
"initialSetupDescription": "Cree la cuenta de administrador del servidor inicial. Solo puede existir un administrador del servidor. Siempre puede cambiar estas credenciales más tarde.", "initialSetupDescription": "Cree la cuenta de administrador del servidor inicial. Solo puede existir un administrador del servidor. Siempre puede cambiar estas credenciales más tarde.",
"createAdminAccount": "Crear cuenta de administrador", "createAdminAccount": "Crear cuenta de administrador",
"setupErrorCreateAdmin": "Se produjo un error al crear la cuenta de administrador del servidor.", "setupErrorCreateAdmin": "Se produjo un error al crear la cuenta de administrador del servidor.",
"certificateStatus": "Estado del certificado",
"loading": "Cargando",
"restart": "Reiniciar",
"domains": "Dominios",
"domainsDescription": "Administrar dominios de tu organización",
"domainsSearch": "Buscar dominios...",
"domainAdd": "Agregar dominio",
"domainAddDescription": "Registrar un nuevo dominio con tu organización",
"domainCreate": "Crear dominio",
"domainCreatedDescription": "Dominio creado con éxito",
"domainDeletedDescription": "Dominio eliminado exitosamente",
"domainQuestionRemove": "¿Está seguro de que desea eliminar el dominio {domain} de su cuenta?",
"domainMessageRemove": "Una vez eliminado, el dominio ya no estará asociado con su cuenta.",
"domainMessageConfirm": "Para confirmar, por favor escriba el nombre del dominio abajo.",
"domainConfirmDelete": "Confirmar eliminación del dominio",
"domainDelete": "Eliminar dominio",
"domain": "Dominio",
"selectDomainTypeNsName": "Delegación de dominio (NS)",
"selectDomainTypeNsDescription": "Este dominio y todos sus subdominios. Usa esto cuando quieras controlar una zona de dominio completa.",
"selectDomainTypeCnameName": "Dominio único (CNAME)",
"selectDomainTypeCnameDescription": "Solo este dominio específico. Úsalo para subdominios individuales o entradas específicas de dominio.",
"selectDomainTypeWildcardName": "Dominio comodín",
"selectDomainTypeWildcardDescription": "Este dominio y su primer nivel de subdominios.",
"domainDelegation": "Dominio único",
"selectType": "Selecciona un tipo",
"actions": "Acciones",
"refresh": "Actualizar",
"refreshError": "Error al actualizar datos",
"verified": "Verificado",
"pending": "Pendiente",
"sidebarBilling": "Facturación",
"billing": "Facturación",
"orgBillingDescription": "Gestiona tu información de facturación y suscripciones",
"github": "GitHub",
"pangolinHosted": "Pangolin Hosted",
"fossorial": "Fossorial",
"completeAccountSetup": "Completar configuración de cuenta",
"completeAccountSetupDescription": "Establece tu contraseña para comenzar",
"accountSetupSent": "Enviaremos un código de configuración de cuenta a esta dirección de correo electrónico.",
"accountSetupCode": "Código de configuración",
"accountSetupCodeDescription": "Revisa tu correo para el código de configuración.",
"passwordCreate": "Crear contraseña",
"passwordCreateConfirm": "Confirmar contraseña",
"accountSetupSubmit": "Enviar código de configuración",
"completeSetup": "Completar configuración",
"accountSetupSuccess": "¡Configuración de cuenta completada! ¡Bienvenido a Pangolin!",
"documentation": "Documentación",
"saveAllSettings": "Guardar todos los ajustes",
"settingsUpdated": "Ajustes actualizados",
"settingsUpdatedDescription": "Todos los ajustes han sido actualizados exitosamente",
"settingsErrorUpdate": "Error al actualizar ajustes",
"settingsErrorUpdateDescription": "Ocurrió un error al actualizar ajustes",
"sidebarCollapse": "Colapsar",
"sidebarExpand": "Expandir",
"newtUpdateAvailable": "Nueva actualización disponible",
"newtUpdateAvailableInfo": "Hay una nueva versión de Newt disponible. Actualice a la última versión para la mejor experiencia.",
"domainPickerEnterDomain": "Ingresa tu dominio",
"domainPickerPlaceholder": "myapp.example.com, api.v1.miDominio.com, o solo myapp",
"domainPickerDescription": "Ingresa el dominio completo del recurso para ver las opciones disponibles.",
"domainPickerDescriptionSaas": "Ingresa un dominio completo, subdominio o simplemente un nombre para ver las opciones disponibles",
"domainPickerTabAll": "Todo",
"domainPickerTabOrganization": "Organización",
"domainPickerTabProvided": "Proporcionado",
"domainPickerSortAsc": "A-Z",
"domainPickerSortDesc": "Z-A",
"domainPickerCheckingAvailability": "Comprobando disponibilidad...",
"domainPickerNoMatchingDomains": "No se encontraron dominios coincidentes para \"{userInput}\". Prueba con un dominio diferente o revisa la configuración de dominio de tu organización.",
"domainPickerOrganizationDomains": "Dominios de la organización",
"domainPickerProvidedDomains": "Dominios proporcionados",
"domainPickerSubdomain": "Subdominio: {subdomain}",
"domainPickerNamespace": "Espacio de nombres: {namespace}",
"domainPickerShowMore": "Mostrar más",
"domainNotFound": "Dominio no encontrado",
"domainNotFoundDescription": "Este recurso está deshabilitado porque el dominio ya no existe en nuestro sistema. Por favor, establece un nuevo dominio para este recurso.",
"failed": "Fallido",
"createNewOrgDescription": "Crear una nueva organización",
"organization": "Organización",
"port": "Puerto",
"securityKeyManage": "Gestionar llaves de seguridad", "securityKeyManage": "Gestionar llaves de seguridad",
"securityKeyDescription": "Agregar o eliminar llaves de seguridad para autenticación sin contraseña", "securityKeyDescription": "Agregar o eliminar llaves de seguridad para autenticación sin contraseña",
"securityKeyRegister": "Registrar nueva llave de seguridad", "securityKeyRegister": "Registrar nueva llave de seguridad",
@ -1142,13 +1227,51 @@
"securityKeyRemove": "Eliminar", "securityKeyRemove": "Eliminar",
"securityKeyLastUsed": "Último uso: {date}", "securityKeyLastUsed": "Último uso: {date}",
"securityKeyNameLabel": "Nombre", "securityKeyNameLabel": "Nombre",
"securityKeyNamePlaceholder": "Ingrese un nombre para esta llave de seguridad",
"securityKeyRegisterSuccess": "Llave de seguridad registrada exitosamente", "securityKeyRegisterSuccess": "Llave de seguridad registrada exitosamente",
"securityKeyRegisterError": "Error al registrar la llave de seguridad", "securityKeyRegisterError": "Error al registrar la llave de seguridad",
"securityKeyRemoveSuccess": "Llave de seguridad eliminada exitosamente", "securityKeyRemoveSuccess": "Llave de seguridad eliminada exitosamente",
"securityKeyRemoveError": "Error al eliminar la llave de seguridad", "securityKeyRemoveError": "Error al eliminar la llave de seguridad",
"securityKeyLoadError": "Error al cargar las llaves de seguridad", "securityKeyLoadError": "Error al cargar las llaves de seguridad",
"securityKeyLogin": "Iniciar sesión con llave de seguridad", "securityKeyLogin": "Continuar con clave de seguridad",
"securityKeyAuthError": "Error al autenticar con llave de seguridad", "securityKeyAuthError": "Error al autenticar con llave de seguridad",
"securityKeyRecommendation": "Considere registrar otra llave de seguridad en un dispositivo diferente para asegurarse de no quedar bloqueado de su cuenta." "securityKeyRecommendation": "Considere registrar otra llave de seguridad en un dispositivo diferente para asegurarse de no quedar bloqueado de su cuenta.",
"registering": "Registrando...",
"securityKeyPrompt": "Por favor, verifica tu identidad usando tu llave de seguridad. Asegúrate de que tu llave de seguridad esté conectada y lista.",
"securityKeyBrowserNotSupported": "Tu navegador no admite llaves de seguridad. Por favor, usa un navegador moderno como Chrome, Firefox o Safari.",
"securityKeyPermissionDenied": "Por favor, permite el acceso a tu llave de seguridad para continuar iniciando sesión.",
"securityKeyRemovedTooQuickly": "Por favor, mantén tu llave de seguridad conectada hasta que el proceso de inicio de sesión se complete.",
"securityKeyNotSupported": "Tu llave de seguridad puede no ser compatible. Por favor, prueba con una llave de seguridad diferente.",
"securityKeyUnknownError": "Hubo un problema al usar tu llave de seguridad. Por favor, inténtalo de nuevo.",
"twoFactorRequired": "Se requiere autenticación de dos factores para registrar una llave de seguridad.",
"twoFactor": "Autenticación de dos factores",
"adminEnabled2FaOnYourAccount": "Su administrador ha habilitado la autenticación de dos factores para {email}. Por favor, complete el proceso de configuración para continuar.",
"continueToApplication": "Continuar a la aplicación",
"securityKeyAdd": "Agregar llave de seguridad",
"securityKeyRegisterTitle": "Registrar nueva llave de seguridad",
"securityKeyRegisterDescription": "Conecta tu llave de seguridad y escribe un nombre para identificarla",
"securityKeyTwoFactorRequired": "Se requiere autenticación de dos factores",
"securityKeyTwoFactorDescription": "Por favor, ingresa tu código de autenticación de dos factores para registrar la llave de seguridad",
"securityKeyTwoFactorRemoveDescription": "Por favor, ingresa tu código de autenticación de dos factores para eliminar la llave de seguridad",
"securityKeyTwoFactorCode": "Código de autenticación de dos factores",
"securityKeyRemoveTitle": "Eliminar llave de seguridad",
"securityKeyRemoveDescription": "Ingresa tu contraseña para eliminar la llave de seguridad \"{name}\"",
"securityKeyNoKeysRegistered": "No hay llaves de seguridad registradas",
"securityKeyNoKeysDescription": "Agrega una llave de seguridad para mejorar la seguridad de tu cuenta",
"createDomainRequired": "Se requiere dominio",
"createDomainAddDnsRecords": "Agregar registros DNS",
"createDomainAddDnsRecordsDescription": "Agrega los siguientes registros DNS a tu proveedor de dominios para completar la configuración.",
"createDomainNsRecords": "Registros NS",
"createDomainRecord": "Registro",
"createDomainType": "Tipo:",
"createDomainName": "Nombre:",
"createDomainValue": "Valor:",
"createDomainCnameRecords": "Registros CNAME",
"createDomainRecordNumber": "Registro {number}",
"createDomainTxtRecords": "Registros TXT",
"createDomainSaveTheseRecords": "Guardar estos registros",
"createDomainSaveTheseRecordsDescription": "Asegúrate de guardar estos registros DNS ya que no los verás de nuevo.",
"createDomainDnsPropagation": "Propagación DNS",
"createDomainDnsPropagationDescription": "Los cambios de DNS pueden tardar un tiempo en propagarse a través de internet. Esto puede tardar desde unos pocos minutos hasta 48 horas, dependiendo de tu proveedor de DNS y la configuración de TTL.",
"resourcePortRequired": "Se requiere número de puerto para recursos no HTTP",
"resourcePortNotAllowed": "El número de puerto no debe establecerse para recursos HTTP"
} }

View file

@ -11,8 +11,9 @@
"componentsErrorNoMemberCreate": "Vous n'êtes actuellement membre d'aucune organisation. Créez une organisation pour commencer.", "componentsErrorNoMemberCreate": "Vous n'êtes actuellement membre d'aucune organisation. Créez une organisation pour commencer.",
"componentsErrorNoMember": "Vous n'êtes actuellement membre d'aucune organisation.", "componentsErrorNoMember": "Vous n'êtes actuellement membre d'aucune organisation.",
"welcome": "Bienvenue à Pangolin", "welcome": "Bienvenue à Pangolin",
"welcomeTo": "Bienvenue chez",
"componentsCreateOrg": "Créer une organisation", "componentsCreateOrg": "Créer une organisation",
"componentsMember": "Vous êtes membre de {count, plural, =0 {aucune organisation} =1 {Une organisation} other {# organisations}}.", "componentsMember": "Vous êtes membre de {count, plural, =0 {aucune organisation} one {une organisation} other {# organisations}}.",
"componentsInvalidKey": "Clés de licence invalides ou expirées détectées. Suivez les conditions de licence pour continuer à utiliser toutes les fonctionnalités.", "componentsInvalidKey": "Clés de licence invalides ou expirées détectées. Suivez les conditions de licence pour continuer à utiliser toutes les fonctionnalités.",
"dismiss": "Refuser", "dismiss": "Refuser",
"componentsLicenseViolation": "Violation de licence : Ce serveur utilise des sites {usedSites} qui dépassent la limite autorisée des sites {maxSites} . Suivez les conditions de licence pour continuer à utiliser toutes les fonctionnalités.", "componentsLicenseViolation": "Violation de licence : Ce serveur utilise des sites {usedSites} qui dépassent la limite autorisée des sites {maxSites} . Suivez les conditions de licence pour continuer à utiliser toutes les fonctionnalités.",
@ -206,6 +207,7 @@
"orgGeneralSettings": "Paramètres de l'organisation", "orgGeneralSettings": "Paramètres de l'organisation",
"orgGeneralSettingsDescription": "Gérer les détails et la configuration de votre organisation", "orgGeneralSettingsDescription": "Gérer les détails et la configuration de votre organisation",
"saveGeneralSettings": "Enregistrer les paramètres généraux", "saveGeneralSettings": "Enregistrer les paramètres généraux",
"saveSettings": "Enregistrer les paramètres",
"orgDangerZone": "Zone de danger", "orgDangerZone": "Zone de danger",
"orgDangerZoneDescription": "Une fois que vous supprimez cette organisation, il n'y a pas de retour en arrière. Soyez certain.", "orgDangerZoneDescription": "Une fois que vous supprimez cette organisation, il n'y a pas de retour en arrière. Soyez certain.",
"orgDelete": "Supprimer l'organisation", "orgDelete": "Supprimer l'organisation",
@ -249,7 +251,7 @@
"weeks": "Semaines", "weeks": "Semaines",
"months": "Mois", "months": "Mois",
"years": "Années", "years": "Années",
"day": "{count, plural, =1 {# jour} other {# jours}}", "day": "{count, plural, one {# jour} other {# jours}}",
"apiKeysTitle": "Informations sur la clé API", "apiKeysTitle": "Informations sur la clé API",
"apiKeysConfirmCopy2": "Vous devez confirmer que vous avez copié la clé API.", "apiKeysConfirmCopy2": "Vous devez confirmer que vous avez copié la clé API.",
"apiKeysErrorCreate": "Erreur lors de la création de la clé API", "apiKeysErrorCreate": "Erreur lors de la création de la clé API",
@ -347,7 +349,7 @@
"licensePurchase": "Acheter une licence", "licensePurchase": "Acheter une licence",
"licensePurchaseSites": "Acheter des sites supplémentaires", "licensePurchaseSites": "Acheter des sites supplémentaires",
"licenseSitesUsedMax": "{usedSites} des sites {maxSites} utilisés", "licenseSitesUsedMax": "{usedSites} des sites {maxSites} utilisés",
"licenseSitesUsed": "{count, plural, =0 {# sites} =1 {# site} other {# sites}} dans le système.", "licenseSitesUsed": "{count, plural, =0 {# sites} one {# site} other {# sites}} dans le système.",
"licensePurchaseDescription": "Choisissez le nombre de sites que vous voulez {selectedMode, select, license {achetez une licence. Vous pouvez toujours ajouter plus de sites plus tard.} other {ajouter à votre licence existante.}}", "licensePurchaseDescription": "Choisissez le nombre de sites que vous voulez {selectedMode, select, license {achetez une licence. Vous pouvez toujours ajouter plus de sites plus tard.} other {ajouter à votre licence existante.}}",
"licenseFee": "Frais de licence", "licenseFee": "Frais de licence",
"licensePriceSite": "Prix par site", "licensePriceSite": "Prix par site",
@ -436,7 +438,7 @@
"accessRoleSelect": "Sélectionner un rôle", "accessRoleSelect": "Sélectionner un rôle",
"inviteEmailSentDescription": "Un e-mail a été envoyé à l'utilisateur avec le lien d'accès ci-dessous. Ils doivent accéder au lien pour accepter l'invitation.", "inviteEmailSentDescription": "Un e-mail a été envoyé à l'utilisateur avec le lien d'accès ci-dessous. Ils doivent accéder au lien pour accepter l'invitation.",
"inviteSentDescription": "L'utilisateur a été invité. Ils doivent accéder au lien ci-dessous pour accepter l'invitation.", "inviteSentDescription": "L'utilisateur a été invité. Ils doivent accéder au lien ci-dessous pour accepter l'invitation.",
"inviteExpiresIn": "L'invitation expirera dans {days, plural, =1 {# jour} other {# jours}}.", "inviteExpiresIn": "L'invitation expirera dans {days, plural, one {# jour} other {# jours}}.",
"idpTitle": "Informations générales", "idpTitle": "Informations générales",
"idpSelect": "Sélectionnez le fournisseur d'identité pour l'utilisateur externe", "idpSelect": "Sélectionnez le fournisseur d'identité pour l'utilisateur externe",
"idpNotConfigured": "Aucun fournisseur d'identité n'est configuré. Veuillez configurer un fournisseur d'identité avant de créer des utilisateurs externes.", "idpNotConfigured": "Aucun fournisseur d'identité n'est configuré. Veuillez configurer un fournisseur d'identité avant de créer des utilisateurs externes.",
@ -958,6 +960,8 @@
"licenseTierProfessionalRequiredDescription": "Cette fonctionnalité n'est disponible que dans l'Édition Professionnelle.", "licenseTierProfessionalRequiredDescription": "Cette fonctionnalité n'est disponible que dans l'Édition Professionnelle.",
"actionGetOrg": "Obtenir l'organisation", "actionGetOrg": "Obtenir l'organisation",
"actionUpdateOrg": "Mettre à jour l'organisation", "actionUpdateOrg": "Mettre à jour l'organisation",
"actionUpdateUser": "Mettre à jour l'utilisateur",
"actionGetUser": "Obtenir l'utilisateur",
"actionGetOrgUser": "Obtenir l'utilisateur de l'organisation", "actionGetOrgUser": "Obtenir l'utilisateur de l'organisation",
"actionListOrgDomains": "Lister les domaines de l'organisation", "actionListOrgDomains": "Lister les domaines de l'organisation",
"actionCreateSite": "Créer un site", "actionCreateSite": "Créer un site",
@ -1090,6 +1094,8 @@
"sidebarAllUsers": "Tous les utilisateurs", "sidebarAllUsers": "Tous les utilisateurs",
"sidebarIdentityProviders": "Fournisseurs d'identité", "sidebarIdentityProviders": "Fournisseurs d'identité",
"sidebarLicense": "Licence", "sidebarLicense": "Licence",
"sidebarClients": "Clients",
"sidebarDomains": "Domaines",
"enableDockerSocket": "Activer Docker Socket", "enableDockerSocket": "Activer Docker Socket",
"enableDockerSocketDescription": "Activer la découverte Docker Socket pour remplir les informations du conteneur. Le chemin du socket doit être fourni à Newt.", "enableDockerSocketDescription": "Activer la découverte Docker Socket pour remplir les informations du conteneur. Le chemin du socket doit être fourni à Newt.",
"enableDockerSocketLink": "En savoir plus", "enableDockerSocketLink": "En savoir plus",
@ -1102,7 +1108,7 @@
"containerNetworks": "Réseaux", "containerNetworks": "Réseaux",
"containerHostnameIp": "Nom d'hôte/IP", "containerHostnameIp": "Nom d'hôte/IP",
"containerLabels": "Étiquettes", "containerLabels": "Étiquettes",
"containerLabelsCount": "{count} étiquette{s,plural,one{} other{s}}", "containerLabelsCount": "{count, plural, one {# étiquette} other {# étiquettes}}",
"containerLabelsTitle": "Étiquettes de conteneur", "containerLabelsTitle": "Étiquettes de conteneur",
"containerLabelEmpty": "<vide>", "containerLabelEmpty": "<vide>",
"containerPorts": "Ports", "containerPorts": "Ports",
@ -1114,7 +1120,7 @@
"showStoppedContainers": "Afficher les conteneurs arrêtés", "showStoppedContainers": "Afficher les conteneurs arrêtés",
"noContainersFound": "Aucun conteneur trouvé. Assurez-vous que les conteneurs Docker sont en cours d'exécution.", "noContainersFound": "Aucun conteneur trouvé. Assurez-vous que les conteneurs Docker sont en cours d'exécution.",
"searchContainersPlaceholder": "Rechercher dans les conteneurs {count}...", "searchContainersPlaceholder": "Rechercher dans les conteneurs {count}...",
"searchResultsCount": "{count} résultat{s,plural,one{} other{s}}", "searchResultsCount": "{count, plural, one {# résultat} other {# résultats}}",
"filters": "Filtres", "filters": "Filtres",
"filterOptions": "Options de filtre", "filterOptions": "Options de filtre",
"filterPorts": "Ports", "filterPorts": "Ports",
@ -1129,10 +1135,89 @@
"dark": "sombre", "dark": "sombre",
"system": "système", "system": "système",
"theme": "Thème", "theme": "Thème",
"subnetRequired": "Le sous-réseau est requis",
"initialSetupTitle": "Configuration initiale du serveur", "initialSetupTitle": "Configuration initiale du serveur",
"initialSetupDescription": "Créer le compte administrateur du serveur initial. Un seul administrateur serveur peut exister. Vous pouvez toujours changer ces informations d'identification plus tard.", "initialSetupDescription": "Créer le compte administrateur du serveur initial. Un seul administrateur serveur peut exister. Vous pouvez toujours changer ces informations d'identification plus tard.",
"createAdminAccount": "Créer un compte administrateur", "createAdminAccount": "Créer un compte administrateur",
"setupErrorCreateAdmin": "Une erreur s'est produite lors de la création du compte administrateur du serveur.", "setupErrorCreateAdmin": "Une erreur s'est produite lors de la création du compte administrateur du serveur.",
"certificateStatus": "Statut du certificat",
"loading": "Chargement",
"restart": "Redémarrer",
"domains": "Domaines",
"domainsDescription": "Gérer les domaines de votre organisation",
"domainsSearch": "Rechercher des domaines...",
"domainAdd": "Ajouter un domaine",
"domainAddDescription": "Enregistrez un nouveau domaine avec votre organisation",
"domainCreate": "Créer un domaine",
"domainCreatedDescription": "Domaine créé avec succès",
"domainDeletedDescription": "Domaine supprimé avec succès",
"domainQuestionRemove": "Êtes-vous sûr de vouloir supprimer le domaine {domain} de votre compte ?",
"domainMessageRemove": "Une fois supprimé, le domaine ne sera plus associé à votre compte.",
"domainMessageConfirm": "Pour confirmer, veuillez taper le nom du domaine ci-dessous.",
"domainConfirmDelete": "Confirmer la suppression du domaine",
"domainDelete": "Supprimer le domaine",
"domain": "Domaine",
"selectDomainTypeNsName": "Délégation de domaine (NS)",
"selectDomainTypeNsDescription": "Ce domaine et tous ses sous-domaines. Utilisez cela lorsque vous souhaitez contrôler une zone de domaine entière.",
"selectDomainTypeCnameName": "Domaine unique (CNAME)",
"selectDomainTypeCnameDescription": "Juste ce domaine spécifique. Utilisez ce paramètre pour des sous-domaines individuels ou des entrées de domaine spécifiques.",
"selectDomainTypeWildcardName": "Domaine Générique",
"selectDomainTypeWildcardDescription": "Ce domaine et son premier niveau de sous-domaines.",
"domainDelegation": "Domaine Unique",
"selectType": "Sélectionnez un type",
"actions": "Actions",
"refresh": "Actualiser",
"refreshError": "Échec de l'actualisation des données",
"verified": "Vérifié",
"pending": "En attente",
"sidebarBilling": "Facturation",
"billing": "Facturation",
"orgBillingDescription": "Gérez vos informations de facturation et vos abonnements",
"github": "GitHub",
"pangolinHosted": "Pangolin Hébergement",
"fossorial": "Fossorial",
"completeAccountSetup": "Complétez la configuration du compte",
"completeAccountSetupDescription": "Définissez votre mot de passe pour commencer",
"accountSetupSent": "Nous enverrons un code de configuration de compte à cette adresse e-mail.",
"accountSetupCode": "Code de configuration",
"accountSetupCodeDescription": "Vérifiez votre e-mail pour le code de configuration.",
"passwordCreate": "Créer un mot de passe",
"passwordCreateConfirm": "Confirmer le mot de passe",
"accountSetupSubmit": "Envoyer le code de configuration",
"completeSetup": "Configuration complète",
"accountSetupSuccess": "Configuration du compte terminée! Bienvenue chez Pangolin !",
"documentation": "Documentation",
"saveAllSettings": "Enregistrer tous les paramètres",
"settingsUpdated": "Paramètres mis à jour",
"settingsUpdatedDescription": "Tous les paramètres ont été mis à jour avec succès",
"settingsErrorUpdate": "Échec de la mise à jour des paramètres",
"settingsErrorUpdateDescription": "Une erreur s'est produite lors de la mise à jour des paramètres",
"sidebarCollapse": "Réduire",
"sidebarExpand": "Développer",
"newtUpdateAvailable": "Mise à jour disponible",
"newtUpdateAvailableInfo": "Une nouvelle version de Newt est disponible. Veuillez mettre à jour vers la dernière version pour une meilleure expérience.",
"domainPickerEnterDomain": "Entrez votre domaine",
"domainPickerPlaceholder": "myapp.example.com, api.v1.mydomain.com, ou simplement myapp",
"domainPickerDescription": "Entrez le domaine complet de la ressource pour voir les options disponibles.",
"domainPickerDescriptionSaas": "Entrez un domaine complet, un sous-domaine ou juste un nom pour voir les options disponibles",
"domainPickerTabAll": "Tous",
"domainPickerTabOrganization": "Organisation",
"domainPickerTabProvided": "Fournis",
"domainPickerSortAsc": "A-Z",
"domainPickerSortDesc": "Z-A",
"domainPickerCheckingAvailability": "Vérification de la disponibilité...",
"domainPickerNoMatchingDomains": "Aucun domaine correspondant trouvé pour \"{userInput}\". Essayez un autre domaine ou vérifiez les paramètres de domaine de votre organisation.",
"domainPickerOrganizationDomains": "Domaines de l'organisation",
"domainPickerProvidedDomains": "Domaines fournis",
"domainPickerSubdomain": "Sous-domaine : {subdomain}",
"domainPickerNamespace": "Espace de noms : {namespace}",
"domainPickerShowMore": "Afficher plus",
"domainNotFound": "Domaine introuvable",
"domainNotFoundDescription": "Cette ressource est désactivée car le domaine n'existe plus dans notre système. Veuillez définir un nouveau domaine pour cette ressource.",
"failed": "Échec",
"createNewOrgDescription": "Créer une nouvelle organisation",
"organization": "Organisation",
"port": "Port",
"securityKeyManage": "Gérer les clés de sécurité", "securityKeyManage": "Gérer les clés de sécurité",
"securityKeyDescription": "Ajouter ou supprimer des clés de sécurité pour l'authentification sans mot de passe", "securityKeyDescription": "Ajouter ou supprimer des clés de sécurité pour l'authentification sans mot de passe",
"securityKeyRegister": "Enregistrer une nouvelle clé de sécurité", "securityKeyRegister": "Enregistrer une nouvelle clé de sécurité",
@ -1142,13 +1227,51 @@
"securityKeyRemove": "Supprimer", "securityKeyRemove": "Supprimer",
"securityKeyLastUsed": "Dernière utilisation : {date}", "securityKeyLastUsed": "Dernière utilisation : {date}",
"securityKeyNameLabel": "Nom", "securityKeyNameLabel": "Nom",
"securityKeyNamePlaceholder": "Entrez un nom pour cette clé de sécurité",
"securityKeyRegisterSuccess": "Clé de sécurité enregistrée avec succès", "securityKeyRegisterSuccess": "Clé de sécurité enregistrée avec succès",
"securityKeyRegisterError": "Échec de l'enregistrement de la clé de sécurité", "securityKeyRegisterError": "Échec de l'enregistrement de la clé de sécurité",
"securityKeyRemoveSuccess": "Clé de sécurité supprimée avec succès", "securityKeyRemoveSuccess": "Clé de sécurité supprimée avec succès",
"securityKeyRemoveError": "Échec de la suppression de la clé de sécurité", "securityKeyRemoveError": "Échec de la suppression de la clé de sécurité",
"securityKeyLoadError": "Échec du chargement des clés de sécurité", "securityKeyLoadError": "Échec du chargement des clés de sécurité",
"securityKeyLogin": "Se connecter avec une clé de sécurité", "securityKeyLogin": "Continuer avec une clé de sécurité",
"securityKeyAuthError": "Échec de l'authentification avec la clé de sécurité", "securityKeyAuthError": "Échec de l'authentification avec la clé de sécurité",
"securityKeyRecommendation": "Envisagez d'enregistrer une autre clé de sécurité sur un appareil différent pour vous assurer de ne pas être bloqué de votre compte." "securityKeyRecommendation": "Envisagez d'enregistrer une autre clé de sécurité sur un appareil différent pour vous assurer de ne pas être bloqué de votre compte.",
"registering": "Enregistrement...",
"securityKeyPrompt": "Veuillez vérifier votre identité à l'aide de votre clé de sécurité. Assurez-vous que votre clé de sécurité est connectée et prête.",
"securityKeyBrowserNotSupported": "Votre navigateur ne prend pas en charge les clés de sécurité. Veuillez utiliser un navigateur moderne comme Chrome, Firefox ou Safari.",
"securityKeyPermissionDenied": "Veuillez autoriser l'accès à votre clé de sécurité pour continuer la connexion.",
"securityKeyRemovedTooQuickly": "Veuillez garder votre clé de sécurité connectée jusqu'à ce que le processus de connexion soit terminé.",
"securityKeyNotSupported": "Votre clé de sécurité peut ne pas être compatible. Veuillez essayer une clé de sécurité différente.",
"securityKeyUnknownError": "Un problème est survenu avec votre clé de sécurité. Veuillez réessayer.",
"twoFactorRequired": "L'authentification à deux facteurs est requise pour enregistrer une clé de sécurité.",
"twoFactor": "Authentification à deux facteurs",
"adminEnabled2FaOnYourAccount": "Votre administrateur a activé l'authentification à deux facteurs pour {email}. Veuillez terminer le processus d'installation pour continuer.",
"continueToApplication": "Continuer vers l'application",
"securityKeyAdd": "Ajouter une clé de sécurité",
"securityKeyRegisterTitle": "Enregistrer une nouvelle clé de sécurité",
"securityKeyRegisterDescription": "Connectez votre clé de sécurité et saisissez un nom pour l'identifier",
"securityKeyTwoFactorRequired": "Authentification à deux facteurs requise",
"securityKeyTwoFactorDescription": "Veuillez entrer votre code d'authentification à deux facteurs pour enregistrer la clé de sécurité",
"securityKeyTwoFactorRemoveDescription": "Veuillez entrer votre code d'authentification à deux facteurs pour supprimer la clé de sécurité",
"securityKeyTwoFactorCode": "Code à deux facteurs",
"securityKeyRemoveTitle": "Supprimer la clé de sécurité",
"securityKeyRemoveDescription": "Saisissez votre mot de passe pour supprimer la clé de sécurité \"{name}\"",
"securityKeyNoKeysRegistered": "Aucune clé de sécurité enregistrée",
"securityKeyNoKeysDescription": "Ajoutez une clé de sécurité pour améliorer la sécurité de votre compte",
"createDomainRequired": "Le domaine est requis",
"createDomainAddDnsRecords": "Ajouter des enregistrements DNS",
"createDomainAddDnsRecordsDescription": "Ajouter les enregistrements DNS suivants à votre fournisseur de domaine pour compléter la configuration.",
"createDomainNsRecords": "Enregistrements NS",
"createDomainRecord": "Enregistrement",
"createDomainType": "Type :",
"createDomainName": "Nom :",
"createDomainValue": "Valeur :",
"createDomainCnameRecords": "Enregistrements CNAME",
"createDomainRecordNumber": "Enregistrement {number}",
"createDomainTxtRecords": "Enregistrements TXT",
"createDomainSaveTheseRecords": "Enregistrez ces enregistrements",
"createDomainSaveTheseRecordsDescription": "Assurez-vous de sauvegarder ces enregistrements DNS car vous ne les reverrez pas.",
"createDomainDnsPropagation": "Propagation DNS",
"createDomainDnsPropagationDescription": "Les modifications DNS peuvent mettre du temps à se propager sur internet. Cela peut prendre de quelques minutes à 48 heures selon votre fournisseur DNS et les réglages TTL.",
"resourcePortRequired": "Le numéro de port est requis pour les ressources non-HTTP",
"resourcePortNotAllowed": "Le numéro de port ne doit pas être défini pour les ressources HTTP"
} }

View file

@ -11,8 +11,9 @@
"componentsErrorNoMemberCreate": "Al momento non sei un membro di nessuna organizzazione. Crea un'organizzazione per iniziare.", "componentsErrorNoMemberCreate": "Al momento non sei un membro di nessuna organizzazione. Crea un'organizzazione per iniziare.",
"componentsErrorNoMember": "Attualmente non sei membro di nessuna organizzazione.", "componentsErrorNoMember": "Attualmente non sei membro di nessuna organizzazione.",
"welcome": "Benvenuti a Pangolin", "welcome": "Benvenuti a Pangolin",
"welcomeTo": "Benvenuto a",
"componentsCreateOrg": "Crea un'organizzazione", "componentsCreateOrg": "Crea un'organizzazione",
"componentsMember": "Sei un membro di {count, plural, =0 {nessuna organizzazione} =1 {una organizzazione} other {# organizzazioni}}.", "componentsMember": "Sei un membro di {count, plural, =0 {nessuna organizzazione} one {un'organizzazione} other {# organizzazioni}}.",
"componentsInvalidKey": "Rilevata chiave di licenza non valida o scaduta. Segui i termini di licenza per continuare a utilizzare tutte le funzionalità.", "componentsInvalidKey": "Rilevata chiave di licenza non valida o scaduta. Segui i termini di licenza per continuare a utilizzare tutte le funzionalità.",
"dismiss": "Ignora", "dismiss": "Ignora",
"componentsLicenseViolation": "Violazione della licenza: Questo server sta usando i siti {usedSites} che superano il suo limite concesso in licenza per i siti {maxSites} . Segui i termini di licenza per continuare a usare tutte le funzionalità.", "componentsLicenseViolation": "Violazione della licenza: Questo server sta usando i siti {usedSites} che superano il suo limite concesso in licenza per i siti {maxSites} . Segui i termini di licenza per continuare a usare tutte le funzionalità.",
@ -206,6 +207,7 @@
"orgGeneralSettings": "Impostazioni Organizzazione", "orgGeneralSettings": "Impostazioni Organizzazione",
"orgGeneralSettingsDescription": "Gestisci i dettagli dell'organizzazione e la configurazione", "orgGeneralSettingsDescription": "Gestisci i dettagli dell'organizzazione e la configurazione",
"saveGeneralSettings": "Salva Impostazioni Generali", "saveGeneralSettings": "Salva Impostazioni Generali",
"saveSettings": "Salva Impostazioni",
"orgDangerZone": "Zona Pericolosa", "orgDangerZone": "Zona Pericolosa",
"orgDangerZoneDescription": "Una volta che si elimina questo org, non c'è ritorno. Si prega di essere certi.", "orgDangerZoneDescription": "Una volta che si elimina questo org, non c'è ritorno. Si prega di essere certi.",
"orgDelete": "Elimina Organizzazione", "orgDelete": "Elimina Organizzazione",
@ -249,7 +251,7 @@
"weeks": "Settimane", "weeks": "Settimane",
"months": "Mesi", "months": "Mesi",
"years": "Anni", "years": "Anni",
"day": "{count, plural, =1 {# giorno} other {# giorni}}", "day": "{count, plural, one {# giorno} other {# giorni}}",
"apiKeysTitle": "Informazioni Chiave API", "apiKeysTitle": "Informazioni Chiave API",
"apiKeysConfirmCopy2": "Devi confermare di aver copiato la chiave API.", "apiKeysConfirmCopy2": "Devi confermare di aver copiato la chiave API.",
"apiKeysErrorCreate": "Errore nella creazione della chiave API", "apiKeysErrorCreate": "Errore nella creazione della chiave API",
@ -347,7 +349,7 @@
"licensePurchase": "Acquista Licenza", "licensePurchase": "Acquista Licenza",
"licensePurchaseSites": "Acquista Siti Aggiuntivi", "licensePurchaseSites": "Acquista Siti Aggiuntivi",
"licenseSitesUsedMax": "{usedSites} di {maxSites} siti utilizzati", "licenseSitesUsedMax": "{usedSites} di {maxSites} siti utilizzati",
"licenseSitesUsed": "{count, plural, =0 {# siti} =1 {# sito} other {# siti}} nel sistema.", "licenseSitesUsed": "{count, plural, =0 {# siti} one {# sito} other {# siti}} nel sistema.",
"licensePurchaseDescription": "Scegli quanti siti vuoi {selectedMode, select, license {acquista una licenza. Puoi sempre aggiungere altri siti più tardi.} other {aggiungi alla tua licenza esistente.}}", "licensePurchaseDescription": "Scegli quanti siti vuoi {selectedMode, select, license {acquista una licenza. Puoi sempre aggiungere altri siti più tardi.} other {aggiungi alla tua licenza esistente.}}",
"licenseFee": "Costo della licenza", "licenseFee": "Costo della licenza",
"licensePriceSite": "Prezzo per sito", "licensePriceSite": "Prezzo per sito",
@ -436,7 +438,7 @@
"accessRoleSelect": "Seleziona ruolo", "accessRoleSelect": "Seleziona ruolo",
"inviteEmailSentDescription": "È stata inviata un'email all'utente con il link di accesso qui sotto. Devono accedere al link per accettare l'invito.", "inviteEmailSentDescription": "È stata inviata un'email all'utente con il link di accesso qui sotto. Devono accedere al link per accettare l'invito.",
"inviteSentDescription": "L'utente è stato invitato. Deve accedere al link qui sotto per accettare l'invito.", "inviteSentDescription": "L'utente è stato invitato. Deve accedere al link qui sotto per accettare l'invito.",
"inviteExpiresIn": "L'invito scadrà tra {days, plural, =1 {# giorno} other {# giorni}}.", "inviteExpiresIn": "L'invito scadrà tra {days, plural, one {# giorno} other {# giorni}}.",
"idpTitle": "Informazioni Generali", "idpTitle": "Informazioni Generali",
"idpSelect": "Seleziona il provider di identità per l'utente esterno", "idpSelect": "Seleziona il provider di identità per l'utente esterno",
"idpNotConfigured": "Nessun provider di identità configurato. Configura un provider di identità prima di creare utenti esterni.", "idpNotConfigured": "Nessun provider di identità configurato. Configura un provider di identità prima di creare utenti esterni.",
@ -958,6 +960,8 @@
"licenseTierProfessionalRequiredDescription": "Questa funzionalità è disponibile solo nell'Edizione Professional.", "licenseTierProfessionalRequiredDescription": "Questa funzionalità è disponibile solo nell'Edizione Professional.",
"actionGetOrg": "Ottieni Organizzazione", "actionGetOrg": "Ottieni Organizzazione",
"actionUpdateOrg": "Aggiorna Organizzazione", "actionUpdateOrg": "Aggiorna Organizzazione",
"actionUpdateUser": "Aggiorna Utente",
"actionGetUser": "Ottieni Utente",
"actionGetOrgUser": "Ottieni Utente Organizzazione", "actionGetOrgUser": "Ottieni Utente Organizzazione",
"actionListOrgDomains": "Elenca Domini Organizzazione", "actionListOrgDomains": "Elenca Domini Organizzazione",
"actionCreateSite": "Crea Sito", "actionCreateSite": "Crea Sito",
@ -1090,6 +1094,8 @@
"sidebarAllUsers": "Tutti Gli Utenti", "sidebarAllUsers": "Tutti Gli Utenti",
"sidebarIdentityProviders": "Fornitori Di Identità", "sidebarIdentityProviders": "Fornitori Di Identità",
"sidebarLicense": "Licenza", "sidebarLicense": "Licenza",
"sidebarClients": "Clienti",
"sidebarDomains": "Domini",
"enableDockerSocket": "Abilita Docker Socket", "enableDockerSocket": "Abilita Docker Socket",
"enableDockerSocketDescription": "Abilita il rilevamento Docker Socket per popolare le informazioni del contenitore. Il percorso del socket deve essere fornito a Newt.", "enableDockerSocketDescription": "Abilita il rilevamento Docker Socket per popolare le informazioni del contenitore. Il percorso del socket deve essere fornito a Newt.",
"enableDockerSocketLink": "Scopri di più", "enableDockerSocketLink": "Scopri di più",
@ -1102,7 +1108,7 @@
"containerNetworks": "Reti", "containerNetworks": "Reti",
"containerHostnameIp": "Hostname/IP", "containerHostnameIp": "Hostname/IP",
"containerLabels": "Etichette", "containerLabels": "Etichette",
"containerLabelsCount": "{count} etichetta{s,plural,one{} other{s}}", "containerLabelsCount": "{count, plural, one {# etichetta} other {# etichette}}",
"containerLabelsTitle": "Etichette Del Contenitore", "containerLabelsTitle": "Etichette Del Contenitore",
"containerLabelEmpty": "<vuoto>", "containerLabelEmpty": "<vuoto>",
"containerPorts": "Porte", "containerPorts": "Porte",
@ -1114,7 +1120,7 @@
"showStoppedContainers": "Mostra contenitori fermati", "showStoppedContainers": "Mostra contenitori fermati",
"noContainersFound": "Nessun contenitore trovato. Assicurarsi che i contenitori Docker siano in esecuzione.", "noContainersFound": "Nessun contenitore trovato. Assicurarsi che i contenitori Docker siano in esecuzione.",
"searchContainersPlaceholder": "Cerca tra i contenitori {count}...", "searchContainersPlaceholder": "Cerca tra i contenitori {count}...",
"searchResultsCount": "{count} risultato{s,plural,one{} other{s}}", "searchResultsCount": "{count, plural, one {# risultato} other {# risultati}}",
"filters": "Filtri", "filters": "Filtri",
"filterOptions": "Opzioni Filtro", "filterOptions": "Opzioni Filtro",
"filterPorts": "Porte", "filterPorts": "Porte",
@ -1129,10 +1135,89 @@
"dark": "scuro", "dark": "scuro",
"system": "sistema", "system": "sistema",
"theme": "Tema", "theme": "Tema",
"subnetRequired": "Sottorete richiesta",
"initialSetupTitle": "Impostazione Iniziale del Server", "initialSetupTitle": "Impostazione Iniziale del Server",
"initialSetupDescription": "Crea l'account amministratore del server iniziale. Può esistere solo un amministratore del server. È sempre possibile modificare queste credenziali in seguito.", "initialSetupDescription": "Crea l'account amministratore del server iniziale. Può esistere solo un amministratore del server. È sempre possibile modificare queste credenziali in seguito.",
"createAdminAccount": "Crea Account Admin", "createAdminAccount": "Crea Account Admin",
"setupErrorCreateAdmin": "Si è verificato un errore durante la creazione dell'account amministratore del server.", "setupErrorCreateAdmin": "Si è verificato un errore durante la creazione dell'account amministratore del server.",
"certificateStatus": "Stato del Certificato",
"loading": "Caricamento",
"restart": "Riavvia",
"domains": "Domini",
"domainsDescription": "Gestisci domini per la tua organizzazione",
"domainsSearch": "Cerca domini...",
"domainAdd": "Aggiungi Dominio",
"domainAddDescription": "Registra un nuovo dominio con la tua organizzazione",
"domainCreate": "Crea Dominio",
"domainCreatedDescription": "Dominio creato con successo",
"domainDeletedDescription": "Dominio eliminato con successo",
"domainQuestionRemove": "Sei sicuro di voler rimuovere il dominio {domain} dal tuo account?",
"domainMessageRemove": "Una volta rimosso, il dominio non sarà più associato al tuo account.",
"domainMessageConfirm": "Per confermare, digita il nome del dominio qui sotto.",
"domainConfirmDelete": "Conferma Eliminazione Dominio",
"domainDelete": "Elimina Dominio",
"domain": "Dominio",
"selectDomainTypeNsName": "Delega Dominio (NS)",
"selectDomainTypeNsDescription": "Questo dominio e tutti i suoi sottodomini. Usa questo quando desideri controllare un'intera zona di dominio.",
"selectDomainTypeCnameName": "Dominio Singolo (CNAME)",
"selectDomainTypeCnameDescription": "Solo questo dominio specifico. Usa questo per sottodomini individuali o specifiche voci di dominio.",
"selectDomainTypeWildcardName": "Dominio Jolly",
"selectDomainTypeWildcardDescription": "Questo dominio e il suo primo livello di sottodomini.",
"domainDelegation": "Dominio Singolo",
"selectType": "Seleziona un tipo",
"actions": "Azioni",
"refresh": "Aggiorna",
"refreshError": "Impossibile aggiornare i dati",
"verified": "Verificato",
"pending": "In attesa",
"sidebarBilling": "Fatturazione",
"billing": "Fatturazione",
"orgBillingDescription": "Gestisci le tue informazioni di fatturazione e abbonamenti",
"github": "GitHub",
"pangolinHosted": "Pangolin Hosted",
"fossorial": "Fossorial",
"completeAccountSetup": "Completa la Configurazione dell'Account",
"completeAccountSetupDescription": "Imposta la tua password per iniziare",
"accountSetupSent": "Invieremo un codice di configurazione dell'account a questo indirizzo email.",
"accountSetupCode": "Codice di Configurazione",
"accountSetupCodeDescription": "Controlla la tua email per il codice di configurazione.",
"passwordCreate": "Crea Password",
"passwordCreateConfirm": "Conferma Password",
"accountSetupSubmit": "Invia Codice di Configurazione",
"completeSetup": "Completa la Configurazione",
"accountSetupSuccess": "Configurazione dell'account completata! Benvenuto su Pangolin!",
"documentation": "Documentazione",
"saveAllSettings": "Salva Tutte le Impostazioni",
"settingsUpdated": "Impostazioni aggiornate",
"settingsUpdatedDescription": "Tutte le impostazioni sono state aggiornate con successo",
"settingsErrorUpdate": "Impossibile aggiornare le impostazioni",
"settingsErrorUpdateDescription": "Si è verificato un errore durante l'aggiornamento delle impostazioni",
"sidebarCollapse": "Comprimi",
"sidebarExpand": "Espandi",
"newtUpdateAvailable": "Aggiornamento Disponibile",
"newtUpdateAvailableInfo": "È disponibile una nuova versione di Newt. Si prega di aggiornare all'ultima versione per la migliore esperienza.",
"domainPickerEnterDomain": "Inserisci il tuo dominio",
"domainPickerPlaceholder": "myapp.example.com, api.v1.mydomain.com, o semplicemente myapp",
"domainPickerDescription": "Inserisci il dominio completo della risorsa per vedere le opzioni disponibili.",
"domainPickerDescriptionSaas": "Inserisci un dominio completo, un sottodominio o semplicemente un nome per vedere le opzioni disponibili",
"domainPickerTabAll": "Tutti",
"domainPickerTabOrganization": "Organizzazione",
"domainPickerTabProvided": "Fornito",
"domainPickerSortAsc": "A-Z",
"domainPickerSortDesc": "Z-A",
"domainPickerCheckingAvailability": "Controllando la disponibilità...",
"domainPickerNoMatchingDomains": "Nessun dominio corrispondente trovato per \"{userInput}\". Prova un altro dominio o controlla le impostazioni del dominio della tua organizzazione.",
"domainPickerOrganizationDomains": "Domini dell'Organizzazione",
"domainPickerProvidedDomains": "Domini Forniti",
"domainPickerSubdomain": "Sottodominio: {subdomain}",
"domainPickerNamespace": "Namespace: {namespace}",
"domainPickerShowMore": "Mostra Altro",
"domainNotFound": "Domini Non Trovati",
"domainNotFoundDescription": "Questa risorsa è disabilitata perché il dominio non esiste più nel nostro sistema. Si prega di impostare un nuovo dominio per questa risorsa.",
"failed": "Fallito",
"createNewOrgDescription": "Crea una nuova organizzazione",
"organization": "Organizzazione",
"port": "Porta",
"securityKeyManage": "Gestisci chiavi di sicurezza", "securityKeyManage": "Gestisci chiavi di sicurezza",
"securityKeyDescription": "Aggiungi o rimuovi chiavi di sicurezza per l'autenticazione senza password", "securityKeyDescription": "Aggiungi o rimuovi chiavi di sicurezza per l'autenticazione senza password",
"securityKeyRegister": "Registra nuova chiave di sicurezza", "securityKeyRegister": "Registra nuova chiave di sicurezza",
@ -1142,13 +1227,51 @@
"securityKeyRemove": "Rimuovi", "securityKeyRemove": "Rimuovi",
"securityKeyLastUsed": "Ultimo utilizzo: {date}", "securityKeyLastUsed": "Ultimo utilizzo: {date}",
"securityKeyNameLabel": "Nome", "securityKeyNameLabel": "Nome",
"securityKeyNamePlaceholder": "Inserisci un nome per questa chiave di sicurezza",
"securityKeyRegisterSuccess": "Chiave di sicurezza registrata con successo", "securityKeyRegisterSuccess": "Chiave di sicurezza registrata con successo",
"securityKeyRegisterError": "Errore durante la registrazione della chiave di sicurezza", "securityKeyRegisterError": "Errore durante la registrazione della chiave di sicurezza",
"securityKeyRemoveSuccess": "Chiave di sicurezza rimossa con successo", "securityKeyRemoveSuccess": "Chiave di sicurezza rimossa con successo",
"securityKeyRemoveError": "Errore durante la rimozione della chiave di sicurezza", "securityKeyRemoveError": "Errore durante la rimozione della chiave di sicurezza",
"securityKeyLoadError": "Errore durante il caricamento delle chiavi di sicurezza", "securityKeyLoadError": "Errore durante il caricamento delle chiavi di sicurezza",
"securityKeyLogin": "Accedi con chiave di sicurezza", "securityKeyLogin": "Continua con la chiave di sicurezza",
"securityKeyAuthError": "Errore durante l'autenticazione con chiave di sicurezza", "securityKeyAuthError": "Errore durante l'autenticazione con chiave di sicurezza",
"securityKeyRecommendation": "Considera di registrare un'altra chiave di sicurezza su un dispositivo diverso per assicurarti di non rimanere bloccato fuori dal tuo account." "securityKeyRecommendation": "Considera di registrare un'altra chiave di sicurezza su un dispositivo diverso per assicurarti di non rimanere bloccato fuori dal tuo account.",
"registering": "Registrazione in corso...",
"securityKeyPrompt": "Verifica la tua identità usando la chiave di sicurezza. Assicurati che sia connessa e pronta.",
"securityKeyBrowserNotSupported": "Il tuo browser non supporta le chiavi di sicurezza. Per favore, usa un browser moderno come Chrome, Firefox o Safari.",
"securityKeyPermissionDenied": "Consenti accesso alla tua chiave di sicurezza per continuare ad accedere.",
"securityKeyRemovedTooQuickly": "Mantieni la chiave di sicurezza connessa fino a quando il processo di accesso non è completato.",
"securityKeyNotSupported": "La tua chiave di sicurezza potrebbe non essere compatibile. Prova un'altra chiave di sicurezza.",
"securityKeyUnknownError": "Si è verificato un problema con la tua chiave di sicurezza. Riprova.",
"twoFactorRequired": "È richiesta l'autenticazione a due fattori per registrare una chiave di sicurezza.",
"twoFactor": "Autenticazione a Due Fattori",
"adminEnabled2FaOnYourAccount": "Il tuo amministratore ha abilitato l'autenticazione a due fattori per {email}. Completa il processo di configurazione per continuare.",
"continueToApplication": "Continua all'Applicazione",
"securityKeyAdd": "Aggiungi Chiave di Sicurezza",
"securityKeyRegisterTitle": "Registra Nuova Chiave di Sicurezza",
"securityKeyRegisterDescription": "Collega la tua chiave di sicurezza e inserisci un nome per identificarla",
"securityKeyTwoFactorRequired": "Autenticazione a Due Fattori Richiesta",
"securityKeyTwoFactorDescription": "Inserisci il codice di autenticazione a due fattori per registrare la chiave di sicurezza",
"securityKeyTwoFactorRemoveDescription": "Inserisci il codice di autenticazione a due fattori per rimuovere la chiave di sicurezza",
"securityKeyTwoFactorCode": "Codice a Due Fattori",
"securityKeyRemoveTitle": "Rimuovi Chiave di Sicurezza",
"securityKeyRemoveDescription": "Inserisci la tua password per rimuovere la chiave di sicurezza \"{name}\"",
"securityKeyNoKeysRegistered": "Nessuna chiave di sicurezza registrata",
"securityKeyNoKeysDescription": "Aggiungi una chiave di sicurezza per migliorare la sicurezza del tuo account",
"createDomainRequired": "Dominio richiesto",
"createDomainAddDnsRecords": "Aggiungi Record DNS",
"createDomainAddDnsRecordsDescription": "Aggiungi i seguenti record DNS al tuo provider di domini per completare la configurazione.",
"createDomainNsRecords": "Record NS",
"createDomainRecord": "Record",
"createDomainType": "Tipo:",
"createDomainName": "Nome:",
"createDomainValue": "Valore:",
"createDomainCnameRecords": "Record CNAME",
"createDomainRecordNumber": "Record {number}",
"createDomainTxtRecords": "Record TXT",
"createDomainSaveTheseRecords": "Salva Questi Record",
"createDomainSaveTheseRecordsDescription": "Assicurati di salvare questi record DNS poiché non li vedrai più.",
"createDomainDnsPropagation": "Propagazione DNS",
"createDomainDnsPropagationDescription": "Le modifiche DNS possono richiedere del tempo per propagarsi in Internet. Questo può richiedere da pochi minuti a 48 ore, a seconda del tuo provider DNS e delle impostazioni TTL.",
"resourcePortRequired": "Numero di porta richiesto per risorse non-HTTP",
"resourcePortNotAllowed": "Il numero di porta non deve essere impostato per risorse HTTP"
} }

View file

@ -1,19 +1,34 @@
{ {
"setupCreate": "조직, 사이트 및 리소스를 생성하십시오.", "setupCreate": "조직, 사이트 및 리소스를 생성하십시오.",
"setupNewOrg": "새 조직",
"setupCreateOrg": "조직 생성",
"setupCreateResources": "리소스 생성",
"setupOrgName": "조직 이름",
"orgDisplayName": "이것은 귀하의 조직의 표시 이름입니다.", "orgDisplayName": "이것은 귀하의 조직의 표시 이름입니다.",
"setupIdentifierMessage": "이것은 귀하의 조직에 대한 고유 식별자입니다. 표시 이름과는 별개입니다.",
"componentsErrorNoMemberCreate": "현재 어떤 조직의 구성원도 아닙니다. 시작하려면 조직을 생성하세요.",
"componentsInvalidKey": "유효하지 않거나 만료된 라이센스 키가 감지되었습니다. 모든 기능을 계속 사용하려면 라이센스 조건을 따르십시오.",
"orgId": "조직 ID", "orgId": "조직 ID",
"siteQuestionRemove": "조직에서 사이트 {selectedSite}를 제거하시겠습니까?", "setupIdentifierMessage": "이것은 귀하의 조직에 대한 고유 식별자입니다. 표시 이름과는 별개입니다.",
"siteCreateDescription2": "아래 단계를 따라 새 사이트를 생성하고 연결하십시오", "setupErrorIdentifier": "조직 ID가 이미 사용 중입니다. 다른 것을 선택해 주세요.",
"componentsErrorNoMemberCreate": "현재 어떤 조직의 구성원도 아닙니다. 시작하려면 조직을 생성하세요.",
"componentsErrorNoMember": "현재 어떤 조직의 구성원도 아닙니다.",
"welcome": "판골린에 오신 것을 환영합니다.",
"welcomeTo": "환영합니다",
"componentsCreateOrg": "조직 생성",
"componentsMember": "당신은 {count, plural, =0 {조직이 없습니다} one {하나의 조직} other {# 개의 조직}}의 구성원입니다.",
"componentsInvalidKey": "유효하지 않거나 만료된 라이센스 키가 감지되었습니다. 모든 기능을 계속 사용하려면 라이센스 조건을 따르십시오.",
"dismiss": "해제",
"componentsLicenseViolation": "라이센스 위반: 이 서버는 {usedSites} 사이트를 사용하고 있으며, 이는 {maxSites} 사이트의 라이센스 한도를 초과합니다. 모든 기능을 계속 사용하려면 라이센스 조건을 따르십시오.", "componentsLicenseViolation": "라이센스 위반: 이 서버는 {usedSites} 사이트를 사용하고 있으며, 이는 {maxSites} 사이트의 라이센스 한도를 초과합니다. 모든 기능을 계속 사용하려면 라이센스 조건을 따르십시오.",
"years": "연도", "componentsSupporterMessage": "{tier}로 판골린을 지원해 주셔서 감사합니다!",
"hours": "시간", "inviteErrorNotValid": "죄송하지만, 접근하려는 초대가 수락되지 않았거나 더 이상 유효하지 않은 것 같습니다.",
"days": "일", "inviteErrorUser": "죄송하지만, 접근하려는 초대가 이 사용자에게 해당되지 않는 것 같습니다.",
"weeks": "주", "inviteLoginUser": "올바른 사용자로 로그인했는지 확인하십시오.",
"months": "개월", "inviteErrorNoUser": "죄송하지만, 접근하려는 초대가 존재하지 않는 사용자에 대한 것인 것 같습니다.",
"inviteCreateUser": "먼저 계정을 생성해 주세요.",
"goHome": "홈으로 가기",
"inviteLogInOtherUser": "다른 사용자로 로그인",
"createAnAccount": "계정 만들기",
"inviteNotAccepted": "초대가 수락되지 않음",
"authCreateAccount": "시작하려면 계정을 생성하세요.", "authCreateAccount": "시작하려면 계정을 생성하세요.",
"authNoAccount": "계정이 없으신가요?",
"email": "이메일", "email": "이메일",
"password": "비밀번호", "password": "비밀번호",
"confirmPassword": "비밀번호 확인", "confirmPassword": "비밀번호 확인",
@ -34,31 +49,12 @@
"siteDelete": "사이트 삭제", "siteDelete": "사이트 삭제",
"siteMessageRemove": "제거되면 사이트에 더 이상 접근할 수 없습니다. 사이트와 관련된 모든 리소스와 대상도 제거됩니다.", "siteMessageRemove": "제거되면 사이트에 더 이상 접근할 수 없습니다. 사이트와 관련된 모든 리소스와 대상도 제거됩니다.",
"siteMessageConfirm": "확인을 위해 아래에 사이트 이름을 입력해 주세요.", "siteMessageConfirm": "확인을 위해 아래에 사이트 이름을 입력해 주세요.",
"setupNewOrg": "새 조직", "siteQuestionRemove": "조직에서 사이트 {selectedSite}를 제거하시겠습니까?",
"setupCreateOrg": "조직 생성",
"setupCreateResources": "리소스 생성",
"setupOrgName": "조직 이름",
"setupErrorIdentifier": "조직 ID가 이미 사용 중입니다. 다른 것을 선택해 주세요.",
"componentsErrorNoMember": "현재 어떤 조직의 구성원도 아닙니다.",
"welcome": "판골린에 오신 것을 환영합니다.",
"componentsCreateOrg": "조직 생성",
"componentsMember": "당신은 {count, plural, =0 {조직이 없습니다} one {하나의 조직} other {# 개의 조직}}의 구성원입니다.",
"componentsSupporterMessage": "{tier}로 판골린을 지원해 주셔서 감사합니다!",
"inviteErrorNotValid": "죄송하지만, 접근하려는 초대가 수락되지 않았거나 더 이상 유효하지 않은 것 같습니다.",
"inviteErrorUser": "죄송하지만, 접근하려는 초대가 이 사용자에게 해당되지 않는 것 같습니다.",
"inviteLoginUser": "올바른 사용자로 로그인했는지 확인하십시오.",
"inviteErrorNoUser": "죄송하지만, 접근하려는 초대가 존재하지 않는 사용자에 대한 것인 것 같습니다.",
"inviteCreateUser": "먼저 계정을 생성해 주세요.",
"goHome": "홈으로 가기",
"inviteLogInOtherUser": "다른 사용자로 로그인",
"createAnAccount": "계정 만들기",
"siteManageSites": "사이트 관리", "siteManageSites": "사이트 관리",
"siteDescription": "안전한 터널을 통해 네트워크에 연결할 수 있도록 허용", "siteDescription": "안전한 터널을 통해 네트워크에 연결할 수 있도록 허용",
"siteCreate": "사이트 생성", "siteCreate": "사이트 생성",
"inviteNotAccepted": "초대가 수락되지 않음", "siteCreateDescription2": "아래 단계를 따라 새 사이트를 생성하고 연결하십시오",
"authNoAccount": "계정이 없으신가요?",
"siteCreateDescription": "리소스를 연결하기 위해 새 사이트를 생성하십시오.", "siteCreateDescription": "리소스를 연결하기 위해 새 사이트를 생성하십시오.",
"dismiss": "해제",
"close": "닫기", "close": "닫기",
"siteErrorCreate": "사이트 생성 오류", "siteErrorCreate": "사이트 생성 오류",
"siteErrorCreateKeyPair": "키 쌍 또는 사이트 기본값을 찾을 수 없습니다", "siteErrorCreateKeyPair": "키 쌍 또는 사이트 기본값을 찾을 수 없습니다",
@ -104,15 +100,6 @@
"siteTunnelDescription": "사이트에 연결하는 방법을 결정하세요", "siteTunnelDescription": "사이트에 연결하는 방법을 결정하세요",
"siteNewtCredentials": "Newt 자격 증명", "siteNewtCredentials": "Newt 자격 증명",
"siteNewtCredentialsDescription": "이것이 Newt가 서버와 인증하는 방법입니다", "siteNewtCredentialsDescription": "이것이 Newt가 서버와 인증하는 방법입니다",
"orgPolicyDeletedDescription": "정책이 성공적으로 삭제되었습니다",
"actionCreateResourceRule": "리소스 규칙 생성",
"defaultMappingsUpdatedDescription": "기본 매핑이 성공적으로 업데이트되었습니다.",
"orgPoliciesAbout": "조직 정책에 대하여",
"orgPoliciesAboutDescription": "조직 정책은 사용자의 ID 토큰에 따라 조직에 대한 액세스를 제어하는 데 사용됩니다. ID 토큰에서 역할 및 조직 정보를 추출하기 위해 JMESPath 표현식을 지정할 수 있습니다.",
"orgPoliciesAboutDescriptionLink": "자세한 내용은 문서를 참조하십시오.",
"actionDeleteResourceRule": "리소스 규칙 삭제",
"defaultMappingsOptional": "기본 매핑(선택 사항)",
"signupError": "가입하는 동안 오류가 발생했습니다.",
"siteCredentialsSave": "자격 증명 저장", "siteCredentialsSave": "자격 증명 저장",
"siteCredentialsSaveDescription": "이것은 한 번만 볼 수 있습니다. 안전한 장소에 복사해 두세요.", "siteCredentialsSaveDescription": "이것은 한 번만 볼 수 있습니다. 안전한 장소에 복사해 두세요.",
"siteInfo": "사이트 정보", "siteInfo": "사이트 정보",
@ -144,7 +131,6 @@
"expireIn": "만료됨", "expireIn": "만료됨",
"neverExpire": "만료되지 않음", "neverExpire": "만료되지 않음",
"shareExpireDescription": "만료 시간은 링크가 사용 가능하고 리소스에 접근할 수 있는 기간입니다. 이 시간이 지나면 링크는 더 이상 작동하지 않으며, 이 링크를 사용한 사용자는 리소스에 대한 접근 권한을 잃게 됩니다.", "shareExpireDescription": "만료 시간은 링크가 사용 가능하고 리소스에 접근할 수 있는 기간입니다. 이 시간이 지나면 링크는 더 이상 작동하지 않으며, 이 링크를 사용한 사용자는 리소스에 대한 접근 권한을 잃게 됩니다.",
"pangolinLogoAlt": "판골린 로고",
"shareSeeOnce": "이 링크는 한 번만 볼 수 있습니다. 반드시 복사해 두세요.", "shareSeeOnce": "이 링크는 한 번만 볼 수 있습니다. 반드시 복사해 두세요.",
"shareAccessHint": "이 링크가 있는 누구나 리소스에 접근할 수 있습니다. 주의해서 공유하세요.", "shareAccessHint": "이 링크가 있는 누구나 리소스에 접근할 수 있습니다. 주의해서 공유하세요.",
"shareTokenUsage": "액세스 토큰 사용 보기", "shareTokenUsage": "액세스 토큰 사용 보기",
@ -166,10 +152,8 @@
"authentication": "인증", "authentication": "인증",
"protected": "보호됨", "protected": "보호됨",
"notProtected": "보호되지 않음", "notProtected": "보호되지 않음",
"inviteAlready": "초대받은 것 같습니다!",
"resourceMessageRemove": "제거되면 리소스에 더 이상 접근할 수 없습니다. 리소스와 연결된 모든 대상도 제거됩니다.", "resourceMessageRemove": "제거되면 리소스에 더 이상 접근할 수 없습니다. 리소스와 연결된 모든 대상도 제거됩니다.",
"resourceMessageConfirm": "확인을 위해 아래에 리소스의 이름을 입력하세요.", "resourceMessageConfirm": "확인을 위해 아래에 리소스의 이름을 입력하세요.",
"tagsEnteredDescription": "입력한 태그는 다음과 같습니다.",
"resourceQuestionRemove": "조직에서 리소스 {selectedResource}를 제거하시겠습니까?", "resourceQuestionRemove": "조직에서 리소스 {selectedResource}를 제거하시겠습니까?",
"resourceHTTP": "HTTPS 리소스", "resourceHTTP": "HTTPS 리소스",
"resourceHTTPDescription": "서브도메인 또는 기본 도메인을 사용하여 HTTPS를 통해 앱에 대한 요청을 프록시합니다.", "resourceHTTPDescription": "서브도메인 또는 기본 도메인을 사용하여 HTTPS를 통해 앱에 대한 요청을 프록시합니다.",
@ -183,7 +167,6 @@
"siteSelect": "사이트 선택", "siteSelect": "사이트 선택",
"siteSearch": "사이트 검색", "siteSearch": "사이트 검색",
"siteNotFound": "사이트를 찾을 수 없습니다.", "siteNotFound": "사이트를 찾을 수 없습니다.",
"otpEnable": "이중 인증 활성화",
"siteSelectionDescription": "이 사이트는 리소스에 대한 연결을 제공합니다.", "siteSelectionDescription": "이 사이트는 리소스에 대한 연결을 제공합니다.",
"resourceType": "리소스 유형", "resourceType": "리소스 유형",
"resourceTypeDescription": "리소스에 접근하는 방법을 결정하세요", "resourceTypeDescription": "리소스에 접근하는 방법을 결정하세요",
@ -194,16 +177,13 @@
"baseDomain": "기본 도메인", "baseDomain": "기본 도메인",
"subdomnainDescription": "리소스에 접근할 수 있는 하위 도메인입니다.", "subdomnainDescription": "리소스에 접근할 수 있는 하위 도메인입니다.",
"resourceRawSettings": "TCP/UDP 설정", "resourceRawSettings": "TCP/UDP 설정",
"otpDisable": "이중 인증 비활성화",
"resourceRawSettingsDescription": "TCP/UDP를 통해 리소스에 접근하는 방법을 구성하세요.", "resourceRawSettingsDescription": "TCP/UDP를 통해 리소스에 접근하는 방법을 구성하세요.",
"protocol": "프로토콜", "protocol": "프로토콜",
"protocolSelect": "프로토콜 선택", "protocolSelect": "프로토콜 선택",
"resourcePortNumber": "포트 번호", "resourcePortNumber": "포트 번호",
"logout": "로그 아웃",
"resourcePortNumberDescription": "요청을 프록시하기 위한 외부 포트 번호입니다.", "resourcePortNumberDescription": "요청을 프록시하기 위한 외부 포트 번호입니다.",
"cancel": "취소", "cancel": "취소",
"resourceConfig": "구성 스니펫", "resourceConfig": "구성 스니펫",
"inviteAlreadyDescription": "초대를 수락하려면 로그인하거나 계정을 생성해야 합니다.",
"resourceConfigDescription": "TCP/UDP 리소스를 설정하기 위해 이 구성 스니펫을 복사하여 붙여넣으십시오.", "resourceConfigDescription": "TCP/UDP 리소스를 설정하기 위해 이 구성 스니펫을 복사하여 붙여넣으십시오.",
"resourceAddEntrypoints": "Traefik: 엔트리포인트 추가", "resourceAddEntrypoints": "Traefik: 엔트리포인트 추가",
"resourceExposePorts": "Gerbil: Docker Compose에서 포트 노출", "resourceExposePorts": "Gerbil: Docker Compose에서 포트 노출",
@ -220,7 +200,6 @@
"proxy": "프록시", "proxy": "프록시",
"rules": "규칙", "rules": "규칙",
"resourceSettingDescription": "리소스의 설정을 구성하세요.", "resourceSettingDescription": "리소스의 설정을 구성하세요.",
"sidebarApiKeys": "API 키",
"resourceSetting": "{resourceName} 설정", "resourceSetting": "{resourceName} 설정",
"alwaysAllow": "항상 허용", "alwaysAllow": "항상 허용",
"alwaysDeny": "항상 거부", "alwaysDeny": "항상 거부",
@ -228,6 +207,7 @@
"orgGeneralSettings": "조직 설정", "orgGeneralSettings": "조직 설정",
"orgGeneralSettingsDescription": "조직 세부정보 및 구성을 관리하세요.", "orgGeneralSettingsDescription": "조직 세부정보 및 구성을 관리하세요.",
"saveGeneralSettings": "일반 설정 저장", "saveGeneralSettings": "일반 설정 저장",
"saveSettings": "설정 저장",
"orgDangerZone": "위험 구역", "orgDangerZone": "위험 구역",
"orgDangerZoneDescription": "이 조직을 삭제하면 되돌릴 수 없습니다. 확실히 하세요.", "orgDangerZoneDescription": "이 조직을 삭제하면 되돌릴 수 없습니다. 확실히 하세요.",
"orgDelete": "조직 삭제", "orgDelete": "조직 삭제",
@ -239,7 +219,6 @@
"orgUpdatedDescription": "조직이 업데이트되었습니다.", "orgUpdatedDescription": "조직이 업데이트되었습니다.",
"orgErrorUpdate": "조직 업데이트에 실패했습니다.", "orgErrorUpdate": "조직 업데이트에 실패했습니다.",
"orgErrorUpdateMessage": "조직을 업데이트하는 동안 오류가 발생했습니다.", "orgErrorUpdateMessage": "조직을 업데이트하는 동안 오류가 발생했습니다.",
"sidebarSettings": "설정",
"orgErrorFetch": "조직을 가져오는 데 실패했습니다.", "orgErrorFetch": "조직을 가져오는 데 실패했습니다.",
"orgErrorFetchMessage": "조직을 나열하는 동안 오류가 발생했습니다", "orgErrorFetchMessage": "조직을 나열하는 동안 오류가 발생했습니다",
"orgErrorDelete": "조직 삭제에 실패했습니다.", "orgErrorDelete": "조직 삭제에 실패했습니다.",
@ -267,9 +246,13 @@
"inviteDescription": "다른 사용자에 대한 초대를 관리하세요", "inviteDescription": "다른 사용자에 대한 초대를 관리하세요",
"inviteSearch": "초대 검색...", "inviteSearch": "초대 검색...",
"minutes": "분", "minutes": "분",
"hours": "시간",
"days": "일",
"weeks": "주",
"months": "개월",
"years": "연도",
"day": "{count, plural, one {#일} other {#일}}", "day": "{count, plural, one {#일} other {#일}}",
"apiKeysTitle": "API 키 정보", "apiKeysTitle": "API 키 정보",
"signupQuestion": "이미 계정이 있습니까?",
"apiKeysConfirmCopy2": "API 키를 복사했음을 확인해야 합니다.", "apiKeysConfirmCopy2": "API 키를 복사했음을 확인해야 합니다.",
"apiKeysErrorCreate": "API 키 생성 오류", "apiKeysErrorCreate": "API 키 생성 오류",
"apiKeysErrorSetPermission": "권한 설정 오류", "apiKeysErrorSetPermission": "권한 설정 오류",
@ -288,7 +271,6 @@
"apiKeysPermissionsErrorLoadingActions": "API 키 작업 로드 오류", "apiKeysPermissionsErrorLoadingActions": "API 키 작업 로드 오류",
"apiKeysPermissionsErrorUpdate": "권한 설정 오류", "apiKeysPermissionsErrorUpdate": "권한 설정 오류",
"apiKeysPermissionsUpdated": "권한이 업데이트되었습니다", "apiKeysPermissionsUpdated": "권한이 업데이트되었습니다",
"login": "로그인",
"apiKeysPermissionsUpdatedDescription": "권한이 업데이트되었습니다.", "apiKeysPermissionsUpdatedDescription": "권한이 업데이트되었습니다.",
"apiKeysPermissionsGeneralSettings": "권한", "apiKeysPermissionsGeneralSettings": "권한",
"apiKeysPermissionsGeneralSettingsDescription": "이 API 키가 수행할 수 있는 작업 결정", "apiKeysPermissionsGeneralSettingsDescription": "이 API 키가 수행할 수 있는 작업 결정",
@ -330,7 +312,6 @@
"licenseErrorKeyLoad": "라이센스 키를 로드하는 데 실패했습니다.", "licenseErrorKeyLoad": "라이센스 키를 로드하는 데 실패했습니다.",
"licenseErrorKeyLoadDescription": "라이센스 키 로드 중 오류가 발생했습니다.", "licenseErrorKeyLoadDescription": "라이센스 키 로드 중 오류가 발생했습니다.",
"licenseErrorKeyDelete": "라이센스 키 삭제에 실패했습니다.", "licenseErrorKeyDelete": "라이센스 키 삭제에 실패했습니다.",
"resourceNotFound": "리소스를 찾을 수 없습니다",
"licenseErrorKeyDeleteDescription": "라이센스 키 삭제 중 오류가 발생했습니다.", "licenseErrorKeyDeleteDescription": "라이센스 키 삭제 중 오류가 발생했습니다.",
"licenseKeyDeleted": "라이센스 키가 삭제되었습니다.", "licenseKeyDeleted": "라이센스 키가 삭제되었습니다.",
"licenseKeyDeletedDescription": "라이센스 키가 삭제되었습니다.", "licenseKeyDeletedDescription": "라이센스 키가 삭제되었습니다.",
@ -351,7 +332,6 @@
"licenseAgreement": "이 상자를 체크함으로써, 귀하는 귀하의 라이선스 키와 관련된 계층에 해당하는 라이선스 조건을 읽고 동의했음을 확인합니다.", "licenseAgreement": "이 상자를 체크함으로써, 귀하는 귀하의 라이선스 키와 관련된 계층에 해당하는 라이선스 조건을 읽고 동의했음을 확인합니다.",
"fossorialLicense": "Fossorial 상업 라이선스 및 구독 약관 보기", "fossorialLicense": "Fossorial 상업 라이선스 및 구독 약관 보기",
"licenseMessageRemove": "이 작업은 라이센스 키와 그에 의해 부여된 모든 관련 권한을 제거합니다.", "licenseMessageRemove": "이 작업은 라이센스 키와 그에 의해 부여된 모든 관련 권한을 제거합니다.",
"sidebarAllUsers": "모든 사용자",
"licenseMessageConfirm": "확인을 위해 아래에 라이센스 키를 입력하세요.", "licenseMessageConfirm": "확인을 위해 아래에 라이센스 키를 입력하세요.",
"licenseQuestionRemove": "라이센스 키 {selectedKey}를 삭제하시겠습니까?", "licenseQuestionRemove": "라이센스 키 {selectedKey}를 삭제하시겠습니까?",
"licenseKeyDelete": "라이센스 키 삭제", "licenseKeyDelete": "라이센스 키 삭제",
@ -365,7 +345,6 @@
"licenseReckeckAll": "모든 키 재확인", "licenseReckeckAll": "모든 키 재확인",
"licenseSiteUsage": "사이트 사용량", "licenseSiteUsage": "사이트 사용량",
"licenseSiteUsageDecsription": "이 라이센스를 사용하는 사이트 수를 확인하세요.", "licenseSiteUsageDecsription": "이 라이센스를 사용하는 사이트 수를 확인하세요.",
"noResults": "결과를 찾을 수 없습니다.",
"licenseNoSiteLimit": "라이선스가 없는 호스트를 사용하는 사이트 수에 제한이 없습니다.", "licenseNoSiteLimit": "라이선스가 없는 호스트를 사용하는 사이트 수에 제한이 없습니다.",
"licensePurchase": "라이센스 구매", "licensePurchase": "라이센스 구매",
"licensePurchaseSites": "추가 사이트 구매", "licensePurchaseSites": "추가 사이트 구매",
@ -434,7 +413,6 @@
"idpErrorFetch": "신원 제공자를 가져오는 데 실패했습니다", "idpErrorFetch": "신원 제공자를 가져오는 데 실패했습니다",
"idpErrorFetchDescription": "신원 공급자를 가져오는 중 오류가 발생했습니다.", "idpErrorFetchDescription": "신원 공급자를 가져오는 중 오류가 발생했습니다.",
"userErrorExists": "사용자가 이미 존재합니다.", "userErrorExists": "사용자가 이미 존재합니다.",
"terabytes": "{count} TB",
"userErrorExistsDescription": "이 사용자는 이미 조직의 구성원입니다.", "userErrorExistsDescription": "이 사용자는 이미 조직의 구성원입니다.",
"inviteError": "사용자 초대에 실패했습니다", "inviteError": "사용자 초대에 실패했습니다",
"inviteErrorDescription": "사용자를 초대하는 동안 오류가 발생했습니다.", "inviteErrorDescription": "사용자를 초대하는 동안 오류가 발생했습니다.",
@ -692,7 +670,6 @@
"resourceErrorTransferDescription": "리소스를 전송하는 동안 오류가 발생했습니다", "resourceErrorTransferDescription": "리소스를 전송하는 동안 오류가 발생했습니다",
"resourceTransferred": "리소스가 전송되었습니다.", "resourceTransferred": "리소스가 전송되었습니다.",
"resourceTransferredDescription": "리소스가 성공적으로 전송되었습니다.", "resourceTransferredDescription": "리소스가 성공적으로 전송되었습니다.",
"gigabytes": "{count} GB",
"resourceErrorToggle": "리소스를 전환하는 데 실패했습니다.", "resourceErrorToggle": "리소스를 전환하는 데 실패했습니다.",
"resourceErrorToggleDescription": "리소스를 업데이트하는 동안 오류가 발생했습니다.", "resourceErrorToggleDescription": "리소스를 업데이트하는 동안 오류가 발생했습니다.",
"resourceVisibilityTitle": "가시성", "resourceVisibilityTitle": "가시성",
@ -794,8 +771,6 @@
"idpSubmit": "아이덴티티 공급자 생성", "idpSubmit": "아이덴티티 공급자 생성",
"orgPolicies": "조직 정책", "orgPolicies": "조직 정책",
"idpSettings": "{idpName} 설정", "idpSettings": "{idpName} 설정",
"megabytes": "{count} MB",
"actionCheckOrgId": "ID 확인",
"idpCreateSettingsDescription": "아이덴티티 공급자의 설정을 구성하십시오", "idpCreateSettingsDescription": "아이덴티티 공급자의 설정을 구성하십시오",
"roleMapping": "역할 매핑", "roleMapping": "역할 매핑",
"orgMapping": "조직 매핑", "orgMapping": "조직 매핑",
@ -806,7 +781,12 @@
"success": "성공", "success": "성공",
"orgPolicyAddedDescription": "정책이 성공적으로 추가되었습니다", "orgPolicyAddedDescription": "정책이 성공적으로 추가되었습니다",
"orgPolicyUpdatedDescription": "정책이 성공적으로 업데이트되었습니다.", "orgPolicyUpdatedDescription": "정책이 성공적으로 업데이트되었습니다.",
"tagsEntered": "입력된 태그", "orgPolicyDeletedDescription": "정책이 성공적으로 삭제되었습니다",
"defaultMappingsUpdatedDescription": "기본 매핑이 성공적으로 업데이트되었습니다.",
"orgPoliciesAbout": "조직 정책에 대하여",
"orgPoliciesAboutDescription": "조직 정책은 사용자의 ID 토큰에 따라 조직에 대한 액세스를 제어하는 데 사용됩니다. ID 토큰에서 역할 및 조직 정보를 추출하기 위해 JMESPath 표현식을 지정할 수 있습니다.",
"orgPoliciesAboutDescriptionLink": "자세한 내용은 문서를 참조하십시오.",
"defaultMappingsOptional": "기본 매핑(선택 사항)",
"defaultMappingsOptionalDescription": "조직에 대해 정의된 정책이 없을 때 기본 매핑이 사용됩니다. 여기에서 기본 역할 및 조직 매핑을 지정하여 대체할 수 있습니다.", "defaultMappingsOptionalDescription": "조직에 대해 정의된 정책이 없을 때 기본 매핑이 사용됩니다. 여기에서 기본 역할 및 조직 매핑을 지정하여 대체할 수 있습니다.",
"defaultMappingsRole": "기본 역할 매핑", "defaultMappingsRole": "기본 역할 매핑",
"defaultMappingsRoleDescription": "이 표현식의 결과는 조직에서 정의된 역할 이름을 문자열로 반환해야 합니다.", "defaultMappingsRoleDescription": "이 표현식의 결과는 조직에서 정의된 역할 이름을 문자열로 반환해야 합니다.",
@ -843,6 +823,13 @@
"emailVerifyResendProgress": "재전송 중...", "emailVerifyResendProgress": "재전송 중...",
"emailVerifyResend": "코드를 받지 못하셨나요? 여기 클릭하여 재전송하세요", "emailVerifyResend": "코드를 받지 못하셨나요? 여기 클릭하여 재전송하세요",
"passwordNotMatch": "비밀번호가 일치하지 않습니다.", "passwordNotMatch": "비밀번호가 일치하지 않습니다.",
"signupError": "가입하는 동안 오류가 발생했습니다.",
"pangolinLogoAlt": "판골린 로고",
"inviteAlready": "초대받은 것 같습니다!",
"inviteAlreadyDescription": "초대를 수락하려면 로그인하거나 계정을 생성해야 합니다.",
"signupQuestion": "이미 계정이 있습니까?",
"login": "로그인",
"resourceNotFound": "리소스를 찾을 수 없습니다",
"resourceNotFoundDescription": "접근하려는 리소스가 존재하지 않습니다.", "resourceNotFoundDescription": "접근하려는 리소스가 존재하지 않습니다.",
"pincodeRequirementsLength": "PIN은 정확히 6자리여야 합니다", "pincodeRequirementsLength": "PIN은 정확히 6자리여야 합니다",
"pincodeRequirementsChars": "PIN은 숫자만 포함해야 합니다.", "pincodeRequirementsChars": "PIN은 숫자만 포함해야 합니다.",
@ -923,6 +910,12 @@
"usersAll": "모든 사용자", "usersAll": "모든 사용자",
"license": "라이선스", "license": "라이선스",
"pangolinDashboard": "대시보드 - 판골린", "pangolinDashboard": "대시보드 - 판골린",
"noResults": "결과를 찾을 수 없습니다.",
"terabytes": "{count} TB",
"gigabytes": "{count} GB",
"megabytes": "{count} MB",
"tagsEntered": "입력된 태그",
"tagsEnteredDescription": "입력한 태그는 다음과 같습니다.",
"tagsWarnCannotBeLessThanZero": "maxTags와 minTags는 0보다 작을 수 없습니다", "tagsWarnCannotBeLessThanZero": "maxTags와 minTags는 0보다 작을 수 없습니다",
"tagsWarnNotAllowedAutocompleteOptions": "자동 완성 옵션에 따라 태그가 허용되지 않습니다", "tagsWarnNotAllowedAutocompleteOptions": "자동 완성 옵션에 따라 태그가 허용되지 않습니다",
"tagsWarnInvalid": "validateTag에 따라 유효하지 않은 태그입니다", "tagsWarnInvalid": "validateTag에 따라 유효하지 않은 태그입니다",
@ -960,10 +953,15 @@
"logoutError": "로그아웃 중 오류 발생", "logoutError": "로그아웃 중 오류 발생",
"signingAs": "로그인한 사용자", "signingAs": "로그인한 사용자",
"serverAdmin": "서버 관리자", "serverAdmin": "서버 관리자",
"otpEnable": "이중 인증 활성화",
"otpDisable": "이중 인증 비활성화",
"logout": "로그 아웃",
"licenseTierProfessionalRequired": "전문 에디션이 필요합니다.", "licenseTierProfessionalRequired": "전문 에디션이 필요합니다.",
"licenseTierProfessionalRequiredDescription": "이 기능은 Professional Edition에서만 사용할 수 있습니다.", "licenseTierProfessionalRequiredDescription": "이 기능은 Professional Edition에서만 사용할 수 있습니다.",
"actionGetOrg": "조직 가져오기", "actionGetOrg": "조직 가져오기",
"actionUpdateOrg": "조직 업데이트", "actionUpdateOrg": "조직 업데이트",
"actionUpdateUser": "사용자 업데이트",
"actionGetUser": "사용자 조회",
"actionGetOrgUser": "조직 사용자 가져오기", "actionGetOrgUser": "조직 사용자 가져오기",
"actionListOrgDomains": "조직 도메인 목록", "actionListOrgDomains": "조직 도메인 목록",
"actionCreateSite": "사이트 생성", "actionCreateSite": "사이트 생성",
@ -1000,13 +998,15 @@
"actionRemoveUser": "사용자 제거", "actionRemoveUser": "사용자 제거",
"actionListUsers": "사용자 목록", "actionListUsers": "사용자 목록",
"actionAddUserRole": "사용자 역할 추가", "actionAddUserRole": "사용자 역할 추가",
"containersIn": "{siteName}의 컨테이너",
"actionGenerateAccessToken": "액세스 토큰 생성", "actionGenerateAccessToken": "액세스 토큰 생성",
"actionDeleteAccessToken": "액세스 토큰 삭제", "actionDeleteAccessToken": "액세스 토큰 삭제",
"actionListAccessTokens": "액세스 토큰 목록", "actionListAccessTokens": "액세스 토큰 목록",
"actionCreateResourceRule": "리소스 규칙 생성",
"actionDeleteResourceRule": "리소스 규칙 삭제",
"actionListResourceRules": "리소스 규칙 목록", "actionListResourceRules": "리소스 규칙 목록",
"actionUpdateResourceRule": "리소스 규칙 업데이트", "actionUpdateResourceRule": "리소스 규칙 업데이트",
"actionListOrgs": "조직 목록", "actionListOrgs": "조직 목록",
"actionCheckOrgId": "ID 확인",
"actionCreateOrg": "조직 생성", "actionCreateOrg": "조직 생성",
"actionDeleteOrg": "조직 삭제", "actionDeleteOrg": "조직 삭제",
"actionListApiKeys": "API 키 목록", "actionListApiKeys": "API 키 목록",
@ -1089,12 +1089,18 @@
"sidebarInvitations": "초대", "sidebarInvitations": "초대",
"sidebarRoles": "역할", "sidebarRoles": "역할",
"sidebarShareableLinks": "공유 가능한 링크", "sidebarShareableLinks": "공유 가능한 링크",
"sidebarApiKeys": "API 키",
"sidebarSettings": "설정",
"sidebarAllUsers": "모든 사용자",
"sidebarIdentityProviders": "신원 공급자", "sidebarIdentityProviders": "신원 공급자",
"sidebarLicense": "라이선스", "sidebarLicense": "라이선스",
"sidebarClients": "클라이언트",
"sidebarDomains": "도메인",
"enableDockerSocket": "Docker 소켓 활성화", "enableDockerSocket": "Docker 소켓 활성화",
"enableDockerSocketDescription": "컨테이너 정보를 채우기 위해 Docker 소켓 검색을 활성화합니다. 소켓 경로는 Newt에 제공되어야 합니다.", "enableDockerSocketDescription": "컨테이너 정보를 채우기 위해 Docker 소켓 검색을 활성화합니다. 소켓 경로는 Newt에 제공되어야 합니다.",
"enableDockerSocketLink": "자세히 알아보기", "enableDockerSocketLink": "자세히 알아보기",
"viewDockerContainers": "도커 컨테이너 보기", "viewDockerContainers": "도커 컨테이너 보기",
"containersIn": "{siteName}의 컨테이너",
"selectContainerDescription": "이 대상을 위한 호스트 이름으로 사용할 컨테이너를 선택하세요. 포트를 사용하려면 포트를 클릭하세요.", "selectContainerDescription": "이 대상을 위한 호스트 이름으로 사용할 컨테이너를 선택하세요. 포트를 사용하려면 포트를 클릭하세요.",
"containerName": "이름", "containerName": "이름",
"containerImage": "이미지", "containerImage": "이미지",
@ -1129,8 +1135,143 @@
"dark": "어두운", "dark": "어두운",
"system": "시스템", "system": "시스템",
"theme": "테마", "theme": "테마",
"subnetRequired": "서브넷은 필수입니다",
"initialSetupTitle": "초기 서버 설정", "initialSetupTitle": "초기 서버 설정",
"initialSetupDescription": "초기 서버 관리자 계정을 생성하세요. 서버 관리자 계정은 하나만 존재할 수 있습니다. 이러한 자격 증명은 나중에 언제든지 변경할 수 있습니다.", "initialSetupDescription": "초기 서버 관리자 계정을 생성하세요. 서버 관리자 계정은 하나만 존재할 수 있습니다. 이러한 자격 증명은 나중에 언제든지 변경할 수 있습니다.",
"createAdminAccount": "관리자 계정 생성", "createAdminAccount": "관리자 계정 생성",
"setupErrorCreateAdmin": "서버 관리자 계정을 생성하는 동안 오류가 발생했습니다." "setupErrorCreateAdmin": "서버 관리자 계정을 생성하는 동안 오류가 발생했습니다.",
"certificateStatus": "인증서 상태",
"loading": "로딩 중",
"restart": "재시작",
"domains": "도메인",
"domainsDescription": "조직의 도메인을 관리합니다",
"domainsSearch": "도메인 검색...",
"domainAdd": "도메인 추가",
"domainAddDescription": "조직에 새로운 도메인을 등록하세요",
"domainCreate": "도메인 생성",
"domainCreatedDescription": "도메인이 성공적으로 생성되었습니다",
"domainDeletedDescription": "도메인이 성공적으로 삭제되었습니다",
"domainQuestionRemove": "도메인 {domain}을(를) 계정에서 제거하시겠습니까?",
"domainMessageRemove": "제거되면 도메인이 더 이상 계정과 연관되지 않습니다.",
"domainMessageConfirm": "확인하려면 아래에 도메인명을 입력하세요.",
"domainConfirmDelete": "도메인 삭제 확인",
"domainDelete": "도메인 삭제",
"domain": "도메인",
"selectDomainTypeNsName": "도메인 위임 (NS)",
"selectDomainTypeNsDescription": "이 도메인과 모든 하위 도메인입니다. 전체 도메인 영역을 제어하려면 이를 사용하세요.",
"selectDomainTypeCnameName": "단일 도메인 (CNAME)",
"selectDomainTypeCnameDescription": "단일 하위 도메인 또는 특정 도메인 항목에 사용됩니다.",
"selectDomainTypeWildcardName": "와일드카드 도메인",
"selectDomainTypeWildcardDescription": "이 도메인과 그 첫 번째 레벨의 하위 도메인입니다.",
"domainDelegation": "단일 도메인",
"selectType": "유형 선택",
"actions": "작업",
"refresh": "새로 고침",
"refreshError": "데이터 새로고침 실패",
"verified": "검증됨",
"pending": "대기 중",
"sidebarBilling": "청구",
"billing": "청구",
"orgBillingDescription": "청구 정보 및 구독을 관리하세요",
"github": "GitHub",
"pangolinHosted": "판골린 호스팅",
"fossorial": "지하 서식",
"completeAccountSetup": "계정 설정 완료",
"completeAccountSetupDescription": "시작하려면 비밀번호를 설정하세요",
"accountSetupSent": "이 이메일 주소로 계정 설정 코드를 보내드리겠습니다.",
"accountSetupCode": "설정 코드",
"accountSetupCodeDescription": "설정 코드를 확인하기 위해 이메일을 확인하세요.",
"passwordCreate": "비밀번호 생성",
"passwordCreateConfirm": "비밀번호 확인",
"accountSetupSubmit": "설정 코드 전송",
"completeSetup": "설정 완료",
"accountSetupSuccess": "계정 설정이 완료되었습니다! 판골린에 오신 것을 환영합니다!",
"documentation": "문서",
"saveAllSettings": "모든 설정 저장",
"settingsUpdated": "설정이 업데이트되었습니다",
"settingsUpdatedDescription": "모든 설정이 성공적으로 업데이트되었습니다",
"settingsErrorUpdate": "설정 업데이트 실패",
"settingsErrorUpdateDescription": "설정을 업데이트하는 동안 오류가 발생했습니다",
"sidebarCollapse": "줄이기",
"sidebarExpand": "확장하기",
"newtUpdateAvailable": "업데이트 가능",
"newtUpdateAvailableInfo": "뉴트의 새 버전이 출시되었습니다. 최상의 경험을 위해 최신 버전으로 업데이트하세요.",
"domainPickerEnterDomain": "도메인 입력",
"domainPickerPlaceholder": "myapp.example.com, api.v1.mydomain.com, 또는 그냥 myapp",
"domainPickerDescription": "Enter the full domain of the resource to see available options.",
"domainPickerDescriptionSaas": "Enter a full domain, subdomain, or just a name to see available options",
"domainPickerTabAll": "모두",
"domainPickerTabOrganization": "조직",
"domainPickerTabProvided": "제공 됨",
"domainPickerSortAsc": "A-Z",
"domainPickerSortDesc": "Z-A",
"domainPickerCheckingAvailability": "가용성을 확인 중...",
"domainPickerNoMatchingDomains": "\"{userInput}\"에 해당하는 도메인을 찾을 수 없습니다. 다른 도메인을 시도하거나 조직의 도메인 설정을 확인하세요.",
"domainPickerOrganizationDomains": "조직 도메인",
"domainPickerProvidedDomains": "제공된 도메인",
"domainPickerSubdomain": "서브도메인: {subdomain}",
"domainPickerNamespace": "이름 공간: {namespace}",
"domainPickerShowMore": "더보기",
"domainNotFound": "도메인을 찾을 수 없습니다",
"domainNotFoundDescription": "이 리소스는 도메인이 더 이상 시스템에 존재하지 않아 비활성화되었습니다. 이 리소스에 대한 새 도메인을 설정하세요.",
"failed": "실패",
"createNewOrgDescription": "새 조직 생성",
"organization": "조직",
"port": "포트",
"securityKeyManage": "보안 키 관리",
"securityKeyDescription": "비밀번호 없는 인증을 위해 보안 키를 추가하거나 제거합니다.",
"securityKeyRegister": "새 보안 키 등록",
"securityKeyList": "귀하의 보안 키",
"securityKeyNone": "등록된 보안 키가 아직 없습니다",
"securityKeyNameRequired": "이름은 필수입니다",
"securityKeyRemove": "제거",
"securityKeyLastUsed": "마지막 사용: {date}",
"securityKeyNameLabel": "보안 키 이름",
"securityKeyRegisterSuccess": "보안 키가 성공적으로 등록되었습니다",
"securityKeyRegisterError": "보안 키 등록 실패",
"securityKeyRemoveSuccess": "보안 키가 성공적으로 제거되었습니다",
"securityKeyRemoveError": "보안 키 제거 실패",
"securityKeyLoadError": "보안 키를 불러오는 데 실패했습니다",
"securityKeyLogin": "Continue with security key",
"securityKeyAuthError": "보안 키를 사용한 인증 실패",
"securityKeyRecommendation": "항상 계정에 액세스할 수 있도록 다른 장치에 백업 보안 키를 등록하세요.",
"registering": "등록 중...",
"securityKeyPrompt": "보안 키를 사용하여 본인 확인을 진행하세요. 보안 키가 연결되어 사용 준비가 되었는지 확인하세요.",
"securityKeyBrowserNotSupported": "귀하의 브라우저는 보안 키를 지원하지 않습니다. Chrome, Firefox, 또는 Safari와 같은 최신 브라우저를 사용하세요.",
"securityKeyPermissionDenied": "로그인을 계속하려면 보안 키에 대한 액세스를 허용하세요.",
"securityKeyRemovedTooQuickly": "로그인 프로세스가 완료될 때까지 보안 키를 연결 상태로 유지하세요.",
"securityKeyNotSupported": "보안 키가 호환되지 않을 수 있습니다. 다른 보안 키를 사용해보세요.",
"securityKeyUnknownError": "보안 키를 사용하는 데 문제가 발생했습니다. 다시 시도하세요.",
"twoFactorRequired": "보안 키를 등록하려면 이중 인증이 필요합니다.",
"twoFactor": "이중 인증",
"adminEnabled2FaOnYourAccount": "관리자가 {email}에 대한 이중 인증을 활성화했습니다. 계속하려면 설정을 완료하세요.",
"continueToApplication": "응용 프로그램으로 계속",
"securityKeyAdd": "보안 키 추가",
"securityKeyRegisterTitle": "새 보안 키 등록",
"securityKeyRegisterDescription": "보안 키를 연결하고 식별할 이름을 입력하세요.",
"securityKeyTwoFactorRequired": "이중 인증 필요",
"securityKeyTwoFactorDescription": "보안 키를 등록하려면 이중 인증 코드를 입력하세요.",
"securityKeyTwoFactorRemoveDescription": "보안 키를 제거하려면 이중 인증 코드를 입력하세요.",
"securityKeyTwoFactorCode": "이중 인증 코드",
"securityKeyRemoveTitle": "보안 키 삭제",
"securityKeyRemoveDescription": "보안 키 \"{name}\"를 제거하려면 비밀번호를 입력하세요",
"securityKeyNoKeysRegistered": "등록된 보안 키가 없습니다",
"securityKeyNoKeysDescription": "계정 보안을 강화하려면 보안 키를 추가하세요.",
"createDomainRequired": "도메인은 필수입니다",
"createDomainAddDnsRecords": "DNS 레코드 추가",
"createDomainAddDnsRecordsDescription": "설정을 완료하려면 도메인 제공자에게 다음 DNS 레코드를 추가하세요.",
"createDomainNsRecords": "NS 레코드",
"createDomainRecord": "레코드",
"createDomainType": "유형:",
"createDomainName": "이름:",
"createDomainValue": "값:",
"createDomainCnameRecords": "CNAME 레코드",
"createDomainRecordNumber": "레코드 {number}",
"createDomainTxtRecords": "TXT 레코드",
"createDomainSaveTheseRecords": "이 레코드 저장",
"createDomainSaveTheseRecordsDescription": "이 DNS 레코드를 저장하여 이후에 다시 볼 수 없습니다.",
"createDomainDnsPropagation": "DNS 전파",
"createDomainDnsPropagationDescription": "DNS 변경 사항은 인터넷 전체에 전파되는 데 시간이 걸립니다. DNS 제공자와 TTL 설정에 따라 몇 분에서 48시간까지 걸릴 수 있습니다.",
"resourcePortRequired": "HTTP 리소스가 아닌 경우 포트 번호가 필요합니다",
"resourcePortNotAllowed": "HTTP 리소스에 대해 포트 번호를 설정하지 마세요"
} }

View file

@ -11,8 +11,9 @@
"componentsErrorNoMemberCreate": "U bent momenteel geen lid van een organisatie. Maak een organisatie aan om aan de slag te gaan.", "componentsErrorNoMemberCreate": "U bent momenteel geen lid van een organisatie. Maak een organisatie aan om aan de slag te gaan.",
"componentsErrorNoMember": "U bent momenteel geen lid van een organisatie.", "componentsErrorNoMember": "U bent momenteel geen lid van een organisatie.",
"welcome": "Welkom bij Pangolin", "welcome": "Welkom bij Pangolin",
"welcomeTo": "Welkom bij",
"componentsCreateOrg": "Maak een Organisatie", "componentsCreateOrg": "Maak een Organisatie",
"componentsMember": "Je bent lid van {count, plural, =0 {geen organisatie} =1 {één organisatie} other {# organisaties}}.", "componentsMember": "Je bent lid van {count, plural, =0 {geen organisatie} one {één organisatie} other {# organisaties}}.",
"componentsInvalidKey": "Ongeldige of verlopen licentiesleutels gedetecteerd. Volg de licentievoorwaarden om alle functies te blijven gebruiken.", "componentsInvalidKey": "Ongeldige of verlopen licentiesleutels gedetecteerd. Volg de licentievoorwaarden om alle functies te blijven gebruiken.",
"dismiss": "Uitschakelen", "dismiss": "Uitschakelen",
"componentsLicenseViolation": "Licentie overtreding: Deze server gebruikt {usedSites} sites die de gelicentieerde limiet van {maxSites} sites overschrijden. Volg de licentievoorwaarden om door te gaan met het gebruik van alle functies.", "componentsLicenseViolation": "Licentie overtreding: Deze server gebruikt {usedSites} sites die de gelicentieerde limiet van {maxSites} sites overschrijden. Volg de licentievoorwaarden om door te gaan met het gebruik van alle functies.",
@ -206,6 +207,7 @@
"orgGeneralSettings": "Organisatie Instellingen", "orgGeneralSettings": "Organisatie Instellingen",
"orgGeneralSettingsDescription": "Beheer de details en configuratie van uw organisatie", "orgGeneralSettingsDescription": "Beheer de details en configuratie van uw organisatie",
"saveGeneralSettings": "Algemene instellingen opslaan", "saveGeneralSettings": "Algemene instellingen opslaan",
"saveSettings": "Instellingen opslaan",
"orgDangerZone": "Gevaarlijke zone", "orgDangerZone": "Gevaarlijke zone",
"orgDangerZoneDescription": "Als u deze instantie verwijdert, is er geen weg terug. Wees het alstublieft zeker.", "orgDangerZoneDescription": "Als u deze instantie verwijdert, is er geen weg terug. Wees het alstublieft zeker.",
"orgDelete": "Verwijder organisatie", "orgDelete": "Verwijder organisatie",
@ -249,7 +251,7 @@
"weeks": "Weken", "weeks": "Weken",
"months": "maanden", "months": "maanden",
"years": "Jaar", "years": "Jaar",
"day": "{count, plural, =1 {# dag} other {# dagen}}", "day": "{count, plural, one {# dag} other {# dagen}}",
"apiKeysTitle": "API Key Informatie", "apiKeysTitle": "API Key Informatie",
"apiKeysConfirmCopy2": "Bevestig dat u de API-sleutel hebt gekopieerd.", "apiKeysConfirmCopy2": "Bevestig dat u de API-sleutel hebt gekopieerd.",
"apiKeysErrorCreate": "Fout bij maken API-sleutel", "apiKeysErrorCreate": "Fout bij maken API-sleutel",
@ -347,7 +349,7 @@
"licensePurchase": "Licentie kopen", "licensePurchase": "Licentie kopen",
"licensePurchaseSites": "Extra sites kopen", "licensePurchaseSites": "Extra sites kopen",
"licenseSitesUsedMax": "{usedSites} van {maxSites} sites gebruikt", "licenseSitesUsedMax": "{usedSites} van {maxSites} sites gebruikt",
"licenseSitesUsed": "{count, plural, =0 {# sites} =1 {# site} other {# sites}} in het systeem.", "licenseSitesUsed": "{count, plural, =0 {# locaties} one {# locatie} other {# locaties}} in het systeem.",
"licensePurchaseDescription": "Kies hoeveel sites je wilt {selectedMode, select, license {Koop een licentie. Je kunt later altijd meer sites toevoegen.} other {Voeg je bestaande licentie toe}}", "licensePurchaseDescription": "Kies hoeveel sites je wilt {selectedMode, select, license {Koop een licentie. Je kunt later altijd meer sites toevoegen.} other {Voeg je bestaande licentie toe}}",
"licenseFee": "Licentie vergoeding", "licenseFee": "Licentie vergoeding",
"licensePriceSite": "Prijs per site", "licensePriceSite": "Prijs per site",
@ -436,7 +438,7 @@
"accessRoleSelect": "Selecteer rol", "accessRoleSelect": "Selecteer rol",
"inviteEmailSentDescription": "Een e-mail is verstuurd naar de gebruiker met de link hieronder. Ze moeten toegang krijgen tot de link om de uitnodiging te accepteren.", "inviteEmailSentDescription": "Een e-mail is verstuurd naar de gebruiker met de link hieronder. Ze moeten toegang krijgen tot de link om de uitnodiging te accepteren.",
"inviteSentDescription": "De gebruiker is uitgenodigd. Ze moeten toegang krijgen tot de link hieronder om de uitnodiging te accepteren.", "inviteSentDescription": "De gebruiker is uitgenodigd. Ze moeten toegang krijgen tot de link hieronder om de uitnodiging te accepteren.",
"inviteExpiresIn": "De uitnodiging vervalt in {days, plural, =1 {# dag} other {# dagen}}.", "inviteExpiresIn": "De uitnodiging vervalt over {days, plural, one {# dag} other {# dagen}}.",
"idpTitle": "Identiteit Provider", "idpTitle": "Identiteit Provider",
"idpSelect": "Identiteitsprovider voor de externe gebruiker selecteren", "idpSelect": "Identiteitsprovider voor de externe gebruiker selecteren",
"idpNotConfigured": "Er zijn geen identiteitsproviders geconfigureerd. Configureer een identiteitsprovider voordat u externe gebruikers aanmaakt.", "idpNotConfigured": "Er zijn geen identiteitsproviders geconfigureerd. Configureer een identiteitsprovider voordat u externe gebruikers aanmaakt.",
@ -958,6 +960,8 @@
"licenseTierProfessionalRequiredDescription": "Deze functie is alleen beschikbaar in de Professional Edition.", "licenseTierProfessionalRequiredDescription": "Deze functie is alleen beschikbaar in de Professional Edition.",
"actionGetOrg": "Krijg Organisatie", "actionGetOrg": "Krijg Organisatie",
"actionUpdateOrg": "Organisatie bijwerken", "actionUpdateOrg": "Organisatie bijwerken",
"actionUpdateUser": "Gebruiker bijwerken",
"actionGetUser": "Gebruiker ophalen",
"actionGetOrgUser": "Krijg organisatie-gebruiker", "actionGetOrgUser": "Krijg organisatie-gebruiker",
"actionListOrgDomains": "Lijst organisatie domeinen", "actionListOrgDomains": "Lijst organisatie domeinen",
"actionCreateSite": "Site maken", "actionCreateSite": "Site maken",
@ -1090,6 +1094,8 @@
"sidebarAllUsers": "Alle gebruikers", "sidebarAllUsers": "Alle gebruikers",
"sidebarIdentityProviders": "Identiteit aanbieders", "sidebarIdentityProviders": "Identiteit aanbieders",
"sidebarLicense": "Licentie", "sidebarLicense": "Licentie",
"sidebarClients": "Cliënten",
"sidebarDomains": "Domeinen",
"enableDockerSocket": "Docker Socket inschakelen", "enableDockerSocket": "Docker Socket inschakelen",
"enableDockerSocketDescription": "Docker Socket-ontdekking inschakelen voor het invullen van containerinformatie. Socket-pad moet aan Newt worden verstrekt.", "enableDockerSocketDescription": "Docker Socket-ontdekking inschakelen voor het invullen van containerinformatie. Socket-pad moet aan Newt worden verstrekt.",
"enableDockerSocketLink": "Meer informatie", "enableDockerSocketLink": "Meer informatie",
@ -1102,7 +1108,7 @@
"containerNetworks": "Netwerken", "containerNetworks": "Netwerken",
"containerHostnameIp": "Hostnaam/IP", "containerHostnameIp": "Hostnaam/IP",
"containerLabels": "Labels", "containerLabels": "Labels",
"containerLabelsCount": "{count} label{s,plural,one{} other{s}}", "containerLabelsCount": "{count, plural, one {# label} other {# labels}}",
"containerLabelsTitle": "Container labels", "containerLabelsTitle": "Container labels",
"containerLabelEmpty": "<leeg>", "containerLabelEmpty": "<leeg>",
"containerPorts": "Poorten", "containerPorts": "Poorten",
@ -1114,7 +1120,7 @@
"showStoppedContainers": "Toon gestopte containers", "showStoppedContainers": "Toon gestopte containers",
"noContainersFound": "Geen containers gevonden. Zorg ervoor dat Docker containers draaien.", "noContainersFound": "Geen containers gevonden. Zorg ervoor dat Docker containers draaien.",
"searchContainersPlaceholder": "Zoek tussen {count} containers...", "searchContainersPlaceholder": "Zoek tussen {count} containers...",
"searchResultsCount": "{count} resultaat{s,plural,one{} other{s}}", "searchResultsCount": "{count, plural, one {# resultaat} other {# resultaten}}",
"filters": "Filters", "filters": "Filters",
"filterOptions": "Filter opties", "filterOptions": "Filter opties",
"filterPorts": "Poorten", "filterPorts": "Poorten",
@ -1129,10 +1135,89 @@
"dark": "donker", "dark": "donker",
"system": "systeem", "system": "systeem",
"theme": "Thema", "theme": "Thema",
"subnetRequired": "Subnet is vereist",
"initialSetupTitle": "Initiële serverconfiguratie", "initialSetupTitle": "Initiële serverconfiguratie",
"initialSetupDescription": "Maak het eerste serverbeheeraccount aan. Er kan slechts één serverbeheerder bestaan. U kunt deze inloggegevens later altijd wijzigen.", "initialSetupDescription": "Maak het eerste serverbeheeraccount aan. Er kan slechts één serverbeheerder bestaan. U kunt deze inloggegevens later altijd wijzigen.",
"createAdminAccount": "Maak een beheeraccount aan", "createAdminAccount": "Maak een beheeraccount aan",
"setupErrorCreateAdmin": "Er is een fout opgetreden bij het maken van het serverbeheerdersaccount.", "setupErrorCreateAdmin": "Er is een fout opgetreden bij het maken van het serverbeheerdersaccount.",
"certificateStatus": "Certificaatstatus",
"loading": "Bezig met laden",
"restart": "Herstarten",
"domains": "Domeinen",
"domainsDescription": "Beheer domeinen voor je organisatie",
"domainsSearch": "Zoek domeinen...",
"domainAdd": "Domein toevoegen",
"domainAddDescription": "Registreer een nieuw domein bij je organisatie",
"domainCreate": "Domein aanmaken",
"domainCreatedDescription": "Domein succesvol aangemaakt",
"domainDeletedDescription": "Domein succesvol verwijderd",
"domainQuestionRemove": "Weet je zeker dat je het domein {domain} uit je account wilt verwijderen?",
"domainMessageRemove": "Na verwijdering zal het domein niet langer aan je account gekoppeld zijn.",
"domainMessageConfirm": "Om te bevestigen, typ hieronder de domeinnaam.",
"domainConfirmDelete": "Bevestig verwijdering van domein",
"domainDelete": "Domein verwijderen",
"domain": "Domein",
"selectDomainTypeNsName": "Domeindelegatie (NS)",
"selectDomainTypeNsDescription": "Dit domein en al zijn subdomeinen. Gebruik dit wanneer je een volledige domeinzone wilt beheersen.",
"selectDomainTypeCnameName": "Enkel domein (CNAME)",
"selectDomainTypeCnameDescription": "Alleen dit specifieke domein. Gebruik dit voor individuele subdomeinen of specifieke domeinvermeldingen.",
"selectDomainTypeWildcardName": "Wildcard Domein",
"selectDomainTypeWildcardDescription": "Dit domein en zijn eerste niveau van subdomeinen.",
"domainDelegation": "Enkel domein",
"selectType": "Selecteer een type",
"actions": "acties",
"refresh": "Vernieuwen",
"refreshError": "Het vernieuwen van gegevens is mislukt",
"verified": "Gecontroleerd",
"pending": "In afwachting",
"sidebarBilling": "Facturering",
"billing": "Facturering",
"orgBillingDescription": "Beheer je factureringsgegevens en abonnementen",
"github": "GitHub",
"pangolinHosted": "Pangolin gehost",
"fossorial": "Fossorial",
"completeAccountSetup": "Voltooi accountinstelling",
"completeAccountSetupDescription": "Stel je wachtwoord in om te beginnen",
"accountSetupSent": "We sturen een accountinstellingscode naar dit e-mailadres.",
"accountSetupCode": "Instellingscode",
"accountSetupCodeDescription": "Controleer je e-mail voor de instellingscode.",
"passwordCreate": "Wachtwoord aanmaken",
"passwordCreateConfirm": "Bevestig wachtwoord",
"accountSetupSubmit": "Instellingscode verzenden",
"completeSetup": "Voltooi instellen",
"accountSetupSuccess": "Accountinstelling voltooid! Welkom bij Pangolin!",
"documentation": "Documentatie",
"saveAllSettings": "Alle instellingen opslaan",
"settingsUpdated": "Instellingen bijgewerkt",
"settingsUpdatedDescription": "Alle instellingen zijn succesvol bijgewerkt",
"settingsErrorUpdate": "Bijwerken van instellingen mislukt",
"settingsErrorUpdateDescription": "Er is een fout opgetreden bij het bijwerken van instellingen",
"sidebarCollapse": "Inklappen",
"sidebarExpand": "Uitklappen",
"newtUpdateAvailable": "Update beschikbaar",
"newtUpdateAvailableInfo": "Er is een nieuwe versie van Newt beschikbaar. Update naar de nieuwste versie voor de beste ervaring.",
"domainPickerEnterDomain": "Voer je domein in",
"domainPickerPlaceholder": "mijnapp.voorbeeld.com, api.v1.mijndomein.com, of gewoon mijnapp",
"domainPickerDescription": "Voer de volledige domein van de bron in om beschikbare opties te zien.",
"domainPickerDescriptionSaas": "Voer een volledig domein, subdomein of gewoon een naam in om beschikbare opties te zien",
"domainPickerTabAll": "Alles",
"domainPickerTabOrganization": "Organisatie",
"domainPickerTabProvided": "Aangeboden",
"domainPickerSortAsc": "A-Z",
"domainPickerSortDesc": "Z-A",
"domainPickerCheckingAvailability": "Beschikbaarheid controleren...",
"domainPickerNoMatchingDomains": "Geen overeenkomende domeinen gevonden voor \"{userInput}\". Probeer een ander domein of controleer de domeininstellingen van je organisatie.",
"domainPickerOrganizationDomains": "Organisatiedomeinen",
"domainPickerProvidedDomains": "Aangeboden domeinen",
"domainPickerSubdomain": "Subdomein: {subdomain}",
"domainPickerNamespace": "Namespace: {namespace}",
"domainPickerShowMore": "Meer weergeven",
"domainNotFound": "Domein niet gevonden",
"domainNotFoundDescription": "Deze bron is uitgeschakeld omdat het domein niet langer in ons systeem bestaat. Stel een nieuw domein in voor deze bron.",
"failed": "Mislukt",
"createNewOrgDescription": "Maak een nieuwe organisatie",
"organization": "Organisatie",
"port": "Poort",
"securityKeyManage": "Beveiligingssleutels beheren", "securityKeyManage": "Beveiligingssleutels beheren",
"securityKeyDescription": "Voeg beveiligingssleutels toe of verwijder ze voor wachtwoordloze authenticatie", "securityKeyDescription": "Voeg beveiligingssleutels toe of verwijder ze voor wachtwoordloze authenticatie",
"securityKeyRegister": "Nieuwe beveiligingssleutel registreren", "securityKeyRegister": "Nieuwe beveiligingssleutel registreren",
@ -1142,13 +1227,51 @@
"securityKeyRemove": "Verwijderen", "securityKeyRemove": "Verwijderen",
"securityKeyLastUsed": "Laatst gebruikt: {date}", "securityKeyLastUsed": "Laatst gebruikt: {date}",
"securityKeyNameLabel": "Naam", "securityKeyNameLabel": "Naam",
"securityKeyNamePlaceholder": "Voer een naam in voor deze beveiligingssleutel",
"securityKeyRegisterSuccess": "Beveiligingssleutel succesvol geregistreerd", "securityKeyRegisterSuccess": "Beveiligingssleutel succesvol geregistreerd",
"securityKeyRegisterError": "Fout bij registreren van beveiligingssleutel", "securityKeyRegisterError": "Fout bij registreren van beveiligingssleutel",
"securityKeyRemoveSuccess": "Beveiligingssleutel succesvol verwijderd", "securityKeyRemoveSuccess": "Beveiligingssleutel succesvol verwijderd",
"securityKeyRemoveError": "Fout bij verwijderen van beveiligingssleutel", "securityKeyRemoveError": "Fout bij verwijderen van beveiligingssleutel",
"securityKeyLoadError": "Fout bij laden van beveiligingssleutels", "securityKeyLoadError": "Fout bij laden van beveiligingssleutels",
"securityKeyLogin": "Inloggen met beveiligingssleutel", "securityKeyLogin": "Doorgaan met beveiligingssleutel",
"securityKeyAuthError": "Fout bij authenticatie met beveiligingssleutel", "securityKeyAuthError": "Fout bij authenticatie met beveiligingssleutel",
"securityKeyRecommendation": "Overweeg om een andere beveiligingssleutel te registreren op een ander apparaat om ervoor te zorgen dat u niet buitengesloten raakt van uw account." "securityKeyRecommendation": "Overweeg om een andere beveiligingssleutel te registreren op een ander apparaat om ervoor te zorgen dat u niet buitengesloten raakt van uw account.",
"registering": "Registreren...",
"securityKeyPrompt": "Verifieer je identiteit met je beveiligingssleutel. Zorg ervoor dat je beveiligingssleutel verbonden en klaar is.",
"securityKeyBrowserNotSupported": "Je browser ondersteunt geen beveiligingssleutels. Gebruik een moderne browser zoals Chrome, Firefox of Safari.",
"securityKeyPermissionDenied": "Verleen toegang tot je beveiligingssleutel om door te gaan met inloggen.",
"securityKeyRemovedTooQuickly": "Houd je beveiligingssleutel verbonden totdat het inlogproces is voltooid.",
"securityKeyNotSupported": "Je beveiligingssleutel is mogelijk niet compatibel. Probeer een andere beveiligingssleutel.",
"securityKeyUnknownError": "Er was een probleem met het gebruik van je beveiligingssleutel. Probeer het opnieuw.",
"twoFactorRequired": "Tweestapsverificatie is vereist om een beveiligingssleutel te registreren.",
"twoFactor": "Tweestapsverificatie",
"adminEnabled2FaOnYourAccount": "Je beheerder heeft tweestapsverificatie voor {email} ingeschakeld. Voltooi het instellingsproces om verder te gaan.",
"continueToApplication": "Doorgaan naar de applicatie",
"securityKeyAdd": "Beveiligingssleutel toevoegen",
"securityKeyRegisterTitle": "Nieuwe beveiligingssleutel registreren",
"securityKeyRegisterDescription": "Verbind je beveiligingssleutel en voer een naam in om deze te identificeren",
"securityKeyTwoFactorRequired": "Tweestapsverificatie vereist",
"securityKeyTwoFactorDescription": "Voer je tweestapsverificatiecode in om de beveiligingssleutel te registreren",
"securityKeyTwoFactorRemoveDescription": "Voer je tweestapsverificatiecode in om de beveiligingssleutel te verwijderen",
"securityKeyTwoFactorCode": "Tweestapsverificatiecode",
"securityKeyRemoveTitle": "Beveiligingssleutel verwijderen",
"securityKeyRemoveDescription": "Voer je wachtwoord in om de beveiligingssleutel \"{name}\" te verwijderen",
"securityKeyNoKeysRegistered": "Geen beveiligingssleutels geregistreerd",
"securityKeyNoKeysDescription": "Voeg een beveiligingssleutel toe om je accountbeveiliging te verbeteren",
"createDomainRequired": "Domein is vereist",
"createDomainAddDnsRecords": "DNS-records toevoegen",
"createDomainAddDnsRecordsDescription": "Voeg de volgende DNS-records toe aan je domeinprovider om het instellen te voltooien.",
"createDomainNsRecords": "NS-records",
"createDomainRecord": "Record",
"createDomainType": "Type:",
"createDomainName": "Naam:",
"createDomainValue": "Waarde:",
"createDomainCnameRecords": "CNAME-records",
"createDomainRecordNumber": "Record {number}",
"createDomainTxtRecords": "TXT-records",
"createDomainSaveTheseRecords": "Deze records opslaan",
"createDomainSaveTheseRecordsDescription": "Zorg ervoor dat je deze DNS-records opslaat, want je zult ze niet opnieuw zien.",
"createDomainDnsPropagation": "DNS-propagatie",
"createDomainDnsPropagationDescription": "DNS-wijzigingen kunnen enige tijd duren om over het internet te worden verspreid. Dit kan enkele minuten tot 48 uur duren, afhankelijk van je DNS-provider en TTL-instellingen.",
"resourcePortRequired": "Poortnummer is vereist voor niet-HTTP-bronnen",
"resourcePortNotAllowed": "Poortnummer mag niet worden ingesteld voor HTTP-bronnen"
} }

View file

@ -11,8 +11,9 @@
"componentsErrorNoMemberCreate": "Nie jesteś obecnie członkiem żadnej organizacji. Aby rozpocząć, utwórz organizację.", "componentsErrorNoMemberCreate": "Nie jesteś obecnie członkiem żadnej organizacji. Aby rozpocząć, utwórz organizację.",
"componentsErrorNoMember": "Nie jesteś obecnie członkiem żadnej organizacji.", "componentsErrorNoMember": "Nie jesteś obecnie członkiem żadnej organizacji.",
"welcome": "Witaj w Pangolinie", "welcome": "Witaj w Pangolinie",
"welcomeTo": "Witaj w",
"componentsCreateOrg": "Utwórz organizację", "componentsCreateOrg": "Utwórz organizację",
"componentsMember": "Jesteś członkiem {count, plural, =0 {Żadna organizacja} =1 {Jedna organizacja} other {# organizacji}}.", "componentsMember": "Jesteś członkiem {count, plural, =0 {żadna organizacja} one {jedna organizacja} few {# organizacje} many {# organizacji} other {# organizacji}}.",
"componentsInvalidKey": "Wykryto nieprawidłowe lub wygasłe klucze licencyjne. Postępuj zgodnie z warunkami licencji, aby kontynuować korzystanie ze wszystkich funkcji.", "componentsInvalidKey": "Wykryto nieprawidłowe lub wygasłe klucze licencyjne. Postępuj zgodnie z warunkami licencji, aby kontynuować korzystanie ze wszystkich funkcji.",
"dismiss": "Odrzuć", "dismiss": "Odrzuć",
"componentsLicenseViolation": "Naruszenie licencji: Ten serwer używa stron {usedSites} , które przekraczają limit licencyjny stron {maxSites} . Postępuj zgodnie z warunkami licencji, aby kontynuować korzystanie ze wszystkich funkcji.", "componentsLicenseViolation": "Naruszenie licencji: Ten serwer używa stron {usedSites} , które przekraczają limit licencyjny stron {maxSites} . Postępuj zgodnie z warunkami licencji, aby kontynuować korzystanie ze wszystkich funkcji.",
@ -34,7 +35,7 @@
"createAccount": "Utwórz konto", "createAccount": "Utwórz konto",
"viewSettings": "Pokaż ustawienia", "viewSettings": "Pokaż ustawienia",
"delete": "Usuń", "delete": "Usuń",
"name": "Nazwisko", "name": "Nazwa",
"online": "Dostępny", "online": "Dostępny",
"offline": "Offline", "offline": "Offline",
"site": "Witryna", "site": "Witryna",
@ -138,7 +139,7 @@
"resourceSearch": "Szukaj zasobów", "resourceSearch": "Szukaj zasobów",
"openMenu": "Otwórz menu", "openMenu": "Otwórz menu",
"resource": "Zasoby", "resource": "Zasoby",
"title": "Rozporządzenie Rady (EWG) nr 2658/87 z dnia 23 lipca 1987 r. w sprawie nomenklatury taryfowej i statystycznej oraz w sprawie Wspólnej Taryfy Celnej (Dz.U. L 256 z 7.9.1987, s. 1).", "title": "Tytuł",
"created": "Utworzono", "created": "Utworzono",
"expires": "Wygasa", "expires": "Wygasa",
"never": "Nigdy", "never": "Nigdy",
@ -206,6 +207,7 @@
"orgGeneralSettings": "Ustawienia organizacji", "orgGeneralSettings": "Ustawienia organizacji",
"orgGeneralSettingsDescription": "Zarządzaj szczegółami swojej organizacji i konfiguracją", "orgGeneralSettingsDescription": "Zarządzaj szczegółami swojej organizacji i konfiguracją",
"saveGeneralSettings": "Zapisz ustawienia ogólne", "saveGeneralSettings": "Zapisz ustawienia ogólne",
"saveSettings": "Zapisz ustawienia",
"orgDangerZone": "Strefa zagrożenia", "orgDangerZone": "Strefa zagrożenia",
"orgDangerZoneDescription": "Po usunięciu tego organa nie ma odwrotu. Upewnij się.", "orgDangerZoneDescription": "Po usunięciu tego organa nie ma odwrotu. Upewnij się.",
"orgDelete": "Usuń organizację", "orgDelete": "Usuń organizację",
@ -249,7 +251,7 @@
"weeks": "Tygodnie", "weeks": "Tygodnie",
"months": "Miesiące", "months": "Miesiące",
"years": "Lata", "years": "Lata",
"day": "{count, plural, =1 {# dzień} other {# dni}}", "day": "{count, plural, one {# dzień} few {# dni} many {# dni} other {# dni}}",
"apiKeysTitle": "Informacje o kluczu API", "apiKeysTitle": "Informacje o kluczu API",
"apiKeysConfirmCopy2": "Musisz potwierdzić, że skopiowałeś klucz API.", "apiKeysConfirmCopy2": "Musisz potwierdzić, że skopiowałeś klucz API.",
"apiKeysErrorCreate": "Błąd podczas tworzenia klucza API", "apiKeysErrorCreate": "Błąd podczas tworzenia klucza API",
@ -347,7 +349,7 @@
"licensePurchase": "Kup licencję", "licensePurchase": "Kup licencję",
"licensePurchaseSites": "Kup dodatkowe witryny", "licensePurchaseSites": "Kup dodatkowe witryny",
"licenseSitesUsedMax": "Użyte strony {usedSites} z {maxSites}", "licenseSitesUsedMax": "Użyte strony {usedSites} z {maxSites}",
"licenseSitesUsed": "{count, plural, =0 {# witryn} =1 {# witryn} other {# witryn}} w systemie.", "licenseSitesUsed": "{count, plural, =0 {# witryn} one {# witryna} few {# witryny} many {# witryn} other {# witryn}} w systemie.",
"licensePurchaseDescription": "Wybierz ile witryn chcesz {selectedMode, select, license {kupić licencję. Zawsze możesz dodać więcej witryn później.} other {dodaj do swojej istniejącej licencji.}}", "licensePurchaseDescription": "Wybierz ile witryn chcesz {selectedMode, select, license {kupić licencję. Zawsze możesz dodać więcej witryn później.} other {dodaj do swojej istniejącej licencji.}}",
"licenseFee": "Opłata licencyjna", "licenseFee": "Opłata licencyjna",
"licensePriceSite": "Cena za witrynę", "licensePriceSite": "Cena za witrynę",
@ -436,7 +438,7 @@
"accessRoleSelect": "Wybierz rolę", "accessRoleSelect": "Wybierz rolę",
"inviteEmailSentDescription": "Email został wysłany do użytkownika z linkiem dostępu poniżej. Musi on uzyskać dostęp do linku, aby zaakceptować zaproszenie.", "inviteEmailSentDescription": "Email został wysłany do użytkownika z linkiem dostępu poniżej. Musi on uzyskać dostęp do linku, aby zaakceptować zaproszenie.",
"inviteSentDescription": "Użytkownik został zaproszony. Musi uzyskać dostęp do poniższego linku, aby zaakceptować zaproszenie.", "inviteSentDescription": "Użytkownik został zaproszony. Musi uzyskać dostęp do poniższego linku, aby zaakceptować zaproszenie.",
"inviteExpiresIn": "Zaproszenie wygaśnie za {days, plural, =1 {# dzień} other {# dni}}.", "inviteExpiresIn": "Zaproszenie wygaśnie za {days, plural, one {# dzień} few {# dni} many {# dni} other {# dni}}.",
"idpTitle": "Informacje ogólne", "idpTitle": "Informacje ogólne",
"idpSelect": "Wybierz dostawcę tożsamości dla użytkownika zewnętrznego", "idpSelect": "Wybierz dostawcę tożsamości dla użytkownika zewnętrznego",
"idpNotConfigured": "Nie skonfigurowano żadnych dostawców tożsamości. Skonfiguruj dostawcę tożsamości przed utworzeniem użytkowników zewnętrznych.", "idpNotConfigured": "Nie skonfigurowano żadnych dostawców tożsamości. Skonfiguruj dostawcę tożsamości przed utworzeniem użytkowników zewnętrznych.",
@ -958,6 +960,8 @@
"licenseTierProfessionalRequiredDescription": "Ta funkcja jest dostępna tylko w edycji Professional.", "licenseTierProfessionalRequiredDescription": "Ta funkcja jest dostępna tylko w edycji Professional.",
"actionGetOrg": "Pobierz organizację", "actionGetOrg": "Pobierz organizację",
"actionUpdateOrg": "Aktualizuj organizację", "actionUpdateOrg": "Aktualizuj organizację",
"actionUpdateUser": "Zaktualizuj użytkownika",
"actionGetUser": "Pobierz użytkownika",
"actionGetOrgUser": "Pobierz użytkownika organizacji", "actionGetOrgUser": "Pobierz użytkownika organizacji",
"actionListOrgDomains": "Lista domen organizacji", "actionListOrgDomains": "Lista domen organizacji",
"actionCreateSite": "Utwórz witrynę", "actionCreateSite": "Utwórz witrynę",
@ -1090,19 +1094,21 @@
"sidebarAllUsers": "Wszyscy użytkownicy", "sidebarAllUsers": "Wszyscy użytkownicy",
"sidebarIdentityProviders": "Dostawcy tożsamości", "sidebarIdentityProviders": "Dostawcy tożsamości",
"sidebarLicense": "Licencja", "sidebarLicense": "Licencja",
"sidebarClients": "Klienci",
"sidebarDomains": "Domeny",
"enableDockerSocket": "Włącz gniazdo dokera", "enableDockerSocket": "Włącz gniazdo dokera",
"enableDockerSocketDescription": "Włącz wykrywanie Docker Socket w celu wypełnienia informacji o kontenerach. Ścieżka gniazda musi być dostarczona do Newt.", "enableDockerSocketDescription": "Włącz wykrywanie Docker Socket w celu wypełnienia informacji o kontenerach. Ścieżka gniazda musi być dostarczona do Newt.",
"enableDockerSocketLink": "Dowiedz się więcej", "enableDockerSocketLink": "Dowiedz się więcej",
"viewDockerContainers": "Zobacz kontenery dokujące", "viewDockerContainers": "Zobacz kontenery dokujące",
"containersIn": "Pojemniki w {siteName}", "containersIn": "Pojemniki w {siteName}",
"selectContainerDescription": "Wybierz dowolny kontener do użycia jako nazwa hosta dla tego celu. Kliknij port, aby użyć portu.", "selectContainerDescription": "Wybierz dowolny kontener do użycia jako nazwa hosta dla tego celu. Kliknij port, aby użyć portu.",
"containerName": "Nazwisko", "containerName": "Nazwa",
"containerImage": "Obraz", "containerImage": "Obraz",
"containerState": "Stan", "containerState": "Stan",
"containerNetworks": "Sieci", "containerNetworks": "Sieci",
"containerHostnameIp": "Nazwa hosta/IP", "containerHostnameIp": "Nazwa hosta/IP",
"containerLabels": "Etykiety", "containerLabels": "Etykiety",
"containerLabelsCount": "{count} etykieta{s,plural,one{} other{s}}", "containerLabelsCount": "{count, plural, one {# etykieta} few {# etykiety} many {# etykiet} other {# etykiet}}",
"containerLabelsTitle": "Etykiety kontenera", "containerLabelsTitle": "Etykiety kontenera",
"containerLabelEmpty": "<empty>", "containerLabelEmpty": "<empty>",
"containerPorts": "Porty", "containerPorts": "Porty",
@ -1114,7 +1120,7 @@
"showStoppedContainers": "Pokaż zatrzymane kontenery", "showStoppedContainers": "Pokaż zatrzymane kontenery",
"noContainersFound": "Nie znaleziono kontenerów. Upewnij się, że kontenery dokujące są uruchomione.", "noContainersFound": "Nie znaleziono kontenerów. Upewnij się, że kontenery dokujące są uruchomione.",
"searchContainersPlaceholder": "Szukaj w {count} kontenerach...", "searchContainersPlaceholder": "Szukaj w {count} kontenerach...",
"searchResultsCount": "{count} wynik{s,plural,one{} other{s}}", "searchResultsCount": "{count, plural, one {# wynik} few {# wyniki} many {# wyników} other {# wyników}}",
"filters": "Filtry", "filters": "Filtry",
"filterOptions": "Opcje filtru", "filterOptions": "Opcje filtru",
"filterPorts": "Porty", "filterPorts": "Porty",
@ -1129,10 +1135,89 @@
"dark": "ciemny", "dark": "ciemny",
"system": "System", "system": "System",
"theme": "Motyw", "theme": "Motyw",
"subnetRequired": "Podsieć jest wymagana",
"initialSetupTitle": "Wstępna konfiguracja serwera", "initialSetupTitle": "Wstępna konfiguracja serwera",
"initialSetupDescription": "Utwórz początkowe konto administratora serwera. Może istnieć tylko jeden administrator serwera. Zawsze można zmienić te dane uwierzytelniające.", "initialSetupDescription": "Utwórz początkowe konto administratora serwera. Może istnieć tylko jeden administrator serwera. Zawsze można zmienić te dane uwierzytelniające.",
"createAdminAccount": "Utwórz konto administratora", "createAdminAccount": "Utwórz konto administratora",
"setupErrorCreateAdmin": "Wystąpił błąd podczas tworzenia konta administratora serwera.", "setupErrorCreateAdmin": "Wystąpił błąd podczas tworzenia konta administratora serwera.",
"certificateStatus": "Status certyfikatu",
"loading": "Ładowanie",
"restart": "Uruchom ponownie",
"domains": "Domeny",
"domainsDescription": "Zarządzaj domenami swojej organizacji",
"domainsSearch": "Szukaj domen...",
"domainAdd": "Dodaj domenę",
"domainAddDescription": "Zarejestruj nową domenę w swojej organizacji",
"domainCreate": "Utwórz domenę",
"domainCreatedDescription": "Domena utworzona pomyślnie",
"domainDeletedDescription": "Domena usunięta pomyślnie",
"domainQuestionRemove": "Czy na pewno chcesz usunąć domenę {domain} ze swojego konta?",
"domainMessageRemove": "Po usunięciu domena nie będzie już powiązana z twoim kontem.",
"domainMessageConfirm": "Aby potwierdzić, wpisz nazwę domeny poniżej.",
"domainConfirmDelete": "Potwierdź usunięcie domeny",
"domainDelete": "Usuń domenę",
"domain": "Domena",
"selectDomainTypeNsName": "Delegacja domeny (NS)",
"selectDomainTypeNsDescription": "Ta domena i wszystkie jej subdomeny. Użyj tego, gdy chcesz kontrolować całą strefę domeny.",
"selectDomainTypeCnameName": "Pojedyncza domena (CNAME)",
"selectDomainTypeCnameDescription": "Tylko ta pojedyncza domena. Użyj tego dla poszczególnych subdomen lub wpisów specyficznych dla domeny.",
"selectDomainTypeWildcardName": "Domena wieloznaczna",
"selectDomainTypeWildcardDescription": "Ta domena i jej pierwsza warstwa subdomen.",
"domainDelegation": "Pojedyncza domena",
"selectType": "Wybierz typ",
"actions": "Akcje",
"refresh": "Odśwież",
"refreshError": "Nie udało się odświeżyć danych",
"verified": "Zatwierdzony",
"pending": "Oczekuje",
"sidebarBilling": "Fakturowanie",
"billing": "Fakturowanie",
"orgBillingDescription": "Zarządzaj swoimi informacjami rozliczeniowymi i subskrypcjami",
"github": "GitHub",
"pangolinHosted": "Logo Pangolin",
"fossorial": "Fossorial",
"completeAccountSetup": "Zakończ konfigurację konta",
"completeAccountSetupDescription": "Ustaw swoje hasło, aby rozpocząć",
"accountSetupSent": "Wyślemy kod konfiguracji konta na ten adres e-mail.",
"accountSetupCode": "Kod konfiguracji",
"accountSetupCodeDescription": "Sprawdź swój e-mail, aby znaleźć kod konfiguracji.",
"passwordCreate": "Utwórz hasło",
"passwordCreateConfirm": "Potwierdź hasło",
"accountSetupSubmit": "Wyślij kod konfiguracji",
"completeSetup": "Zakończ konfigurację",
"accountSetupSuccess": "Konfiguracja konta zakończona! Witaj w Pangolin!",
"documentation": "Dokumentacja",
"saveAllSettings": "Zapisz wszystkie ustawienia",
"settingsUpdated": "Ustawienia zaktualizowane",
"settingsUpdatedDescription": "Wszystkie ustawienia zostały pomyślnie zaktualizowane",
"settingsErrorUpdate": "Nie udało się zaktualizować ustawień",
"settingsErrorUpdateDescription": "Wystąpił błąd podczas aktualizacji ustawień",
"sidebarCollapse": "Zwiń",
"sidebarExpand": "Rozwiń",
"newtUpdateAvailable": "Dostępna aktualizacja",
"newtUpdateAvailableInfo": "Nowa wersja Newt jest dostępna. Prosimy o aktualizację do najnowszej wersji dla najlepszej pracy.",
"domainPickerEnterDomain": "Wprowadź swoją domenę",
"domainPickerPlaceholder": "myapp.example.com, api.v1.mydomain.com lub po prostu myapp",
"domainPickerDescription": "Wpisz pełną domenę zasobu, aby zobaczyć dostępne opcje.",
"domainPickerDescriptionSaas": "Wprowadź pełną domenę, subdomenę lub po prostu nazwę, aby zobaczyć dostępne opcje",
"domainPickerTabAll": "Wszystko",
"domainPickerTabOrganization": "Organizacja",
"domainPickerTabProvided": "Dostarczona",
"domainPickerSortAsc": "A-Z",
"domainPickerSortDesc": "Z-A",
"domainPickerCheckingAvailability": "Sprawdzanie dostępności...",
"domainPickerNoMatchingDomains": "Nie znaleziono żadnych pasujących domen dla \"{userInput}\". Spróbuj innej domeny lub sprawdź ustawienia domeny swojej organizacji.",
"domainPickerOrganizationDomains": "Domeny organizacji",
"domainPickerProvidedDomains": "Dostarczone domeny",
"domainPickerSubdomain": "Subdomena: {subdomain}",
"domainPickerNamespace": "Przestrzeń nazw: {namespace}",
"domainPickerShowMore": "Pokaż więcej",
"domainNotFound": "Nie znaleziono domeny",
"domainNotFoundDescription": "Zasób jest wyłączony, ponieważ domena nie istnieje już w naszym systemie. Proszę ustawić nową domenę dla tego zasobu.",
"failed": "Niepowodzenie",
"createNewOrgDescription": "Utwórz nową organizację",
"organization": "Organizacja",
"port": "Port",
"securityKeyManage": "Zarządzaj kluczami bezpieczeństwa", "securityKeyManage": "Zarządzaj kluczami bezpieczeństwa",
"securityKeyDescription": "Dodaj lub usuń klucze bezpieczeństwa do uwierzytelniania bez hasła", "securityKeyDescription": "Dodaj lub usuń klucze bezpieczeństwa do uwierzytelniania bez hasła",
"securityKeyRegister": "Zarejestruj nowy klucz bezpieczeństwa", "securityKeyRegister": "Zarejestruj nowy klucz bezpieczeństwa",
@ -1142,7 +1227,6 @@
"securityKeyRemove": "Usuń", "securityKeyRemove": "Usuń",
"securityKeyLastUsed": "Ostatnio używany: {date}", "securityKeyLastUsed": "Ostatnio używany: {date}",
"securityKeyNameLabel": "Nazwa", "securityKeyNameLabel": "Nazwa",
"securityKeyNamePlaceholder": "Wprowadź nazwę dla tego klucza bezpieczeństwa",
"securityKeyRegisterSuccess": "Klucz bezpieczeństwa został pomyślnie zarejestrowany", "securityKeyRegisterSuccess": "Klucz bezpieczeństwa został pomyślnie zarejestrowany",
"securityKeyRegisterError": "Błąd podczas rejestracji klucza bezpieczeństwa", "securityKeyRegisterError": "Błąd podczas rejestracji klucza bezpieczeństwa",
"securityKeyRemoveSuccess": "Klucz bezpieczeństwa został pomyślnie usunięty", "securityKeyRemoveSuccess": "Klucz bezpieczeństwa został pomyślnie usunięty",
@ -1150,5 +1234,44 @@
"securityKeyLoadError": "Błąd podczas ładowania kluczy bezpieczeństwa", "securityKeyLoadError": "Błąd podczas ładowania kluczy bezpieczeństwa",
"securityKeyLogin": "Zaloguj się kluczem bezpieczeństwa", "securityKeyLogin": "Zaloguj się kluczem bezpieczeństwa",
"securityKeyAuthError": "Błąd podczas uwierzytelniania kluczem bezpieczeństwa", "securityKeyAuthError": "Błąd podczas uwierzytelniania kluczem bezpieczeństwa",
"securityKeyRecommendation": "Rozważ zarejestrowanie innego klucza bezpieczeństwa na innym urządzeniu, aby upewnić się, że nie zostaniesz zablokowany z dostępu do swojego konta." "securityKeyRecommendation": "Rozważ zarejestrowanie innego klucza bezpieczeństwa na innym urządzeniu, aby upewnić się, że nie zostaniesz zablokowany z dostępu do swojego konta.",
"registering": "Rejestracja...",
"securityKeyPrompt": "Proszę zweryfikować swoją tożsamość, używając klucza bezpieczeństwa. Upewnij się, że twój klucz bezpieczeństwa jest podłączony i gotowy.",
"securityKeyBrowserNotSupported": "Twoja przeglądarka nie obsługuje kluczy bezpieczeństwa. Proszę użyć nowoczesnej przeglądarki, takiej jak Chrome, Firefox lub Safari.",
"securityKeyPermissionDenied": "Proszę umożliwić dostęp do klucza bezpieczeństwa, aby kontynuować logowanie.",
"securityKeyRemovedTooQuickly": "Proszę utrzymać klucz bezpieczeństwa podłączony, dopóki proces logowania się nie zakończy.",
"securityKeyNotSupported": "Twój klucz bezpieczeństwa może być niekompatybilny. Proszę spróbować innego klucza bezpieczeństwa.",
"securityKeyUnknownError": "Wystąpił problem z używaniem klucza bezpieczeństwa. Proszę spróbować ponownie.",
"twoFactorRequired": "Uwierzytelnianie dwuskładnikowe jest wymagane do zarejestrowania klucza bezpieczeństwa.",
"twoFactor": "Uwierzytelnianie dwuskładnikowe",
"adminEnabled2FaOnYourAccount": "Twój administrator włączył uwierzytelnianie dwuskładnikowe dla {email}. Proszę ukończyć proces konfiguracji, aby kontynuować.",
"continueToApplication": "Kontynuuj do aplikacji",
"securityKeyAdd": "Dodaj klucz bezpieczeństwa",
"securityKeyRegisterTitle": "Zarejestruj nowy klucz bezpieczeństwa",
"securityKeyRegisterDescription": "Podłącz swój klucz bezpieczeństwa i wprowadź nazwę, aby go zidentyfikować",
"securityKeyTwoFactorRequired": "Wymagane uwierzytelnianie dwuskładnikowe",
"securityKeyTwoFactorDescription": "Proszę wprowadzić kod uwierzytelnienia dwuskładnikowego, aby zarejestrować klucz bezpieczeństwa",
"securityKeyTwoFactorRemoveDescription": "Proszę wprowadzić kod uwierzytelnienia dwuskładnikowego, aby usunąć klucz bezpieczeństwa",
"securityKeyTwoFactorCode": "Kod dwuskładnikowy",
"securityKeyRemoveTitle": "Usuń klucz bezpieczeństwa",
"securityKeyRemoveDescription": "Wprowadź hasło, aby usunąć klucz bezpieczeństwa \"{name}\"",
"securityKeyNoKeysRegistered": "Nie zarejestrowano kluczy bezpieczeństwa",
"securityKeyNoKeysDescription": "Dodaj klucz bezpieczeństwa, aby zwiększyć swoje zabezpieczenia konta",
"createDomainRequired": "Domena jest wymagana",
"createDomainAddDnsRecords": "Dodaj rekordy DNS",
"createDomainAddDnsRecordsDescription": "Dodaj poniższe rekordy DNS do swojego dostawcy domeny, aby zakończyć konfigurację.",
"createDomainNsRecords": "Rekordy NS",
"createDomainRecord": "Rekord",
"createDomainType": "Typ:",
"createDomainName": "Nazwa:",
"createDomainValue": "Wartość:",
"createDomainCnameRecords": "Rekordy CNAME",
"createDomainRecordNumber": "Rekord {number}",
"createDomainTxtRecords": "Rekordy TXT",
"createDomainSaveTheseRecords": "Zapisz te rekordy",
"createDomainSaveTheseRecordsDescription": "Upewnij się, że zapiszesz te rekordy DNS, ponieważ nie będziesz mieć ich ponownie na ekranie.",
"createDomainDnsPropagation": "Propagacja DNS",
"createDomainDnsPropagationDescription": "Zmiany DNS mogą zająć trochę czasu na rozpropagowanie się w Internecie. Może to potrwać od kilku minut do 48 godzin, w zależności od dostawcy DNS i ustawień TTL.",
"resourcePortRequired": "Numer portu jest wymagany dla zasobów non-HTTP",
"resourcePortNotAllowed": "Numer portu nie powinien być ustawiony dla zasobów HTTP"
} }

View file

@ -11,8 +11,9 @@
"componentsErrorNoMemberCreate": "Você não é atualmente um membro de nenhuma organização. Crie uma organização para começar.", "componentsErrorNoMemberCreate": "Você não é atualmente um membro de nenhuma organização. Crie uma organização para começar.",
"componentsErrorNoMember": "Você não é atualmente um membro de nenhuma organização.", "componentsErrorNoMember": "Você não é atualmente um membro de nenhuma organização.",
"welcome": "Bem-vindo ao Pangolin", "welcome": "Bem-vindo ao Pangolin",
"welcomeTo": "Bem-vindo ao",
"componentsCreateOrg": "Criar uma organização", "componentsCreateOrg": "Criar uma organização",
"componentsMember": "Você é membro de {count, plural, =0 {Nenhuma organização} =1 {Uma organização} other {# organizações}}", "componentsMember": "Você é membro de {count, plural, =0 {nenhuma organização} one {uma organização} other {# organizações}}.",
"componentsInvalidKey": "Chaves de licença inválidas ou expiradas detectadas. Siga os termos da licença para continuar usando todos os recursos.", "componentsInvalidKey": "Chaves de licença inválidas ou expiradas detectadas. Siga os termos da licença para continuar usando todos os recursos.",
"dismiss": "Descartar", "dismiss": "Descartar",
"componentsLicenseViolation": "Violação de Licença: Este servidor está usando sites {usedSites} que excedem o limite licenciado de sites {maxSites} . Siga os termos da licença para continuar usando todos os recursos.", "componentsLicenseViolation": "Violação de Licença: Este servidor está usando sites {usedSites} que excedem o limite licenciado de sites {maxSites} . Siga os termos da licença para continuar usando todos os recursos.",
@ -206,6 +207,7 @@
"orgGeneralSettings": "Configurações da organização", "orgGeneralSettings": "Configurações da organização",
"orgGeneralSettingsDescription": "Gerencie os detalhes e a configuração da sua organização", "orgGeneralSettingsDescription": "Gerencie os detalhes e a configuração da sua organização",
"saveGeneralSettings": "Salvar configurações gerais", "saveGeneralSettings": "Salvar configurações gerais",
"saveSettings": "Salvar Configurações",
"orgDangerZone": "Zona de Perigo", "orgDangerZone": "Zona de Perigo",
"orgDangerZoneDescription": "Uma vez que você exclui esta organização, não há volta. Por favor, tenha certeza.", "orgDangerZoneDescription": "Uma vez que você exclui esta organização, não há volta. Por favor, tenha certeza.",
"orgDelete": "Excluir Organização", "orgDelete": "Excluir Organização",
@ -249,7 +251,7 @@
"weeks": "semanas", "weeks": "semanas",
"months": "Meses", "months": "Meses",
"years": "anos", "years": "anos",
"day": "{count, plural, =1 {# dia} other {# dias}}", "day": "{count, plural, one {# dia} other {# dias}}",
"apiKeysTitle": "Informações da Chave API", "apiKeysTitle": "Informações da Chave API",
"apiKeysConfirmCopy2": "Você deve confirmar que copiou a chave API.", "apiKeysConfirmCopy2": "Você deve confirmar que copiou a chave API.",
"apiKeysErrorCreate": "Erro ao criar chave API", "apiKeysErrorCreate": "Erro ao criar chave API",
@ -347,7 +349,7 @@
"licensePurchase": "Comprar Licença", "licensePurchase": "Comprar Licença",
"licensePurchaseSites": "Comprar Sites Adicionais", "licensePurchaseSites": "Comprar Sites Adicionais",
"licenseSitesUsedMax": "{usedSites} de {maxSites} utilizados", "licenseSitesUsedMax": "{usedSites} de {maxSites} utilizados",
"licenseSitesUsed": "{count, plural, =0 {# sites} =1 {# site} other {# sites}} no sistema.", "licenseSitesUsed": "{count, plural, =0 {# sites} one {# site} other {# sites}} no sistema.",
"licensePurchaseDescription": "Escolha quantos sites você quer {selectedMode, select, license {Compre uma licença. Você sempre pode adicionar mais sites depois.} other {adicione à sua licença existente.}}", "licensePurchaseDescription": "Escolha quantos sites você quer {selectedMode, select, license {Compre uma licença. Você sempre pode adicionar mais sites depois.} other {adicione à sua licença existente.}}",
"licenseFee": "Taxa de licença", "licenseFee": "Taxa de licença",
"licensePriceSite": "Preço por site", "licensePriceSite": "Preço por site",
@ -436,7 +438,7 @@
"accessRoleSelect": "Selecionar função", "accessRoleSelect": "Selecionar função",
"inviteEmailSentDescription": "Um e-mail foi enviado ao usuário com o link de acesso abaixo. Eles devem acessar o link para aceitar o convite.", "inviteEmailSentDescription": "Um e-mail foi enviado ao usuário com o link de acesso abaixo. Eles devem acessar o link para aceitar o convite.",
"inviteSentDescription": "O usuário foi convidado. Eles devem acessar o link abaixo para aceitar o convite.", "inviteSentDescription": "O usuário foi convidado. Eles devem acessar o link abaixo para aceitar o convite.",
"inviteExpiresIn": "O convite expirará em {days, plural, =1 {# dia} other {# dias}}.", "inviteExpiresIn": "O convite expirará em {days, plural, one {# dia} other {# dias}}.",
"idpTitle": "Informações Gerais", "idpTitle": "Informações Gerais",
"idpSelect": "Selecione o provedor de identidade para o usuário externo", "idpSelect": "Selecione o provedor de identidade para o usuário externo",
"idpNotConfigured": "Nenhum provedor de identidade está configurado. Configure um provedor de identidade antes de criar usuários externos.", "idpNotConfigured": "Nenhum provedor de identidade está configurado. Configure um provedor de identidade antes de criar usuários externos.",
@ -958,6 +960,8 @@
"licenseTierProfessionalRequiredDescription": "Esta funcionalidade só está disponível na Edição Profissional.", "licenseTierProfessionalRequiredDescription": "Esta funcionalidade só está disponível na Edição Profissional.",
"actionGetOrg": "Obter Organização", "actionGetOrg": "Obter Organização",
"actionUpdateOrg": "Atualizar Organização", "actionUpdateOrg": "Atualizar Organização",
"actionUpdateUser": "Atualizar Usuário",
"actionGetUser": "Obter Usuário",
"actionGetOrgUser": "Obter Utilizador da Organização", "actionGetOrgUser": "Obter Utilizador da Organização",
"actionListOrgDomains": "Listar Domínios da Organização", "actionListOrgDomains": "Listar Domínios da Organização",
"actionCreateSite": "Criar Site", "actionCreateSite": "Criar Site",
@ -1090,6 +1094,8 @@
"sidebarAllUsers": "Todos os usuários", "sidebarAllUsers": "Todos os usuários",
"sidebarIdentityProviders": "Provedores de identidade", "sidebarIdentityProviders": "Provedores de identidade",
"sidebarLicense": "Tipo:", "sidebarLicense": "Tipo:",
"sidebarClients": "Clientes",
"sidebarDomains": "Domínios",
"enableDockerSocket": "Habilitar Docker Socket", "enableDockerSocket": "Habilitar Docker Socket",
"enableDockerSocketDescription": "Ativar a descoberta do Docker Socket para preencher informações do contêiner. O caminho do socket deve ser fornecido ao Newt.", "enableDockerSocketDescription": "Ativar a descoberta do Docker Socket para preencher informações do contêiner. O caminho do socket deve ser fornecido ao Newt.",
"enableDockerSocketLink": "Saiba mais", "enableDockerSocketLink": "Saiba mais",
@ -1102,7 +1108,7 @@
"containerNetworks": "Redes", "containerNetworks": "Redes",
"containerHostnameIp": "Hostname/IP", "containerHostnameIp": "Hostname/IP",
"containerLabels": "Marcadores", "containerLabels": "Marcadores",
"containerLabelsCount": "{count} rótulo{s,plural,one{} other{s}}", "containerLabelsCount": "{count, plural, one {# rótulo} other {# rótulos}}",
"containerLabelsTitle": "Etiquetas do Contêiner", "containerLabelsTitle": "Etiquetas do Contêiner",
"containerLabelEmpty": "<vazio>", "containerLabelEmpty": "<vazio>",
"containerPorts": "Portas", "containerPorts": "Portas",
@ -1114,7 +1120,7 @@
"showStoppedContainers": "Mostrar contêineres parados", "showStoppedContainers": "Mostrar contêineres parados",
"noContainersFound": "Nenhum contêiner encontrado. Certifique-se de que os contêineres Docker estão em execução.", "noContainersFound": "Nenhum contêiner encontrado. Certifique-se de que os contêineres Docker estão em execução.",
"searchContainersPlaceholder": "Pesquisar entre os contêineres {count}...", "searchContainersPlaceholder": "Pesquisar entre os contêineres {count}...",
"searchResultsCount": "{count} resultado{s,plural,one{} other{s}}", "searchResultsCount": "{count, plural, one {# resultado} other {# resultados}}",
"filters": "Filtros", "filters": "Filtros",
"filterOptions": "Opções de Filtro", "filterOptions": "Opções de Filtro",
"filterPorts": "Portas", "filterPorts": "Portas",
@ -1129,10 +1135,89 @@
"dark": "escuro", "dark": "escuro",
"system": "sistema", "system": "sistema",
"theme": "Tema", "theme": "Tema",
"subnetRequired": "Sub-rede é obrigatória",
"initialSetupTitle": "Configuração Inicial do Servidor", "initialSetupTitle": "Configuração Inicial do Servidor",
"initialSetupDescription": "Crie a conta de administrador inicial do servidor. Apenas um administrador do servidor pode existir. Você sempre pode alterar essas credenciais posteriormente.", "initialSetupDescription": "Crie a conta de administrador inicial do servidor. Apenas um administrador do servidor pode existir. Você sempre pode alterar essas credenciais posteriormente.",
"createAdminAccount": "Criar Conta de Administrador", "createAdminAccount": "Criar Conta de Administrador",
"setupErrorCreateAdmin": "Ocorreu um erro ao criar a conta de administrador do servidor.", "setupErrorCreateAdmin": "Ocorreu um erro ao criar a conta de administrador do servidor.",
"certificateStatus": "Status do Certificado",
"loading": "Carregando",
"restart": "Reiniciar",
"domains": "Domínios",
"domainsDescription": "Gerencie domínios para sua organização",
"domainsSearch": "Pesquisar domínios...",
"domainAdd": "Adicionar Domínio",
"domainAddDescription": "Registre um novo domínio com sua organização",
"domainCreate": "Criar Domínio",
"domainCreatedDescription": "Domínio criado com sucesso",
"domainDeletedDescription": "Domínio deletado com sucesso",
"domainQuestionRemove": "Tem certeza de que deseja remover o domínio {domain} da sua conta?",
"domainMessageRemove": "Uma vez removido, o domínio não estará mais associado à sua conta.",
"domainMessageConfirm": "Para confirmar, digite o nome do domínio abaixo.",
"domainConfirmDelete": "Confirmar Exclusão de Domínio",
"domainDelete": "Excluir Domínio",
"domain": "Domínio",
"selectDomainTypeNsName": "Delegação de Domínio (NS)",
"selectDomainTypeNsDescription": "Este domínio e todos os seus subdomínios. Use isso quando quiser controlar uma zona de domínio inteira.",
"selectDomainTypeCnameName": "Domínio Único (CNAME)",
"selectDomainTypeCnameDescription": "Apenas este domínio específico. Use isso para subdomínios individuais ou entradas de domínio específicas.",
"selectDomainTypeWildcardName": "Domínio Coringa",
"selectDomainTypeWildcardDescription": "Este domínio e seu primeiro nível de subdomínios.",
"domainDelegation": "Domínio Único",
"selectType": "Selecione um tipo",
"actions": "Ações",
"refresh": "Atualizar",
"refreshError": "Falha ao atualizar dados",
"verified": "Verificado",
"pending": "Pendente",
"sidebarBilling": "Faturamento",
"billing": "Faturamento",
"orgBillingDescription": "Gerencie suas informações de faturamento e assinaturas",
"github": "GitHub",
"pangolinHosted": "Hospedagem Pangolin",
"fossorial": "Fossorial",
"completeAccountSetup": "Completar Configuração da Conta",
"completeAccountSetupDescription": "Defina sua senha para começar",
"accountSetupSent": "Enviaremos um código de ativação da conta para este endereço de e-mail.",
"accountSetupCode": "Código de Ativação",
"accountSetupCodeDescription": "Verifique seu e-mail para obter o código de ativação.",
"passwordCreate": "Criar Senha",
"passwordCreateConfirm": "Confirmar Senha",
"accountSetupSubmit": "Enviar Código de Ativação",
"completeSetup": "Configuração Completa",
"accountSetupSuccess": "Configuração da conta concluída! Bem-vindo ao Pangolin!",
"documentation": "Documentação",
"saveAllSettings": "Salvar Todas as Configurações",
"settingsUpdated": "Configurações atualizadas",
"settingsUpdatedDescription": "Todas as configurações foram atualizadas com sucesso",
"settingsErrorUpdate": "Falha ao atualizar configurações",
"settingsErrorUpdateDescription": "Ocorreu um erro ao atualizar configurações",
"sidebarCollapse": "Recolher",
"sidebarExpand": "Expandir",
"newtUpdateAvailable": "Nova Atualização Disponível",
"newtUpdateAvailableInfo": "Uma nova versão do Newt está disponível. Atualize para a versão mais recente para uma melhor experiência.",
"domainPickerEnterDomain": "Insira seu domínio",
"domainPickerPlaceholder": "meuapp.exemplo.com, api.v1.meudominio.com, ou apenas meuapp",
"domainPickerDescription": "Insira o domínio completo do recurso para ver as opções disponíveis.",
"domainPickerDescriptionSaas": "Insira um domínio completo, subdomínio ou apenas um nome para ver as opções disponíveis",
"domainPickerTabAll": "Todos",
"domainPickerTabOrganization": "Organização",
"domainPickerTabProvided": "Fornecido",
"domainPickerSortAsc": "A-Z",
"domainPickerSortDesc": "Z-A",
"domainPickerCheckingAvailability": "Verificando disponibilidade...",
"domainPickerNoMatchingDomains": "Nenhum domínio correspondente encontrado para \"{userInput}\". Tente um domínio diferente ou verifique as configurações de domínio da sua organização.",
"domainPickerOrganizationDomains": "Domínios da Organização",
"domainPickerProvidedDomains": "Domínios Fornecidos",
"domainPickerSubdomain": "Subdomínio: {subdomain}",
"domainPickerNamespace": "Namespace: {namespace}",
"domainPickerShowMore": "Mostrar Mais",
"domainNotFound": "Domínio Não Encontrado",
"domainNotFoundDescription": "Este recurso está desativado porque o domínio não existe mais em nosso sistema. Defina um novo domínio para este recurso.",
"failed": "Falhou",
"createNewOrgDescription": "Crie uma nova organização",
"organization": "Organização",
"port": "Porta",
"securityKeyManage": "Gerenciar chaves de segurança", "securityKeyManage": "Gerenciar chaves de segurança",
"securityKeyDescription": "Adicionar ou remover chaves de segurança para autenticação sem senha", "securityKeyDescription": "Adicionar ou remover chaves de segurança para autenticação sem senha",
"securityKeyRegister": "Registrar nova chave de segurança", "securityKeyRegister": "Registrar nova chave de segurança",
@ -1142,13 +1227,51 @@
"securityKeyRemove": "Remover", "securityKeyRemove": "Remover",
"securityKeyLastUsed": "Último uso: {date}", "securityKeyLastUsed": "Último uso: {date}",
"securityKeyNameLabel": "Nome", "securityKeyNameLabel": "Nome",
"securityKeyNamePlaceholder": "Digite um nome para esta chave de segurança",
"securityKeyRegisterSuccess": "Chave de segurança registrada com sucesso", "securityKeyRegisterSuccess": "Chave de segurança registrada com sucesso",
"securityKeyRegisterError": "Erro ao registrar chave de segurança", "securityKeyRegisterError": "Erro ao registrar chave de segurança",
"securityKeyRemoveSuccess": "Chave de segurança removida com sucesso", "securityKeyRemoveSuccess": "Chave de segurança removida com sucesso",
"securityKeyRemoveError": "Erro ao remover chave de segurança", "securityKeyRemoveError": "Erro ao remover chave de segurança",
"securityKeyLoadError": "Erro ao carregar chaves de segurança", "securityKeyLoadError": "Erro ao carregar chaves de segurança",
"securityKeyLogin": "Entrar com chave de segurança", "securityKeyLogin": "Continuar com a chave de segurança",
"securityKeyAuthError": "Erro ao autenticar com chave de segurança", "securityKeyAuthError": "Erro ao autenticar com chave de segurança",
"securityKeyRecommendation": "Considere registrar outra chave de segurança em um dispositivo diferente para garantir que você não fique bloqueado da sua conta." "securityKeyRecommendation": "Considere registrar outra chave de segurança em um dispositivo diferente para garantir que você não fique bloqueado da sua conta.",
"registering": "Registrando...",
"securityKeyPrompt": "Verifique sua identidade usando sua chave de segurança. Certifique-se de que sua chave de segurança está conectada e pronta.",
"securityKeyBrowserNotSupported": "Seu navegador não suporta chaves de segurança. Use um navegador moderno como Chrome, Firefox ou Safari.",
"securityKeyPermissionDenied": "Permita o acesso à sua chave de segurança para continuar o login.",
"securityKeyRemovedTooQuickly": "Mantenha sua chave de segurança conectada até que o processo de login seja concluído.",
"securityKeyNotSupported": "Sua chave de segurança pode não ser compatível. Tente uma chave de segurança diferente.",
"securityKeyUnknownError": "Houve um problema ao usar sua chave de segurança. Tente novamente.",
"twoFactorRequired": "A autenticação de dois fatores é necessária para registrar uma chave de segurança.",
"twoFactor": "Autenticação de Dois Fatores",
"adminEnabled2FaOnYourAccount": "Seu administrador ativou a autenticação de dois fatores para {email}. Complete o processo de configuração para continuar.",
"continueToApplication": "Continuar para Aplicativo",
"securityKeyAdd": "Adicionar Chave de Segurança",
"securityKeyRegisterTitle": "Registrar Nova Chave de Segurança",
"securityKeyRegisterDescription": "Conecte sua chave de segurança e insira um nome para identificá-la",
"securityKeyTwoFactorRequired": "Autenticação de Dois Fatores Obrigatória",
"securityKeyTwoFactorDescription": "Insira seu código de autenticação de dois fatores para registrar a chave de segurança",
"securityKeyTwoFactorRemoveDescription": "Insira seu código de autenticação de dois fatores para remover a chave de segurança",
"securityKeyTwoFactorCode": "Código de Dois Fatores",
"securityKeyRemoveTitle": "Remover Chave de Segurança",
"securityKeyRemoveDescription": "Insira sua senha para remover a chave de segurança \"{name}\"",
"securityKeyNoKeysRegistered": "Nenhuma chave de segurança registrada",
"securityKeyNoKeysDescription": "Adicione uma chave de segurança para melhorar a segurança da sua conta",
"createDomainRequired": "Domínio é obrigatório",
"createDomainAddDnsRecords": "Adicionar Registros DNS",
"createDomainAddDnsRecordsDescription": "Adicione os seguintes registros DNS ao seu provedor de domínio para completar a configuração.",
"createDomainNsRecords": "Registros NS",
"createDomainRecord": "Registrar",
"createDomainType": "Tipo:",
"createDomainName": "Nome:",
"createDomainValue": "Valor:",
"createDomainCnameRecords": "Registros CNAME",
"createDomainRecordNumber": "Registrar {number}",
"createDomainTxtRecords": "Registros TXT",
"createDomainSaveTheseRecords": "Salvar Esses Registros",
"createDomainSaveTheseRecordsDescription": "Certifique-se de salvar esses registros DNS, pois você não os verá novamente.",
"createDomainDnsPropagation": "Propagação DNS",
"createDomainDnsPropagationDescription": "Alterações no DNS podem levar algum tempo para se propagar pela internet. Pode levar de alguns minutos a 48 horas, dependendo do seu provedor de DNS e das configurações de TTL.",
"resourcePortRequired": "Número da porta é obrigatório para recursos não-HTTP",
"resourcePortNotAllowed": "Número da porta não deve ser definido para recursos HTTP"
} }

1277
messages/ru-RU.json Normal file

File diff suppressed because it is too large Load diff

View file

@ -11,8 +11,9 @@
"componentsErrorNoMemberCreate": "Şu anda herhangi bir organizasyona üye değilsiniz. Başlamak için bir organizasyon oluşturun.", "componentsErrorNoMemberCreate": "Şu anda herhangi bir organizasyona üye değilsiniz. Başlamak için bir organizasyon oluşturun.",
"componentsErrorNoMember": "Şu anda herhangi bir organizasyona üye değilsiniz.", "componentsErrorNoMember": "Şu anda herhangi bir organizasyona üye değilsiniz.",
"welcome": "Pangolin'e hoş geldiniz", "welcome": "Pangolin'e hoş geldiniz",
"welcomeTo": "Hoş geldiniz",
"componentsCreateOrg": "Bir Organizasyon Oluşturun", "componentsCreateOrg": "Bir Organizasyon Oluşturun",
"componentsMember": "You're a member of {count, plural, =0 {no organization} =1 {one organization} other {# organizations}}.", "componentsMember": "{count, plural, =0 {hiçbir organizasyon} one {bir organizasyon} other {# organizasyon}} üyesisiniz.",
"componentsInvalidKey": "Geçersiz veya süresi dolmuş lisans anahtarları tespit edildi. Tüm özellikleri kullanmaya devam etmek için lisans koşullarına uyun.", "componentsInvalidKey": "Geçersiz veya süresi dolmuş lisans anahtarları tespit edildi. Tüm özellikleri kullanmaya devam etmek için lisans koşullarına uyun.",
"dismiss": "Kapat", "dismiss": "Kapat",
"componentsLicenseViolation": "Lisans İhlali: Bu sunucu, lisanslı sınırı olan {maxSites} sitesini aşarak {usedSites} site kullanmaktadır. Tüm özellikleri kullanmaya devam etmek için lisans koşullarına uyun.", "componentsLicenseViolation": "Lisans İhlali: Bu sunucu, lisanslı sınırı olan {maxSites} sitesini aşarak {usedSites} site kullanmaktadır. Tüm özellikleri kullanmaya devam etmek için lisans koşullarına uyun.",
@ -206,6 +207,7 @@
"orgGeneralSettings": "Organizasyon Ayarları", "orgGeneralSettings": "Organizasyon Ayarları",
"orgGeneralSettingsDescription": "Organizasyon detaylarınızı ve yapılandırmanızı yönetin", "orgGeneralSettingsDescription": "Organizasyon detaylarınızı ve yapılandırmanızı yönetin",
"saveGeneralSettings": "Genel Ayarları Kaydet", "saveGeneralSettings": "Genel Ayarları Kaydet",
"saveSettings": "Ayarları Kaydet",
"orgDangerZone": "Tehlike Alanı", "orgDangerZone": "Tehlike Alanı",
"orgDangerZoneDescription": "Bu organizasyonu sildikten sonra geri dönüş yoktur. Emin olun.", "orgDangerZoneDescription": "Bu organizasyonu sildikten sonra geri dönüş yoktur. Emin olun.",
"orgDelete": "Organizasyonu Sil", "orgDelete": "Organizasyonu Sil",
@ -249,7 +251,7 @@
"weeks": "Hafta", "weeks": "Hafta",
"months": "Ay", "months": "Ay",
"years": "Yıl", "years": "Yıl",
"day": "{count, plural, =1 {# day} other {# days}}", "day": "{count, plural, one {# gün} other {# gün}}",
"apiKeysTitle": "API Anahtar Bilgilendirmesi", "apiKeysTitle": "API Anahtar Bilgilendirmesi",
"apiKeysConfirmCopy2": "API anahtarını kopyaladığınızı onaylamanız gerekmektedir.", "apiKeysConfirmCopy2": "API anahtarını kopyaladığınızı onaylamanız gerekmektedir.",
"apiKeysErrorCreate": "API anahtarı oluşturulurken hata", "apiKeysErrorCreate": "API anahtarı oluşturulurken hata",
@ -347,7 +349,7 @@
"licensePurchase": "Lisans Satın Al", "licensePurchase": "Lisans Satın Al",
"licensePurchaseSites": "Ek Siteler Satın Al", "licensePurchaseSites": "Ek Siteler Satın Al",
"licenseSitesUsedMax": "{usedSites} / {maxSites} siteleri kullanıldı", "licenseSitesUsedMax": "{usedSites} / {maxSites} siteleri kullanıldı",
"licenseSitesUsed": "{count, plural, =0 {# site} =1 {# site} other {# site}} sistemde bulunmaktadır.", "licenseSitesUsed": "{count, plural, =0 {# site} one {# site} other {# site}} sistemde bulunmaktadır.",
"licensePurchaseDescription": "{selectedMode, select, license {Lisans satın almak için kaç site istediğinizi seçin. Daha sonra daha fazla site ekleyebilirsiniz.} other {mevcut lisansınıza kaç site ekleneceğini seçin.}}", "licensePurchaseDescription": "{selectedMode, select, license {Lisans satın almak için kaç site istediğinizi seçin. Daha sonra daha fazla site ekleyebilirsiniz.} other {mevcut lisansınıza kaç site ekleneceğini seçin.}}",
"licenseFee": "Lisans ücreti", "licenseFee": "Lisans ücreti",
"licensePriceSite": "Site başına fiyat", "licensePriceSite": "Site başına fiyat",
@ -436,7 +438,7 @@
"accessRoleSelect": "Rol seçin", "accessRoleSelect": "Rol seçin",
"inviteEmailSentDescription": "Kullanıcıya erişim bağlantısı ile bir e-posta gönderildi. Daveti kabul etmek için bağlantıya erişmelidirler.", "inviteEmailSentDescription": "Kullanıcıya erişim bağlantısı ile bir e-posta gönderildi. Daveti kabul etmek için bağlantıya erişmelidirler.",
"inviteSentDescription": "Kullanıcı davet edilmiştir. Daveti kabul etmek için aşağıdaki bağlantıya erişmelidirler.", "inviteSentDescription": "Kullanıcı davet edilmiştir. Daveti kabul etmek için aşağıdaki bağlantıya erişmelidirler.",
"inviteExpiresIn": "The invite will expire in {days, plural, =1 {# day} other {# days}}.", "inviteExpiresIn": "Davetiye {days, plural, one {# gün} other {# gün}} içinde sona erecektir.",
"idpTitle": "General Information", "idpTitle": "General Information",
"idpSelect": "Dış kullanıcı için kimlik sağlayıcıyı seçin", "idpSelect": "Dış kullanıcı için kimlik sağlayıcıyı seçin",
"idpNotConfigured": "Herhangi bir kimlik sağlayıcı yapılandırılmamış. Harici kullanıcılar oluşturulmadan önce lütfen bir kimlik sağlayıcı yapılandırın.", "idpNotConfigured": "Herhangi bir kimlik sağlayıcı yapılandırılmamış. Harici kullanıcılar oluşturulmadan önce lütfen bir kimlik sağlayıcı yapılandırın.",
@ -958,6 +960,8 @@
"licenseTierProfessionalRequiredDescription": "Bu özellik yalnızca Professional Edition'da kullanılabilir.", "licenseTierProfessionalRequiredDescription": "Bu özellik yalnızca Professional Edition'da kullanılabilir.",
"actionGetOrg": "Kuruluşu Al", "actionGetOrg": "Kuruluşu Al",
"actionUpdateOrg": "Kuruluşu Güncelle", "actionUpdateOrg": "Kuruluşu Güncelle",
"actionUpdateUser": "Kullanıcıyı Güncelle",
"actionGetUser": "Kullanıcıyı Getir",
"actionGetOrgUser": "Kuruluş Kullanıcısını Al", "actionGetOrgUser": "Kuruluş Kullanıcısını Al",
"actionListOrgDomains": "Kuruluş Alan Adlarını Listele", "actionListOrgDomains": "Kuruluş Alan Adlarını Listele",
"actionCreateSite": "Site Oluştur", "actionCreateSite": "Site Oluştur",
@ -1090,6 +1094,8 @@
"sidebarAllUsers": "Tüm Kullanıcılar", "sidebarAllUsers": "Tüm Kullanıcılar",
"sidebarIdentityProviders": "Kimlik Sağlayıcılar", "sidebarIdentityProviders": "Kimlik Sağlayıcılar",
"sidebarLicense": "Lisans", "sidebarLicense": "Lisans",
"sidebarClients": "Müşteriler",
"sidebarDomains": "Alan Adları",
"enableDockerSocket": "Docker Soketi Etkinleştir", "enableDockerSocket": "Docker Soketi Etkinleştir",
"enableDockerSocketDescription": "Konteyner bilgilerini doldurmak için Docker Socket keşfini etkinleştirin. Socket yolu Newt'e sağlanmalıdır.", "enableDockerSocketDescription": "Konteyner bilgilerini doldurmak için Docker Socket keşfini etkinleştirin. Socket yolu Newt'e sağlanmalıdır.",
"enableDockerSocketLink": "Daha fazla bilgi", "enableDockerSocketLink": "Daha fazla bilgi",
@ -1102,7 +1108,7 @@
"containerNetworks": "Ağlar", "containerNetworks": "Ağlar",
"containerHostnameIp": "Ana Makine/IP", "containerHostnameIp": "Ana Makine/IP",
"containerLabels": "Etiketler", "containerLabels": "Etiketler",
"containerLabelsCount": "{count} etiket{s,plural,one{} other{ler}}", "containerLabelsCount": "{count, plural, one {# etiket} other {# etiketler}}",
"containerLabelsTitle": "Konteyner Etiketleri", "containerLabelsTitle": "Konteyner Etiketleri",
"containerLabelEmpty": "<boş>", "containerLabelEmpty": "<boş>",
"containerPorts": "Bağlantı Noktaları", "containerPorts": "Bağlantı Noktaları",
@ -1114,7 +1120,7 @@
"showStoppedContainers": "Durdurulmuş konteynerleri göster", "showStoppedContainers": "Durdurulmuş konteynerleri göster",
"noContainersFound": "Konteyner bulunamadı. Docker konteynerlerinin çalıştığından emin olun.", "noContainersFound": "Konteyner bulunamadı. Docker konteynerlerinin çalıştığından emin olun.",
"searchContainersPlaceholder": "{count} konteyner arasında arama yapın...", "searchContainersPlaceholder": "{count} konteyner arasında arama yapın...",
"searchResultsCount": "{count} sonuç{s,plural,one{} other{lar}}", "searchResultsCount": "{count, plural, one {# sonuç} other {# sonuçlar}}",
"filters": "Filtreler", "filters": "Filtreler",
"filterOptions": "Filtre Seçenekleri", "filterOptions": "Filtre Seçenekleri",
"filterPorts": "Bağlantı Noktaları", "filterPorts": "Bağlantı Noktaları",
@ -1129,10 +1135,89 @@
"dark": "koyu", "dark": "koyu",
"system": "sistem", "system": "sistem",
"theme": "Tema", "theme": "Tema",
"subnetRequired": "Alt ağ gereklidir",
"initialSetupTitle": "İlk Sunucu Kurulumu", "initialSetupTitle": "İlk Sunucu Kurulumu",
"initialSetupDescription": "İlk sunucu yönetici hesabını oluşturun. Yalnızca bir sunucu yöneticisi olabilir. Bu kimlik bilgilerini daha sonra her zaman değiştirebilirsiniz.", "initialSetupDescription": "İlk sunucu yönetici hesabını oluşturun. Yalnızca bir sunucu yöneticisi olabilir. Bu kimlik bilgilerini daha sonra her zaman değiştirebilirsiniz.",
"createAdminAccount": "Yönetici Hesabı Oluştur", "createAdminAccount": "Yönetici Hesabı Oluştur",
"setupErrorCreateAdmin": "Sunucu yönetici hesabı oluşturulurken bir hata oluştu.", "setupErrorCreateAdmin": "Sunucu yönetici hesabı oluşturulurken bir hata oluştu.",
"certificateStatus": "Sertifika Durumu",
"loading": "Yükleniyor",
"restart": "Yeniden Başlat",
"domains": "Alan Adları",
"domainsDescription": "Organizasyonunuz için alan adlarını yönetin",
"domainsSearch": "Alan adlarını ara...",
"domainAdd": "Alan Adı Ekle",
"domainAddDescription": "Organizasyonunuz için yeni bir alan adı kaydedin",
"domainCreate": "Alan Adı Oluştur",
"domainCreatedDescription": "Alan adı başarıyla oluşturuldu",
"domainDeletedDescription": "Alan adı başarıyla silindi",
"domainQuestionRemove": "{domain} alan adını hesabınızdan kaldırmak istediğinizden emin misiniz?",
"domainMessageRemove": "Kaldırıldığında, alan adı hesabınızla ilişkilendirilmeyecek.",
"domainMessageConfirm": "Onaylamak için lütfen aşağıya alan adını yazın.",
"domainConfirmDelete": "Alan Adı Silinmesini Onayla",
"domainDelete": "Alan Adını Sil",
"domain": "Alan Adı",
"selectDomainTypeNsName": "Alan Adı Delege Etme (NS)",
"selectDomainTypeNsDescription": "Bu alan adı ve tüm alt alan adları. Tüm bir alan adı bölgesini kontrol etmek istediğinizde bunu kullanın.",
"selectDomainTypeCnameName": "Tekil Alan Adı (CNAME)",
"selectDomainTypeCnameDescription": "Sadece bu belirli alan adı. Bireysel alt alan adları veya belirli alan adı girişleri için bunu kullanın.",
"selectDomainTypeWildcardName": "Wildcard Alan Adı",
"selectDomainTypeWildcardDescription": "Bu alan adı ve onun ilk alt alan düzeyi.",
"domainDelegation": "Tekil Alan Adı",
"selectType": "Bir tür seçin",
"actions": "İşlemler",
"refresh": "Yenile",
"refreshError": "Veriler yenilenemedi",
"verified": "Doğrulandı",
"pending": "Beklemede",
"sidebarBilling": "Faturalama",
"billing": "Faturalama",
"orgBillingDescription": "Fatura bilgilerinizi ve aboneliklerinizi yönetin",
"github": "GitHub",
"pangolinHosted": "Pangolin Barındırılan",
"fossorial": "Fossorial",
"completeAccountSetup": "Hesap Kurulumunu Tamamla",
"completeAccountSetupDescription": "Başlamak için şifrenizi ayarlayın",
"accountSetupSent": "Bu e-posta adresine bir hesap kurulum kodu göndereceğiz.",
"accountSetupCode": "Kurulum Kodu",
"accountSetupCodeDescription": "Kurulum kodu için e-posta gelen kutunuzu kontrol edin.",
"passwordCreate": "Parola Oluştur",
"passwordCreateConfirm": "Şifreyi Onayla",
"accountSetupSubmit": "Kurulum Kodunu Gönder",
"completeSetup": "Kurulumu Tamamla",
"accountSetupSuccess": "Hesap kurulumu tamamlandı! Pangolin'e hoş geldiniz!",
"documentation": "Dokümantasyon",
"saveAllSettings": "Tüm Ayarları Kaydet",
"settingsUpdated": "Ayarlar güncellendi",
"settingsUpdatedDescription": "Tüm ayarlar başarıyla güncellendi",
"settingsErrorUpdate": "Ayarlar güncellenemedi",
"settingsErrorUpdateDescription": "Ayarları güncellerken bir hata oluştu",
"sidebarCollapse": "Daralt",
"sidebarExpand": "Genişlet",
"newtUpdateAvailable": "Güncelleme Mevcut",
"newtUpdateAvailableInfo": "Newt'in yeni bir versiyonu mevcut. En iyi deneyim için lütfen en son sürüme güncelleyin.",
"domainPickerEnterDomain": "Alan adınızı girin",
"domainPickerPlaceholder": "myapp.example.com, api.v1.mydomain.com veya sadece myapp",
"domainPickerDescription": "Mevcut seçenekleri görmek için kaynağın tam etki alanını girin.",
"domainPickerDescriptionSaas": "Mevcut seçenekleri görmek için tam etki alanı, alt etki alanı veya sadece bir isim girin",
"domainPickerTabAll": "Tümü",
"domainPickerTabOrganization": "Organizasyon",
"domainPickerTabProvided": "Sağlanan",
"domainPickerSortAsc": "A-Z",
"domainPickerSortDesc": "Z-A",
"domainPickerCheckingAvailability": "Kullanılabilirlik kontrol ediliyor...",
"domainPickerNoMatchingDomains": "\"{userInput}\" için uygun alan adı bulunamadı. Farklı bir alan adı deneyin veya organizasyonunuzun alan adı ayarlarını kontrol edin.",
"domainPickerOrganizationDomains": "Organizasyon Alan Adları",
"domainPickerProvidedDomains": "Sağlanan Alan Adları",
"domainPickerSubdomain": "Alt Alan: {subdomain}",
"domainPickerNamespace": "Ad Alanı: {namespace}",
"domainPickerShowMore": "Daha Fazla Göster",
"domainNotFound": "Alan Adı Bulunamadı",
"domainNotFoundDescription": "Bu kaynak devre dışıdır çünkü alan adı sistemimizde artık mevcut değil. Bu kaynak için yeni bir alan adı belirleyin.",
"failed": "Başarısız",
"createNewOrgDescription": "Yeni bir organizasyon oluşturun",
"organization": "Kuruluş",
"port": "Bağlantı Noktası",
"securityKeyManage": "Güvenlik Anahtarlarını Yönet", "securityKeyManage": "Güvenlik Anahtarlarını Yönet",
"securityKeyDescription": "Şifresiz kimlik doğrulama için güvenlik anahtarları ekleyin veya kaldırın", "securityKeyDescription": "Şifresiz kimlik doğrulama için güvenlik anahtarları ekleyin veya kaldırın",
"securityKeyRegister": "Yeni Güvenlik Anahtarı Kaydet", "securityKeyRegister": "Yeni Güvenlik Anahtarı Kaydet",
@ -1142,13 +1227,51 @@
"securityKeyRemove": "Kaldır", "securityKeyRemove": "Kaldır",
"securityKeyLastUsed": "Son kullanım: {date}", "securityKeyLastUsed": "Son kullanım: {date}",
"securityKeyNameLabel": "İsim", "securityKeyNameLabel": "İsim",
"securityKeyNamePlaceholder": "Bu güvenlik anahtarı için bir isim girin",
"securityKeyRegisterSuccess": "Güvenlik anahtarı başarıyla kaydedildi", "securityKeyRegisterSuccess": "Güvenlik anahtarı başarıyla kaydedildi",
"securityKeyRegisterError": "Güvenlik anahtarı kaydedilirken hata oluştu", "securityKeyRegisterError": "Güvenlik anahtarı kaydedilirken hata oluştu",
"securityKeyRemoveSuccess": "Güvenlik anahtarı başarıyla kaldırıldı", "securityKeyRemoveSuccess": "Güvenlik anahtarı başarıyla kaldırıldı",
"securityKeyRemoveError": "Güvenlik anahtarı kaldırılırken hata oluştu", "securityKeyRemoveError": "Güvenlik anahtarı kaldırılırken hata oluştu",
"securityKeyLoadError": "Güvenlik anahtarları yüklenirken hata oluştu", "securityKeyLoadError": "Güvenlik anahtarları yüklenirken hata oluştu",
"securityKeyLogin": "Güvenlik anahtarı ile giriş yap", "securityKeyLogin": "Güvenlik anahtarı ile devam edin",
"securityKeyAuthError": "Güvenlik anahtarı ile kimlik doğrulama başarısız oldu", "securityKeyAuthError": "Güvenlik anahtarı ile kimlik doğrulama başarısız oldu",
"securityKeyRecommendation": "Hesabınızdan kilitlenmediğinizden emin olmak için farklı bir cihazda başka bir güvenlik anahtarı kaydetmeyi düşünün." "securityKeyRecommendation": "Hesabınızdan kilitlenmediğinizden emin olmak için farklı bir cihazda başka bir güvenlik anahtarı kaydetmeyi düşünün.",
"registering": "Kaydediliyor...",
"securityKeyPrompt": "Lütfen güvenlik anahtarınızı kullanarak kimliğinizi doğrulayın. Güvenlik anahtarınızın bağlı ve hazır olduğundan emin olun.",
"securityKeyBrowserNotSupported": "Tarayıcınız güvenlik anahtarlarını desteklemiyor. Lütfen Chrome, Firefox veya Safari gibi modern bir tarayıcı kullanın.",
"securityKeyPermissionDenied": "Giriş yapmaya devam etmek için lütfen güvenlik anahtarınıza erişime izin verin.",
"securityKeyRemovedTooQuickly": "Güvenlik anahtarınızın bağlantısını kesmeden önce oturum açma işlemi tamamlanana kadar bağlı kalmasını sağlayın.",
"securityKeyNotSupported": "Güvenlik anahtarınız uyumlu olmayabilir. Lütfen farklı bir güvenlik anahtarı deneyin.",
"securityKeyUnknownError": "Güvenlik anahtarınızı kullanırken bir sorun oluştu. Lütfen tekrar deneyin.",
"twoFactorRequired": "Güvenlik anahtarını kaydetmek için iki faktörlü kimlik doğrulama gereklidir.",
"twoFactor": "İki Faktörlü Kimlik Doğrulama",
"adminEnabled2FaOnYourAccount": "Yöneticiniz {email} için iki faktörlü kimlik doğrulamayı etkinleştirdi. Devam etmek için kurulum işlemini tamamlayın.",
"continueToApplication": "Uygulamaya Devam Et",
"securityKeyAdd": "Güvenlik Anahtarı Ekle",
"securityKeyRegisterTitle": "Yeni Güvenlik Anahtarı Kaydet",
"securityKeyRegisterDescription": "Güvenlik anahtarınızı bağlayın ve tanımlamak için bir ad girin",
"securityKeyTwoFactorRequired": "İki Faktörlü Kimlik Doğrulama Gereklidir",
"securityKeyTwoFactorDescription": "Güvenlik anahtarını kaydetmek için lütfen iki faktörlü kimlik doğrulama kodunuzu girin",
"securityKeyTwoFactorRemoveDescription": "Güvenlik anahtarını kaldırmak için lütfen iki faktörlü kimlik doğrulama kodunuzu girin",
"securityKeyTwoFactorCode": "İki Faktörlü Kod",
"securityKeyRemoveTitle": "Güvenlik Anahtarını Kaldır",
"securityKeyRemoveDescription": "Güvenlik anahtarını \"{name}\" kaldırmak için şifrenizi girin",
"securityKeyNoKeysRegistered": "Kayıtlı güvenlik anahtarı yok",
"securityKeyNoKeysDescription": "Hesabınızın güvenliğini artırmak için bir güvenlik anahtarı ekleyin",
"createDomainRequired": "Alan adı gereklidir",
"createDomainAddDnsRecords": "DNS Kayıtlarını Ekle",
"createDomainAddDnsRecordsDescription": "Kurulumu tamamlamak için alan sağlayıcınıza şu DNS kayıtlarını ekleyin.",
"createDomainNsRecords": "NS Kayıtları",
"createDomainRecord": "Kayıt",
"createDomainType": "Tür:",
"createDomainName": "Ad:",
"createDomainValue": "Değer:",
"createDomainCnameRecords": "CNAME Kayıtları",
"createDomainRecordNumber": "Kayıt {number}",
"createDomainTxtRecords": "TXT Kayıtları",
"createDomainSaveTheseRecords": "Bu Kayıtları Kaydet",
"createDomainSaveTheseRecordsDescription": "Bu DNS kayıtlarını kaydettiğinizden emin olun çünkü tekrar görmeyeceksiniz.",
"createDomainDnsPropagation": "DNS Yayılması",
"createDomainDnsPropagationDescription": "DNS değişikliklerinin internet genelinde yayılması zaman alabilir. DNS sağlayıcınız ve TTL ayarlarına bağlı olarak bu birkaç dakika ile 48 saat arasında değişebilir.",
"resourcePortRequired": "HTTP dışı kaynaklar için bağlantı noktası numarası gereklidir",
"resourcePortNotAllowed": "HTTP kaynakları için bağlantı noktası numarası ayarlanmamalı"
} }

View file

@ -11,8 +11,9 @@
"componentsErrorNoMemberCreate": "您目前不是任何组织的成员。创建组织以开始操作。", "componentsErrorNoMemberCreate": "您目前不是任何组织的成员。创建组织以开始操作。",
"componentsErrorNoMember": "您目前不是任何组织的成员。", "componentsErrorNoMember": "您目前不是任何组织的成员。",
"welcome": "欢迎使用 Pangolin", "welcome": "欢迎使用 Pangolin",
"welcomeTo": "欢迎来到",
"componentsCreateOrg": "创建组织", "componentsCreateOrg": "创建组织",
"componentsMember": "您属于 {count, plural, =0 {无组织} =1 {一个组织} other {# 个组织}}。", "componentsMember": "您属于{count, plural, =0 {没有组织} one {一个组织} other {# 个组织}}。",
"componentsInvalidKey": "检测到无效或过期的许可证密钥。按照许可证条款操作以继续使用所有功能。", "componentsInvalidKey": "检测到无效或过期的许可证密钥。按照许可证条款操作以继续使用所有功能。",
"dismiss": "忽略", "dismiss": "忽略",
"componentsLicenseViolation": "许可证超限:该服务器使用了 {usedSites} 个站点,已超过授权的 {maxSites} 个。请遵守许可证条款以继续使用全部功能。", "componentsLicenseViolation": "许可证超限:该服务器使用了 {usedSites} 个站点,已超过授权的 {maxSites} 个。请遵守许可证条款以继续使用全部功能。",
@ -206,6 +207,7 @@
"orgGeneralSettings": "组织设置", "orgGeneralSettings": "组织设置",
"orgGeneralSettingsDescription": "管理您的机构详细信息和配置", "orgGeneralSettingsDescription": "管理您的机构详细信息和配置",
"saveGeneralSettings": "保存常规设置", "saveGeneralSettings": "保存常规设置",
"saveSettings": "保存设置",
"orgDangerZone": "危险区域", "orgDangerZone": "危险区域",
"orgDangerZoneDescription": "一旦删除该组织,将无法恢复,请务必确认。", "orgDangerZoneDescription": "一旦删除该组织,将无法恢复,请务必确认。",
"orgDelete": "删除组织", "orgDelete": "删除组织",
@ -249,7 +251,7 @@
"weeks": "周", "weeks": "周",
"months": "月", "months": "月",
"years": "年", "years": "年",
"day": "{count, plural, =1 {# 天} other {# 天}}", "day": "{count, plural, other {# 天}}",
"apiKeysTitle": "API 密钥", "apiKeysTitle": "API 密钥",
"apiKeysConfirmCopy2": "您必须确认您已复制 API 密钥。", "apiKeysConfirmCopy2": "您必须确认您已复制 API 密钥。",
"apiKeysErrorCreate": "创建 API 密钥出错", "apiKeysErrorCreate": "创建 API 密钥出错",
@ -347,7 +349,7 @@
"licensePurchase": "购买许可证", "licensePurchase": "购买许可证",
"licensePurchaseSites": "购买更多站点", "licensePurchaseSites": "购买更多站点",
"licenseSitesUsedMax": "使用了 {usedSites}/{maxSites} 个站点", "licenseSitesUsedMax": "使用了 {usedSites}/{maxSites} 个站点",
"licenseSitesUsed": "{count, plural, =0 {# 站点} =1 {# 站点} other {# 站点}}", "licenseSitesUsed": "{count, plural, =0 {# 站点} one {# 站点} other {# 站点}}",
"licensePurchaseDescription": "请选择您希望 {selectedMode, select, license {直接购买许可证,您可以随时增加更多站点。} other {为现有许可证购买更多站点}}", "licensePurchaseDescription": "请选择您希望 {selectedMode, select, license {直接购买许可证,您可以随时增加更多站点。} other {为现有许可证购买更多站点}}",
"licenseFee": "许可证费用", "licenseFee": "许可证费用",
"licensePriceSite": "每个站点的价格", "licensePriceSite": "每个站点的价格",
@ -436,7 +438,7 @@
"accessRoleSelect": "选择角色", "accessRoleSelect": "选择角色",
"inviteEmailSentDescription": "一封电子邮件已经发送给用户,带有下面的访问链接。他们必须访问该链接才能接受邀请。", "inviteEmailSentDescription": "一封电子邮件已经发送给用户,带有下面的访问链接。他们必须访问该链接才能接受邀请。",
"inviteSentDescription": "用户已被邀请。他们必须访问下面的链接才能接受邀请。", "inviteSentDescription": "用户已被邀请。他们必须访问下面的链接才能接受邀请。",
"inviteExpiresIn": "邀请将于 {days, plural, =1 {# 天} other {# 天}}", "inviteExpiresIn": "邀请将在{days, plural, other {# 天}}后过期。",
"idpTitle": "身份提供商", "idpTitle": "身份提供商",
"idpSelect": "为外部用户选择身份提供商", "idpSelect": "为外部用户选择身份提供商",
"idpNotConfigured": "没有配置身份提供者。请在创建外部用户之前配置身份提供者。", "idpNotConfigured": "没有配置身份提供者。请在创建外部用户之前配置身份提供者。",
@ -958,6 +960,8 @@
"licenseTierProfessionalRequiredDescription": "此功能仅在专业版可用。", "licenseTierProfessionalRequiredDescription": "此功能仅在专业版可用。",
"actionGetOrg": "获取组织", "actionGetOrg": "获取组织",
"actionUpdateOrg": "更新组织", "actionUpdateOrg": "更新组织",
"actionUpdateUser": "更新用户",
"actionGetUser": "获取用户",
"actionGetOrgUser": "获取组织用户", "actionGetOrgUser": "获取组织用户",
"actionListOrgDomains": "列出组织域", "actionListOrgDomains": "列出组织域",
"actionCreateSite": "创建站点", "actionCreateSite": "创建站点",
@ -1090,6 +1094,8 @@
"sidebarAllUsers": "所有用户", "sidebarAllUsers": "所有用户",
"sidebarIdentityProviders": "身份提供商", "sidebarIdentityProviders": "身份提供商",
"sidebarLicense": "证书", "sidebarLicense": "证书",
"sidebarClients": "客户",
"sidebarDomains": "域",
"enableDockerSocket": "启用停靠套接字", "enableDockerSocket": "启用停靠套接字",
"enableDockerSocketDescription": "启用 Docker Socket 发现以填充容器信息。必须向 Newt 提供 Socket 路径。", "enableDockerSocketDescription": "启用 Docker Socket 发现以填充容器信息。必须向 Newt 提供 Socket 路径。",
"enableDockerSocketLink": "了解更多", "enableDockerSocketLink": "了解更多",
@ -1102,7 +1108,7 @@
"containerNetworks": "网络", "containerNetworks": "网络",
"containerHostnameIp": "主机名/IP", "containerHostnameIp": "主机名/IP",
"containerLabels": "标签", "containerLabels": "标签",
"containerLabelsCount": "{count} label{s,plural,one{} other{s}}", "containerLabelsCount": "{count, plural, other {# 标签}}",
"containerLabelsTitle": "容器标签", "containerLabelsTitle": "容器标签",
"containerLabelEmpty": "<empty>", "containerLabelEmpty": "<empty>",
"containerPorts": "端口", "containerPorts": "端口",
@ -1114,7 +1120,7 @@
"showStoppedContainers": "显示已停止的容器", "showStoppedContainers": "显示已停止的容器",
"noContainersFound": "未找到容器。请确保Docker容器正在运行。", "noContainersFound": "未找到容器。请确保Docker容器正在运行。",
"searchContainersPlaceholder": "在 {count} 个容器中搜索...", "searchContainersPlaceholder": "在 {count} 个容器中搜索...",
"searchResultsCount": "{count} result{s,plural,one{} other{s}}", "searchResultsCount": "{count, plural, other {# 个结果}}",
"filters": "筛选器", "filters": "筛选器",
"filterOptions": "过滤器选项", "filterOptions": "过滤器选项",
"filterPorts": "端口", "filterPorts": "端口",
@ -1129,10 +1135,89 @@
"dark": "深色", "dark": "深色",
"system": "系统", "system": "系统",
"theme": "主题", "theme": "主题",
"subnetRequired": "子网是必填项",
"initialSetupTitle": "初始服务器设置", "initialSetupTitle": "初始服务器设置",
"initialSetupDescription": "创建初始服务器管理员帐户。 只能存在一个服务器管理员。 您可以随时更改这些凭据。", "initialSetupDescription": "创建初始服务器管理员帐户。 只能存在一个服务器管理员。 您可以随时更改这些凭据。",
"createAdminAccount": "创建管理员帐户", "createAdminAccount": "创建管理员帐户",
"setupErrorCreateAdmin": "创建服务器管理员账户时发生错误。", "setupErrorCreateAdmin": "创建服务器管理员账户时发生错误。",
"certificateStatus": "证书状态",
"loading": "加载中",
"restart": "重启",
"domains": "域",
"domainsDescription": "管理您的组织域",
"domainsSearch": "搜索域...",
"domainAdd": "添加域",
"domainAddDescription": "在您的组织中注册新域",
"domainCreate": "创建域",
"domainCreatedDescription": "域创建成功",
"domainDeletedDescription": "成功删除域",
"domainQuestionRemove": "您确定要从您的账户中移除域{domain}吗?",
"domainMessageRemove": "移除后,该域将不再与您的账户关联。",
"domainMessageConfirm": "要确认,请在下方输入域名。",
"domainConfirmDelete": "确认删除域",
"domainDelete": "删除域",
"domain": "域",
"selectDomainTypeNsName": "域委派NS",
"selectDomainTypeNsDescription": "此域及其所有子域。当您希望控制整个域区域时使用此选项。",
"selectDomainTypeCnameName": "单个域CNAME",
"selectDomainTypeCnameDescription": "仅此特定域。用于单个子域或特定域条目。",
"selectDomainTypeWildcardName": "通配符域",
"selectDomainTypeWildcardDescription": "此域及其第一级子域。",
"domainDelegation": "单个域",
"selectType": "选择一个类型",
"actions": "操作",
"refresh": "刷新",
"refreshError": "刷新数据失败",
"verified": "已验证",
"pending": "待定",
"sidebarBilling": "计费",
"billing": "计费",
"orgBillingDescription": "管理您的账单信息和订阅",
"github": "GitHub",
"pangolinHosted": "Pangolin 托管",
"fossorial": "Fossorial",
"completeAccountSetup": "完成账户设置",
"completeAccountSetupDescription": "设置您的密码以开始",
"accountSetupSent": "我们将发送账号设置代码到该电子邮件地址。",
"accountSetupCode": "设置代码",
"accountSetupCodeDescription": "请检查您的邮箱以获取设置代码。",
"passwordCreate": "创建密码",
"passwordCreateConfirm": "确认密码",
"accountSetupSubmit": "发送设置代码",
"completeSetup": "完成设置",
"accountSetupSuccess": "账号设置完成!欢迎来到 Pangolin",
"documentation": "文档",
"saveAllSettings": "保存所有设置",
"settingsUpdated": "设置已更新",
"settingsUpdatedDescription": "所有设置已成功更新",
"settingsErrorUpdate": "设置更新失败",
"settingsErrorUpdateDescription": "更新设置时发生错误",
"sidebarCollapse": "折叠",
"sidebarExpand": "展开",
"newtUpdateAvailable": "更新可用",
"newtUpdateAvailableInfo": "新版本的 Newt 已可用。请更新到最新版本以获得最佳体验。",
"domainPickerEnterDomain": "输入您的域",
"domainPickerPlaceholder": "myapp.example.com、api.v1.mydomain.com 或仅 myapp",
"domainPickerDescription": "输入资源的完整域名以查看可用选项。",
"domainPickerDescriptionSaas": "输入完整域名、子域或名称以查看可用选项。",
"domainPickerTabAll": "所有",
"domainPickerTabOrganization": "组织",
"domainPickerTabProvided": "提供的",
"domainPickerSortAsc": "A-Z",
"domainPickerSortDesc": "Z-A",
"domainPickerCheckingAvailability": "检查可用性...",
"domainPickerNoMatchingDomains": "未找到 \"{userInput}\" 的匹配域。尝试其他域或检查您组织的域设置。",
"domainPickerOrganizationDomains": "组织域",
"domainPickerProvidedDomains": "提供的域",
"domainPickerSubdomain": "子域:{subdomain}",
"domainPickerNamespace": "命名空间:{namespace}",
"domainPickerShowMore": "显示更多",
"domainNotFound": "域未找到",
"domainNotFoundDescription": "此资源已禁用,因为该域不再在我们的系统中存在。请为此资源设置一个新域。",
"failed": "失败",
"createNewOrgDescription": "创建一个新组织",
"organization": "组织",
"port": "端口",
"securityKeyManage": "管理安全密钥", "securityKeyManage": "管理安全密钥",
"securityKeyDescription": "添加或删除用于无密码认证的安全密钥", "securityKeyDescription": "添加或删除用于无密码认证的安全密钥",
"securityKeyRegister": "注册新的安全密钥", "securityKeyRegister": "注册新的安全密钥",
@ -1142,13 +1227,51 @@
"securityKeyRemove": "删除", "securityKeyRemove": "删除",
"securityKeyLastUsed": "上次使用:{date}", "securityKeyLastUsed": "上次使用:{date}",
"securityKeyNameLabel": "名称", "securityKeyNameLabel": "名称",
"securityKeyNamePlaceholder": "为此安全密钥输入名称",
"securityKeyRegisterSuccess": "安全密钥注册成功", "securityKeyRegisterSuccess": "安全密钥注册成功",
"securityKeyRegisterError": "注册安全密钥失败", "securityKeyRegisterError": "注册安全密钥失败",
"securityKeyRemoveSuccess": "安全密钥删除成功", "securityKeyRemoveSuccess": "安全密钥删除成功",
"securityKeyRemoveError": "删除安全密钥失败", "securityKeyRemoveError": "删除安全密钥失败",
"securityKeyLoadError": "加载安全密钥失败", "securityKeyLoadError": "加载安全密钥失败",
"securityKeyLogin": "使用安全密钥登录", "securityKeyLogin": "使用安全密钥继续",
"securityKeyAuthError": "使用安全密钥认证失败", "securityKeyAuthError": "使用安全密钥认证失败",
"securityKeyRecommendation": "考虑在其他设备上注册另一个安全密钥,以确保不会被锁定在您的账户之外。" "securityKeyRecommendation": "考虑在其他设备上注册另一个安全密钥,以确保不会被锁定在您的账户之外。",
"registering": "注册中...",
"securityKeyPrompt": "请使用您的安全密钥验证身份。确保您的安全密钥已连接并准备好。",
"securityKeyBrowserNotSupported": "您的浏览器不支持安全密钥。请使用像 Chrome、Firefox 或 Safari 这样的现代浏览器。",
"securityKeyPermissionDenied": "请允许访问您的安全密钥以继续登录。",
"securityKeyRemovedTooQuickly": "请保持您的安全密钥连接,直到登录过程完成。",
"securityKeyNotSupported": "您的安全密钥可能不兼容。请尝试不同的安全密钥。",
"securityKeyUnknownError": "使用安全密钥时出现问题。请再试一次。",
"twoFactorRequired": "注册安全密钥需要两步验证。",
"twoFactor": "两步验证",
"adminEnabled2FaOnYourAccount": "管理员已为{email}启用两步验证。请完成设置以继续。",
"continueToApplication": "继续到应用程序",
"securityKeyAdd": "添加安全密钥",
"securityKeyRegisterTitle": "注册新安全密钥",
"securityKeyRegisterDescription": "连接您的安全密钥并输入名称以便识别",
"securityKeyTwoFactorRequired": "要求两步验证",
"securityKeyTwoFactorDescription": "请输入你的两步验证代码以注册安全密钥",
"securityKeyTwoFactorRemoveDescription": "请输入你的两步验证代码以移除安全密钥",
"securityKeyTwoFactorCode": "双因素代码",
"securityKeyRemoveTitle": "移除安全密钥",
"securityKeyRemoveDescription": "输入您的密码以移除安全密钥 \"{name}\"",
"securityKeyNoKeysRegistered": "没有注册安全密钥",
"securityKeyNoKeysDescription": "添加安全密钥以加强您的账户安全",
"createDomainRequired": "必须输入域",
"createDomainAddDnsRecords": "添加 DNS 记录",
"createDomainAddDnsRecordsDescription": "将以下 DNS 记录添加到您的域名提供商以完成设置。",
"createDomainNsRecords": "NS 记录",
"createDomainRecord": "记录",
"createDomainType": "类型:",
"createDomainName": "名称:",
"createDomainValue": "值:",
"createDomainCnameRecords": "CNAME 记录",
"createDomainRecordNumber": "记录 {number}",
"createDomainTxtRecords": "TXT 记录",
"createDomainSaveTheseRecords": "保存这些记录",
"createDomainSaveTheseRecordsDescription": "务必保存这些 DNS 记录,因为您将无法再次查看它们。",
"createDomainDnsPropagation": "DNS 传播",
"createDomainDnsPropagationDescription": "DNS 更改可能需要一些时间才能在互联网上传播。这可能需要从几分钟到 48 小时,具体取决于您的 DNS 提供商和 TTL 设置。",
"resourcePortRequired": "非 HTTP 资源必须输入端口号",
"resourcePortNotAllowed": "HTTP 资源不应设置端口号"
} }

797
package-lock.json generated

File diff suppressed because it is too large Load diff

View file

@ -49,6 +49,7 @@
"@radix-ui/react-switch": "1.2.5", "@radix-ui/react-switch": "1.2.5",
"@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",
"@react-email/components": "0.3.1", "@react-email/components": "0.3.1",
"@react-email/render": "^1.1.2", "@react-email/render": "^1.1.2",
"@simplewebauthn/browser": "^13.1.0", "@simplewebauthn/browser": "^13.1.0",
@ -113,7 +114,7 @@
"yargs": "18.0.0" "yargs": "18.0.0"
}, },
"devDependencies": { "devDependencies": {
"@dotenvx/dotenvx": "1.47.3", "@dotenvx/dotenvx": "1.47.6",
"@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",
@ -127,6 +128,7 @@
"@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/react": "19.1.8", "@types/react": "19.1.8",
"@types/react-dom": "19.1.6", "@types/react-dom": "19.1.6",
"@types/semver": "^7.7.0", "@types/semver": "^7.7.0",

BIN
public/auth-diagram1.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 647 KiB

BIN
public/clip.gif Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 500 KiB

132
public/diagram-dark.svg Normal file

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 52 KiB

132
public/diagram.svg Normal file

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 52 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 574 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 748 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 688 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 687 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 669 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 713 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 636 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 434 KiB

After

Width:  |  Height:  |  Size: 713 KiB

Before After
Before After

Binary file not shown.

After

Width:  |  Height:  |  Size: 356 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 707 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 713 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 556 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 585 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 456 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 674 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 597 KiB

View file

@ -5,20 +5,25 @@ import config from "@server/lib/config";
import logger from "@server/logger"; import logger from "@server/logger";
import { import {
errorHandlerMiddleware, errorHandlerMiddleware,
notFoundMiddleware, notFoundMiddleware
rateLimitMiddleware
} from "@server/middlewares"; } from "@server/middlewares";
import { authenticated, unauthenticated } from "@server/routers/external"; import { authenticated, unauthenticated } from "@server/routers/external";
import { router as wsRouter, handleWSUpgrade } from "@server/routers/ws"; import { router as wsRouter, handleWSUpgrade } from "@server/routers/ws";
import { logIncomingMiddleware } from "./middlewares/logIncoming"; import { logIncomingMiddleware } from "./middlewares/logIncoming";
import { csrfProtectionMiddleware } from "./middlewares/csrfProtection"; import { csrfProtectionMiddleware } from "./middlewares/csrfProtection";
import helmet from "helmet"; import helmet from "helmet";
import rateLimit from "express-rate-limit";
import createHttpError from "http-errors";
import HttpCode from "./types/HttpCode";
import requestTimeoutMiddleware from "./middlewares/requestTimeout";
import { createStore } from "./lib/rateLimitStore";
const dev = config.isDev; const dev = config.isDev;
const externalPort = config.getRawConfig().server.external_port; const externalPort = config.getRawConfig().server.external_port;
export function createApiServer() { export function createApiServer() {
const apiServer = express(); const apiServer = express();
const prefix = `/api/v1`;
const trustProxy = config.getRawConfig().server.trust_proxy; const trustProxy = config.getRawConfig().server.trust_proxy;
if (trustProxy) { if (trustProxy) {
@ -54,19 +59,30 @@ export function createApiServer() {
apiServer.use(cookieParser()); apiServer.use(cookieParser());
apiServer.use(express.json()); apiServer.use(express.json());
// Add request timeout middleware
apiServer.use(requestTimeoutMiddleware(60000)); // 60 second timeout
if (!dev) { if (!dev) {
apiServer.use( apiServer.use(
rateLimitMiddleware({ rateLimit({
windowMin: windowMs:
config.getRawConfig().rate_limits.global.window_minutes, config.getRawConfig().rate_limits.global.window_minutes *
60 *
1000,
max: config.getRawConfig().rate_limits.global.max_requests, max: config.getRawConfig().rate_limits.global.max_requests,
type: "IP_AND_PATH" keyGenerator: (req) => `apiServerGlobal:${req.ip}:${req.path}`,
handler: (req, res, next) => {
const message = `Rate limit exceeded. You can make ${config.getRawConfig().rate_limits.global.max_requests} requests every ${config.getRawConfig().rate_limits.global.window_minutes} minute(s).`;
return next(
createHttpError(HttpCode.TOO_MANY_REQUESTS, message)
);
},
store: createStore()
}) })
); );
} }
// API routes // API routes
const prefix = `/api/v1`;
apiServer.use(logIncomingMiddleware); apiServer.use(logIncomingMiddleware);
apiServer.use(prefix, unauthenticated); apiServer.use(prefix, unauthenticated);
apiServer.use(prefix, authenticated); apiServer.use(prefix, authenticated);

View file

@ -69,6 +69,11 @@ export enum ActionsEnum {
deleteResourceRule = "deleteResourceRule", deleteResourceRule = "deleteResourceRule",
listResourceRules = "listResourceRules", listResourceRules = "listResourceRules",
updateResourceRule = "updateResourceRule", updateResourceRule = "updateResourceRule",
createClient = "createClient",
deleteClient = "deleteClient",
updateClient = "updateClient",
listClients = "listClients",
getClient = "getClient",
listOrgDomains = "listOrgDomains", listOrgDomains = "listOrgDomains",
createNewt = "createNewt", createNewt = "createNewt",
createIdp = "createIdp", createIdp = "createIdp",
@ -88,7 +93,10 @@ export enum ActionsEnum {
listApiKeyActions = "listApiKeyActions", listApiKeyActions = "listApiKeyActions",
listApiKeys = "listApiKeys", listApiKeys = "listApiKeys",
getApiKey = "getApiKey", getApiKey = "getApiKey",
resetUserPassword = "resetUserPassword" resetUserPassword = "resetUserPassword",
createOrgDomain = "createOrgDomain",
deleteOrgDomain = "deleteOrgDomain",
restartOrgDomain = "restartOrgDomain"
} }
export async function checkUserActionPermission( export async function checkUserActionPermission(

View file

@ -1,40 +0,0 @@
import { db } from '@server/db';
import { limitsTable } from '@server/db';
import { and, eq } from 'drizzle-orm';
import createHttpError from 'http-errors';
import HttpCode from '@server/types/HttpCode';
interface CheckLimitOptions {
orgId: string;
limitName: string;
currentValue: number;
increment?: number;
}
export async function checkOrgLimit({ orgId, limitName, currentValue, increment = 0 }: CheckLimitOptions): Promise<boolean> {
try {
const limit = await db.select()
.from(limitsTable)
.where(
and(
eq(limitsTable.orgId, orgId),
eq(limitsTable.name, limitName)
)
)
.limit(1);
if (limit.length === 0) {
throw createHttpError(HttpCode.NOT_FOUND, `Limit "${limitName}" not found for organization`);
}
const limitValue = limit[0].value;
// Check if the current value plus the increment is within the limit
return (currentValue + increment) <= limitValue;
} catch (error) {
if (error instanceof Error) {
throw createHttpError(HttpCode.INTERNAL_SERVER_ERROR, `Error checking limit: ${error.message}`);
}
throw createHttpError(HttpCode.INTERNAL_SERVER_ERROR, 'Unknown error occurred while checking limit');
}
}

View file

@ -0,0 +1,72 @@
import {
encodeHexLowerCase,
} from "@oslojs/encoding";
import { sha256 } from "@oslojs/crypto/sha2";
import { Olm, olms, olmSessions, OlmSession } from "@server/db";
import { db } from "@server/db";
import { eq } from "drizzle-orm";
export const EXPIRES = 1000 * 60 * 60 * 24 * 30;
export async function createOlmSession(
token: string,
olmId: string,
): Promise<OlmSession> {
const sessionId = encodeHexLowerCase(
sha256(new TextEncoder().encode(token)),
);
const session: OlmSession = {
sessionId: sessionId,
olmId,
expiresAt: new Date(Date.now() + EXPIRES).getTime(),
};
await db.insert(olmSessions).values(session);
return session;
}
export async function validateOlmSessionToken(
token: string,
): Promise<SessionValidationResult> {
const sessionId = encodeHexLowerCase(
sha256(new TextEncoder().encode(token)),
);
const result = await db
.select({ olm: olms, session: olmSessions })
.from(olmSessions)
.innerJoin(olms, eq(olmSessions.olmId, olms.olmId))
.where(eq(olmSessions.sessionId, sessionId));
if (result.length < 1) {
return { session: null, olm: null };
}
const { olm, session } = result[0];
if (Date.now() >= session.expiresAt) {
await db
.delete(olmSessions)
.where(eq(olmSessions.sessionId, session.sessionId));
return { session: null, olm: null };
}
if (Date.now() >= session.expiresAt - (EXPIRES / 2)) {
session.expiresAt = new Date(
Date.now() + EXPIRES,
).getTime();
await db
.update(olmSessions)
.set({
expiresAt: session.expiresAt,
})
.where(eq(olmSessions.sessionId, session.sessionId));
}
return { session, olm };
}
export async function invalidateOlmSession(sessionId: string): Promise<void> {
await db.delete(olmSessions).where(eq(olmSessions.sessionId, sessionId));
}
export async function invalidateAllOlmSessions(olmId: string): Promise<void> {
await db.delete(olmSessions).where(eq(olmSessions.olmId, olmId));
}
export type SessionValidationResult =
| { session: OlmSession; olm: Olm }
| { session: null; olm: null };

1
server/build.ts Normal file
View file

@ -0,0 +1 @@
export const build = "oss" as any;

View file

@ -59,7 +59,7 @@ export async function getUniqueExitNodeEndpointName(): Promise<string> {
export function generateName(): string { export function generateName(): string {
return ( const name = (
names.descriptors[ names.descriptors[
Math.floor(Math.random() * names.descriptors.length) Math.floor(Math.random() * names.descriptors.length)
] + ] +
@ -68,4 +68,7 @@ export function generateName(): string {
) )
.toLowerCase() .toLowerCase()
.replace(/\s/g, "-"); .replace(/\s/g, "-");
// clean out any non-alphanumeric characters except for dashes
return name.replace(/[^a-z0-9-]/g, "");
} }

View file

@ -1,4 +1,5 @@
import { drizzle as DrizzlePostgres } from "drizzle-orm/node-postgres"; import { drizzle as DrizzlePostgres } from "drizzle-orm/node-postgres";
import { Pool } from "pg";
import { readConfigFile } from "@server/lib/readConfigFile"; import { readConfigFile } from "@server/lib/readConfigFile";
import { withReplicas } from "drizzle-orm/pg-core"; import { withReplicas } from "drizzle-orm/pg-core";
@ -20,19 +21,31 @@ function createDb() {
); );
} }
const primary = DrizzlePostgres(connectionString); // Create connection pools instead of individual connections
const primaryPool = new Pool({
connectionString,
max: 20,
idleTimeoutMillis: 30000,
connectionTimeoutMillis: 2000,
});
const replicas = []; const replicas = [];
if (!replicaConnections.length) { if (!replicaConnections.length) {
replicas.push(primary); replicas.push(DrizzlePostgres(primaryPool));
} else { } else {
for (const conn of replicaConnections) { for (const conn of replicaConnections) {
const replica = DrizzlePostgres(conn.connection_string); const replicaPool = new Pool({
replicas.push(replica); connectionString: conn.connection_string,
max: 10,
idleTimeoutMillis: 30000,
connectionTimeoutMillis: 2000,
});
replicas.push(DrizzlePostgres(replicaPool));
} }
} }
return withReplicas(primary, replicas as any); return withReplicas(DrizzlePostgres(primaryPool), replicas as any);
} }
export const db = createDb(); export const db = createDb();

View file

@ -1,2 +1,2 @@
export * from "./driver"; export * from "./driver";
export * from "./schema"; export * from "./schema";

View file

@ -1,5 +1,5 @@
import { migrate } from "drizzle-orm/node-postgres/migrator"; import { migrate } from "drizzle-orm/node-postgres/migrator";
import db from "./driver"; import { db } from "./driver";
import path from "path"; import path from "path";
const migrationsFolder = path.join("server/migrations"); const migrationsFolder = path.join("server/migrations");

View file

@ -12,13 +12,18 @@ import { InferSelectModel } from "drizzle-orm";
export const domains = pgTable("domains", { export const domains = pgTable("domains", {
domainId: varchar("domainId").primaryKey(), domainId: varchar("domainId").primaryKey(),
baseDomain: varchar("baseDomain").notNull(), baseDomain: varchar("baseDomain").notNull(),
configManaged: boolean("configManaged").notNull().default(false) configManaged: boolean("configManaged").notNull().default(false),
type: varchar("type"), // "ns", "cname", "wildcard"
verified: boolean("verified").notNull().default(false),
failed: boolean("failed").notNull().default(false),
tries: integer("tries").notNull().default(0)
}); });
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(),
passwordResetTokenExpiryHours: integer("passwordResetTokenExpiryHours").notNull().default(1) passwordResetTokenExpiryHours: integer("passwordResetTokenExpiryHours").notNull().default(1)
subnet: varchar("subnet")
}); });
export const orgDomains = pgTable("orgDomains", { export const orgDomains = pgTable("orgDomains", {
@ -43,12 +48,17 @@ export const sites = pgTable("sites", {
}), }),
name: varchar("name").notNull(), name: varchar("name").notNull(),
pubKey: varchar("pubKey"), pubKey: varchar("pubKey"),
subnet: varchar("subnet").notNull(), subnet: varchar("subnet"),
megabytesIn: real("bytesIn"), megabytesIn: real("bytesIn").default(0),
megabytesOut: real("bytesOut"), megabytesOut: real("bytesOut").default(0),
lastBandwidthUpdate: varchar("lastBandwidthUpdate"), lastBandwidthUpdate: varchar("lastBandwidthUpdate"),
type: varchar("type").notNull(), // "newt" or "wireguard" type: varchar("type").notNull(), // "newt" or "wireguard"
online: boolean("online").notNull().default(false), online: boolean("online").notNull().default(false),
address: varchar("address"),
endpoint: varchar("endpoint"),
publicKey: varchar("publicKey"),
lastHolePunch: bigint("lastHolePunch", { mode: "number" }),
listenPort: integer("listenPort"),
dockerSocketEnabled: boolean("dockerSocketEnabled").notNull().default(true) dockerSocketEnabled: boolean("dockerSocketEnabled").notNull().default(true)
}); });
@ -79,7 +89,6 @@ export const resources = pgTable("resources", {
emailWhitelistEnabled: boolean("emailWhitelistEnabled") emailWhitelistEnabled: boolean("emailWhitelistEnabled")
.notNull() .notNull()
.default(false), .default(false),
isBaseDomain: boolean("isBaseDomain"),
applyRules: boolean("applyRules").notNull().default(false), applyRules: boolean("applyRules").notNull().default(false),
enabled: boolean("enabled").notNull().default(true), enabled: boolean("enabled").notNull().default(true),
stickySession: boolean("stickySession").notNull().default(false), stickySession: boolean("stickySession").notNull().default(false),
@ -108,7 +117,8 @@ export const exitNodes = pgTable("exitNodes", {
endpoint: varchar("endpoint").notNull(), endpoint: varchar("endpoint").notNull(),
publicKey: varchar("publicKey").notNull(), publicKey: varchar("publicKey").notNull(),
listenPort: integer("listenPort").notNull(), listenPort: integer("listenPort").notNull(),
reachableAt: varchar("reachableAt") reachableAt: varchar("reachableAt"),
maxConnections: integer("maxConnections")
}); });
export const users = pgTable("user", { export const users = pgTable("user", {
@ -133,6 +143,7 @@ export const newts = pgTable("newt", {
newtId: varchar("id").primaryKey(), newtId: varchar("id").primaryKey(),
secretHash: varchar("secretHash").notNull(), secretHash: varchar("secretHash").notNull(),
dateCreated: varchar("dateCreated").notNull(), dateCreated: varchar("dateCreated").notNull(),
version: varchar("version"),
siteId: integer("siteId").references(() => sites.siteId, { siteId: integer("siteId").references(() => sites.siteId, {
onDelete: "cascade" onDelete: "cascade"
}) })
@ -275,18 +286,6 @@ export const userResources = pgTable("userResources", {
.references(() => resources.resourceId, { onDelete: "cascade" }) .references(() => resources.resourceId, { onDelete: "cascade" })
}); });
export const limitsTable = pgTable("limits", {
limitId: serial("limitId").primaryKey(),
orgId: varchar("orgId")
.references(() => orgs.orgId, {
onDelete: "cascade"
})
.notNull(),
name: varchar("name").notNull(),
value: bigint("value", { mode: "number" }).notNull(),
description: varchar("description")
});
export const userInvites = pgTable("userInvites", { export const userInvites = pgTable("userInvites", {
inviteId: varchar("inviteId").primaryKey(), inviteId: varchar("inviteId").primaryKey(),
orgId: varchar("orgId") orgId: varchar("orgId")
@ -493,6 +492,75 @@ export const idpOrg = pgTable("idpOrg", {
orgMapping: varchar("orgMapping") orgMapping: varchar("orgMapping")
}); });
export const clients = pgTable("clients", {
clientId: serial("id").primaryKey(),
orgId: varchar("orgId")
.references(() => orgs.orgId, {
onDelete: "cascade"
})
.notNull(),
exitNodeId: integer("exitNode").references(() => exitNodes.exitNodeId, {
onDelete: "set null"
}),
name: varchar("name").notNull(),
pubKey: varchar("pubKey"),
subnet: varchar("subnet").notNull(),
megabytesIn: integer("bytesIn"),
megabytesOut: integer("bytesOut"),
lastBandwidthUpdate: varchar("lastBandwidthUpdate"),
lastPing: varchar("lastPing"),
type: varchar("type").notNull(), // "olm"
online: boolean("online").notNull().default(false),
endpoint: varchar("endpoint"),
lastHolePunch: integer("lastHolePunch"),
maxConnections: integer("maxConnections")
});
export const clientSites = pgTable("clientSites", {
clientId: integer("clientId")
.notNull()
.references(() => clients.clientId, { onDelete: "cascade" }),
siteId: integer("siteId")
.notNull()
.references(() => sites.siteId, { onDelete: "cascade" }),
isRelayed: boolean("isRelayed").notNull().default(false)
});
export const olms = pgTable("olms", {
olmId: varchar("id").primaryKey(),
secretHash: varchar("secretHash").notNull(),
dateCreated: varchar("dateCreated").notNull(),
clientId: integer("clientId").references(() => clients.clientId, {
onDelete: "cascade"
})
});
export const olmSessions = pgTable("clientSession", {
sessionId: varchar("id").primaryKey(),
olmId: varchar("olmId")
.notNull()
.references(() => olms.olmId, { onDelete: "cascade" }),
expiresAt: integer("expiresAt").notNull()
});
export const userClients = pgTable("userClients", {
userId: varchar("userId")
.notNull()
.references(() => users.userId, { onDelete: "cascade" }),
clientId: integer("clientId")
.notNull()
.references(() => clients.clientId, { onDelete: "cascade" })
});
export const roleClients = pgTable("roleClients", {
roleId: integer("roleId")
.notNull()
.references(() => roles.roleId, { onDelete: "cascade" }),
clientId: integer("clientId")
.notNull()
.references(() => clients.clientId, { onDelete: "cascade" })
});
export const securityKeys = pgTable("webauthnCredentials", { export const securityKeys = pgTable("webauthnCredentials", {
credentialId: varchar("credentialId").primaryKey(), credentialId: varchar("credentialId").primaryKey(),
userId: varchar("userId").notNull().references(() => users.userId, { userId: varchar("userId").notNull().references(() => users.userId, {
@ -539,7 +607,6 @@ export type RoleSite = InferSelectModel<typeof roleSites>;
export type UserSite = InferSelectModel<typeof userSites>; export type UserSite = InferSelectModel<typeof userSites>;
export type RoleResource = InferSelectModel<typeof roleResources>; export type RoleResource = InferSelectModel<typeof roleResources>;
export type UserResource = InferSelectModel<typeof userResources>; export type UserResource = InferSelectModel<typeof userResources>;
export type Limit = InferSelectModel<typeof limitsTable>;
export type UserInvite = InferSelectModel<typeof userInvites>; export type UserInvite = InferSelectModel<typeof userInvites>;
export type UserOrg = InferSelectModel<typeof userOrgs>; export type UserOrg = InferSelectModel<typeof userOrgs>;
export type ResourceSession = InferSelectModel<typeof resourceSessions>; export type ResourceSession = InferSelectModel<typeof resourceSessions>;
@ -556,3 +623,10 @@ 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 Client = InferSelectModel<typeof clients>;
export type ClientSite = InferSelectModel<typeof clientSites>;
export type Olm = InferSelectModel<typeof olms>;
export type OlmSession = InferSelectModel<typeof olmSessions>;
export type UserClient = InferSelectModel<typeof userClients>;
export type RoleClient = InferSelectModel<typeof roleClients>;
export type OrgDomains = InferSelectModel<typeof orgDomains>;

View file

@ -2,28 +2,26 @@ import { drizzle as DrizzleSqlite } from "drizzle-orm/better-sqlite3";
import Database from "better-sqlite3"; import Database from "better-sqlite3";
import * as schema from "./schema"; import * as schema from "./schema";
import path from "path"; import path from "path";
import fs from "fs/promises"; import fs from "fs";
import { APP_PATH } from "@server/lib/consts"; import { APP_PATH } from "@server/lib/consts";
import { existsSync, mkdirSync } from "fs"; import { existsSync, mkdirSync } from "fs";
export const location = path.join(APP_PATH, "db", "db.sqlite"); export const location = path.join(APP_PATH, "db", "db.sqlite");
export const exists = await checkFileExists(location); export const exists = checkFileExists(location);
bootstrapVolume(); bootstrapVolume();
function createDb() { function createDb() {
const sqlite = new Database(location); const sqlite = new Database(location);
sqlite.pragma('foreign_keys = ON');
sqlite.exec('VACUUM;'); // This will initialize the database file with a valid SQLite header
return DrizzleSqlite(sqlite, { schema }); return DrizzleSqlite(sqlite, { schema });
} }
export const db = createDb(); export const db = createDb();
export default db; export default db;
async function checkFileExists(filePath: string): Promise<boolean> { function checkFileExists(filePath: string): boolean {
try { try {
await fs.access(filePath); fs.accessSync(filePath);
return true; return true;
} catch { } catch {
return false; return false;

View file

@ -1,5 +1,5 @@
import { migrate } from "drizzle-orm/better-sqlite3/migrator"; import { migrate } from "drizzle-orm/better-sqlite3/migrator";
import db from "./driver"; import { db } from "./driver";
import path from "path"; import path from "path";
const migrationsFolder = path.join("server/migrations"); const migrationsFolder = path.join("server/migrations");
@ -7,7 +7,7 @@ const migrationsFolder = path.join("server/migrations");
const runMigrations = async () => { const runMigrations = async () => {
console.log("Running migrations..."); console.log("Running migrations...");
try { try {
await migrate(db as any, { migrate(db as any, {
migrationsFolder: migrationsFolder, migrationsFolder: migrationsFolder,
}); });
console.log("Migrations completed successfully."); console.log("Migrations completed successfully.");

View file

@ -6,13 +6,27 @@ export const domains = sqliteTable("domains", {
baseDomain: text("baseDomain").notNull(), baseDomain: text("baseDomain").notNull(),
configManaged: integer("configManaged", { mode: "boolean" }) configManaged: integer("configManaged", { mode: "boolean" })
.notNull() .notNull()
.default(false) .default(false),
type: text("type"), // "ns", "cname", "wildcard"
verified: integer("verified", { mode: "boolean" }).notNull().default(false),
failed: integer("failed", { mode: "boolean" }).notNull().default(false),
tries: integer("tries").notNull().default(0)
}); });
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(),
passwordResetTokenExpiryHours: integer("passwordResetTokenExpiryHours").notNull().default(1) passwordResetTokenExpiryHours: integer("passwordResetTokenExpiryHours").notNull().default(1)
subnet: text("subnet")
});
export const userDomains = sqliteTable("userDomains", {
userId: text("userId")
.notNull()
.references(() => users.userId, { onDelete: "cascade" }),
domainId: text("domainId")
.notNull()
.references(() => domains.domainId, { onDelete: "cascade" })
}); });
export const orgDomains = sqliteTable("orgDomains", { export const orgDomains = sqliteTable("orgDomains", {
@ -37,12 +51,19 @@ export const sites = sqliteTable("sites", {
}), }),
name: text("name").notNull(), name: text("name").notNull(),
pubKey: text("pubKey"), pubKey: text("pubKey"),
subnet: text("subnet").notNull(), subnet: text("subnet"),
megabytesIn: integer("bytesIn"), megabytesIn: integer("bytesIn").default(0),
megabytesOut: integer("bytesOut"), megabytesOut: integer("bytesOut").default(0),
lastBandwidthUpdate: text("lastBandwidthUpdate"), lastBandwidthUpdate: text("lastBandwidthUpdate"),
type: text("type").notNull(), // "newt" or "wireguard" type: text("type").notNull(), // "newt" or "wireguard"
online: integer("online", { mode: "boolean" }).notNull().default(false), online: integer("online", { mode: "boolean" }).notNull().default(false),
// exit node stuff that is how to connect to the site when it has a wg server
address: text("address"), // this is the address of the wireguard interface in newt
endpoint: text("endpoint"), // this is how to reach gerbil externally - gets put into the wireguard config
publicKey: text("publicKey"), // TODO: Fix typo in publicKey
lastHolePunch: integer("lastHolePunch"),
listenPort: integer("listenPort"),
dockerSocketEnabled: integer("dockerSocketEnabled", { mode: "boolean" }) dockerSocketEnabled: integer("dockerSocketEnabled", { mode: "boolean" })
.notNull() .notNull()
.default(true) .default(true)
@ -77,7 +98,6 @@ export const resources = sqliteTable("resources", {
emailWhitelistEnabled: integer("emailWhitelistEnabled", { mode: "boolean" }) emailWhitelistEnabled: integer("emailWhitelistEnabled", { mode: "boolean" })
.notNull() .notNull()
.default(false), .default(false),
isBaseDomain: integer("isBaseDomain", { mode: "boolean" }),
applyRules: integer("applyRules", { mode: "boolean" }) applyRules: integer("applyRules", { mode: "boolean" })
.notNull() .notNull()
.default(false), .default(false),
@ -110,7 +130,8 @@ export const exitNodes = sqliteTable("exitNodes", {
endpoint: text("endpoint").notNull(), // this is how to reach gerbil externally - gets put into the wireguard config endpoint: text("endpoint").notNull(), // this is how to reach gerbil externally - gets put into the wireguard config
publicKey: text("publicKey").notNull(), publicKey: text("publicKey").notNull(),
listenPort: integer("listenPort").notNull(), listenPort: integer("listenPort").notNull(),
reachableAt: text("reachableAt") // this is the internal address of the gerbil http server for command control reachableAt: text("reachableAt"), // this is the internal address of the gerbil http server for command control
maxConnections: integer("maxConnections")
}); });
export const users = sqliteTable("user", { export const users = sqliteTable("user", {
@ -166,11 +187,54 @@ export const newts = sqliteTable("newt", {
newtId: text("id").primaryKey(), newtId: text("id").primaryKey(),
secretHash: text("secretHash").notNull(), secretHash: text("secretHash").notNull(),
dateCreated: text("dateCreated").notNull(), dateCreated: text("dateCreated").notNull(),
version: text("version"),
siteId: integer("siteId").references(() => sites.siteId, { siteId: integer("siteId").references(() => sites.siteId, {
onDelete: "cascade" onDelete: "cascade"
}) })
}); });
export const clients = sqliteTable("clients", {
clientId: integer("id").primaryKey({ autoIncrement: true }),
orgId: text("orgId")
.references(() => orgs.orgId, {
onDelete: "cascade"
})
.notNull(),
exitNodeId: integer("exitNode").references(() => exitNodes.exitNodeId, {
onDelete: "set null"
}),
name: text("name").notNull(),
pubKey: text("pubKey"),
subnet: text("subnet").notNull(),
megabytesIn: integer("bytesIn"),
megabytesOut: integer("bytesOut"),
lastBandwidthUpdate: text("lastBandwidthUpdate"),
lastPing: text("lastPing"),
type: text("type").notNull(), // "olm"
online: integer("online", { mode: "boolean" }).notNull().default(false),
endpoint: text("endpoint"),
lastHolePunch: integer("lastHolePunch")
});
export const clientSites = sqliteTable("clientSites", {
clientId: integer("clientId")
.notNull()
.references(() => clients.clientId, { onDelete: "cascade" }),
siteId: integer("siteId")
.notNull()
.references(() => sites.siteId, { onDelete: "cascade" }),
isRelayed: integer("isRelayed", { mode: "boolean" }).notNull().default(false)
});
export const olms = sqliteTable("olms", {
olmId: text("id").primaryKey(),
secretHash: text("secretHash").notNull(),
dateCreated: text("dateCreated").notNull(),
clientId: integer("clientId").references(() => clients.clientId, {
onDelete: "cascade"
})
});
export const twoFactorBackupCodes = sqliteTable("twoFactorBackupCodes", { export const twoFactorBackupCodes = sqliteTable("twoFactorBackupCodes", {
codeId: integer("id").primaryKey({ autoIncrement: true }), codeId: integer("id").primaryKey({ autoIncrement: true }),
userId: text("userId") userId: text("userId")
@ -195,6 +259,14 @@ export const newtSessions = sqliteTable("newtSession", {
expiresAt: integer("expiresAt").notNull() expiresAt: integer("expiresAt").notNull()
}); });
export const olmSessions = sqliteTable("clientSession", {
sessionId: text("id").primaryKey(),
olmId: text("olmId")
.notNull()
.references(() => olms.olmId, { onDelete: "cascade" }),
expiresAt: integer("expiresAt").notNull()
});
export const userOrgs = sqliteTable("userOrgs", { export const userOrgs = sqliteTable("userOrgs", {
userId: text("userId") userId: text("userId")
.notNull() .notNull()
@ -290,6 +362,24 @@ export const userSites = sqliteTable("userSites", {
.references(() => sites.siteId, { onDelete: "cascade" }) .references(() => sites.siteId, { onDelete: "cascade" })
}); });
export const userClients = sqliteTable("userClients", {
userId: text("userId")
.notNull()
.references(() => users.userId, { onDelete: "cascade" }),
clientId: integer("clientId")
.notNull()
.references(() => clients.clientId, { onDelete: "cascade" })
});
export const roleClients = sqliteTable("roleClients", {
roleId: integer("roleId")
.notNull()
.references(() => roles.roleId, { onDelete: "cascade" }),
clientId: integer("clientId")
.notNull()
.references(() => clients.clientId, { onDelete: "cascade" })
});
export const roleResources = sqliteTable("roleResources", { export const roleResources = sqliteTable("roleResources", {
roleId: integer("roleId") roleId: integer("roleId")
.notNull() .notNull()
@ -548,6 +638,8 @@ export type Target = InferSelectModel<typeof targets>;
export type Session = InferSelectModel<typeof sessions>; export type Session = InferSelectModel<typeof sessions>;
export type Newt = InferSelectModel<typeof newts>; export type Newt = InferSelectModel<typeof newts>;
export type NewtSession = InferSelectModel<typeof newtSessions>; export type NewtSession = InferSelectModel<typeof newtSessions>;
export type Olm = InferSelectModel<typeof olms>;
export type OlmSession = InferSelectModel<typeof olmSessions>;
export type EmailVerificationCode = InferSelectModel< export type EmailVerificationCode = InferSelectModel<
typeof emailVerificationCodes typeof emailVerificationCodes
>; >;
@ -573,8 +665,13 @@ export type ResourceWhitelist = InferSelectModel<typeof resourceWhitelist>;
export type VersionMigration = InferSelectModel<typeof versionMigrations>; export type VersionMigration = InferSelectModel<typeof versionMigrations>;
export type ResourceRule = InferSelectModel<typeof resourceRules>; export type ResourceRule = InferSelectModel<typeof resourceRules>;
export type Domain = InferSelectModel<typeof domains>; export type Domain = InferSelectModel<typeof domains>;
export type Client = InferSelectModel<typeof clients>;
export type ClientSite = InferSelectModel<typeof clientSites>;
export type RoleClient = InferSelectModel<typeof roleClients>;
export type UserClient = InferSelectModel<typeof userClients>;
export type SupporterKey = InferSelectModel<typeof supporterKey>; export type SupporterKey = InferSelectModel<typeof supporterKey>;
export type Idp = InferSelectModel<typeof idp>; 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 OrgDomains = InferSelectModel<typeof orgDomains>;

View file

@ -2,6 +2,7 @@ import { render } from "@react-email/render";
import { ReactElement } from "react"; import { ReactElement } from "react";
import emailClient from "@server/emails"; import emailClient from "@server/emails";
import logger from "@server/logger"; import logger from "@server/logger";
import config from "@server/lib/config";
export async function sendEmail( export async function sendEmail(
template: ReactElement, template: ReactElement,
@ -24,9 +25,11 @@ export async function sendEmail(
const emailHtml = await render(template); const emailHtml = await render(template);
const appName = "Pangolin";
await emailClient.sendMail({ await emailClient.sendMail({
from: { from: {
name: opts.name || "Pangolin", name: opts.name || appName,
address: opts.from, address: opts.from,
}, },
to: opts.to, to: opts.to,

View file

@ -1,11 +1,5 @@
import { import React from "react";
Body, import { Body, Head, Html, Preview, Tailwind } from "@react-email/components";
Head,
Html,
Preview,
Tailwind
} from "@react-email/components";
import * as React from "react";
import { themeColors } from "./lib/theme"; import { themeColors } from "./lib/theme";
import { import {
EmailContainer, EmailContainer,
@ -22,29 +16,29 @@ interface Props {
} }
export const ConfirmPasswordReset = ({ email }: Props) => { export const ConfirmPasswordReset = ({ email }: Props) => {
const previewText = `Your password has been reset`; const previewText = `Your password has been successfully reset.`;
return ( return (
<Html> <Html>
<Head /> <Head />
<Preview>{previewText}</Preview> <Preview>{previewText}</Preview>
<Tailwind config={themeColors}> <Tailwind config={themeColors}>
<Body className="font-sans relative"> <Body className="font-sans bg-gray-50">
<EmailContainer> <EmailContainer>
<EmailLetterHead /> <EmailLetterHead />
<EmailHeading>Password Reset Confirmation</EmailHeading> {/* <EmailHeading>Password Successfully Reset</EmailHeading> */}
<EmailGreeting>Hi {email || "there"},</EmailGreeting> <EmailGreeting>Hi there,</EmailGreeting>
<EmailText> <EmailText>
This email confirms that your password has just been Your password has been successfully reset. You can
reset. If you made this change, no further action is now sign in to your account using your new password.
required.
</EmailText> </EmailText>
<EmailText> <EmailText>
Thank you for keeping your account secure. If you didn't make this change, please contact our
support team immediately to secure your account.
</EmailText> </EmailText>
<EmailFooter> <EmailFooter>

View file

@ -1,11 +1,5 @@
import { import React from "react";
Body, import { Body, Head, Html, Preview, Tailwind } from "@react-email/components";
Head,
Html,
Preview,
Tailwind
} from "@react-email/components";
import * as React from "react";
import { themeColors } from "./lib/theme"; import { themeColors } from "./lib/theme";
import { import {
EmailContainer, EmailContainer,
@ -18,6 +12,7 @@ import {
EmailText EmailText
} from "./components/Email"; } from "./components/Email";
import CopyCodeBox from "./components/CopyCodeBox"; import CopyCodeBox from "./components/CopyCodeBox";
import ButtonLink from "./components/ButtonLink";
interface Props { interface Props {
email: string; email: string;
@ -26,37 +21,39 @@ interface Props {
} }
export const ResetPasswordCode = ({ email, code, link }: Props) => { export const ResetPasswordCode = ({ email, code, link }: Props) => {
const previewText = `Your password reset code is ${code}`; const previewText = `Reset your password with code: ${code}`;
return ( return (
<Html> <Html>
<Head /> <Head />
<Preview>{previewText}</Preview> <Preview>{previewText}</Preview>
<Tailwind config={themeColors}> <Tailwind config={themeColors}>
<Body className="font-sans"> <Body className="font-sans bg-gray-50">
<EmailContainer> <EmailContainer>
<EmailLetterHead /> <EmailLetterHead />
<EmailHeading>Password Reset Request</EmailHeading> {/* <EmailHeading>Reset Your Password</EmailHeading> */}
<EmailGreeting>Hi {email || "there"},</EmailGreeting> <EmailGreeting>Hi there,</EmailGreeting>
<EmailText> <EmailText>
Youve requested to reset your password. Please{" "} You've requested to reset your password. Click the
<a href={link} className="text-primary"> button below to reset your password, or use the
click here verification code provided if prompted.
</a>{" "}
and follow the instructions to reset your password,
or manually enter the following code:
</EmailText> </EmailText>
<EmailSection>
<ButtonLink href={link}>Reset Password</ButtonLink>
</EmailSection>
<EmailSection> <EmailSection>
<CopyCodeBox text={code} /> <CopyCodeBox text={code} />
</EmailSection> </EmailSection>
<EmailText> <EmailText>
If you didnt request this, you can safely ignore This reset code will expire in 2 hours. If you
this email. didn't request a password reset, you can safely
ignore this email.
</EmailText> </EmailText>
<EmailFooter> <EmailFooter>

View file

@ -1,11 +1,5 @@
import { import React from "react";
Body, import { Body, Head, Html, Preview, Tailwind } from "@react-email/components";
Head,
Html,
Preview,
Tailwind
} from "@react-email/components";
import * as React from "react";
import { import {
EmailContainer, EmailContainer,
EmailLetterHead, EmailLetterHead,
@ -32,34 +26,40 @@ export const ResourceOTPCode = ({
orgName: organizationName, orgName: organizationName,
otp otp
}: ResourceOTPCodeProps) => { }: ResourceOTPCodeProps) => {
const previewText = `Your one-time password for ${resourceName} is ${otp}`; const previewText = `Your access code for ${resourceName}: ${otp}`;
return ( return (
<Html> <Html>
<Head /> <Head />
<Preview>{previewText}</Preview> <Preview>{previewText}</Preview>
<Tailwind config={themeColors}> <Tailwind config={themeColors}>
<Body className="font-sans"> <Body className="font-sans bg-gray-50">
<EmailContainer> <EmailContainer>
<EmailLetterHead /> <EmailLetterHead />
<EmailHeading> {/* <EmailHeading> */}
Your One-Time Code for {resourceName} {/* Access Code for {resourceName} */}
</EmailHeading> {/* </EmailHeading> */}
<EmailGreeting>Hi {email || "there"},</EmailGreeting> <EmailGreeting>Hi there,</EmailGreeting>
<EmailText> <EmailText>
Youve requested a one-time password to access{" "} You've requested access to{" "}
<strong>{resourceName}</strong> in{" "} <strong>{resourceName}</strong> in{" "}
<strong>{organizationName}</strong>. Use the code <strong>{organizationName}</strong>. Use the
below to complete your authentication: verification code below to complete your
authentication.
</EmailText> </EmailText>
<EmailSection> <EmailSection>
<CopyCodeBox text={otp} /> <CopyCodeBox text={otp} />
</EmailSection> </EmailSection>
<EmailText>
This code will expire in 15 minutes. If you didn't
request this code, please ignore this email.
</EmailText>
<EmailFooter> <EmailFooter>
<EmailSignature /> <EmailSignature />
</EmailFooter> </EmailFooter>

View file

@ -1,11 +1,5 @@
import { import React from "react";
Body, import { Body, Head, Html, Preview, Tailwind } from "@react-email/components";
Head,
Html,
Preview,
Tailwind,
} from "@react-email/components";
import * as React from "react";
import { themeColors } from "./lib/theme"; import { themeColors } from "./lib/theme";
import { import {
EmailContainer, EmailContainer,
@ -41,35 +35,44 @@ export const SendInviteLink = ({
<Head /> <Head />
<Preview>{previewText}</Preview> <Preview>{previewText}</Preview>
<Tailwind config={themeColors}> <Tailwind config={themeColors}>
<Body className="font-sans"> <Body className="font-sans bg-gray-50">
<EmailContainer> <EmailContainer>
<EmailLetterHead /> <EmailLetterHead />
<EmailHeading>Invited to Join {orgName}</EmailHeading> {/* <EmailHeading> */}
{/* You're Invited to Join {orgName} */}
{/* </EmailHeading> */}
<EmailGreeting>Hi {email || "there"},</EmailGreeting> <EmailGreeting>Hi there,</EmailGreeting>
<EmailText> <EmailText>
Youve been invited to join the organization{" "} You've been invited to join{" "}
<strong>{orgName}</strong> <strong>{orgName}</strong>
{inviterName ? ` by ${inviterName}.` : "."} Please {inviterName ? ` by ${inviterName}` : ""}. Click the
access the link below to accept the invite. button below to accept your invitation and get
</EmailText> started.
<EmailText>
This invite will expire in{" "}
<strong>
{expiresInDays}{" "}
{expiresInDays === "1" ? "day" : "days"}.
</strong>
</EmailText> </EmailText>
<EmailSection> <EmailSection>
<ButtonLink href={inviteLink}> <ButtonLink href={inviteLink}>
Accept Invite to {orgName} Accept Invitation
</ButtonLink> </ButtonLink>
</EmailSection> </EmailSection>
{/* <EmailText> */}
{/* If you're having trouble clicking the button, copy */}
{/* and paste the URL below into your web browser: */}
{/* <br /> */}
{/* <span className="break-all">{inviteLink}</span> */}
{/* </EmailText> */}
<EmailText>
This invite expires in {expiresInDays}{" "}
{expiresInDays === "1" ? "day" : "days"}. If the
link has expired, please contact the owner of the
organization to request a new invitation.
</EmailText>
<EmailFooter> <EmailFooter>
<EmailSignature /> <EmailSignature />
</EmailFooter> </EmailFooter>

View file

@ -1,11 +1,5 @@
import { import React from "react";
Body, import { Body, Head, Html, Preview, Tailwind } from "@react-email/components";
Head,
Html,
Preview,
Tailwind
} from "@react-email/components";
import * as React from "react";
import { themeColors } from "./lib/theme"; import { themeColors } from "./lib/theme";
import { import {
EmailContainer, EmailContainer,
@ -23,44 +17,52 @@ interface Props {
} }
export const TwoFactorAuthNotification = ({ email, enabled }: Props) => { export const TwoFactorAuthNotification = ({ email, enabled }: Props) => {
const previewText = `Two-Factor Authentication has been ${enabled ? "enabled" : "disabled"}`; const previewText = `Two-Factor Authentication ${enabled ? "enabled" : "disabled"} for your account`;
return ( return (
<Html> <Html>
<Head /> <Head />
<Preview>{previewText}</Preview> <Preview>{previewText}</Preview>
<Tailwind config={themeColors}> <Tailwind config={themeColors}>
<Body className="font-sans"> <Body className="font-sans bg-gray-50">
<EmailContainer> <EmailContainer>
<EmailLetterHead /> <EmailLetterHead />
<EmailHeading> {/* <EmailHeading> */}
Two-Factor Authentication{" "} {/* Security Update: 2FA{" "} */}
{enabled ? "Enabled" : "Disabled"} {/* {enabled ? "Enabled" : "Disabled"} */}
</EmailHeading> {/* </EmailHeading> */}
<EmailGreeting>Hi {email || "there"},</EmailGreeting> <EmailGreeting>Hi there,</EmailGreeting>
<EmailText> <EmailText>
This email confirms that Two-Factor Authentication Two-factor authentication has been successfully{" "}
has been successfully{" "} <strong>{enabled ? "enabled" : "disabled"}</strong>{" "}
{enabled ? "enabled" : "disabled"} on your account. on your account.
</EmailText> </EmailText>
{enabled ? ( {enabled ? (
<EmailText> <>
With Two-Factor Authentication enabled, your <EmailText>
account is now more secure. Please ensure you Your account is now protected with an
keep your authentication method safe. additional layer of security. Keep your
</EmailText> authentication method safe and accessible.
</EmailText>
</>
) : ( ) : (
<EmailText> <>
With Two-Factor Authentication disabled, your <EmailText>
account may be less secure. We recommend We recommend re-enabling two-factor
enabling it to protect your account. authentication to keep your account secure.
</EmailText> </EmailText>
</>
)} )}
<EmailText>
If you didn't make this change, please contact our
support team immediately.
</EmailText>
<EmailFooter> <EmailFooter>
<EmailSignature /> <EmailSignature />
</EmailFooter> </EmailFooter>

View file

@ -1,5 +1,5 @@
import React from "react";
import { Body, Head, Html, Preview, Tailwind } from "@react-email/components"; import { Body, Head, Html, Preview, Tailwind } from "@react-email/components";
import * as React from "react";
import { themeColors } from "./lib/theme"; import { themeColors } from "./lib/theme";
import { import {
EmailContainer, EmailContainer,
@ -24,25 +24,24 @@ export const VerifyEmail = ({
verificationCode, verificationCode,
verifyLink verifyLink
}: VerifyEmailProps) => { }: VerifyEmailProps) => {
const previewText = `Your verification code is ${verificationCode}`; const previewText = `Verify your email with code: ${verificationCode}`;
return ( return (
<Html> <Html>
<Head /> <Head />
<Preview>{previewText}</Preview> <Preview>{previewText}</Preview>
<Tailwind config={themeColors}> <Tailwind config={themeColors}>
<Body className="font-sans"> <Body className="font-sans bg-gray-50">
<EmailContainer> <EmailContainer>
<EmailLetterHead /> <EmailLetterHead />
<EmailHeading>Please Verify Your Email</EmailHeading> {/* <EmailHeading>Verify Your Email Address</EmailHeading> */}
<EmailGreeting>Hi {username || "there"},</EmailGreeting> <EmailGreeting>Hi there,</EmailGreeting>
<EmailText> <EmailText>
Youve requested to verify your email. Please use Welcome! To complete your account setup, please
the code below to complete the verification process verify your email address using the code below.
upon logging in.
</EmailText> </EmailText>
<EmailSection> <EmailSection>
@ -50,7 +49,8 @@ export const VerifyEmail = ({
</EmailSection> </EmailSection>
<EmailText> <EmailText>
If you didnt request this, you can safely ignore This verification code will expire in 15 minutes. If
you didn't create an account, you can safely ignore
this email. this email.
</EmailText> </EmailText>

View file

@ -0,0 +1,131 @@
import React from "react";
import { Body, Head, Html, Preview, Tailwind } from "@react-email/components";
import { themeColors } from "./lib/theme";
import {
EmailContainer,
EmailFooter,
EmailGreeting,
EmailHeading,
EmailLetterHead,
EmailSection,
EmailSignature,
EmailText,
EmailInfoSection
} from "./components/Email";
import ButtonLink from "./components/ButtonLink";
import CopyCodeBox from "./components/CopyCodeBox";
interface WelcomeQuickStartProps {
username?: string;
link: string;
fallbackLink: string;
resourceMethod: string;
resourceHostname: string;
resourcePort: string | number;
resourceUrl: string;
cliCommand: string;
}
export const WelcomeQuickStart = ({
username,
link,
fallbackLink,
resourceMethod,
resourceHostname,
resourcePort,
resourceUrl,
cliCommand
}: WelcomeQuickStartProps) => {
const previewText = "Welcome! Here's what to do next";
return (
<Html>
<Head />
<Preview>{previewText}</Preview>
<Tailwind config={themeColors}>
<Body className="font-sans bg-gray-50">
<EmailContainer>
<EmailLetterHead />
<EmailGreeting>Hi there,</EmailGreeting>
<EmailText>
Thank you for trying out Pangolin! We're excited to
have you on board.
</EmailText>
<EmailText>
To continue to configure your site, resources, and
other features, complete your account setup to
access the full dashboard.
</EmailText>
<EmailSection>
<ButtonLink href={link}>
View Your Dashboard
</ButtonLink>
{/* <p className="text-sm text-gray-300 mt-2"> */}
{/* If the button above doesn't work, you can also */}
{/* use this{" "} */}
{/* <a href={fallbackLink} className="underline"> */}
{/* link */}
{/* </a> */}
{/* . */}
{/* </p> */}
</EmailSection>
<EmailSection>
<div className="mb-2 font-semibold text-gray-900 text-base text-left">
Connect your site using Newt
</div>
<div className="inline-block w-full">
<div className="bg-gray-50 border border-gray-200 rounded-lg px-6 py-4 mx-auto text-left">
<span className="text-sm font-mono text-gray-900 tracking-wider">
{cliCommand}
</span>
</div>
<p className="text-xs text-gray-500 mt-2">
To learn how to use Newt, including more
installation methods, visit the{" "}
<a
href="https://docs.fossorial.io"
className="underline"
>
docs
</a>
.
</p>
</div>
</EmailSection>
<EmailInfoSection
title="Your Demo Resource"
items={[
{ label: "Method", value: resourceMethod },
{ label: "Hostname", value: resourceHostname },
{ label: "Port", value: resourcePort },
{
label: "Resource URL",
value: (
<a
href={resourceUrl}
className="underline text-blue-600"
>
{resourceUrl}
</a>
)
}
]}
/>
<EmailFooter>
<EmailSignature />
</EmailFooter>
</EmailContainer>
</Body>
</Tailwind>
</Html>
);
};
export default WelcomeQuickStart;

View file

@ -12,7 +12,11 @@ export default function ButtonLink({
return ( return (
<a <a
href={href} href={href}
className={`rounded-full bg-primary px-4 py-2 text-center font-semibold text-white text-xl no-underline inline-block ${className}`} className={`inline-block bg-primary text-white font-semibold px-8 py-3 rounded-lg text-center no-underline ${className}`}
style={{
backgroundColor: "#F97316",
textDecoration: "none"
}}
> >
{children} {children}
</a> </a>

View file

@ -2,10 +2,15 @@ import React from "react";
export default function CopyCodeBox({ text }: { text: string }) { export default function CopyCodeBox({ text }: { text: string }) {
return ( return (
<div className="text-center rounded-lg bg-neutral-100 p-2"> <div className="inline-block">
<span className="text-2xl font-mono text-neutral-600 tracking-wide"> <div className="bg-gray-50 border border-gray-200 rounded-lg px-6 py-4 mx-auto">
{text} <span className="text-2xl font-mono text-gray-900 tracking-wider font-semibold">
</span> {text}
</span>
</div>
<p className="text-xs text-gray-500 mt-2">
Copy and paste this code when prompted
</p>
</div> </div>
); );
} }

View file

@ -1,47 +1,27 @@
import { Container } from "@react-email/components";
import React from "react"; import React from "react";
import { Container, Img } from "@react-email/components";
import { build } from "@server/build";
// EmailContainer: Wraps the entire email layout // EmailContainer: Wraps the entire email layout
export function EmailContainer({ children }: { children: React.ReactNode }) { export function EmailContainer({ children }: { children: React.ReactNode }) {
return ( return (
<Container className="bg-white border border-solid border-gray-200 p-6 max-w-lg mx-auto my-8 rounded-lg"> <Container className="bg-white border border-solid border-gray-200 max-w-lg mx-auto my-8 rounded-lg overflow-hidden shadow-sm">
{children} {children}
</Container> </Container>
); );
} }
// EmailLetterHead: For branding or logo at the top // EmailLetterHead: For branding with logo on dark background
export function EmailLetterHead() { export function EmailLetterHead() {
return ( return (
<div className="mb-4"> <div className="px-6 pt-8 pb-2 text-center">
<table <Img
role="presentation" src="https://fossorial-public-assets.s3.us-east-1.amazonaws.com/word_mark_black.png"
width="100%" alt="Fossorial"
style={{ width="120"
marginBottom: "24px" height="auto"
}} className="mx-auto"
> />
<tr>
<td
style={{
fontSize: "14px",
fontWeight: "bold",
color: "#F97317"
}}
>
Pangolin
</td>
<td
style={{
fontSize: "14px",
textAlign: "right",
color: "#6B7280"
}}
>
{new Date().getFullYear()}
</td>
</tr>
</table>
</div> </div>
); );
} }
@ -49,14 +29,22 @@ export function EmailLetterHead() {
// EmailHeading: For the primary message or headline // EmailHeading: For the primary message or headline
export function EmailHeading({ children }: { children: React.ReactNode }) { export function EmailHeading({ children }: { children: React.ReactNode }) {
return ( return (
<h1 className="text-2xl font-semibold text-gray-800 text-center"> <div className="px-6 pt-4 pb-1">
{children} <h1 className="text-2xl font-semibold text-gray-900 text-center leading-tight">
</h1> {children}
</h1>
</div>
); );
} }
export function EmailGreeting({ children }: { children: React.ReactNode }) { export function EmailGreeting({ children }: { children: React.ReactNode }) {
return <p className="text-base text-gray-700 my-4">{children}</p>; return (
<div className="px-6">
<p className="text-base text-gray-700 leading-relaxed">
{children}
</p>
</div>
);
} }
// EmailText: For general text content // EmailText: For general text content
@ -68,9 +56,13 @@ export function EmailText({
className?: string; className?: string;
}) { }) {
return ( return (
<p className={`my-2 text-base text-gray-700 ${className}`}> <div className="px-6">
{children} <p
</p> className={`text-base text-gray-700 leading-relaxed ${className}`}
>
{children}
</p>
</div>
); );
} }
@ -82,20 +74,74 @@ export function EmailSection({
children: React.ReactNode; children: React.ReactNode;
className?: string; className?: string;
}) { }) {
return <div className={`text-center my-6 ${className}`}>{children}</div>; return (
<div className={`px-6 py-6 text-center ${className}`}>{children}</div>
);
} }
// EmailFooter: For closing or signature // EmailFooter: For closing or signature
export function EmailFooter({ children }: { children: React.ReactNode }) { export function EmailFooter({ children }: { children: React.ReactNode }) {
return <div className="text-sm text-gray-500 mt-6">{children}</div>; return (
<>
{build === "saas" && (
<div className="px-6 py-6 border-t border-gray-100 bg-gray-50">
{children}
<p className="text-xs text-gray-400 mt-4">
For any questions or support, please contact us at:
<br />
support@fossorial.io
</p>
<p className="text-xs text-gray-300 text-center mt-4">
&copy; {new Date().getFullYear()} Fossorial, Inc. All
rights reserved.
</p>
</div>
)}
</>
);
} }
export function EmailSignature() { export function EmailSignature() {
return ( return (
<p> <div className="text-sm text-gray-600">
Best regards, <p className="mb-2">
<br /> Best regards,
Fossorial <br />
</p> <strong>The Fossorial Team</strong>
</p>
</div>
);
}
// EmailInfoSection: For structured key-value info (like resource details)
export function EmailInfoSection({
title,
items
}: {
title?: string;
items: { label: string; value: React.ReactNode }[];
}) {
return (
<div className="px-6 py-4">
{title && (
<div className="mb-2 font-semibold text-gray-900 text-base">
{title}
</div>
)}
<table className="w-full text-sm text-left">
<tbody>
{items.map((item, idx) => (
<tr key={idx}>
<td className="pr-4 py-1 text-gray-600 align-top whitespace-nowrap">
{item.label}
</td>
<td className="py-1 text-gray-900 break-all">
{item.value}
</td>
</tr>
))}
</tbody>
</table>
</div>
); );
} }

View file

@ -1,3 +1,5 @@
import React from "react";
export const themeColors = { export const themeColors = {
theme: { theme: {
extend: { extend: {

View file

@ -9,6 +9,7 @@ import { createIntegrationApiServer } from "./integrationApiServer";
import config from "@server/lib/config"; import config from "@server/lib/config";
async function startServers() { async function startServers() {
await config.initServer();
await runSetupFunctions(); await runSetupFunctions();
// Start all servers // Start all servers

View file

@ -20,8 +20,9 @@ const externalPort = config.getRawConfig().server.integration_port;
export function createIntegrationApiServer() { export function createIntegrationApiServer() {
const apiServer = express(); const apiServer = express();
if (config.getRawConfig().server.trust_proxy) { const trustProxy = config.getRawConfig().server.trust_proxy;
apiServer.set("trust proxy", 1); if (trustProxy) {
apiServer.set("trust proxy", trustProxy);
} }
apiServer.use(cors()); apiServer.use(cors());

View file

@ -17,10 +17,6 @@ export class Config {
isDev: boolean = process.env.ENVIRONMENT !== "prod"; isDev: boolean = process.env.ENVIRONMENT !== "prod";
constructor() { constructor() {
this.load();
}
public load() {
const environment = readConfigFile(); const environment = readConfigFile();
const { const {
@ -85,22 +81,37 @@ export class Config {
parsedConfig.server.resource_access_token_headers.token; parsedConfig.server.resource_access_token_headers.token;
process.env.RESOURCE_SESSION_REQUEST_PARAM = process.env.RESOURCE_SESSION_REQUEST_PARAM =
parsedConfig.server.resource_session_request_param; parsedConfig.server.resource_session_request_param;
process.env.FLAGS_ALLOW_BASE_DOMAIN_RESOURCES = parsedConfig.flags process.env.DASHBOARD_URL = parsedConfig.app.dashboard_url;
?.allow_base_domain_resources process.env.FLAGS_DISABLE_LOCAL_SITES = parsedConfig.flags
?.disable_local_sites
? "true"
: "false";
process.env.FLAGS_DISABLE_BASIC_WIREGUARD_SITES = parsedConfig.flags
?.disable_basic_wireguard_sites
? "true" ? "true"
: "false"; : "false";
process.env.DASHBOARD_URL = parsedConfig.app.dashboard_url;
license.setServerSecret(parsedConfig.server.secret); process.env.FLAGS_ENABLE_CLIENTS = parsedConfig.flags?.enable_clients
? "true"
this.checkKeyStatus(); : "false";
this.rawConfig = parsedConfig; this.rawConfig = parsedConfig;
} }
public async initServer() {
if (!this.rawConfig) {
throw new Error("Config not loaded. Call load() first.");
}
license.setServerSecret(this.rawConfig.server.secret);
await this.checkKeyStatus();
}
private async checkKeyStatus() { private async checkKeyStatus() {
const licenseStatus = await license.check(); const licenseStatus = await license.check();
if (!licenseStatus.isHostLicensed) { if (
!licenseStatus.isHostLicensed
) {
this.checkSupporterKey(); this.checkSupporterKey();
} }
} }
@ -116,6 +127,9 @@ export class Config {
} }
public getDomain(domainId: string) { public getDomain(domainId: string) {
if (!this.rawConfig.domains || !this.rawConfig.domains[domainId]) {
return null;
}
return this.rawConfig.domains[domainId]; return this.rawConfig.domains[domainId];
} }

View file

@ -2,7 +2,7 @@ import path from "path";
import { fileURLToPath } from "url"; import { fileURLToPath } from "url";
// This is a placeholder value replaced by the build process // This is a placeholder value replaced by the build process
export const APP_VERSION = "1.6.0"; export const APP_VERSION = "1.7.0";
export const __FILENAME = fileURLToPath(import.meta.url); export const __FILENAME = fileURLToPath(import.meta.url);
export const __DIRNAME = path.dirname(__FILENAME); export const __DIRNAME = path.dirname(__FILENAME);

View file

@ -4,7 +4,14 @@ import { assertEquals } from "@test/assert";
// Test cases // Test cases
function testFindNextAvailableCidr() { function testFindNextAvailableCidr() {
console.log("Running findNextAvailableCidr tests..."); console.log("Running findNextAvailableCidr tests...");
// Test 0: Basic IPv4 allocation with a subnet in the wrong range
{
const existing = ["100.90.130.1/30", "100.90.128.4/30"];
const result = findNextAvailableCidr(existing, 30, "100.90.130.1/24");
assertEquals(result, "100.90.130.4/30", "Basic IPv4 allocation failed");
}
// Test 1: Basic IPv4 allocation // Test 1: Basic IPv4 allocation
{ {
const existing = ["10.0.0.0/16", "10.1.0.0/16"]; const existing = ["10.0.0.0/16", "10.1.0.0/16"];
@ -26,6 +33,12 @@ function testFindNextAvailableCidr() {
assertEquals(result, null, "No available space test failed"); assertEquals(result, null, "No available space test failed");
} }
// Test 4: Empty existing
{
const existing: string[] = [];
const result = findNextAvailableCidr(existing, 30, "10.0.0.0/8");
assertEquals(result, "10.0.0.0/30", "Empty existing test failed");
}
// // Test 4: IPv6 allocation // // Test 4: IPv6 allocation
// { // {
// const existing = ["2001:db8::/32", "2001:db8:1::/32"]; // const existing = ["2001:db8::/32", "2001:db8:1::/32"];

View file

@ -1,3 +1,8 @@
import { db } from "@server/db";
import { clients, orgs, sites } from "@server/db";
import { and, eq, isNotNull } from "drizzle-orm";
import config from "@server/lib/config";
interface IPRange { interface IPRange {
start: bigint; start: bigint;
end: bigint; end: bigint;
@ -9,7 +14,7 @@ type IPVersion = 4 | 6;
* Detects IP version from address string * Detects IP version from address string
*/ */
function detectIpVersion(ip: string): IPVersion { function detectIpVersion(ip: string): IPVersion {
return ip.includes(':') ? 6 : 4; return ip.includes(":") ? 6 : 4;
} }
/** /**
@ -19,34 +24,34 @@ function ipToBigInt(ip: string): bigint {
const version = detectIpVersion(ip); const version = detectIpVersion(ip);
if (version === 4) { if (version === 4) {
return ip.split('.') return ip.split(".").reduce((acc, octet) => {
.reduce((acc, octet) => { const num = parseInt(octet);
const num = parseInt(octet); if (isNaN(num) || num < 0 || num > 255) {
if (isNaN(num) || num < 0 || num > 255) { throw new Error(`Invalid IPv4 octet: ${octet}`);
throw new Error(`Invalid IPv4 octet: ${octet}`); }
} return BigInt.asUintN(64, (acc << BigInt(8)) + BigInt(num));
return BigInt.asUintN(64, (acc << BigInt(8)) + BigInt(num)); }, BigInt(0));
}, BigInt(0));
} else { } else {
// Handle IPv6 // Handle IPv6
// Expand :: notation // Expand :: notation
let fullAddress = ip; let fullAddress = ip;
if (ip.includes('::')) { if (ip.includes("::")) {
const parts = ip.split('::'); const parts = ip.split("::");
if (parts.length > 2) throw new Error('Invalid IPv6 address: multiple :: found'); if (parts.length > 2)
const missing = 8 - (parts[0].split(':').length + parts[1].split(':').length); throw new Error("Invalid IPv6 address: multiple :: found");
const padding = Array(missing).fill('0').join(':'); const missing =
8 - (parts[0].split(":").length + parts[1].split(":").length);
const padding = Array(missing).fill("0").join(":");
fullAddress = `${parts[0]}:${padding}:${parts[1]}`; fullAddress = `${parts[0]}:${padding}:${parts[1]}`;
} }
return fullAddress.split(':') return fullAddress.split(":").reduce((acc, hextet) => {
.reduce((acc, hextet) => { const num = parseInt(hextet || "0", 16);
const num = parseInt(hextet || '0', 16); if (isNaN(num) || num < 0 || num > 65535) {
if (isNaN(num) || num < 0 || num > 65535) { throw new Error(`Invalid IPv6 hextet: ${hextet}`);
throw new Error(`Invalid IPv6 hextet: ${hextet}`); }
} return BigInt.asUintN(128, (acc << BigInt(16)) + BigInt(num));
return BigInt.asUintN(128, (acc << BigInt(16)) + BigInt(num)); }, BigInt(0));
}, BigInt(0));
} }
} }
@ -60,11 +65,15 @@ function bigIntToIp(num: bigint, version: IPVersion): string {
octets.unshift(Number(num & BigInt(255))); octets.unshift(Number(num & BigInt(255)));
num = num >> BigInt(8); num = num >> BigInt(8);
} }
return octets.join('.'); return octets.join(".");
} else { } else {
const hextets: string[] = []; const hextets: string[] = [];
for (let i = 0; i < 8; i++) { for (let i = 0; i < 8; i++) {
hextets.unshift(Number(num & BigInt(65535)).toString(16).padStart(4, '0')); hextets.unshift(
Number(num & BigInt(65535))
.toString(16)
.padStart(4, "0")
);
num = num >> BigInt(16); num = num >> BigInt(16);
} }
// Compress zero sequences // Compress zero sequences
@ -74,7 +83,7 @@ function bigIntToIp(num: bigint, version: IPVersion): string {
let currentZeroLength = 0; let currentZeroLength = 0;
for (let i = 0; i < hextets.length; i++) { for (let i = 0; i < hextets.length; i++) {
if (hextets[i] === '0000') { if (hextets[i] === "0000") {
if (currentZeroStart === -1) currentZeroStart = i; if (currentZeroStart === -1) currentZeroStart = i;
currentZeroLength++; currentZeroLength++;
if (currentZeroLength > maxZeroLength) { if (currentZeroLength > maxZeroLength) {
@ -88,12 +97,14 @@ function bigIntToIp(num: bigint, version: IPVersion): string {
} }
if (maxZeroLength > 1) { if (maxZeroLength > 1) {
hextets.splice(maxZeroStart, maxZeroLength, ''); hextets.splice(maxZeroStart, maxZeroLength, "");
if (maxZeroStart === 0) hextets.unshift(''); if (maxZeroStart === 0) hextets.unshift("");
if (maxZeroStart + maxZeroLength === 8) hextets.push(''); if (maxZeroStart + maxZeroLength === 8) hextets.push("");
} }
return hextets.map(h => h === '0000' ? '0' : h.replace(/^0+/, '')).join(':'); return hextets
.map((h) => (h === "0000" ? "0" : h.replace(/^0+/, "")))
.join(":");
} }
} }
@ -101,7 +112,7 @@ function bigIntToIp(num: bigint, version: IPVersion): string {
* Converts CIDR to IP range * Converts CIDR to IP range
*/ */
export function cidrToRange(cidr: string): IPRange { export function cidrToRange(cidr: string): IPRange {
const [ip, prefix] = cidr.split('/'); const [ip, prefix] = cidr.split("/");
const version = detectIpVersion(ip); const version = detectIpVersion(ip);
const prefixBits = parseInt(prefix); const prefixBits = parseInt(prefix);
const ipBigInt = ipToBigInt(ip); const ipBigInt = ipToBigInt(ip);
@ -113,7 +124,10 @@ export function cidrToRange(cidr: string): IPRange {
} }
const shiftBits = BigInt(maxPrefix - prefixBits); const shiftBits = BigInt(maxPrefix - prefixBits);
const mask = BigInt.asUintN(version === 4 ? 64 : 128, (BigInt(1) << shiftBits) - BigInt(1)); const mask = BigInt.asUintN(
version === 4 ? 64 : 128,
(BigInt(1) << shiftBits) - BigInt(1)
);
const start = ipBigInt & ~mask; const start = ipBigInt & ~mask;
const end = start | mask; const end = start | mask;
@ -132,28 +146,32 @@ export function findNextAvailableCidr(
blockSize: number, blockSize: number,
startCidr?: string startCidr?: string
): string | null { ): string | null {
if (!startCidr && existingCidrs.length === 0) { if (!startCidr && existingCidrs.length === 0) {
return null; return null;
} }
// If no existing CIDRs, use the IP version from startCidr // If no existing CIDRs, use the IP version from startCidr
const version = startCidr const version = startCidr ? detectIpVersion(startCidr.split("/")[0]) : 4; // Default to IPv4 if no startCidr provided
? detectIpVersion(startCidr.split('/')[0])
: 4; // Default to IPv4 if no startCidr provided
// Use appropriate default startCidr if none provided // Use appropriate default startCidr if none provided
startCidr = startCidr || (version === 4 ? "0.0.0.0/0" : "::/0"); startCidr = startCidr || (version === 4 ? "0.0.0.0/0" : "::/0");
// If there are existing CIDRs, ensure all are same version // If there are existing CIDRs, ensure all are same version
if (existingCidrs.length > 0 && if (
existingCidrs.some(cidr => detectIpVersion(cidr.split('/')[0]) !== version)) { existingCidrs.length > 0 &&
throw new Error('All CIDRs must be of the same IP version'); existingCidrs.some(
(cidr) => detectIpVersion(cidr.split("/")[0]) !== version
)
) {
throw new Error("All CIDRs must be of the same IP version");
} }
// Extract the network part from startCidr to ensure we stay in the right subnet
const startCidrRange = cidrToRange(startCidr);
// Convert existing CIDRs to ranges and sort them // Convert existing CIDRs to ranges and sort them
const existingRanges = existingCidrs const existingRanges = existingCidrs
.map(cidr => cidrToRange(cidr)) .map((cidr) => cidrToRange(cidr))
.sort((a, b) => (a.start < b.start ? -1 : 1)); .sort((a, b) => (a.start < b.start ? -1 : 1));
// Calculate block size // Calculate block size
@ -161,14 +179,17 @@ export function findNextAvailableCidr(
const blockSizeBigInt = BigInt(1) << BigInt(maxPrefix - blockSize); const blockSizeBigInt = BigInt(1) << BigInt(maxPrefix - blockSize);
// Start from the beginning of the given CIDR // Start from the beginning of the given CIDR
let current = cidrToRange(startCidr).start; let current = startCidrRange.start;
const maxIp = cidrToRange(startCidr).end; const maxIp = startCidrRange.end;
// Iterate through existing ranges // Iterate through existing ranges
for (let i = 0; i <= existingRanges.length; i++) { for (let i = 0; i <= existingRanges.length; i++) {
const nextRange = existingRanges[i]; const nextRange = existingRanges[i];
// Align current to block size // Align current to block size
const alignedCurrent = current + ((blockSizeBigInt - (current % blockSizeBigInt)) % blockSizeBigInt); const alignedCurrent =
current +
((blockSizeBigInt - (current % blockSizeBigInt)) % blockSizeBigInt);
// Check if we've gone beyond the maximum allowed IP // Check if we've gone beyond the maximum allowed IP
if (alignedCurrent + blockSizeBigInt - BigInt(1) > maxIp) { if (alignedCurrent + blockSizeBigInt - BigInt(1) > maxIp) {
@ -176,12 +197,18 @@ export function findNextAvailableCidr(
} }
// If we're at the end of existing ranges or found a gap // If we're at the end of existing ranges or found a gap
if (!nextRange || alignedCurrent + blockSizeBigInt - BigInt(1) < nextRange.start) { if (
!nextRange ||
alignedCurrent + blockSizeBigInt - BigInt(1) < nextRange.start
) {
return `${bigIntToIp(alignedCurrent, version)}/${blockSize}`; return `${bigIntToIp(alignedCurrent, version)}/${blockSize}`;
} }
// Move current pointer to after the current range // If next range overlaps with our search space, move past it
current = nextRange.end + BigInt(1); if (nextRange.end >= startCidrRange.start && nextRange.start <= maxIp) {
// Move current pointer to after the current range
current = nextRange.end + BigInt(1);
}
} }
return null; return null;
@ -195,7 +222,7 @@ export function findNextAvailableCidr(
*/ */
export function isIpInCidr(ip: string, cidr: string): boolean { export function isIpInCidr(ip: string, cidr: string): boolean {
const ipVersion = detectIpVersion(ip); const ipVersion = detectIpVersion(ip);
const cidrVersion = detectIpVersion(cidr.split('/')[0]); const cidrVersion = detectIpVersion(cidr.split("/")[0]);
// If IP versions don't match, the IP cannot be in the CIDR range // If IP versions don't match, the IP cannot be in the CIDR range
if (ipVersion !== cidrVersion) { if (ipVersion !== cidrVersion) {
@ -207,3 +234,69 @@ export function isIpInCidr(ip: string, cidr: string): boolean {
const range = cidrToRange(cidr); const range = cidrToRange(cidr);
return ipBigInt >= range.start && ipBigInt <= range.end; return ipBigInt >= range.start && ipBigInt <= range.end;
} }
export async function getNextAvailableClientSubnet(
orgId: string
): Promise<string> {
const [org] = await db.select().from(orgs).where(eq(orgs.orgId, orgId));
if (!org) {
throw new Error(`Organization with ID ${orgId} not found`);
}
if (!org.subnet) {
throw new Error(`Organization with ID ${orgId} has no subnet defined`);
}
const existingAddressesSites = await db
.select({
address: sites.address
})
.from(sites)
.where(and(isNotNull(sites.address), eq(sites.orgId, orgId)));
const existingAddressesClients = await db
.select({
address: clients.subnet
})
.from(clients)
.where(and(isNotNull(clients.subnet), eq(clients.orgId, orgId)));
const addresses = [
...existingAddressesSites.map(
(site) => `${site.address?.split("/")[0]}/32`
), // we are overriding the 32 so that we pick individual addresses in the subnet of the org for the site and the client even though they are stored with the /block_size of the org
...existingAddressesClients.map(
(client) => `${client.address.split("/")}/32`
)
].filter((address) => address !== null) as string[];
let subnet = findNextAvailableCidr(addresses, 32, org.subnet); // pick the sites address in the org
if (!subnet) {
throw new Error("No available subnets remaining in space");
}
return subnet;
}
export async function getNextAvailableOrgSubnet(): Promise<string> {
const existingAddresses = await db
.select({
subnet: orgs.subnet
})
.from(orgs)
.where(isNotNull(orgs.subnet));
const addresses = existingAddresses.map((org) => org.subnet!);
let subnet = findNextAvailableCidr(
addresses,
config.getRawConfig().orgs.block_size,
config.getRawConfig().orgs.subnet_group
);
if (!subnet) {
throw new Error("No available subnets remaining in space");
}
return subnet;
}

View file

@ -0,0 +1,6 @@
import { MemoryStore, Store } from "express-rate-limit";
export function createStore(): Store {
let rateLimitStore: Store = new MemoryStore();
return rateLimitStore;
}

View file

@ -3,8 +3,7 @@ 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 { passwordSchema } from "@server/auth/passwordSchema"; import { build } from "@server/build";
import { fromError } from "zod-validation-error";
const portSchema = z.number().positive().gt(0).lte(65535); const portSchema = z.number().positive().gt(0).lte(65535);
@ -12,203 +11,243 @@ const getEnvOrYaml = (envVar: string) => (valFromYaml: any) => {
return process.env[envVar] ?? valFromYaml; return process.env[envVar] ?? valFromYaml;
}; };
export const configSchema = z.object({ export const configSchema = z
app: z.object({ .object({
dashboard_url: z app: z.object({
.string() dashboard_url: z
.url() .string()
.optional() .url()
.pipe(z.string().url()) .optional()
.transform((url) => url.toLowerCase()), .pipe(z.string().url())
log_level: z .transform((url) => url.toLowerCase()),
.enum(["debug", "info", "warn", "error"]) log_level: z
.optional() .enum(["debug", "info", "warn", "error"])
.default("info"), .optional()
save_logs: z.boolean().optional().default(false), .default("info"),
log_failed_attempts: z.boolean().optional().default(false) save_logs: z.boolean().optional().default(false),
}), log_failed_attempts: z.boolean().optional().default(false)
domains: z }),
.record( domains: z
z.string(), .record(
z.object({ z.string(),
base_domain: z z.object({
.string() base_domain: z
.nonempty("base_domain must not be empty") .string()
.transform((url) => url.toLowerCase()), .nonempty("base_domain must not be empty")
cert_resolver: z.string().optional().default("letsencrypt"), .transform((url) => url.toLowerCase()),
prefer_wildcard_cert: z.boolean().optional().default(false) cert_resolver: z.string().optional().default("letsencrypt"),
}) prefer_wildcard_cert: z.boolean().optional().default(false)
) })
.refine( )
(domains) => {
const keys = Object.keys(domains);
if (keys.length === 0) {
return false;
}
return true;
},
{
message: "At least one domain must be defined"
}
),
server: z.object({
integration_port: portSchema
.optional()
.default(3003)
.transform(stoi)
.pipe(portSchema.optional()),
external_port: portSchema
.optional()
.default(3000)
.transform(stoi)
.pipe(portSchema),
internal_port: portSchema
.optional()
.default(3001)
.transform(stoi)
.pipe(portSchema),
next_port: portSchema
.optional()
.default(3002)
.transform(stoi)
.pipe(portSchema),
internal_hostname: z
.string()
.optional()
.default("pangolin")
.transform((url) => url.toLowerCase()),
session_cookie_name: z.string().optional().default("p_session_token"),
resource_access_token_param: z.string().optional().default("p_token"),
resource_access_token_headers: z
.object({
id: z.string().optional().default("P-Access-Token-Id"),
token: z.string().optional().default("P-Access-Token")
})
.optional()
.default({}),
resource_session_request_param: z
.string()
.optional()
.default("resource_session_request_param"),
dashboard_session_length_hours: z
.number()
.positive()
.gt(0)
.optional()
.default(720),
resource_session_length_hours: z
.number()
.positive()
.gt(0)
.optional()
.default(720),
cors: z
.object({
origins: z.array(z.string()).optional(),
methods: z.array(z.string()).optional(),
allowed_headers: z.array(z.string()).optional(),
credentials: z.boolean().optional()
})
.optional(), .optional(),
trust_proxy: z.number().int().gte(0).optional().default(1), server: z.object({
secret: z integration_port: portSchema
.string()
.optional()
.transform(getEnvOrYaml("SERVER_SECRET"))
.pipe(z.string().min(8))
}),
postgres: z
.object({
connection_string: z.string(),
replicas: z
.array(
z.object({
connection_string: z.string()
})
)
.optional() .optional()
}) .default(3003)
.optional(), .transform(stoi)
traefik: z .pipe(portSchema.optional()),
.object({ external_port: portSchema
http_entrypoint: z.string().optional().default("web"),
https_entrypoint: z.string().optional().default("websecure"),
additional_middlewares: z.array(z.string()).optional()
})
.optional()
.default({}),
gerbil: z
.object({
start_port: portSchema
.optional() .optional()
.default(51820) .default(3000)
.transform(stoi) .transform(stoi)
.pipe(portSchema), .pipe(portSchema),
base_endpoint: z internal_port: portSchema
.optional()
.default(3001)
.transform(stoi)
.pipe(portSchema),
next_port: portSchema
.optional()
.default(3002)
.transform(stoi)
.pipe(portSchema),
internal_hostname: z
.string() .string()
.optional() .optional()
.pipe(z.string()) .default("pangolin")
.transform((url) => url.toLowerCase()), .transform((url) => url.toLowerCase()),
use_subdomain: z.boolean().optional().default(false), session_cookie_name: z
subnet_group: z.string().optional().default("100.89.137.0/20"), .string()
block_size: z.number().positive().gt(0).optional().default(24), .optional()
site_block_size: z.number().positive().gt(0).optional().default(30) .default("p_session_token"),
}) resource_access_token_param: z
.optional() .string()
.default({}), .optional()
rate_limits: z .default("p_token"),
.object({ resource_access_token_headers: z
global: z
.object({ .object({
window_minutes: z id: z.string().optional().default("P-Access-Token-Id"),
.number() token: z.string().optional().default("P-Access-Token")
.positive()
.gt(0)
.optional()
.default(1),
max_requests: z
.number()
.positive()
.gt(0)
.optional()
.default(500)
}) })
.optional() .optional()
.default({}), .default({}),
auth: z resource_session_request_param: z
.object({ .string()
window_minutes: z.number().positive().gt(0),
max_requests: z.number().positive().gt(0)
})
.optional() .optional()
}) .default("resource_session_request_param"),
.optional() dashboard_session_length_hours: z
.default({}), .number()
email: z .positive()
.object({ .gt(0)
smtp_host: z.string().optional(), .optional()
smtp_port: portSchema.optional(), .default(720),
smtp_user: z.string().optional(), resource_session_length_hours: z
smtp_pass: z.string().optional(), .number()
smtp_secure: z.boolean().optional(), .positive()
smtp_tls_reject_unauthorized: z.boolean().optional(), .gt(0)
no_reply: z.string().email().optional() .optional()
}) .default(720),
.optional(), cors: z
flags: z .object({
.object({ origins: z.array(z.string()).optional(),
require_email_verification: z.boolean().optional(), methods: z.array(z.string()).optional(),
disable_signup_without_invite: z.boolean().optional(), allowed_headers: z.array(z.string()).optional(),
disable_user_create_org: z.boolean().optional(), credentials: z.boolean().optional()
allow_raw_resources: z.boolean().optional(), })
allow_base_domain_resources: z.boolean().optional(), .optional(),
allow_local_sites: z.boolean().optional(), trust_proxy: z.number().int().gte(0).optional().default(1),
enable_integration_api: z.boolean().optional() secret: z
}) .string()
.optional() .optional()
}); .transform(getEnvOrYaml("SERVER_SECRET"))
.pipe(z.string().min(8))
}),
postgres: z
.object({
connection_string: z.string(),
replicas: z
.array(
z.object({
connection_string: z.string()
})
)
.optional()
})
.optional(),
traefik: z
.object({
http_entrypoint: z.string().optional().default("web"),
https_entrypoint: z.string().optional().default("websecure"),
additional_middlewares: z.array(z.string()).optional(),
cert_resolver: z.string().optional().default("letsencrypt"),
prefer_wildcard_cert: z.boolean().optional().default(false)
})
.optional()
.default({}),
gerbil: z
.object({
exit_node_name: z.string().optional(),
start_port: portSchema
.optional()
.default(51820)
.transform(stoi)
.pipe(portSchema),
base_endpoint: z
.string()
.optional()
.pipe(z.string())
.transform((url) => url.toLowerCase()),
use_subdomain: z.boolean().optional().default(false),
subnet_group: z.string().optional().default("100.89.137.0/20"),
block_size: z.number().positive().gt(0).optional().default(24),
site_block_size: z
.number()
.positive()
.gt(0)
.optional()
.default(30)
})
.optional()
.default({}),
orgs: z
.object({
block_size: z.number().positive().gt(0).optional().default(24),
subnet_group: z.string().optional().default("100.90.128.0/24")
})
.optional()
.default({
block_size: 24,
subnet_group: "100.90.128.0/24"
}),
rate_limits: z
.object({
global: z
.object({
window_minutes: z
.number()
.positive()
.gt(0)
.optional()
.default(1),
max_requests: z
.number()
.positive()
.gt(0)
.optional()
.default(500)
})
.optional()
.default({}),
auth: z
.object({
window_minutes: z
.number()
.positive()
.gt(0)
.optional()
.default(1),
max_requests: z
.number()
.positive()
.gt(0)
.optional()
.default(500)
})
.optional()
.default({})
})
.optional()
.default({}),
email: z
.object({
smtp_host: z.string().optional(),
smtp_port: portSchema.optional(),
smtp_user: z.string().optional(),
smtp_pass: z.string().optional(),
smtp_secure: z.boolean().optional(),
smtp_tls_reject_unauthorized: z.boolean().optional(),
no_reply: z.string().email().optional()
})
.optional(),
flags: z
.object({
require_email_verification: z.boolean().optional(),
disable_signup_without_invite: z.boolean().optional(),
disable_user_create_org: z.boolean().optional(),
allow_raw_resources: z.boolean().optional(),
enable_integration_api: z.boolean().optional(),
disable_local_sites: z.boolean().optional(),
disable_basic_wireguard_sites: z.boolean().optional(),
disable_config_managed_domains: z.boolean().optional(),
enable_clients: z.boolean().optional()
})
.optional()
})
.refine(
(data) => {
const keys = Object.keys(data.domains || {});
if (data.flags?.disable_config_managed_domains) {
return true;
}
if (keys.length === 0) {
return false;
}
return true;
},
{
message: "At least one domain must be defined"
}
);
export function readConfigFile() { export function readConfigFile() {
const loadConfig = (configPath: string) => { const loadConfig = (configPath: string) => {

File diff suppressed because it is too large Load diff

View file

@ -1,5 +1,4 @@
export * from "./notFound"; export * from "./notFound";
export * from "./rateLimit";
export * from "./formatError"; export * from "./formatError";
export * from "./verifySession"; export * from "./verifySession";
export * from "./verifyUser"; export * from "./verifyUser";
@ -14,9 +13,17 @@ export * from "./verifyAdmin";
export * from "./verifySetResourceUsers"; export * from "./verifySetResourceUsers";
export * from "./verifyUserInRole"; export * from "./verifyUserInRole";
export * from "./verifyAccessTokenAccess"; export * from "./verifyAccessTokenAccess";
export * from "./requestTimeout";
export * from "./verifyClientAccess";
export * from "./verifyUserHasAction";
export * from "./verifyUserIsServerAdmin"; export * from "./verifyUserIsServerAdmin";
export * from "./verifyIsLoggedInUser"; export * from "./verifyIsLoggedInUser";
export * from "./verifyIsLoggedInUser";
export * from "./verifyClientAccess";
export * from "./integration"; export * from "./integration";
export * from "./verifyValidLicense"; export * from "./verifyValidLicense";
export * from "./verifyUserHasAction"; export * from "./verifyUserHasAction";
export * from "./verifyApiKeyAccess"; export * from "./verifyApiKeyAccess";
export * from "./verifyDomainAccess";
export * from "./verifyClientsEnabled";
export * from "./verifyUserIsOrgOwner";

View file

@ -1,49 +0,0 @@
import { rateLimit } from "express-rate-limit";
import createHttpError from "http-errors";
import { NextFunction, Request, Response } from "express";
import logger from "@server/logger";
import HttpCode from "@server/types/HttpCode";
export function rateLimitMiddleware({
windowMin,
max,
type,
skipCondition,
}: {
windowMin: number;
max: number;
type: "IP_ONLY" | "IP_AND_PATH";
skipCondition?: (req: Request, res: Response) => boolean;
}) {
if (type === "IP_AND_PATH") {
return rateLimit({
windowMs: windowMin * 60 * 1000,
max,
skip: skipCondition,
keyGenerator: (req: Request) => {
return `${req.ip}-${req.path}`;
},
handler: (req: Request, res: Response, next: NextFunction) => {
const message = `Rate limit exceeded. You can make ${max} requests every ${windowMin} minute(s).`;
logger.warn(
`Rate limit exceeded for IP ${req.ip} on path ${req.path}`,
);
return next(
createHttpError(HttpCode.TOO_MANY_REQUESTS, message),
);
},
});
}
return rateLimit({
windowMs: windowMin * 60 * 1000,
max,
skip: skipCondition,
handler: (req: Request, res: Response, next: NextFunction) => {
const message = `Rate limit exceeded. You can make ${max} requests every ${windowMin} minute(s).`;
logger.warn(`Rate limit exceeded for IP ${req.ip}`);
return next(createHttpError(HttpCode.TOO_MANY_REQUESTS, message));
},
});
}
export default rateLimitMiddleware;

View file

@ -0,0 +1,35 @@
import { Request, Response, NextFunction } from 'express';
import logger from '@server/logger';
import createHttpError from 'http-errors';
import HttpCode from '@server/types/HttpCode';
export function requestTimeoutMiddleware(timeoutMs: number = 30000) {
return (req: Request, res: Response, next: NextFunction) => {
// Set a timeout for the request
const timeout = setTimeout(() => {
if (!res.headersSent) {
logger.error(`Request timeout: ${req.method} ${req.url} from ${req.ip}`);
return next(
createHttpError(
HttpCode.REQUEST_TIMEOUT,
'Request timeout - operation took too long to complete'
)
);
}
}, timeoutMs);
// Clear timeout when response finishes
res.on('finish', () => {
clearTimeout(timeout);
});
// Clear timeout when response closes
res.on('close', () => {
clearTimeout(timeout);
});
next();
};
}
export default requestTimeoutMiddleware;

View file

@ -0,0 +1,131 @@
import { Request, Response, NextFunction } from "express";
import { db } from "@server/db";
import { userOrgs, clients, roleClients, userClients } from "@server/db";
import { and, eq } from "drizzle-orm";
import createHttpError from "http-errors";
import HttpCode from "@server/types/HttpCode";
export async function verifyClientAccess(
req: Request,
res: Response,
next: NextFunction
) {
const userId = req.user!.userId; // Assuming you have user information in the request
const clientId = parseInt(
req.params.clientId || req.body.clientId || req.query.clientId
);
if (!userId) {
return next(
createHttpError(HttpCode.UNAUTHORIZED, "User not authenticated")
);
}
if (isNaN(clientId)) {
return next(createHttpError(HttpCode.BAD_REQUEST, "Invalid client ID"));
}
try {
// Get the client
const [client] = await db
.select()
.from(clients)
.where(eq(clients.clientId, clientId))
.limit(1);
if (!client) {
return next(
createHttpError(
HttpCode.NOT_FOUND,
`Client with ID ${clientId} not found`
)
);
}
if (!client.orgId) {
return next(
createHttpError(
HttpCode.INTERNAL_SERVER_ERROR,
`Client with ID ${clientId} does not have an organization ID`
)
);
}
if (!req.userOrg) {
// Get user's role ID in the organization
const userOrgRole = await db
.select()
.from(userOrgs)
.where(
and(
eq(userOrgs.userId, userId),
eq(userOrgs.orgId, client.orgId)
)
)
.limit(1);
req.userOrg = userOrgRole[0];
}
if (!req.userOrg) {
return next(
createHttpError(
HttpCode.FORBIDDEN,
"User does not have access to this organization"
)
);
}
const userOrgRoleId = req.userOrg.roleId;
req.userOrgRoleId = userOrgRoleId;
req.userOrgId = client.orgId;
// Check role-based site access first
const [roleClientAccess] = await db
.select()
.from(roleClients)
.where(
and(
eq(roleClients.clientId, clientId),
eq(roleClients.roleId, userOrgRoleId)
)
)
.limit(1);
if (roleClientAccess) {
// User has access to the site through their role
return next();
}
// If role doesn't have access, check user-specific site access
const [userClientAccess] = await db
.select()
.from(userClients)
.where(
and(
eq(userClients.userId, userId),
eq(userClients.clientId, clientId)
)
)
.limit(1);
if (userClientAccess) {
// User has direct access to the site
return next();
}
// If we reach here, the user doesn't have access to the site
return next(
createHttpError(
HttpCode.FORBIDDEN,
"User does not have access to this client"
)
);
} catch (error) {
return next(
createHttpError(
HttpCode.INTERNAL_SERVER_ERROR,
"Error verifying site access"
)
);
}
}

View file

@ -0,0 +1,29 @@
import { Request, Response, NextFunction } from "express";
import createHttpError from "http-errors";
import HttpCode from "@server/types/HttpCode";
import config from "@server/lib/config";
export async function verifyClientsEnabled(
req: Request,
res: Response,
next: NextFunction
) {
try {
if (!config.getRawConfig().flags?.enable_clients) {
return next(
createHttpError(
HttpCode.NOT_IMPLEMENTED,
"Clients are not enabled on this server."
)
);
}
return next();
} catch (error) {
return next(
createHttpError(
HttpCode.INTERNAL_SERVER_ERROR,
"Failed to check if clients are enabled"
)
);
}
}

View file

@ -0,0 +1,93 @@
import { Request, Response, NextFunction } from "express";
import { db, domains, orgDomains } from "@server/db";
import { userOrgs, apiKeyOrg } from "@server/db";
import { and, eq } from "drizzle-orm";
import createHttpError from "http-errors";
import HttpCode from "@server/types/HttpCode";
export async function verifyDomainAccess(
req: Request,
res: Response,
next: NextFunction
) {
try {
const userId = req.user!.userId;
const domainId =
req.params.domainId || req.body.apiKeyId || req.query.apiKeyId;
const orgId = req.params.orgId;
if (!userId) {
return next(
createHttpError(HttpCode.UNAUTHORIZED, "User not authenticated")
);
}
if (!orgId) {
return next(
createHttpError(HttpCode.BAD_REQUEST, "Invalid organization ID")
);
}
if (!domainId) {
return next(
createHttpError(HttpCode.BAD_REQUEST, "Invalid domain ID")
);
}
const [domain] = await db
.select()
.from(domains)
.innerJoin(orgDomains, eq(orgDomains.domainId, domains.domainId))
.where(
and(
eq(orgDomains.domainId, domainId),
eq(orgDomains.orgId, orgId)
)
)
.limit(1);
if (!domain.orgDomains) {
return next(
createHttpError(
HttpCode.NOT_FOUND,
`Domain with ID ${domainId} not found`
)
);
}
if (!req.userOrg) {
const userOrgRole = await db
.select()
.from(userOrgs)
.where(
and(
eq(userOrgs.userId, userId),
eq(userOrgs.orgId, apiKeyOrg.orgId)
)
)
.limit(1);
req.userOrg = userOrgRole[0];
}
if (!req.userOrg) {
return next(
createHttpError(
HttpCode.FORBIDDEN,
"User does not have access to this organization"
)
);
}
const userOrgRoleId = req.userOrg.roleId;
req.userOrgRoleId = userOrgRoleId;
return next();
} catch (error) {
return next(
createHttpError(
HttpCode.INTERNAL_SERVER_ERROR,
"Error verifying domain access"
)
);
}
}

View file

@ -14,5 +14,6 @@ export enum OpenAPITags {
AccessToken = "Access Token", AccessToken = "Access Token",
Idp = "Identity Provider", Idp = "Identity Provider",
Client = "Client", Client = "Client",
ApiKey = "API Key" ApiKey = "API Key",
Domain = "Domain"
} }

View file

@ -112,7 +112,11 @@ export async function requestTotpSecret(
const hex = crypto.getRandomValues(new Uint8Array(20)); const hex = crypto.getRandomValues(new Uint8Array(20));
const secret = encodeHex(hex); const secret = encodeHex(hex);
const uri = createTOTPKeyURI("Pangolin", user.email!, hex); const uri = createTOTPKeyURI(
"Pangolin",
user.email!,
hex
);
await db await db
.update(users) .update(users)

View file

@ -107,7 +107,8 @@ async function clearChallenge(sessionId: string) {
export const registerSecurityKeyBody = z.object({ export const registerSecurityKeyBody = z.object({
name: z.string().min(1), name: z.string().min(1),
password: z.string().min(1) password: z.string().min(1),
code: z.string().optional()
}).strict(); }).strict();
export const verifyRegistrationBody = z.object({ export const verifyRegistrationBody = z.object({
@ -143,7 +144,7 @@ export async function startRegistration(
); );
} }
const { name, password } = parsedBody.data; const { name, password, code } = parsedBody.data;
const user = req.user as User; const user = req.user as User;
// Only allow internal users to use security keys // Only allow internal users to use security keys
@ -163,6 +164,39 @@ export async function startRegistration(
return next(unauthorized()); return next(unauthorized());
} }
// If user has 2FA enabled, require and verify the code
if (user.twoFactorEnabled) {
if (!code) {
return response<{ codeRequested: boolean }>(res, {
data: { codeRequested: true },
success: true,
error: false,
message: "Two-factor authentication required",
status: HttpCode.ACCEPTED
});
}
const validOTP = await verifyTotpCode(
code,
user.twoFactorSecret!,
user.userId
);
if (!validOTP) {
if (config.getRawConfig().app.log_failed_attempts) {
logger.info(
`Two-factor code incorrect. Email: ${user.email}. IP: ${req.ip}.`
);
}
return next(
createHttpError(
HttpCode.UNAUTHORIZED,
"The two-factor code you entered is incorrect"
)
);
}
}
// Get existing security keys for user // Get existing security keys for user
const existingSecurityKeys = await db const existingSecurityKeys = await db
.select() .select()

View file

@ -1,8 +1,7 @@
import { NextFunction, Request, Response } from "express"; import { NextFunction, Request, Response } from "express";
import { db } from "@server/db"; import { db, users } from "@server/db";
import HttpCode from "@server/types/HttpCode"; import HttpCode from "@server/types/HttpCode";
import { z } from "zod"; import { z } from "zod";
import { users } from "@server/db";
import { fromError } from "zod-validation-error"; import { fromError } from "zod-validation-error";
import createHttpError from "http-errors"; import createHttpError from "http-errors";
import response from "@server/lib/response"; import response from "@server/lib/response";
@ -57,9 +56,6 @@ export async function signup(
} }
const { name, email, password, inviteToken, inviteId } = parsedBody.data; const { name, email, password, inviteToken, inviteId } = parsedBody.data;
logger.debug("signup", { name, email, password, inviteToken, inviteId });
const passwordHash = await hashPassword(password); const passwordHash = await hashPassword(password);
const userId = generateId(15); const userId = generateId(15);
@ -144,15 +140,21 @@ export async function signup(
if (diff < 2) { if (diff < 2) {
// If the user was created less than 2 hours ago, we don't want to create a new user // If the user was created less than 2 hours ago, we don't want to create a new user
return response<SignUpResponse>(res, { return next(
data: { createHttpError(
emailVerificationRequired: true HttpCode.BAD_REQUEST,
}, "A user with that email address already exists"
success: true, )
error: false, );
message: `A user with that email address already exists. We sent an email to ${email} with a verification code.`, // return response<SignUpResponse>(res, {
status: HttpCode.OK // data: {
}); // emailVerificationRequired: true
// },
// success: true,
// error: false,
// message: `A user with that email address already exists. We sent an email to ${email} with a verification code.`,
// status: HttpCode.OK
// });
} else { } else {
// If the user was created more than 2 hours ago, we want to delete the old user and create a new one // If the user was created more than 2 hours ago, we want to delete the old user and create a new one
await db.delete(users).where(eq(users.userId, user.userId)); await db.delete(users).where(eq(users.userId, user.userId));

View file

@ -4,7 +4,7 @@ import { z } from "zod";
import { fromError } from "zod-validation-error"; import { fromError } from "zod-validation-error";
import HttpCode from "@server/types/HttpCode"; import HttpCode from "@server/types/HttpCode";
import { response } from "@server/lib"; import { response } from "@server/lib";
import { db } from "@server/db"; import { db, userOrgs } from "@server/db";
import { User, emailVerificationCodes, users } from "@server/db"; import { User, emailVerificationCodes, users } from "@server/db";
import { eq } from "drizzle-orm"; import { eq } from "drizzle-orm";
import { isWithinExpirationDate } from "oslo"; import { isWithinExpirationDate } from "oslo";

View file

@ -75,6 +75,14 @@ export async function verifyTotp(
) )
); );
user = res; user = res;
const validPassword = await verifyPassword(
password,
user.passwordHash!
);
if (!validPassword) {
return next(unauthorized());
}
} }
if (!user) { if (!user) {
@ -90,25 +98,6 @@ export async function verifyTotp(
) )
); );
} }
// Add type guard to ensure password is defined
if (!password) {
return next(
createHttpError(
HttpCode.BAD_REQUEST,
"Password is required for two-factor authentication"
)
);
}
const validPassword = await verifyPassword(
password,
user.passwordHash!
);
if (!validPassword) {
return next(unauthorized());
}
if (user.type !== UserType.Internal) { if (user.type !== UserType.Internal) {
return next( return next(
createHttpError( createHttpError(

View file

@ -0,0 +1,261 @@
import { Request, Response, NextFunction } from "express";
import { z } from "zod";
import { db } from "@server/db";
import {
roles,
Client,
clients,
roleClients,
userClients,
olms,
clientSites,
exitNodes,
orgs,
sites
} from "@server/db";
import response from "@server/lib/response";
import HttpCode from "@server/types/HttpCode";
import createHttpError from "http-errors";
import logger from "@server/logger";
import { eq, and } from "drizzle-orm";
import { fromError } from "zod-validation-error";
import moment from "moment";
import { hashPassword } from "@server/auth/password";
import { isValidCIDR, isValidIP } from "@server/lib/validators";
import { isIpInCidr } from "@server/lib/ip";
import { OpenAPITags, registry } from "@server/openApi";
const createClientParamsSchema = z
.object({
orgId: z.string()
})
.strict();
const createClientSchema = z
.object({
name: z.string().min(1).max(255),
siteIds: z.array(z.number().int().positive()),
olmId: z.string(),
secret: z.string(),
subnet: z.string(),
type: z.enum(["olm"])
})
.strict();
export type CreateClientBody = z.infer<typeof createClientSchema>;
export type CreateClientResponse = Client;
registry.registerPath({
method: "put",
path: "/org/{orgId}/client",
description: "Create a new client.",
tags: [OpenAPITags.Client, OpenAPITags.Org],
request: {
params: createClientParamsSchema,
body: {
content: {
"application/json": {
schema: createClientSchema
}
}
}
},
responses: {}
});
export async function createClient(
req: Request,
res: Response,
next: NextFunction
): Promise<any> {
try {
const parsedBody = createClientSchema.safeParse(req.body);
if (!parsedBody.success) {
return next(
createHttpError(
HttpCode.BAD_REQUEST,
fromError(parsedBody.error).toString()
)
);
}
const { name, type, siteIds, olmId, secret, subnet } = parsedBody.data;
const parsedParams = createClientParamsSchema.safeParse(req.params);
if (!parsedParams.success) {
return next(
createHttpError(
HttpCode.BAD_REQUEST,
fromError(parsedParams.error).toString()
)
);
}
const { orgId } = parsedParams.data;
if (req.user && !req.userOrgRoleId) {
return next(
createHttpError(HttpCode.FORBIDDEN, "User does not have a role")
);
}
if (!isValidIP(subnet)) {
return next(
createHttpError(
HttpCode.BAD_REQUEST,
"Invalid subnet format. Please provide a valid CIDR notation."
)
);
}
const [org] = await db.select().from(orgs).where(eq(orgs.orgId, orgId));
if (!org) {
return next(
createHttpError(
HttpCode.NOT_FOUND,
`Organization with ID ${orgId} not found`
)
);
}
if (!org.subnet) {
return next(
createHttpError(
HttpCode.BAD_REQUEST,
`Organization with ID ${orgId} has no subnet defined`
)
);
}
if (!isIpInCidr(subnet, org.subnet)) {
return next(
createHttpError(
HttpCode.BAD_REQUEST,
"IP is not in the CIDR range of the subnet."
)
);
}
const updatedSubnet = `${subnet}/${org.subnet.split("/")[1]}`; // we want the block size of the whole org
// make sure the subnet is unique
const subnetExistsClients = await db
.select()
.from(clients)
.where(eq(clients.subnet, updatedSubnet))
.limit(1);
if (subnetExistsClients.length > 0) {
return next(
createHttpError(
HttpCode.CONFLICT,
`Subnet ${subnet} already exists`
)
);
}
const subnetExistsSites = await db
.select()
.from(sites)
.where(eq(sites.address, updatedSubnet))
.limit(1);
if (subnetExistsSites.length > 0) {
return next(
createHttpError(
HttpCode.CONFLICT,
`Subnet ${subnet} already exists`
)
);
}
await db.transaction(async (trx) => {
// TODO: more intelligent way to pick the exit node
// make sure there is an exit node by counting the exit nodes table
const nodes = await db.select().from(exitNodes);
if (nodes.length === 0) {
return next(
createHttpError(
HttpCode.NOT_FOUND,
"No exit nodes available"
)
);
}
// get the first exit node
const exitNode = nodes[0];
const adminRole = await trx
.select()
.from(roles)
.where(and(eq(roles.isAdmin, true), eq(roles.orgId, orgId)))
.limit(1);
if (adminRole.length === 0) {
trx.rollback();
return next(
createHttpError(HttpCode.NOT_FOUND, `Admin role not found`)
);
}
const [newClient] = await trx
.insert(clients)
.values({
exitNodeId: exitNode.exitNodeId,
orgId,
name,
subnet: updatedSubnet,
type
})
.returning();
await trx.insert(roleClients).values({
roleId: adminRole[0].roleId,
clientId: newClient.clientId
});
if (req.user && req.userOrgRoleId != adminRole[0].roleId) {
// make sure the user can access the site
trx.insert(userClients).values({
userId: req.user?.userId!,
clientId: newClient.clientId
});
}
// Create site to client associations
if (siteIds && siteIds.length > 0) {
await trx.insert(clientSites).values(
siteIds.map((siteId) => ({
clientId: newClient.clientId,
siteId
}))
);
}
const secretHash = await hashPassword(secret);
await trx.insert(olms).values({
olmId,
secretHash,
clientId: newClient.clientId,
dateCreated: moment().toISOString()
});
return response<CreateClientResponse>(res, {
data: newClient,
success: true,
error: false,
message: "Site created successfully",
status: HttpCode.CREATED
});
});
} catch (error) {
logger.error(error);
return next(
createHttpError(HttpCode.INTERNAL_SERVER_ERROR, "An error occurred")
);
}
}

View file

@ -0,0 +1,88 @@
import { Request, Response, NextFunction } from "express";
import { z } from "zod";
import { db } from "@server/db";
import { clients, clientSites } from "@server/db";
import { eq } 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";
import { OpenAPITags, registry } from "@server/openApi";
const deleteClientSchema = z
.object({
clientId: z.string().transform(Number).pipe(z.number().int().positive())
})
.strict();
registry.registerPath({
method: "delete",
path: "/client/{clientId}",
description: "Delete a client by its client ID.",
tags: [OpenAPITags.Client],
request: {
params: deleteClientSchema
},
responses: {}
});
export async function deleteClient(
req: Request,
res: Response,
next: NextFunction
): Promise<any> {
try {
const parsedParams = deleteClientSchema.safeParse(req.params);
if (!parsedParams.success) {
return next(
createHttpError(
HttpCode.BAD_REQUEST,
fromError(parsedParams.error).toString()
)
);
}
const { clientId } = parsedParams.data;
const [client] = await db
.select()
.from(clients)
.where(eq(clients.clientId, clientId))
.limit(1);
if (!client) {
return next(
createHttpError(
HttpCode.NOT_FOUND,
`Client with ID ${clientId} not found`
)
);
}
await db.transaction(async (trx) => {
// Delete the client-site associations first
await trx
.delete(clientSites)
.where(eq(clientSites.clientId, clientId));
// Then delete the client itself
await trx
.delete(clients)
.where(eq(clients.clientId, clientId));
});
return response(res, {
data: null,
success: true,
error: false,
message: "Client deleted successfully",
status: HttpCode.OK
});
} catch (error) {
logger.error(error);
return next(
createHttpError(HttpCode.INTERNAL_SERVER_ERROR, "An error occurred")
);
}
}

View file

@ -0,0 +1,101 @@
import { Request, Response, NextFunction } from "express";
import { z } from "zod";
import { db } from "@server/db";
import { clients, clientSites } 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 stoi from "@server/lib/stoi";
import { fromError } from "zod-validation-error";
import { OpenAPITags, registry } from "@server/openApi";
const getClientSchema = z
.object({
clientId: z.string().transform(stoi).pipe(z.number().int().positive()),
orgId: z.string().optional()
})
.strict();
async function query(clientId: number) {
// Get the client
const [client] = await db
.select()
.from(clients)
.where(eq(clients.clientId, clientId))
.limit(1);
if (!client) {
return null;
}
// Get the siteIds associated with this client
const sites = await db
.select({ siteId: clientSites.siteId })
.from(clientSites)
.where(eq(clientSites.clientId, clientId));
// Add the siteIds to the client object
return {
...client,
siteIds: sites.map(site => site.siteId)
};
}
export type GetClientResponse = NonNullable<Awaited<ReturnType<typeof query>>>;
registry.registerPath({
method: "get",
path: "/org/{orgId}/client/{clientId}",
description: "Get a client by its client ID.",
tags: [OpenAPITags.Client, OpenAPITags.Org],
request: {
params: getClientSchema
},
responses: {}
});
export async function getClient(
req: Request,
res: Response,
next: NextFunction
): Promise<any> {
try {
const parsedParams = getClientSchema.safeParse(req.params);
if (!parsedParams.success) {
logger.error(
`Error parsing params: ${fromError(parsedParams.error).toString()}`
);
return next(
createHttpError(
HttpCode.BAD_REQUEST,
fromError(parsedParams.error).toString()
)
);
}
const { clientId } = parsedParams.data;
const client = await query(clientId);
if (!client) {
return next(
createHttpError(HttpCode.NOT_FOUND, "Client not found")
);
}
return response<GetClientResponse>(res, {
data: client,
success: true,
error: false,
message: "Client retrieved successfully",
status: HttpCode.OK
});
} catch (error) {
logger.error(error);
return next(
createHttpError(HttpCode.INTERNAL_SERVER_ERROR, "An error occurred")
);
}
}

View file

@ -0,0 +1,6 @@
export * from "./pickClientDefaults";
export * from "./createClient";
export * from "./deleteClient";
export * from "./listClients";
export * from "./updateClient";
export * from "./getClient";

Some files were not shown because too many files have changed in this diff Show more