diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS new file mode 100644 index 000000000..c5f140320 --- /dev/null +++ b/.github/CODEOWNERS @@ -0,0 +1 @@ +* @oschwartz10612 @miloschwartz diff --git a/.github/workflows/cicd.yml b/.github/workflows/cicd.yml index d4cde4ac4..54ae22194 100644 --- a/.github/workflows/cicd.yml +++ b/.github/workflows/cicd.yml @@ -266,7 +266,7 @@ jobs: - name: Install Go uses: actions/setup-go@4b73464bb391d4059bd26b0524d20df3927bd417 # v6.3.0 with: - go-version: 1.24 + go-version: 1.25 - name: Update version in package.json run: | diff --git a/README.md b/README.md index bac7b7e56..f11196f77 100644 --- a/README.md +++ b/README.md @@ -35,43 +35,53 @@ -
-
-
-
-
Get started with Pangolin at app.pangolin.net
-Pangolin is an open-source, identity-based remote access platform built on WireGuard that enables secure, seamless connectivity to private and public resources. Pangolin combines reverse proxy and VPN capabilities into one platform, providing browser-based access to web applications and client-based access to any private resources, all with zero-trust security and granular access control. +Pangolin is an open-source, identity-based remote access platform built on WireGuard that enables secure, seamless connectivity to private and public resources. Pangolin combines reverse proxy and VPN capabilities into one platform, providing browser-based access to web applications and client-based access to any private resources with NAT traversal, all with granular access controls. ## Installation -- Check out the [quick install guide](https://docs.pangolin.net/self-host/quick-install) for how to install and set up Pangolin. -- Install from the [DigitalOcean marketplace](https://marketplace.digitalocean.com/apps/pangolin-ce-1?refcode=edf0480eeb81) for a one-click pre-configured installer. +- Get started for free with [Pangolin Cloud](https://app.pangolin.net/). +- Or, check out the [quick install guide](https://docs.pangolin.net/self-host/quick-install) for how to self-host Pangolin. + - Install from the [DigitalOcean marketplace](https://marketplace.digitalocean.com/apps/pangolin-ce-1?refcode=edf0480eeb81) for a one-click pre-configured installer. -
+
## Deployment Options
-| 



+
+### Browser-based reverse proxy access
+
+Expose web applications through identity and context-aware tunneled reverse proxies. Users access applications through any web browser with authentication and granular access control without installing a client. Pangolin handles routing, load balancing, health checking, and automatic SSL certificates without exposing your network directly to the internet.
+
+
+
+### Client-based private resource access
+
+Access private resources like SSH servers, databases, RDP, and entire network ranges through Pangolin clients. Intelligent NAT traversal enables connections even through restrictive firewalls, while DNS aliases provide friendly names and fast connections to resources across all your sites. Add redundancy by routing traffic through multiple connectors in your network.
+
+
+
+### Give users and roles access to resources
+
+Use Pangolin's built in users or bring your own identity provider and set up role based access control (RBAC). Grant users access to specific resources, not entire networks. Unlike traditional VPNs that expose full network access, Pangolin's zero-trust model ensures users can only reach the applications, services, and routes you explicitly define.
+
+
## Download Clients
@@ -87,7 +97,7 @@ Download the Pangolin client for your platform:
### Sign up now
-Create an account at [app.pangolin.net](https://app.pangolin.net) to get started with Pangolin Cloud. A generous free tier is available.
+Create a free account at [app.pangolin.net](https://app.pangolin.net) to get started with Pangolin Cloud.
### Check out the docs
@@ -102,7 +112,3 @@ Pangolin is dual licensed under the AGPL-3 and the [Fossorial Commercial License
## Contributions
Please see [CONTRIBUTING](./CONTRIBUTING.md) in the repository for guidelines and best practices.
-
----
-
-WireGuard® is a registered trademark of Jason A. Donenfeld.
diff --git a/SECURITY.md b/SECURITY.md
index 6b7372c24..02fbe77d7 100644
--- a/SECURITY.md
+++ b/SECURITY.md
@@ -3,7 +3,7 @@
If you discover a security vulnerability, please follow the steps below to responsibly disclose it to us:
1. **Do not create a public GitHub issue or discussion post.** This could put the security of other users at risk.
-2. Send a detailed report to [security@pangolin.net](mailto:security@pangolin.net) or send a **private** message to a maintainer on [Discord](https://discord.gg/HCJR8Xhme4). Include:
+2. Send a detailed report to [security@pangolin.net](mailto:security@pangolin.net) with the following information:
- Description and location of the vulnerability.
- Potential impact of the vulnerability.
diff --git a/config/db/.gitignore b/config/db/.gitignore
new file mode 100644
index 000000000..9d4b1bb9c
--- /dev/null
+++ b/config/db/.gitignore
@@ -0,0 +1 @@
+*-journal
diff --git a/install/config/crowdsec/traefik_config.yml b/install/config/crowdsec/traefik_config.yml
index b3e4f0839..c1145934d 100644
--- a/install/config/crowdsec/traefik_config.yml
+++ b/install/config/crowdsec/traefik_config.yml
@@ -86,6 +86,8 @@ entryPoints:
http:
tls:
certResolver: "letsencrypt"
+ middlewares:
+ - crowdsec@file
encodedCharacters:
allowEncodedSlash: true
allowEncodedQuestionMark: true
diff --git a/license.py b/license.py
deleted file mode 100644
index 865dfad7a..000000000
--- a/license.py
+++ /dev/null
@@ -1,115 +0,0 @@
-import os
-import sys
-
-# --- Configuration ---
-# The header text to be added to the files.
-HEADER_TEXT = """/*
- * This file is part of a proprietary work.
- *
- * Copyright (c) 2025 Fossorial, Inc.
- * All rights reserved.
- *
- * This file is licensed under the Fossorial Commercial License.
- * You may not use this file except in compliance with the License.
- * Unauthorized use, copying, modification, or distribution is strictly prohibited.
- *
- * This file is not licensed under the AGPLv3.
- */
-"""
-
-def should_add_header(file_path):
- """
- Checks if a file should receive the commercial license header.
- Returns True if 'private' is in the path or file content.
- """
- # Check if 'private' is in the file path (case-insensitive)
- if 'server/private' in file_path.lower():
- return True
-
- # Check if 'private' is in the file content (case-insensitive)
- # try:
- # with open(file_path, 'r', encoding='utf-8', errors='ignore') as f:
- # content = f.read()
- # if 'private' in content.lower():
- # return True
- # except Exception as e:
- # print(f"Could not read file {file_path}: {e}")
-
- return False
-
-def process_directory(root_dir):
- """
- Recursively scans a directory and adds headers to qualifying .ts or .tsx files,
- skipping any 'node_modules' directories.
- """
- print(f"Scanning directory: {root_dir}")
- files_processed = 0
- headers_added = 0
-
- for root, dirs, files in os.walk(root_dir):
- # --- MODIFICATION ---
- # Exclude 'node_modules' directories from the scan to improve performance.
- if 'node_modules' in dirs:
- dirs.remove('node_modules')
-
- for file in files:
- if file.endswith('.ts') or file.endswith('.tsx'):
- file_path = os.path.join(root, file)
- files_processed += 1
-
- try:
- with open(file_path, 'r+', encoding='utf-8') as f:
- original_content = f.read()
- has_header = original_content.startswith(HEADER_TEXT.strip())
-
- if should_add_header(file_path):
- # Add header only if it's not already there
- if not has_header:
- f.seek(0, 0) # Go to the beginning of the file
- f.write(HEADER_TEXT.strip() + '\n\n' + original_content)
- print(f"Added header to: {file_path}")
- headers_added += 1
- else:
- print(f"Header already exists in: {file_path}")
- else:
- # Remove header if it exists but shouldn't be there
- if has_header:
- # Find the end of the header and remove it (including following newlines)
- header_with_newlines = HEADER_TEXT.strip() + '\n\n'
- if original_content.startswith(header_with_newlines):
- content_without_header = original_content[len(header_with_newlines):]
- else:
- # Handle case where there might be different newline patterns
- header_end = len(HEADER_TEXT.strip())
- # Skip any newlines after the header
- while header_end < len(original_content) and original_content[header_end] in '\n\r':
- header_end += 1
- content_without_header = original_content[header_end:]
-
- f.seek(0)
- f.write(content_without_header)
- f.truncate()
- print(f"Removed header from: {file_path}")
- headers_added += 1 # Reusing counter for modifications
-
- except Exception as e:
- print(f"Error processing file {file_path}: {e}")
-
- print("\n--- Scan Complete ---")
- print(f"Total .ts or .tsx files found: {files_processed}")
- print(f"Files modified (headers added/removed): {headers_added}")
-
-
-if __name__ == "__main__":
- # Get the target directory from the command line arguments.
- # If no directory is provided, it uses the current directory ('.').
- if len(sys.argv) > 1:
- target_directory = sys.argv[1]
- else:
- target_directory = '.' # Default to current directory
-
- if not os.path.isdir(target_directory):
- print(f"Error: Directory '{target_directory}' not found.")
- sys.exit(1)
-
- process_directory(os.path.abspath(target_directory))
diff --git a/license_header_checker.py b/license_header_checker.py
new file mode 100644
index 000000000..c173d693b
--- /dev/null
+++ b/license_header_checker.py
@@ -0,0 +1,137 @@
+import os
+import sys
+
+# --- Configuration ---
+# The header text to be added to the files.
+HEADER_TEXT = """/*
+ * This file is part of a proprietary work.
+ *
+ * Copyright (c) 2025-2026 Fossorial, Inc.
+ * All rights reserved.
+ *
+ * This file is licensed under the Fossorial Commercial License.
+ * You may not use this file except in compliance with the License.
+ * Unauthorized use, copying, modification, or distribution is strictly prohibited.
+ *
+ * This file is not licensed under the AGPLv3.
+ */
+"""
+
+HEADER_NORMALIZED = HEADER_TEXT.strip()
+
+
+def extract_leading_block_comment(content):
+ """
+ If the file content begins with a /* ... */ block comment, return the
+ full text of that comment (including the delimiters) and the index at
+ which the rest of the file starts (after any trailing newlines).
+ Returns (None, 0) when no such comment is found.
+ """
+ stripped = content.lstrip()
+ if not stripped.startswith('/*'):
+ return None, 0
+
+ # Account for any leading whitespace before the comment
+ comment_start = content.index('/*')
+ end_marker = content.find('*/', comment_start + 2)
+ if end_marker == -1:
+ return None, 0
+
+ comment_end = end_marker + 2 # position just after '*/'
+ comment_text = content[comment_start:comment_end].strip()
+
+ # Advance past any whitespace / newlines that follow the closing */
+ rest_start = comment_end
+ while rest_start < len(content) and content[rest_start] in '\n\r':
+ rest_start += 1
+
+ return comment_text, rest_start
+
+
+def should_add_header(file_path):
+ """
+ Checks if a file should receive the commercial license header.
+ Returns True if 'server/private' is in the path.
+ """
+ if 'server/private' in file_path.lower():
+ return True
+
+ return False
+
+
+def process_directory(root_dir):
+ """
+ Recursively scans a directory and adds/replaces/removes headers in
+ qualifying .ts or .tsx files, skipping any 'node_modules' directories.
+ """
+ print(f"Scanning directory: {root_dir}")
+ files_processed = 0
+ files_modified = 0
+
+ for root, dirs, files in os.walk(root_dir):
+ # Exclude 'node_modules' directories from the scan.
+ if 'node_modules' in dirs:
+ dirs.remove('node_modules')
+
+ for file in files:
+ if not (file.endswith('.ts') or file.endswith('.tsx')):
+ continue
+
+ file_path = os.path.join(root, file)
+ files_processed += 1
+
+ try:
+ with open(file_path, 'r', encoding='utf-8') as f:
+ original_content = f.read()
+
+ existing_comment, body_start = extract_leading_block_comment(
+ original_content
+ )
+ has_any_header = existing_comment is not None
+ has_correct_header = existing_comment == HEADER_NORMALIZED
+
+ body = original_content[body_start:] if has_any_header else original_content
+
+ if should_add_header(file_path):
+ if has_correct_header:
+ print(f"Header up-to-date: {file_path}")
+ else:
+ # Either no header exists or the header is outdated — write
+ # the correct one.
+ action = "Replaced header in" if has_any_header else "Added header to"
+ new_content = HEADER_NORMALIZED + '\n\n' + body
+ with open(file_path, 'w', encoding='utf-8') as f:
+ f.write(new_content)
+ print(f"{action}: {file_path}")
+ files_modified += 1
+ else:
+ if has_any_header:
+ # Remove the header — it shouldn't be here.
+ with open(file_path, 'w', encoding='utf-8') as f:
+ f.write(body)
+ print(f"Removed header from: {file_path}")
+ files_modified += 1
+ else:
+ print(f"No header needed: {file_path}")
+
+ except Exception as e:
+ print(f"Error processing file {file_path}: {e}")
+
+ print("\n--- Scan Complete ---")
+ print(f"Total .ts or .tsx files found: {files_processed}")
+ print(f"Files modified (added/replaced/removed): {files_modified}")
+
+
+if __name__ == "__main__":
+ # Get the target directory from the command line arguments.
+ # If no directory is provided, it uses the current directory ('.').
+ if len(sys.argv) > 1:
+ target_directory = sys.argv[1]
+ else:
+ target_directory = '.' # Default to current directory
+
+ if not os.path.isdir(target_directory):
+ print(f"Error: Directory '{target_directory}' not found.")
+ sys.exit(1)
+
+ process_directory(os.path.abspath(target_directory))
\ No newline at end of file
diff --git a/messages/bg-BG.json b/messages/bg-BG.json
index 10b83f38d..aec6f718c 100644
--- a/messages/bg-BG.json
+++ b/messages/bg-BG.json
@@ -148,6 +148,11 @@
"createLink": "Създаване на връзка",
"resourcesNotFound": "Не са намерени ресурси",
"resourceSearch": "Търсене на ресурси",
+ "machineSearch": "Търсене на машини",
+ "machinesSearch": "Търсене на клиенти на машини...",
+ "machineNotFound": "Не са намерени машини",
+ "userDeviceSearch": "Търсене на устройства на потребителя",
+ "userDevicesSearch": "Търсене на устройства на потребителя...",
"openMenu": "Отваряне на менюто",
"resource": "Ресурс",
"title": "Заглавие",
@@ -323,6 +328,54 @@
"apiKeysDelete": "Изтрийте API ключа",
"apiKeysManage": "Управление на API ключове",
"apiKeysDescription": "API ключове се използват за удостоверяване с интеграционния API",
+ "provisioningKeysTitle": "Ключ за осигуряване",
+ "provisioningKeysManage": "Управление на ключове за осигуряване",
+ "provisioningKeysDescription": "Ключовете за осигуряване се използват за удостоверяване на автоматичното осигуряване на сайта за вашата организация.",
+ "provisioningManage": "Осигуряване",
+ "provisioningDescription": "Управление на ключовете за осигуряване и преглед на чаканещите сайтове за одобрение.",
+ "pendingSites": "Чаканещи сайтове",
+ "siteApproveSuccess": "Сайтът е одобрен успешно",
+ "siteApproveError": "Грешка при одобряването на сайта",
+ "provisioningKeys": "Ключове за осигуряване",
+ "searchProvisioningKeys": "Търсене на ключове за осигуряване...",
+ "provisioningKeysAdd": "Генериране на ключ за осигуряване",
+ "provisioningKeysErrorDelete": "Грешка при изтриване на ключ за осигуряване",
+ "provisioningKeysErrorDeleteMessage": "Грешка при изтриване на ключ за осигуряване",
+ "provisioningKeysQuestionRemove": "Сигурни ли сте, че искате да премахнете този ключ за осигуряване от организацията?",
+ "provisioningKeysMessageRemove": "След като бъде премахнат, ключът няма да бъде използван за осигуряване на сайтове.",
+ "provisioningKeysDeleteConfirm": "Потвърдете изтриването на ключ за осигуряване",
+ "provisioningKeysDelete": "Изтриване на ключ за осигуряване",
+ "provisioningKeysCreate": "Генериране на ключ за осигуряване",
+ "provisioningKeysCreateDescription": "Генерирайте нов ключ за осигуряване за организацията",
+ "provisioningKeysSeeAll": "Вижте всички ключове за осигуряване",
+ "provisioningKeysSave": "Запазете ключа за осигуряване",
+ "provisioningKeysSaveDescription": "Ще можете да видите това само веднъж. Копирайте го на сигурно място.",
+ "provisioningKeysErrorCreate": "Грешка при създаване на ключ за осигуряване",
+ "provisioningKeysList": "Нов ключ за осигуряване",
+ "provisioningKeysMaxBatchSize": "Максимален размер на пакет",
+ "provisioningKeysUnlimitedBatchSize": "Неограничен размер на партида (без лимит)",
+ "provisioningKeysMaxBatchUnlimited": "Неограничено",
+ "provisioningKeysMaxBatchSizeInvalid": "Въведете валиден максимален размер на партида (1–1,000,000).",
+ "provisioningKeysValidUntil": "Валиден до",
+ "provisioningKeysValidUntilHint": "Оставете празно за неограничено валидност.",
+ "provisioningKeysValidUntilInvalid": "Въведете валидна дата и час.",
+ "provisioningKeysNumUsed": "Брой използвания",
+ "provisioningKeysLastUsed": "Последно използван",
+ "provisioningKeysNoExpiry": "Без изтичане",
+ "provisioningKeysNeverUsed": "Никога",
+ "provisioningKeysEdit": "Редактиране на ключ за осигуряване",
+ "provisioningKeysEditDescription": "Актуализирайте максималния размер на партида и времето на изтичане за този ключ.",
+ "provisioningKeysApproveNewSites": "Одобрете нови сайтове",
+ "provisioningKeysApproveNewSitesDescription": "Автоматично одобряване на сайтове, които се регистрират с този ключ.",
+ "provisioningKeysUpdateError": "Грешка при актуализирането на ключа за осигуряване",
+ "provisioningKeysUpdated": "Ключът за осигуряване е актуализиран",
+ "provisioningKeysUpdatedDescription": "Вашите промени бяха запазени.",
+ "provisioningKeysBannerTitle": "Ключове за осигуряване на сайта",
+ "provisioningKeysBannerDescription": "Генерирайте ключ за осигуряване и го използвайте със съединителя Newt за автоматично създаване на сайтове при първоначално стартиране - не е необходимо да се създават отделни идентификационни данни за всеки сайт.",
+ "provisioningKeysBannerButtonText": "Научете повече",
+ "pendingSitesBannerTitle": "Чакащи сайтове",
+ "pendingSitesBannerDescription": "Сайтовете, които се свързват с ключ за осигуряване, ще се появят тук за преглед.",
+ "pendingSitesBannerButtonText": "Научете повече",
"apiKeysSettings": "Настройки на {apiKeyName}",
"userTitle": "Управление на всички потребители",
"userDescription": "Преглед и управление на всички потребители в системата",
@@ -352,6 +405,10 @@
"licenseErrorKeyActivate": "Неуспешно активиране на лицензионния ключ",
"licenseErrorKeyActivateDescription": "Възникна грешка при активирането на лицензионния ключ.",
"licenseAbout": "Относно лицензите",
+ "licenseBannerTitle": "Активирайте своята корпоративна лицензия",
+ "licenseBannerDescription": "Отключете корпоративните функции за вашият хостинг на Pangolin. Закупете лицензионен ключ, за да активирате премиум възможности, след това го добавете по-долу.",
+ "licenseBannerGetLicense": "Вземете лиценз",
+ "licenseBannerViewDocs": "Преглед на документацията",
"communityEdition": "Комюнити издание",
"licenseAboutDescription": "Това е за бизнес и корпоративни потребители, които използват Pangolin в търговска среда. Ако използвате Pangolin за лична употреба, можете да игнорирате този раздел.",
"licenseKeyActivated": "Лицензионният ключ е активиран",
@@ -509,9 +566,12 @@
"userSaved": "Потребителят е запазен",
"userSavedDescription": "Потребителят беше актуализиран.",
"autoProvisioned": "Автоматично предоставено",
+ "autoProvisionSettings": "Настройки за автоматично осигуряване",
"autoProvisionedDescription": "Позволете този потребител да бъде автоматично управляван от доставчик на идентификационни данни",
"accessControlsDescription": "Управлявайте какво може да достъпва и прави този потребител в организацията",
"accessControlsSubmit": "Запазване на контролите за достъп",
+ "singleRolePerUserPlanNotice": "Вашият план поддържа само една роля на потребител.",
+ "singleRolePerUserEditionNotice": "Това издание поддържа само една роля на потребител.",
"roles": "Роли",
"accessUsersRoles": "Управление на потребители и роли",
"accessUsersRolesDescription": "Поканете потребители и ги добавете към роли, за да управлявате достъпа до организацията",
@@ -568,6 +628,8 @@
"targetErrorInvalidPortDescription": "Моля, въведете валиден номер на порт",
"targetErrorNoSite": "Няма избран сайт",
"targetErrorNoSiteDescription": "Моля, изберете сайт за целта",
+ "targetTargetsCleared": "Мишените са премахнати",
+ "targetTargetsClearedDescription": "Всички цели са били премахнати от този ресурс",
"targetCreated": "Целта е създадена",
"targetCreatedDescription": "Целта беше успешно създадена",
"targetErrorCreate": "Неуспешно създаване на целта",
@@ -836,6 +898,7 @@
"idpDisplayName": "Име за показване за този доставчик на идентичност",
"idpAutoProvisionUsers": "Автоматично потребителско създаване",
"idpAutoProvisionUsersDescription": "Когато е активирано, потребителите ще бъдат автоматично създадени в системата при първо влизане с възможност за свързване на потребителите с роли и организации.",
+ "idpAutoProvisionConfigureAfterCreate": "Можете да конфигурирате настройките за автоматично предоставяне, след като дистрибуторът на самоличност бъде създаден.",
"licenseBadge": "ЕЕ",
"idpType": "Тип доставчик",
"idpTypeDescription": "Изберете типа доставчик на идентичност, който искате да конфигурирате",
@@ -887,7 +950,7 @@
"defaultMappingsRole": "Карта на роля по подразбиране",
"defaultMappingsRoleDescription": "Резултатът от този израз трябва да върне името на ролята, както е дефинирано в организацията, като стринг.",
"defaultMappingsOrg": "Карта на организация по подразбиране",
- "defaultMappingsOrgDescription": "Този израз трябва да върне ID на организацията или 'true', за да бъде разрешен достъпът на потребителя до организацията.",
+ "defaultMappingsOrgDescription": "При задаване, този израз трябва да върне идентификационния номер на организацията или true, за да се даде достъп на потребителя до тази организация. Ако не е зададено, дефинирането на роля е достатъчно: потребителят има право на достъп, стига валидно картографиране на роля да бъде разрешено за него в рамките на организацията.",
"defaultMappingsSubmit": "Запазване на файловете по подразбиране",
"orgPoliciesEdit": "Редактиране на Организационна Политика",
"org": "Организация",
@@ -1119,6 +1182,7 @@
"setupTokenDescription": "Въведете конфигурационния токен от сървърната конзола.",
"setupTokenRequired": "Необходим е конфигурационен токен",
"actionUpdateSite": "Актуализиране на сайт",
+ "actionResetSiteBandwidth": "Нулиране на честотната лента на организацията",
"actionListSiteRoles": "Изброяване на позволените роли за сайта",
"actionCreateResource": "Създаване на ресурс",
"actionDeleteResource": "Изтриване на ресурс",
@@ -1148,7 +1212,7 @@
"actionRemoveUser": "Изтрийте потребител",
"actionListUsers": "Изброяване на потребители",
"actionAddUserRole": "Добавяне на роля на потребител",
- "actionSetUserOrgRoles": "Set User Roles",
+ "actionSetUserOrgRoles": "Задайте роли на потребители",
"actionGenerateAccessToken": "Генериране на токен за достъп",
"actionDeleteAccessToken": "Изтриване на токен за достъп",
"actionListAccessTokens": "Изброяване на токени за достъп",
@@ -1265,6 +1329,7 @@
"sidebarRoles": "Роли",
"sidebarShareableLinks": "Връзки",
"sidebarApiKeys": "API ключове",
+ "sidebarProvisioning": "Осигуряване",
"sidebarSettings": "Настройки",
"sidebarAllUsers": "Всички потребители",
"sidebarIdentityProviders": "Идентификационни доставчици",
@@ -1890,6 +1955,40 @@
"exitNode": "Изходен възел",
"country": "Държава",
"rulesMatchCountry": "Понастоящем на базата на изходния IP",
+ "region": "Регион",
+ "selectRegion": "Изберете регион",
+ "searchRegions": "Търсене на региони...",
+ "noRegionFound": "Регионът не е намерен.",
+ "rulesMatchRegion": "Изберете регионална групировка на държави",
+ "rulesErrorInvalidRegion": "Невалиден регион",
+ "rulesErrorInvalidRegionDescription": "Моля, изберете валиден регион.",
+ "regionAfrica": "Африка",
+ "regionNorthernAfrica": "Северна Африка",
+ "regionEasternAfrica": "Източна Африка",
+ "regionMiddleAfrica": "Централна Африка",
+ "regionSouthernAfrica": "Южна Африка",
+ "regionWesternAfrica": "Западна Африка",
+ "regionAmericas": "Америките",
+ "regionCaribbean": "Карибите",
+ "regionCentralAmerica": "Централна Америка",
+ "regionSouthAmerica": "Южна Америка",
+ "regionNorthernAmerica": "Северна Америка",
+ "regionAsia": "Азия",
+ "regionCentralAsia": "Централна Азия",
+ "regionEasternAsia": "Източна Азия",
+ "regionSouthEasternAsia": "Югоизточна Азия",
+ "regionSouthernAsia": "Южна Азия",
+ "regionWesternAsia": "Западна Азия",
+ "regionEurope": "Европа",
+ "regionEasternEurope": "Източна Европа",
+ "regionNorthernEurope": "Северна Европа",
+ "regionSouthernEurope": "Южна Европа",
+ "regionWesternEurope": "Западна Европа",
+ "regionOceania": "Океания",
+ "regionAustraliaAndNewZealand": "Австралия и Нова Зеландия",
+ "regionMelanesia": "Меланезия",
+ "regionMicronesia": "Микронезия",
+ "regionPolynesia": "Полинезия",
"managedSelfHosted": {
"title": "Управлявано Самостоятелно-хоствано",
"description": "По-надежден и по-нисък поддръжка на Самостоятелно-хостван Панголиин сървър с допълнителни екстри",
@@ -1928,7 +2027,7 @@
},
"internationaldomaindetected": "Открит международен домейн",
"willbestoredas": "Ще бъде съхранено като:",
- "roleMappingDescription": "Определете как се разпределят ролите на потребителите при вписване, когато е активирано автоматично предоставяне.",
+ "roleMappingDescription": "Определете как ролите се присвояват на потребителите, когато се вписват с този доставчик на самоличност.",
"selectRole": "Избор на роля",
"roleMappingExpression": "Израз",
"selectRolePlaceholder": "Избор на роля",
@@ -1938,6 +2037,25 @@
"invalidValue": "Невалидна стойност",
"idpTypeLabel": "Тип на доставчика на идентичност",
"roleMappingExpressionPlaceholder": "напр.: contains(groups, 'admin') && 'Admin' || 'Member'",
+ "roleMappingModeFixedRoles": "Фиксирани роли",
+ "roleMappingModeMappingBuilder": "Строител на карти",
+ "roleMappingModeRawExpression": "Необработено израз",
+ "roleMappingFixedRolesPlaceholderSelect": "Изберете една или повече роли",
+ "roleMappingFixedRolesPlaceholderFreeform": "Въведете имена на роли (точно съвпадение на организацията)",
+ "roleMappingFixedRolesDescriptionSameForAll": "Присвойте същият набор от роли на всеки автоматично осигурен потребител.",
+ "roleMappingFixedRolesDescriptionDefaultPolicy": "За стандартните политики въведете имена на роли, които съществуват във всяка организация, където е осигурен потребител. Имената трябва да съвпадат точно.",
+ "roleMappingClaimPath": "Път на иск",
+ "roleMappingClaimPathPlaceholder": "групи",
+ "roleMappingClaimPathDescription": "Път в съдържанието на маркера, който съдържа изходни стойности (например групи).",
+ "roleMappingMatchValue": "Съвпадение на стойност",
+ "roleMappingAssignRoles": "Присвояване на роли",
+ "roleMappingAddMappingRule": "Добавяне на правило за картироване",
+ "roleMappingRawExpressionResultDescription": "Изразът трябва да бъде оценен на низ или масив от низове.",
+ "roleMappingRawExpressionResultDescriptionSingleRole": "Изразът трябва да бъде оценен на низ (едно име на роля).",
+ "roleMappingMatchValuePlaceholder": "Съвпадение на стойност (например: администратор)",
+ "roleMappingAssignRolesPlaceholderFreeform": "Въведете имена на роли (точно по организация)",
+ "roleMappingBuilderFreeformRowHint": "Имената на ролите трябва да съвпадат с роля във всяка целева организация.",
+ "roleMappingRemoveRule": "Премахни",
"idpGoogleConfiguration": "Конфигурация на Google",
"idpGoogleConfigurationDescription": "Конфигурирайте Google OAuth2 идентификационни данни",
"idpGoogleClientIdDescription": "Google OAuth2 идентификационен клиент",
@@ -2001,8 +2119,10 @@
"selectDomainForOrgAuthPage": "Изберете домейн за страницата за удостоверяване на организацията",
"domainPickerProvidedDomain": "Предоставен домейн",
"domainPickerFreeProvidedDomain": "Безплатен предоставен домейн",
+ "domainPickerFreeDomainsPaidFeature": "Предоставените домейни са платена функция. Абонирайте се, за да получите домейн, включен във вашия план - няма нужда да използвате вашия собствен.",
"domainPickerVerified": "Проверено",
"domainPickerUnverified": "Непроверено",
+ "domainPickerManual": "Ръчно",
"domainPickerInvalidSubdomainStructure": "Този поддомен съдържа невалидни знаци или структура. Ще бъде автоматично пречистен при запазване.",
"domainPickerError": "Грешка",
"domainPickerErrorLoadDomains": "Неуспешно зареждане на домейни на организацията",
@@ -2232,10 +2352,10 @@
},
"scale": {
"title": "Скала",
- "description": "Предприятие, 50 потребители, 50 сайта и приоритетна поддръжка."
+ "description": "Функции за корпоративни клиенти, 50 потребители, 100 сайта и приоритетна поддръжка."
}
},
- "personalUseOnly": "Само за лична употреба (безплатен лиценз — без плащане)",
+ "personalUseOnly": "Само за лична употреба (безплатен лиценз - без проверка)",
"buttons": {
"continueToCheckout": "Продължете към плащане"
},
@@ -2334,6 +2454,8 @@
"logRetentionAccessDescription": "Колко дълго да се задържат логовете за достъп",
"logRetentionActionLabel": "Задържане на логове за действия",
"logRetentionActionDescription": "Колко дълго да се задържат логовете за действия",
+ "logRetentionConnectionLabel": "Запазване на дневниците на връзките",
+ "logRetentionConnectionDescription": "Колко дълго да се съхраняват дневниците на връзките",
"logRetentionDisabled": "Деактивирано",
"logRetention3Days": "3 дни",
"logRetention7Days": "7 дни",
@@ -2344,6 +2466,13 @@
"logRetentionEndOfFollowingYear": "Край на следващата година",
"actionLogsDescription": "Прегледайте историята на действията, извършени в тази организация",
"accessLogsDescription": "Прегледайте заявките за удостоверяване на достъпа до ресурсите в тази организация",
+ "connectionLogs": "Логове на връзката",
+ "connectionLogsDescription": "Вижте логовете на връзките за тунелите в тази организация",
+ "sidebarLogsConnection": "Логове на връзката",
+ "sidebarLogsStreaming": "Потоци",
+ "sourceAddress": "Източен адрес",
+ "destinationAddress": "Адрес на дестинация",
+ "duration": "Продължителност",
"licenseRequiredToUse": "Изисква се лиценз за + {cfg.name || t("streamingUnnamedDestination")} +
++ HTTP +
++ {cfg.url || ( + + {t("streamingNoUrlConfigured")} + + )} +
+ + {/* Footer: edit button + three-dots menu */} ++ {t("streamingDeleteDialogAreYouSure")}{" "} + + {parseHttpConfig(deleteTarget.config).name || + t("streamingDeleteDialogThisDestination")} + + {t("streamingDeleteDialogPermanentlyRemoved")} +
+ } + buttonText={t("streamingDeleteButtonText")} + onConfirm={handleDeleteConfirm} + /> + )} + > + ); +} diff --git a/src/app/[orgId]/settings/page.tsx b/src/app/[orgId]/settings/page.tsx index 9956bc859..bf8beab72 100644 --- a/src/app/[orgId]/settings/page.tsx +++ b/src/app/[orgId]/settings/page.tsx @@ -1,5 +1,10 @@ +import type { Metadata } from "next"; import { redirect } from "next/navigation"; +export const metadata: Metadata = { + title: "Settings" +}; + type OrgPageProps = { params: Promise<{ orgId: string }>; }; diff --git a/src/app/[orgId]/settings/provisioning/keys/page.tsx b/src/app/[orgId]/settings/provisioning/keys/page.tsx index 9637b03b3..fc95a655d 100644 --- a/src/app/[orgId]/settings/provisioning/keys/page.tsx +++ b/src/app/[orgId]/settings/provisioning/keys/page.tsx @@ -4,7 +4,7 @@ import { AxiosResponse } from "axios"; import { PaidFeaturesAlert } from "@app/components/PaidFeaturesAlert"; import SiteProvisioningKeysTable, { SiteProvisioningKeyRow -} from "../../../../../components/SiteProvisioningKeysTable"; +} from "@app/components/SiteProvisioningKeysTable"; import { ListSiteProvisioningKeysResponse } from "@server/routers/siteProvisioning/types"; import { getTranslations } from "next-intl/server"; import { TierFeature, tierMatrix } from "@server/lib/billing/tierMatrix"; @@ -12,6 +12,11 @@ import DismissableBanner from "@app/components/DismissableBanner"; import Link from "next/link"; import { Button } from "@app/components/ui/button"; import { ArrowRight, Plug } from "lucide-react"; +import type { Metadata } from "next"; + +export const metadata: Metadata = { + title: "Provisioning Keys" +}; type ProvisioningKeysPageProps = { params: Promise<{ orgId: string }>; diff --git a/src/app/[orgId]/settings/provisioning/page.tsx b/src/app/[orgId]/settings/provisioning/page.tsx index 51db66c2d..1e0377590 100644 --- a/src/app/[orgId]/settings/provisioning/page.tsx +++ b/src/app/[orgId]/settings/provisioning/page.tsx @@ -1,5 +1,10 @@ +import type { Metadata } from "next"; import { redirect } from "next/navigation"; +export const metadata: Metadata = { + title: "Provisioning" +}; + type ProvisioningPageProps = { params: Promise<{ orgId: string }>; }; @@ -7,4 +12,4 @@ type ProvisioningPageProps = { export default async function ProvisioningPage(props: ProvisioningPageProps) { const params = await props.params; redirect(`/${params.orgId}/settings/provisioning/keys`); -} \ No newline at end of file +} diff --git a/src/app/[orgId]/settings/provisioning/pending/page.tsx b/src/app/[orgId]/settings/provisioning/pending/page.tsx index 637f828b8..ee7246821 100644 --- a/src/app/[orgId]/settings/provisioning/pending/page.tsx +++ b/src/app/[orgId]/settings/provisioning/pending/page.tsx @@ -9,6 +9,13 @@ import DismissableBanner from "@app/components/DismissableBanner"; import Link from "next/link"; import { Button } from "@app/components/ui/button"; import { ArrowRight, Plug } from "lucide-react"; +import { PaidFeaturesAlert } from "@app/components/PaidFeaturesAlert"; +import { TierFeature, tierMatrix } from "@server/lib/billing/tierMatrix"; +import type { Metadata } from "next"; + +export const metadata: Metadata = { + title: "Pending Sites" +}; type PendingSitesPageProps = { params: Promise<{ orgId: string }>; @@ -96,6 +103,10 @@ export default async function PendingSitesPage(props: PendingSitesPageProps) { ++ {t("httpDestNoHeadersConfigured")} +
+ )} + {headers.map((h, i) => ( ++ {urlError} +
+ )} ++ {t("httpDestAuthDescription")} +
++ {t("httpDestAuthNoneDescription")} +
++ {t("httpDestAuthBearerDescription")} +
++ {t("httpDestAuthBasicDescription")} +
++ {t("httpDestAuthCustomDescription")} +
++ {t("httpDestCustomHeadersDescription")} +
++ {t("httpDestBodyTemplateDescription")} +
++ {t("httpDestPayloadFormatDescription")} +
++ {t("httpDestFormatJsonArrayDescription")} +
++ {t("httpDestFormatNdjsonDescription")} +
++ {t("httpDestFormatSingleDescription")} +
++ {t("httpDestLogTypesDescription")} +
++ {t("httpDestAccessLogsDescription")} +
++ {t("httpDestActionLogsDescription")} +
++ {t("httpDestConnectionLogsDescription")} +
++ {t("httpDestRequestLogsDescription")} +
+- This organization has reached its user limit. Please contact the organization administrator to upgrade their plan before accepting this invite. + This organization has reached its user limit. Please + contact the organization administrator to upgrade their + plan before accepting this invite.
); diff --git a/src/components/LayoutHeader.tsx b/src/components/LayoutHeader.tsx index bef016853..29850f115 100644 --- a/src/components/LayoutHeader.tsx +++ b/src/components/LayoutHeader.tsx @@ -49,7 +49,7 @@ export function LayoutHeader({ showTopBar }: LayoutHeaderProps) { return ({t("idpQuestionRemove")}
-{t("idpMessageRemove")}
+{t("idpDeleteGlobalQuestion")}
+{t("idpDeleteGlobalDescription")}
{t("idpUnassociateQuestion")}
+{t("idpUnassociateDescription")}
+