Compare commits

..

5 Commits

Author SHA1 Message Date
Owen Schwartz
bdc45887f9 Add chainId to dedup messages (#2737)
* ChainId send through on sensitive messages
2026-03-29 12:08:29 -07:00
Owen Schwartz
6d7a19b0a0 Merge pull request #2716 from fosrl/patch-1
Add typecasts
2026-03-25 22:12:59 -07:00
Owen
6b3a6fa380 Add typecasts 2026-03-25 22:11:56 -07:00
Owen Schwartz
e2a65b4b74 Merge pull request #2715 from fosrl/batch-band
Batch set bandwidth
2026-03-25 21:54:44 -07:00
Owen
1f01108b62 Batch set bandwidth 2026-03-25 21:53:20 -07:00
198 changed files with 2562 additions and 13666 deletions

View File

@@ -14,7 +14,6 @@ import (
"os/exec"
"path/filepath"
"runtime"
"strconv"
"strings"
"text/template"
"time"
@@ -91,13 +90,6 @@ func main() {
var config Config
var alreadyInstalled = false
// Determine installation directory
installDir := findOrSelectInstallDirectory()
if err := os.Chdir(installDir); err != nil {
fmt.Printf("Error changing to installation directory: %v\n", err)
os.Exit(1)
}
// check if there is already a config file
if _, err := os.Stat("config/config.yml"); err != nil {
config = collectUserInput()
@@ -295,117 +287,6 @@ func main() {
fmt.Printf("\nTo complete the initial setup, please visit:\nhttps://%s/auth/initial-setup\n", config.DashboardDomain)
}
func hasExistingInstall(dir string) bool {
configPath := filepath.Join(dir, "config", "config.yml")
_, err := os.Stat(configPath)
return err == nil
}
func findOrSelectInstallDirectory() string {
const defaultInstallDir = "/opt/pangolin"
// Get current working directory
cwd, err := os.Getwd()
if err != nil {
fmt.Printf("Error getting current directory: %v\n", err)
os.Exit(1)
}
// 1. Check current directory for existing install
if hasExistingInstall(cwd) {
fmt.Printf("Found existing Pangolin installation in current directory: %s\n", cwd)
return cwd
}
// 2. Check default location (/opt/pangolin) for existing install
if cwd != defaultInstallDir && hasExistingInstall(defaultInstallDir) {
fmt.Printf("\nFound existing Pangolin installation at: %s\n", defaultInstallDir)
if readBool(fmt.Sprintf("Would you like to use the existing installation at %s?", defaultInstallDir), true) {
return defaultInstallDir
}
}
// 3. No existing install found, prompt for installation directory
fmt.Println("\n=== Installation Directory ===")
fmt.Println("No existing Pangolin installation detected.")
installDir := readString("Enter the installation directory", defaultInstallDir)
// Expand ~ to home directory if present
if strings.HasPrefix(installDir, "~") {
home, err := os.UserHomeDir()
if err != nil {
fmt.Printf("Error getting home directory: %v\n", err)
os.Exit(1)
}
installDir = filepath.Join(home, installDir[1:])
}
// Convert to absolute path
absPath, err := filepath.Abs(installDir)
if err != nil {
fmt.Printf("Error resolving path: %v\n", err)
os.Exit(1)
}
installDir = absPath
// Check if directory exists
if _, err := os.Stat(installDir); os.IsNotExist(err) {
// Directory doesn't exist, create it
if readBool(fmt.Sprintf("Directory %s does not exist. Create it?", installDir), true) {
if err := os.MkdirAll(installDir, 0755); err != nil {
fmt.Printf("Error creating directory: %v\n", err)
os.Exit(1)
}
fmt.Printf("Created directory: %s\n", installDir)
// Offer to change ownership if running via sudo
changeDirectoryOwnership(installDir)
} else {
fmt.Println("Installation cancelled.")
os.Exit(0)
}
}
fmt.Printf("Installation directory: %s\n", installDir)
return installDir
}
func changeDirectoryOwnership(dir string) {
// Check if we're running via sudo by looking for SUDO_USER
sudoUser := os.Getenv("SUDO_USER")
if sudoUser == "" || os.Geteuid() != 0 {
return
}
sudoUID := os.Getenv("SUDO_UID")
sudoGID := os.Getenv("SUDO_GID")
if sudoUID == "" || sudoGID == "" {
return
}
fmt.Printf("\nRunning as root via sudo (original user: %s)\n", sudoUser)
if readBool(fmt.Sprintf("Would you like to change ownership of %s to user '%s'? This makes it easier to manage config files without sudo.", dir, sudoUser), true) {
uid, err := strconv.Atoi(sudoUID)
if err != nil {
fmt.Printf("Warning: Could not parse SUDO_UID: %v\n", err)
return
}
gid, err := strconv.Atoi(sudoGID)
if err != nil {
fmt.Printf("Warning: Could not parse SUDO_GID: %v\n", err)
return
}
if err := os.Chown(dir, uid, gid); err != nil {
fmt.Printf("Warning: Could not change ownership: %v\n", err)
} else {
fmt.Printf("Changed ownership of %s to %s\n", dir, sudoUser)
}
}
}
func podmanOrDocker() SupportedContainer {
inputContainer := readString("Would you like to run Pangolin as Docker or Podman containers?", "docker")

View File

@@ -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))

View File

@@ -148,11 +148,6 @@
"createLink": "Създаване на връзка",
"resourcesNotFound": "Не са намерени ресурси",
"resourceSearch": "Търсене на ресурси",
"machineSearch": "Search machines",
"machinesSearch": "Search machine clients...",
"machineNotFound": "No machines found",
"userDeviceSearch": "Search user devices",
"userDevicesSearch": "Search user devices...",
"openMenu": "Отваряне на менюто",
"resource": "Ресурс",
"title": "Заглавие",
@@ -328,54 +323,6 @@
"apiKeysDelete": "Изтрийте API ключа",
"apiKeysManage": "Управление на API ключове",
"apiKeysDescription": "API ключове се използват за удостоверяване с интеграционния API",
"provisioningKeysTitle": "Provisioning Key",
"provisioningKeysManage": "Manage Provisioning Keys",
"provisioningKeysDescription": "Provisioning keys are used to authenticate automated site provisioning for your organization.",
"provisioningManage": "Provisioning",
"provisioningDescription": "Manage provisioning keys and review pending sites awaiting approval.",
"pendingSites": "Pending Sites",
"siteApproveSuccess": "Site approved successfully",
"siteApproveError": "Error approving site",
"provisioningKeys": "Provisioning Keys",
"searchProvisioningKeys": "Search provisioning keys...",
"provisioningKeysAdd": "Generate Provisioning Key",
"provisioningKeysErrorDelete": "Error deleting provisioning key",
"provisioningKeysErrorDeleteMessage": "Error deleting provisioning key",
"provisioningKeysQuestionRemove": "Are you sure you want to remove this provisioning key from the organization?",
"provisioningKeysMessageRemove": "Once removed, the key can no longer be used for site provisioning.",
"provisioningKeysDeleteConfirm": "Confirm Delete Provisioning Key",
"provisioningKeysDelete": "Delete Provisioning key",
"provisioningKeysCreate": "Generate Provisioning Key",
"provisioningKeysCreateDescription": "Generate a new provisioning key for the organization",
"provisioningKeysSeeAll": "See all provisioning keys",
"provisioningKeysSave": "Save the provisioning key",
"provisioningKeysSaveDescription": "You will only be able to see this once. Copy it to a secure place.",
"provisioningKeysErrorCreate": "Error creating provisioning key",
"provisioningKeysList": "New provisioning key",
"provisioningKeysMaxBatchSize": "Max batch size",
"provisioningKeysUnlimitedBatchSize": "Unlimited batch size (no limit)",
"provisioningKeysMaxBatchUnlimited": "Unlimited",
"provisioningKeysMaxBatchSizeInvalid": "Enter a valid max batch size (11,000,000).",
"provisioningKeysValidUntil": "Valid until",
"provisioningKeysValidUntilHint": "Leave empty for no expiration.",
"provisioningKeysValidUntilInvalid": "Enter a valid date and time.",
"provisioningKeysNumUsed": "Times used",
"provisioningKeysLastUsed": "Last used",
"provisioningKeysNoExpiry": "No expiration",
"provisioningKeysNeverUsed": "Never",
"provisioningKeysEdit": "Edit Provisioning Key",
"provisioningKeysEditDescription": "Update the max batch size and expiration time for this key.",
"provisioningKeysApproveNewSites": "Approve new sites",
"provisioningKeysApproveNewSitesDescription": "Automatically approve sites that register with this key.",
"provisioningKeysUpdateError": "Error updating provisioning key",
"provisioningKeysUpdated": "Provisioning key updated",
"provisioningKeysUpdatedDescription": "Your changes have been saved.",
"provisioningKeysBannerTitle": "Site Provisioning Keys",
"provisioningKeysBannerDescription": "Generate a provisioning key and use it with the Newt connector to automatically create sites on first startup — no need to set up separate credentials for each site.",
"provisioningKeysBannerButtonText": "Learn More",
"pendingSitesBannerTitle": "Pending Sites",
"pendingSitesBannerDescription": "Sites that connect using a provisioning key appear here for review. Approve each site before it becomes active and gains access to your resources.",
"pendingSitesBannerButtonText": "Learn More",
"apiKeysSettings": "Настройки на {apiKeyName}",
"userTitle": "Управление на всички потребители",
"userDescription": "Преглед и управление на всички потребители в системата",
@@ -562,12 +509,9 @@
"userSaved": "Потребителят е запазен",
"userSavedDescription": "Потребителят беше актуализиран.",
"autoProvisioned": "Автоматично предоставено",
"autoProvisionSettings": "Auto Provision Settings",
"autoProvisionedDescription": "Позволете този потребител да бъде автоматично управляван от доставчик на идентификационни данни",
"accessControlsDescription": "Управлявайте какво може да достъпва и прави този потребител в организацията",
"accessControlsSubmit": "Запазване на контролите за достъп",
"singleRolePerUserPlanNotice": "Your plan only supports one role per user.",
"singleRolePerUserEditionNotice": "This edition only supports one role per user.",
"roles": "Роли",
"accessUsersRoles": "Управление на потребители и роли",
"accessUsersRolesDescription": "Поканете потребители и ги добавете към роли, за да управлявате достъпа до организацията",
@@ -943,7 +887,7 @@
"defaultMappingsRole": "Карта на роля по подразбиране",
"defaultMappingsRoleDescription": "Резултатът от този израз трябва да върне името на ролята, както е дефинирано в организацията, като стринг.",
"defaultMappingsOrg": "Карта на организация по подразбиране",
"defaultMappingsOrgDescription": "When set, this expression must return the organization ID or true for the user to access that organization. When unset, defining an organization policy for that org is enough: the user is allowed in as long as a valid role mapping can be resolved for them within the organization.",
"defaultMappingsOrgDescription": "Този израз трябва да върне ID на организацията или 'true', за да бъде разрешен достъпът на потребителя до организацията.",
"defaultMappingsSubmit": "Запазване на файловете по подразбиране",
"orgPoliciesEdit": "Редактиране на Организационна Политика",
"org": "Организация",
@@ -1175,7 +1119,6 @@
"setupTokenDescription": "Въведете конфигурационния токен от сървърната конзола.",
"setupTokenRequired": "Необходим е конфигурационен токен",
"actionUpdateSite": "Актуализиране на сайт",
"actionResetSiteBandwidth": "Reset Organization Bandwidth",
"actionListSiteRoles": "Изброяване на позволените роли за сайта",
"actionCreateResource": "Създаване на ресурс",
"actionDeleteResource": "Изтриване на ресурс",
@@ -1205,7 +1148,6 @@
"actionRemoveUser": "Изтрийте потребител",
"actionListUsers": "Изброяване на потребители",
"actionAddUserRole": "Добавяне на роля на потребител",
"actionSetUserOrgRoles": "Set User Roles",
"actionGenerateAccessToken": "Генериране на токен за достъп",
"actionDeleteAccessToken": "Изтриване на токен за достъп",
"actionListAccessTokens": "Изброяване на токени за достъп",
@@ -1322,7 +1264,6 @@
"sidebarRoles": "Роли",
"sidebarShareableLinks": "Връзки",
"sidebarApiKeys": "API ключове",
"sidebarProvisioning": "Provisioning",
"sidebarSettings": "Настройки",
"sidebarAllUsers": "Всички потребители",
"sidebarIdentityProviders": "Идентификационни доставчици",
@@ -1948,40 +1889,6 @@
"exitNode": "Изходен възел",
"country": "Държава",
"rulesMatchCountry": "Понастоящем на базата на изходния IP",
"region": "Region",
"selectRegion": "Select region",
"searchRegions": "Search regions...",
"noRegionFound": "No region found.",
"rulesMatchRegion": "Select a regional grouping of countries",
"rulesErrorInvalidRegion": "Invalid region",
"rulesErrorInvalidRegionDescription": "Please select a valid region.",
"regionAfrica": "Africa",
"regionNorthernAfrica": "Northern Africa",
"regionEasternAfrica": "Eastern Africa",
"regionMiddleAfrica": "Middle Africa",
"regionSouthernAfrica": "Southern Africa",
"regionWesternAfrica": "Western Africa",
"regionAmericas": "Americas",
"regionCaribbean": "Caribbean",
"regionCentralAmerica": "Central America",
"regionSouthAmerica": "South America",
"regionNorthernAmerica": "Northern America",
"regionAsia": "Asia",
"regionCentralAsia": "Central Asia",
"regionEasternAsia": "Eastern Asia",
"regionSouthEasternAsia": "South-Eastern Asia",
"regionSouthernAsia": "Southern Asia",
"regionWesternAsia": "Western Asia",
"regionEurope": "Europe",
"regionEasternEurope": "Eastern Europe",
"regionNorthernEurope": "Northern Europe",
"regionSouthernEurope": "Southern Europe",
"regionWesternEurope": "Western Europe",
"regionOceania": "Oceania",
"regionAustraliaAndNewZealand": "Australia and New Zealand",
"regionMelanesia": "Melanesia",
"regionMicronesia": "Micronesia",
"regionPolynesia": "Polynesia",
"managedSelfHosted": {
"title": "Управлявано Самостоятелно-хоствано",
"description": "По-надежден и по-нисък поддръжка на Самостоятелно-хостван Панголиин сървър с допълнителни екстри",
@@ -2030,25 +1937,6 @@
"invalidValue": "Невалидна стойност",
"idpTypeLabel": "Тип на доставчика на идентичност",
"roleMappingExpressionPlaceholder": "напр.: contains(groups, 'admin') && 'Admin' || 'Member'",
"roleMappingModeFixedRoles": "Fixed Roles",
"roleMappingModeMappingBuilder": "Mapping Builder",
"roleMappingModeRawExpression": "Raw Expression",
"roleMappingFixedRolesPlaceholderSelect": "Select one or more roles",
"roleMappingFixedRolesPlaceholderFreeform": "Type role names (exact match per organization)",
"roleMappingFixedRolesDescriptionSameForAll": "Assign the same role set to every auto-provisioned user.",
"roleMappingFixedRolesDescriptionDefaultPolicy": "For default policies, type role names that exist in each organization where users are provisioned. Names must match exactly.",
"roleMappingClaimPath": "Claim Path",
"roleMappingClaimPathPlaceholder": "groups",
"roleMappingClaimPathDescription": "Path in the token payload that contains source values (for example, groups).",
"roleMappingMatchValue": "Match Value",
"roleMappingAssignRoles": "Assign Roles",
"roleMappingAddMappingRule": "Add Mapping Rule",
"roleMappingRawExpressionResultDescription": "Expression must evaluate to a string or string array.",
"roleMappingRawExpressionResultDescriptionSingleRole": "Expression must evaluate to a string (a single role name).",
"roleMappingMatchValuePlaceholder": "Match value (for example: admin)",
"roleMappingAssignRolesPlaceholderFreeform": "Type role names (exact per org)",
"roleMappingBuilderFreeformRowHint": "Role names must match a role in each target organization.",
"roleMappingRemoveRule": "Remove",
"idpGoogleConfiguration": "Конфигурация на Google",
"idpGoogleConfigurationDescription": "Конфигурирайте Google OAuth2 идентификационни данни",
"idpGoogleClientIdDescription": "Google OAuth2 идентификационен клиент",
@@ -2445,8 +2333,6 @@
"logRetentionAccessDescription": "Колко дълго да се задържат логовете за достъп",
"logRetentionActionLabel": "Задържане на логове за действия",
"logRetentionActionDescription": "Колко дълго да се задържат логовете за действия",
"logRetentionConnectionLabel": "Connection Log Retention",
"logRetentionConnectionDescription": "How long to retain connection logs",
"logRetentionDisabled": "Деактивирано",
"logRetention3Days": "3 дни",
"logRetention7Days": "7 дни",
@@ -2457,14 +2343,8 @@
"logRetentionEndOfFollowingYear": "Край на следващата година",
"actionLogsDescription": "Прегледайте историята на действията, извършени в тази организация",
"accessLogsDescription": "Прегледайте заявките за удостоверяване на достъпа до ресурсите в тази организация",
"connectionLogs": "Connection Logs",
"connectionLogsDescription": "View connection logs for tunnels in this organization",
"sidebarLogsConnection": "Connection Logs",
"sourceAddress": "Source Address",
"destinationAddress": "Destination Address",
"duration": "Duration",
"licenseRequiredToUse": "An <enterpriseLicenseLink>Enterprise Edition</enterpriseLicenseLink> license or <pangolinCloudLink>Pangolin Cloud</pangolinCloudLink> is required to use this feature. <bookADemoLink>Book a free demo or POC trial to learn more</bookADemoLink>.",
"ossEnterpriseEditionRequired": "The <enterpriseEditionLink>Enterprise Edition</enterpriseEditionLink> is required to use this feature. This feature is also available in <pangolinCloudLink>Pangolin Cloud</pangolinCloudLink>. <bookADemoLink>Book a free demo or POC trial to learn more</bookADemoLink>.",
"licenseRequiredToUse": "Изисква се лиценз за <enterpriseLicenseLink>Enterprise Edition</enterpriseLicenseLink> или <pangolinCloudLink>Pangolin Cloud</pangolinCloudLink> за използване на тази функция. <bookADemoLink>Резервирайте демонстрация или пробен POC</bookADemoLink>.",
"ossEnterpriseEditionRequired": "<enterpriseEditionLink>Enterprise Edition</enterpriseEditionLink> е необходим за използване на тази функция. Тази функция също е налична в <pangolinCloudLink>Pangolin Cloud</pangolinCloudLink>. <bookADemoLink>Резервирайте демонстрация или пробен POC</bookADemoLink>.",
"certResolver": "Решавач на сертификати",
"certResolverDescription": "Изберете решавач на сертификати за използване за този ресурс.",
"selectCertResolver": "Изберете решавач на сертификати",
@@ -2802,6 +2682,5 @@
"approvalsEmptyStateStep2Description": "Редактирайте ролята и активирайте опцията 'Изискване на одобрения за устройства'. Потребители с тази роля ще трябва администраторско одобрение за нови устройства.",
"approvalsEmptyStatePreviewDescription": "Преглед: Когато е активирано, чакащите заявки за устройства ще се появят тук за преглед",
"approvalsEmptyStateButtonText": "Управлявайте роли",
"domainErrorTitle": "Имаме проблем с проверката на вашия домейн",
"idpAdminAutoProvisionPoliciesTabHint": "Configure role mapping and organization policies on the <policiesTabLink>Auto Provision Settings</policiesTabLink> tab."
"domainErrorTitle": "Имаме проблем с проверката на вашия домейн"
}

View File

@@ -148,11 +148,6 @@
"createLink": "Vytvořit odkaz",
"resourcesNotFound": "Nebyly nalezeny žádné zdroje",
"resourceSearch": "Vyhledat zdroje",
"machineSearch": "Search machines",
"machinesSearch": "Search machine clients...",
"machineNotFound": "No machines found",
"userDeviceSearch": "Search user devices",
"userDevicesSearch": "Search user devices...",
"openMenu": "Otevřít nabídku",
"resource": "Zdroj",
"title": "Název",
@@ -328,54 +323,6 @@
"apiKeysDelete": "Odstranit klíč API",
"apiKeysManage": "Správa API klíčů",
"apiKeysDescription": "API klíče se používají k ověření s integračním API",
"provisioningKeysTitle": "Provisioning Key",
"provisioningKeysManage": "Manage Provisioning Keys",
"provisioningKeysDescription": "Provisioning keys are used to authenticate automated site provisioning for your organization.",
"provisioningManage": "Provisioning",
"provisioningDescription": "Manage provisioning keys and review pending sites awaiting approval.",
"pendingSites": "Pending Sites",
"siteApproveSuccess": "Site approved successfully",
"siteApproveError": "Error approving site",
"provisioningKeys": "Provisioning Keys",
"searchProvisioningKeys": "Search provisioning keys...",
"provisioningKeysAdd": "Generate Provisioning Key",
"provisioningKeysErrorDelete": "Error deleting provisioning key",
"provisioningKeysErrorDeleteMessage": "Error deleting provisioning key",
"provisioningKeysQuestionRemove": "Are you sure you want to remove this provisioning key from the organization?",
"provisioningKeysMessageRemove": "Once removed, the key can no longer be used for site provisioning.",
"provisioningKeysDeleteConfirm": "Confirm Delete Provisioning Key",
"provisioningKeysDelete": "Delete Provisioning key",
"provisioningKeysCreate": "Generate Provisioning Key",
"provisioningKeysCreateDescription": "Generate a new provisioning key for the organization",
"provisioningKeysSeeAll": "See all provisioning keys",
"provisioningKeysSave": "Save the provisioning key",
"provisioningKeysSaveDescription": "You will only be able to see this once. Copy it to a secure place.",
"provisioningKeysErrorCreate": "Error creating provisioning key",
"provisioningKeysList": "New provisioning key",
"provisioningKeysMaxBatchSize": "Max batch size",
"provisioningKeysUnlimitedBatchSize": "Unlimited batch size (no limit)",
"provisioningKeysMaxBatchUnlimited": "Unlimited",
"provisioningKeysMaxBatchSizeInvalid": "Enter a valid max batch size (11,000,000).",
"provisioningKeysValidUntil": "Valid until",
"provisioningKeysValidUntilHint": "Leave empty for no expiration.",
"provisioningKeysValidUntilInvalid": "Enter a valid date and time.",
"provisioningKeysNumUsed": "Times used",
"provisioningKeysLastUsed": "Last used",
"provisioningKeysNoExpiry": "No expiration",
"provisioningKeysNeverUsed": "Never",
"provisioningKeysEdit": "Edit Provisioning Key",
"provisioningKeysEditDescription": "Update the max batch size and expiration time for this key.",
"provisioningKeysApproveNewSites": "Approve new sites",
"provisioningKeysApproveNewSitesDescription": "Automatically approve sites that register with this key.",
"provisioningKeysUpdateError": "Error updating provisioning key",
"provisioningKeysUpdated": "Provisioning key updated",
"provisioningKeysUpdatedDescription": "Your changes have been saved.",
"provisioningKeysBannerTitle": "Site Provisioning Keys",
"provisioningKeysBannerDescription": "Generate a provisioning key and use it with the Newt connector to automatically create sites on first startup — no need to set up separate credentials for each site.",
"provisioningKeysBannerButtonText": "Learn More",
"pendingSitesBannerTitle": "Pending Sites",
"pendingSitesBannerDescription": "Sites that connect using a provisioning key appear here for review. Approve each site before it becomes active and gains access to your resources.",
"pendingSitesBannerButtonText": "Learn More",
"apiKeysSettings": "Nastavení {apiKeyName}",
"userTitle": "Spravovat všechny uživatele",
"userDescription": "Zobrazit a spravovat všechny uživatele v systému",
@@ -562,12 +509,9 @@
"userSaved": "Uživatel uložen",
"userSavedDescription": "Uživatel byl aktualizován.",
"autoProvisioned": "Automaticky poskytnuto",
"autoProvisionSettings": "Auto Provision Settings",
"autoProvisionedDescription": "Povolit tomuto uživateli automaticky spravovat poskytovatel identity",
"accessControlsDescription": "Spravovat co může tento uživatel přistupovat a dělat v organizaci",
"accessControlsSubmit": "Uložit kontroly přístupu",
"singleRolePerUserPlanNotice": "Your plan only supports one role per user.",
"singleRolePerUserEditionNotice": "This edition only supports one role per user.",
"roles": "Role",
"accessUsersRoles": "Spravovat uživatele a role",
"accessUsersRolesDescription": "Pozvěte uživatele a přidejte je do rolí pro správu přístupu k organizaci",
@@ -943,7 +887,7 @@
"defaultMappingsRole": "Výchozí mapování rolí",
"defaultMappingsRoleDescription": "Výsledek tohoto výrazu musí vrátit název role definovaný v organizaci jako řetězec.",
"defaultMappingsOrg": "Výchozí mapování organizace",
"defaultMappingsOrgDescription": "When set, this expression must return the organization ID or true for the user to access that organization. When unset, defining an organization policy for that org is enough: the user is allowed in as long as a valid role mapping can be resolved for them within the organization.",
"defaultMappingsOrgDescription": "Tento výraz musí vrátit org ID nebo pravdu, aby měl uživatel přístup k organizaci.",
"defaultMappingsSubmit": "Uložit výchozí mapování",
"orgPoliciesEdit": "Upravit zásady organizace",
"org": "Organizace",
@@ -1175,7 +1119,6 @@
"setupTokenDescription": "Zadejte nastavovací token z konzole serveru.",
"setupTokenRequired": "Je vyžadován token nastavení",
"actionUpdateSite": "Aktualizovat stránku",
"actionResetSiteBandwidth": "Reset Organization Bandwidth",
"actionListSiteRoles": "Seznam povolených rolí webu",
"actionCreateResource": "Vytvořit zdroj",
"actionDeleteResource": "Odstranit dokument",
@@ -1205,7 +1148,6 @@
"actionRemoveUser": "Odstranit uživatele",
"actionListUsers": "Seznam uživatelů",
"actionAddUserRole": "Přidat uživatelskou roli",
"actionSetUserOrgRoles": "Set User Roles",
"actionGenerateAccessToken": "Generovat přístupový token",
"actionDeleteAccessToken": "Odstranit přístupový token",
"actionListAccessTokens": "Seznam přístupových tokenů",
@@ -1322,7 +1264,6 @@
"sidebarRoles": "Role",
"sidebarShareableLinks": "Odkazy",
"sidebarApiKeys": "API klíče",
"sidebarProvisioning": "Provisioning",
"sidebarSettings": "Nastavení",
"sidebarAllUsers": "Všichni uživatelé",
"sidebarIdentityProviders": "Poskytovatelé identity",
@@ -1948,40 +1889,6 @@
"exitNode": "Ukončit uzel",
"country": "L 343, 22.12.2009, s. 1).",
"rulesMatchCountry": "Aktuálně založené na zdrojové IP adrese",
"region": "Region",
"selectRegion": "Select region",
"searchRegions": "Search regions...",
"noRegionFound": "No region found.",
"rulesMatchRegion": "Select a regional grouping of countries",
"rulesErrorInvalidRegion": "Invalid region",
"rulesErrorInvalidRegionDescription": "Please select a valid region.",
"regionAfrica": "Africa",
"regionNorthernAfrica": "Northern Africa",
"regionEasternAfrica": "Eastern Africa",
"regionMiddleAfrica": "Middle Africa",
"regionSouthernAfrica": "Southern Africa",
"regionWesternAfrica": "Western Africa",
"regionAmericas": "Americas",
"regionCaribbean": "Caribbean",
"regionCentralAmerica": "Central America",
"regionSouthAmerica": "South America",
"regionNorthernAmerica": "Northern America",
"regionAsia": "Asia",
"regionCentralAsia": "Central Asia",
"regionEasternAsia": "Eastern Asia",
"regionSouthEasternAsia": "South-Eastern Asia",
"regionSouthernAsia": "Southern Asia",
"regionWesternAsia": "Western Asia",
"regionEurope": "Europe",
"regionEasternEurope": "Eastern Europe",
"regionNorthernEurope": "Northern Europe",
"regionSouthernEurope": "Southern Europe",
"regionWesternEurope": "Western Europe",
"regionOceania": "Oceania",
"regionAustraliaAndNewZealand": "Australia and New Zealand",
"regionMelanesia": "Melanesia",
"regionMicronesia": "Micronesia",
"regionPolynesia": "Polynesia",
"managedSelfHosted": {
"title": "Spravované vlastní hostování",
"description": "Spolehlivější a nízko udržovaný Pangolinův server s dalšími zvony a bičkami",
@@ -2030,25 +1937,6 @@
"invalidValue": "Neplatná hodnota",
"idpTypeLabel": "Typ poskytovatele identity",
"roleMappingExpressionPlaceholder": "např. obsahuje(skupiny, 'admin') && 'Admin' || 'Member'",
"roleMappingModeFixedRoles": "Fixed Roles",
"roleMappingModeMappingBuilder": "Mapping Builder",
"roleMappingModeRawExpression": "Raw Expression",
"roleMappingFixedRolesPlaceholderSelect": "Select one or more roles",
"roleMappingFixedRolesPlaceholderFreeform": "Type role names (exact match per organization)",
"roleMappingFixedRolesDescriptionSameForAll": "Assign the same role set to every auto-provisioned user.",
"roleMappingFixedRolesDescriptionDefaultPolicy": "For default policies, type role names that exist in each organization where users are provisioned. Names must match exactly.",
"roleMappingClaimPath": "Claim Path",
"roleMappingClaimPathPlaceholder": "groups",
"roleMappingClaimPathDescription": "Path in the token payload that contains source values (for example, groups).",
"roleMappingMatchValue": "Match Value",
"roleMappingAssignRoles": "Assign Roles",
"roleMappingAddMappingRule": "Add Mapping Rule",
"roleMappingRawExpressionResultDescription": "Expression must evaluate to a string or string array.",
"roleMappingRawExpressionResultDescriptionSingleRole": "Expression must evaluate to a string (a single role name).",
"roleMappingMatchValuePlaceholder": "Match value (for example: admin)",
"roleMappingAssignRolesPlaceholderFreeform": "Type role names (exact per org)",
"roleMappingBuilderFreeformRowHint": "Role names must match a role in each target organization.",
"roleMappingRemoveRule": "Remove",
"idpGoogleConfiguration": "Konfigurace Google",
"idpGoogleConfigurationDescription": "Konfigurace přihlašovacích údajů Google OAuth2",
"idpGoogleClientIdDescription": "Google OAuth2 Client ID",
@@ -2445,8 +2333,6 @@
"logRetentionAccessDescription": "Jak dlouho uchovávat přístupové záznamy",
"logRetentionActionLabel": "Uchovávání protokolu akcí",
"logRetentionActionDescription": "Jak dlouho uchovávat záznamy akcí",
"logRetentionConnectionLabel": "Connection Log Retention",
"logRetentionConnectionDescription": "How long to retain connection logs",
"logRetentionDisabled": "Zakázáno",
"logRetention3Days": "3 dny",
"logRetention7Days": "7 dní",
@@ -2457,14 +2343,8 @@
"logRetentionEndOfFollowingYear": "Konec následujícího roku",
"actionLogsDescription": "Zobrazit historii akcí provedených v této organizaci",
"accessLogsDescription": "Zobrazit žádosti o ověření přístupu pro zdroje v této organizaci",
"connectionLogs": "Connection Logs",
"connectionLogsDescription": "View connection logs for tunnels in this organization",
"sidebarLogsConnection": "Connection Logs",
"sourceAddress": "Source Address",
"destinationAddress": "Destination Address",
"duration": "Duration",
"licenseRequiredToUse": "An <enterpriseLicenseLink>Enterprise Edition</enterpriseLicenseLink> license or <pangolinCloudLink>Pangolin Cloud</pangolinCloudLink> is required to use this feature. <bookADemoLink>Book a free demo or POC trial to learn more</bookADemoLink>.",
"ossEnterpriseEditionRequired": "The <enterpriseEditionLink>Enterprise Edition</enterpriseEditionLink> is required to use this feature. This feature is also available in <pangolinCloudLink>Pangolin Cloud</pangolinCloudLink>. <bookADemoLink>Book a free demo or POC trial to learn more</bookADemoLink>.",
"licenseRequiredToUse": "Pro použití této funkce je vyžadována licence <enterpriseLicenseLink>Enterprise Edition</enterpriseLicenseLink> nebo <pangolinCloudLink>Pangolin Cloud</pangolinCloudLink> . <bookADemoLink>Zarezervujte si demo nebo POC zkušební verzi</bookADemoLink>.",
"ossEnterpriseEditionRequired": "<enterpriseEditionLink>Enterprise Edition</enterpriseEditionLink> je vyžadována pro použití této funkce. Tato funkce je také k dispozici v <pangolinCloudLink>Pangolin Cloud</pangolinCloudLink>. <bookADemoLink>Rezervujte si demo nebo POC zkušební verzi</bookADemoLink>.",
"certResolver": "Oddělovač certifikátů",
"certResolverDescription": "Vyberte řešitele certifikátů pro tento dokument.",
"selectCertResolver": "Vyberte řešič certifikátů",
@@ -2802,6 +2682,5 @@
"approvalsEmptyStateStep2Description": "Upravte roli a povolte možnost 'Vyžadovat schválení zařízení'. Uživatelé s touto rolí budou potřebovat schválení pro nová zařízení správce.",
"approvalsEmptyStatePreviewDescription": "Náhled: Pokud je povoleno, čekající na zařízení se zde zobrazí žádosti o recenzi",
"approvalsEmptyStateButtonText": "Spravovat role",
"domainErrorTitle": "Máme problém s ověřením tvé domény",
"idpAdminAutoProvisionPoliciesTabHint": "Configure role mapping and organization policies on the <policiesTabLink>Auto Provision Settings</policiesTabLink> tab."
"domainErrorTitle": "Máme problém s ověřením tvé domény"
}

View File

@@ -148,11 +148,6 @@
"createLink": "Link erstellen",
"resourcesNotFound": "Keine Ressourcen gefunden",
"resourceSearch": "Suche Ressourcen",
"machineSearch": "Search machines",
"machinesSearch": "Search machine clients...",
"machineNotFound": "No machines found",
"userDeviceSearch": "Search user devices",
"userDevicesSearch": "Search user devices...",
"openMenu": "Menü öffnen",
"resource": "Ressource",
"title": "Titel",
@@ -328,54 +323,6 @@
"apiKeysDelete": "API-Schlüssel löschen",
"apiKeysManage": "API-Schlüssel verwalten",
"apiKeysDescription": "API-Schlüssel werden zur Authentifizierung mit der Integrations-API verwendet",
"provisioningKeysTitle": "Provisioning Key",
"provisioningKeysManage": "Manage Provisioning Keys",
"provisioningKeysDescription": "Provisioning keys are used to authenticate automated site provisioning for your organization.",
"provisioningManage": "Provisioning",
"provisioningDescription": "Manage provisioning keys and review pending sites awaiting approval.",
"pendingSites": "Pending Sites",
"siteApproveSuccess": "Site approved successfully",
"siteApproveError": "Error approving site",
"provisioningKeys": "Provisioning Keys",
"searchProvisioningKeys": "Search provisioning keys...",
"provisioningKeysAdd": "Generate Provisioning Key",
"provisioningKeysErrorDelete": "Error deleting provisioning key",
"provisioningKeysErrorDeleteMessage": "Error deleting provisioning key",
"provisioningKeysQuestionRemove": "Are you sure you want to remove this provisioning key from the organization?",
"provisioningKeysMessageRemove": "Once removed, the key can no longer be used for site provisioning.",
"provisioningKeysDeleteConfirm": "Confirm Delete Provisioning Key",
"provisioningKeysDelete": "Delete Provisioning key",
"provisioningKeysCreate": "Generate Provisioning Key",
"provisioningKeysCreateDescription": "Generate a new provisioning key for the organization",
"provisioningKeysSeeAll": "See all provisioning keys",
"provisioningKeysSave": "Save the provisioning key",
"provisioningKeysSaveDescription": "You will only be able to see this once. Copy it to a secure place.",
"provisioningKeysErrorCreate": "Error creating provisioning key",
"provisioningKeysList": "New provisioning key",
"provisioningKeysMaxBatchSize": "Max batch size",
"provisioningKeysUnlimitedBatchSize": "Unlimited batch size (no limit)",
"provisioningKeysMaxBatchUnlimited": "Unlimited",
"provisioningKeysMaxBatchSizeInvalid": "Enter a valid max batch size (11,000,000).",
"provisioningKeysValidUntil": "Valid until",
"provisioningKeysValidUntilHint": "Leave empty for no expiration.",
"provisioningKeysValidUntilInvalid": "Enter a valid date and time.",
"provisioningKeysNumUsed": "Times used",
"provisioningKeysLastUsed": "Last used",
"provisioningKeysNoExpiry": "No expiration",
"provisioningKeysNeverUsed": "Never",
"provisioningKeysEdit": "Edit Provisioning Key",
"provisioningKeysEditDescription": "Update the max batch size and expiration time for this key.",
"provisioningKeysApproveNewSites": "Approve new sites",
"provisioningKeysApproveNewSitesDescription": "Automatically approve sites that register with this key.",
"provisioningKeysUpdateError": "Error updating provisioning key",
"provisioningKeysUpdated": "Provisioning key updated",
"provisioningKeysUpdatedDescription": "Your changes have been saved.",
"provisioningKeysBannerTitle": "Site Provisioning Keys",
"provisioningKeysBannerDescription": "Generate a provisioning key and use it with the Newt connector to automatically create sites on first startup — no need to set up separate credentials for each site.",
"provisioningKeysBannerButtonText": "Learn More",
"pendingSitesBannerTitle": "Pending Sites",
"pendingSitesBannerDescription": "Sites that connect using a provisioning key appear here for review. Approve each site before it becomes active and gains access to your resources.",
"pendingSitesBannerButtonText": "Learn More",
"apiKeysSettings": "{apiKeyName} Einstellungen",
"userTitle": "Alle Benutzer verwalten",
"userDescription": "Alle Benutzer im System anzeigen und verwalten",
@@ -562,12 +509,9 @@
"userSaved": "Benutzer gespeichert",
"userSavedDescription": "Der Benutzer wurde aktualisiert.",
"autoProvisioned": "Automatisch bereitgestellt",
"autoProvisionSettings": "Auto Provision Settings",
"autoProvisionedDescription": "Erlaube diesem Benutzer die automatische Verwaltung durch Identitätsanbieter",
"accessControlsDescription": "Verwalten Sie, worauf dieser Benutzer in der Organisation zugreifen und was er tun kann",
"accessControlsSubmit": "Zugriffskontrollen speichern",
"singleRolePerUserPlanNotice": "Your plan only supports one role per user.",
"singleRolePerUserEditionNotice": "This edition only supports one role per user.",
"roles": "Rollen",
"accessUsersRoles": "Benutzer & Rollen verwalten",
"accessUsersRolesDescription": "Lade Benutzer ein und füge sie zu Rollen hinzu, um den Zugriff auf die Organisation zu verwalten",
@@ -943,7 +887,7 @@
"defaultMappingsRole": "Standard-Rollenzuordnung",
"defaultMappingsRoleDescription": "JMESPath zur Extraktion von Rolleninformationen aus dem ID-Token. Das Ergebnis dieses Ausdrucks muss den Rollennamen als String zurückgeben, wie er in der Organisation definiert ist.",
"defaultMappingsOrg": "Standard-Organisationszuordnung",
"defaultMappingsOrgDescription": "When set, this expression must return the organization ID or true for the user to access that organization. When unset, defining an organization policy for that org is enough: the user is allowed in as long as a valid role mapping can be resolved for them within the organization.",
"defaultMappingsOrgDescription": "JMESPath zur Extraktion von Organisationsinformationen aus dem ID-Token. Dieser Ausdruck muss die Organisations-ID oder true zurückgeben, damit der Benutzer Zugriff auf die Organisation erhält.",
"defaultMappingsSubmit": "Standardzuordnungen speichern",
"orgPoliciesEdit": "Organisationsrichtlinie bearbeiten",
"org": "Organisation",
@@ -1175,7 +1119,6 @@
"setupTokenDescription": "Geben Sie das Setup-Token von der Serverkonsole ein.",
"setupTokenRequired": "Setup-Token ist erforderlich",
"actionUpdateSite": "Standorte aktualisieren",
"actionResetSiteBandwidth": "Reset Organization Bandwidth",
"actionListSiteRoles": "Erlaubte Standort-Rollen auflisten",
"actionCreateResource": "Ressource erstellen",
"actionDeleteResource": "Ressource löschen",
@@ -1205,7 +1148,6 @@
"actionRemoveUser": "Benutzer entfernen",
"actionListUsers": "Benutzer auflisten",
"actionAddUserRole": "Benutzerrolle hinzufügen",
"actionSetUserOrgRoles": "Set User Roles",
"actionGenerateAccessToken": "Zugriffstoken generieren",
"actionDeleteAccessToken": "Zugriffstoken löschen",
"actionListAccessTokens": "Zugriffstoken auflisten",
@@ -1322,7 +1264,6 @@
"sidebarRoles": "Rollen",
"sidebarShareableLinks": "Links",
"sidebarApiKeys": "API-Schlüssel",
"sidebarProvisioning": "Provisioning",
"sidebarSettings": "Einstellungen",
"sidebarAllUsers": "Alle Benutzer",
"sidebarIdentityProviders": "Identitätsanbieter",
@@ -1948,40 +1889,6 @@
"exitNode": "Exit-Node",
"country": "Land",
"rulesMatchCountry": "Derzeit basierend auf der Quell-IP",
"region": "Region",
"selectRegion": "Select region",
"searchRegions": "Search regions...",
"noRegionFound": "No region found.",
"rulesMatchRegion": "Select a regional grouping of countries",
"rulesErrorInvalidRegion": "Invalid region",
"rulesErrorInvalidRegionDescription": "Please select a valid region.",
"regionAfrica": "Africa",
"regionNorthernAfrica": "Northern Africa",
"regionEasternAfrica": "Eastern Africa",
"regionMiddleAfrica": "Middle Africa",
"regionSouthernAfrica": "Southern Africa",
"regionWesternAfrica": "Western Africa",
"regionAmericas": "Americas",
"regionCaribbean": "Caribbean",
"regionCentralAmerica": "Central America",
"regionSouthAmerica": "South America",
"regionNorthernAmerica": "Northern America",
"regionAsia": "Asia",
"regionCentralAsia": "Central Asia",
"regionEasternAsia": "Eastern Asia",
"regionSouthEasternAsia": "South-Eastern Asia",
"regionSouthernAsia": "Southern Asia",
"regionWesternAsia": "Western Asia",
"regionEurope": "Europe",
"regionEasternEurope": "Eastern Europe",
"regionNorthernEurope": "Northern Europe",
"regionSouthernEurope": "Southern Europe",
"regionWesternEurope": "Western Europe",
"regionOceania": "Oceania",
"regionAustraliaAndNewZealand": "Australia and New Zealand",
"regionMelanesia": "Melanesia",
"regionMicronesia": "Micronesia",
"regionPolynesia": "Polynesia",
"managedSelfHosted": {
"title": "Verwaltetes Selbsthosted",
"description": "Zuverlässiger und wartungsarmer Pangolin Server mit zusätzlichen Glocken und Pfeifen",
@@ -2030,25 +1937,6 @@
"invalidValue": "Ungültiger Wert",
"idpTypeLabel": "Identitätsanbietertyp",
"roleMappingExpressionPlaceholder": "z. B. enthalten(Gruppen, 'admin') && 'Admin' || 'Mitglied'",
"roleMappingModeFixedRoles": "Fixed Roles",
"roleMappingModeMappingBuilder": "Mapping Builder",
"roleMappingModeRawExpression": "Raw Expression",
"roleMappingFixedRolesPlaceholderSelect": "Select one or more roles",
"roleMappingFixedRolesPlaceholderFreeform": "Type role names (exact match per organization)",
"roleMappingFixedRolesDescriptionSameForAll": "Assign the same role set to every auto-provisioned user.",
"roleMappingFixedRolesDescriptionDefaultPolicy": "For default policies, type role names that exist in each organization where users are provisioned. Names must match exactly.",
"roleMappingClaimPath": "Claim Path",
"roleMappingClaimPathPlaceholder": "groups",
"roleMappingClaimPathDescription": "Path in the token payload that contains source values (for example, groups).",
"roleMappingMatchValue": "Match Value",
"roleMappingAssignRoles": "Assign Roles",
"roleMappingAddMappingRule": "Add Mapping Rule",
"roleMappingRawExpressionResultDescription": "Expression must evaluate to a string or string array.",
"roleMappingRawExpressionResultDescriptionSingleRole": "Expression must evaluate to a string (a single role name).",
"roleMappingMatchValuePlaceholder": "Match value (for example: admin)",
"roleMappingAssignRolesPlaceholderFreeform": "Type role names (exact per org)",
"roleMappingBuilderFreeformRowHint": "Role names must match a role in each target organization.",
"roleMappingRemoveRule": "Remove",
"idpGoogleConfiguration": "Google-Konfiguration",
"idpGoogleConfigurationDescription": "Google OAuth2 Zugangsdaten konfigurieren",
"idpGoogleClientIdDescription": "Google OAuth2 Client ID",
@@ -2445,8 +2333,6 @@
"logRetentionAccessDescription": "Wie lange Zugriffsprotokolle beibehalten werden sollen",
"logRetentionActionLabel": "Aktionsprotokoll-Speicherung",
"logRetentionActionDescription": "Dauer des Action-Logs",
"logRetentionConnectionLabel": "Connection Log Retention",
"logRetentionConnectionDescription": "How long to retain connection logs",
"logRetentionDisabled": "Deaktiviert",
"logRetention3Days": "3 Tage",
"logRetention7Days": "7 Tage",
@@ -2457,14 +2343,8 @@
"logRetentionEndOfFollowingYear": "Ende des folgenden Jahres",
"actionLogsDescription": "Verlauf der in dieser Organisation durchgeführten Aktionen anzeigen",
"accessLogsDescription": "Zugriffsauth-Anfragen für Ressourcen in dieser Organisation anzeigen",
"connectionLogs": "Connection Logs",
"connectionLogsDescription": "View connection logs for tunnels in this organization",
"sidebarLogsConnection": "Connection Logs",
"sourceAddress": "Source Address",
"destinationAddress": "Destination Address",
"duration": "Duration",
"licenseRequiredToUse": "An <enterpriseLicenseLink>Enterprise Edition</enterpriseLicenseLink> license or <pangolinCloudLink>Pangolin Cloud</pangolinCloudLink> is required to use this feature. <bookADemoLink>Book a free demo or POC trial to learn more</bookADemoLink>.",
"ossEnterpriseEditionRequired": "The <enterpriseEditionLink>Enterprise Edition</enterpriseEditionLink> is required to use this feature. This feature is also available in <pangolinCloudLink>Pangolin Cloud</pangolinCloudLink>. <bookADemoLink>Book a free demo or POC trial to learn more</bookADemoLink>.",
"licenseRequiredToUse": "Eine <enterpriseLicenseLink>Enterprise Edition</enterpriseLicenseLink> Lizenz oder <pangolinCloudLink>Pangolin Cloud</pangolinCloudLink> wird benötigt, um diese Funktion nutzen zu können. <bookADemoLink>Buchen Sie eine Demo oder POC Testversion</bookADemoLink>.",
"ossEnterpriseEditionRequired": "Die <enterpriseEditionLink>Enterprise Edition</enterpriseEditionLink> wird benötigt, um diese Funktion nutzen zu können. Diese Funktion ist auch in <pangolinCloudLink>Pangolin Cloud</pangolinCloudLink>verfügbar. <bookADemoLink>Buchen Sie eine Demo oder POC Testversion</bookADemoLink>.",
"certResolver": "Zertifikatsauflöser",
"certResolverDescription": "Wählen Sie den Zertifikatslöser aus, der für diese Ressource verwendet werden soll.",
"selectCertResolver": "Zertifikatsauflöser auswählen",
@@ -2802,6 +2682,5 @@
"approvalsEmptyStateStep2Description": "Bearbeite eine Rolle und aktiviere die Option 'Gerätegenehmigung erforderlich'. Benutzer mit dieser Rolle benötigen Administrator-Genehmigung für neue Geräte.",
"approvalsEmptyStatePreviewDescription": "Vorschau: Wenn aktiviert, werden ausstehende Geräteanfragen hier zur Überprüfung angezeigt",
"approvalsEmptyStateButtonText": "Rollen verwalten",
"domainErrorTitle": "Wir haben Probleme mit der Überprüfung deiner Domain",
"idpAdminAutoProvisionPoliciesTabHint": "Configure role mapping and organization policies on the <policiesTabLink>Auto Provision Settings</policiesTabLink> tab."
"domainErrorTitle": "Wir haben Probleme mit der Überprüfung deiner Domain"
}

View File

@@ -148,11 +148,6 @@
"createLink": "Create Link",
"resourcesNotFound": "No resources found",
"resourceSearch": "Search resources",
"machineSearch": "Search machines",
"machinesSearch": "Search machine clients...",
"machineNotFound": "No machines found",
"userDeviceSearch": "Search user devices",
"userDevicesSearch": "Search user devices...",
"openMenu": "Open menu",
"resource": "Resource",
"title": "Title",
@@ -328,41 +323,6 @@
"apiKeysDelete": "Delete API Key",
"apiKeysManage": "Manage API Keys",
"apiKeysDescription": "API keys are used to authenticate with the integration API",
"provisioningKeysTitle": "Provisioning Key",
"provisioningKeysManage": "Manage Provisioning Keys",
"provisioningKeysDescription": "Provisioning keys are used to authenticate automated site provisioning for your organization.",
"provisioningKeys": "Provisioning Keys",
"searchProvisioningKeys": "Search provisioning keys...",
"provisioningKeysAdd": "Generate Provisioning Key",
"provisioningKeysErrorDelete": "Error deleting provisioning key",
"provisioningKeysErrorDeleteMessage": "Error deleting provisioning key",
"provisioningKeysQuestionRemove": "Are you sure you want to remove this provisioning key from the organization?",
"provisioningKeysMessageRemove": "Once removed, the key can no longer be used for site provisioning.",
"provisioningKeysDeleteConfirm": "Confirm Delete Provisioning Key",
"provisioningKeysDelete": "Delete Provisioning key",
"provisioningKeysCreate": "Generate Provisioning Key",
"provisioningKeysCreateDescription": "Generate a new provisioning key for the organization",
"provisioningKeysSeeAll": "See all provisioning keys",
"provisioningKeysSave": "Save the provisioning key",
"provisioningKeysSaveDescription": "You will only be able to see this once. Copy it to a secure place.",
"provisioningKeysErrorCreate": "Error creating provisioning key",
"provisioningKeysList": "New provisioning key",
"provisioningKeysMaxBatchSize": "Max batch size",
"provisioningKeysUnlimitedBatchSize": "Unlimited batch size (no limit)",
"provisioningKeysMaxBatchUnlimited": "Unlimited",
"provisioningKeysMaxBatchSizeInvalid": "Enter a valid max batch size (11,000,000).",
"provisioningKeysValidUntil": "Valid until",
"provisioningKeysValidUntilHint": "Leave empty for no expiration.",
"provisioningKeysValidUntilInvalid": "Enter a valid date and time.",
"provisioningKeysNumUsed": "Times used",
"provisioningKeysLastUsed": "Last used",
"provisioningKeysNoExpiry": "No expiration",
"provisioningKeysNeverUsed": "Never",
"provisioningKeysEdit": "Edit Provisioning Key",
"provisioningKeysEditDescription": "Update the max batch size and expiration time for this key.",
"provisioningKeysUpdateError": "Error updating provisioning key",
"provisioningKeysUpdated": "Provisioning key updated",
"provisioningKeysUpdatedDescription": "Your changes have been saved.",
"apiKeysSettings": "{apiKeyName} Settings",
"userTitle": "Manage All Users",
"userDescription": "View and manage all users in the system",
@@ -549,12 +509,9 @@
"userSaved": "User saved",
"userSavedDescription": "The user has been updated.",
"autoProvisioned": "Auto Provisioned",
"autoProvisionSettings": "Auto Provision Settings",
"autoProvisionedDescription": "Allow this user to be automatically managed by identity provider",
"accessControlsDescription": "Manage what this user can access and do in the organization",
"accessControlsSubmit": "Save Access Controls",
"singleRolePerUserPlanNotice": "Your plan only supports one role per user.",
"singleRolePerUserEditionNotice": "This edition only supports one role per user.",
"roles": "Roles",
"accessUsersRoles": "Manage Users & Roles",
"accessUsersRolesDescription": "Invite users and add them to roles to manage access to the organization",
@@ -930,7 +887,7 @@
"defaultMappingsRole": "Default Role Mapping",
"defaultMappingsRoleDescription": "The result of this expression must return the role name as defined in the organization as a string.",
"defaultMappingsOrg": "Default Organization Mapping",
"defaultMappingsOrgDescription": "When set, this expression must return the organization ID or true for the user to access that organization. When unset, defining an organization policy for that org is enough: the user is allowed in as long as a valid role mapping can be resolved for them within the organization.",
"defaultMappingsOrgDescription": "This expression must return the org ID or true for the user to be allowed to access the organization.",
"defaultMappingsSubmit": "Save Default Mappings",
"orgPoliciesEdit": "Edit Organization Policy",
"org": "Organization",
@@ -1083,6 +1040,7 @@
"pageNotFoundDescription": "Oops! The page you're looking for doesn't exist.",
"overview": "Overview",
"home": "Home",
"accessControl": "Access Control",
"settings": "Settings",
"usersAll": "All Users",
"license": "License",
@@ -1192,7 +1150,6 @@
"actionRemoveUser": "Remove User",
"actionListUsers": "List Users",
"actionAddUserRole": "Add User Role",
"actionSetUserOrgRoles": "Set User Roles",
"actionGenerateAccessToken": "Generate Access Token",
"actionDeleteAccessToken": "Delete Access Token",
"actionListAccessTokens": "List Access Tokens",
@@ -1309,7 +1266,6 @@
"sidebarRoles": "Roles",
"sidebarShareableLinks": "Links",
"sidebarApiKeys": "API Keys",
"sidebarProvisioning": "Provisioning",
"sidebarSettings": "Settings",
"sidebarAllUsers": "All Users",
"sidebarIdentityProviders": "Identity Providers",
@@ -1935,40 +1891,6 @@
"exitNode": "Exit Node",
"country": "Country",
"rulesMatchCountry": "Currently based on source IP",
"region": "Region",
"selectRegion": "Select region",
"searchRegions": "Search regions...",
"noRegionFound": "No region found.",
"rulesMatchRegion": "Select a regional grouping of countries",
"rulesErrorInvalidRegion": "Invalid region",
"rulesErrorInvalidRegionDescription": "Please select a valid region.",
"regionAfrica": "Africa",
"regionNorthernAfrica": "Northern Africa",
"regionEasternAfrica": "Eastern Africa",
"regionMiddleAfrica": "Middle Africa",
"regionSouthernAfrica": "Southern Africa",
"regionWesternAfrica": "Western Africa",
"regionAmericas": "Americas",
"regionCaribbean": "Caribbean",
"regionCentralAmerica": "Central America",
"regionSouthAmerica": "South America",
"regionNorthernAmerica": "Northern America",
"regionAsia": "Asia",
"regionCentralAsia": "Central Asia",
"regionEasternAsia": "Eastern Asia",
"regionSouthEasternAsia": "South-Eastern Asia",
"regionSouthernAsia": "Southern Asia",
"regionWesternAsia": "Western Asia",
"regionEurope": "Europe",
"regionEasternEurope": "Eastern Europe",
"regionNorthernEurope": "Northern Europe",
"regionSouthernEurope": "Southern Europe",
"regionWesternEurope": "Western Europe",
"regionOceania": "Oceania",
"regionAustraliaAndNewZealand": "Australia and New Zealand",
"regionMelanesia": "Melanesia",
"regionMicronesia": "Micronesia",
"regionPolynesia": "Polynesia",
"managedSelfHosted": {
"title": "Managed Self-Hosted",
"description": "More reliable and low-maintenance self-hosted Pangolin server with extra bells and whistles",
@@ -2017,25 +1939,6 @@
"invalidValue": "Invalid value",
"idpTypeLabel": "Identity Provider Type",
"roleMappingExpressionPlaceholder": "e.g., contains(groups, 'admin') && 'Admin' || 'Member'",
"roleMappingModeFixedRoles": "Fixed Roles",
"roleMappingModeMappingBuilder": "Mapping Builder",
"roleMappingModeRawExpression": "Raw Expression",
"roleMappingFixedRolesPlaceholderSelect": "Select one or more roles",
"roleMappingFixedRolesPlaceholderFreeform": "Type role names (exact match per organization)",
"roleMappingFixedRolesDescriptionSameForAll": "Assign the same role set to every auto-provisioned user.",
"roleMappingFixedRolesDescriptionDefaultPolicy": "For default policies, type role names that exist in each organization where users are provisioned. Names must match exactly.",
"roleMappingClaimPath": "Claim Path",
"roleMappingClaimPathPlaceholder": "groups",
"roleMappingClaimPathDescription": "Path in the token payload that contains source values (for example, groups).",
"roleMappingMatchValue": "Match Value",
"roleMappingAssignRoles": "Assign Roles",
"roleMappingAddMappingRule": "Add Mapping Rule",
"roleMappingRawExpressionResultDescription": "Expression must evaluate to a string or string array.",
"roleMappingRawExpressionResultDescriptionSingleRole": "Expression must evaluate to a string (a single role name).",
"roleMappingMatchValuePlaceholder": "Match value (for example: admin)",
"roleMappingAssignRolesPlaceholderFreeform": "Type role names (exact per org)",
"roleMappingBuilderFreeformRowHint": "Role names must match a role in each target organization.",
"roleMappingRemoveRule": "Remove",
"idpGoogleConfiguration": "Google Configuration",
"idpGoogleConfigurationDescription": "Configure the Google OAuth2 credentials",
"idpGoogleClientIdDescription": "Google OAuth2 Client ID",
@@ -2432,8 +2335,6 @@
"logRetentionAccessDescription": "How long to retain access logs",
"logRetentionActionLabel": "Action Log Retention",
"logRetentionActionDescription": "How long to retain action logs",
"logRetentionConnectionLabel": "Connection Log Retention",
"logRetentionConnectionDescription": "How long to retain connection logs",
"logRetentionDisabled": "Disabled",
"logRetention3Days": "3 days",
"logRetention7Days": "7 days",
@@ -2444,12 +2345,6 @@
"logRetentionEndOfFollowingYear": "End of following year",
"actionLogsDescription": "View a history of actions performed in this organization",
"accessLogsDescription": "View access auth requests for resources in this organization",
"connectionLogs": "Connection Logs",
"connectionLogsDescription": "View connection logs for tunnels in this organization",
"sidebarLogsConnection": "Connection Logs",
"sourceAddress": "Source Address",
"destinationAddress": "Destination Address",
"duration": "Duration",
"licenseRequiredToUse": "An <enterpriseLicenseLink>Enterprise Edition</enterpriseLicenseLink> license or <pangolinCloudLink>Pangolin Cloud</pangolinCloudLink> is required to use this feature. <bookADemoLink>Book a demo or POC trial</bookADemoLink>.",
"ossEnterpriseEditionRequired": "The <enterpriseEditionLink>Enterprise Edition</enterpriseEditionLink> is required to use this feature. This feature is also available in <pangolinCloudLink>Pangolin Cloud</pangolinCloudLink>. <bookADemoLink>Book a demo or POC trial</bookADemoLink>.",
"certResolver": "Certificate Resolver",
@@ -2616,9 +2511,9 @@
"remoteExitNodeRegenerateCredentialsConfirmation": "Are you sure you want to regenerate the credentials for this remote exit node?",
"remoteExitNodeRegenerateCredentialsWarning": "This will regenerate the credentials. The remote exit node will stay connected until you manually restart it and use the new credentials.",
"agent": "Agent",
"personalUseOnly": "Personal Use Only",
"loginPageLicenseWatermark": "This instance is licensed for personal use only.",
"instanceIsUnlicensed": "This instance is unlicensed.",
"personalUseOnly": "Personal Use Only",
"loginPageLicenseWatermark": "This instance is licensed for personal use only.",
"instanceIsUnlicensed": "This instance is unlicensed.",
"portRestrictions": "Port Restrictions",
"allPorts": "All",
"custom": "Custom",
@@ -2672,7 +2567,7 @@
"automaticModeDescription": " Show maintenance page only when all backend targets are down or unhealthy. Your resource continues working normally as long as at least one target is healthy.",
"forced": "Forced",
"forcedModeDescription": "Always show the maintenance page regardless of backend health. Use this for planned maintenance when you want to prevent all access.",
"warning:": "Warning:",
"warning:" : "Warning:",
"forcedeModeWarning": "All traffic will be directed to the maintenance page. Your backend resources will not receive any requests.",
"pageTitle": "Page Title",
"pageTitleDescription": "The main heading displayed on the maintenance page",
@@ -2789,6 +2684,5 @@
"approvalsEmptyStateStep2Description": "Edit a role and enable the 'Require Device Approvals' option. Users with this role will need admin approval for new devices.",
"approvalsEmptyStatePreviewDescription": "Preview: When enabled, pending device requests will appear here for review",
"approvalsEmptyStateButtonText": "Manage Roles",
"domainErrorTitle": "We are having trouble verifying your domain",
"idpAdminAutoProvisionPoliciesTabHint": "Configure role mapping and organization policies on the <policiesTabLink>Auto Provision Settings</policiesTabLink> tab."
"domainErrorTitle": "We are having trouble verifying your domain"
}

View File

@@ -148,11 +148,6 @@
"createLink": "Crear enlace",
"resourcesNotFound": "No se encontraron recursos",
"resourceSearch": "Buscar recursos",
"machineSearch": "Search machines",
"machinesSearch": "Search machine clients...",
"machineNotFound": "No machines found",
"userDeviceSearch": "Search user devices",
"userDevicesSearch": "Search user devices...",
"openMenu": "Abrir menú",
"resource": "Recurso",
"title": "Título",
@@ -328,54 +323,6 @@
"apiKeysDelete": "Borrar Clave API",
"apiKeysManage": "Administrar claves API",
"apiKeysDescription": "Las claves API se utilizan para autenticar con la API de integración",
"provisioningKeysTitle": "Provisioning Key",
"provisioningKeysManage": "Manage Provisioning Keys",
"provisioningKeysDescription": "Provisioning keys are used to authenticate automated site provisioning for your organization.",
"provisioningManage": "Provisioning",
"provisioningDescription": "Manage provisioning keys and review pending sites awaiting approval.",
"pendingSites": "Pending Sites",
"siteApproveSuccess": "Site approved successfully",
"siteApproveError": "Error approving site",
"provisioningKeys": "Provisioning Keys",
"searchProvisioningKeys": "Search provisioning keys...",
"provisioningKeysAdd": "Generate Provisioning Key",
"provisioningKeysErrorDelete": "Error deleting provisioning key",
"provisioningKeysErrorDeleteMessage": "Error deleting provisioning key",
"provisioningKeysQuestionRemove": "Are you sure you want to remove this provisioning key from the organization?",
"provisioningKeysMessageRemove": "Once removed, the key can no longer be used for site provisioning.",
"provisioningKeysDeleteConfirm": "Confirm Delete Provisioning Key",
"provisioningKeysDelete": "Delete Provisioning key",
"provisioningKeysCreate": "Generate Provisioning Key",
"provisioningKeysCreateDescription": "Generate a new provisioning key for the organization",
"provisioningKeysSeeAll": "See all provisioning keys",
"provisioningKeysSave": "Save the provisioning key",
"provisioningKeysSaveDescription": "You will only be able to see this once. Copy it to a secure place.",
"provisioningKeysErrorCreate": "Error creating provisioning key",
"provisioningKeysList": "New provisioning key",
"provisioningKeysMaxBatchSize": "Max batch size",
"provisioningKeysUnlimitedBatchSize": "Unlimited batch size (no limit)",
"provisioningKeysMaxBatchUnlimited": "Unlimited",
"provisioningKeysMaxBatchSizeInvalid": "Enter a valid max batch size (11,000,000).",
"provisioningKeysValidUntil": "Valid until",
"provisioningKeysValidUntilHint": "Leave empty for no expiration.",
"provisioningKeysValidUntilInvalid": "Enter a valid date and time.",
"provisioningKeysNumUsed": "Times used",
"provisioningKeysLastUsed": "Last used",
"provisioningKeysNoExpiry": "No expiration",
"provisioningKeysNeverUsed": "Never",
"provisioningKeysEdit": "Edit Provisioning Key",
"provisioningKeysEditDescription": "Update the max batch size and expiration time for this key.",
"provisioningKeysApproveNewSites": "Approve new sites",
"provisioningKeysApproveNewSitesDescription": "Automatically approve sites that register with this key.",
"provisioningKeysUpdateError": "Error updating provisioning key",
"provisioningKeysUpdated": "Provisioning key updated",
"provisioningKeysUpdatedDescription": "Your changes have been saved.",
"provisioningKeysBannerTitle": "Site Provisioning Keys",
"provisioningKeysBannerDescription": "Generate a provisioning key and use it with the Newt connector to automatically create sites on first startup — no need to set up separate credentials for each site.",
"provisioningKeysBannerButtonText": "Learn More",
"pendingSitesBannerTitle": "Pending Sites",
"pendingSitesBannerDescription": "Sites that connect using a provisioning key appear here for review. Approve each site before it becomes active and gains access to your resources.",
"pendingSitesBannerButtonText": "Learn More",
"apiKeysSettings": "Ajustes {apiKeyName}",
"userTitle": "Administrar todos los usuarios",
"userDescription": "Ver y administrar todos los usuarios en el sistema",
@@ -562,12 +509,9 @@
"userSaved": "Usuario guardado",
"userSavedDescription": "El usuario ha sido actualizado.",
"autoProvisioned": "Auto asegurado",
"autoProvisionSettings": "Auto Provision Settings",
"autoProvisionedDescription": "Permitir a este usuario ser administrado automáticamente por el proveedor de identidad",
"accessControlsDescription": "Administrar lo que este usuario puede acceder y hacer en la organización",
"accessControlsSubmit": "Guardar controles de acceso",
"singleRolePerUserPlanNotice": "Your plan only supports one role per user.",
"singleRolePerUserEditionNotice": "This edition only supports one role per user.",
"roles": "Roles",
"accessUsersRoles": "Administrar usuarios y roles",
"accessUsersRolesDescription": "Invitar usuarios y añadirlos a roles para administrar el acceso a la organización",
@@ -943,7 +887,7 @@
"defaultMappingsRole": "Mapeo de Rol por defecto",
"defaultMappingsRoleDescription": "El resultado de esta expresión debe devolver el nombre del rol tal y como se define en la organización como una cadena.",
"defaultMappingsOrg": "Mapeo de organización por defecto",
"defaultMappingsOrgDescription": "When set, this expression must return the organization ID or true for the user to access that organization. When unset, defining an organization policy for that org is enough: the user is allowed in as long as a valid role mapping can be resolved for them within the organization.",
"defaultMappingsOrgDescription": "Esta expresión debe devolver el ID de org o verdadero para que el usuario pueda acceder a la organización.",
"defaultMappingsSubmit": "Guardar asignaciones por defecto",
"orgPoliciesEdit": "Editar Política de Organización",
"org": "Organización",
@@ -1175,7 +1119,6 @@
"setupTokenDescription": "Ingrese el token de configuración desde la consola del servidor.",
"setupTokenRequired": "Se requiere el token de configuración",
"actionUpdateSite": "Actualizar sitio",
"actionResetSiteBandwidth": "Reset Organization Bandwidth",
"actionListSiteRoles": "Lista de roles permitidos del sitio",
"actionCreateResource": "Crear Recurso",
"actionDeleteResource": "Eliminar Recurso",
@@ -1205,7 +1148,6 @@
"actionRemoveUser": "Eliminar usuario",
"actionListUsers": "Listar usuarios",
"actionAddUserRole": "Añadir rol de usuario",
"actionSetUserOrgRoles": "Set User Roles",
"actionGenerateAccessToken": "Generar token de acceso",
"actionDeleteAccessToken": "Eliminar token de acceso",
"actionListAccessTokens": "Lista de Tokens de Acceso",
@@ -1322,7 +1264,6 @@
"sidebarRoles": "Roles",
"sidebarShareableLinks": "Enlaces",
"sidebarApiKeys": "Claves API",
"sidebarProvisioning": "Provisioning",
"sidebarSettings": "Ajustes",
"sidebarAllUsers": "Todos los usuarios",
"sidebarIdentityProviders": "Proveedores de identidad",
@@ -1948,40 +1889,6 @@
"exitNode": "Nodo de Salida",
"country": "País",
"rulesMatchCountry": "Actualmente basado en IP de origen",
"region": "Region",
"selectRegion": "Select region",
"searchRegions": "Search regions...",
"noRegionFound": "No region found.",
"rulesMatchRegion": "Select a regional grouping of countries",
"rulesErrorInvalidRegion": "Invalid region",
"rulesErrorInvalidRegionDescription": "Please select a valid region.",
"regionAfrica": "Africa",
"regionNorthernAfrica": "Northern Africa",
"regionEasternAfrica": "Eastern Africa",
"regionMiddleAfrica": "Middle Africa",
"regionSouthernAfrica": "Southern Africa",
"regionWesternAfrica": "Western Africa",
"regionAmericas": "Americas",
"regionCaribbean": "Caribbean",
"regionCentralAmerica": "Central America",
"regionSouthAmerica": "South America",
"regionNorthernAmerica": "Northern America",
"regionAsia": "Asia",
"regionCentralAsia": "Central Asia",
"regionEasternAsia": "Eastern Asia",
"regionSouthEasternAsia": "South-Eastern Asia",
"regionSouthernAsia": "Southern Asia",
"regionWesternAsia": "Western Asia",
"regionEurope": "Europe",
"regionEasternEurope": "Eastern Europe",
"regionNorthernEurope": "Northern Europe",
"regionSouthernEurope": "Southern Europe",
"regionWesternEurope": "Western Europe",
"regionOceania": "Oceania",
"regionAustraliaAndNewZealand": "Australia and New Zealand",
"regionMelanesia": "Melanesia",
"regionMicronesia": "Micronesia",
"regionPolynesia": "Polynesia",
"managedSelfHosted": {
"title": "Autogestionado",
"description": "Servidor Pangolin autoalojado más fiable y de bajo mantenimiento con campanas y silbidos extra",
@@ -2030,25 +1937,6 @@
"invalidValue": "Valor inválido",
"idpTypeLabel": "Tipo de proveedor de identidad",
"roleMappingExpressionPlaceholder": "e.g., contiene(grupos, 'administrador') && 'administrador' || 'miembro'",
"roleMappingModeFixedRoles": "Fixed Roles",
"roleMappingModeMappingBuilder": "Mapping Builder",
"roleMappingModeRawExpression": "Raw Expression",
"roleMappingFixedRolesPlaceholderSelect": "Select one or more roles",
"roleMappingFixedRolesPlaceholderFreeform": "Type role names (exact match per organization)",
"roleMappingFixedRolesDescriptionSameForAll": "Assign the same role set to every auto-provisioned user.",
"roleMappingFixedRolesDescriptionDefaultPolicy": "For default policies, type role names that exist in each organization where users are provisioned. Names must match exactly.",
"roleMappingClaimPath": "Claim Path",
"roleMappingClaimPathPlaceholder": "groups",
"roleMappingClaimPathDescription": "Path in the token payload that contains source values (for example, groups).",
"roleMappingMatchValue": "Match Value",
"roleMappingAssignRoles": "Assign Roles",
"roleMappingAddMappingRule": "Add Mapping Rule",
"roleMappingRawExpressionResultDescription": "Expression must evaluate to a string or string array.",
"roleMappingRawExpressionResultDescriptionSingleRole": "Expression must evaluate to a string (a single role name).",
"roleMappingMatchValuePlaceholder": "Match value (for example: admin)",
"roleMappingAssignRolesPlaceholderFreeform": "Type role names (exact per org)",
"roleMappingBuilderFreeformRowHint": "Role names must match a role in each target organization.",
"roleMappingRemoveRule": "Remove",
"idpGoogleConfiguration": "Configuración de Google",
"idpGoogleConfigurationDescription": "Configurar las credenciales de Google OAuth2",
"idpGoogleClientIdDescription": "Google OAuth2 Client ID",
@@ -2445,8 +2333,6 @@
"logRetentionAccessDescription": "Cuánto tiempo retener los registros de acceso",
"logRetentionActionLabel": "Retención de registro de acción",
"logRetentionActionDescription": "Cuánto tiempo retener los registros de acción",
"logRetentionConnectionLabel": "Connection Log Retention",
"logRetentionConnectionDescription": "How long to retain connection logs",
"logRetentionDisabled": "Deshabilitado",
"logRetention3Days": "3 días",
"logRetention7Days": "7 días",
@@ -2457,14 +2343,8 @@
"logRetentionEndOfFollowingYear": "Fin del año siguiente",
"actionLogsDescription": "Ver un historial de acciones realizadas en esta organización",
"accessLogsDescription": "Ver solicitudes de acceso a los recursos de esta organización",
"connectionLogs": "Connection Logs",
"connectionLogsDescription": "View connection logs for tunnels in this organization",
"sidebarLogsConnection": "Connection Logs",
"sourceAddress": "Source Address",
"destinationAddress": "Destination Address",
"duration": "Duration",
"licenseRequiredToUse": "An <enterpriseLicenseLink>Enterprise Edition</enterpriseLicenseLink> license or <pangolinCloudLink>Pangolin Cloud</pangolinCloudLink> is required to use this feature. <bookADemoLink>Book a free demo or POC trial to learn more</bookADemoLink>.",
"ossEnterpriseEditionRequired": "The <enterpriseEditionLink>Enterprise Edition</enterpriseEditionLink> is required to use this feature. This feature is also available in <pangolinCloudLink>Pangolin Cloud</pangolinCloudLink>. <bookADemoLink>Book a free demo or POC trial to learn more</bookADemoLink>.",
"licenseRequiredToUse": "Se requiere una licencia <enterpriseLicenseLink>Enterprise Edition</enterpriseLicenseLink> o <pangolinCloudLink>Pangolin Cloud</pangolinCloudLink> para usar esta función. <bookADemoLink>Reserve una demostración o prueba POC</bookADemoLink>.",
"ossEnterpriseEditionRequired": "La <enterpriseEditionLink>Enterprise Edition</enterpriseEditionLink> es necesaria para utilizar esta función. Esta función también está disponible en <pangolinCloudLink>Pangolin Cloud</pangolinCloudLink>. <bookADemoLink>Reserva una demostración o prueba POC</bookADemoLink>.",
"certResolver": "Resolver certificado",
"certResolverDescription": "Seleccione la resolución de certificados a utilizar para este recurso.",
"selectCertResolver": "Seleccionar Resolver Certificado",
@@ -2802,6 +2682,5 @@
"approvalsEmptyStateStep2Description": "Editar un rol y habilitar la opción 'Requerir aprobaciones de dispositivos'. Los usuarios con este rol necesitarán la aprobación del administrador para nuevos dispositivos.",
"approvalsEmptyStatePreviewDescription": "Vista previa: Cuando está habilitado, las solicitudes de dispositivo pendientes aparecerán aquí para su revisión",
"approvalsEmptyStateButtonText": "Administrar roles",
"domainErrorTitle": "Estamos teniendo problemas para verificar su dominio",
"idpAdminAutoProvisionPoliciesTabHint": "Configure role mapping and organization policies on the <policiesTabLink>Auto Provision Settings</policiesTabLink> tab."
"domainErrorTitle": "Estamos teniendo problemas para verificar su dominio"
}

View File

@@ -148,11 +148,6 @@
"createLink": "Créer un lien",
"resourcesNotFound": "Aucune ressource trouvée",
"resourceSearch": "Rechercher des ressources",
"machineSearch": "Search machines",
"machinesSearch": "Search machine clients...",
"machineNotFound": "No machines found",
"userDeviceSearch": "Search user devices",
"userDevicesSearch": "Search user devices...",
"openMenu": "Ouvrir le menu",
"resource": "Ressource",
"title": "Titre de la page",
@@ -328,54 +323,6 @@
"apiKeysDelete": "Supprimer la clé d'API",
"apiKeysManage": "Gérer les clés d'API",
"apiKeysDescription": "Les clés d'API sont utilisées pour s'authentifier avec l'API d'intégration",
"provisioningKeysTitle": "Provisioning Key",
"provisioningKeysManage": "Manage Provisioning Keys",
"provisioningKeysDescription": "Provisioning keys are used to authenticate automated site provisioning for your organization.",
"provisioningManage": "Provisioning",
"provisioningDescription": "Manage provisioning keys and review pending sites awaiting approval.",
"pendingSites": "Pending Sites",
"siteApproveSuccess": "Site approved successfully",
"siteApproveError": "Error approving site",
"provisioningKeys": "Provisioning Keys",
"searchProvisioningKeys": "Search provisioning keys...",
"provisioningKeysAdd": "Generate Provisioning Key",
"provisioningKeysErrorDelete": "Error deleting provisioning key",
"provisioningKeysErrorDeleteMessage": "Error deleting provisioning key",
"provisioningKeysQuestionRemove": "Are you sure you want to remove this provisioning key from the organization?",
"provisioningKeysMessageRemove": "Once removed, the key can no longer be used for site provisioning.",
"provisioningKeysDeleteConfirm": "Confirm Delete Provisioning Key",
"provisioningKeysDelete": "Delete Provisioning key",
"provisioningKeysCreate": "Generate Provisioning Key",
"provisioningKeysCreateDescription": "Generate a new provisioning key for the organization",
"provisioningKeysSeeAll": "See all provisioning keys",
"provisioningKeysSave": "Save the provisioning key",
"provisioningKeysSaveDescription": "You will only be able to see this once. Copy it to a secure place.",
"provisioningKeysErrorCreate": "Error creating provisioning key",
"provisioningKeysList": "New provisioning key",
"provisioningKeysMaxBatchSize": "Max batch size",
"provisioningKeysUnlimitedBatchSize": "Unlimited batch size (no limit)",
"provisioningKeysMaxBatchUnlimited": "Unlimited",
"provisioningKeysMaxBatchSizeInvalid": "Enter a valid max batch size (11,000,000).",
"provisioningKeysValidUntil": "Valid until",
"provisioningKeysValidUntilHint": "Leave empty for no expiration.",
"provisioningKeysValidUntilInvalid": "Enter a valid date and time.",
"provisioningKeysNumUsed": "Times used",
"provisioningKeysLastUsed": "Last used",
"provisioningKeysNoExpiry": "No expiration",
"provisioningKeysNeverUsed": "Never",
"provisioningKeysEdit": "Edit Provisioning Key",
"provisioningKeysEditDescription": "Update the max batch size and expiration time for this key.",
"provisioningKeysApproveNewSites": "Approve new sites",
"provisioningKeysApproveNewSitesDescription": "Automatically approve sites that register with this key.",
"provisioningKeysUpdateError": "Error updating provisioning key",
"provisioningKeysUpdated": "Provisioning key updated",
"provisioningKeysUpdatedDescription": "Your changes have been saved.",
"provisioningKeysBannerTitle": "Site Provisioning Keys",
"provisioningKeysBannerDescription": "Generate a provisioning key and use it with the Newt connector to automatically create sites on first startup — no need to set up separate credentials for each site.",
"provisioningKeysBannerButtonText": "Learn More",
"pendingSitesBannerTitle": "Pending Sites",
"pendingSitesBannerDescription": "Sites that connect using a provisioning key appear here for review. Approve each site before it becomes active and gains access to your resources.",
"pendingSitesBannerButtonText": "Learn More",
"apiKeysSettings": "Paramètres de {apiKeyName}",
"userTitle": "Gérer tous les utilisateurs",
"userDescription": "Voir et gérer tous les utilisateurs du système",
@@ -562,12 +509,9 @@
"userSaved": "Utilisateur enregistré",
"userSavedDescription": "L'utilisateur a été mis à jour.",
"autoProvisioned": "Auto-provisionné",
"autoProvisionSettings": "Auto Provision Settings",
"autoProvisionedDescription": "Permettre à cet utilisateur d'être géré automatiquement par le fournisseur d'identité",
"accessControlsDescription": "Gérer ce que cet utilisateur peut accéder et faire dans l'organisation",
"accessControlsSubmit": "Enregistrer les contrôles d'accès",
"singleRolePerUserPlanNotice": "Your plan only supports one role per user.",
"singleRolePerUserEditionNotice": "This edition only supports one role per user.",
"roles": "Rôles",
"accessUsersRoles": "Gérer les utilisateurs et les rôles",
"accessUsersRolesDescription": "Invitez des utilisateurs et ajoutez-les aux rôles pour gérer l'accès à l'organisation",
@@ -943,7 +887,7 @@
"defaultMappingsRole": "Mappage de rôle par défaut",
"defaultMappingsRoleDescription": "JMESPath pour extraire les informations de rôle du jeton ID. Le résultat de cette expression doit renvoyer le nom du rôle tel que défini dans l'organisation sous forme de chaîne.",
"defaultMappingsOrg": "Mappage d'organisation par défaut",
"defaultMappingsOrgDescription": "When set, this expression must return the organization ID or true for the user to access that organization. When unset, defining an organization policy for that org is enough: the user is allowed in as long as a valid role mapping can be resolved for them within the organization.",
"defaultMappingsOrgDescription": "JMESPath pour extraire les informations d'organisation du jeton ID. Cette expression doit renvoyer l'ID de l'organisation ou true pour que l'utilisateur soit autorisé à accéder à l'organisation.",
"defaultMappingsSubmit": "Enregistrer les mappages par défaut",
"orgPoliciesEdit": "Modifier la politique d'organisation",
"org": "Organisation",
@@ -1175,7 +1119,6 @@
"setupTokenDescription": "Entrez le jeton de configuration depuis la console du serveur.",
"setupTokenRequired": "Le jeton de configuration est requis.",
"actionUpdateSite": "Mettre à jour un site",
"actionResetSiteBandwidth": "Reset Organization Bandwidth",
"actionListSiteRoles": "Lister les rôles autorisés du site",
"actionCreateResource": "Créer une ressource",
"actionDeleteResource": "Supprimer une ressource",
@@ -1205,7 +1148,6 @@
"actionRemoveUser": "Supprimer un utilisateur",
"actionListUsers": "Lister les utilisateurs",
"actionAddUserRole": "Ajouter un rôle utilisateur",
"actionSetUserOrgRoles": "Set User Roles",
"actionGenerateAccessToken": "Générer un jeton d'accès",
"actionDeleteAccessToken": "Supprimer un jeton d'accès",
"actionListAccessTokens": "Lister les jetons d'accès",
@@ -1322,7 +1264,6 @@
"sidebarRoles": "Rôles",
"sidebarShareableLinks": "Liens",
"sidebarApiKeys": "Clés API",
"sidebarProvisioning": "Provisioning",
"sidebarSettings": "Réglages",
"sidebarAllUsers": "Tous les utilisateurs",
"sidebarIdentityProviders": "Fournisseurs d'identité",
@@ -1948,40 +1889,6 @@
"exitNode": "Nœud de sortie",
"country": "Pays",
"rulesMatchCountry": "Actuellement basé sur l'IP source",
"region": "Region",
"selectRegion": "Select region",
"searchRegions": "Search regions...",
"noRegionFound": "No region found.",
"rulesMatchRegion": "Select a regional grouping of countries",
"rulesErrorInvalidRegion": "Invalid region",
"rulesErrorInvalidRegionDescription": "Please select a valid region.",
"regionAfrica": "Africa",
"regionNorthernAfrica": "Northern Africa",
"regionEasternAfrica": "Eastern Africa",
"regionMiddleAfrica": "Middle Africa",
"regionSouthernAfrica": "Southern Africa",
"regionWesternAfrica": "Western Africa",
"regionAmericas": "Americas",
"regionCaribbean": "Caribbean",
"regionCentralAmerica": "Central America",
"regionSouthAmerica": "South America",
"regionNorthernAmerica": "Northern America",
"regionAsia": "Asia",
"regionCentralAsia": "Central Asia",
"regionEasternAsia": "Eastern Asia",
"regionSouthEasternAsia": "South-Eastern Asia",
"regionSouthernAsia": "Southern Asia",
"regionWesternAsia": "Western Asia",
"regionEurope": "Europe",
"regionEasternEurope": "Eastern Europe",
"regionNorthernEurope": "Northern Europe",
"regionSouthernEurope": "Southern Europe",
"regionWesternEurope": "Western Europe",
"regionOceania": "Oceania",
"regionAustraliaAndNewZealand": "Australia and New Zealand",
"regionMelanesia": "Melanesia",
"regionMicronesia": "Micronesia",
"regionPolynesia": "Polynesia",
"managedSelfHosted": {
"title": "Gestion autonome",
"description": "Serveur Pangolin auto-hébergé avec des cloches et des sifflets supplémentaires",
@@ -2030,25 +1937,6 @@
"invalidValue": "Valeur non valide",
"idpTypeLabel": "Type de fournisseur d'identité",
"roleMappingExpressionPlaceholder": "ex: contenu(groupes) && 'admin' || 'membre'",
"roleMappingModeFixedRoles": "Fixed Roles",
"roleMappingModeMappingBuilder": "Mapping Builder",
"roleMappingModeRawExpression": "Raw Expression",
"roleMappingFixedRolesPlaceholderSelect": "Select one or more roles",
"roleMappingFixedRolesPlaceholderFreeform": "Type role names (exact match per organization)",
"roleMappingFixedRolesDescriptionSameForAll": "Assign the same role set to every auto-provisioned user.",
"roleMappingFixedRolesDescriptionDefaultPolicy": "For default policies, type role names that exist in each organization where users are provisioned. Names must match exactly.",
"roleMappingClaimPath": "Claim Path",
"roleMappingClaimPathPlaceholder": "groups",
"roleMappingClaimPathDescription": "Path in the token payload that contains source values (for example, groups).",
"roleMappingMatchValue": "Match Value",
"roleMappingAssignRoles": "Assign Roles",
"roleMappingAddMappingRule": "Add Mapping Rule",
"roleMappingRawExpressionResultDescription": "Expression must evaluate to a string or string array.",
"roleMappingRawExpressionResultDescriptionSingleRole": "Expression must evaluate to a string (a single role name).",
"roleMappingMatchValuePlaceholder": "Match value (for example: admin)",
"roleMappingAssignRolesPlaceholderFreeform": "Type role names (exact per org)",
"roleMappingBuilderFreeformRowHint": "Role names must match a role in each target organization.",
"roleMappingRemoveRule": "Remove",
"idpGoogleConfiguration": "Configuration Google",
"idpGoogleConfigurationDescription": "Configurer les identifiants Google OAuth2",
"idpGoogleClientIdDescription": "Google OAuth2 Client ID",
@@ -2445,8 +2333,6 @@
"logRetentionAccessDescription": "Durée de conservation des journaux d'accès",
"logRetentionActionLabel": "Retention du journal des actions",
"logRetentionActionDescription": "Durée de conservation du journal des actions",
"logRetentionConnectionLabel": "Connection Log Retention",
"logRetentionConnectionDescription": "How long to retain connection logs",
"logRetentionDisabled": "Désactivé",
"logRetention3Days": "3 jours",
"logRetention7Days": "7 jours",
@@ -2457,14 +2343,8 @@
"logRetentionEndOfFollowingYear": "Fin de l'année suivante",
"actionLogsDescription": "Voir l'historique des actions effectuées dans cette organisation",
"accessLogsDescription": "Voir les demandes d'authentification d'accès aux ressources de cette organisation",
"connectionLogs": "Connection Logs",
"connectionLogsDescription": "View connection logs for tunnels in this organization",
"sidebarLogsConnection": "Connection Logs",
"sourceAddress": "Source Address",
"destinationAddress": "Destination Address",
"duration": "Duration",
"licenseRequiredToUse": "An <enterpriseLicenseLink>Enterprise Edition</enterpriseLicenseLink> license or <pangolinCloudLink>Pangolin Cloud</pangolinCloudLink> is required to use this feature. <bookADemoLink>Book a free demo or POC trial to learn more</bookADemoLink>.",
"ossEnterpriseEditionRequired": "The <enterpriseEditionLink>Enterprise Edition</enterpriseEditionLink> is required to use this feature. This feature is also available in <pangolinCloudLink>Pangolin Cloud</pangolinCloudLink>. <bookADemoLink>Book a free demo or POC trial to learn more</bookADemoLink>.",
"licenseRequiredToUse": "Une <enterpriseLicenseLink>licence Enterprise Edition</enterpriseLicenseLink> ou <pangolinCloudLink>Pangolin Cloud</pangolinCloudLink> est requise pour utiliser cette fonctionnalité. <bookADemoLink>Réservez une démonstration ou une évaluation de POC</bookADemoLink>.",
"ossEnterpriseEditionRequired": "La version <enterpriseEditionLink>Enterprise Edition</enterpriseEditionLink> est requise pour utiliser cette fonctionnalité. Cette fonctionnalité est également disponible dans <pangolinCloudLink>Pangolin Cloud</pangolinCloudLink>. <bookADemoLink>Réservez une démo ou un essai POC</bookADemoLink>.",
"certResolver": "Résolveur de certificat",
"certResolverDescription": "Sélectionnez le solveur de certificat à utiliser pour cette ressource.",
"selectCertResolver": "Sélectionnez le résolveur de certificat",
@@ -2802,6 +2682,5 @@
"approvalsEmptyStateStep2Description": "Modifier un rôle et activer l'option 'Exiger les autorisations de l'appareil'. Les utilisateurs avec ce rôle auront besoin de l'approbation de l'administrateur pour les nouveaux appareils.",
"approvalsEmptyStatePreviewDescription": "Aperçu: Lorsque cette option est activée, les demandes de périphérique en attente apparaîtront ici pour vérification",
"approvalsEmptyStateButtonText": "Gérer les rôles",
"domainErrorTitle": "Nous avons des difficultés à vérifier votre domaine",
"idpAdminAutoProvisionPoliciesTabHint": "Configure role mapping and organization policies on the <policiesTabLink>Auto Provision Settings</policiesTabLink> tab."
"domainErrorTitle": "Nous avons des difficultés à vérifier votre domaine"
}

View File

@@ -148,11 +148,6 @@
"createLink": "Crea Collegamento",
"resourcesNotFound": "Nessuna risorsa trovata",
"resourceSearch": "Cerca risorse",
"machineSearch": "Search machines",
"machinesSearch": "Search machine clients...",
"machineNotFound": "No machines found",
"userDeviceSearch": "Search user devices",
"userDevicesSearch": "Search user devices...",
"openMenu": "Apri menu",
"resource": "Risorsa",
"title": "Titolo",
@@ -328,54 +323,6 @@
"apiKeysDelete": "Elimina Chiave API",
"apiKeysManage": "Gestisci Chiavi API",
"apiKeysDescription": "Le chiavi API sono utilizzate per autenticarsi con l'API di integrazione",
"provisioningKeysTitle": "Provisioning Key",
"provisioningKeysManage": "Manage Provisioning Keys",
"provisioningKeysDescription": "Provisioning keys are used to authenticate automated site provisioning for your organization.",
"provisioningManage": "Provisioning",
"provisioningDescription": "Manage provisioning keys and review pending sites awaiting approval.",
"pendingSites": "Pending Sites",
"siteApproveSuccess": "Site approved successfully",
"siteApproveError": "Error approving site",
"provisioningKeys": "Provisioning Keys",
"searchProvisioningKeys": "Search provisioning keys...",
"provisioningKeysAdd": "Generate Provisioning Key",
"provisioningKeysErrorDelete": "Error deleting provisioning key",
"provisioningKeysErrorDeleteMessage": "Error deleting provisioning key",
"provisioningKeysQuestionRemove": "Are you sure you want to remove this provisioning key from the organization?",
"provisioningKeysMessageRemove": "Once removed, the key can no longer be used for site provisioning.",
"provisioningKeysDeleteConfirm": "Confirm Delete Provisioning Key",
"provisioningKeysDelete": "Delete Provisioning key",
"provisioningKeysCreate": "Generate Provisioning Key",
"provisioningKeysCreateDescription": "Generate a new provisioning key for the organization",
"provisioningKeysSeeAll": "See all provisioning keys",
"provisioningKeysSave": "Save the provisioning key",
"provisioningKeysSaveDescription": "You will only be able to see this once. Copy it to a secure place.",
"provisioningKeysErrorCreate": "Error creating provisioning key",
"provisioningKeysList": "New provisioning key",
"provisioningKeysMaxBatchSize": "Max batch size",
"provisioningKeysUnlimitedBatchSize": "Unlimited batch size (no limit)",
"provisioningKeysMaxBatchUnlimited": "Unlimited",
"provisioningKeysMaxBatchSizeInvalid": "Enter a valid max batch size (11,000,000).",
"provisioningKeysValidUntil": "Valid until",
"provisioningKeysValidUntilHint": "Leave empty for no expiration.",
"provisioningKeysValidUntilInvalid": "Enter a valid date and time.",
"provisioningKeysNumUsed": "Times used",
"provisioningKeysLastUsed": "Last used",
"provisioningKeysNoExpiry": "No expiration",
"provisioningKeysNeverUsed": "Never",
"provisioningKeysEdit": "Edit Provisioning Key",
"provisioningKeysEditDescription": "Update the max batch size and expiration time for this key.",
"provisioningKeysApproveNewSites": "Approve new sites",
"provisioningKeysApproveNewSitesDescription": "Automatically approve sites that register with this key.",
"provisioningKeysUpdateError": "Error updating provisioning key",
"provisioningKeysUpdated": "Provisioning key updated",
"provisioningKeysUpdatedDescription": "Your changes have been saved.",
"provisioningKeysBannerTitle": "Site Provisioning Keys",
"provisioningKeysBannerDescription": "Generate a provisioning key and use it with the Newt connector to automatically create sites on first startup — no need to set up separate credentials for each site.",
"provisioningKeysBannerButtonText": "Learn More",
"pendingSitesBannerTitle": "Pending Sites",
"pendingSitesBannerDescription": "Sites that connect using a provisioning key appear here for review. Approve each site before it becomes active and gains access to your resources.",
"pendingSitesBannerButtonText": "Learn More",
"apiKeysSettings": "Impostazioni {apiKeyName}",
"userTitle": "Gestisci Tutti Gli Utenti",
"userDescription": "Visualizza e gestisci tutti gli utenti del sistema",
@@ -562,12 +509,9 @@
"userSaved": "Utente salvato",
"userSavedDescription": "L'utente è stato aggiornato.",
"autoProvisioned": "Auto Provisioned",
"autoProvisionSettings": "Auto Provision Settings",
"autoProvisionedDescription": "Permetti a questo utente di essere gestito automaticamente dal provider di identità",
"accessControlsDescription": "Gestisci cosa questo utente può accedere e fare nell'organizzazione",
"accessControlsSubmit": "Salva Controlli di Accesso",
"singleRolePerUserPlanNotice": "Your plan only supports one role per user.",
"singleRolePerUserEditionNotice": "This edition only supports one role per user.",
"roles": "Ruoli",
"accessUsersRoles": "Gestisci Utenti e Ruoli",
"accessUsersRolesDescription": "Invita gli utenti e aggiungili ai ruoli per gestire l'accesso all'organizzazione",
@@ -943,7 +887,7 @@
"defaultMappingsRole": "Mappatura Ruolo Predefinito",
"defaultMappingsRoleDescription": "JMESPath per estrarre informazioni sul ruolo dal token ID. Il risultato di questa espressione deve restituire il nome del ruolo come definito nell'organizzazione come stringa.",
"defaultMappingsOrg": "Mappatura Organizzazione Predefinita",
"defaultMappingsOrgDescription": "When set, this expression must return the organization ID or true for the user to access that organization. When unset, defining an organization policy for that org is enough: the user is allowed in as long as a valid role mapping can be resolved for them within the organization.",
"defaultMappingsOrgDescription": "JMESPath per estrarre informazioni sull'organizzazione dal token ID. Questa espressione deve restituire l'ID dell'organizzazione o true affinché l'utente possa accedere all'organizzazione.",
"defaultMappingsSubmit": "Salva Mappature Predefinite",
"orgPoliciesEdit": "Modifica Politica Organizzazione",
"org": "Organizzazione",
@@ -1175,7 +1119,6 @@
"setupTokenDescription": "Inserisci il token di configurazione dalla console del server.",
"setupTokenRequired": "Il token di configurazione è richiesto",
"actionUpdateSite": "Aggiorna Sito",
"actionResetSiteBandwidth": "Reset Organization Bandwidth",
"actionListSiteRoles": "Elenca Ruoli Sito Consentiti",
"actionCreateResource": "Crea Risorsa",
"actionDeleteResource": "Elimina Risorsa",
@@ -1205,7 +1148,6 @@
"actionRemoveUser": "Rimuovi Utente",
"actionListUsers": "Elenca Utenti",
"actionAddUserRole": "Aggiungi Ruolo Utente",
"actionSetUserOrgRoles": "Set User Roles",
"actionGenerateAccessToken": "Genera Token di Accesso",
"actionDeleteAccessToken": "Elimina Token di Accesso",
"actionListAccessTokens": "Elenca Token di Accesso",
@@ -1322,7 +1264,6 @@
"sidebarRoles": "Ruoli",
"sidebarShareableLinks": "Collegamenti",
"sidebarApiKeys": "Chiavi API",
"sidebarProvisioning": "Provisioning",
"sidebarSettings": "Impostazioni",
"sidebarAllUsers": "Tutti Gli Utenti",
"sidebarIdentityProviders": "Fornitori Di Identità",
@@ -1948,40 +1889,6 @@
"exitNode": "Nodo di Uscita",
"country": "Paese",
"rulesMatchCountry": "Attualmente basato sull'IP di origine",
"region": "Region",
"selectRegion": "Select region",
"searchRegions": "Search regions...",
"noRegionFound": "No region found.",
"rulesMatchRegion": "Select a regional grouping of countries",
"rulesErrorInvalidRegion": "Invalid region",
"rulesErrorInvalidRegionDescription": "Please select a valid region.",
"regionAfrica": "Africa",
"regionNorthernAfrica": "Northern Africa",
"regionEasternAfrica": "Eastern Africa",
"regionMiddleAfrica": "Middle Africa",
"regionSouthernAfrica": "Southern Africa",
"regionWesternAfrica": "Western Africa",
"regionAmericas": "Americas",
"regionCaribbean": "Caribbean",
"regionCentralAmerica": "Central America",
"regionSouthAmerica": "South America",
"regionNorthernAmerica": "Northern America",
"regionAsia": "Asia",
"regionCentralAsia": "Central Asia",
"regionEasternAsia": "Eastern Asia",
"regionSouthEasternAsia": "South-Eastern Asia",
"regionSouthernAsia": "Southern Asia",
"regionWesternAsia": "Western Asia",
"regionEurope": "Europe",
"regionEasternEurope": "Eastern Europe",
"regionNorthernEurope": "Northern Europe",
"regionSouthernEurope": "Southern Europe",
"regionWesternEurope": "Western Europe",
"regionOceania": "Oceania",
"regionAustraliaAndNewZealand": "Australia and New Zealand",
"regionMelanesia": "Melanesia",
"regionMicronesia": "Micronesia",
"regionPolynesia": "Polynesia",
"managedSelfHosted": {
"title": "Gestito Auto-Ospitato",
"description": "Server Pangolin self-hosted più affidabile e a bassa manutenzione con campanelli e fischietti extra",
@@ -2030,25 +1937,6 @@
"invalidValue": "Valore non valido",
"idpTypeLabel": "Tipo Provider Identità",
"roleMappingExpressionPlaceholder": "es. contiene(gruppi, 'admin') && 'Admin' <unk> <unk> 'Membro'",
"roleMappingModeFixedRoles": "Fixed Roles",
"roleMappingModeMappingBuilder": "Mapping Builder",
"roleMappingModeRawExpression": "Raw Expression",
"roleMappingFixedRolesPlaceholderSelect": "Select one or more roles",
"roleMappingFixedRolesPlaceholderFreeform": "Type role names (exact match per organization)",
"roleMappingFixedRolesDescriptionSameForAll": "Assign the same role set to every auto-provisioned user.",
"roleMappingFixedRolesDescriptionDefaultPolicy": "For default policies, type role names that exist in each organization where users are provisioned. Names must match exactly.",
"roleMappingClaimPath": "Claim Path",
"roleMappingClaimPathPlaceholder": "groups",
"roleMappingClaimPathDescription": "Path in the token payload that contains source values (for example, groups).",
"roleMappingMatchValue": "Match Value",
"roleMappingAssignRoles": "Assign Roles",
"roleMappingAddMappingRule": "Add Mapping Rule",
"roleMappingRawExpressionResultDescription": "Expression must evaluate to a string or string array.",
"roleMappingRawExpressionResultDescriptionSingleRole": "Expression must evaluate to a string (a single role name).",
"roleMappingMatchValuePlaceholder": "Match value (for example: admin)",
"roleMappingAssignRolesPlaceholderFreeform": "Type role names (exact per org)",
"roleMappingBuilderFreeformRowHint": "Role names must match a role in each target organization.",
"roleMappingRemoveRule": "Remove",
"idpGoogleConfiguration": "Configurazione Google",
"idpGoogleConfigurationDescription": "Configura le credenziali di Google OAuth2",
"idpGoogleClientIdDescription": "Google OAuth2 Client ID",
@@ -2445,8 +2333,6 @@
"logRetentionAccessDescription": "Per quanto tempo conservare i log di accesso",
"logRetentionActionLabel": "Ritenzione Registro Azioni",
"logRetentionActionDescription": "Per quanto tempo conservare i log delle azioni",
"logRetentionConnectionLabel": "Connection Log Retention",
"logRetentionConnectionDescription": "How long to retain connection logs",
"logRetentionDisabled": "Disabilitato",
"logRetention3Days": "3 giorni",
"logRetention7Days": "7 giorni",
@@ -2457,14 +2343,8 @@
"logRetentionEndOfFollowingYear": "Fine dell'anno successivo",
"actionLogsDescription": "Visualizza una cronologia delle azioni eseguite in questa organizzazione",
"accessLogsDescription": "Visualizza le richieste di autenticazione di accesso per le risorse in questa organizzazione",
"connectionLogs": "Connection Logs",
"connectionLogsDescription": "View connection logs for tunnels in this organization",
"sidebarLogsConnection": "Connection Logs",
"sourceAddress": "Source Address",
"destinationAddress": "Destination Address",
"duration": "Duration",
"licenseRequiredToUse": "An <enterpriseLicenseLink>Enterprise Edition</enterpriseLicenseLink> license or <pangolinCloudLink>Pangolin Cloud</pangolinCloudLink> is required to use this feature. <bookADemoLink>Book a free demo or POC trial to learn more</bookADemoLink>.",
"ossEnterpriseEditionRequired": "The <enterpriseEditionLink>Enterprise Edition</enterpriseEditionLink> is required to use this feature. This feature is also available in <pangolinCloudLink>Pangolin Cloud</pangolinCloudLink>. <bookADemoLink>Book a free demo or POC trial to learn more</bookADemoLink>.",
"licenseRequiredToUse": "Per utilizzare questa funzione è necessaria una licenza <enterpriseLicenseLink>Enterprise Edition</enterpriseLicenseLink> o <pangolinCloudLink>Pangolin Cloud</pangolinCloudLink> . <bookADemoLink>Prenota una demo o una prova POC</bookADemoLink>.",
"ossEnterpriseEditionRequired": "L' <enterpriseEditionLink>Enterprise Edition</enterpriseEditionLink> è necessaria per utilizzare questa funzione. Questa funzione è disponibile anche in <pangolinCloudLink>Pangolin Cloud</pangolinCloudLink>. <bookADemoLink>Prenota una demo o una prova POC</bookADemoLink>.",
"certResolver": "Risolutore Di Certificato",
"certResolverDescription": "Selezionare il risolutore di certificati da usare per questa risorsa.",
"selectCertResolver": "Seleziona Risolutore Di Certificato",
@@ -2802,6 +2682,5 @@
"approvalsEmptyStateStep2Description": "Modifica un ruolo e abilita l'opzione 'Richiedi l'approvazione del dispositivo'. Gli utenti con questo ruolo avranno bisogno dell'approvazione dell'amministratore per i nuovi dispositivi.",
"approvalsEmptyStatePreviewDescription": "Anteprima: quando abilitato, le richieste di dispositivo in attesa appariranno qui per la revisione",
"approvalsEmptyStateButtonText": "Gestisci Ruoli",
"domainErrorTitle": "Stiamo avendo problemi a verificare il tuo dominio",
"idpAdminAutoProvisionPoliciesTabHint": "Configure role mapping and organization policies on the <policiesTabLink>Auto Provision Settings</policiesTabLink> tab."
"domainErrorTitle": "Stiamo avendo problemi a verificare il tuo dominio"
}

View File

@@ -148,11 +148,6 @@
"createLink": "링크 생성",
"resourcesNotFound": "리소스가 발견되지 않았습니다.",
"resourceSearch": "리소스 검색",
"machineSearch": "Search machines",
"machinesSearch": "Search machine clients...",
"machineNotFound": "No machines found",
"userDeviceSearch": "Search user devices",
"userDevicesSearch": "Search user devices...",
"openMenu": "메뉴 열기",
"resource": "리소스",
"title": "제목",
@@ -328,54 +323,6 @@
"apiKeysDelete": "API 키 삭제",
"apiKeysManage": "API 키 관리",
"apiKeysDescription": "API 키는 통합 API와 인증하는 데 사용됩니다.",
"provisioningKeysTitle": "Provisioning Key",
"provisioningKeysManage": "Manage Provisioning Keys",
"provisioningKeysDescription": "Provisioning keys are used to authenticate automated site provisioning for your organization.",
"provisioningManage": "Provisioning",
"provisioningDescription": "Manage provisioning keys and review pending sites awaiting approval.",
"pendingSites": "Pending Sites",
"siteApproveSuccess": "Site approved successfully",
"siteApproveError": "Error approving site",
"provisioningKeys": "Provisioning Keys",
"searchProvisioningKeys": "Search provisioning keys...",
"provisioningKeysAdd": "Generate Provisioning Key",
"provisioningKeysErrorDelete": "Error deleting provisioning key",
"provisioningKeysErrorDeleteMessage": "Error deleting provisioning key",
"provisioningKeysQuestionRemove": "Are you sure you want to remove this provisioning key from the organization?",
"provisioningKeysMessageRemove": "Once removed, the key can no longer be used for site provisioning.",
"provisioningKeysDeleteConfirm": "Confirm Delete Provisioning Key",
"provisioningKeysDelete": "Delete Provisioning key",
"provisioningKeysCreate": "Generate Provisioning Key",
"provisioningKeysCreateDescription": "Generate a new provisioning key for the organization",
"provisioningKeysSeeAll": "See all provisioning keys",
"provisioningKeysSave": "Save the provisioning key",
"provisioningKeysSaveDescription": "You will only be able to see this once. Copy it to a secure place.",
"provisioningKeysErrorCreate": "Error creating provisioning key",
"provisioningKeysList": "New provisioning key",
"provisioningKeysMaxBatchSize": "Max batch size",
"provisioningKeysUnlimitedBatchSize": "Unlimited batch size (no limit)",
"provisioningKeysMaxBatchUnlimited": "Unlimited",
"provisioningKeysMaxBatchSizeInvalid": "Enter a valid max batch size (11,000,000).",
"provisioningKeysValidUntil": "Valid until",
"provisioningKeysValidUntilHint": "Leave empty for no expiration.",
"provisioningKeysValidUntilInvalid": "Enter a valid date and time.",
"provisioningKeysNumUsed": "Times used",
"provisioningKeysLastUsed": "Last used",
"provisioningKeysNoExpiry": "No expiration",
"provisioningKeysNeverUsed": "Never",
"provisioningKeysEdit": "Edit Provisioning Key",
"provisioningKeysEditDescription": "Update the max batch size and expiration time for this key.",
"provisioningKeysApproveNewSites": "Approve new sites",
"provisioningKeysApproveNewSitesDescription": "Automatically approve sites that register with this key.",
"provisioningKeysUpdateError": "Error updating provisioning key",
"provisioningKeysUpdated": "Provisioning key updated",
"provisioningKeysUpdatedDescription": "Your changes have been saved.",
"provisioningKeysBannerTitle": "Site Provisioning Keys",
"provisioningKeysBannerDescription": "Generate a provisioning key and use it with the Newt connector to automatically create sites on first startup — no need to set up separate credentials for each site.",
"provisioningKeysBannerButtonText": "Learn More",
"pendingSitesBannerTitle": "Pending Sites",
"pendingSitesBannerDescription": "Sites that connect using a provisioning key appear here for review. Approve each site before it becomes active and gains access to your resources.",
"pendingSitesBannerButtonText": "Learn More",
"apiKeysSettings": "{apiKeyName} 설정",
"userTitle": "모든 사용자 관리",
"userDescription": "시스템의 모든 사용자를 보고 관리합니다",
@@ -562,12 +509,9 @@
"userSaved": "사용자 저장됨",
"userSavedDescription": "사용자가 업데이트되었습니다.",
"autoProvisioned": "자동 프로비저닝됨",
"autoProvisionSettings": "Auto Provision Settings",
"autoProvisionedDescription": "이 사용자가 ID 공급자에 의해 자동으로 관리될 수 있도록 허용합니다",
"accessControlsDescription": "이 사용자가 조직에서 접근하고 수행할 수 있는 작업을 관리하세요",
"accessControlsSubmit": "접근 제어 저장",
"singleRolePerUserPlanNotice": "Your plan only supports one role per user.",
"singleRolePerUserEditionNotice": "This edition only supports one role per user.",
"roles": "역할",
"accessUsersRoles": "사용자 및 역할 관리",
"accessUsersRolesDescription": "사용자를 초대하고 역할에 추가하여 조직에 대한 접근을 관리하세요",
@@ -943,7 +887,7 @@
"defaultMappingsRole": "기본 역할 매핑",
"defaultMappingsRoleDescription": "이 표현식의 결과는 조직에서 정의된 역할 이름을 문자열로 반환해야 합니다.",
"defaultMappingsOrg": "기본 조직 매핑",
"defaultMappingsOrgDescription": "When set, this expression must return the organization ID or true for the user to access that organization. When unset, defining an organization policy for that org is enough: the user is allowed in as long as a valid role mapping can be resolved for them within the organization.",
"defaultMappingsOrgDescription": "이 표현식은 사용자가 조직에 접근할 수 있도록 조직 ID 또는 true를 반환해야 합니다.",
"defaultMappingsSubmit": "기본 매핑 저장",
"orgPoliciesEdit": "조직 정책 편집",
"org": "조직",
@@ -1175,7 +1119,6 @@
"setupTokenDescription": "서버 콘솔에서 설정 토큰 입력.",
"setupTokenRequired": "설정 토큰이 필요합니다",
"actionUpdateSite": "사이트 업데이트",
"actionResetSiteBandwidth": "Reset Organization Bandwidth",
"actionListSiteRoles": "허용된 사이트 역할 목록",
"actionCreateResource": "리소스 생성",
"actionDeleteResource": "리소스 삭제",
@@ -1205,7 +1148,6 @@
"actionRemoveUser": "사용자 제거",
"actionListUsers": "사용자 목록",
"actionAddUserRole": "사용자 역할 추가",
"actionSetUserOrgRoles": "Set User Roles",
"actionGenerateAccessToken": "액세스 토큰 생성",
"actionDeleteAccessToken": "액세스 토큰 삭제",
"actionListAccessTokens": "액세스 토큰 목록",
@@ -1322,7 +1264,6 @@
"sidebarRoles": "역할",
"sidebarShareableLinks": "링크",
"sidebarApiKeys": "API 키",
"sidebarProvisioning": "Provisioning",
"sidebarSettings": "설정",
"sidebarAllUsers": "모든 사용자",
"sidebarIdentityProviders": "신원 공급자",
@@ -1948,40 +1889,6 @@
"exitNode": "종단 노드",
"country": "국가",
"rulesMatchCountry": "현재 소스 IP를 기반으로 합니다",
"region": "Region",
"selectRegion": "Select region",
"searchRegions": "Search regions...",
"noRegionFound": "No region found.",
"rulesMatchRegion": "Select a regional grouping of countries",
"rulesErrorInvalidRegion": "Invalid region",
"rulesErrorInvalidRegionDescription": "Please select a valid region.",
"regionAfrica": "Africa",
"regionNorthernAfrica": "Northern Africa",
"regionEasternAfrica": "Eastern Africa",
"regionMiddleAfrica": "Middle Africa",
"regionSouthernAfrica": "Southern Africa",
"regionWesternAfrica": "Western Africa",
"regionAmericas": "Americas",
"regionCaribbean": "Caribbean",
"regionCentralAmerica": "Central America",
"regionSouthAmerica": "South America",
"regionNorthernAmerica": "Northern America",
"regionAsia": "Asia",
"regionCentralAsia": "Central Asia",
"regionEasternAsia": "Eastern Asia",
"regionSouthEasternAsia": "South-Eastern Asia",
"regionSouthernAsia": "Southern Asia",
"regionWesternAsia": "Western Asia",
"regionEurope": "Europe",
"regionEasternEurope": "Eastern Europe",
"regionNorthernEurope": "Northern Europe",
"regionSouthernEurope": "Southern Europe",
"regionWesternEurope": "Western Europe",
"regionOceania": "Oceania",
"regionAustraliaAndNewZealand": "Australia and New Zealand",
"regionMelanesia": "Melanesia",
"regionMicronesia": "Micronesia",
"regionPolynesia": "Polynesia",
"managedSelfHosted": {
"title": "관리 자체 호스팅",
"description": "더 신뢰할 수 있고 낮은 유지보수의 자체 호스팅 팡골린 서버, 추가 기능 포함",
@@ -2030,25 +1937,6 @@
"invalidValue": "잘못된 값",
"idpTypeLabel": "신원 공급자 유형",
"roleMappingExpressionPlaceholder": "예: contains(groups, 'admin') && 'Admin' || 'Member'",
"roleMappingModeFixedRoles": "Fixed Roles",
"roleMappingModeMappingBuilder": "Mapping Builder",
"roleMappingModeRawExpression": "Raw Expression",
"roleMappingFixedRolesPlaceholderSelect": "Select one or more roles",
"roleMappingFixedRolesPlaceholderFreeform": "Type role names (exact match per organization)",
"roleMappingFixedRolesDescriptionSameForAll": "Assign the same role set to every auto-provisioned user.",
"roleMappingFixedRolesDescriptionDefaultPolicy": "For default policies, type role names that exist in each organization where users are provisioned. Names must match exactly.",
"roleMappingClaimPath": "Claim Path",
"roleMappingClaimPathPlaceholder": "groups",
"roleMappingClaimPathDescription": "Path in the token payload that contains source values (for example, groups).",
"roleMappingMatchValue": "Match Value",
"roleMappingAssignRoles": "Assign Roles",
"roleMappingAddMappingRule": "Add Mapping Rule",
"roleMappingRawExpressionResultDescription": "Expression must evaluate to a string or string array.",
"roleMappingRawExpressionResultDescriptionSingleRole": "Expression must evaluate to a string (a single role name).",
"roleMappingMatchValuePlaceholder": "Match value (for example: admin)",
"roleMappingAssignRolesPlaceholderFreeform": "Type role names (exact per org)",
"roleMappingBuilderFreeformRowHint": "Role names must match a role in each target organization.",
"roleMappingRemoveRule": "Remove",
"idpGoogleConfiguration": "Google 구성",
"idpGoogleConfigurationDescription": "Google OAuth2 자격 증명을 구성합니다.",
"idpGoogleClientIdDescription": "Google OAuth2 클라이언트 ID",
@@ -2445,8 +2333,6 @@
"logRetentionAccessDescription": "접근 로그를 얼마나 오래 보관할지",
"logRetentionActionLabel": "작업 로그 보관",
"logRetentionActionDescription": "작업 로그를 얼마나 오래 보관할지",
"logRetentionConnectionLabel": "Connection Log Retention",
"logRetentionConnectionDescription": "How long to retain connection logs",
"logRetentionDisabled": "비활성화됨",
"logRetention3Days": "3 일",
"logRetention7Days": "7 일",
@@ -2457,14 +2343,8 @@
"logRetentionEndOfFollowingYear": "다음 연도 말",
"actionLogsDescription": "이 조직에서 수행된 작업의 기록을 봅니다",
"accessLogsDescription": "이 조직의 자원에 대한 접근 인증 요청을 확인합니다",
"connectionLogs": "Connection Logs",
"connectionLogsDescription": "View connection logs for tunnels in this organization",
"sidebarLogsConnection": "Connection Logs",
"sourceAddress": "Source Address",
"destinationAddress": "Destination Address",
"duration": "Duration",
"licenseRequiredToUse": "An <enterpriseLicenseLink>Enterprise Edition</enterpriseLicenseLink> license or <pangolinCloudLink>Pangolin Cloud</pangolinCloudLink> is required to use this feature. <bookADemoLink>Book a free demo or POC trial to learn more</bookADemoLink>.",
"ossEnterpriseEditionRequired": "The <enterpriseEditionLink>Enterprise Edition</enterpriseEditionLink> is required to use this feature. This feature is also available in <pangolinCloudLink>Pangolin Cloud</pangolinCloudLink>. <bookADemoLink>Book a free demo or POC trial to learn more</bookADemoLink>.",
"licenseRequiredToUse": "이 기능을 사용하려면 <enterpriseLicenseLink>엔터프라이즈 에디션</enterpriseLicenseLink> 라이선스가 필요합니다. 이 기능은 <pangolinCloudLink>판골린 클라우드</pangolinCloudLink>에서도 사용할 수 있습니다. <bookADemoLink>데모 또는 POC 체험을 예약하세요</bookADemoLink>.",
"ossEnterpriseEditionRequired": "이 기능을 사용하려면 <enterpriseEditionLink>엔터프라이즈 에디션</enterpriseEditionLink>이(가) 필요합니다. 이 기능은 <pangolinCloudLink>판골린 클라우드</pangolinCloudLink>에서도 사용할 수 있습니다. <bookADemoLink>데모 또는 POC 체험을 예약하세요</bookADemoLink>.",
"certResolver": "인증서 해결사",
"certResolverDescription": "이 리소스에 사용할 인증서 해결사를 선택하세요.",
"selectCertResolver": "인증서 해결사 선택",
@@ -2802,6 +2682,5 @@
"approvalsEmptyStateStep2Description": "역할을 편집하고 '장치 승인 요구' 옵션을 활성화하세요. 이 역할을 가진 사용자는 새 장치에 대해 관리자의 승인이 필요합니다.",
"approvalsEmptyStatePreviewDescription": "미리 보기: 활성화된 경우, 승인 대기 중인 장치 요청이 검토용으로 여기에 표시됩니다.",
"approvalsEmptyStateButtonText": "역할 관리",
"domainErrorTitle": "도메인 확인에 문제가 발생했습니다.",
"idpAdminAutoProvisionPoliciesTabHint": "Configure role mapping and organization policies on the <policiesTabLink>Auto Provision Settings</policiesTabLink> tab."
"domainErrorTitle": "도메인 확인에 문제가 발생했습니다."
}

View File

@@ -148,11 +148,6 @@
"createLink": "Opprett lenke",
"resourcesNotFound": "Ingen ressurser funnet",
"resourceSearch": "Søk i ressurser",
"machineSearch": "Search machines",
"machinesSearch": "Search machine clients...",
"machineNotFound": "No machines found",
"userDeviceSearch": "Search user devices",
"userDevicesSearch": "Search user devices...",
"openMenu": "Åpne meny",
"resource": "Ressurs",
"title": "Tittel",
@@ -328,54 +323,6 @@
"apiKeysDelete": "Slett API-nøkkel",
"apiKeysManage": "Administrer API-nøkler",
"apiKeysDescription": "API-nøkler brukes for å autentisere med integrasjons-API",
"provisioningKeysTitle": "Provisioning Key",
"provisioningKeysManage": "Manage Provisioning Keys",
"provisioningKeysDescription": "Provisioning keys are used to authenticate automated site provisioning for your organization.",
"provisioningManage": "Provisioning",
"provisioningDescription": "Manage provisioning keys and review pending sites awaiting approval.",
"pendingSites": "Pending Sites",
"siteApproveSuccess": "Site approved successfully",
"siteApproveError": "Error approving site",
"provisioningKeys": "Provisioning Keys",
"searchProvisioningKeys": "Search provisioning keys...",
"provisioningKeysAdd": "Generate Provisioning Key",
"provisioningKeysErrorDelete": "Error deleting provisioning key",
"provisioningKeysErrorDeleteMessage": "Error deleting provisioning key",
"provisioningKeysQuestionRemove": "Are you sure you want to remove this provisioning key from the organization?",
"provisioningKeysMessageRemove": "Once removed, the key can no longer be used for site provisioning.",
"provisioningKeysDeleteConfirm": "Confirm Delete Provisioning Key",
"provisioningKeysDelete": "Delete Provisioning key",
"provisioningKeysCreate": "Generate Provisioning Key",
"provisioningKeysCreateDescription": "Generate a new provisioning key for the organization",
"provisioningKeysSeeAll": "See all provisioning keys",
"provisioningKeysSave": "Save the provisioning key",
"provisioningKeysSaveDescription": "You will only be able to see this once. Copy it to a secure place.",
"provisioningKeysErrorCreate": "Error creating provisioning key",
"provisioningKeysList": "New provisioning key",
"provisioningKeysMaxBatchSize": "Max batch size",
"provisioningKeysUnlimitedBatchSize": "Unlimited batch size (no limit)",
"provisioningKeysMaxBatchUnlimited": "Unlimited",
"provisioningKeysMaxBatchSizeInvalid": "Enter a valid max batch size (11,000,000).",
"provisioningKeysValidUntil": "Valid until",
"provisioningKeysValidUntilHint": "Leave empty for no expiration.",
"provisioningKeysValidUntilInvalid": "Enter a valid date and time.",
"provisioningKeysNumUsed": "Times used",
"provisioningKeysLastUsed": "Last used",
"provisioningKeysNoExpiry": "No expiration",
"provisioningKeysNeverUsed": "Never",
"provisioningKeysEdit": "Edit Provisioning Key",
"provisioningKeysEditDescription": "Update the max batch size and expiration time for this key.",
"provisioningKeysApproveNewSites": "Approve new sites",
"provisioningKeysApproveNewSitesDescription": "Automatically approve sites that register with this key.",
"provisioningKeysUpdateError": "Error updating provisioning key",
"provisioningKeysUpdated": "Provisioning key updated",
"provisioningKeysUpdatedDescription": "Your changes have been saved.",
"provisioningKeysBannerTitle": "Site Provisioning Keys",
"provisioningKeysBannerDescription": "Generate a provisioning key and use it with the Newt connector to automatically create sites on first startup — no need to set up separate credentials for each site.",
"provisioningKeysBannerButtonText": "Learn More",
"pendingSitesBannerTitle": "Pending Sites",
"pendingSitesBannerDescription": "Sites that connect using a provisioning key appear here for review. Approve each site before it becomes active and gains access to your resources.",
"pendingSitesBannerButtonText": "Learn More",
"apiKeysSettings": "{apiKeyName} Innstillinger",
"userTitle": "Administrer alle brukere",
"userDescription": "Vis og administrer alle brukere i systemet",
@@ -562,12 +509,9 @@
"userSaved": "Bruker lagret",
"userSavedDescription": "Brukeren har blitt oppdatert.",
"autoProvisioned": "Auto avlyst",
"autoProvisionSettings": "Auto Provision Settings",
"autoProvisionedDescription": "Tillat denne brukeren å bli automatisk administrert av en identitetsleverandør",
"accessControlsDescription": "Administrer hva denne brukeren kan få tilgang til og gjøre i organisasjonen",
"accessControlsSubmit": "Lagre tilgangskontroller",
"singleRolePerUserPlanNotice": "Your plan only supports one role per user.",
"singleRolePerUserEditionNotice": "This edition only supports one role per user.",
"roles": "Roller",
"accessUsersRoles": "Administrer brukere og roller",
"accessUsersRolesDescription": "Inviter brukere og legg dem til roller for å administrere tilgang til organisasjonen",
@@ -943,7 +887,7 @@
"defaultMappingsRole": "Standard rolletilordning",
"defaultMappingsRoleDescription": "Resultatet av dette uttrykket må returnere rollenavnet slik det er definert i organisasjonen som en streng.",
"defaultMappingsOrg": "Standard organisasjonstilordning",
"defaultMappingsOrgDescription": "When set, this expression must return the organization ID or true for the user to access that organization. When unset, defining an organization policy for that org is enough: the user is allowed in as long as a valid role mapping can be resolved for them within the organization.",
"defaultMappingsOrgDescription": "Dette uttrykket må returnere organisasjons-ID-en eller «true» for å gi brukeren tilgang til organisasjonen.",
"defaultMappingsSubmit": "Lagre standard tilordninger",
"orgPoliciesEdit": "Rediger Organisasjonspolicy",
"org": "Organisasjon",
@@ -1175,7 +1119,6 @@
"setupTokenDescription": "Skriv inn oppsetttoken fra serverkonsollen.",
"setupTokenRequired": "Oppsetttoken er nødvendig",
"actionUpdateSite": "Oppdater område",
"actionResetSiteBandwidth": "Reset Organization Bandwidth",
"actionListSiteRoles": "List opp tillatte områderoller",
"actionCreateResource": "Opprett ressurs",
"actionDeleteResource": "Slett ressurs",
@@ -1205,7 +1148,6 @@
"actionRemoveUser": "Fjern bruker",
"actionListUsers": "List opp brukere",
"actionAddUserRole": "Legg til brukerrolle",
"actionSetUserOrgRoles": "Set User Roles",
"actionGenerateAccessToken": "Generer tilgangstoken",
"actionDeleteAccessToken": "Slett tilgangstoken",
"actionListAccessTokens": "List opp tilgangstokener",
@@ -1322,7 +1264,6 @@
"sidebarRoles": "Roller",
"sidebarShareableLinks": "Lenker",
"sidebarApiKeys": "API-nøkler",
"sidebarProvisioning": "Provisioning",
"sidebarSettings": "Innstillinger",
"sidebarAllUsers": "Alle brukere",
"sidebarIdentityProviders": "Identitetsleverandører",
@@ -1948,40 +1889,6 @@
"exitNode": "Utgangsnode",
"country": "Land",
"rulesMatchCountry": "For tiden basert på kilde IP",
"region": "Region",
"selectRegion": "Select region",
"searchRegions": "Search regions...",
"noRegionFound": "No region found.",
"rulesMatchRegion": "Select a regional grouping of countries",
"rulesErrorInvalidRegion": "Invalid region",
"rulesErrorInvalidRegionDescription": "Please select a valid region.",
"regionAfrica": "Africa",
"regionNorthernAfrica": "Northern Africa",
"regionEasternAfrica": "Eastern Africa",
"regionMiddleAfrica": "Middle Africa",
"regionSouthernAfrica": "Southern Africa",
"regionWesternAfrica": "Western Africa",
"regionAmericas": "Americas",
"regionCaribbean": "Caribbean",
"regionCentralAmerica": "Central America",
"regionSouthAmerica": "South America",
"regionNorthernAmerica": "Northern America",
"regionAsia": "Asia",
"regionCentralAsia": "Central Asia",
"regionEasternAsia": "Eastern Asia",
"regionSouthEasternAsia": "South-Eastern Asia",
"regionSouthernAsia": "Southern Asia",
"regionWesternAsia": "Western Asia",
"regionEurope": "Europe",
"regionEasternEurope": "Eastern Europe",
"regionNorthernEurope": "Northern Europe",
"regionSouthernEurope": "Southern Europe",
"regionWesternEurope": "Western Europe",
"regionOceania": "Oceania",
"regionAustraliaAndNewZealand": "Australia and New Zealand",
"regionMelanesia": "Melanesia",
"regionMicronesia": "Micronesia",
"regionPolynesia": "Polynesia",
"managedSelfHosted": {
"title": "Administrert selv-hostet",
"description": "Sikre og lavvedlikeholdsservere, selvbetjente Pangolin med ekstra klokker, og understell",
@@ -2030,25 +1937,6 @@
"invalidValue": "Ugyldig verdi",
"idpTypeLabel": "Identitet leverandør type",
"roleMappingExpressionPlaceholder": "F.eks. inneholder(grupper, 'admin') && 'Admin' ⋅'Medlem'",
"roleMappingModeFixedRoles": "Fixed Roles",
"roleMappingModeMappingBuilder": "Mapping Builder",
"roleMappingModeRawExpression": "Raw Expression",
"roleMappingFixedRolesPlaceholderSelect": "Select one or more roles",
"roleMappingFixedRolesPlaceholderFreeform": "Type role names (exact match per organization)",
"roleMappingFixedRolesDescriptionSameForAll": "Assign the same role set to every auto-provisioned user.",
"roleMappingFixedRolesDescriptionDefaultPolicy": "For default policies, type role names that exist in each organization where users are provisioned. Names must match exactly.",
"roleMappingClaimPath": "Claim Path",
"roleMappingClaimPathPlaceholder": "groups",
"roleMappingClaimPathDescription": "Path in the token payload that contains source values (for example, groups).",
"roleMappingMatchValue": "Match Value",
"roleMappingAssignRoles": "Assign Roles",
"roleMappingAddMappingRule": "Add Mapping Rule",
"roleMappingRawExpressionResultDescription": "Expression must evaluate to a string or string array.",
"roleMappingRawExpressionResultDescriptionSingleRole": "Expression must evaluate to a string (a single role name).",
"roleMappingMatchValuePlaceholder": "Match value (for example: admin)",
"roleMappingAssignRolesPlaceholderFreeform": "Type role names (exact per org)",
"roleMappingBuilderFreeformRowHint": "Role names must match a role in each target organization.",
"roleMappingRemoveRule": "Remove",
"idpGoogleConfiguration": "Google Konfigurasjon",
"idpGoogleConfigurationDescription": "Konfigurer Google OAuth2 legitimasjonen",
"idpGoogleClientIdDescription": "Google OAuth2 Client ID",
@@ -2445,8 +2333,6 @@
"logRetentionAccessDescription": "Hvor lenge du vil beholde adgangslogger",
"logRetentionActionLabel": "Handlings logg nytt",
"logRetentionActionDescription": "Hvor lenge handlingen skal lagres",
"logRetentionConnectionLabel": "Connection Log Retention",
"logRetentionConnectionDescription": "How long to retain connection logs",
"logRetentionDisabled": "Deaktivert",
"logRetention3Days": "3 dager",
"logRetention7Days": "7 dager",
@@ -2457,14 +2343,8 @@
"logRetentionEndOfFollowingYear": "Slutt på neste år",
"actionLogsDescription": "Vis historikk for handlinger som er utført i denne organisasjonen",
"accessLogsDescription": "Vis autoriseringsforespørsler for ressurser i denne organisasjonen",
"connectionLogs": "Connection Logs",
"connectionLogsDescription": "View connection logs for tunnels in this organization",
"sidebarLogsConnection": "Connection Logs",
"sourceAddress": "Source Address",
"destinationAddress": "Destination Address",
"duration": "Duration",
"licenseRequiredToUse": "An <enterpriseLicenseLink>Enterprise Edition</enterpriseLicenseLink> license or <pangolinCloudLink>Pangolin Cloud</pangolinCloudLink> is required to use this feature. <bookADemoLink>Book a free demo or POC trial to learn more</bookADemoLink>.",
"ossEnterpriseEditionRequired": "The <enterpriseEditionLink>Enterprise Edition</enterpriseEditionLink> is required to use this feature. This feature is also available in <pangolinCloudLink>Pangolin Cloud</pangolinCloudLink>. <bookADemoLink>Book a free demo or POC trial to learn more</bookADemoLink>.",
"licenseRequiredToUse": "En <enterpriseLicenseLink>Enterprise Edition</enterpriseLicenseLink> lisens eller <pangolinCloudLink>Pangolin Cloud</pangolinCloudLink> er påkrevd for å bruke denne funksjonen. <bookADemoLink>Bestill en demo eller POC prøveversjon</bookADemoLink>.",
"ossEnterpriseEditionRequired": "<enterpriseEditionLink>Enterprise Edition</enterpriseEditionLink> er nødvendig for å bruke denne funksjonen. Denne funksjonen er også tilgjengelig i <pangolinCloudLink>Pangolin Cloud</pangolinCloudLink>. <bookADemoLink>Bestill en demo eller POC studie</bookADemoLink>.",
"certResolver": "Sertifikat løser",
"certResolverDescription": "Velg sertifikatløser som skal brukes for denne ressursen.",
"selectCertResolver": "Velg sertifikatløser",
@@ -2802,6 +2682,5 @@
"approvalsEmptyStateStep2Description": "Rediger en rolle og aktiver alternativet 'Kreve enhetsgodkjenninger'. Brukere med denne rollen vil trenge administratorgodkjenning for nye enheter.",
"approvalsEmptyStatePreviewDescription": "Forhåndsvisning: Når aktivert, ventende enhets forespørsler vil vises her for vurdering",
"approvalsEmptyStateButtonText": "Administrer Roller",
"domainErrorTitle": "Vi har problemer med å verifisere domenet ditt",
"idpAdminAutoProvisionPoliciesTabHint": "Configure role mapping and organization policies on the <policiesTabLink>Auto Provision Settings</policiesTabLink> tab."
"domainErrorTitle": "Vi har problemer med å verifisere domenet ditt"
}

View File

@@ -148,11 +148,6 @@
"createLink": "Koppeling aanmaken",
"resourcesNotFound": "Geen bronnen gevonden",
"resourceSearch": "Zoek bronnen",
"machineSearch": "Search machines",
"machinesSearch": "Search machine clients...",
"machineNotFound": "No machines found",
"userDeviceSearch": "Search user devices",
"userDevicesSearch": "Search user devices...",
"openMenu": "Menu openen",
"resource": "Bron",
"title": "Aanspreektitel",
@@ -328,54 +323,6 @@
"apiKeysDelete": "API-sleutel verwijderen",
"apiKeysManage": "API-sleutels beheren",
"apiKeysDescription": "API-sleutels worden gebruikt om te verifiëren met de integratie-API",
"provisioningKeysTitle": "Provisioning Key",
"provisioningKeysManage": "Manage Provisioning Keys",
"provisioningKeysDescription": "Provisioning keys are used to authenticate automated site provisioning for your organization.",
"provisioningManage": "Provisioning",
"provisioningDescription": "Manage provisioning keys and review pending sites awaiting approval.",
"pendingSites": "Pending Sites",
"siteApproveSuccess": "Site approved successfully",
"siteApproveError": "Error approving site",
"provisioningKeys": "Provisioning Keys",
"searchProvisioningKeys": "Search provisioning keys...",
"provisioningKeysAdd": "Generate Provisioning Key",
"provisioningKeysErrorDelete": "Error deleting provisioning key",
"provisioningKeysErrorDeleteMessage": "Error deleting provisioning key",
"provisioningKeysQuestionRemove": "Are you sure you want to remove this provisioning key from the organization?",
"provisioningKeysMessageRemove": "Once removed, the key can no longer be used for site provisioning.",
"provisioningKeysDeleteConfirm": "Confirm Delete Provisioning Key",
"provisioningKeysDelete": "Delete Provisioning key",
"provisioningKeysCreate": "Generate Provisioning Key",
"provisioningKeysCreateDescription": "Generate a new provisioning key for the organization",
"provisioningKeysSeeAll": "See all provisioning keys",
"provisioningKeysSave": "Save the provisioning key",
"provisioningKeysSaveDescription": "You will only be able to see this once. Copy it to a secure place.",
"provisioningKeysErrorCreate": "Error creating provisioning key",
"provisioningKeysList": "New provisioning key",
"provisioningKeysMaxBatchSize": "Max batch size",
"provisioningKeysUnlimitedBatchSize": "Unlimited batch size (no limit)",
"provisioningKeysMaxBatchUnlimited": "Unlimited",
"provisioningKeysMaxBatchSizeInvalid": "Enter a valid max batch size (11,000,000).",
"provisioningKeysValidUntil": "Valid until",
"provisioningKeysValidUntilHint": "Leave empty for no expiration.",
"provisioningKeysValidUntilInvalid": "Enter a valid date and time.",
"provisioningKeysNumUsed": "Times used",
"provisioningKeysLastUsed": "Last used",
"provisioningKeysNoExpiry": "No expiration",
"provisioningKeysNeverUsed": "Never",
"provisioningKeysEdit": "Edit Provisioning Key",
"provisioningKeysEditDescription": "Update the max batch size and expiration time for this key.",
"provisioningKeysApproveNewSites": "Approve new sites",
"provisioningKeysApproveNewSitesDescription": "Automatically approve sites that register with this key.",
"provisioningKeysUpdateError": "Error updating provisioning key",
"provisioningKeysUpdated": "Provisioning key updated",
"provisioningKeysUpdatedDescription": "Your changes have been saved.",
"provisioningKeysBannerTitle": "Site Provisioning Keys",
"provisioningKeysBannerDescription": "Generate a provisioning key and use it with the Newt connector to automatically create sites on first startup — no need to set up separate credentials for each site.",
"provisioningKeysBannerButtonText": "Learn More",
"pendingSitesBannerTitle": "Pending Sites",
"pendingSitesBannerDescription": "Sites that connect using a provisioning key appear here for review. Approve each site before it becomes active and gains access to your resources.",
"pendingSitesBannerButtonText": "Learn More",
"apiKeysSettings": "{apiKeyName} instellingen",
"userTitle": "Alle gebruikers beheren",
"userDescription": "Bekijk en beheer alle gebruikers in het systeem",
@@ -562,12 +509,9 @@
"userSaved": "Gebruiker opgeslagen",
"userSavedDescription": "De gebruiker is bijgewerkt.",
"autoProvisioned": "Automatisch bevestigen",
"autoProvisionSettings": "Auto Provision Settings",
"autoProvisionedDescription": "Toestaan dat deze gebruiker automatisch wordt beheerd door een identiteitsprovider",
"accessControlsDescription": "Beheer wat deze gebruiker toegang heeft tot en doet in de organisatie",
"accessControlsSubmit": "Bewaar Toegangsbesturing",
"singleRolePerUserPlanNotice": "Your plan only supports one role per user.",
"singleRolePerUserEditionNotice": "This edition only supports one role per user.",
"roles": "Rollen",
"accessUsersRoles": "Beheer Gebruikers & Rollen",
"accessUsersRolesDescription": "Nodig gebruikers uit en voeg ze toe aan de rollen om toegang tot de organisatie te beheren",
@@ -943,7 +887,7 @@
"defaultMappingsRole": "Standaard Rol Toewijzing",
"defaultMappingsRoleDescription": "Het resultaat van deze uitdrukking moet de rolnaam zoals gedefinieerd in de organisatie als tekenreeks teruggeven.",
"defaultMappingsOrg": "Standaard organisatie mapping",
"defaultMappingsOrgDescription": "When set, this expression must return the organization ID or true for the user to access that organization. When unset, defining an organization policy for that org is enough: the user is allowed in as long as a valid role mapping can be resolved for them within the organization.",
"defaultMappingsOrgDescription": "Deze expressie moet de org-ID teruggeven of waar om de gebruiker toegang te geven tot de organisatie.",
"defaultMappingsSubmit": "Standaard toewijzingen opslaan",
"orgPoliciesEdit": "Organisatie beleid bewerken",
"org": "Organisatie",
@@ -1175,7 +1119,6 @@
"setupTokenDescription": "Voer het setup-token in vanaf de serverconsole.",
"setupTokenRequired": "Setup-token is vereist",
"actionUpdateSite": "Site bijwerken",
"actionResetSiteBandwidth": "Reset Organization Bandwidth",
"actionListSiteRoles": "Toon toegestane sitenollen",
"actionCreateResource": "Bron maken",
"actionDeleteResource": "Document verwijderen",
@@ -1205,7 +1148,6 @@
"actionRemoveUser": "Gebruiker verwijderen",
"actionListUsers": "Gebruikers weergeven",
"actionAddUserRole": "Gebruikersrol toevoegen",
"actionSetUserOrgRoles": "Set User Roles",
"actionGenerateAccessToken": "Genereer Toegangstoken",
"actionDeleteAccessToken": "Verwijder toegangstoken",
"actionListAccessTokens": "Lijst toegangstokens",
@@ -1322,7 +1264,6 @@
"sidebarRoles": "Rollen",
"sidebarShareableLinks": "Koppelingen",
"sidebarApiKeys": "API sleutels",
"sidebarProvisioning": "Provisioning",
"sidebarSettings": "Instellingen",
"sidebarAllUsers": "Alle gebruikers",
"sidebarIdentityProviders": "Identiteit aanbieders",
@@ -1948,40 +1889,6 @@
"exitNode": "Exit Node",
"country": "Land",
"rulesMatchCountry": "Momenteel gebaseerd op bron IP",
"region": "Region",
"selectRegion": "Select region",
"searchRegions": "Search regions...",
"noRegionFound": "No region found.",
"rulesMatchRegion": "Select a regional grouping of countries",
"rulesErrorInvalidRegion": "Invalid region",
"rulesErrorInvalidRegionDescription": "Please select a valid region.",
"regionAfrica": "Africa",
"regionNorthernAfrica": "Northern Africa",
"regionEasternAfrica": "Eastern Africa",
"regionMiddleAfrica": "Middle Africa",
"regionSouthernAfrica": "Southern Africa",
"regionWesternAfrica": "Western Africa",
"regionAmericas": "Americas",
"regionCaribbean": "Caribbean",
"regionCentralAmerica": "Central America",
"regionSouthAmerica": "South America",
"regionNorthernAmerica": "Northern America",
"regionAsia": "Asia",
"regionCentralAsia": "Central Asia",
"regionEasternAsia": "Eastern Asia",
"regionSouthEasternAsia": "South-Eastern Asia",
"regionSouthernAsia": "Southern Asia",
"regionWesternAsia": "Western Asia",
"regionEurope": "Europe",
"regionEasternEurope": "Eastern Europe",
"regionNorthernEurope": "Northern Europe",
"regionSouthernEurope": "Southern Europe",
"regionWesternEurope": "Western Europe",
"regionOceania": "Oceania",
"regionAustraliaAndNewZealand": "Australia and New Zealand",
"regionMelanesia": "Melanesia",
"regionMicronesia": "Micronesia",
"regionPolynesia": "Polynesia",
"managedSelfHosted": {
"title": "Beheerde Self-Hosted",
"description": "betrouwbaardere en slecht onderhouden Pangolin server met extra klokken en klokkenluiders",
@@ -2030,25 +1937,6 @@
"invalidValue": "Ongeldige waarde",
"idpTypeLabel": "Identiteit provider type",
"roleMappingExpressionPlaceholder": "bijvoorbeeld bevat (groepen, 'admin') && 'Admin' ½ 'Member'",
"roleMappingModeFixedRoles": "Fixed Roles",
"roleMappingModeMappingBuilder": "Mapping Builder",
"roleMappingModeRawExpression": "Raw Expression",
"roleMappingFixedRolesPlaceholderSelect": "Select one or more roles",
"roleMappingFixedRolesPlaceholderFreeform": "Type role names (exact match per organization)",
"roleMappingFixedRolesDescriptionSameForAll": "Assign the same role set to every auto-provisioned user.",
"roleMappingFixedRolesDescriptionDefaultPolicy": "For default policies, type role names that exist in each organization where users are provisioned. Names must match exactly.",
"roleMappingClaimPath": "Claim Path",
"roleMappingClaimPathPlaceholder": "groups",
"roleMappingClaimPathDescription": "Path in the token payload that contains source values (for example, groups).",
"roleMappingMatchValue": "Match Value",
"roleMappingAssignRoles": "Assign Roles",
"roleMappingAddMappingRule": "Add Mapping Rule",
"roleMappingRawExpressionResultDescription": "Expression must evaluate to a string or string array.",
"roleMappingRawExpressionResultDescriptionSingleRole": "Expression must evaluate to a string (a single role name).",
"roleMappingMatchValuePlaceholder": "Match value (for example: admin)",
"roleMappingAssignRolesPlaceholderFreeform": "Type role names (exact per org)",
"roleMappingBuilderFreeformRowHint": "Role names must match a role in each target organization.",
"roleMappingRemoveRule": "Remove",
"idpGoogleConfiguration": "Google Configuratie",
"idpGoogleConfigurationDescription": "Configureer de Google OAuth2-referenties",
"idpGoogleClientIdDescription": "Google OAuth2 Client ID",
@@ -2445,8 +2333,6 @@
"logRetentionAccessDescription": "Hoe lang de toegangslogboeken behouden blijven",
"logRetentionActionLabel": "Actie log bewaring",
"logRetentionActionDescription": "Hoe lang de action logs behouden moeten blijven",
"logRetentionConnectionLabel": "Connection Log Retention",
"logRetentionConnectionDescription": "How long to retain connection logs",
"logRetentionDisabled": "Uitgeschakeld",
"logRetention3Days": "3 dagen",
"logRetention7Days": "7 dagen",
@@ -2457,14 +2343,8 @@
"logRetentionEndOfFollowingYear": "Einde van volgend jaar",
"actionLogsDescription": "Bekijk een geschiedenis van acties die worden uitgevoerd in deze organisatie",
"accessLogsDescription": "Toegangsverificatieverzoeken voor resources in deze organisatie bekijken",
"connectionLogs": "Connection Logs",
"connectionLogsDescription": "View connection logs for tunnels in this organization",
"sidebarLogsConnection": "Connection Logs",
"sourceAddress": "Source Address",
"destinationAddress": "Destination Address",
"duration": "Duration",
"licenseRequiredToUse": "An <enterpriseLicenseLink>Enterprise Edition</enterpriseLicenseLink> license or <pangolinCloudLink>Pangolin Cloud</pangolinCloudLink> is required to use this feature. <bookADemoLink>Book a free demo or POC trial to learn more</bookADemoLink>.",
"ossEnterpriseEditionRequired": "The <enterpriseEditionLink>Enterprise Edition</enterpriseEditionLink> is required to use this feature. This feature is also available in <pangolinCloudLink>Pangolin Cloud</pangolinCloudLink>. <bookADemoLink>Book a free demo or POC trial to learn more</bookADemoLink>.",
"licenseRequiredToUse": "Een <enterpriseLicenseLink>Enterprise Edition</enterpriseLicenseLink> licentie of <pangolinCloudLink>Pangolin Cloud</pangolinCloudLink> is vereist om deze functie te gebruiken. <bookADemoLink>Boek een demo of POC trial</bookADemoLink>.",
"ossEnterpriseEditionRequired": "De <enterpriseEditionLink>Enterprise Edition</enterpriseEditionLink> is vereist om deze functie te gebruiken. Deze functie is ook beschikbaar in <pangolinCloudLink>Pangolin Cloud</pangolinCloudLink>. <bookADemoLink>Boek een demo of POC trial</bookADemoLink>.",
"certResolver": "Certificaat Resolver",
"certResolverDescription": "Selecteer de certificaat resolver die moet worden gebruikt voor deze resource.",
"selectCertResolver": "Certificaat Resolver selecteren",
@@ -2802,6 +2682,5 @@
"approvalsEmptyStateStep2Description": "Bewerk een rol en schakel de optie 'Vereist Apparaat Goedkeuringen' in. Gebruikers met deze rol hebben admin goedkeuring nodig voor nieuwe apparaten.",
"approvalsEmptyStatePreviewDescription": "Voorbeeld: Indien ingeschakeld, zullen in afwachting van apparaatverzoeken hier verschijnen om te beoordelen",
"approvalsEmptyStateButtonText": "Rollen beheren",
"domainErrorTitle": "We ondervinden problemen bij het controleren van uw domein",
"idpAdminAutoProvisionPoliciesTabHint": "Configure role mapping and organization policies on the <policiesTabLink>Auto Provision Settings</policiesTabLink> tab."
"domainErrorTitle": "We ondervinden problemen bij het controleren van uw domein"
}

View File

@@ -148,11 +148,6 @@
"createLink": "Utwórz link",
"resourcesNotFound": "Nie znaleziono zasobów",
"resourceSearch": "Szukaj zasobów",
"machineSearch": "Search machines",
"machinesSearch": "Search machine clients...",
"machineNotFound": "No machines found",
"userDeviceSearch": "Search user devices",
"userDevicesSearch": "Search user devices...",
"openMenu": "Otwórz menu",
"resource": "Zasoby",
"title": "Tytuł",
@@ -328,54 +323,6 @@
"apiKeysDelete": "Usuń klucz API",
"apiKeysManage": "Zarządzaj kluczami API",
"apiKeysDescription": "Klucze API służą do uwierzytelniania z API integracji",
"provisioningKeysTitle": "Provisioning Key",
"provisioningKeysManage": "Manage Provisioning Keys",
"provisioningKeysDescription": "Provisioning keys are used to authenticate automated site provisioning for your organization.",
"provisioningManage": "Provisioning",
"provisioningDescription": "Manage provisioning keys and review pending sites awaiting approval.",
"pendingSites": "Pending Sites",
"siteApproveSuccess": "Site approved successfully",
"siteApproveError": "Error approving site",
"provisioningKeys": "Provisioning Keys",
"searchProvisioningKeys": "Search provisioning keys...",
"provisioningKeysAdd": "Generate Provisioning Key",
"provisioningKeysErrorDelete": "Error deleting provisioning key",
"provisioningKeysErrorDeleteMessage": "Error deleting provisioning key",
"provisioningKeysQuestionRemove": "Are you sure you want to remove this provisioning key from the organization?",
"provisioningKeysMessageRemove": "Once removed, the key can no longer be used for site provisioning.",
"provisioningKeysDeleteConfirm": "Confirm Delete Provisioning Key",
"provisioningKeysDelete": "Delete Provisioning key",
"provisioningKeysCreate": "Generate Provisioning Key",
"provisioningKeysCreateDescription": "Generate a new provisioning key for the organization",
"provisioningKeysSeeAll": "See all provisioning keys",
"provisioningKeysSave": "Save the provisioning key",
"provisioningKeysSaveDescription": "You will only be able to see this once. Copy it to a secure place.",
"provisioningKeysErrorCreate": "Error creating provisioning key",
"provisioningKeysList": "New provisioning key",
"provisioningKeysMaxBatchSize": "Max batch size",
"provisioningKeysUnlimitedBatchSize": "Unlimited batch size (no limit)",
"provisioningKeysMaxBatchUnlimited": "Unlimited",
"provisioningKeysMaxBatchSizeInvalid": "Enter a valid max batch size (11,000,000).",
"provisioningKeysValidUntil": "Valid until",
"provisioningKeysValidUntilHint": "Leave empty for no expiration.",
"provisioningKeysValidUntilInvalid": "Enter a valid date and time.",
"provisioningKeysNumUsed": "Times used",
"provisioningKeysLastUsed": "Last used",
"provisioningKeysNoExpiry": "No expiration",
"provisioningKeysNeverUsed": "Never",
"provisioningKeysEdit": "Edit Provisioning Key",
"provisioningKeysEditDescription": "Update the max batch size and expiration time for this key.",
"provisioningKeysApproveNewSites": "Approve new sites",
"provisioningKeysApproveNewSitesDescription": "Automatically approve sites that register with this key.",
"provisioningKeysUpdateError": "Error updating provisioning key",
"provisioningKeysUpdated": "Provisioning key updated",
"provisioningKeysUpdatedDescription": "Your changes have been saved.",
"provisioningKeysBannerTitle": "Site Provisioning Keys",
"provisioningKeysBannerDescription": "Generate a provisioning key and use it with the Newt connector to automatically create sites on first startup — no need to set up separate credentials for each site.",
"provisioningKeysBannerButtonText": "Learn More",
"pendingSitesBannerTitle": "Pending Sites",
"pendingSitesBannerDescription": "Sites that connect using a provisioning key appear here for review. Approve each site before it becomes active and gains access to your resources.",
"pendingSitesBannerButtonText": "Learn More",
"apiKeysSettings": "Ustawienia {apiKeyName}",
"userTitle": "Zarządzaj wszystkimi użytkownikami",
"userDescription": "Zobacz i zarządzaj wszystkimi użytkownikami w systemie",
@@ -562,12 +509,9 @@
"userSaved": "Użytkownik zapisany",
"userSavedDescription": "Użytkownik został zaktualizowany.",
"autoProvisioned": "Przesłane automatycznie",
"autoProvisionSettings": "Auto Provision Settings",
"autoProvisionedDescription": "Pozwól temu użytkownikowi na automatyczne zarządzanie przez dostawcę tożsamości",
"accessControlsDescription": "Zarządzaj tym, do czego użytkownik ma dostęp i co może robić w organizacji",
"accessControlsSubmit": "Zapisz kontrole dostępu",
"singleRolePerUserPlanNotice": "Your plan only supports one role per user.",
"singleRolePerUserEditionNotice": "This edition only supports one role per user.",
"roles": "Role",
"accessUsersRoles": "Zarządzaj użytkownikami i rolami",
"accessUsersRolesDescription": "Zaproś użytkowników i dodaj je do ról do zarządzania dostępem do organizacji",
@@ -943,7 +887,7 @@
"defaultMappingsRole": "Domyślne mapowanie roli",
"defaultMappingsRoleDescription": "JMESPath do wydobycia informacji o roli z tokena ID. Wynik tego wyrażenia musi zwrócić nazwę roli zdefiniowaną w organizacji jako ciąg znaków.",
"defaultMappingsOrg": "Domyślne mapowanie organizacji",
"defaultMappingsOrgDescription": "When set, this expression must return the organization ID or true for the user to access that organization. When unset, defining an organization policy for that org is enough: the user is allowed in as long as a valid role mapping can be resolved for them within the organization.",
"defaultMappingsOrgDescription": "JMESPath do wydobycia informacji o organizacji z tokena ID. To wyrażenie musi zwrócić ID organizacji lub true, aby użytkownik mógł uzyskać dostęp do organizacji.",
"defaultMappingsSubmit": "Zapisz domyślne mapowania",
"orgPoliciesEdit": "Edytuj politykę organizacji",
"org": "Organizacja",
@@ -1175,7 +1119,6 @@
"setupTokenDescription": "Wprowadź token konfiguracji z konsoli serwera.",
"setupTokenRequired": "Wymagany jest token konfiguracji",
"actionUpdateSite": "Aktualizuj witrynę",
"actionResetSiteBandwidth": "Reset Organization Bandwidth",
"actionListSiteRoles": "Lista dozwolonych ról witryny",
"actionCreateResource": "Utwórz zasób",
"actionDeleteResource": "Usuń zasób",
@@ -1205,7 +1148,6 @@
"actionRemoveUser": "Usuń użytkownika",
"actionListUsers": "Lista użytkowników",
"actionAddUserRole": "Dodaj rolę użytkownika",
"actionSetUserOrgRoles": "Set User Roles",
"actionGenerateAccessToken": "Wygeneruj token dostępu",
"actionDeleteAccessToken": "Usuń token dostępu",
"actionListAccessTokens": "Lista tokenów dostępu",
@@ -1322,7 +1264,6 @@
"sidebarRoles": "Role",
"sidebarShareableLinks": "Linki",
"sidebarApiKeys": "Klucze API",
"sidebarProvisioning": "Provisioning",
"sidebarSettings": "Ustawienia",
"sidebarAllUsers": "Wszyscy użytkownicy",
"sidebarIdentityProviders": "Dostawcy tożsamości",
@@ -1948,40 +1889,6 @@
"exitNode": "Węzeł Wyjściowy",
"country": "Kraj",
"rulesMatchCountry": "Obecnie bazuje na adresie IP źródła",
"region": "Region",
"selectRegion": "Select region",
"searchRegions": "Search regions...",
"noRegionFound": "No region found.",
"rulesMatchRegion": "Select a regional grouping of countries",
"rulesErrorInvalidRegion": "Invalid region",
"rulesErrorInvalidRegionDescription": "Please select a valid region.",
"regionAfrica": "Africa",
"regionNorthernAfrica": "Northern Africa",
"regionEasternAfrica": "Eastern Africa",
"regionMiddleAfrica": "Middle Africa",
"regionSouthernAfrica": "Southern Africa",
"regionWesternAfrica": "Western Africa",
"regionAmericas": "Americas",
"regionCaribbean": "Caribbean",
"regionCentralAmerica": "Central America",
"regionSouthAmerica": "South America",
"regionNorthernAmerica": "Northern America",
"regionAsia": "Asia",
"regionCentralAsia": "Central Asia",
"regionEasternAsia": "Eastern Asia",
"regionSouthEasternAsia": "South-Eastern Asia",
"regionSouthernAsia": "Southern Asia",
"regionWesternAsia": "Western Asia",
"regionEurope": "Europe",
"regionEasternEurope": "Eastern Europe",
"regionNorthernEurope": "Northern Europe",
"regionSouthernEurope": "Southern Europe",
"regionWesternEurope": "Western Europe",
"regionOceania": "Oceania",
"regionAustraliaAndNewZealand": "Australia and New Zealand",
"regionMelanesia": "Melanesia",
"regionMicronesia": "Micronesia",
"regionPolynesia": "Polynesia",
"managedSelfHosted": {
"title": "Zarządzane Samodzielnie-Hostingowane",
"description": "Większa niezawodność i niska konserwacja serwera Pangolin z dodatkowymi dzwonkami i sygnałami",
@@ -2030,25 +1937,6 @@
"invalidValue": "Nieprawidłowa wartość",
"idpTypeLabel": "Typ dostawcy tożsamości",
"roleMappingExpressionPlaceholder": "np. zawiera(grupy, 'admin') && 'Admin' || 'Członek'",
"roleMappingModeFixedRoles": "Fixed Roles",
"roleMappingModeMappingBuilder": "Mapping Builder",
"roleMappingModeRawExpression": "Raw Expression",
"roleMappingFixedRolesPlaceholderSelect": "Select one or more roles",
"roleMappingFixedRolesPlaceholderFreeform": "Type role names (exact match per organization)",
"roleMappingFixedRolesDescriptionSameForAll": "Assign the same role set to every auto-provisioned user.",
"roleMappingFixedRolesDescriptionDefaultPolicy": "For default policies, type role names that exist in each organization where users are provisioned. Names must match exactly.",
"roleMappingClaimPath": "Claim Path",
"roleMappingClaimPathPlaceholder": "groups",
"roleMappingClaimPathDescription": "Path in the token payload that contains source values (for example, groups).",
"roleMappingMatchValue": "Match Value",
"roleMappingAssignRoles": "Assign Roles",
"roleMappingAddMappingRule": "Add Mapping Rule",
"roleMappingRawExpressionResultDescription": "Expression must evaluate to a string or string array.",
"roleMappingRawExpressionResultDescriptionSingleRole": "Expression must evaluate to a string (a single role name).",
"roleMappingMatchValuePlaceholder": "Match value (for example: admin)",
"roleMappingAssignRolesPlaceholderFreeform": "Type role names (exact per org)",
"roleMappingBuilderFreeformRowHint": "Role names must match a role in each target organization.",
"roleMappingRemoveRule": "Remove",
"idpGoogleConfiguration": "Konfiguracja Google",
"idpGoogleConfigurationDescription": "Skonfiguruj dane logowania Google OAuth2",
"idpGoogleClientIdDescription": "Google OAuth2 Client ID",
@@ -2445,8 +2333,6 @@
"logRetentionAccessDescription": "Jak długo zachować dzienniki dostępu",
"logRetentionActionLabel": "Zachowanie dziennika akcji",
"logRetentionActionDescription": "Jak długo zachować dzienniki akcji",
"logRetentionConnectionLabel": "Connection Log Retention",
"logRetentionConnectionDescription": "How long to retain connection logs",
"logRetentionDisabled": "Wyłączone",
"logRetention3Days": "3 dni",
"logRetention7Days": "7 dni",
@@ -2457,14 +2343,8 @@
"logRetentionEndOfFollowingYear": "Koniec następnego roku",
"actionLogsDescription": "Zobacz historię działań wykonywanych w tej organizacji",
"accessLogsDescription": "Wyświetl prośby o autoryzację dostępu do zasobów w tej organizacji",
"connectionLogs": "Connection Logs",
"connectionLogsDescription": "View connection logs for tunnels in this organization",
"sidebarLogsConnection": "Connection Logs",
"sourceAddress": "Source Address",
"destinationAddress": "Destination Address",
"duration": "Duration",
"licenseRequiredToUse": "An <enterpriseLicenseLink>Enterprise Edition</enterpriseLicenseLink> license or <pangolinCloudLink>Pangolin Cloud</pangolinCloudLink> is required to use this feature. <bookADemoLink>Book a free demo or POC trial to learn more</bookADemoLink>.",
"ossEnterpriseEditionRequired": "The <enterpriseEditionLink>Enterprise Edition</enterpriseEditionLink> is required to use this feature. This feature is also available in <pangolinCloudLink>Pangolin Cloud</pangolinCloudLink>. <bookADemoLink>Book a free demo or POC trial to learn more</bookADemoLink>.",
"licenseRequiredToUse": "Do korzystania z tej funkcji wymagana jest licencja <enterpriseLicenseLink>Enterprise Edition</enterpriseLicenseLink> lub <pangolinCloudLink>Pangolin Cloud</pangolinCloudLink> . <bookADemoLink>Zarezerwuj wersję demonstracyjną lub wersję próbną POC</bookADemoLink>.",
"ossEnterpriseEditionRequired": "<enterpriseEditionLink>Enterprise Edition</enterpriseEditionLink> jest wymagany do korzystania z tej funkcji. Ta funkcja jest również dostępna w <pangolinCloudLink>Pangolin Cloud</pangolinCloudLink>. <bookADemoLink>Zarezerwuj demo lub okres próbny POC</bookADemoLink>.",
"certResolver": "Rozwiązywanie certyfikatów",
"certResolverDescription": "Wybierz resolver certyfikatów do użycia dla tego zasobu.",
"selectCertResolver": "Wybierz Resolver certyfikatów",
@@ -2802,6 +2682,5 @@
"approvalsEmptyStateStep2Description": "Edytuj rolę i włącz opcję \"Wymagaj zatwierdzenia urządzenia\". Użytkownicy z tą rolą będą potrzebowali zatwierdzenia administratora dla nowych urządzeń.",
"approvalsEmptyStatePreviewDescription": "Podgląd: Gdy włączone, oczekujące prośby o sprawdzenie pojawią się tutaj",
"approvalsEmptyStateButtonText": "Zarządzaj rolami",
"domainErrorTitle": "Mamy problem z weryfikacją Twojej domeny",
"idpAdminAutoProvisionPoliciesTabHint": "Configure role mapping and organization policies on the <policiesTabLink>Auto Provision Settings</policiesTabLink> tab."
"domainErrorTitle": "Mamy problem z weryfikacją Twojej domeny"
}

View File

@@ -148,11 +148,6 @@
"createLink": "Criar Link",
"resourcesNotFound": "Nenhum recurso encontrado",
"resourceSearch": "Recursos de pesquisa",
"machineSearch": "Search machines",
"machinesSearch": "Search machine clients...",
"machineNotFound": "No machines found",
"userDeviceSearch": "Search user devices",
"userDevicesSearch": "Search user devices...",
"openMenu": "Abrir menu",
"resource": "Recurso",
"title": "Título",
@@ -328,54 +323,6 @@
"apiKeysDelete": "Excluir Chave API",
"apiKeysManage": "Gerir Chaves API",
"apiKeysDescription": "As chaves API são usadas para autenticar com a API de integração",
"provisioningKeysTitle": "Provisioning Key",
"provisioningKeysManage": "Manage Provisioning Keys",
"provisioningKeysDescription": "Provisioning keys are used to authenticate automated site provisioning for your organization.",
"provisioningManage": "Provisioning",
"provisioningDescription": "Manage provisioning keys and review pending sites awaiting approval.",
"pendingSites": "Pending Sites",
"siteApproveSuccess": "Site approved successfully",
"siteApproveError": "Error approving site",
"provisioningKeys": "Provisioning Keys",
"searchProvisioningKeys": "Search provisioning keys...",
"provisioningKeysAdd": "Generate Provisioning Key",
"provisioningKeysErrorDelete": "Error deleting provisioning key",
"provisioningKeysErrorDeleteMessage": "Error deleting provisioning key",
"provisioningKeysQuestionRemove": "Are you sure you want to remove this provisioning key from the organization?",
"provisioningKeysMessageRemove": "Once removed, the key can no longer be used for site provisioning.",
"provisioningKeysDeleteConfirm": "Confirm Delete Provisioning Key",
"provisioningKeysDelete": "Delete Provisioning key",
"provisioningKeysCreate": "Generate Provisioning Key",
"provisioningKeysCreateDescription": "Generate a new provisioning key for the organization",
"provisioningKeysSeeAll": "See all provisioning keys",
"provisioningKeysSave": "Save the provisioning key",
"provisioningKeysSaveDescription": "You will only be able to see this once. Copy it to a secure place.",
"provisioningKeysErrorCreate": "Error creating provisioning key",
"provisioningKeysList": "New provisioning key",
"provisioningKeysMaxBatchSize": "Max batch size",
"provisioningKeysUnlimitedBatchSize": "Unlimited batch size (no limit)",
"provisioningKeysMaxBatchUnlimited": "Unlimited",
"provisioningKeysMaxBatchSizeInvalid": "Enter a valid max batch size (11,000,000).",
"provisioningKeysValidUntil": "Valid until",
"provisioningKeysValidUntilHint": "Leave empty for no expiration.",
"provisioningKeysValidUntilInvalid": "Enter a valid date and time.",
"provisioningKeysNumUsed": "Times used",
"provisioningKeysLastUsed": "Last used",
"provisioningKeysNoExpiry": "No expiration",
"provisioningKeysNeverUsed": "Never",
"provisioningKeysEdit": "Edit Provisioning Key",
"provisioningKeysEditDescription": "Update the max batch size and expiration time for this key.",
"provisioningKeysApproveNewSites": "Approve new sites",
"provisioningKeysApproveNewSitesDescription": "Automatically approve sites that register with this key.",
"provisioningKeysUpdateError": "Error updating provisioning key",
"provisioningKeysUpdated": "Provisioning key updated",
"provisioningKeysUpdatedDescription": "Your changes have been saved.",
"provisioningKeysBannerTitle": "Site Provisioning Keys",
"provisioningKeysBannerDescription": "Generate a provisioning key and use it with the Newt connector to automatically create sites on first startup — no need to set up separate credentials for each site.",
"provisioningKeysBannerButtonText": "Learn More",
"pendingSitesBannerTitle": "Pending Sites",
"pendingSitesBannerDescription": "Sites that connect using a provisioning key appear here for review. Approve each site before it becomes active and gains access to your resources.",
"pendingSitesBannerButtonText": "Learn More",
"apiKeysSettings": "Configurações de {apiKeyName}",
"userTitle": "Gerir Todos os Utilizadores",
"userDescription": "Visualizar e gerir todos os utilizadores no sistema",
@@ -562,12 +509,9 @@
"userSaved": "Usuário salvo",
"userSavedDescription": "O utilizador foi atualizado.",
"autoProvisioned": "Auto provisionado",
"autoProvisionSettings": "Auto Provision Settings",
"autoProvisionedDescription": "Permitir que este utilizador seja gerido automaticamente pelo provedor de identidade",
"accessControlsDescription": "Gerir o que este utilizador pode aceder e fazer na organização",
"accessControlsSubmit": "Guardar Controlos de Acesso",
"singleRolePerUserPlanNotice": "Your plan only supports one role per user.",
"singleRolePerUserEditionNotice": "This edition only supports one role per user.",
"roles": "Funções",
"accessUsersRoles": "Gerir Utilizadores e Funções",
"accessUsersRolesDescription": "Convidar usuários e adicioná-los a funções para gerenciar o acesso à organização",
@@ -943,7 +887,7 @@
"defaultMappingsRole": "Mapeamento de Função Padrão",
"defaultMappingsRoleDescription": "JMESPath para extrair informações de função do token ID. O resultado desta expressão deve retornar o nome da função como definido na organização como uma string.",
"defaultMappingsOrg": "Mapeamento de Organização Padrão",
"defaultMappingsOrgDescription": "When set, this expression must return the organization ID or true for the user to access that organization. When unset, defining an organization policy for that org is enough: the user is allowed in as long as a valid role mapping can be resolved for them within the organization.",
"defaultMappingsOrgDescription": "JMESPath para extrair informações da organização do token ID. Esta expressão deve retornar o ID da organização ou verdadeiro para que o utilizador tenha permissão para aceder à organização.",
"defaultMappingsSubmit": "Guardar Mapeamentos Padrão",
"orgPoliciesEdit": "Editar Política da Organização",
"org": "Organização",
@@ -1175,7 +1119,6 @@
"setupTokenDescription": "Digite o token de configuração do console do servidor.",
"setupTokenRequired": "Token de configuração é necessário",
"actionUpdateSite": "Atualizar Site",
"actionResetSiteBandwidth": "Reset Organization Bandwidth",
"actionListSiteRoles": "Listar Funções Permitidas do Site",
"actionCreateResource": "Criar Recurso",
"actionDeleteResource": "Eliminar Recurso",
@@ -1205,7 +1148,6 @@
"actionRemoveUser": "Remover Utilizador",
"actionListUsers": "Listar Utilizadores",
"actionAddUserRole": "Adicionar Função ao Utilizador",
"actionSetUserOrgRoles": "Set User Roles",
"actionGenerateAccessToken": "Gerar Token de Acesso",
"actionDeleteAccessToken": "Eliminar Token de Acesso",
"actionListAccessTokens": "Listar Tokens de Acesso",
@@ -1322,7 +1264,6 @@
"sidebarRoles": "Papéis",
"sidebarShareableLinks": "Links",
"sidebarApiKeys": "Chaves API",
"sidebarProvisioning": "Provisioning",
"sidebarSettings": "Configurações",
"sidebarAllUsers": "Todos os utilizadores",
"sidebarIdentityProviders": "Provedores de identidade",
@@ -1948,40 +1889,6 @@
"exitNode": "Nodo de Saída",
"country": "País",
"rulesMatchCountry": "Atualmente baseado no IP de origem",
"region": "Region",
"selectRegion": "Select region",
"searchRegions": "Search regions...",
"noRegionFound": "No region found.",
"rulesMatchRegion": "Select a regional grouping of countries",
"rulesErrorInvalidRegion": "Invalid region",
"rulesErrorInvalidRegionDescription": "Please select a valid region.",
"regionAfrica": "Africa",
"regionNorthernAfrica": "Northern Africa",
"regionEasternAfrica": "Eastern Africa",
"regionMiddleAfrica": "Middle Africa",
"regionSouthernAfrica": "Southern Africa",
"regionWesternAfrica": "Western Africa",
"regionAmericas": "Americas",
"regionCaribbean": "Caribbean",
"regionCentralAmerica": "Central America",
"regionSouthAmerica": "South America",
"regionNorthernAmerica": "Northern America",
"regionAsia": "Asia",
"regionCentralAsia": "Central Asia",
"regionEasternAsia": "Eastern Asia",
"regionSouthEasternAsia": "South-Eastern Asia",
"regionSouthernAsia": "Southern Asia",
"regionWesternAsia": "Western Asia",
"regionEurope": "Europe",
"regionEasternEurope": "Eastern Europe",
"regionNorthernEurope": "Northern Europe",
"regionSouthernEurope": "Southern Europe",
"regionWesternEurope": "Western Europe",
"regionOceania": "Oceania",
"regionAustraliaAndNewZealand": "Australia and New Zealand",
"regionMelanesia": "Melanesia",
"regionMicronesia": "Micronesia",
"regionPolynesia": "Polynesia",
"managedSelfHosted": {
"title": "Gerenciado Auto-Hospedado",
"description": "Servidor Pangolin auto-hospedado mais confiável e com baixa manutenção com sinos extras e assobiamentos",
@@ -2030,25 +1937,6 @@
"invalidValue": "Valor Inválido",
"idpTypeLabel": "Tipo de provedor de identidade",
"roleMappingExpressionPlaceholder": "ex.: Contem (grupos, 'administrador') && 'Administrador' 「'Membro'",
"roleMappingModeFixedRoles": "Fixed Roles",
"roleMappingModeMappingBuilder": "Mapping Builder",
"roleMappingModeRawExpression": "Raw Expression",
"roleMappingFixedRolesPlaceholderSelect": "Select one or more roles",
"roleMappingFixedRolesPlaceholderFreeform": "Type role names (exact match per organization)",
"roleMappingFixedRolesDescriptionSameForAll": "Assign the same role set to every auto-provisioned user.",
"roleMappingFixedRolesDescriptionDefaultPolicy": "For default policies, type role names that exist in each organization where users are provisioned. Names must match exactly.",
"roleMappingClaimPath": "Claim Path",
"roleMappingClaimPathPlaceholder": "groups",
"roleMappingClaimPathDescription": "Path in the token payload that contains source values (for example, groups).",
"roleMappingMatchValue": "Match Value",
"roleMappingAssignRoles": "Assign Roles",
"roleMappingAddMappingRule": "Add Mapping Rule",
"roleMappingRawExpressionResultDescription": "Expression must evaluate to a string or string array.",
"roleMappingRawExpressionResultDescriptionSingleRole": "Expression must evaluate to a string (a single role name).",
"roleMappingMatchValuePlaceholder": "Match value (for example: admin)",
"roleMappingAssignRolesPlaceholderFreeform": "Type role names (exact per org)",
"roleMappingBuilderFreeformRowHint": "Role names must match a role in each target organization.",
"roleMappingRemoveRule": "Remove",
"idpGoogleConfiguration": "Configuração do Google",
"idpGoogleConfigurationDescription": "Configurar as credenciais do Google OAuth2",
"idpGoogleClientIdDescription": "Google OAuth2 Client ID",
@@ -2445,8 +2333,6 @@
"logRetentionAccessDescription": "Por quanto tempo manter os registros de acesso",
"logRetentionActionLabel": "Ação de Retenção no Log",
"logRetentionActionDescription": "Por quanto tempo manter os registros de ação",
"logRetentionConnectionLabel": "Connection Log Retention",
"logRetentionConnectionDescription": "How long to retain connection logs",
"logRetentionDisabled": "Desabilitado",
"logRetention3Days": "3 dias",
"logRetention7Days": "7 dias",
@@ -2457,14 +2343,8 @@
"logRetentionEndOfFollowingYear": "Fim do ano seguinte",
"actionLogsDescription": "Visualizar histórico de ações realizadas nesta organização",
"accessLogsDescription": "Ver solicitações de autenticação de recursos nesta organização",
"connectionLogs": "Connection Logs",
"connectionLogsDescription": "View connection logs for tunnels in this organization",
"sidebarLogsConnection": "Connection Logs",
"sourceAddress": "Source Address",
"destinationAddress": "Destination Address",
"duration": "Duration",
"licenseRequiredToUse": "An <enterpriseLicenseLink>Enterprise Edition</enterpriseLicenseLink> license or <pangolinCloudLink>Pangolin Cloud</pangolinCloudLink> is required to use this feature. <bookADemoLink>Book a free demo or POC trial to learn more</bookADemoLink>.",
"ossEnterpriseEditionRequired": "The <enterpriseEditionLink>Enterprise Edition</enterpriseEditionLink> is required to use this feature. This feature is also available in <pangolinCloudLink>Pangolin Cloud</pangolinCloudLink>. <bookADemoLink>Book a free demo or POC trial to learn more</bookADemoLink>.",
"licenseRequiredToUse": "Uma licença <enterpriseLicenseLink>Enterprise Edition</enterpriseLicenseLink> ou <pangolinCloudLink>Pangolin Cloud</pangolinCloudLink> é necessária para usar este recurso. <bookADemoLink>Reserve um teste de demonstração ou POC</bookADemoLink>.",
"ossEnterpriseEditionRequired": "O <enterpriseEditionLink>Enterprise Edition</enterpriseEditionLink> é necessário para usar este recurso. Este recurso também está disponível no <pangolinCloudLink>Pangolin Cloud</pangolinCloudLink>. <bookADemoLink>Reserve uma demonstração ou avaliação POC</bookADemoLink>.",
"certResolver": "Resolvedor de Certificado",
"certResolverDescription": "Selecione o resolvedor de certificados para este recurso.",
"selectCertResolver": "Selecionar solucionador de certificado",
@@ -2802,6 +2682,5 @@
"approvalsEmptyStateStep2Description": "Editar uma função e habilitar a opção 'Exigir aprovação de dispositivos'. Usuários com essa função precisarão de aprovação de administrador para novos dispositivos.",
"approvalsEmptyStatePreviewDescription": "Pré-visualização: Quando ativado, solicitações de dispositivo pendentes aparecerão aqui para revisão",
"approvalsEmptyStateButtonText": "Gerir Funções",
"domainErrorTitle": "Estamos tendo problemas ao verificar seu domínio",
"idpAdminAutoProvisionPoliciesTabHint": "Configure role mapping and organization policies on the <policiesTabLink>Auto Provision Settings</policiesTabLink> tab."
"domainErrorTitle": "Estamos tendo problemas ao verificar seu domínio"
}

View File

@@ -148,11 +148,6 @@
"createLink": "Создать ссылку",
"resourcesNotFound": "Ресурсы не найдены",
"resourceSearch": "Поиск ресурсов",
"machineSearch": "Search machines",
"machinesSearch": "Search machine clients...",
"machineNotFound": "No machines found",
"userDeviceSearch": "Search user devices",
"userDevicesSearch": "Search user devices...",
"openMenu": "Открыть меню",
"resource": "Ресурс",
"title": "Заголовок",
@@ -328,54 +323,6 @@
"apiKeysDelete": "Удаление ключа API",
"apiKeysManage": "Управление ключами API",
"apiKeysDescription": "Ключи API используются для аутентификации в интеграционном API",
"provisioningKeysTitle": "Provisioning Key",
"provisioningKeysManage": "Manage Provisioning Keys",
"provisioningKeysDescription": "Provisioning keys are used to authenticate automated site provisioning for your organization.",
"provisioningManage": "Provisioning",
"provisioningDescription": "Manage provisioning keys and review pending sites awaiting approval.",
"pendingSites": "Pending Sites",
"siteApproveSuccess": "Site approved successfully",
"siteApproveError": "Error approving site",
"provisioningKeys": "Provisioning Keys",
"searchProvisioningKeys": "Search provisioning keys...",
"provisioningKeysAdd": "Generate Provisioning Key",
"provisioningKeysErrorDelete": "Error deleting provisioning key",
"provisioningKeysErrorDeleteMessage": "Error deleting provisioning key",
"provisioningKeysQuestionRemove": "Are you sure you want to remove this provisioning key from the organization?",
"provisioningKeysMessageRemove": "Once removed, the key can no longer be used for site provisioning.",
"provisioningKeysDeleteConfirm": "Confirm Delete Provisioning Key",
"provisioningKeysDelete": "Delete Provisioning key",
"provisioningKeysCreate": "Generate Provisioning Key",
"provisioningKeysCreateDescription": "Generate a new provisioning key for the organization",
"provisioningKeysSeeAll": "See all provisioning keys",
"provisioningKeysSave": "Save the provisioning key",
"provisioningKeysSaveDescription": "You will only be able to see this once. Copy it to a secure place.",
"provisioningKeysErrorCreate": "Error creating provisioning key",
"provisioningKeysList": "New provisioning key",
"provisioningKeysMaxBatchSize": "Max batch size",
"provisioningKeysUnlimitedBatchSize": "Unlimited batch size (no limit)",
"provisioningKeysMaxBatchUnlimited": "Unlimited",
"provisioningKeysMaxBatchSizeInvalid": "Enter a valid max batch size (11,000,000).",
"provisioningKeysValidUntil": "Valid until",
"provisioningKeysValidUntilHint": "Leave empty for no expiration.",
"provisioningKeysValidUntilInvalid": "Enter a valid date and time.",
"provisioningKeysNumUsed": "Times used",
"provisioningKeysLastUsed": "Last used",
"provisioningKeysNoExpiry": "No expiration",
"provisioningKeysNeverUsed": "Never",
"provisioningKeysEdit": "Edit Provisioning Key",
"provisioningKeysEditDescription": "Update the max batch size and expiration time for this key.",
"provisioningKeysApproveNewSites": "Approve new sites",
"provisioningKeysApproveNewSitesDescription": "Automatically approve sites that register with this key.",
"provisioningKeysUpdateError": "Error updating provisioning key",
"provisioningKeysUpdated": "Provisioning key updated",
"provisioningKeysUpdatedDescription": "Your changes have been saved.",
"provisioningKeysBannerTitle": "Site Provisioning Keys",
"provisioningKeysBannerDescription": "Generate a provisioning key and use it with the Newt connector to automatically create sites on first startup — no need to set up separate credentials for each site.",
"provisioningKeysBannerButtonText": "Learn More",
"pendingSitesBannerTitle": "Pending Sites",
"pendingSitesBannerDescription": "Sites that connect using a provisioning key appear here for review. Approve each site before it becomes active and gains access to your resources.",
"pendingSitesBannerButtonText": "Learn More",
"apiKeysSettings": "Настройки {apiKeyName}",
"userTitle": "Управление всеми пользователями",
"userDescription": "Просмотр и управление всеми пользователями в системе",
@@ -562,12 +509,9 @@
"userSaved": "Пользователь сохранён",
"userSavedDescription": "Пользователь был обновлён.",
"autoProvisioned": "Автоподбор",
"autoProvisionSettings": "Auto Provision Settings",
"autoProvisionedDescription": "Разрешить автоматическое управление этим пользователем",
"accessControlsDescription": "Управляйте тем, к чему этот пользователь может получить доступ и что делать в организации",
"accessControlsSubmit": "Сохранить контроль доступа",
"singleRolePerUserPlanNotice": "Your plan only supports one role per user.",
"singleRolePerUserEditionNotice": "This edition only supports one role per user.",
"roles": "Роли",
"accessUsersRoles": "Управление пользователями и ролями",
"accessUsersRolesDescription": "Пригласить пользователей и добавить их в роли для управления доступом к организации",
@@ -943,7 +887,7 @@
"defaultMappingsRole": "Сопоставление ролей по умолчанию",
"defaultMappingsRoleDescription": "Результат этого выражения должен возвращать имя роли, как определено в организации, в виде строки.",
"defaultMappingsOrg": "Сопоставление организаций по умолчанию",
"defaultMappingsOrgDescription": "When set, this expression must return the organization ID or true for the user to access that organization. When unset, defining an organization policy for that org is enough: the user is allowed in as long as a valid role mapping can be resolved for them within the organization.",
"defaultMappingsOrgDescription": "Это выражение должно возвращать ID организации или true для разрешения доступа пользователя к организации.",
"defaultMappingsSubmit": "Сохранить сопоставления по умолчанию",
"orgPoliciesEdit": "Редактировать политику организации",
"org": "Организация",
@@ -1175,7 +1119,6 @@
"setupTokenDescription": "Введите токен настройки из консоли сервера.",
"setupTokenRequired": "Токен настройки обязателен",
"actionUpdateSite": "Обновить сайт",
"actionResetSiteBandwidth": "Reset Organization Bandwidth",
"actionListSiteRoles": "Список разрешенных ролей сайта",
"actionCreateResource": "Создать ресурс",
"actionDeleteResource": "Удалить ресурс",
@@ -1205,7 +1148,6 @@
"actionRemoveUser": "Удалить пользователя",
"actionListUsers": "Список пользователей",
"actionAddUserRole": "Добавить роль пользователя",
"actionSetUserOrgRoles": "Set User Roles",
"actionGenerateAccessToken": "Сгенерировать токен доступа",
"actionDeleteAccessToken": "Удалить токен доступа",
"actionListAccessTokens": "Список токенов доступа",
@@ -1322,7 +1264,6 @@
"sidebarRoles": "Роли",
"sidebarShareableLinks": "Ссылки",
"sidebarApiKeys": "API ключи",
"sidebarProvisioning": "Provisioning",
"sidebarSettings": "Настройки",
"sidebarAllUsers": "Все пользователи",
"sidebarIdentityProviders": "Поставщики удостоверений",
@@ -1948,40 +1889,6 @@
"exitNode": "Узел выхода",
"country": "Страна",
"rulesMatchCountry": "В настоящее время основано на исходном IP",
"region": "Region",
"selectRegion": "Select region",
"searchRegions": "Search regions...",
"noRegionFound": "No region found.",
"rulesMatchRegion": "Select a regional grouping of countries",
"rulesErrorInvalidRegion": "Invalid region",
"rulesErrorInvalidRegionDescription": "Please select a valid region.",
"regionAfrica": "Africa",
"regionNorthernAfrica": "Northern Africa",
"regionEasternAfrica": "Eastern Africa",
"regionMiddleAfrica": "Middle Africa",
"regionSouthernAfrica": "Southern Africa",
"regionWesternAfrica": "Western Africa",
"regionAmericas": "Americas",
"regionCaribbean": "Caribbean",
"regionCentralAmerica": "Central America",
"regionSouthAmerica": "South America",
"regionNorthernAmerica": "Northern America",
"regionAsia": "Asia",
"regionCentralAsia": "Central Asia",
"regionEasternAsia": "Eastern Asia",
"regionSouthEasternAsia": "South-Eastern Asia",
"regionSouthernAsia": "Southern Asia",
"regionWesternAsia": "Western Asia",
"regionEurope": "Europe",
"regionEasternEurope": "Eastern Europe",
"regionNorthernEurope": "Northern Europe",
"regionSouthernEurope": "Southern Europe",
"regionWesternEurope": "Western Europe",
"regionOceania": "Oceania",
"regionAustraliaAndNewZealand": "Australia and New Zealand",
"regionMelanesia": "Melanesia",
"regionMicronesia": "Micronesia",
"regionPolynesia": "Polynesia",
"managedSelfHosted": {
"title": "Управляемый с самовывоза",
"description": "Более надежный и низко обслуживаемый сервер Pangolin с дополнительными колокольнями и свистками",
@@ -2030,25 +1937,6 @@
"invalidValue": "Неверное значение",
"idpTypeLabel": "Тип поставщика удостоверений",
"roleMappingExpressionPlaceholder": "например, contains(groups, 'admin') && 'Admin' || 'Member'",
"roleMappingModeFixedRoles": "Fixed Roles",
"roleMappingModeMappingBuilder": "Mapping Builder",
"roleMappingModeRawExpression": "Raw Expression",
"roleMappingFixedRolesPlaceholderSelect": "Select one or more roles",
"roleMappingFixedRolesPlaceholderFreeform": "Type role names (exact match per organization)",
"roleMappingFixedRolesDescriptionSameForAll": "Assign the same role set to every auto-provisioned user.",
"roleMappingFixedRolesDescriptionDefaultPolicy": "For default policies, type role names that exist in each organization where users are provisioned. Names must match exactly.",
"roleMappingClaimPath": "Claim Path",
"roleMappingClaimPathPlaceholder": "groups",
"roleMappingClaimPathDescription": "Path in the token payload that contains source values (for example, groups).",
"roleMappingMatchValue": "Match Value",
"roleMappingAssignRoles": "Assign Roles",
"roleMappingAddMappingRule": "Add Mapping Rule",
"roleMappingRawExpressionResultDescription": "Expression must evaluate to a string or string array.",
"roleMappingRawExpressionResultDescriptionSingleRole": "Expression must evaluate to a string (a single role name).",
"roleMappingMatchValuePlaceholder": "Match value (for example: admin)",
"roleMappingAssignRolesPlaceholderFreeform": "Type role names (exact per org)",
"roleMappingBuilderFreeformRowHint": "Role names must match a role in each target organization.",
"roleMappingRemoveRule": "Remove",
"idpGoogleConfiguration": "Конфигурация Google",
"idpGoogleConfigurationDescription": "Настройка учетных данных Google OAuth2",
"idpGoogleClientIdDescription": "Google OAuth2 Client ID",
@@ -2445,8 +2333,6 @@
"logRetentionAccessDescription": "Как долго сохранять журналы доступа",
"logRetentionActionLabel": "Сохранение журнала действий",
"logRetentionActionDescription": "Как долго хранить журналы действий",
"logRetentionConnectionLabel": "Connection Log Retention",
"logRetentionConnectionDescription": "How long to retain connection logs",
"logRetentionDisabled": "Отключено",
"logRetention3Days": "3 дня",
"logRetention7Days": "7 дней",
@@ -2457,14 +2343,8 @@
"logRetentionEndOfFollowingYear": "Конец следующего года",
"actionLogsDescription": "Просмотр истории действий, выполненных в этой организации",
"accessLogsDescription": "Просмотр запросов авторизации доступа к ресурсам этой организации",
"connectionLogs": "Connection Logs",
"connectionLogsDescription": "View connection logs for tunnels in this organization",
"sidebarLogsConnection": "Connection Logs",
"sourceAddress": "Source Address",
"destinationAddress": "Destination Address",
"duration": "Duration",
"licenseRequiredToUse": "An <enterpriseLicenseLink>Enterprise Edition</enterpriseLicenseLink> license or <pangolinCloudLink>Pangolin Cloud</pangolinCloudLink> is required to use this feature. <bookADemoLink>Book a free demo or POC trial to learn more</bookADemoLink>.",
"ossEnterpriseEditionRequired": "The <enterpriseEditionLink>Enterprise Edition</enterpriseEditionLink> is required to use this feature. This feature is also available in <pangolinCloudLink>Pangolin Cloud</pangolinCloudLink>. <bookADemoLink>Book a free demo or POC trial to learn more</bookADemoLink>.",
"licenseRequiredToUse": "Требуется лицензия на <enterpriseLicenseLink>Enterprise Edition</enterpriseLicenseLink> или <pangolinCloudLink>Pangolin Cloud</pangolinCloudLink> для использования этой функции. <bookADemoLink>Забронируйте демонстрацию или пробный POC</bookADemoLink>.",
"ossEnterpriseEditionRequired": "<enterpriseEditionLink>Enterprise Edition</enterpriseEditionLink> требуется для использования этой функции. Эта функция также доступна в <pangolinCloudLink>Pangolin Cloud</pangolinCloudLink>. <bookADemoLink>Забронируйте демонстрацию или пробный POC</bookADemoLink>.",
"certResolver": "Резольвер сертификата",
"certResolverDescription": "Выберите резолвер сертификата, который будет использоваться для этого ресурса.",
"selectCertResolver": "Выберите резолвер сертификата",
@@ -2802,6 +2682,5 @@
"approvalsEmptyStateStep2Description": "Редактировать роль и включить опцию 'Требовать утверждения устройств'. Пользователям с этой ролью потребуется подтверждение администратора для новых устройств.",
"approvalsEmptyStatePreviewDescription": "Предпросмотр: Если включено, ожидающие запросы на устройство появятся здесь для проверки",
"approvalsEmptyStateButtonText": "Управление ролями",
"domainErrorTitle": "У нас возникли проблемы с проверкой вашего домена",
"idpAdminAutoProvisionPoliciesTabHint": "Configure role mapping and organization policies on the <policiesTabLink>Auto Provision Settings</policiesTabLink> tab."
"domainErrorTitle": "У нас возникли проблемы с проверкой вашего домена"
}

View File

@@ -148,11 +148,6 @@
"createLink": "Bağlantı Oluştur",
"resourcesNotFound": "Hiçbir kaynak bulunamadı",
"resourceSearch": "Kaynak ara",
"machineSearch": "Search machines",
"machinesSearch": "Search machine clients...",
"machineNotFound": "No machines found",
"userDeviceSearch": "Search user devices",
"userDevicesSearch": "Search user devices...",
"openMenu": "Menüyü Aç",
"resource": "Kaynak",
"title": "Başlık",
@@ -328,54 +323,6 @@
"apiKeysDelete": "API Anahtarını Sil",
"apiKeysManage": "API Anahtarlarını Yönet",
"apiKeysDescription": "API anahtarları entegrasyon API'sini doğrulamak için kullanılır",
"provisioningKeysTitle": "Provisioning Key",
"provisioningKeysManage": "Manage Provisioning Keys",
"provisioningKeysDescription": "Provisioning keys are used to authenticate automated site provisioning for your organization.",
"provisioningManage": "Provisioning",
"provisioningDescription": "Manage provisioning keys and review pending sites awaiting approval.",
"pendingSites": "Pending Sites",
"siteApproveSuccess": "Site approved successfully",
"siteApproveError": "Error approving site",
"provisioningKeys": "Provisioning Keys",
"searchProvisioningKeys": "Search provisioning keys...",
"provisioningKeysAdd": "Generate Provisioning Key",
"provisioningKeysErrorDelete": "Error deleting provisioning key",
"provisioningKeysErrorDeleteMessage": "Error deleting provisioning key",
"provisioningKeysQuestionRemove": "Are you sure you want to remove this provisioning key from the organization?",
"provisioningKeysMessageRemove": "Once removed, the key can no longer be used for site provisioning.",
"provisioningKeysDeleteConfirm": "Confirm Delete Provisioning Key",
"provisioningKeysDelete": "Delete Provisioning key",
"provisioningKeysCreate": "Generate Provisioning Key",
"provisioningKeysCreateDescription": "Generate a new provisioning key for the organization",
"provisioningKeysSeeAll": "See all provisioning keys",
"provisioningKeysSave": "Save the provisioning key",
"provisioningKeysSaveDescription": "You will only be able to see this once. Copy it to a secure place.",
"provisioningKeysErrorCreate": "Error creating provisioning key",
"provisioningKeysList": "New provisioning key",
"provisioningKeysMaxBatchSize": "Max batch size",
"provisioningKeysUnlimitedBatchSize": "Unlimited batch size (no limit)",
"provisioningKeysMaxBatchUnlimited": "Unlimited",
"provisioningKeysMaxBatchSizeInvalid": "Enter a valid max batch size (11,000,000).",
"provisioningKeysValidUntil": "Valid until",
"provisioningKeysValidUntilHint": "Leave empty for no expiration.",
"provisioningKeysValidUntilInvalid": "Enter a valid date and time.",
"provisioningKeysNumUsed": "Times used",
"provisioningKeysLastUsed": "Last used",
"provisioningKeysNoExpiry": "No expiration",
"provisioningKeysNeverUsed": "Never",
"provisioningKeysEdit": "Edit Provisioning Key",
"provisioningKeysEditDescription": "Update the max batch size and expiration time for this key.",
"provisioningKeysApproveNewSites": "Approve new sites",
"provisioningKeysApproveNewSitesDescription": "Automatically approve sites that register with this key.",
"provisioningKeysUpdateError": "Error updating provisioning key",
"provisioningKeysUpdated": "Provisioning key updated",
"provisioningKeysUpdatedDescription": "Your changes have been saved.",
"provisioningKeysBannerTitle": "Site Provisioning Keys",
"provisioningKeysBannerDescription": "Generate a provisioning key and use it with the Newt connector to automatically create sites on first startup — no need to set up separate credentials for each site.",
"provisioningKeysBannerButtonText": "Learn More",
"pendingSitesBannerTitle": "Pending Sites",
"pendingSitesBannerDescription": "Sites that connect using a provisioning key appear here for review. Approve each site before it becomes active and gains access to your resources.",
"pendingSitesBannerButtonText": "Learn More",
"apiKeysSettings": "{apiKeyName} Ayarları",
"userTitle": "Tüm Kullanıcıları Yönet",
"userDescription": "Sistemdeki tüm kullanıcıları görün ve yönetin",
@@ -562,12 +509,9 @@
"userSaved": "Kullanıcı kaydedildi",
"userSavedDescription": "Kullanıcı güncellenmiştir.",
"autoProvisioned": "Otomatik Sağlandı",
"autoProvisionSettings": "Auto Provision Settings",
"autoProvisionedDescription": "Bu kullanıcının kimlik sağlayıcısı tarafından otomatik olarak yönetilmesine izin ver",
"accessControlsDescription": "Bu kullanıcının organizasyonda neleri erişebileceğini ve yapabileceğini yönetin",
"accessControlsSubmit": "Erişim Kontrollerini Kaydet",
"singleRolePerUserPlanNotice": "Your plan only supports one role per user.",
"singleRolePerUserEditionNotice": "This edition only supports one role per user.",
"roles": "Roller",
"accessUsersRoles": "Kullanıcılar ve Roller Yönetin",
"accessUsersRolesDescription": "Kullanıcılara davet gönderin ve organizasyona erişimi yönetmek için rollere ekleyin",
@@ -943,7 +887,7 @@
"defaultMappingsRole": "Varsayılan Rol Eşleme",
"defaultMappingsRoleDescription": "JMESPath to extract role information from the ID token. The result of this expression must return the role name as defined in the organization as a string.",
"defaultMappingsOrg": "Varsayılan Kuruluş Eşleme",
"defaultMappingsOrgDescription": "When set, this expression must return the organization ID or true for the user to access that organization. When unset, defining an organization policy for that org is enough: the user is allowed in as long as a valid role mapping can be resolved for them within the organization.",
"defaultMappingsOrgDescription": "JMESPath to extract organization information from the ID token. This expression must return the org ID or true for the user to be allowed to access the organization.",
"defaultMappingsSubmit": "Varsayılan Eşlemeleri Kaydet",
"orgPoliciesEdit": "Kuruluş Politikasını Düzenle",
"org": "Kuruluş",
@@ -1175,7 +1119,6 @@
"setupTokenDescription": "Sunucu konsolundan kurulum simgesini girin.",
"setupTokenRequired": "Kurulum simgesi gerekli",
"actionUpdateSite": "Siteyi Güncelle",
"actionResetSiteBandwidth": "Reset Organization Bandwidth",
"actionListSiteRoles": "İzin Verilen Site Rolleri Listele",
"actionCreateResource": "Kaynak Oluştur",
"actionDeleteResource": "Kaynağı Sil",
@@ -1205,7 +1148,6 @@
"actionRemoveUser": "Kullanıcıyı Kaldır",
"actionListUsers": "Kullanıcıları Listele",
"actionAddUserRole": "Kullanıcı Rolü Ekle",
"actionSetUserOrgRoles": "Set User Roles",
"actionGenerateAccessToken": "Erişim Jetonu Oluştur",
"actionDeleteAccessToken": "Erişim Jetonunu Sil",
"actionListAccessTokens": "Erişim Jetonlarını Listele",
@@ -1322,7 +1264,6 @@
"sidebarRoles": "Roller",
"sidebarShareableLinks": "Bağlantılar",
"sidebarApiKeys": "API Anahtarları",
"sidebarProvisioning": "Provisioning",
"sidebarSettings": "Ayarlar",
"sidebarAllUsers": "Tüm Kullanıcılar",
"sidebarIdentityProviders": "Kimlik Sağlayıcılar",
@@ -1948,40 +1889,6 @@
"exitNode": ıkış Düğümü",
"country": "Ülke",
"rulesMatchCountry": "Şu anda kaynak IP'ye dayanarak",
"region": "Region",
"selectRegion": "Select region",
"searchRegions": "Search regions...",
"noRegionFound": "No region found.",
"rulesMatchRegion": "Select a regional grouping of countries",
"rulesErrorInvalidRegion": "Invalid region",
"rulesErrorInvalidRegionDescription": "Please select a valid region.",
"regionAfrica": "Africa",
"regionNorthernAfrica": "Northern Africa",
"regionEasternAfrica": "Eastern Africa",
"regionMiddleAfrica": "Middle Africa",
"regionSouthernAfrica": "Southern Africa",
"regionWesternAfrica": "Western Africa",
"regionAmericas": "Americas",
"regionCaribbean": "Caribbean",
"regionCentralAmerica": "Central America",
"regionSouthAmerica": "South America",
"regionNorthernAmerica": "Northern America",
"regionAsia": "Asia",
"regionCentralAsia": "Central Asia",
"regionEasternAsia": "Eastern Asia",
"regionSouthEasternAsia": "South-Eastern Asia",
"regionSouthernAsia": "Southern Asia",
"regionWesternAsia": "Western Asia",
"regionEurope": "Europe",
"regionEasternEurope": "Eastern Europe",
"regionNorthernEurope": "Northern Europe",
"regionSouthernEurope": "Southern Europe",
"regionWesternEurope": "Western Europe",
"regionOceania": "Oceania",
"regionAustraliaAndNewZealand": "Australia and New Zealand",
"regionMelanesia": "Melanesia",
"regionMicronesia": "Micronesia",
"regionPolynesia": "Polynesia",
"managedSelfHosted": {
"title": "Yönetilen Self-Hosted",
"description": "Daha güvenilir ve düşük bakım gerektiren, ekstra özelliklere sahip kendi kendine barındırabileceğiniz Pangolin sunucusu",
@@ -2030,25 +1937,6 @@
"invalidValue": "Geçersiz değer",
"idpTypeLabel": "Kimlik Sağlayıcı Türü",
"roleMappingExpressionPlaceholder": "örn., contains(gruplar, 'yönetici') && 'Yönetici' || 'Üye'",
"roleMappingModeFixedRoles": "Fixed Roles",
"roleMappingModeMappingBuilder": "Mapping Builder",
"roleMappingModeRawExpression": "Raw Expression",
"roleMappingFixedRolesPlaceholderSelect": "Select one or more roles",
"roleMappingFixedRolesPlaceholderFreeform": "Type role names (exact match per organization)",
"roleMappingFixedRolesDescriptionSameForAll": "Assign the same role set to every auto-provisioned user.",
"roleMappingFixedRolesDescriptionDefaultPolicy": "For default policies, type role names that exist in each organization where users are provisioned. Names must match exactly.",
"roleMappingClaimPath": "Claim Path",
"roleMappingClaimPathPlaceholder": "groups",
"roleMappingClaimPathDescription": "Path in the token payload that contains source values (for example, groups).",
"roleMappingMatchValue": "Match Value",
"roleMappingAssignRoles": "Assign Roles",
"roleMappingAddMappingRule": "Add Mapping Rule",
"roleMappingRawExpressionResultDescription": "Expression must evaluate to a string or string array.",
"roleMappingRawExpressionResultDescriptionSingleRole": "Expression must evaluate to a string (a single role name).",
"roleMappingMatchValuePlaceholder": "Match value (for example: admin)",
"roleMappingAssignRolesPlaceholderFreeform": "Type role names (exact per org)",
"roleMappingBuilderFreeformRowHint": "Role names must match a role in each target organization.",
"roleMappingRemoveRule": "Remove",
"idpGoogleConfiguration": "Google Yapılandırması",
"idpGoogleConfigurationDescription": "Google OAuth2 kimlik bilgilerinizi yapılandırın",
"idpGoogleClientIdDescription": "Google OAuth2 İstemci Kimliğiniz",
@@ -2445,8 +2333,6 @@
"logRetentionAccessDescription": "Erişim günlüklerini ne kadar süre tutacağını belirle",
"logRetentionActionLabel": "Eylem Günlüğü Saklama",
"logRetentionActionDescription": "Eylem günlüklerini ne kadar süre tutacağını belirle",
"logRetentionConnectionLabel": "Connection Log Retention",
"logRetentionConnectionDescription": "How long to retain connection logs",
"logRetentionDisabled": "Devre Dışı",
"logRetention3Days": "3 gün",
"logRetention7Days": "7 gün",
@@ -2457,14 +2343,8 @@
"logRetentionEndOfFollowingYear": "Bir sonraki yılın sonu",
"actionLogsDescription": "Bu organizasyondaki eylemler geçmişini görüntüleyin",
"accessLogsDescription": "Bu organizasyondaki kaynaklar için erişim kimlik doğrulama isteklerini görüntüleyin",
"connectionLogs": "Connection Logs",
"connectionLogsDescription": "View connection logs for tunnels in this organization",
"sidebarLogsConnection": "Connection Logs",
"sourceAddress": "Source Address",
"destinationAddress": "Destination Address",
"duration": "Duration",
"licenseRequiredToUse": "An <enterpriseLicenseLink>Enterprise Edition</enterpriseLicenseLink> license or <pangolinCloudLink>Pangolin Cloud</pangolinCloudLink> is required to use this feature. <bookADemoLink>Book a free demo or POC trial to learn more</bookADemoLink>.",
"ossEnterpriseEditionRequired": "The <enterpriseEditionLink>Enterprise Edition</enterpriseEditionLink> is required to use this feature. This feature is also available in <pangolinCloudLink>Pangolin Cloud</pangolinCloudLink>. <bookADemoLink>Book a free demo or POC trial to learn more</bookADemoLink>.",
"licenseRequiredToUse": "Bu özelliği kullanmak için bir <enterpriseLicenseLink>Enterprise Edition</enterpriseLicenseLink> lisansı veya <pangolinCloudLink>Pangolin Cloud</pangolinCloudLink> gereklidir. <bookADemoLink>Tanıtım veya POC denemesi ayarlayın</bookADemoLink>.",
"ossEnterpriseEditionRequired": "Bu özelliği kullanmak için <enterpriseEditionLink>Enterprise Edition</enterpriseEditionLink> gereklidir. Bu özellik ayrıca <pangolinCloudLink>Pangolin Cloud</pangolinCloudLink>da da mevcuttur. <bookADemoLink>Tanıtım veya POC denemesi ayarlayın</bookADemoLink>.",
"certResolver": "Sertifika Çözücü",
"certResolverDescription": "Bu kaynak için kullanılacak sertifika çözücüsünü seçin.",
"selectCertResolver": "Sertifika Çözücü Seçin",
@@ -2802,6 +2682,5 @@
"approvalsEmptyStateStep2Description": "Bir rolü düzenleyin ve 'Cihaz Onaylarını Gerektir' seçeneğini etkinleştirin. Bu role sahip kullanıcıların yeni cihazlar için yönetici onayına ihtiyacı olacaktır.",
"approvalsEmptyStatePreviewDescription": "Önizleme: Etkinleştirildiğinde, bekleyen cihaz talepleri incelenmek üzere burada görünecektir.",
"approvalsEmptyStateButtonText": "Rolleri Yönet",
"domainErrorTitle": "Alan adınızı doğrulamada sorun yaşıyoruz",
"idpAdminAutoProvisionPoliciesTabHint": "Configure role mapping and organization policies on the <policiesTabLink>Auto Provision Settings</policiesTabLink> tab."
"domainErrorTitle": "Alan adınızı doğrulamada sorun yaşıyoruz"
}

View File

@@ -148,11 +148,6 @@
"createLink": "创建链接",
"resourcesNotFound": "找不到资源",
"resourceSearch": "搜索资源",
"machineSearch": "Search machines",
"machinesSearch": "Search machine clients...",
"machineNotFound": "No machines found",
"userDeviceSearch": "Search user devices",
"userDevicesSearch": "Search user devices...",
"openMenu": "打开菜单",
"resource": "资源",
"title": "标题",
@@ -328,54 +323,6 @@
"apiKeysDelete": "删除 API 密钥",
"apiKeysManage": "管理 API 密钥",
"apiKeysDescription": "API 密钥用于认证集成 API",
"provisioningKeysTitle": "Provisioning Key",
"provisioningKeysManage": "Manage Provisioning Keys",
"provisioningKeysDescription": "Provisioning keys are used to authenticate automated site provisioning for your organization.",
"provisioningManage": "Provisioning",
"provisioningDescription": "Manage provisioning keys and review pending sites awaiting approval.",
"pendingSites": "Pending Sites",
"siteApproveSuccess": "Site approved successfully",
"siteApproveError": "Error approving site",
"provisioningKeys": "Provisioning Keys",
"searchProvisioningKeys": "Search provisioning keys...",
"provisioningKeysAdd": "Generate Provisioning Key",
"provisioningKeysErrorDelete": "Error deleting provisioning key",
"provisioningKeysErrorDeleteMessage": "Error deleting provisioning key",
"provisioningKeysQuestionRemove": "Are you sure you want to remove this provisioning key from the organization?",
"provisioningKeysMessageRemove": "Once removed, the key can no longer be used for site provisioning.",
"provisioningKeysDeleteConfirm": "Confirm Delete Provisioning Key",
"provisioningKeysDelete": "Delete Provisioning key",
"provisioningKeysCreate": "Generate Provisioning Key",
"provisioningKeysCreateDescription": "Generate a new provisioning key for the organization",
"provisioningKeysSeeAll": "See all provisioning keys",
"provisioningKeysSave": "Save the provisioning key",
"provisioningKeysSaveDescription": "You will only be able to see this once. Copy it to a secure place.",
"provisioningKeysErrorCreate": "Error creating provisioning key",
"provisioningKeysList": "New provisioning key",
"provisioningKeysMaxBatchSize": "Max batch size",
"provisioningKeysUnlimitedBatchSize": "Unlimited batch size (no limit)",
"provisioningKeysMaxBatchUnlimited": "Unlimited",
"provisioningKeysMaxBatchSizeInvalid": "Enter a valid max batch size (11,000,000).",
"provisioningKeysValidUntil": "Valid until",
"provisioningKeysValidUntilHint": "Leave empty for no expiration.",
"provisioningKeysValidUntilInvalid": "Enter a valid date and time.",
"provisioningKeysNumUsed": "Times used",
"provisioningKeysLastUsed": "Last used",
"provisioningKeysNoExpiry": "No expiration",
"provisioningKeysNeverUsed": "Never",
"provisioningKeysEdit": "Edit Provisioning Key",
"provisioningKeysEditDescription": "Update the max batch size and expiration time for this key.",
"provisioningKeysApproveNewSites": "Approve new sites",
"provisioningKeysApproveNewSitesDescription": "Automatically approve sites that register with this key.",
"provisioningKeysUpdateError": "Error updating provisioning key",
"provisioningKeysUpdated": "Provisioning key updated",
"provisioningKeysUpdatedDescription": "Your changes have been saved.",
"provisioningKeysBannerTitle": "Site Provisioning Keys",
"provisioningKeysBannerDescription": "Generate a provisioning key and use it with the Newt connector to automatically create sites on first startup — no need to set up separate credentials for each site.",
"provisioningKeysBannerButtonText": "Learn More",
"pendingSitesBannerTitle": "Pending Sites",
"pendingSitesBannerDescription": "Sites that connect using a provisioning key appear here for review. Approve each site before it becomes active and gains access to your resources.",
"pendingSitesBannerButtonText": "Learn More",
"apiKeysSettings": "{apiKeyName} 设置",
"userTitle": "管理所有用户",
"userDescription": "查看和管理系统中的所有用户",
@@ -562,12 +509,9 @@
"userSaved": "用户已保存",
"userSavedDescription": "用户已更新。",
"autoProvisioned": "自动设置",
"autoProvisionSettings": "Auto Provision Settings",
"autoProvisionedDescription": "允许此用户由身份提供商自动管理",
"accessControlsDescription": "管理此用户在组织中可以访问和做什么",
"accessControlsSubmit": "保存访问控制",
"singleRolePerUserPlanNotice": "Your plan only supports one role per user.",
"singleRolePerUserEditionNotice": "This edition only supports one role per user.",
"roles": "角色",
"accessUsersRoles": "管理用户和角色",
"accessUsersRolesDescription": "邀请用户加入角色来管理访问组织",
@@ -943,7 +887,7 @@
"defaultMappingsRole": "默认角色映射",
"defaultMappingsRoleDescription": "此表达式的结果必须返回组织中定义的角色名称作为字符串。",
"defaultMappingsOrg": "默认组织映射",
"defaultMappingsOrgDescription": "When set, this expression must return the organization ID or true for the user to access that organization. When unset, defining an organization policy for that org is enough: the user is allowed in as long as a valid role mapping can be resolved for them within the organization.",
"defaultMappingsOrgDescription": "此表达式必须返回 组织ID 或 true 才能允许用户访问组织。",
"defaultMappingsSubmit": "保存默认映射",
"orgPoliciesEdit": "编辑组织策略",
"org": "组织",
@@ -1175,7 +1119,6 @@
"setupTokenDescription": "从服务器控制台输入设置令牌。",
"setupTokenRequired": "需要设置令牌",
"actionUpdateSite": "更新站点",
"actionResetSiteBandwidth": "Reset Organization Bandwidth",
"actionListSiteRoles": "允许站点角色列表",
"actionCreateResource": "创建资源",
"actionDeleteResource": "删除资源",
@@ -1205,7 +1148,6 @@
"actionRemoveUser": "删除用户",
"actionListUsers": "列出用户",
"actionAddUserRole": "添加用户角色",
"actionSetUserOrgRoles": "Set User Roles",
"actionGenerateAccessToken": "生成访问令牌",
"actionDeleteAccessToken": "删除访问令牌",
"actionListAccessTokens": "访问令牌",
@@ -1322,7 +1264,6 @@
"sidebarRoles": "角色",
"sidebarShareableLinks": "链接",
"sidebarApiKeys": "API密钥",
"sidebarProvisioning": "Provisioning",
"sidebarSettings": "设置",
"sidebarAllUsers": "所有用户",
"sidebarIdentityProviders": "身份提供商",
@@ -1948,40 +1889,6 @@
"exitNode": "出口节点",
"country": "国家",
"rulesMatchCountry": "当前基于源 IP",
"region": "Region",
"selectRegion": "Select region",
"searchRegions": "Search regions...",
"noRegionFound": "No region found.",
"rulesMatchRegion": "Select a regional grouping of countries",
"rulesErrorInvalidRegion": "Invalid region",
"rulesErrorInvalidRegionDescription": "Please select a valid region.",
"regionAfrica": "Africa",
"regionNorthernAfrica": "Northern Africa",
"regionEasternAfrica": "Eastern Africa",
"regionMiddleAfrica": "Middle Africa",
"regionSouthernAfrica": "Southern Africa",
"regionWesternAfrica": "Western Africa",
"regionAmericas": "Americas",
"regionCaribbean": "Caribbean",
"regionCentralAmerica": "Central America",
"regionSouthAmerica": "South America",
"regionNorthernAmerica": "Northern America",
"regionAsia": "Asia",
"regionCentralAsia": "Central Asia",
"regionEasternAsia": "Eastern Asia",
"regionSouthEasternAsia": "South-Eastern Asia",
"regionSouthernAsia": "Southern Asia",
"regionWesternAsia": "Western Asia",
"regionEurope": "Europe",
"regionEasternEurope": "Eastern Europe",
"regionNorthernEurope": "Northern Europe",
"regionSouthernEurope": "Southern Europe",
"regionWesternEurope": "Western Europe",
"regionOceania": "Oceania",
"regionAustraliaAndNewZealand": "Australia and New Zealand",
"regionMelanesia": "Melanesia",
"regionMicronesia": "Micronesia",
"regionPolynesia": "Polynesia",
"managedSelfHosted": {
"title": "托管自托管",
"description": "更可靠和低维护自我托管的 Pangolin 服务器,带有额外的铃声和告密器",
@@ -2030,25 +1937,6 @@
"invalidValue": "无效的值",
"idpTypeLabel": "身份提供者类型",
"roleMappingExpressionPlaceholder": "例如: contains(group, 'admin' &'Admin' || 'Member'",
"roleMappingModeFixedRoles": "Fixed Roles",
"roleMappingModeMappingBuilder": "Mapping Builder",
"roleMappingModeRawExpression": "Raw Expression",
"roleMappingFixedRolesPlaceholderSelect": "Select one or more roles",
"roleMappingFixedRolesPlaceholderFreeform": "Type role names (exact match per organization)",
"roleMappingFixedRolesDescriptionSameForAll": "Assign the same role set to every auto-provisioned user.",
"roleMappingFixedRolesDescriptionDefaultPolicy": "For default policies, type role names that exist in each organization where users are provisioned. Names must match exactly.",
"roleMappingClaimPath": "Claim Path",
"roleMappingClaimPathPlaceholder": "groups",
"roleMappingClaimPathDescription": "Path in the token payload that contains source values (for example, groups).",
"roleMappingMatchValue": "Match Value",
"roleMappingAssignRoles": "Assign Roles",
"roleMappingAddMappingRule": "Add Mapping Rule",
"roleMappingRawExpressionResultDescription": "Expression must evaluate to a string or string array.",
"roleMappingRawExpressionResultDescriptionSingleRole": "Expression must evaluate to a string (a single role name).",
"roleMappingMatchValuePlaceholder": "Match value (for example: admin)",
"roleMappingAssignRolesPlaceholderFreeform": "Type role names (exact per org)",
"roleMappingBuilderFreeformRowHint": "Role names must match a role in each target organization.",
"roleMappingRemoveRule": "Remove",
"idpGoogleConfiguration": "Google 配置",
"idpGoogleConfigurationDescription": "配置 Google OAuth2 凭据",
"idpGoogleClientIdDescription": "Google OAuth2 Client ID",
@@ -2445,8 +2333,6 @@
"logRetentionAccessDescription": "保留访问日志的时间",
"logRetentionActionLabel": "动作日志保留",
"logRetentionActionDescription": "保留操作日志的时间",
"logRetentionConnectionLabel": "Connection Log Retention",
"logRetentionConnectionDescription": "How long to retain connection logs",
"logRetentionDisabled": "已禁用",
"logRetention3Days": "3 天",
"logRetention7Days": "7 天",
@@ -2457,14 +2343,8 @@
"logRetentionEndOfFollowingYear": "下一年结束",
"actionLogsDescription": "查看此机构执行的操作历史",
"accessLogsDescription": "查看此机构资源的访问认证请求",
"connectionLogs": "Connection Logs",
"connectionLogsDescription": "View connection logs for tunnels in this organization",
"sidebarLogsConnection": "Connection Logs",
"sourceAddress": "Source Address",
"destinationAddress": "Destination Address",
"duration": "Duration",
"licenseRequiredToUse": "An <enterpriseLicenseLink>Enterprise Edition</enterpriseLicenseLink> license or <pangolinCloudLink>Pangolin Cloud</pangolinCloudLink> is required to use this feature. <bookADemoLink>Book a free demo or POC trial to learn more</bookADemoLink>.",
"ossEnterpriseEditionRequired": "The <enterpriseEditionLink>Enterprise Edition</enterpriseEditionLink> is required to use this feature. This feature is also available in <pangolinCloudLink>Pangolin Cloud</pangolinCloudLink>. <bookADemoLink>Book a free demo or POC trial to learn more</bookADemoLink>.",
"licenseRequiredToUse": "使用此功能需要<enterpriseLicenseLink>企业版</enterpriseLicenseLink>许可证或<pangolinCloudLink>Pangolin Cloud</pangolinCloudLink>。<bookADemoLink>预约演示或POC试用</bookADemoLink>。",
"ossEnterpriseEditionRequired": "需要 <enterpriseEditionLink>Enterprise Edition</enterpriseEditionLink> 才能使用此功能。 此功能也可在 <pangolinCloudLink>Pangolin Cloud</pangolinCloudLink>上获取。 <bookADemoLink>预订演示或POC 试用</bookADemoLink>。",
"certResolver": "证书解决器",
"certResolverDescription": "选择用于此资源的证书解析器。",
"selectCertResolver": "选择证书解析",
@@ -2802,6 +2682,5 @@
"approvalsEmptyStateStep2Description": "编辑角色并启用“需要设备审批”选项。具有此角色的用户需要管理员批准新设备。",
"approvalsEmptyStatePreviewDescription": "预览:如果启用,待处理设备请求将出现在这里供审核",
"approvalsEmptyStateButtonText": "管理角色",
"domainErrorTitle": "我们在验证您的域名时遇到了问题",
"idpAdminAutoProvisionPoliciesTabHint": "Configure role mapping and organization policies on the <policiesTabLink>Auto Provision Settings</policiesTabLink> tab."
"domainErrorTitle": "我们在验证您的域名时遇到了问题"
}

View File

@@ -1091,7 +1091,6 @@
"actionRemoveUser": "刪除用戶",
"actionListUsers": "列出用戶",
"actionAddUserRole": "添加用戶角色",
"actionSetUserOrgRoles": "Set User Roles",
"actionGenerateAccessToken": "生成訪問令牌",
"actionDeleteAccessToken": "刪除訪問令牌",
"actionListAccessTokens": "訪問令牌",

View File

@@ -1,10 +1,9 @@
import { Request } from "express";
import { db } from "@server/db";
import { userActions, roleActions } from "@server/db";
import { and, eq, inArray } from "drizzle-orm";
import { userActions, roleActions, userOrgs } from "@server/db";
import { and, eq } from "drizzle-orm";
import createHttpError from "http-errors";
import HttpCode from "@server/types/HttpCode";
import { getUserOrgRoleIds } from "@server/lib/userOrgRoles";
export enum ActionsEnum {
createOrgUser = "createOrgUser",
@@ -54,8 +53,6 @@ export enum ActionsEnum {
listRoleResources = "listRoleResources",
// listRoleActions = "listRoleActions",
addUserRole = "addUserRole",
removeUserRole = "removeUserRole",
setUserOrgRoles = "setUserOrgRoles",
// addUserSite = "addUserSite",
// addUserAction = "addUserAction",
// removeUserAction = "removeUserAction",
@@ -112,10 +109,6 @@ export enum ActionsEnum {
listApiKeyActions = "listApiKeyActions",
listApiKeys = "listApiKeys",
getApiKey = "getApiKey",
createSiteProvisioningKey = "createSiteProvisioningKey",
listSiteProvisioningKeys = "listSiteProvisioningKeys",
updateSiteProvisioningKey = "updateSiteProvisioningKey",
deleteSiteProvisioningKey = "deleteSiteProvisioningKey",
getCertificate = "getCertificate",
restartCertificate = "restartCertificate",
billing = "billing",
@@ -161,16 +154,29 @@ export async function checkUserActionPermission(
}
try {
let userOrgRoleIds = req.userOrgRoleIds;
let userOrgRoleId = req.userOrgRoleId;
if (userOrgRoleIds === undefined) {
userOrgRoleIds = await getUserOrgRoleIds(userId, req.userOrgId!);
if (userOrgRoleIds.length === 0) {
// If userOrgRoleId is not available on the request, fetch it
if (userOrgRoleId === undefined) {
const userOrgRole = await db
.select()
.from(userOrgs)
.where(
and(
eq(userOrgs.userId, userId),
eq(userOrgs.orgId, req.userOrgId!)
)
)
.limit(1);
if (userOrgRole.length === 0) {
throw createHttpError(
HttpCode.FORBIDDEN,
"User does not have access to this organization"
);
}
userOrgRoleId = userOrgRole[0].roleId;
}
// Check if the user has direct permission for the action in the current org
@@ -181,7 +187,7 @@ export async function checkUserActionPermission(
and(
eq(userActions.userId, userId),
eq(userActions.actionId, actionId),
eq(userActions.orgId, req.userOrgId!)
eq(userActions.orgId, req.userOrgId!) // TODO: we cant pass the org id if we are not checking the org
)
)
.limit(1);
@@ -190,14 +196,14 @@ export async function checkUserActionPermission(
return true;
}
// If no direct permission, check role-based permission (any of user's roles)
// If no direct permission, check role-based permission
const roleActionPermission = await db
.select()
.from(roleActions)
.where(
and(
eq(roleActions.actionId, actionId),
inArray(roleActions.roleId, userOrgRoleIds),
eq(roleActions.roleId, userOrgRoleId!),
eq(roleActions.orgId, req.userOrgId!)
)
)

View File

@@ -1,29 +1,26 @@
import { db } from "@server/db";
import { and, eq, inArray } from "drizzle-orm";
import { and, eq } from "drizzle-orm";
import { roleResources, userResources } from "@server/db";
export async function canUserAccessResource({
userId,
resourceId,
roleIds
roleId
}: {
userId: string;
resourceId: number;
roleIds: number[];
roleId: number;
}): Promise<boolean> {
const roleResourceAccess =
roleIds.length > 0
? await db
.select()
.from(roleResources)
.where(
and(
eq(roleResources.resourceId, resourceId),
inArray(roleResources.roleId, roleIds)
)
)
.limit(1)
: [];
const roleResourceAccess = await db
.select()
.from(roleResources)
.where(
and(
eq(roleResources.resourceId, resourceId),
eq(roleResources.roleId, roleId)
)
)
.limit(1);
if (roleResourceAccess.length > 0) {
return true;

View File

@@ -1,29 +1,26 @@
import { db } from "@server/db";
import { and, eq, inArray } from "drizzle-orm";
import { and, eq } from "drizzle-orm";
import { roleSiteResources, userSiteResources } from "@server/db";
export async function canUserAccessSiteResource({
userId,
resourceId,
roleIds
roleId
}: {
userId: string;
resourceId: number;
roleIds: number[];
roleId: number;
}): Promise<boolean> {
const roleResourceAccess =
roleIds.length > 0
? await db
.select()
.from(roleSiteResources)
.where(
and(
eq(roleSiteResources.siteResourceId, resourceId),
inArray(roleSiteResources.roleId, roleIds)
)
)
.limit(1)
: [];
const roleResourceAccess = await db
.select()
.from(roleSiteResources)
.where(
and(
eq(roleSiteResources.siteResourceId, resourceId),
eq(roleSiteResources.roleId, roleId)
)
)
.limit(1);
if (roleResourceAccess.length > 0) {
return true;

View File

@@ -1,5 +1,4 @@
import { flushBandwidthToDb } from "@server/routers/newt/handleReceiveBandwidthMessage";
import { flushConnectionLogToDb } from "#dynamic/routers/newt";
import { flushSiteBandwidthToDb } from "@server/routers/gerbil/receiveBandwidth";
import { stopPingAccumulator } from "@server/routers/newt/pingAccumulator";
import { cleanup as wsCleanup } from "#dynamic/routers/ws";
@@ -7,7 +6,6 @@ import { cleanup as wsCleanup } from "#dynamic/routers/ws";
async function cleanup() {
await stopPingAccumulator();
await flushBandwidthToDb();
await flushConnectionLogToDb();
await flushSiteBandwidthToDb();
await wsCleanup();
@@ -18,4 +16,4 @@ export async function initCleanup() {
// Handle process termination
process.on("SIGTERM", () => cleanup());
process.on("SIGINT", () => cleanup());
}
}

View File

@@ -7,8 +7,7 @@ import {
bigint,
real,
text,
index,
primaryKey
index
} from "drizzle-orm/pg-core";
import { InferSelectModel } from "drizzle-orm";
import {
@@ -18,9 +17,7 @@ import {
users,
exitNodes,
sessions,
clients,
siteResources,
sites
clients
} from "./schema";
export const certificates = pgTable("certificates", {
@@ -92,9 +89,7 @@ export const subscriptions = pgTable("subscriptions", {
export const subscriptionItems = pgTable("subscriptionItems", {
subscriptionItemId: serial("subscriptionItemId").primaryKey(),
stripeSubscriptionItemId: varchar("stripeSubscriptionItemId", {
length: 255
}),
stripeSubscriptionItemId: varchar("stripeSubscriptionItemId", { length: 255 }),
subscriptionId: varchar("subscriptionId", { length: 255 })
.notNull()
.references(() => subscriptions.subscriptionId, {
@@ -291,7 +286,6 @@ export const accessAuditLog = pgTable(
actor: varchar("actor", { length: 255 }),
actorId: varchar("actorId", { length: 255 }),
resourceId: integer("resourceId"),
siteResourceId: integer("siteResourceId"),
ip: varchar("ip", { length: 45 }),
type: varchar("type", { length: 100 }).notNull(),
action: boolean("action").notNull(),
@@ -308,45 +302,6 @@ export const accessAuditLog = pgTable(
]
);
export const connectionAuditLog = pgTable(
"connectionAuditLog",
{
id: serial("id").primaryKey(),
sessionId: text("sessionId").notNull(),
siteResourceId: integer("siteResourceId").references(
() => siteResources.siteResourceId,
{ onDelete: "cascade" }
),
orgId: text("orgId").references(() => orgs.orgId, {
onDelete: "cascade"
}),
siteId: integer("siteId").references(() => sites.siteId, {
onDelete: "cascade"
}),
clientId: integer("clientId").references(() => clients.clientId, {
onDelete: "cascade"
}),
userId: text("userId").references(() => users.userId, {
onDelete: "cascade"
}),
sourceAddr: text("sourceAddr").notNull(),
destAddr: text("destAddr").notNull(),
protocol: text("protocol").notNull(),
startedAt: integer("startedAt").notNull(),
endedAt: integer("endedAt"),
bytesTx: integer("bytesTx"),
bytesRx: integer("bytesRx")
},
(table) => [
index("idx_accessAuditLog_startedAt").on(table.startedAt),
index("idx_accessAuditLog_org_startedAt").on(
table.orgId,
table.startedAt
),
index("idx_accessAuditLog_siteResourceId").on(table.siteResourceId)
]
);
export const approvals = pgTable("approvals", {
approvalId: serial("approvalId").primaryKey(),
timestamp: integer("timestamp").notNull(), // this is EPOCH time in seconds
@@ -374,48 +329,13 @@ export const approvals = pgTable("approvals", {
});
export const bannedEmails = pgTable("bannedEmails", {
email: varchar("email", { length: 255 }).primaryKey()
email: varchar("email", { length: 255 }).primaryKey(),
});
export const bannedIps = pgTable("bannedIps", {
ip: varchar("ip", { length: 255 }).primaryKey()
ip: varchar("ip", { length: 255 }).primaryKey(),
});
export const siteProvisioningKeys = pgTable("siteProvisioningKeys", {
siteProvisioningKeyId: varchar("siteProvisioningKeyId", {
length: 255
}).primaryKey(),
name: varchar("name", { length: 255 }).notNull(),
siteProvisioningKeyHash: text("siteProvisioningKeyHash").notNull(),
lastChars: varchar("lastChars", { length: 4 }).notNull(),
createdAt: varchar("dateCreated", { length: 255 }).notNull(),
lastUsed: varchar("lastUsed", { length: 255 }),
maxBatchSize: integer("maxBatchSize"), // null = no limit
numUsed: integer("numUsed").notNull().default(0),
validUntil: varchar("validUntil", { length: 255 })
});
export const siteProvisioningKeyOrg = pgTable(
"siteProvisioningKeyOrg",
{
siteProvisioningKeyId: varchar("siteProvisioningKeyId", {
length: 255
})
.notNull()
.references(() => siteProvisioningKeys.siteProvisioningKeyId, {
onDelete: "cascade"
}),
orgId: varchar("orgId", { length: 255 })
.notNull()
.references(() => orgs.orgId, { onDelete: "cascade" })
},
(table) => [
primaryKey({
columns: [table.siteProvisioningKeyId, table.orgId]
})
]
);
export type Approval = InferSelectModel<typeof approvals>;
export type Limit = InferSelectModel<typeof limits>;
export type Account = InferSelectModel<typeof account>;
@@ -437,4 +357,3 @@ export type LoginPage = InferSelectModel<typeof loginPage>;
export type LoginPageBranding = InferSelectModel<typeof loginPageBranding>;
export type ActionAuditLog = InferSelectModel<typeof actionAuditLog>;
export type AccessAuditLog = InferSelectModel<typeof accessAuditLog>;
export type ConnectionAuditLog = InferSelectModel<typeof connectionAuditLog>;

View File

@@ -6,11 +6,9 @@ import {
index,
integer,
pgTable,
primaryKey,
real,
serial,
text,
unique,
varchar
} from "drizzle-orm/pg-core";
@@ -57,9 +55,6 @@ export const orgs = pgTable("orgs", {
settingsLogRetentionDaysAction: integer("settingsLogRetentionDaysAction") // where 0 = dont keep logs and -1 = keep forever and 9001 = end of the following year
.notNull()
.default(0),
settingsLogRetentionDaysConnection: integer("settingsLogRetentionDaysConnection") // where 0 = dont keep logs and -1 = keep forever and 9001 = end of the following year
.notNull()
.default(0),
sshCaPrivateKey: text("sshCaPrivateKey"), // Encrypted SSH CA private key (PEM format)
sshCaPublicKey: text("sshCaPublicKey"), // SSH CA public key (OpenSSH format)
isBillingOrg: boolean("isBillingOrg"),
@@ -340,6 +335,9 @@ export const userOrgs = pgTable("userOrgs", {
onDelete: "cascade"
})
.notNull(),
roleId: integer("roleId")
.notNull()
.references(() => roles.roleId),
isOwner: boolean("isOwner").notNull().default(false),
autoProvisioned: boolean("autoProvisioned").default(false),
pamUsername: varchar("pamUsername") // cleaned username for ssh and such
@@ -388,22 +386,6 @@ export const roles = pgTable("roles", {
sshUnixGroups: text("sshUnixGroups").default("[]")
});
export const userOrgRoles = pgTable(
"userOrgRoles",
{
userId: varchar("userId")
.notNull()
.references(() => users.userId, { onDelete: "cascade" }),
orgId: varchar("orgId")
.notNull()
.references(() => orgs.orgId, { onDelete: "cascade" }),
roleId: integer("roleId")
.notNull()
.references(() => roles.roleId, { onDelete: "cascade" })
},
(t) => [unique().on(t.userId, t.orgId, t.roleId)]
);
export const roleActions = pgTable("roleActions", {
roleId: integer("roleId")
.notNull()
@@ -471,22 +453,12 @@ export const userInvites = pgTable("userInvites", {
.references(() => orgs.orgId, { onDelete: "cascade" }),
email: varchar("email").notNull(),
expiresAt: bigint("expiresAt", { mode: "number" }).notNull(),
tokenHash: varchar("token").notNull()
tokenHash: varchar("token").notNull(),
roleId: integer("roleId")
.notNull()
.references(() => roles.roleId, { onDelete: "cascade" })
});
export const userInviteRoles = pgTable(
"userInviteRoles",
{
inviteId: varchar("inviteId")
.notNull()
.references(() => userInvites.inviteId, { onDelete: "cascade" }),
roleId: integer("roleId")
.notNull()
.references(() => roles.roleId, { onDelete: "cascade" })
},
(t) => [primaryKey({ columns: [t.inviteId, t.roleId] })]
);
export const resourcePincode = pgTable("resourcePincode", {
pincodeId: serial("pincodeId").primaryKey(),
resourceId: integer("resourceId")
@@ -1062,9 +1034,7 @@ export type UserSite = InferSelectModel<typeof userSites>;
export type RoleResource = InferSelectModel<typeof roleResources>;
export type UserResource = InferSelectModel<typeof userResources>;
export type UserInvite = InferSelectModel<typeof userInvites>;
export type UserInviteRole = InferSelectModel<typeof userInviteRoles>;
export type UserOrg = InferSelectModel<typeof userOrgs>;
export type UserOrgRole = InferSelectModel<typeof userOrgRoles>;
export type ResourceSession = InferSelectModel<typeof resourceSessions>;
export type ResourcePincode = InferSelectModel<typeof resourcePincode>;
export type ResourcePassword = InferSelectModel<typeof resourcePassword>;

View File

@@ -1,12 +1,4 @@
import {
db,
loginPage,
LoginPage,
loginPageOrg,
Org,
orgs,
roles
} from "@server/db";
import { db, loginPage, LoginPage, loginPageOrg, Org, orgs, roles } from "@server/db";
import {
Resource,
ResourcePassword,
@@ -20,12 +12,13 @@ import {
resources,
roleResources,
sessions,
userOrgs,
userResources,
users,
ResourceHeaderAuthExtendedCompatibility,
resourceHeaderAuthExtendedCompatibility
} from "@server/db";
import { and, eq, inArray } from "drizzle-orm";
import { and, eq } from "drizzle-orm";
export type ResourceWithAuth = {
resource: Resource | null;
@@ -111,15 +104,24 @@ export async function getUserSessionWithUser(
}
/**
* Get role name by role ID (for display).
* Get user organization role
*/
export async function getRoleName(roleId: number): Promise<string | null> {
const [row] = await db
.select({ name: roles.name })
.from(roles)
.where(eq(roles.roleId, roleId))
export async function getUserOrgRole(userId: string, orgId: string) {
const userOrgRole = await db
.select({
userId: userOrgs.userId,
orgId: userOrgs.orgId,
roleId: userOrgs.roleId,
isOwner: userOrgs.isOwner,
autoProvisioned: userOrgs.autoProvisioned,
roleName: roles.name
})
.from(userOrgs)
.where(and(eq(userOrgs.userId, userId), eq(userOrgs.orgId, orgId)))
.leftJoin(roles, eq(userOrgs.roleId, roles.roleId))
.limit(1);
return row?.name ?? null;
return userOrgRole.length > 0 ? userOrgRole[0] : null;
}
/**
@@ -127,7 +129,7 @@ export async function getRoleName(roleId: number): Promise<string | null> {
*/
export async function getRoleResourceAccess(
resourceId: number,
roleIds: number[]
roleId: number
) {
const roleResourceAccess = await db
.select()
@@ -135,11 +137,12 @@ export async function getRoleResourceAccess(
.where(
and(
eq(roleResources.resourceId, resourceId),
inArray(roleResources.roleId, roleIds)
eq(roleResources.roleId, roleId)
)
);
)
.limit(1);
return roleResourceAccess.length > 0 ? roleResourceAccess : null;
return roleResourceAccess.length > 0 ? roleResourceAccess[0] : null;
}
/**

View File

@@ -1,196 +0,0 @@
// Regions of the World
// as of 2025-10-25
//
// Adapted according to the United Nations Geoscheme
// see https://www.unicode.org/cldr/charts/48/supplemental/territory_containment_un_m_49.html
// see https://unstats.un.org/unsd/methodology/m49
export const REGIONS = [
{
name: "regionAfrica",
id: "002",
includes: [
{
name: "regionNorthernAfrica",
id: "015",
countries: ["DZ", "EG", "LY", "MA", "SD", "TN", "EH"]
},
{
name: "regionEasternAfrica",
id: "014",
countries: ["IO", "BI", "KM", "DJ", "ER", "ET", "TF", "KE", "MG", "MW", "MU", "YT", "MZ", "RE", "RW", "SC", "SO", "SS", "UG", "ZM", "ZW"]
},
{
name: "regionMiddleAfrica",
id: "017",
countries: ["AO", "CM", "CF", "TD", "CG", "CD", "GQ", "GA", "ST"]
},
{
name: "regionSouthernAfrica",
id: "018",
countries: ["BW", "SZ", "LS", "NA", "ZA"]
},
{
name: "regionWesternAfrica",
id: "011",
countries: ["BJ", "BF", "CV", "CI", "GM", "GH", "GN", "GW", "LR", "ML", "MR", "NE", "NG", "SH", "SN", "SL", "TG"]
}
]
},
{
name: "regionAmericas",
id: "019",
includes: [
{
name: "regionCaribbean",
id: "029",
countries: ["AI", "AG", "AW", "BS", "BB", "BQ", "VG", "KY", "CU", "CW", "DM", "DO", "GD", "GP", "HT", "JM", "MQ", "MS", "PR", "BL", "KN", "LC", "MF", "VC", "SX", "TT", "TC", "VI"]
},
{
name: "regionCentralAmerica",
id: "013",
countries: ["BZ", "CR", "SV", "GT", "HN", "MX", "NI", "PA"]
},
{
name: "regionSouthAmerica",
id: "005",
countries: ["AR", "BO", "BV", "BR", "CL", "CO", "EC", "FK", "GF", "GY", "PY", "PE", "GS", "SR", "UY", "VE"]
},
{
name: "regionNorthernAmerica",
id: "021",
countries: ["BM", "CA", "GL", "PM", "US"]
}
]
},
{
name: "regionAsia",
id: "142",
includes: [
{
name: "regionCentralAsia",
id: "143",
countries: ["KZ", "KG", "TJ", "TM", "UZ"]
},
{
name: "regionEasternAsia",
id: "030",
countries: ["CN", "HK", "MO", "KP", "JP", "MN", "KR"]
},
{
name: "regionSouthEasternAsia",
id: "035",
countries: ["BN", "KH", "ID", "LA", "MY", "MM", "PH", "SG", "TH", "TL", "VN"]
},
{
name: "regionSouthernAsia",
id: "034",
countries: ["AF", "BD", "BT", "IN", "IR", "MV", "NP", "PK", "LK"]
},
{
name: "regionWesternAsia",
id: "145",
countries: ["AM", "AZ", "BH", "CY", "GE", "IQ", "IL", "JO", "KW", "LB", "OM", "QA", "SA", "PS", "SY", "TR", "AE", "YE"]
}
]
},
{
name: "regionEurope",
id: "150",
includes: [
{
name: "regionEasternEurope",
id: "151",
countries: ["BY", "BG", "CZ", "HU", "PL", "MD", "RO", "RU", "SK", "UA"]
},
{
name: "regionNorthernEurope",
id: "154",
countries: ["AX", "DK", "EE", "FO", "FI", "GG", "IS", "IE", "IM", "JE", "LV", "LT", "NO", "SJ", "SE", "GB"]
},
{
name: "regionSouthernEurope",
id: "039",
countries: ["AL", "AD", "BA", "HR", "GI", "GR", "VA", "IT", "MT", "ME", "MK", "PT", "SM", "RS", "SI", "ES"]
},
{
name: "regionWesternEurope",
id: "155",
countries: ["AT", "BE", "FR", "DE", "LI", "LU", "MC", "NL", "CH"]
}
]
},
{
name: "regionOceania",
id: "009",
includes: [
{
name: "regionAustraliaAndNewZealand",
id: "053",
countries: ["AU", "CX", "CC", "HM", "NZ", "NF"]
},
{
name: "regionMelanesia",
id: "054",
countries: ["FJ", "NC", "PG", "SB", "VU"]
},
{
name: "regionMicronesia",
id: "057",
countries: ["GU", "KI", "MH", "FM", "NR", "MP", "PW", "UM"]
},
{
name: "regionPolynesia",
id: "061",
countries: ["AS", "CK", "PF", "NU", "PN", "WS", "TK", "TO", "TV", "WF"]
}
]
}
];
type Subregion = {
name: string;
id: string;
countries: string[];
};
type Region = {
name: string;
id: string;
includes: Subregion[];
};
export function getRegionNameById(regionId: string): string | undefined {
// Check top-level regions
const region = REGIONS.find((r) => r.id === regionId);
if (region) {
return region.name;
}
// Check subregions
for (const region of REGIONS) {
for (const subregion of region.includes) {
if (subregion.id === regionId) {
return subregion.name;
}
}
}
return undefined;
}
export function isValidRegionId(regionId: string): boolean {
// Check top-level regions
if (REGIONS.find((r) => r.id === regionId)) {
return true;
}
// Check subregions
for (const region of REGIONS) {
if (region.includes.find((s) => s.id === regionId)) {
return true;
}
}
return false;
}

View File

@@ -2,12 +2,11 @@ import { InferSelectModel } from "drizzle-orm";
import {
index,
integer,
primaryKey,
real,
sqliteTable,
text
} from "drizzle-orm/sqlite-core";
import { clients, domains, exitNodes, orgs, sessions, siteResources, sites, users } from "./schema";
import { clients, domains, exitNodes, orgs, sessions, users } from "./schema";
export const certificates = sqliteTable("certificates", {
certId: integer("certId").primaryKey({ autoIncrement: true }),
@@ -279,7 +278,6 @@ export const accessAuditLog = sqliteTable(
actor: text("actor"),
actorId: text("actorId"),
resourceId: integer("resourceId"),
siteResourceId: integer("siteResourceId"),
ip: text("ip"),
location: text("location"),
type: text("type").notNull(),
@@ -296,45 +294,6 @@ export const accessAuditLog = sqliteTable(
]
);
export const connectionAuditLog = sqliteTable(
"connectionAuditLog",
{
id: integer("id").primaryKey({ autoIncrement: true }),
sessionId: text("sessionId").notNull(),
siteResourceId: integer("siteResourceId").references(
() => siteResources.siteResourceId,
{ onDelete: "cascade" }
),
orgId: text("orgId").references(() => orgs.orgId, {
onDelete: "cascade"
}),
siteId: integer("siteId").references(() => sites.siteId, {
onDelete: "cascade"
}),
clientId: integer("clientId").references(() => clients.clientId, {
onDelete: "cascade"
}),
userId: text("userId").references(() => users.userId, {
onDelete: "cascade"
}),
sourceAddr: text("sourceAddr").notNull(),
destAddr: text("destAddr").notNull(),
protocol: text("protocol").notNull(),
startedAt: integer("startedAt").notNull(),
endedAt: integer("endedAt"),
bytesTx: integer("bytesTx"),
bytesRx: integer("bytesRx")
},
(table) => [
index("idx_accessAuditLog_startedAt").on(table.startedAt),
index("idx_accessAuditLog_org_startedAt").on(
table.orgId,
table.startedAt
),
index("idx_accessAuditLog_siteResourceId").on(table.siteResourceId)
]
);
export const approvals = sqliteTable("approvals", {
approvalId: integer("approvalId").primaryKey({ autoIncrement: true }),
timestamp: integer("timestamp").notNull(), // this is EPOCH time in seconds
@@ -359,6 +318,7 @@ export const approvals = sqliteTable("approvals", {
.notNull()
});
export const bannedEmails = sqliteTable("bannedEmails", {
email: text("email").primaryKey()
});
@@ -367,37 +327,6 @@ export const bannedIps = sqliteTable("bannedIps", {
ip: text("ip").primaryKey()
});
export const siteProvisioningKeys = sqliteTable("siteProvisioningKeys", {
siteProvisioningKeyId: text("siteProvisioningKeyId").primaryKey(),
name: text("name").notNull(),
siteProvisioningKeyHash: text("siteProvisioningKeyHash").notNull(),
lastChars: text("lastChars").notNull(),
createdAt: text("dateCreated").notNull(),
lastUsed: text("lastUsed"),
maxBatchSize: integer("maxBatchSize"), // null = no limit
numUsed: integer("numUsed").notNull().default(0),
validUntil: text("validUntil")
});
export const siteProvisioningKeyOrg = sqliteTable(
"siteProvisioningKeyOrg",
{
siteProvisioningKeyId: text("siteProvisioningKeyId")
.notNull()
.references(() => siteProvisioningKeys.siteProvisioningKeyId, {
onDelete: "cascade"
}),
orgId: text("orgId")
.notNull()
.references(() => orgs.orgId, { onDelete: "cascade" })
},
(table) => [
primaryKey({
columns: [table.siteProvisioningKeyId, table.orgId]
})
]
);
export type Approval = InferSelectModel<typeof approvals>;
export type Limit = InferSelectModel<typeof limits>;
export type Account = InferSelectModel<typeof account>;
@@ -419,4 +348,3 @@ export type LoginPage = InferSelectModel<typeof loginPage>;
export type LoginPageBranding = InferSelectModel<typeof loginPageBranding>;
export type ActionAuditLog = InferSelectModel<typeof actionAuditLog>;
export type AccessAuditLog = InferSelectModel<typeof accessAuditLog>;
export type ConnectionAuditLog = InferSelectModel<typeof connectionAuditLog>;

View File

@@ -1,13 +1,6 @@
import { randomUUID } from "crypto";
import { InferSelectModel } from "drizzle-orm";
import {
index,
integer,
primaryKey,
sqliteTable,
text,
unique
} from "drizzle-orm/sqlite-core";
import { index, integer, sqliteTable, text } from "drizzle-orm/sqlite-core";
export const domains = sqliteTable("domains", {
domainId: text("domainId").primaryKey(),
@@ -54,9 +47,6 @@ export const orgs = sqliteTable("orgs", {
settingsLogRetentionDaysAction: integer("settingsLogRetentionDaysAction") // where 0 = dont keep logs and -1 = keep forever and 9001 = end of the following year
.notNull()
.default(0),
settingsLogRetentionDaysConnection: integer("settingsLogRetentionDaysConnection") // where 0 = dont keep logs and -1 = keep forever and 9001 = end of the following year
.notNull()
.default(0),
sshCaPrivateKey: text("sshCaPrivateKey"), // Encrypted SSH CA private key (PEM format)
sshCaPublicKey: text("sshCaPublicKey"), // SSH CA public key (OpenSSH format)
isBillingOrg: integer("isBillingOrg", { mode: "boolean" }),
@@ -653,6 +643,9 @@ export const userOrgs = sqliteTable("userOrgs", {
onDelete: "cascade"
})
.notNull(),
roleId: integer("roleId")
.notNull()
.references(() => roles.roleId),
isOwner: integer("isOwner", { mode: "boolean" }).notNull().default(false),
autoProvisioned: integer("autoProvisioned", {
mode: "boolean"
@@ -707,22 +700,6 @@ export const roles = sqliteTable("roles", {
sshUnixGroups: text("sshUnixGroups").default("[]")
});
export const userOrgRoles = sqliteTable(
"userOrgRoles",
{
userId: text("userId")
.notNull()
.references(() => users.userId, { onDelete: "cascade" }),
orgId: text("orgId")
.notNull()
.references(() => orgs.orgId, { onDelete: "cascade" }),
roleId: integer("roleId")
.notNull()
.references(() => roles.roleId, { onDelete: "cascade" })
},
(t) => [unique().on(t.userId, t.orgId, t.roleId)]
);
export const roleActions = sqliteTable("roleActions", {
roleId: integer("roleId")
.notNull()
@@ -808,22 +785,12 @@ export const userInvites = sqliteTable("userInvites", {
.references(() => orgs.orgId, { onDelete: "cascade" }),
email: text("email").notNull(),
expiresAt: integer("expiresAt").notNull(),
tokenHash: text("token").notNull()
tokenHash: text("token").notNull(),
roleId: integer("roleId")
.notNull()
.references(() => roles.roleId, { onDelete: "cascade" })
});
export const userInviteRoles = sqliteTable(
"userInviteRoles",
{
inviteId: text("inviteId")
.notNull()
.references(() => userInvites.inviteId, { onDelete: "cascade" }),
roleId: integer("roleId")
.notNull()
.references(() => roles.roleId, { onDelete: "cascade" })
},
(t) => [primaryKey({ columns: [t.inviteId, t.roleId] })]
);
export const resourcePincode = sqliteTable("resourcePincode", {
pincodeId: integer("pincodeId").primaryKey({
autoIncrement: true
@@ -1166,9 +1133,7 @@ export type UserSite = InferSelectModel<typeof userSites>;
export type RoleResource = InferSelectModel<typeof roleResources>;
export type UserResource = InferSelectModel<typeof userResources>;
export type UserInvite = InferSelectModel<typeof userInvites>;
export type UserInviteRole = InferSelectModel<typeof userInviteRoles>;
export type UserOrg = InferSelectModel<typeof userOrgs>;
export type UserOrgRole = InferSelectModel<typeof userOrgRoles>;
export type ResourceSession = InferSelectModel<typeof resourceSessions>;
export type ResourcePincode = InferSelectModel<typeof resourcePincode>;
export type ResourcePassword = InferSelectModel<typeof resourcePassword>;

View File

@@ -74,7 +74,7 @@ declare global {
session: Session;
userOrg?: UserOrg;
apiKeyOrg?: ApiKeyOrg;
userOrgRoleIds?: number[];
userOrgRoleId?: number;
userOrgId?: string;
userOrgIds?: string[];
remoteExitNode?: RemoteExitNode;

View File

@@ -8,7 +8,6 @@ export enum TierFeature {
LogExport = "logExport",
AccessLogs = "accessLogs", // set the retention period to none on downgrade
ActionLogs = "actionLogs", // set the retention period to none on downgrade
ConnectionLogs = "connectionLogs",
RotateCredentials = "rotateCredentials",
MaintencePage = "maintencePage", // handle downgrade
DevicePosture = "devicePosture",
@@ -16,9 +15,7 @@ export enum TierFeature {
SessionDurationPolicies = "sessionDurationPolicies", // handle downgrade by setting to default duration
PasswordExpirationPolicies = "passwordExpirationPolicies", // handle downgrade by setting to default duration
AutoProvisioning = "autoProvisioning", // handle downgrade by disabling auto provisioning
SshPam = "sshPam",
FullRbac = "fullRbac",
SiteProvisioningKeys = "siteProvisioningKeys" // handle downgrade by revoking keys if needed
SshPam = "sshPam"
}
export const tierMatrix: Record<TierFeature, Tier[]> = {
@@ -29,7 +26,6 @@ export const tierMatrix: Record<TierFeature, Tier[]> = {
[TierFeature.LogExport]: ["tier3", "enterprise"],
[TierFeature.AccessLogs]: ["tier2", "tier3", "enterprise"],
[TierFeature.ActionLogs]: ["tier2", "tier3", "enterprise"],
[TierFeature.ConnectionLogs]: ["tier2", "tier3", "enterprise"],
[TierFeature.RotateCredentials]: ["tier1", "tier2", "tier3", "enterprise"],
[TierFeature.MaintencePage]: ["tier1", "tier2", "tier3", "enterprise"],
[TierFeature.DevicePosture]: ["tier2", "tier3", "enterprise"],
@@ -52,7 +48,5 @@ export const tierMatrix: Record<TierFeature, Tier[]> = {
"enterprise"
],
[TierFeature.AutoProvisioning]: ["tier1", "tier3", "enterprise"],
[TierFeature.SshPam]: ["tier1", "tier3", "enterprise"],
[TierFeature.FullRbac]: ["tier1", "tier2", "tier3", "enterprise"],
[TierFeature.SiteProvisioningKeys]: ["enterprise"]
[TierFeature.SshPam]: ["tier1", "tier3", "enterprise"]
};

View File

@@ -31,7 +31,6 @@ import { pickPort } from "@server/routers/target/helpers";
import { resourcePassword } from "@server/db";
import { hashPassword } from "@server/auth/password";
import { isValidCIDR, isValidIP, isValidUrlGlobPattern } from "../validators";
import { isValidRegionId } from "@server/db/regions";
import { isLicensedOrSubscribed } from "#dynamic/lib/isLicencedOrSubscribed";
import { tierMatrix } from "../billing/tierMatrix";
@@ -864,10 +863,6 @@ function validateRule(rule: any) {
if (!isValidUrlGlobPattern(rule.value)) {
throw new Error(`Invalid URL glob pattern: ${rule.value}`);
}
} else if (rule.match === "region") {
if (!isValidRegionId(rule.value)) {
throw new Error(`Invalid region ID provided: ${rule.value}`);
}
}
}

View File

@@ -1,7 +1,6 @@
import { z } from "zod";
import { portRangeStringSchema } from "@server/lib/ip";
import { MaintenanceSchema } from "#dynamic/lib/blueprints/MaintenanceSchema";
import { isValidRegionId } from "@server/db/regions";
export const SiteSchema = z.object({
name: z.string().min(1).max(100),
@@ -78,7 +77,7 @@ export const AuthSchema = z.object({
export const RuleSchema = z
.object({
action: z.enum(["allow", "deny", "pass"]),
match: z.enum(["cidr", "path", "ip", "country", "asn", "region"]),
match: z.enum(["cidr", "path", "ip", "country", "asn"]),
value: z.string(),
priority: z.int().optional()
})
@@ -138,19 +137,6 @@ export const RuleSchema = z
message:
"Value must be 'AS<number>' format or 'ALL' when match is 'asn'"
}
)
.refine(
(rule) => {
if (rule.match === "region") {
return isValidRegionId(rule.value);
}
return true;
},
{
path: ["value"],
message:
"Value must be a valid UN M.49 region or subregion ID when match is 'region'"
}
);
export const HeaderSchema = z.object({

View File

@@ -10,7 +10,6 @@ import {
roles,
Transaction,
userClients,
userOrgRoles,
userOrgs
} from "@server/db";
import { getUniqueClientName } from "@server/db/names";
@@ -40,36 +39,20 @@ export async function calculateUserClientsForOrgs(
return;
}
// Get all user orgs with all roles (for org list and role-based logic)
const userOrgRoleRows = await transaction
// Get all user orgs
const allUserOrgs = await transaction
.select()
.from(userOrgs)
.innerJoin(
userOrgRoles,
and(
eq(userOrgs.userId, userOrgRoles.userId),
eq(userOrgs.orgId, userOrgRoles.orgId)
)
)
.innerJoin(roles, eq(userOrgRoles.roleId, roles.roleId))
.innerJoin(roles, eq(roles.roleId, userOrgs.roleId))
.where(eq(userOrgs.userId, userId));
const userOrgIds = [...new Set(userOrgRoleRows.map((r) => r.userOrgs.orgId))];
const orgIdToRoleRows = new Map<
string,
(typeof userOrgRoleRows)[0][]
>();
for (const r of userOrgRoleRows) {
const list = orgIdToRoleRows.get(r.userOrgs.orgId) ?? [];
list.push(r);
orgIdToRoleRows.set(r.userOrgs.orgId, list);
}
const userOrgIds = allUserOrgs.map(({ userOrgs: uo }) => uo.orgId);
// For each OLM, ensure there's a client in each org the user is in
for (const olm of userOlms) {
for (const orgId of orgIdToRoleRows.keys()) {
const roleRowsForOrg = orgIdToRoleRows.get(orgId)!;
const userOrg = roleRowsForOrg[0].userOrgs;
for (const userRoleOrg of allUserOrgs) {
const { userOrgs: userOrg, roles: role } = userRoleOrg;
const orgId = userOrg.orgId;
const [org] = await transaction
.select()
@@ -213,7 +196,7 @@ export async function calculateUserClientsForOrgs(
const requireApproval =
build !== "oss" &&
isOrgLicensed &&
roleRowsForOrg.some((r) => r.roles.requireDeviceApproval);
role.requireDeviceApproval;
const newClientData: InferInsertModel<typeof clients> = {
userId,

View File

@@ -2,7 +2,6 @@ import { db, orgs } from "@server/db";
import { cleanUpOldLogs as cleanUpOldAccessLogs } from "#dynamic/lib/logAccessAudit";
import { cleanUpOldLogs as cleanUpOldActionLogs } from "#dynamic/middlewares/logActionAudit";
import { cleanUpOldLogs as cleanUpOldRequestLogs } from "@server/routers/badger/logRequestAudit";
import { cleanUpOldLogs as cleanUpOldConnectionLogs } from "#dynamic/routers/newt";
import { gt, or } from "drizzle-orm";
import { cleanUpOldFingerprintSnapshots } from "@server/routers/olm/fingerprintingUtils";
import { build } from "@server/build";
@@ -21,17 +20,14 @@ export function initLogCleanupInterval() {
settingsLogRetentionDaysAccess:
orgs.settingsLogRetentionDaysAccess,
settingsLogRetentionDaysRequest:
orgs.settingsLogRetentionDaysRequest,
settingsLogRetentionDaysConnection:
orgs.settingsLogRetentionDaysConnection
orgs.settingsLogRetentionDaysRequest
})
.from(orgs)
.where(
or(
gt(orgs.settingsLogRetentionDaysAction, 0),
gt(orgs.settingsLogRetentionDaysAccess, 0),
gt(orgs.settingsLogRetentionDaysRequest, 0),
gt(orgs.settingsLogRetentionDaysConnection, 0)
gt(orgs.settingsLogRetentionDaysRequest, 0)
)
);
@@ -41,8 +37,7 @@ export function initLogCleanupInterval() {
orgId,
settingsLogRetentionDaysAction,
settingsLogRetentionDaysAccess,
settingsLogRetentionDaysRequest,
settingsLogRetentionDaysConnection
settingsLogRetentionDaysRequest
} = org;
if (settingsLogRetentionDaysAction > 0) {
@@ -65,13 +60,6 @@ export function initLogCleanupInterval() {
settingsLogRetentionDaysRequest
);
}
if (settingsLogRetentionDaysConnection > 0) {
await cleanUpOldConnectionLogs(
orgId,
settingsLogRetentionDaysConnection
);
}
}
await cleanUpOldFingerprintSnapshots(365);

View File

@@ -571,133 +571,6 @@ export function generateSubnetProxyTargets(
return targets;
}
export type SubnetProxyTargetV2 = {
sourcePrefixes: string[]; // must be cidrs
destPrefix: string; // must be a cidr
disableIcmp?: boolean;
rewriteTo?: string; // must be a cidr
portRange?: {
min: number;
max: number;
protocol: "tcp" | "udp";
}[];
resourceId?: number;
};
export function generateSubnetProxyTargetV2(
siteResource: SiteResource,
clients: {
clientId: number;
pubKey: string | null;
subnet: string | null;
}[]
): SubnetProxyTargetV2 | undefined {
if (clients.length === 0) {
logger.debug(
`No clients have access to site resource ${siteResource.siteResourceId}, skipping target generation.`
);
return;
}
let target: SubnetProxyTargetV2 | null = null;
const portRange = [
...parsePortRangeString(siteResource.tcpPortRangeString, "tcp"),
...parsePortRangeString(siteResource.udpPortRangeString, "udp")
];
const disableIcmp = siteResource.disableIcmp ?? false;
if (siteResource.mode == "host") {
let destination = siteResource.destination;
// check if this is a valid ip
const ipSchema = z.union([z.ipv4(), z.ipv6()]);
if (ipSchema.safeParse(destination).success) {
destination = `${destination}/32`;
target = {
sourcePrefixes: [],
destPrefix: destination,
portRange,
disableIcmp,
resourceId: siteResource.siteResourceId,
};
}
if (siteResource.alias && siteResource.aliasAddress) {
// also push a match for the alias address
target = {
sourcePrefixes: [],
destPrefix: `${siteResource.aliasAddress}/32`,
rewriteTo: destination,
portRange,
disableIcmp,
resourceId: siteResource.siteResourceId,
};
}
} else if (siteResource.mode == "cidr") {
target = {
sourcePrefixes: [],
destPrefix: siteResource.destination,
portRange,
disableIcmp,
resourceId: siteResource.siteResourceId,
};
}
if (!target) {
return;
}
for (const clientSite of clients) {
if (!clientSite.subnet) {
logger.debug(
`Client ${clientSite.clientId} has no subnet, skipping for site resource ${siteResource.siteResourceId}.`
);
continue;
}
const clientPrefix = `${clientSite.subnet.split("/")[0]}/32`;
// add client prefix to source prefixes
target.sourcePrefixes.push(clientPrefix);
}
// print a nice representation of the targets
// logger.debug(
// `Generated subnet proxy targets for: ${JSON.stringify(targets, null, 2)}`
// );
return target;
}
/**
* Converts a SubnetProxyTargetV2 to an array of SubnetProxyTarget (v1)
* by expanding each source prefix into its own target entry.
* @param targetV2 - The v2 target to convert
* @returns Array of v1 SubnetProxyTarget objects
*/
export function convertSubnetProxyTargetsV2ToV1(
targetsV2: SubnetProxyTargetV2[]
): SubnetProxyTarget[] {
return targetsV2.flatMap((targetV2) =>
targetV2.sourcePrefixes.map((sourcePrefix) => ({
sourcePrefix,
destPrefix: targetV2.destPrefix,
...(targetV2.disableIcmp !== undefined && {
disableIcmp: targetV2.disableIcmp
}),
...(targetV2.rewriteTo !== undefined && {
rewriteTo: targetV2.rewriteTo
}),
...(targetV2.portRange !== undefined && {
portRange: targetV2.portRange
})
}))
);
}
// Custom schema for validating port range strings
// Format: "80,443,8000-9000" or "*" for all ports, or empty string
export const portRangeStringSchema = z

View File

@@ -79,7 +79,6 @@ export const configSchema = z
.default(3001)
.transform(stoi)
.pipe(portSchema),
badger_override: z.string().optional(),
next_port: portSchema
.optional()
.default(3002)
@@ -303,8 +302,8 @@ export const configSchema = z
.optional()
.default({
block_size: 24,
subnet_group: "100.90.128.0/20",
utility_subnet_group: "100.96.128.0/20"
subnet_group: "100.90.128.0/24",
utility_subnet_group: "100.96.128.0/24"
}),
rate_limits: z
.object({

View File

@@ -14,7 +14,6 @@ import {
siteResources,
sites,
Transaction,
userOrgRoles,
userOrgs,
userSiteResources
} from "@server/db";
@@ -33,7 +32,7 @@ import logger from "@server/logger";
import {
generateAliasConfig,
generateRemoteSubnets,
generateSubnetProxyTargetV2,
generateSubnetProxyTargets,
parseEndpoint,
formatEndpoint
} from "@server/lib/ip";
@@ -78,10 +77,10 @@ export async function getClientSiteResourceAccess(
// get all of the users in these roles
const userIdsFromRoles = await trx
.select({
userId: userOrgRoles.userId
userId: userOrgs.userId
})
.from(userOrgRoles)
.where(inArray(userOrgRoles.roleId, roleIds))
.from(userOrgs)
.where(inArray(userOrgs.roleId, roleIds))
.then((rows) => rows.map((row) => row.userId));
const newAllUserIds = Array.from(
@@ -661,16 +660,19 @@ async function handleSubnetProxyTargetUpdates(
);
if (addedClients.length > 0) {
const targetToAdd = generateSubnetProxyTargetV2(
const targetsToAdd = generateSubnetProxyTargets(
siteResource,
addedClients
);
if (targetToAdd) {
if (targetsToAdd.length > 0) {
logger.info(
`Adding ${targetsToAdd.length} subnet proxy targets for siteResource ${siteResource.siteResourceId}`
);
proxyJobs.push(
addSubnetProxyTargets(
newt.newtId,
[targetToAdd],
targetsToAdd,
newt.version
)
);
@@ -698,16 +700,19 @@ async function handleSubnetProxyTargetUpdates(
);
if (removedClients.length > 0) {
const targetToRemove = generateSubnetProxyTargetV2(
const targetsToRemove = generateSubnetProxyTargets(
siteResource,
removedClients
);
if (targetToRemove) {
if (targetsToRemove.length > 0) {
logger.info(
`Removing ${targetsToRemove.length} subnet proxy targets for siteResource ${siteResource.siteResourceId}`
);
proxyJobs.push(
removeSubnetProxyTargets(
newt.newtId,
[targetToRemove],
targetsToRemove,
newt.version
)
);
@@ -815,12 +820,12 @@ export async function rebuildClientAssociationsFromClient(
// Role-based access
const roleIds = await trx
.select({ roleId: userOrgRoles.roleId })
.from(userOrgRoles)
.select({ roleId: userOrgs.roleId })
.from(userOrgs)
.where(
and(
eq(userOrgRoles.userId, client.userId),
eq(userOrgRoles.orgId, client.orgId)
eq(userOrgs.userId, client.userId),
eq(userOrgs.orgId, client.orgId)
)
) // this needs to be locked onto this org or else cross-org access could happen
.then((rows) => rows.map((row) => row.roleId));
@@ -1164,7 +1169,7 @@ async function handleMessagesForClientResources(
}
for (const resource of resources) {
const target = generateSubnetProxyTargetV2(resource, [
const targets = generateSubnetProxyTargets(resource, [
{
clientId: client.clientId,
pubKey: client.pubKey,
@@ -1172,11 +1177,11 @@ async function handleMessagesForClientResources(
}
]);
if (target) {
if (targets.length > 0) {
proxyJobs.push(
addSubnetProxyTargets(
newt.newtId,
[target],
targets,
newt.version
)
);
@@ -1241,7 +1246,7 @@ async function handleMessagesForClientResources(
}
for (const resource of resources) {
const target = generateSubnetProxyTargetV2(resource, [
const targets = generateSubnetProxyTargets(resource, [
{
clientId: client.clientId,
pubKey: client.pubKey,
@@ -1249,11 +1254,11 @@ async function handleMessagesForClientResources(
}
]);
if (target) {
if (targets.length > 0) {
proxyJobs.push(
removeSubnetProxyTargets(
newt.newtId,
[target],
targets,
newt.version
)
);

View File

@@ -6,7 +6,7 @@ import {
siteResources,
sites,
Transaction,
userOrgRoles,
UserOrg,
userOrgs,
userResources,
userSiteResources,
@@ -19,22 +19,9 @@ import { FeatureId } from "@server/lib/billing";
export async function assignUserToOrg(
org: Org,
values: typeof userOrgs.$inferInsert,
roleIds: number[],
trx: Transaction | typeof db = db
) {
const uniqueRoleIds = [...new Set(roleIds)];
if (uniqueRoleIds.length === 0) {
throw new Error("assignUserToOrg requires at least one roleId");
}
const [userOrg] = await trx.insert(userOrgs).values(values).returning();
await trx.insert(userOrgRoles).values(
uniqueRoleIds.map((roleId) => ({
userId: userOrg.userId,
orgId: userOrg.orgId,
roleId
}))
);
// calculate if the user is in any other of the orgs before we count it as an add to the billing org
if (org.billingOrgId) {
@@ -71,14 +58,6 @@ export async function removeUserFromOrg(
userId: string,
trx: Transaction | typeof db = db
) {
await trx
.delete(userOrgRoles)
.where(
and(
eq(userOrgRoles.userId, userId),
eq(userOrgRoles.orgId, org.orgId)
)
);
await trx
.delete(userOrgs)
.where(and(eq(userOrgs.userId, userId), eq(userOrgs.orgId, org.orgId)));

View File

@@ -1,36 +0,0 @@
import { db, roles, userOrgRoles } from "@server/db";
import { and, eq } from "drizzle-orm";
/**
* Get all role IDs a user has in an organization.
* Returns empty array if the user has no roles in the org (callers must treat as no access).
*/
export async function getUserOrgRoleIds(
userId: string,
orgId: string
): Promise<number[]> {
const rows = await db
.select({ roleId: userOrgRoles.roleId })
.from(userOrgRoles)
.where(
and(
eq(userOrgRoles.userId, userId),
eq(userOrgRoles.orgId, orgId)
)
);
return rows.map((r) => r.roleId);
}
export async function getUserOrgRoles(
userId: string,
orgId: string
): Promise<{ roleId: number; roleName: string }[]> {
const rows = await db
.select({ roleId: userOrgRoles.roleId, roleName: roles.name })
.from(userOrgRoles)
.innerJoin(roles, eq(userOrgRoles.roleId, roles.roleId))
.where(
and(eq(userOrgRoles.userId, userId), eq(userOrgRoles.orgId, orgId))
);
return rows;
}

View File

@@ -21,7 +21,8 @@ export async function getUserOrgs(
try {
const userOrganizations = await db
.select({
orgId: userOrgs.orgId
orgId: userOrgs.orgId,
roleId: userOrgs.roleId
})
.from(userOrgs)
.where(eq(userOrgs.userId, userId));

View File

@@ -17,7 +17,6 @@ export * from "./verifyAccessTokenAccess";
export * from "./requestTimeout";
export * from "./verifyClientAccess";
export * from "./verifyUserHasAction";
export * from "./verifyUserCanSetUserOrgRoles";
export * from "./verifyUserIsServerAdmin";
export * from "./verifyIsLoggedInUser";
export * from "./verifyIsLoggedInUser";
@@ -25,7 +24,6 @@ export * from "./verifyClientAccess";
export * from "./integration";
export * from "./verifyUserHasAction";
export * from "./verifyApiKeyAccess";
export * from "./verifySiteProvisioningKeyAccess";
export * from "./verifyDomainAccess";
export * from "./verifyUserIsOrgOwner";
export * from "./verifySiteResourceAccess";

View File

@@ -1,7 +1,6 @@
export * from "./verifyApiKey";
export * from "./verifyApiKeyOrgAccess";
export * from "./verifyApiKeyHasAction";
export * from "./verifyApiKeyCanSetUserOrgRoles";
export * from "./verifyApiKeySiteAccess";
export * from "./verifyApiKeyResourceAccess";
export * from "./verifyApiKeyTargetAccess";

View File

@@ -1,74 +0,0 @@
import { Request, Response, NextFunction } from "express";
import createHttpError from "http-errors";
import HttpCode from "@server/types/HttpCode";
import logger from "@server/logger";
import { ActionsEnum } from "@server/auth/actions";
import { db } from "@server/db";
import { apiKeyActions } from "@server/db";
import { and, eq } from "drizzle-orm";
async function apiKeyHasAction(apiKeyId: string, actionId: ActionsEnum) {
const [row] = await db
.select()
.from(apiKeyActions)
.where(
and(
eq(apiKeyActions.apiKeyId, apiKeyId),
eq(apiKeyActions.actionId, actionId)
)
);
return !!row;
}
/**
* Allows setUserOrgRoles on the key, or both addUserRole and removeUserRole.
*/
export function verifyApiKeyCanSetUserOrgRoles() {
return async function (
req: Request,
res: Response,
next: NextFunction
): Promise<any> {
try {
if (!req.apiKey) {
return next(
createHttpError(
HttpCode.UNAUTHORIZED,
"API Key not authenticated"
)
);
}
const keyId = req.apiKey.apiKeyId;
if (await apiKeyHasAction(keyId, ActionsEnum.setUserOrgRoles)) {
return next();
}
const hasAdd = await apiKeyHasAction(keyId, ActionsEnum.addUserRole);
const hasRemove = await apiKeyHasAction(
keyId,
ActionsEnum.removeUserRole
);
if (hasAdd && hasRemove) {
return next();
}
return next(
createHttpError(
HttpCode.FORBIDDEN,
"Key does not have permission perform this action"
)
);
} catch (error) {
logger.error("Error verifying API key set user org roles:", error);
return next(
createHttpError(
HttpCode.INTERNAL_SERVER_ERROR,
"Error verifying key action access"
)
);
}
};
}

View File

@@ -6,7 +6,6 @@ import createHttpError from "http-errors";
import HttpCode from "@server/types/HttpCode";
import { canUserAccessResource } from "@server/auth/canUserAccessResource";
import { checkOrgAccessPolicy } from "#dynamic/lib/checkOrgAccessPolicy";
import { getUserOrgRoleIds } from "@server/lib/userOrgRoles";
export async function verifyAccessTokenAccess(
req: Request,
@@ -94,10 +93,7 @@ export async function verifyAccessTokenAccess(
)
);
} else {
req.userOrgRoleIds = await getUserOrgRoleIds(
req.userOrg.userId,
resource[0].orgId!
);
req.userOrgRoleId = req.userOrg.roleId;
req.userOrgId = resource[0].orgId!;
}
@@ -122,7 +118,7 @@ export async function verifyAccessTokenAccess(
const resourceAllowed = await canUserAccessResource({
userId,
resourceId,
roleIds: req.userOrgRoleIds ?? []
roleId: req.userOrgRoleId!
});
if (!resourceAllowed) {

View File

@@ -1,11 +1,10 @@
import { Request, Response, NextFunction } from "express";
import { db } from "@server/db";
import { roles, userOrgs } from "@server/db";
import { and, eq, inArray } from "drizzle-orm";
import { and, eq } from "drizzle-orm";
import createHttpError from "http-errors";
import HttpCode from "@server/types/HttpCode";
import { checkOrgAccessPolicy } from "#dynamic/lib/checkOrgAccessPolicy";
import { getUserOrgRoleIds } from "@server/lib/userOrgRoles";
export async function verifyAdmin(
req: Request,
@@ -63,29 +62,13 @@ export async function verifyAdmin(
}
}
req.userOrgRoleIds = await getUserOrgRoleIds(req.userOrg.userId, orgId!);
if (req.userOrgRoleIds.length === 0) {
return next(
createHttpError(
HttpCode.FORBIDDEN,
"User does not have Admin access"
)
);
}
const userAdminRoles = await db
const userRole = await db
.select()
.from(roles)
.where(
and(
inArray(roles.roleId, req.userOrgRoleIds),
eq(roles.isAdmin, true)
)
)
.where(eq(roles.roleId, req.userOrg.roleId))
.limit(1);
if (userAdminRoles.length === 0) {
if (userRole.length === 0 || !userRole[0].isAdmin) {
return next(
createHttpError(
HttpCode.FORBIDDEN,

View File

@@ -1,11 +1,10 @@
import { Request, Response, NextFunction } from "express";
import { db } from "@server/db";
import { userOrgs, apiKeys, apiKeyOrg } from "@server/db";
import { and, eq } from "drizzle-orm";
import { and, eq, or } from "drizzle-orm";
import createHttpError from "http-errors";
import HttpCode from "@server/types/HttpCode";
import { checkOrgAccessPolicy } from "#dynamic/lib/checkOrgAccessPolicy";
import { getUserOrgRoleIds } from "@server/lib/userOrgRoles";
export async function verifyApiKeyAccess(
req: Request,
@@ -104,10 +103,8 @@ export async function verifyApiKeyAccess(
}
}
req.userOrgRoleIds = await getUserOrgRoleIds(
req.userOrg.userId,
orgId
);
const userOrgRoleId = req.userOrg.roleId;
req.userOrgRoleId = userOrgRoleId;
return next();
} catch (error) {

View File

@@ -1,12 +1,11 @@
import { Request, Response, NextFunction } from "express";
import { Client, db } from "@server/db";
import { userOrgs, clients, roleClients, userClients } from "@server/db";
import { and, eq, inArray } from "drizzle-orm";
import { and, eq } from "drizzle-orm";
import createHttpError from "http-errors";
import HttpCode from "@server/types/HttpCode";
import { checkOrgAccessPolicy } from "#dynamic/lib/checkOrgAccessPolicy";
import logger from "@server/logger";
import { getUserOrgRoleIds } from "@server/lib/userOrgRoles";
export async function verifyClientAccess(
req: Request,
@@ -114,30 +113,21 @@ export async function verifyClientAccess(
}
}
req.userOrgRoleIds = await getUserOrgRoleIds(
req.userOrg.userId,
client.orgId
);
const userOrgRoleId = req.userOrg.roleId;
req.userOrgRoleId = userOrgRoleId;
req.userOrgId = client.orgId;
// Check role-based client access (any of user's roles)
const roleClientAccessList =
(req.userOrgRoleIds?.length ?? 0) > 0
? await db
.select()
.from(roleClients)
.where(
and(
eq(roleClients.clientId, client.clientId),
inArray(
roleClients.roleId,
req.userOrgRoleIds!
)
)
)
.limit(1)
: [];
const [roleClientAccess] = roleClientAccessList;
// Check role-based site access first
const [roleClientAccess] = await db
.select()
.from(roleClients)
.where(
and(
eq(roleClients.clientId, client.clientId),
eq(roleClients.roleId, userOrgRoleId)
)
)
.limit(1);
if (roleClientAccess) {
// User has access to the site through their role

View File

@@ -1,11 +1,10 @@
import { Request, Response, NextFunction } from "express";
import { db, domains, orgDomains } from "@server/db";
import { userOrgs } 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";
import { checkOrgAccessPolicy } from "#dynamic/lib/checkOrgAccessPolicy";
import { getUserOrgRoleIds } from "@server/lib/userOrgRoles";
export async function verifyDomainAccess(
req: Request,
@@ -64,7 +63,7 @@ export async function verifyDomainAccess(
.where(
and(
eq(userOrgs.userId, userId),
eq(userOrgs.orgId, orgId)
eq(userOrgs.orgId, apiKeyOrg.orgId)
)
)
.limit(1);
@@ -98,7 +97,8 @@ export async function verifyDomainAccess(
}
}
req.userOrgRoleIds = await getUserOrgRoleIds(req.userOrg.userId, orgId);
const userOrgRoleId = req.userOrg.roleId;
req.userOrgRoleId = userOrgRoleId;
return next();
} catch (error) {

View File

@@ -1,11 +1,10 @@
import { Request, Response, NextFunction } from "express";
import { db } from "@server/db";
import { db, orgs } from "@server/db";
import { userOrgs } from "@server/db";
import { and, eq } from "drizzle-orm";
import createHttpError from "http-errors";
import HttpCode from "@server/types/HttpCode";
import { checkOrgAccessPolicy } from "#dynamic/lib/checkOrgAccessPolicy";
import { getUserOrgRoleIds } from "@server/lib/userOrgRoles";
export async function verifyOrgAccess(
req: Request,
@@ -65,8 +64,8 @@ export async function verifyOrgAccess(
}
}
// User has access, attach the user's role(s) to the request for potential future use
req.userOrgRoleIds = await getUserOrgRoleIds(req.userOrg.userId, orgId);
// User has access, attach the user's role to the request for potential future use
req.userOrgRoleId = req.userOrg.roleId;
req.userOrgId = orgId;
return next();

View File

@@ -1,11 +1,10 @@
import { Request, Response, NextFunction } from "express";
import { db, Resource } from "@server/db";
import { resources, userOrgs, userResources, roleResources } from "@server/db";
import { and, eq, inArray } from "drizzle-orm";
import { and, eq } from "drizzle-orm";
import createHttpError from "http-errors";
import HttpCode from "@server/types/HttpCode";
import { checkOrgAccessPolicy } from "#dynamic/lib/checkOrgAccessPolicy";
import { getUserOrgRoleIds } from "@server/lib/userOrgRoles";
export async function verifyResourceAccess(
req: Request,
@@ -108,28 +107,20 @@ export async function verifyResourceAccess(
}
}
req.userOrgRoleIds = await getUserOrgRoleIds(
req.userOrg.userId,
resource.orgId
);
const userOrgRoleId = req.userOrg.roleId;
req.userOrgRoleId = userOrgRoleId;
req.userOrgId = resource.orgId;
const roleResourceAccess =
(req.userOrgRoleIds?.length ?? 0) > 0
? await db
.select()
.from(roleResources)
.where(
and(
eq(roleResources.resourceId, resource.resourceId),
inArray(
roleResources.roleId,
req.userOrgRoleIds!
)
)
)
.limit(1)
: [];
const roleResourceAccess = await db
.select()
.from(roleResources)
.where(
and(
eq(roleResources.resourceId, resource.resourceId),
eq(roleResources.roleId, userOrgRoleId)
)
)
.limit(1);
if (roleResourceAccess.length > 0) {
return next();

View File

@@ -6,7 +6,6 @@ import createHttpError from "http-errors";
import HttpCode from "@server/types/HttpCode";
import logger from "@server/logger";
import { checkOrgAccessPolicy } from "#dynamic/lib/checkOrgAccessPolicy";
import { getUserOrgRoleIds } from "@server/lib/userOrgRoles";
export async function verifyRoleAccess(
req: Request,
@@ -100,6 +99,7 @@ export async function verifyRoleAccess(
}
if (!req.userOrg) {
// get the userORg
const userOrg = await db
.select()
.from(userOrgs)
@@ -109,7 +109,7 @@ export async function verifyRoleAccess(
.limit(1);
req.userOrg = userOrg[0];
req.userOrgRoleIds = await getUserOrgRoleIds(userId, orgId!);
req.userOrgRoleId = userOrg[0].roleId;
}
if (!req.userOrg) {

View File

@@ -1,11 +1,10 @@
import { Request, Response, NextFunction } from "express";
import { db } from "@server/db";
import { sites, Site, userOrgs, userSites, roleSites, roles } from "@server/db";
import { and, eq, inArray, or } from "drizzle-orm";
import { and, eq, or } from "drizzle-orm";
import createHttpError from "http-errors";
import HttpCode from "@server/types/HttpCode";
import { checkOrgAccessPolicy } from "#dynamic/lib/checkOrgAccessPolicy";
import { getUserOrgRoleIds } from "@server/lib/userOrgRoles";
export async function verifySiteAccess(
req: Request,
@@ -113,29 +112,21 @@ export async function verifySiteAccess(
}
}
req.userOrgRoleIds = await getUserOrgRoleIds(
req.userOrg.userId,
site.orgId
);
const userOrgRoleId = req.userOrg.roleId;
req.userOrgRoleId = userOrgRoleId;
req.userOrgId = site.orgId;
// Check role-based site access first (any of user's roles)
const roleSiteAccess =
(req.userOrgRoleIds?.length ?? 0) > 0
? await db
.select()
.from(roleSites)
.where(
and(
eq(roleSites.siteId, site.siteId),
inArray(
roleSites.roleId,
req.userOrgRoleIds!
)
)
)
.limit(1)
: [];
// Check role-based site access first
const roleSiteAccess = await db
.select()
.from(roleSites)
.where(
and(
eq(roleSites.siteId, site.siteId),
eq(roleSites.roleId, userOrgRoleId)
)
)
.limit(1);
if (roleSiteAccess.length > 0) {
// User's role has access to the site

View File

@@ -1,131 +0,0 @@
import { Request, Response, NextFunction } from "express";
import { db, userOrgs, siteProvisioningKeys, siteProvisioningKeyOrg } from "@server/db";
import { and, eq } from "drizzle-orm";
import createHttpError from "http-errors";
import HttpCode from "@server/types/HttpCode";
import { checkOrgAccessPolicy } from "#dynamic/lib/checkOrgAccessPolicy";
export async function verifySiteProvisioningKeyAccess(
req: Request,
res: Response,
next: NextFunction
) {
try {
const userId = req.user!.userId;
const siteProvisioningKeyId = req.params.siteProvisioningKeyId;
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 (!siteProvisioningKeyId) {
return next(
createHttpError(HttpCode.BAD_REQUEST, "Invalid key ID")
);
}
const [row] = await db
.select()
.from(siteProvisioningKeys)
.innerJoin(
siteProvisioningKeyOrg,
and(
eq(
siteProvisioningKeys.siteProvisioningKeyId,
siteProvisioningKeyOrg.siteProvisioningKeyId
),
eq(siteProvisioningKeyOrg.orgId, orgId)
)
)
.where(
eq(
siteProvisioningKeys.siteProvisioningKeyId,
siteProvisioningKeyId
)
)
.limit(1);
if (!row?.siteProvisioningKeys) {
return next(
createHttpError(
HttpCode.NOT_FOUND,
`Site provisioning key with ID ${siteProvisioningKeyId} not found`
)
);
}
if (!row.siteProvisioningKeyOrg.orgId) {
return next(
createHttpError(
HttpCode.INTERNAL_SERVER_ERROR,
`Site provisioning key with ID ${siteProvisioningKeyId} does not have an organization ID`
)
);
}
if (!req.userOrg) {
const userOrgRole = await db
.select()
.from(userOrgs)
.where(
and(
eq(userOrgs.userId, userId),
eq(
userOrgs.orgId,
row.siteProvisioningKeyOrg.orgId
)
)
)
.limit(1);
req.userOrg = userOrgRole[0];
}
if (!req.userOrg) {
return next(
createHttpError(
HttpCode.FORBIDDEN,
"User does not have access to this organization"
)
);
}
if (req.orgPolicyAllowed === undefined && req.userOrg.orgId) {
const policyCheck = await checkOrgAccessPolicy({
orgId: req.userOrg.orgId,
userId,
session: req.session
});
req.orgPolicyAllowed = policyCheck.allowed;
if (!policyCheck.allowed || policyCheck.error) {
return next(
createHttpError(
HttpCode.FORBIDDEN,
"Failed organization access policy check: " +
(policyCheck.error || "Unknown error")
)
);
}
}
const userOrgRoleId = req.userOrg.roleId;
req.userOrgRoleId = userOrgRoleId;
return next();
} catch (error) {
return next(
createHttpError(
HttpCode.INTERNAL_SERVER_ERROR,
"Error verifying site provisioning key access"
)
);
}
}

View File

@@ -1,12 +1,11 @@
import { Request, Response, NextFunction } from "express";
import { db, roleSiteResources, userOrgs, userSiteResources } from "@server/db";
import { siteResources } from "@server/db";
import { eq, and, inArray } from "drizzle-orm";
import { eq, and } from "drizzle-orm";
import createHttpError from "http-errors";
import HttpCode from "@server/types/HttpCode";
import logger from "@server/logger";
import { checkOrgAccessPolicy } from "#dynamic/lib/checkOrgAccessPolicy";
import { getUserOrgRoleIds } from "@server/lib/userOrgRoles";
export async function verifySiteResourceAccess(
req: Request,
@@ -110,34 +109,23 @@ export async function verifySiteResourceAccess(
}
}
req.userOrgRoleIds = await getUserOrgRoleIds(
req.userOrg.userId,
siteResource.orgId
);
const userOrgRoleId = req.userOrg.roleId;
req.userOrgRoleId = userOrgRoleId;
req.userOrgId = siteResource.orgId;
// Attach the siteResource to the request for use in the next middleware/route
req.siteResource = siteResource;
const roleResourceAccess =
(req.userOrgRoleIds?.length ?? 0) > 0
? await db
.select()
.from(roleSiteResources)
.where(
and(
eq(
roleSiteResources.siteResourceId,
siteResourceIdNum
),
inArray(
roleSiteResources.roleId,
req.userOrgRoleIds!
)
)
)
.limit(1)
: [];
const roleResourceAccess = await db
.select()
.from(roleSiteResources)
.where(
and(
eq(roleSiteResources.siteResourceId, siteResourceIdNum),
eq(roleSiteResources.roleId, userOrgRoleId)
)
)
.limit(1);
if (roleResourceAccess.length > 0) {
return next();

View File

@@ -6,7 +6,6 @@ import createHttpError from "http-errors";
import HttpCode from "@server/types/HttpCode";
import { canUserAccessResource } from "../auth/canUserAccessResource";
import { checkOrgAccessPolicy } from "#dynamic/lib/checkOrgAccessPolicy";
import { getUserOrgRoleIds } from "@server/lib/userOrgRoles";
export async function verifyTargetAccess(
req: Request,
@@ -100,10 +99,7 @@ export async function verifyTargetAccess(
)
);
} else {
req.userOrgRoleIds = await getUserOrgRoleIds(
req.userOrg.userId,
resource[0].orgId!
);
req.userOrgRoleId = req.userOrg.roleId;
req.userOrgId = resource[0].orgId!;
}
@@ -130,7 +126,7 @@ export async function verifyTargetAccess(
const resourceAllowed = await canUserAccessResource({
userId,
resourceId,
roleIds: req.userOrgRoleIds ?? []
roleId: req.userOrgRoleId!
});
if (!resourceAllowed) {

View File

@@ -1,54 +0,0 @@
import { Request, Response, NextFunction } from "express";
import createHttpError from "http-errors";
import HttpCode from "@server/types/HttpCode";
import logger from "@server/logger";
import { ActionsEnum, checkUserActionPermission } from "@server/auth/actions";
/**
* Allows the new setUserOrgRoles action, or legacy permission pair addUserRole + removeUserRole.
*/
export function verifyUserCanSetUserOrgRoles() {
return async function (
req: Request,
res: Response,
next: NextFunction
): Promise<any> {
try {
const canSet = await checkUserActionPermission(
ActionsEnum.setUserOrgRoles,
req
);
if (canSet) {
return next();
}
const canAdd = await checkUserActionPermission(
ActionsEnum.addUserRole,
req
);
const canRemove = await checkUserActionPermission(
ActionsEnum.removeUserRole,
req
);
if (canAdd && canRemove) {
return next();
}
return next(
createHttpError(
HttpCode.FORBIDDEN,
"User does not have permission perform this action"
)
);
} catch (error) {
logger.error("Error verifying set user org roles access:", error);
return next(
createHttpError(
HttpCode.INTERNAL_SERVER_ERROR,
"Error verifying role access"
)
);
}
};
}

View File

@@ -12,7 +12,7 @@ export async function verifyUserInRole(
const roleId = parseInt(
req.params.roleId || req.body.roleId || req.query.roleId
);
const userOrgRoleIds = req.userOrgRoleIds ?? [];
const userRoleId = req.userOrgRoleId;
if (isNaN(roleId)) {
return next(
@@ -20,7 +20,7 @@ export async function verifyUserInRole(
);
}
if (userOrgRoleIds.length === 0) {
if (!userRoleId) {
return next(
createHttpError(
HttpCode.FORBIDDEN,
@@ -29,7 +29,7 @@ export async function verifyUserInRole(
);
}
if (!userOrgRoleIds.includes(roleId)) {
if (userRoleId !== roleId) {
return next(
createHttpError(
HttpCode.FORBIDDEN,

View File

@@ -14,14 +14,12 @@
import { rateLimitService } from "#private/lib/rateLimit";
import { cleanup as wsCleanup } from "#private/routers/ws";
import { flushBandwidthToDb } from "@server/routers/newt/handleReceiveBandwidthMessage";
import { flushConnectionLogToDb } from "#dynamic/routers/newt";
import { flushSiteBandwidthToDb } from "@server/routers/gerbil/receiveBandwidth";
import { stopPingAccumulator } from "@server/routers/newt/pingAccumulator";
async function cleanup() {
await stopPingAccumulator();
await flushBandwidthToDb();
await flushConnectionLogToDb();
await flushSiteBandwidthToDb();
await rateLimitService.cleanup();
await wsCleanup();
@@ -33,4 +31,4 @@ export async function initCleanup() {
// Handle process termination
process.on("SIGTERM", () => cleanup());
process.on("SIGINT", () => cleanup());
}
}

View File

@@ -74,7 +74,6 @@ export async function logAccessAudit(data: {
type: string;
orgId: string;
resourceId?: number;
siteResourceId?: number;
user?: { username: string; userId: string };
apiKey?: { name: string | null; apiKeyId: string };
metadata?: any;
@@ -135,7 +134,6 @@ export async function logAccessAudit(data: {
type: data.type,
metadata,
resourceId: data.resourceId,
siteResourceId: data.siteResourceId,
userAgent: data.userAgent,
ip: clientIp,
location: countryCode

View File

@@ -57,10 +57,7 @@ export const privateConfigSchema = z.object({
.object({
host: z.string(),
port: portSchema,
password: z
.string()
.optional()
.transform(getEnvOrYaml("REDIS_PASSWORD")),
password: z.string().optional(),
db: z.int().nonnegative().optional().default(0),
replicas: z
.array(

View File

@@ -13,10 +13,9 @@
import { Request, Response, NextFunction } from "express";
import { userOrgs, db, idp, idpOrg } from "@server/db";
import { and, eq } from "drizzle-orm";
import { and, eq, or } from "drizzle-orm";
import createHttpError from "http-errors";
import HttpCode from "@server/types/HttpCode";
import { getUserOrgRoleIds } from "@server/lib/userOrgRoles";
export async function verifyIdpAccess(
req: Request,
@@ -85,10 +84,8 @@ export async function verifyIdpAccess(
);
}
req.userOrgRoleIds = await getUserOrgRoleIds(
req.userOrg.userId,
idpRes.idpOrg.orgId
);
const userOrgRoleId = req.userOrg.roleId;
req.userOrgRoleId = userOrgRoleId;
return next();
} catch (error) {

View File

@@ -12,12 +12,11 @@
*/
import { Request, Response, NextFunction } from "express";
import { db, exitNodeOrgs, remoteExitNodes } from "@server/db";
import { userOrgs } from "@server/db";
import { and, eq } from "drizzle-orm";
import { db, exitNodeOrgs, exitNodes, remoteExitNodes } from "@server/db";
import { sites, userOrgs, userSites, roleSites, roles } from "@server/db";
import { and, eq, or } from "drizzle-orm";
import createHttpError from "http-errors";
import HttpCode from "@server/types/HttpCode";
import { getUserOrgRoleIds } from "@server/lib/userOrgRoles";
export async function verifyRemoteExitNodeAccess(
req: Request,
@@ -104,10 +103,8 @@ export async function verifyRemoteExitNodeAccess(
);
}
req.userOrgRoleIds = await getUserOrgRoleIds(
req.userOrg.userId,
exitNodeOrg.orgId
);
const userOrgRoleId = req.userOrg.roleId;
req.userOrgRoleId = userOrgRoleId;
return next();
} catch (error) {

View File

@@ -1,99 +0,0 @@
/*
* 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.
*/
import { registry } from "@server/openApi";
import { NextFunction } from "express";
import { Request, Response } from "express";
import { OpenAPITags } from "@server/openApi";
import createHttpError from "http-errors";
import HttpCode from "@server/types/HttpCode";
import { fromError } from "zod-validation-error";
import logger from "@server/logger";
import {
queryConnectionAuditLogsParams,
queryConnectionAuditLogsQuery,
queryConnection,
countConnectionQuery
} from "./queryConnectionAuditLog";
import { generateCSV } from "@server/routers/auditLogs/generateCSV";
import { MAX_EXPORT_LIMIT } from "@server/routers/auditLogs";
registry.registerPath({
method: "get",
path: "/org/{orgId}/logs/connection/export",
description: "Export the connection audit log for an organization as CSV",
tags: [OpenAPITags.Logs],
request: {
query: queryConnectionAuditLogsQuery,
params: queryConnectionAuditLogsParams
},
responses: {}
});
export async function exportConnectionAuditLogs(
req: Request,
res: Response,
next: NextFunction
): Promise<any> {
try {
const parsedQuery = queryConnectionAuditLogsQuery.safeParse(req.query);
if (!parsedQuery.success) {
return next(
createHttpError(
HttpCode.BAD_REQUEST,
fromError(parsedQuery.error)
)
);
}
const parsedParams = queryConnectionAuditLogsParams.safeParse(req.params);
if (!parsedParams.success) {
return next(
createHttpError(
HttpCode.BAD_REQUEST,
fromError(parsedParams.error)
)
);
}
const data = { ...parsedQuery.data, ...parsedParams.data };
const [{ count }] = await countConnectionQuery(data);
if (count > MAX_EXPORT_LIMIT) {
return next(
createHttpError(
HttpCode.BAD_REQUEST,
`Export limit exceeded. Your selection contains ${count} rows, but the maximum is ${MAX_EXPORT_LIMIT} rows. Please select a shorter time range to reduce the data.`
)
);
}
const baseQuery = queryConnection(data);
const log = await baseQuery.limit(data.limit).offset(data.offset);
const csvData = generateCSV(log);
res.setHeader("Content-Type", "text/csv");
res.setHeader(
"Content-Disposition",
`attachment; filename="connection-audit-logs-${data.orgId}-${Date.now()}.csv"`
);
return res.send(csvData);
} catch (error) {
logger.error(error);
return next(
createHttpError(HttpCode.INTERNAL_SERVER_ERROR, "An error occurred")
);
}
}

View File

@@ -15,5 +15,3 @@ export * from "./queryActionAuditLog";
export * from "./exportActionAuditLog";
export * from "./queryAccessAuditLog";
export * from "./exportAccessAuditLog";
export * from "./queryConnectionAuditLog";
export * from "./exportConnectionAuditLog";

View File

@@ -11,11 +11,11 @@
* This file is not licensed under the AGPLv3.
*/
import { accessAuditLog, logsDb, resources, siteResources, db, primaryDb } from "@server/db";
import { accessAuditLog, logsDb, resources, db, primaryDb } from "@server/db";
import { registry } from "@server/openApi";
import { NextFunction } from "express";
import { Request, Response } from "express";
import { eq, gt, lt, and, count, desc, inArray, isNull } from "drizzle-orm";
import { eq, gt, lt, and, count, desc, inArray } from "drizzle-orm";
import { OpenAPITags } from "@server/openApi";
import { z } from "zod";
import createHttpError from "http-errors";
@@ -122,7 +122,6 @@ export function queryAccess(data: Q) {
actorType: accessAuditLog.actorType,
actorId: accessAuditLog.actorId,
resourceId: accessAuditLog.resourceId,
siteResourceId: accessAuditLog.siteResourceId,
ip: accessAuditLog.ip,
location: accessAuditLog.location,
userAgent: accessAuditLog.userAgent,
@@ -137,73 +136,37 @@ export function queryAccess(data: Q) {
}
async function enrichWithResourceDetails(logs: Awaited<ReturnType<typeof queryAccess>>) {
// If logs database is the same as main database, we can do a join
// Otherwise, we need to fetch resource details separately
const resourceIds = logs
.map(log => log.resourceId)
.filter((id): id is number => id !== null && id !== undefined);
const siteResourceIds = logs
.filter(log => log.resourceId == null && log.siteResourceId != null)
.map(log => log.siteResourceId)
.filter((id): id is number => id !== null && id !== undefined);
if (resourceIds.length === 0 && siteResourceIds.length === 0) {
if (resourceIds.length === 0) {
return logs.map(log => ({ ...log, resourceName: null, resourceNiceId: null }));
}
const resourceMap = new Map<number, { name: string | null; niceId: string | null }>();
// Fetch resource details from main database
const resourceDetails = await primaryDb
.select({
resourceId: resources.resourceId,
name: resources.name,
niceId: resources.niceId
})
.from(resources)
.where(inArray(resources.resourceId, resourceIds));
if (resourceIds.length > 0) {
const resourceDetails = await primaryDb
.select({
resourceId: resources.resourceId,
name: resources.name,
niceId: resources.niceId
})
.from(resources)
.where(inArray(resources.resourceId, resourceIds));
for (const r of resourceDetails) {
resourceMap.set(r.resourceId, { name: r.name, niceId: r.niceId });
}
}
const siteResourceMap = new Map<number, { name: string | null; niceId: string | null }>();
if (siteResourceIds.length > 0) {
const siteResourceDetails = await primaryDb
.select({
siteResourceId: siteResources.siteResourceId,
name: siteResources.name,
niceId: siteResources.niceId
})
.from(siteResources)
.where(inArray(siteResources.siteResourceId, siteResourceIds));
for (const r of siteResourceDetails) {
siteResourceMap.set(r.siteResourceId, { name: r.name, niceId: r.niceId });
}
}
// Create a map for quick lookup
const resourceMap = new Map(
resourceDetails.map(r => [r.resourceId, { name: r.name, niceId: r.niceId }])
);
// Enrich logs with resource details
return logs.map(log => {
if (log.resourceId != null) {
const details = resourceMap.get(log.resourceId);
return {
...log,
resourceName: details?.name ?? null,
resourceNiceId: details?.niceId ?? null
};
} else if (log.siteResourceId != null) {
const details = siteResourceMap.get(log.siteResourceId);
return {
...log,
resourceId: log.siteResourceId,
resourceName: details?.name ?? null,
resourceNiceId: details?.niceId ?? null
};
}
return { ...log, resourceName: null, resourceNiceId: null };
});
return logs.map(log => ({
...log,
resourceName: log.resourceId ? resourceMap.get(log.resourceId)?.name ?? null : null,
resourceNiceId: log.resourceId ? resourceMap.get(log.resourceId)?.niceId ?? null : null
}));
}
export function countAccessQuery(data: Q) {
@@ -249,23 +212,11 @@ async function queryUniqueFilterAttributes(
.from(accessAuditLog)
.where(baseConditions);
// Get unique siteResources (only for logs where resourceId is null)
const uniqueSiteResources = await logsDb
.selectDistinct({
id: accessAuditLog.siteResourceId
})
.from(accessAuditLog)
.where(and(baseConditions, isNull(accessAuditLog.resourceId)));
// Fetch resource names from main database for the unique resource IDs
const resourceIds = uniqueResources
.map(row => row.id)
.filter((id): id is number => id !== null);
const siteResourceIds = uniqueSiteResources
.map(row => row.id)
.filter((id): id is number => id !== null);
let resourcesWithNames: Array<{ id: number; name: string | null }> = [];
if (resourceIds.length > 0) {
@@ -277,31 +228,10 @@ async function queryUniqueFilterAttributes(
.from(resources)
.where(inArray(resources.resourceId, resourceIds));
resourcesWithNames = [
...resourcesWithNames,
...resourceDetails.map(r => ({
id: r.resourceId,
name: r.name
}))
];
}
if (siteResourceIds.length > 0) {
const siteResourceDetails = await primaryDb
.select({
siteResourceId: siteResources.siteResourceId,
name: siteResources.name
})
.from(siteResources)
.where(inArray(siteResources.siteResourceId, siteResourceIds));
resourcesWithNames = [
...resourcesWithNames,
...siteResourceDetails.map(r => ({
id: r.siteResourceId,
name: r.name
}))
];
resourcesWithNames = resourceDetails.map(r => ({
id: r.resourceId,
name: r.name
}));
}
return {

View File

@@ -1,524 +0,0 @@
/*
* 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.
*/
import {
connectionAuditLog,
logsDb,
siteResources,
sites,
clients,
users,
primaryDb
} from "@server/db";
import { registry } from "@server/openApi";
import { NextFunction } from "express";
import { Request, Response } from "express";
import { eq, gt, lt, and, count, desc, inArray } from "drizzle-orm";
import { OpenAPITags } from "@server/openApi";
import { z } from "zod";
import createHttpError from "http-errors";
import HttpCode from "@server/types/HttpCode";
import { fromError } from "zod-validation-error";
import { QueryConnectionAuditLogResponse } from "@server/routers/auditLogs/types";
import response from "@server/lib/response";
import logger from "@server/logger";
import { getSevenDaysAgo } from "@app/lib/getSevenDaysAgo";
export const queryConnectionAuditLogsQuery = z.object({
// iso string just validate its a parseable date
timeStart: z
.string()
.refine((val) => !isNaN(Date.parse(val)), {
error: "timeStart must be a valid ISO date string"
})
.transform((val) => Math.floor(new Date(val).getTime() / 1000))
.prefault(() => getSevenDaysAgo().toISOString())
.openapi({
type: "string",
format: "date-time",
description:
"Start time as ISO date string (defaults to 7 days ago)"
}),
timeEnd: z
.string()
.refine((val) => !isNaN(Date.parse(val)), {
error: "timeEnd must be a valid ISO date string"
})
.transform((val) => Math.floor(new Date(val).getTime() / 1000))
.optional()
.prefault(() => new Date().toISOString())
.openapi({
type: "string",
format: "date-time",
description:
"End time as ISO date string (defaults to current time)"
}),
protocol: z.string().optional(),
sourceAddr: z.string().optional(),
destAddr: z.string().optional(),
clientId: z
.string()
.optional()
.transform(Number)
.pipe(z.int().positive())
.optional(),
siteId: z
.string()
.optional()
.transform(Number)
.pipe(z.int().positive())
.optional(),
siteResourceId: z
.string()
.optional()
.transform(Number)
.pipe(z.int().positive())
.optional(),
userId: z.string().optional(),
limit: z
.string()
.optional()
.default("1000")
.transform(Number)
.pipe(z.int().positive()),
offset: z
.string()
.optional()
.default("0")
.transform(Number)
.pipe(z.int().nonnegative())
});
export const queryConnectionAuditLogsParams = z.object({
orgId: z.string()
});
export const queryConnectionAuditLogsCombined =
queryConnectionAuditLogsQuery.merge(queryConnectionAuditLogsParams);
type Q = z.infer<typeof queryConnectionAuditLogsCombined>;
function getWhere(data: Q) {
return and(
gt(connectionAuditLog.startedAt, data.timeStart),
lt(connectionAuditLog.startedAt, data.timeEnd),
eq(connectionAuditLog.orgId, data.orgId),
data.protocol
? eq(connectionAuditLog.protocol, data.protocol)
: undefined,
data.sourceAddr
? eq(connectionAuditLog.sourceAddr, data.sourceAddr)
: undefined,
data.destAddr
? eq(connectionAuditLog.destAddr, data.destAddr)
: undefined,
data.clientId
? eq(connectionAuditLog.clientId, data.clientId)
: undefined,
data.siteId
? eq(connectionAuditLog.siteId, data.siteId)
: undefined,
data.siteResourceId
? eq(connectionAuditLog.siteResourceId, data.siteResourceId)
: undefined,
data.userId
? eq(connectionAuditLog.userId, data.userId)
: undefined
);
}
export function queryConnection(data: Q) {
return logsDb
.select({
sessionId: connectionAuditLog.sessionId,
siteResourceId: connectionAuditLog.siteResourceId,
orgId: connectionAuditLog.orgId,
siteId: connectionAuditLog.siteId,
clientId: connectionAuditLog.clientId,
userId: connectionAuditLog.userId,
sourceAddr: connectionAuditLog.sourceAddr,
destAddr: connectionAuditLog.destAddr,
protocol: connectionAuditLog.protocol,
startedAt: connectionAuditLog.startedAt,
endedAt: connectionAuditLog.endedAt,
bytesTx: connectionAuditLog.bytesTx,
bytesRx: connectionAuditLog.bytesRx
})
.from(connectionAuditLog)
.where(getWhere(data))
.orderBy(
desc(connectionAuditLog.startedAt),
desc(connectionAuditLog.id)
);
}
export function countConnectionQuery(data: Q) {
const countQuery = logsDb
.select({ count: count() })
.from(connectionAuditLog)
.where(getWhere(data));
return countQuery;
}
async function enrichWithDetails(
logs: Awaited<ReturnType<typeof queryConnection>>
) {
// Collect unique IDs from logs
const siteResourceIds = [
...new Set(
logs
.map((log) => log.siteResourceId)
.filter((id): id is number => id !== null && id !== undefined)
)
];
const siteIds = [
...new Set(
logs
.map((log) => log.siteId)
.filter((id): id is number => id !== null && id !== undefined)
)
];
const clientIds = [
...new Set(
logs
.map((log) => log.clientId)
.filter((id): id is number => id !== null && id !== undefined)
)
];
const userIds = [
...new Set(
logs
.map((log) => log.userId)
.filter((id): id is string => id !== null && id !== undefined)
)
];
// Fetch resource details from main database
const resourceMap = new Map<
number,
{ name: string; niceId: string }
>();
if (siteResourceIds.length > 0) {
const resourceDetails = await primaryDb
.select({
siteResourceId: siteResources.siteResourceId,
name: siteResources.name,
niceId: siteResources.niceId
})
.from(siteResources)
.where(inArray(siteResources.siteResourceId, siteResourceIds));
for (const r of resourceDetails) {
resourceMap.set(r.siteResourceId, {
name: r.name,
niceId: r.niceId
});
}
}
// Fetch site details from main database
const siteMap = new Map<number, { name: string; niceId: string }>();
if (siteIds.length > 0) {
const siteDetails = await primaryDb
.select({
siteId: sites.siteId,
name: sites.name,
niceId: sites.niceId
})
.from(sites)
.where(inArray(sites.siteId, siteIds));
for (const s of siteDetails) {
siteMap.set(s.siteId, { name: s.name, niceId: s.niceId });
}
}
// Fetch client details from main database
const clientMap = new Map<
number,
{ name: string; niceId: string; type: string }
>();
if (clientIds.length > 0) {
const clientDetails = await primaryDb
.select({
clientId: clients.clientId,
name: clients.name,
niceId: clients.niceId,
type: clients.type
})
.from(clients)
.where(inArray(clients.clientId, clientIds));
for (const c of clientDetails) {
clientMap.set(c.clientId, {
name: c.name,
niceId: c.niceId,
type: c.type
});
}
}
// Fetch user details from main database
const userMap = new Map<
string,
{ email: string | null }
>();
if (userIds.length > 0) {
const userDetails = await primaryDb
.select({
userId: users.userId,
email: users.email
})
.from(users)
.where(inArray(users.userId, userIds));
for (const u of userDetails) {
userMap.set(u.userId, { email: u.email });
}
}
// Enrich logs with details
return logs.map((log) => ({
...log,
resourceName: log.siteResourceId
? resourceMap.get(log.siteResourceId)?.name ?? null
: null,
resourceNiceId: log.siteResourceId
? resourceMap.get(log.siteResourceId)?.niceId ?? null
: null,
siteName: log.siteId
? siteMap.get(log.siteId)?.name ?? null
: null,
siteNiceId: log.siteId
? siteMap.get(log.siteId)?.niceId ?? null
: null,
clientName: log.clientId
? clientMap.get(log.clientId)?.name ?? null
: null,
clientNiceId: log.clientId
? clientMap.get(log.clientId)?.niceId ?? null
: null,
clientType: log.clientId
? clientMap.get(log.clientId)?.type ?? null
: null,
userEmail: log.userId
? userMap.get(log.userId)?.email ?? null
: null
}));
}
async function queryUniqueFilterAttributes(
timeStart: number,
timeEnd: number,
orgId: string
) {
const baseConditions = and(
gt(connectionAuditLog.startedAt, timeStart),
lt(connectionAuditLog.startedAt, timeEnd),
eq(connectionAuditLog.orgId, orgId)
);
// Get unique protocols
const uniqueProtocols = await logsDb
.selectDistinct({
protocol: connectionAuditLog.protocol
})
.from(connectionAuditLog)
.where(baseConditions);
// Get unique destination addresses
const uniqueDestAddrs = await logsDb
.selectDistinct({
destAddr: connectionAuditLog.destAddr
})
.from(connectionAuditLog)
.where(baseConditions);
// Get unique client IDs
const uniqueClients = await logsDb
.selectDistinct({
clientId: connectionAuditLog.clientId
})
.from(connectionAuditLog)
.where(baseConditions);
// Get unique resource IDs
const uniqueResources = await logsDb
.selectDistinct({
siteResourceId: connectionAuditLog.siteResourceId
})
.from(connectionAuditLog)
.where(baseConditions);
// Get unique user IDs
const uniqueUsers = await logsDb
.selectDistinct({
userId: connectionAuditLog.userId
})
.from(connectionAuditLog)
.where(baseConditions);
// Enrich client IDs with names from main database
const clientIds = uniqueClients
.map((row) => row.clientId)
.filter((id): id is number => id !== null);
let clientsWithNames: Array<{ id: number; name: string }> = [];
if (clientIds.length > 0) {
const clientDetails = await primaryDb
.select({
clientId: clients.clientId,
name: clients.name
})
.from(clients)
.where(inArray(clients.clientId, clientIds));
clientsWithNames = clientDetails.map((c) => ({
id: c.clientId,
name: c.name
}));
}
// Enrich resource IDs with names from main database
const resourceIds = uniqueResources
.map((row) => row.siteResourceId)
.filter((id): id is number => id !== null);
let resourcesWithNames: Array<{ id: number; name: string | null }> = [];
if (resourceIds.length > 0) {
const resourceDetails = await primaryDb
.select({
siteResourceId: siteResources.siteResourceId,
name: siteResources.name
})
.from(siteResources)
.where(inArray(siteResources.siteResourceId, resourceIds));
resourcesWithNames = resourceDetails.map((r) => ({
id: r.siteResourceId,
name: r.name
}));
}
// Enrich user IDs with emails from main database
const userIdsList = uniqueUsers
.map((row) => row.userId)
.filter((id): id is string => id !== null);
let usersWithEmails: Array<{ id: string; email: string | null }> = [];
if (userIdsList.length > 0) {
const userDetails = await primaryDb
.select({
userId: users.userId,
email: users.email
})
.from(users)
.where(inArray(users.userId, userIdsList));
usersWithEmails = userDetails.map((u) => ({
id: u.userId,
email: u.email
}));
}
return {
protocols: uniqueProtocols
.map((row) => row.protocol)
.filter((protocol): protocol is string => protocol !== null),
destAddrs: uniqueDestAddrs
.map((row) => row.destAddr)
.filter((addr): addr is string => addr !== null),
clients: clientsWithNames,
resources: resourcesWithNames,
users: usersWithEmails
};
}
registry.registerPath({
method: "get",
path: "/org/{orgId}/logs/connection",
description: "Query the connection audit log for an organization",
tags: [OpenAPITags.Logs],
request: {
query: queryConnectionAuditLogsQuery,
params: queryConnectionAuditLogsParams
},
responses: {}
});
export async function queryConnectionAuditLogs(
req: Request,
res: Response,
next: NextFunction
): Promise<any> {
try {
const parsedQuery = queryConnectionAuditLogsQuery.safeParse(req.query);
if (!parsedQuery.success) {
return next(
createHttpError(
HttpCode.BAD_REQUEST,
fromError(parsedQuery.error)
)
);
}
const parsedParams = queryConnectionAuditLogsParams.safeParse(
req.params
);
if (!parsedParams.success) {
return next(
createHttpError(
HttpCode.BAD_REQUEST,
fromError(parsedParams.error)
)
);
}
const data = { ...parsedQuery.data, ...parsedParams.data };
const baseQuery = queryConnection(data);
const logsRaw = await baseQuery.limit(data.limit).offset(data.offset);
// Enrich with resource, site, client, and user details
const log = await enrichWithDetails(logsRaw);
const totalCountResult = await countConnectionQuery(data);
const totalCount = totalCountResult[0].count;
const filterAttributes = await queryUniqueFilterAttributes(
data.timeStart,
data.timeEnd,
data.orgId
);
return response<QueryConnectionAuditLogResponse>(res, {
data: {
log: log,
pagination: {
total: totalCount,
limit: data.limit,
offset: data.offset
},
filterAttributes
},
success: true,
error: false,
message: "Connection audit logs retrieved successfully",
status: HttpCode.OK
});
} catch (error) {
logger.error(error);
return next(
createHttpError(HttpCode.INTERNAL_SERVER_ERROR, "An error occurred")
);
}
}

View File

@@ -26,12 +26,9 @@ import {
orgs,
resources,
roles,
siteResources,
userOrgRoles,
siteProvisioningKeyOrg,
siteProvisioningKeys,
siteResources
} from "@server/db";
import { and, eq } from "drizzle-orm";
import { eq } from "drizzle-orm";
/**
* Get the maximum allowed retention days for a given tier
@@ -120,18 +117,6 @@ async function capRetentionDays(
);
}
// Cap action log retention if it exceeds the limit
if (
org.settingsLogRetentionDaysConnection !== null &&
org.settingsLogRetentionDaysConnection > maxRetentionDays
) {
updates.settingsLogRetentionDaysConnection = maxRetentionDays;
needsUpdate = true;
logger.info(
`Capping connection log retention from ${org.settingsLogRetentionDaysConnection} to ${maxRetentionDays} days for org ${orgId}`
);
}
// Apply updates if needed
if (needsUpdate) {
await db.update(orgs).set(updates).where(eq(orgs.orgId, orgId));
@@ -274,10 +259,6 @@ async function disableFeature(
await disableActionLogs(orgId);
break;
case TierFeature.ConnectionLogs:
await disableConnectionLogs(orgId);
break;
case TierFeature.RotateCredentials:
await disableRotateCredentials(orgId);
break;
@@ -310,14 +291,6 @@ async function disableFeature(
await disableSshPam(orgId);
break;
case TierFeature.FullRbac:
await disableFullRbac(orgId);
break;
case TierFeature.SiteProvisioningKeys:
await disableSiteProvisioningKeys(orgId);
break;
default:
logger.warn(
`Unknown feature ${feature} for org ${orgId}, skipping`
@@ -353,61 +326,6 @@ async function disableSshPam(orgId: string): Promise<void> {
);
}
async function disableFullRbac(orgId: string): Promise<void> {
logger.info(`Disabled full RBAC for org ${orgId}`);
}
async function disableSiteProvisioningKeys(orgId: string): Promise<void> {
const rows = await db
.select({
siteProvisioningKeyId:
siteProvisioningKeyOrg.siteProvisioningKeyId
})
.from(siteProvisioningKeyOrg)
.where(eq(siteProvisioningKeyOrg.orgId, orgId));
for (const { siteProvisioningKeyId } of rows) {
await db.transaction(async (trx) => {
await trx
.delete(siteProvisioningKeyOrg)
.where(
and(
eq(
siteProvisioningKeyOrg.siteProvisioningKeyId,
siteProvisioningKeyId
),
eq(siteProvisioningKeyOrg.orgId, orgId)
)
);
const remaining = await trx
.select()
.from(siteProvisioningKeyOrg)
.where(
eq(
siteProvisioningKeyOrg.siteProvisioningKeyId,
siteProvisioningKeyId
)
);
if (remaining.length === 0) {
await trx
.delete(siteProvisioningKeys)
.where(
eq(
siteProvisioningKeys.siteProvisioningKeyId,
siteProvisioningKeyId
)
);
}
});
}
logger.info(
`Removed site provisioning keys for org ${orgId} after tier downgrade`
);
}
async function disableLoginPageBranding(orgId: string): Promise<void> {
const [existingBranding] = await db
.select()
@@ -474,15 +392,6 @@ async function disableActionLogs(orgId: string): Promise<void> {
logger.info(`Disabled action logs for org ${orgId}`);
}
async function disableConnectionLogs(orgId: string): Promise<void> {
await db
.update(orgs)
.set({ settingsLogRetentionDaysConnection: 0 })
.where(eq(orgs.orgId, orgId));
logger.info(`Disabled connection logs for org ${orgId}`);
}
async function disableRotateCredentials(orgId: string): Promise<void> {}
async function disableMaintencePage(orgId: string): Promise<void> {

View File

@@ -26,8 +26,6 @@ import * as misc from "#private/routers/misc";
import * as reKey from "#private/routers/re-key";
import * as approval from "#private/routers/approvals";
import * as ssh from "#private/routers/ssh";
import * as user from "#private/routers/user";
import * as siteProvisioning from "#private/routers/siteProvisioning";
import {
verifyOrgAccess,
@@ -35,11 +33,7 @@ import {
verifyUserIsServerAdmin,
verifySiteAccess,
verifyClientAccess,
verifyLimits,
verifyRoleAccess,
verifyUserAccess,
verifyUserCanSetUserOrgRoles,
verifySiteProvisioningKeyAccess
verifyLimits
} from "@server/middlewares";
import { ActionsEnum } from "@server/auth/actions";
import {
@@ -484,25 +478,6 @@ authenticated.get(
logs.exportAccessAuditLogs
);
authenticated.get(
"/org/:orgId/logs/connection",
verifyValidLicense,
verifyValidSubscription(tierMatrix.connectionLogs),
verifyOrgAccess,
verifyUserHasAction(ActionsEnum.exportLogs),
logs.queryConnectionAuditLogs
);
authenticated.get(
"/org/:orgId/logs/connection/export",
verifyValidLicense,
verifyValidSubscription(tierMatrix.logExport),
verifyOrgAccess,
verifyUserHasAction(ActionsEnum.exportLogs),
logActionAudit(ActionsEnum.exportLogs),
logs.exportConnectionAuditLogs
);
authenticated.post(
"/re-key/:clientId/regenerate-client-secret",
verifyClientAccess, // this is first to set the org id
@@ -543,75 +518,3 @@ authenticated.post(
// logActionAudit(ActionsEnum.signSshKey), // it is handled inside of the function below so we can include more metadata
ssh.signSshKey
);
authenticated.post(
"/user/:userId/add-role/:roleId",
verifyRoleAccess,
verifyUserAccess,
verifyLimits,
verifyUserHasAction(ActionsEnum.addUserRole),
logActionAudit(ActionsEnum.addUserRole),
user.addUserRole
);
authenticated.delete(
"/user/:userId/remove-role/:roleId",
verifyRoleAccess,
verifyUserAccess,
verifyLimits,
verifyUserHasAction(ActionsEnum.removeUserRole),
logActionAudit(ActionsEnum.removeUserRole),
user.removeUserRole
);
authenticated.post(
"/user/:userId/org/:orgId/roles",
verifyOrgAccess,
verifyUserAccess,
verifyLimits,
verifyUserCanSetUserOrgRoles(),
logActionAudit(ActionsEnum.setUserOrgRoles),
user.setUserOrgRoles
);
authenticated.put(
"/org/:orgId/site-provisioning-key",
verifyValidLicense,
verifyValidSubscription(tierMatrix.siteProvisioningKeys),
verifyOrgAccess,
verifyLimits,
verifyUserHasAction(ActionsEnum.createSiteProvisioningKey),
logActionAudit(ActionsEnum.createSiteProvisioningKey),
siteProvisioning.createSiteProvisioningKey
);
authenticated.get(
"/org/:orgId/site-provisioning-keys",
verifyValidLicense,
verifyValidSubscription(tierMatrix.siteProvisioningKeys),
verifyOrgAccess,
verifyUserHasAction(ActionsEnum.listSiteProvisioningKeys),
siteProvisioning.listSiteProvisioningKeys
);
authenticated.delete(
"/org/:orgId/site-provisioning-key/:siteProvisioningKeyId",
verifyValidLicense,
verifyValidSubscription(tierMatrix.siteProvisioningKeys),
verifyOrgAccess,
verifySiteProvisioningKeyAccess,
verifyUserHasAction(ActionsEnum.deleteSiteProvisioningKey),
logActionAudit(ActionsEnum.deleteSiteProvisioningKey),
siteProvisioning.deleteSiteProvisioningKey
);
authenticated.patch(
"/org/:orgId/site-provisioning-key/:siteProvisioningKeyId",
verifyValidLicense,
verifyValidSubscription(tierMatrix.siteProvisioningKeys),
verifyOrgAccess,
verifySiteProvisioningKeyAccess,
verifyUserHasAction(ActionsEnum.updateSiteProvisioningKey),
logActionAudit(ActionsEnum.updateSiteProvisioningKey),
siteProvisioning.updateSiteProvisioningKey
);

View File

@@ -52,9 +52,7 @@ import {
userOrgs,
roleResources,
userResources,
resourceRules,
userOrgRoles,
roles
resourceRules
} from "@server/db";
import { eq, and, inArray, isNotNull, ne } from "drizzle-orm";
import { response } from "@server/lib/response";
@@ -106,13 +104,6 @@ const getUserOrgSessionVerifySchema = z.strictObject({
sessionId: z.string().min(1, "Session ID is required")
});
const getRoleNameParamsSchema = z.strictObject({
roleId: z
.string()
.transform(Number)
.pipe(z.int().positive("Role ID must be a positive integer"))
});
const getRoleResourceAccessParamsSchema = z.strictObject({
roleId: z
.string()
@@ -124,23 +115,6 @@ const getRoleResourceAccessParamsSchema = z.strictObject({
.pipe(z.int().positive("Resource ID must be a positive integer"))
});
const getResourceAccessParamsSchema = z.strictObject({
resourceId: z
.string()
.transform(Number)
.pipe(z.int().positive("Resource ID must be a positive integer"))
});
const getResourceAccessQuerySchema = z.strictObject({
roleIds: z
.union([z.array(z.string()), z.string()])
.transform((val) =>
(Array.isArray(val) ? val : [val])
.map(Number)
.filter((n) => !isNaN(n))
)
});
const getUserResourceAccessParamsSchema = z.strictObject({
userId: z.string().min(1, "User ID is required"),
resourceId: z
@@ -786,7 +760,7 @@ hybridRouter.get(
// Get user organization role
hybridRouter.get(
"/user/:userId/org/:orgId/roles",
"/user/:userId/org/:orgId/role",
async (req: Request, res: Response, next: NextFunction) => {
try {
const parsedParams = getUserOrgRoleParamsSchema.safeParse(
@@ -822,129 +796,23 @@ hybridRouter.get(
);
}
const userOrgRoleRows = await db
.select({ roleId: userOrgRoles.roleId, roleName: roles.name })
.from(userOrgRoles)
.innerJoin(roles, eq(roles.roleId, userOrgRoles.roleId))
const userOrgRole = await db
.select()
.from(userOrgs)
.where(
and(
eq(userOrgRoles.userId, userId),
eq(userOrgRoles.orgId, orgId)
)
);
logger.debug(`User ${userId} has roles in org ${orgId}:`, userOrgRoleRows);
return response<{ roleId: number, roleName: string }[]>(res, {
data: userOrgRoleRows,
success: true,
error: false,
message:
userOrgRoleRows.length > 0
? "User org roles retrieved successfully"
: "User has no roles in this organization",
status: HttpCode.OK
});
} catch (error) {
logger.error(error);
return next(
createHttpError(
HttpCode.INTERNAL_SERVER_ERROR,
"Failed to get user org role"
and(eq(userOrgs.userId, userId), eq(userOrgs.orgId, orgId))
)
);
}
}
);
.limit(1);
// DEPRICATED Get user organization role
// used for backward compatibility with old remote nodes
hybridRouter.get(
"/user/:userId/org/:orgId/role", // <- note the missing s
async (req: Request, res: Response, next: NextFunction) => {
try {
const parsedParams = getUserOrgRoleParamsSchema.safeParse(
req.params
);
if (!parsedParams.success) {
return next(
createHttpError(
HttpCode.BAD_REQUEST,
fromError(parsedParams.error).toString()
)
);
}
const result = userOrgRole.length > 0 ? userOrgRole[0] : null;
const { userId, orgId } = parsedParams.data;
const remoteExitNode = req.remoteExitNode;
if (!remoteExitNode || !remoteExitNode.exitNodeId) {
return next(
createHttpError(
HttpCode.BAD_REQUEST,
"Remote exit node not found"
)
);
}
if (await checkExitNodeOrg(remoteExitNode.exitNodeId, orgId)) {
return next(
createHttpError(
HttpCode.UNAUTHORIZED,
"User is not authorized to access this organization"
)
);
}
// get the roles on the user
const userOrgRoleRows = await db
.select({ roleId: userOrgRoles.roleId })
.from(userOrgRoles)
.where(
and(
eq(userOrgRoles.userId, userId),
eq(userOrgRoles.orgId, orgId)
)
);
const roleIds = userOrgRoleRows.map((r) => r.roleId);
let roleId: number | null = null;
if (userOrgRoleRows.length === 0) {
// User has no roles in this organization
roleId = null;
} else if (userOrgRoleRows.length === 1) {
// User has exactly one role, return it
roleId = userOrgRoleRows[0].roleId;
} else {
// User has multiple roles
// Check if any of these roles are also assigned to a resource
// If we find a match, prefer that role; otherwise return the first role
// Get all resources that have any of these roles assigned
const roleResourceMatches = await db
.select({ roleId: roleResources.roleId })
.from(roleResources)
.where(inArray(roleResources.roleId, roleIds))
.limit(1);
if (roleResourceMatches.length > 0) {
// Return the first role that's also on a resource
roleId = roleResourceMatches[0].roleId;
} else {
// No resource match found, return the first role
roleId = userOrgRoleRows[0].roleId;
}
}
return response<{ roleId: number | null }>(res, {
data: { roleId },
return response<typeof userOrgs.$inferSelect | null>(res, {
data: result,
success: true,
error: false,
message:
roleIds.length > 0
? "User org roles retrieved successfully"
: "User has no roles in this organization",
message: result
? "User org role retrieved successfully"
: "User org role not found",
status: HttpCode.OK
});
} catch (error) {
@@ -1022,60 +890,6 @@ hybridRouter.get(
}
);
// Get role name by ID
hybridRouter.get(
"/role/:roleId/name",
async (req: Request, res: Response, next: NextFunction) => {
try {
const parsedParams = getRoleNameParamsSchema.safeParse(req.params);
if (!parsedParams.success) {
return next(
createHttpError(
HttpCode.BAD_REQUEST,
fromError(parsedParams.error).toString()
)
);
}
const { roleId } = parsedParams.data;
const remoteExitNode = req.remoteExitNode;
if (!remoteExitNode?.exitNodeId) {
return next(
createHttpError(
HttpCode.BAD_REQUEST,
"Remote exit node not found"
)
);
}
const [role] = await db
.select({ name: roles.name })
.from(roles)
.where(eq(roles.roleId, roleId))
.limit(1);
return response<string | null>(res, {
data: role?.name ?? null,
success: true,
error: false,
message: role
? "Role name retrieved successfully"
: "Role not found",
status: HttpCode.OK
});
} catch (error) {
logger.error(error);
return next(
createHttpError(
HttpCode.INTERNAL_SERVER_ERROR,
"Failed to get role name"
)
);
}
}
);
// Check if role has access to resource
hybridRouter.get(
"/role/:roleId/resource/:resourceId/access",
@@ -1161,101 +975,6 @@ hybridRouter.get(
}
);
// Check if role has access to resource
hybridRouter.get(
"/resource/:resourceId/access",
async (req: Request, res: Response, next: NextFunction) => {
try {
const parsedParams = getResourceAccessParamsSchema.safeParse(
req.params
);
if (!parsedParams.success) {
return next(
createHttpError(
HttpCode.BAD_REQUEST,
fromError(parsedParams.error).toString()
)
);
}
const { resourceId } = parsedParams.data;
const parsedQuery = getResourceAccessQuerySchema.safeParse(
req.query
);
const roleIds = parsedQuery.success ? parsedQuery.data.roleIds : [];
const remoteExitNode = req.remoteExitNode;
if (!remoteExitNode?.exitNodeId) {
return next(
createHttpError(
HttpCode.BAD_REQUEST,
"Remote exit node not found"
)
);
}
const [resource] = await db
.select()
.from(resources)
.where(eq(resources.resourceId, resourceId))
.limit(1);
if (
await checkExitNodeOrg(
remoteExitNode.exitNodeId,
resource.orgId
)
) {
// If the exit node is not allowed for the org, return an error
return next(
createHttpError(
HttpCode.FORBIDDEN,
"Exit node not allowed for this organization"
)
);
}
const roleResourceAccess = await db
.select({
resourceId: roleResources.resourceId,
roleId: roleResources.roleId
})
.from(roleResources)
.where(
and(
eq(roleResources.resourceId, resourceId),
inArray(roleResources.roleId, roleIds)
)
);
const result =
roleResourceAccess.length > 0 ? roleResourceAccess : null;
return response<{ resourceId: number; roleId: number }[] | null>(
res,
{
data: result,
success: true,
error: false,
message: result
? "Role resource access retrieved successfully"
: "Role resource access not found",
status: HttpCode.OK
}
);
} catch (error) {
logger.error(error);
return next(
createHttpError(
HttpCode.INTERNAL_SERVER_ERROR,
"Failed to get role resource access"
)
);
}
}
);
// Check if user has direct access to resource
hybridRouter.get(
"/user/:userId/resource/:resourceId/access",
@@ -2154,8 +1873,7 @@ hybridRouter.post(
// userAgent: data.userAgent, // TODO: add this
// headers: data.body.headers,
// query: data.body.query,
originalRequestURL:
sanitizeString(logEntry.originalRequestURL) ?? "",
originalRequestURL: sanitizeString(logEntry.originalRequestURL) ?? "",
scheme: sanitizeString(logEntry.scheme) ?? "",
host: sanitizeString(logEntry.host) ?? "",
path: sanitizeString(logEntry.path) ?? "",

View File

@@ -20,11 +20,8 @@ import {
verifyApiKeyIsRoot,
verifyApiKeyOrgAccess,
verifyApiKeyIdpAccess,
verifyApiKeyRoleAccess,
verifyApiKeyUserAccess,
verifyLimits
} from "@server/middlewares";
import * as user from "#private/routers/user";
import {
verifyValidSubscription,
verifyValidLicense
@@ -94,25 +91,6 @@ authenticated.get(
logs.exportAccessAuditLogs
);
authenticated.get(
"/org/:orgId/logs/connection",
verifyValidLicense,
verifyValidSubscription(tierMatrix.connectionLogs),
verifyApiKeyOrgAccess,
verifyApiKeyHasAction(ActionsEnum.exportLogs),
logs.queryConnectionAuditLogs
);
authenticated.get(
"/org/:orgId/logs/connection/export",
verifyValidLicense,
verifyValidSubscription(tierMatrix.logExport),
verifyApiKeyOrgAccess,
verifyApiKeyHasAction(ActionsEnum.exportLogs),
logActionAudit(ActionsEnum.exportLogs),
logs.exportConnectionAuditLogs
);
authenticated.put(
"/org/:orgId/idp/oidc",
verifyValidLicense,
@@ -162,23 +140,3 @@ authenticated.get(
verifyApiKeyHasAction(ActionsEnum.listIdps),
orgIdp.listOrgIdps
);
authenticated.post(
"/user/:userId/add-role/:roleId",
verifyApiKeyRoleAccess,
verifyApiKeyUserAccess,
verifyLimits,
verifyApiKeyHasAction(ActionsEnum.addUserRole),
logActionAudit(ActionsEnum.addUserRole),
user.addUserRole
);
authenticated.delete(
"/user/:userId/remove-role/:roleId",
verifyApiKeyRoleAccess,
verifyApiKeyUserAccess,
verifyLimits,
verifyApiKeyHasAction(ActionsEnum.removeUserRole),
logActionAudit(ActionsEnum.removeUserRole),
user.removeUserRole
);

View File

@@ -1,394 +0,0 @@
import { db, logsDb } from "@server/db";
import { MessageHandler } from "@server/routers/ws";
import { connectionAuditLog, sites, Newt, clients, orgs } from "@server/db";
import { and, eq, lt, inArray } from "drizzle-orm";
import logger from "@server/logger";
import { inflate } from "zlib";
import { promisify } from "util";
import { calculateCutoffTimestamp } from "@server/lib/cleanupLogs";
const zlibInflate = promisify(inflate);
// Retry configuration for deadlock handling
const MAX_RETRIES = 3;
const BASE_DELAY_MS = 50;
// How often to flush accumulated connection log data to the database
const FLUSH_INTERVAL_MS = 30_000; // 30 seconds
// Maximum number of records to buffer before forcing a flush
const MAX_BUFFERED_RECORDS = 500;
// Maximum number of records to insert in a single batch
const INSERT_BATCH_SIZE = 100;
interface ConnectionSessionData {
sessionId: string;
resourceId: number;
sourceAddr: string;
destAddr: string;
protocol: string;
startedAt: string; // ISO 8601 timestamp
endedAt?: string; // ISO 8601 timestamp
bytesTx?: number;
bytesRx?: number;
}
interface ConnectionLogRecord {
sessionId: string;
siteResourceId: number;
orgId: string;
siteId: number;
clientId: number | null;
userId: string | null;
sourceAddr: string;
destAddr: string;
protocol: string;
startedAt: number; // epoch seconds
endedAt: number | null;
bytesTx: number | null;
bytesRx: number | null;
}
// In-memory buffer of records waiting to be flushed
let buffer: ConnectionLogRecord[] = [];
/**
* Check if an error is a deadlock error
*/
function isDeadlockError(error: any): boolean {
return (
error?.code === "40P01" ||
error?.cause?.code === "40P01" ||
(error?.message && error.message.includes("deadlock"))
);
}
/**
* Execute a function with retry logic for deadlock handling
*/
async function withDeadlockRetry<T>(
operation: () => Promise<T>,
context: string
): Promise<T> {
let attempt = 0;
while (true) {
try {
return await operation();
} catch (error: any) {
if (isDeadlockError(error) && attempt < MAX_RETRIES) {
attempt++;
const baseDelay = Math.pow(2, attempt - 1) * BASE_DELAY_MS;
const jitter = Math.random() * baseDelay;
const delay = baseDelay + jitter;
logger.warn(
`Deadlock detected in ${context}, retrying attempt ${attempt}/${MAX_RETRIES} after ${delay.toFixed(0)}ms`
);
await new Promise((resolve) => setTimeout(resolve, delay));
continue;
}
throw error;
}
}
}
/**
* Decompress a base64-encoded zlib-compressed string into parsed JSON.
*/
async function decompressConnectionLog(
compressed: string
): Promise<ConnectionSessionData[]> {
const compressedBuffer = Buffer.from(compressed, "base64");
const decompressed = await zlibInflate(compressedBuffer);
const jsonString = decompressed.toString("utf-8");
const parsed = JSON.parse(jsonString);
if (!Array.isArray(parsed)) {
throw new Error("Decompressed connection log data is not an array");
}
return parsed;
}
/**
* Convert an ISO 8601 timestamp string to epoch seconds.
* Returns null if the input is falsy.
*/
function toEpochSeconds(isoString: string | undefined | null): number | null {
if (!isoString) {
return null;
}
const ms = new Date(isoString).getTime();
if (isNaN(ms)) {
return null;
}
return Math.floor(ms / 1000);
}
/**
* Flush all buffered connection log records to the database.
*
* Swaps out the buffer before writing so that any records added during the
* flush are captured in the new buffer rather than being lost. Entries that
* fail to write are re-queued back into the buffer so they will be retried
* on the next flush.
*
* This function is exported so that the application's graceful-shutdown
* cleanup handler can call it before the process exits.
*/
export async function flushConnectionLogToDb(): Promise<void> {
if (buffer.length === 0) {
return;
}
// Atomically swap out the buffer so new data keeps flowing in
const snapshot = buffer;
buffer = [];
logger.debug(
`Flushing ${snapshot.length} connection log record(s) to the database`
);
// Insert in batches to avoid overly large SQL statements
for (let i = 0; i < snapshot.length; i += INSERT_BATCH_SIZE) {
const batch = snapshot.slice(i, i + INSERT_BATCH_SIZE);
try {
await withDeadlockRetry(async () => {
await logsDb.insert(connectionAuditLog).values(batch);
}, `flush connection log batch (${batch.length} records)`);
} catch (error) {
logger.error(
`Failed to flush connection log batch of ${batch.length} records:`,
error
);
// Re-queue the failed batch so it is retried on the next flush
buffer = [...batch, ...buffer];
// Cap buffer to prevent unbounded growth if DB is unreachable
if (buffer.length > MAX_BUFFERED_RECORDS * 5) {
const dropped = buffer.length - MAX_BUFFERED_RECORDS * 5;
buffer = buffer.slice(0, MAX_BUFFERED_RECORDS * 5);
logger.warn(
`Connection log buffer overflow, dropped ${dropped} oldest records`
);
}
// Stop trying further batches from this snapshot — they'll be
// picked up by the next flush via the re-queued records above
const remaining = snapshot.slice(i + INSERT_BATCH_SIZE);
if (remaining.length > 0) {
buffer = [...remaining, ...buffer];
}
break;
}
}
}
const flushTimer = setInterval(async () => {
try {
await flushConnectionLogToDb();
} catch (error) {
logger.error(
"Unexpected error during periodic connection log flush:",
error
);
}
}, FLUSH_INTERVAL_MS);
// Calling unref() means this timer will not keep the Node.js event loop alive
// on its own — the process can still exit normally when there is no other work
// left. The graceful-shutdown path will call flushConnectionLogToDb() explicitly
// before process.exit(), so no data is lost.
flushTimer.unref();
export async function cleanUpOldLogs(orgId: string, retentionDays: number) {
const cutoffTimestamp = calculateCutoffTimestamp(retentionDays);
try {
await logsDb
.delete(connectionAuditLog)
.where(
and(
lt(connectionAuditLog.startedAt, cutoffTimestamp),
eq(connectionAuditLog.orgId, orgId)
)
);
// logger.debug(
// `Cleaned up connection audit logs older than ${retentionDays} days`
// );
} catch (error) {
logger.error("Error cleaning up old connection audit logs:", error);
}
}
export const handleConnectionLogMessage: MessageHandler = async (context) => {
const { message, client } = context;
const newt = client as Newt;
if (!newt) {
logger.warn("Connection log received but no newt client in context");
return;
}
if (!newt.siteId) {
logger.warn("Connection log received but newt has no siteId");
return;
}
if (!message.data?.compressed) {
logger.warn("Connection log message missing compressed data");
return;
}
// Look up the org for this site
const [site] = await db
.select({ orgId: sites.orgId, orgSubnet: orgs.subnet })
.from(sites)
.innerJoin(orgs, eq(sites.orgId, orgs.orgId))
.where(eq(sites.siteId, newt.siteId));
if (!site) {
logger.warn(
`Connection log received but site ${newt.siteId} not found in database`
);
return;
}
const orgId = site.orgId;
// Extract the CIDR suffix (e.g. "/16") from the org subnet so we can
// reconstruct the exact subnet string stored on each client record.
const cidrSuffix = site.orgSubnet?.includes("/")
? site.orgSubnet.substring(site.orgSubnet.indexOf("/"))
: null;
let sessions: ConnectionSessionData[];
try {
sessions = await decompressConnectionLog(message.data.compressed);
} catch (error) {
logger.error("Failed to decompress connection log data:", error);
return;
}
if (sessions.length === 0) {
return;
}
logger.debug(`Sessions: ${JSON.stringify(sessions)}`)
// Build a map from sourceAddr → { clientId, userId } by querying clients
// whose subnet field matches exactly. Client subnets are stored with the
// org's CIDR suffix (e.g. "100.90.128.5/16"), so we reconstruct that from
// each unique sourceAddr + the org's CIDR suffix and do a targeted IN query.
const ipToClient = new Map<string, { clientId: number; userId: string | null }>();
if (cidrSuffix) {
// Collect unique source addresses so we only query for what we need
const uniqueSourceAddrs = new Set<string>();
for (const session of sessions) {
if (session.sourceAddr) {
uniqueSourceAddrs.add(session.sourceAddr);
}
}
if (uniqueSourceAddrs.size > 0) {
// Construct the exact subnet strings as stored in the DB
const subnetQueries = Array.from(uniqueSourceAddrs).map(
(addr) => {
// Strip port if present (e.g. "100.90.128.1:38004" → "100.90.128.1")
const ip = addr.includes(":") ? addr.split(":")[0] : addr;
return `${ip}${cidrSuffix}`;
}
);
logger.debug(`Subnet queries: ${JSON.stringify(subnetQueries)}`);
const matchedClients = await db
.select({
clientId: clients.clientId,
userId: clients.userId,
subnet: clients.subnet
})
.from(clients)
.where(
and(
eq(clients.orgId, orgId),
inArray(clients.subnet, subnetQueries)
)
);
for (const c of matchedClients) {
const ip = c.subnet.split("/")[0];
logger.debug(`Client ${c.clientId} subnet ${c.subnet} matches ${ip}`);
ipToClient.set(ip, { clientId: c.clientId, userId: c.userId });
}
}
}
// Convert to DB records and add to the buffer
for (const session of sessions) {
// Validate required fields
if (
!session.sessionId ||
!session.resourceId ||
!session.sourceAddr ||
!session.destAddr ||
!session.protocol
) {
logger.debug(
`Skipping connection log session with missing required fields: ${JSON.stringify(session)}`
);
continue;
}
const startedAt = toEpochSeconds(session.startedAt);
if (startedAt === null) {
logger.debug(
`Skipping connection log session with invalid startedAt: ${session.startedAt}`
);
continue;
}
// Match the source address to a client. The sourceAddr is the
// client's IP on the WireGuard network, which corresponds to the IP
// portion of the client's subnet CIDR (e.g. "100.90.128.5/24").
// Strip port if present (e.g. "100.90.128.1:38004" → "100.90.128.1")
const sourceIp = session.sourceAddr.includes(":") ? session.sourceAddr.split(":")[0] : session.sourceAddr;
const clientInfo = ipToClient.get(sourceIp) ?? null;
buffer.push({
sessionId: session.sessionId,
siteResourceId: session.resourceId,
orgId,
siteId: newt.siteId,
clientId: clientInfo?.clientId ?? null,
userId: clientInfo?.userId ?? null,
sourceAddr: session.sourceAddr,
destAddr: session.destAddr,
protocol: session.protocol,
startedAt,
endedAt: toEpochSeconds(session.endedAt),
bytesTx: session.bytesTx ?? null,
bytesRx: session.bytesRx ?? null
});
}
logger.debug(
`Buffered ${sessions.length} connection log session(s) from newt ${newt.newtId} (site ${newt.siteId})`
);
// If the buffer has grown large enough, trigger an immediate flush
if (buffer.length >= MAX_BUFFERED_RECORDS) {
// Fire and forget — errors are handled inside flushConnectionLogToDb
flushConnectionLogToDb().catch((error) => {
logger.error(
"Unexpected error during size-triggered connection log flush:",
error
);
});
}
};

View File

@@ -1 +0,0 @@
export * from "./handleConnectionLogMessage";

View File

@@ -14,7 +14,7 @@
import { Request, Response, NextFunction } from "express";
import { z } from "zod";
import { db } from "@server/db";
import { userOrgs, userOrgRoles, users, roles, orgs } from "@server/db";
import { userOrgs, users, roles, orgs } from "@server/db";
import { eq, and, or } from "drizzle-orm";
import response from "@server/lib/response";
import HttpCode from "@server/types/HttpCode";
@@ -95,14 +95,7 @@ async function getOrgAdmins(orgId: string) {
})
.from(userOrgs)
.innerJoin(users, eq(userOrgs.userId, users.userId))
.leftJoin(
userOrgRoles,
and(
eq(userOrgs.userId, userOrgRoles.userId),
eq(userOrgs.orgId, userOrgRoles.orgId)
)
)
.leftJoin(roles, eq(userOrgRoles.roleId, roles.roleId))
.leftJoin(roles, eq(userOrgs.roleId, roles.roleId))
.where(
and(
eq(userOrgs.orgId, orgId),
@@ -110,11 +103,8 @@ async function getOrgAdmins(orgId: string) {
)
);
// Dedupe by userId (user may have multiple roles)
const byUserId = new Map(
admins.map((a) => [a.userId, a])
);
const orgAdmins = Array.from(byUserId.values()).filter(
// Filter to only include users with verified emails
const orgAdmins = admins.filter(
(admin) => admin.email && admin.email.length > 0
);

View File

@@ -79,7 +79,7 @@ export async function createRemoteExitNode(
const { remoteExitNodeId, secret } = parsedBody.data;
if (req.user && (!req.userOrgRoleIds || req.userOrgRoleIds.length === 0)) {
if (req.user && !req.userOrgRoleId) {
return next(
createHttpError(HttpCode.FORBIDDEN, "User does not have a role")
);

View File

@@ -1,146 +0,0 @@
/*
* 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.
*/
import { NextFunction, Request, Response } from "express";
import { db, siteProvisioningKeyOrg, siteProvisioningKeys } from "@server/db";
import HttpCode from "@server/types/HttpCode";
import { z } from "zod";
import { fromError } from "zod-validation-error";
import createHttpError from "http-errors";
import response from "@server/lib/response";
import moment from "moment";
import {
generateId,
generateIdFromEntropySize
} from "@server/auth/sessions/app";
import logger from "@server/logger";
import { hashPassword } from "@server/auth/password";
import type { CreateSiteProvisioningKeyResponse } from "@server/routers/siteProvisioning/types";
const paramsSchema = z.object({
orgId: z.string().nonempty()
});
const bodySchema = z
.strictObject({
name: z.string().min(1).max(255),
maxBatchSize: z.union([
z.null(),
z.coerce.number().int().positive().max(1_000_000)
]),
validUntil: z.string().max(255).optional()
})
.superRefine((data, ctx) => {
const v = data.validUntil;
if (v == null || v.trim() === "") {
return;
}
if (Number.isNaN(Date.parse(v))) {
ctx.addIssue({
code: "custom",
message: "Invalid validUntil",
path: ["validUntil"]
});
}
});
export type CreateSiteProvisioningKeyBody = z.infer<typeof bodySchema>;
export async function createSiteProvisioningKey(
req: Request,
res: Response,
next: NextFunction
): Promise<any> {
const parsedParams = paramsSchema.safeParse(req.params);
if (!parsedParams.success) {
return next(
createHttpError(
HttpCode.BAD_REQUEST,
fromError(parsedParams.error).toString()
)
);
}
const parsedBody = bodySchema.safeParse(req.body);
if (!parsedBody.success) {
return next(
createHttpError(
HttpCode.BAD_REQUEST,
fromError(parsedBody.error).toString()
)
);
}
const { orgId } = parsedParams.data;
const { name, maxBatchSize } = parsedBody.data;
const vuRaw = parsedBody.data.validUntil;
const validUntil =
vuRaw == null || vuRaw.trim() === ""
? null
: new Date(Date.parse(vuRaw)).toISOString();
const siteProvisioningKeyId = `spk-${generateId(15)}`;
const siteProvisioningKey = generateIdFromEntropySize(25);
const siteProvisioningKeyHash = await hashPassword(siteProvisioningKey);
const lastChars = siteProvisioningKey.slice(-4);
const createdAt = moment().toISOString();
const provisioningKey = `${siteProvisioningKeyId}.${siteProvisioningKey}`;
await db.transaction(async (trx) => {
await trx.insert(siteProvisioningKeys).values({
siteProvisioningKeyId,
name,
siteProvisioningKeyHash,
createdAt,
lastChars,
lastUsed: null,
maxBatchSize,
numUsed: 0,
validUntil
});
await trx.insert(siteProvisioningKeyOrg).values({
siteProvisioningKeyId,
orgId
});
});
try {
return response<CreateSiteProvisioningKeyResponse>(res, {
data: {
siteProvisioningKeyId,
orgId,
name,
siteProvisioningKey: provisioningKey,
lastChars,
createdAt,
lastUsed: null,
maxBatchSize,
numUsed: 0,
validUntil
},
success: true,
error: false,
message: "Site provisioning key created",
status: HttpCode.CREATED
});
} catch (e) {
logger.error(e);
return next(
createHttpError(
HttpCode.INTERNAL_SERVER_ERROR,
"Failed to create site provisioning key"
)
);
}
}

View File

@@ -1,129 +0,0 @@
/*
* 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.
*/
import { Request, Response, NextFunction } from "express";
import { z } from "zod";
import {
db,
siteProvisioningKeyOrg,
siteProvisioningKeys
} from "@server/db";
import { and, 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";
const paramsSchema = z.object({
siteProvisioningKeyId: z.string().nonempty(),
orgId: z.string().nonempty()
});
export async function deleteSiteProvisioningKey(
req: Request,
res: Response,
next: NextFunction
): Promise<any> {
try {
const parsedParams = paramsSchema.safeParse(req.params);
if (!parsedParams.success) {
return next(
createHttpError(
HttpCode.BAD_REQUEST,
fromError(parsedParams.error).toString()
)
);
}
const { siteProvisioningKeyId, orgId } = parsedParams.data;
const [row] = await db
.select()
.from(siteProvisioningKeys)
.where(
eq(
siteProvisioningKeys.siteProvisioningKeyId,
siteProvisioningKeyId
)
)
.innerJoin(
siteProvisioningKeyOrg,
and(
eq(
siteProvisioningKeys.siteProvisioningKeyId,
siteProvisioningKeyOrg.siteProvisioningKeyId
),
eq(siteProvisioningKeyOrg.orgId, orgId)
)
)
.limit(1);
if (!row) {
return next(
createHttpError(
HttpCode.NOT_FOUND,
`Site provisioning key with ID ${siteProvisioningKeyId} not found`
)
);
}
await db.transaction(async (trx) => {
await trx
.delete(siteProvisioningKeyOrg)
.where(
and(
eq(
siteProvisioningKeyOrg.siteProvisioningKeyId,
siteProvisioningKeyId
),
eq(siteProvisioningKeyOrg.orgId, orgId)
)
);
const siteProvisioningKeyOrgs = await trx
.select()
.from(siteProvisioningKeyOrg)
.where(
eq(
siteProvisioningKeyOrg.siteProvisioningKeyId,
siteProvisioningKeyId
)
);
if (siteProvisioningKeyOrgs.length === 0) {
await trx
.delete(siteProvisioningKeys)
.where(
eq(
siteProvisioningKeys.siteProvisioningKeyId,
siteProvisioningKeyId
)
);
}
});
return response(res, {
data: null,
success: true,
error: false,
message: "Site provisioning key deleted successfully",
status: HttpCode.OK
});
} catch (error) {
logger.error(error);
return next(
createHttpError(HttpCode.INTERNAL_SERVER_ERROR, "An error occurred")
);
}
}

View File

@@ -1,17 +0,0 @@
/*
* 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.
*/
export * from "./createSiteProvisioningKey";
export * from "./listSiteProvisioningKeys";
export * from "./deleteSiteProvisioningKey";
export * from "./updateSiteProvisioningKey";

View File

@@ -1,126 +0,0 @@
/*
* 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.
*/
import {
db,
siteProvisioningKeyOrg,
siteProvisioningKeys
} from "@server/db";
import logger from "@server/logger";
import HttpCode from "@server/types/HttpCode";
import response from "@server/lib/response";
import { NextFunction, Request, Response } from "express";
import createHttpError from "http-errors";
import { z } from "zod";
import { fromError } from "zod-validation-error";
import { eq } from "drizzle-orm";
import type { ListSiteProvisioningKeysResponse } from "@server/routers/siteProvisioning/types";
const paramsSchema = z.object({
orgId: z.string().nonempty()
});
const querySchema = z.object({
limit: z
.string()
.optional()
.default("1000")
.transform(Number)
.pipe(z.int().positive()),
offset: z
.string()
.optional()
.default("0")
.transform(Number)
.pipe(z.int().nonnegative())
});
function querySiteProvisioningKeys(orgId: string) {
return db
.select({
siteProvisioningKeyId:
siteProvisioningKeys.siteProvisioningKeyId,
orgId: siteProvisioningKeyOrg.orgId,
lastChars: siteProvisioningKeys.lastChars,
createdAt: siteProvisioningKeys.createdAt,
name: siteProvisioningKeys.name,
lastUsed: siteProvisioningKeys.lastUsed,
maxBatchSize: siteProvisioningKeys.maxBatchSize,
numUsed: siteProvisioningKeys.numUsed,
validUntil: siteProvisioningKeys.validUntil
})
.from(siteProvisioningKeyOrg)
.innerJoin(
siteProvisioningKeys,
eq(
siteProvisioningKeys.siteProvisioningKeyId,
siteProvisioningKeyOrg.siteProvisioningKeyId
)
)
.where(eq(siteProvisioningKeyOrg.orgId, orgId));
}
export async function listSiteProvisioningKeys(
req: Request,
res: Response,
next: NextFunction
): Promise<any> {
try {
const parsedParams = paramsSchema.safeParse(req.params);
if (!parsedParams.success) {
return next(
createHttpError(
HttpCode.BAD_REQUEST,
fromError(parsedParams.error)
)
);
}
const parsedQuery = querySchema.safeParse(req.query);
if (!parsedQuery.success) {
return next(
createHttpError(
HttpCode.BAD_REQUEST,
fromError(parsedQuery.error)
)
);
}
const { orgId } = parsedParams.data;
const { limit, offset } = parsedQuery.data;
const siteProvisioningKeysList = await querySiteProvisioningKeys(orgId)
.limit(limit)
.offset(offset);
return response<ListSiteProvisioningKeysResponse>(res, {
data: {
siteProvisioningKeys: siteProvisioningKeysList,
pagination: {
total: siteProvisioningKeysList.length,
limit,
offset
}
},
success: true,
error: false,
message: "Site provisioning keys retrieved successfully",
status: HttpCode.OK
});
} catch (error) {
logger.error(error);
return next(
createHttpError(HttpCode.INTERNAL_SERVER_ERROR, "An error occurred")
);
}
}

View File

@@ -1,199 +0,0 @@
/*
* 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.
*/
import { Request, Response, NextFunction } from "express";
import { z } from "zod";
import {
db,
siteProvisioningKeyOrg,
siteProvisioningKeys
} from "@server/db";
import { and, 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 type { UpdateSiteProvisioningKeyResponse } from "@server/routers/siteProvisioning/types";
const paramsSchema = z.object({
siteProvisioningKeyId: z.string().nonempty(),
orgId: z.string().nonempty()
});
const bodySchema = z
.strictObject({
maxBatchSize: z
.union([
z.null(),
z.coerce.number().int().positive().max(1_000_000)
])
.optional(),
validUntil: z.string().max(255).optional()
})
.superRefine((data, ctx) => {
if (
data.maxBatchSize === undefined &&
data.validUntil === undefined
) {
ctx.addIssue({
code: "custom",
message: "Provide maxBatchSize and/or validUntil",
path: ["maxBatchSize"]
});
}
const v = data.validUntil;
if (v == null || v.trim() === "") {
return;
}
if (Number.isNaN(Date.parse(v))) {
ctx.addIssue({
code: "custom",
message: "Invalid validUntil",
path: ["validUntil"]
});
}
});
export type UpdateSiteProvisioningKeyBody = z.infer<typeof bodySchema>;
export async function updateSiteProvisioningKey(
req: Request,
res: Response,
next: NextFunction
): Promise<any> {
try {
const parsedParams = paramsSchema.safeParse(req.params);
if (!parsedParams.success) {
return next(
createHttpError(
HttpCode.BAD_REQUEST,
fromError(parsedParams.error).toString()
)
);
}
const parsedBody = bodySchema.safeParse(req.body);
if (!parsedBody.success) {
return next(
createHttpError(
HttpCode.BAD_REQUEST,
fromError(parsedBody.error).toString()
)
);
}
const { siteProvisioningKeyId, orgId } = parsedParams.data;
const body = parsedBody.data;
const [row] = await db
.select()
.from(siteProvisioningKeys)
.where(
eq(
siteProvisioningKeys.siteProvisioningKeyId,
siteProvisioningKeyId
)
)
.innerJoin(
siteProvisioningKeyOrg,
and(
eq(
siteProvisioningKeys.siteProvisioningKeyId,
siteProvisioningKeyOrg.siteProvisioningKeyId
),
eq(siteProvisioningKeyOrg.orgId, orgId)
)
)
.limit(1);
if (!row) {
return next(
createHttpError(
HttpCode.NOT_FOUND,
`Site provisioning key with ID ${siteProvisioningKeyId} not found`
)
);
}
const setValues: {
maxBatchSize?: number | null;
validUntil?: string | null;
} = {};
if (body.maxBatchSize !== undefined) {
setValues.maxBatchSize = body.maxBatchSize;
}
if (body.validUntil !== undefined) {
setValues.validUntil =
body.validUntil.trim() === ""
? null
: new Date(Date.parse(body.validUntil)).toISOString();
}
await db
.update(siteProvisioningKeys)
.set(setValues)
.where(
eq(
siteProvisioningKeys.siteProvisioningKeyId,
siteProvisioningKeyId
)
);
const [updated] = await db
.select({
siteProvisioningKeyId:
siteProvisioningKeys.siteProvisioningKeyId,
name: siteProvisioningKeys.name,
lastChars: siteProvisioningKeys.lastChars,
createdAt: siteProvisioningKeys.createdAt,
lastUsed: siteProvisioningKeys.lastUsed,
maxBatchSize: siteProvisioningKeys.maxBatchSize,
numUsed: siteProvisioningKeys.numUsed,
validUntil: siteProvisioningKeys.validUntil
})
.from(siteProvisioningKeys)
.where(
eq(
siteProvisioningKeys.siteProvisioningKeyId,
siteProvisioningKeyId
)
)
.limit(1);
if (!updated) {
return next(
createHttpError(
HttpCode.INTERNAL_SERVER_ERROR,
"Failed to load updated site provisioning key"
)
);
}
return response<UpdateSiteProvisioningKeyResponse>(res, {
data: {
...updated,
orgId
},
success: true,
error: false,
message: "Site provisioning key updated successfully",
status: HttpCode.OK
});
} catch (error) {
logger.error(error);
return next(
createHttpError(HttpCode.INTERNAL_SERVER_ERROR, "An error occurred")
);
}
}

View File

@@ -24,7 +24,6 @@ import {
sites,
userOrgs
} from "@server/db";
import { logAccessAudit } from "#private/lib/logAccessAudit";
import { isLicensedOrSubscribed } from "#private/lib/isLicencedOrSubscribed";
import { tierMatrix } from "@server/lib/billing/tierMatrix";
import response from "@server/lib/response";
@@ -32,7 +31,7 @@ import HttpCode from "@server/types/HttpCode";
import createHttpError from "http-errors";
import logger from "@server/logger";
import { fromError } from "zod-validation-error";
import { and, eq, inArray, or } from "drizzle-orm";
import { eq, or, and } from "drizzle-orm";
import { canUserAccessSiteResource } from "@server/auth/canUserAccessSiteResource";
import { signPublicKey, getOrgCAKeys } from "@server/lib/sshCA";
import config from "@server/lib/config";
@@ -126,7 +125,7 @@ export async function signSshKey(
resource: resourceQueryString
} = parsedBody.data;
const userId = req.user?.userId;
const roleIds = req.userOrgRoleIds ?? [];
const roleId = req.userOrgRoleId!;
if (!userId) {
return next(
@@ -134,15 +133,6 @@ export async function signSshKey(
);
}
if (roleIds.length === 0) {
return next(
createHttpError(
HttpCode.FORBIDDEN,
"User has no role in organization"
)
);
}
const [userOrg] = await db
.select()
.from(userOrgs)
@@ -349,7 +339,7 @@ export async function signSshKey(
const hasAccess = await canUserAccessSiteResource({
userId: userId,
resourceId: resource.siteResourceId,
roleIds
roleId: roleId
});
if (!hasAccess) {
@@ -361,39 +351,28 @@ export async function signSshKey(
);
}
const roleRows = await db
const [roleRow] = await db
.select()
.from(roles)
.where(inArray(roles.roleId, roleIds));
.where(eq(roles.roleId, roleId))
.limit(1);
const parsedSudoCommands: string[] = [];
const parsedGroupsSet = new Set<string>();
let homedir: boolean | null = null;
const sudoModeOrder = { none: 0, commands: 1, all: 2 };
let sudoMode: "none" | "commands" | "all" = "none";
for (const roleRow of roleRows) {
try {
const cmds = JSON.parse(roleRow?.sshSudoCommands ?? "[]");
if (Array.isArray(cmds)) parsedSudoCommands.push(...cmds);
} catch {
// skip
}
try {
const grps = JSON.parse(roleRow?.sshUnixGroups ?? "[]");
if (Array.isArray(grps)) grps.forEach((g: string) => parsedGroupsSet.add(g));
} catch {
// skip
}
if (roleRow?.sshCreateHomeDir === true) homedir = true;
const m = roleRow?.sshSudoMode ?? "none";
if (sudoModeOrder[m as keyof typeof sudoModeOrder] > sudoModeOrder[sudoMode]) {
sudoMode = m as "none" | "commands" | "all";
}
let parsedSudoCommands: string[] = [];
let parsedGroups: string[] = [];
try {
parsedSudoCommands = JSON.parse(roleRow?.sshSudoCommands ?? "[]");
if (!Array.isArray(parsedSudoCommands)) parsedSudoCommands = [];
} catch {
parsedSudoCommands = [];
}
const parsedGroups = Array.from(parsedGroupsSet);
if (homedir === null && roleRows.length > 0) {
homedir = roleRows[0].sshCreateHomeDir ?? null;
try {
parsedGroups = JSON.parse(roleRow?.sshUnixGroups ?? "[]");
if (!Array.isArray(parsedGroups)) parsedGroups = [];
} catch {
parsedGroups = [];
}
const homedir = roleRow?.sshCreateHomeDir ?? null;
const sudoMode = roleRow?.sshSudoMode ?? "none";
// get the site
const [newt] = await db
@@ -484,24 +463,6 @@ export async function signSshKey(
})
});
await logAccessAudit({
action: true,
type: "ssh",
orgId: orgId,
siteResourceId: resource.siteResourceId,
user: req.user
? { username: req.user.username ?? "", userId: req.user.userId }
: undefined,
metadata: {
resourceName: resource.name,
siteId: resource.siteId,
sshUsername: usernameToUse,
sshHost: sshHost
},
userAgent: req.headers["user-agent"],
requestIp: req.ip
});
return response<SignSshKeyResponse>(res, {
data: {
certificate: cert.certificate,

View File

@@ -1,16 +0,0 @@
/*
* 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.
*/
export * from "./addUserRole";
export * from "./removeUserRole";
export * from "./setUserOrgRoles";

View File

@@ -1,171 +0,0 @@
/*
* 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.
*/
import { Request, Response, NextFunction } from "express";
import { z } from "zod";
import stoi from "@server/lib/stoi";
import { db } from "@server/db";
import { userOrgRoles, userOrgs, roles, clients } from "@server/db";
import { eq, and } from "drizzle-orm";
import response from "@server/lib/response";
import HttpCode from "@server/types/HttpCode";
import createHttpError from "http-errors";
import logger from "@server/logger";
import { fromError } from "zod-validation-error";
import { OpenAPITags, registry } from "@server/openApi";
import { rebuildClientAssociationsFromClient } from "@server/lib/rebuildClientAssociations";
const removeUserRoleParamsSchema = z.strictObject({
userId: z.string(),
roleId: z.string().transform(stoi).pipe(z.number())
});
registry.registerPath({
method: "delete",
path: "/user/{userId}/remove-role/{roleId}",
description:
"Remove a role from a user. User must have at least one role left in the org.",
tags: [OpenAPITags.Role, OpenAPITags.User],
request: {
params: removeUserRoleParamsSchema
},
responses: {}
});
export async function removeUserRole(
req: Request,
res: Response,
next: NextFunction
): Promise<any> {
try {
const parsedParams = removeUserRoleParamsSchema.safeParse(req.params);
if (!parsedParams.success) {
return next(
createHttpError(
HttpCode.BAD_REQUEST,
fromError(parsedParams.error).toString()
)
);
}
const { userId, roleId } = parsedParams.data;
if (req.user && !req.userOrg) {
return next(
createHttpError(
HttpCode.FORBIDDEN,
"You do not have access to this organization"
)
);
}
const [role] = await db
.select()
.from(roles)
.where(eq(roles.roleId, roleId))
.limit(1);
if (!role) {
return next(
createHttpError(HttpCode.BAD_REQUEST, "Invalid role ID")
);
}
const [existingUser] = await db
.select()
.from(userOrgs)
.where(
and(eq(userOrgs.userId, userId), eq(userOrgs.orgId, role.orgId))
)
.limit(1);
if (!existingUser) {
return next(
createHttpError(
HttpCode.NOT_FOUND,
"User not found or does not belong to the specified organization"
)
);
}
if (existingUser.isOwner) {
return next(
createHttpError(
HttpCode.FORBIDDEN,
"Cannot change the roles of the owner of the organization"
)
);
}
const remainingRoles = await db
.select({ roleId: userOrgRoles.roleId })
.from(userOrgRoles)
.where(
and(
eq(userOrgRoles.userId, userId),
eq(userOrgRoles.orgId, role.orgId)
)
);
if (remainingRoles.length <= 1) {
const hasThisRole = remainingRoles.some((r) => r.roleId === roleId);
if (hasThisRole) {
return next(
createHttpError(
HttpCode.FORBIDDEN,
"User must have at least one role in the organization. Remove the last role is not allowed."
)
);
}
}
await db.transaction(async (trx) => {
await trx
.delete(userOrgRoles)
.where(
and(
eq(userOrgRoles.userId, userId),
eq(userOrgRoles.orgId, role.orgId),
eq(userOrgRoles.roleId, roleId)
)
);
const orgClients = await trx
.select()
.from(clients)
.where(
and(
eq(clients.userId, userId),
eq(clients.orgId, role.orgId)
)
);
for (const orgClient of orgClients) {
await rebuildClientAssociationsFromClient(orgClient, trx);
}
});
return response(res, {
data: { userId, orgId: role.orgId, roleId },
success: true,
error: false,
message: "Role removed from user successfully",
status: HttpCode.OK
});
} catch (error) {
logger.error(error);
return next(
createHttpError(HttpCode.INTERNAL_SERVER_ERROR, "An error occurred")
);
}
}

View File

@@ -1,163 +0,0 @@
/*
* 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.
*/
import { Request, Response, NextFunction } from "express";
import { z } from "zod";
import { clients, db } from "@server/db";
import { userOrgRoles, userOrgs, roles } from "@server/db";
import { eq, and, inArray } 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 { rebuildClientAssociationsFromClient } from "@server/lib/rebuildClientAssociations";
const setUserOrgRolesParamsSchema = z.strictObject({
orgId: z.string(),
userId: z.string()
});
const setUserOrgRolesBodySchema = z.strictObject({
roleIds: z.array(z.int().positive()).min(1)
});
export async function setUserOrgRoles(
req: Request,
res: Response,
next: NextFunction
): Promise<any> {
try {
const parsedParams = setUserOrgRolesParamsSchema.safeParse(req.params);
if (!parsedParams.success) {
return next(
createHttpError(
HttpCode.BAD_REQUEST,
fromError(parsedParams.error).toString()
)
);
}
const parsedBody = setUserOrgRolesBodySchema.safeParse(req.body);
if (!parsedBody.success) {
return next(
createHttpError(
HttpCode.BAD_REQUEST,
fromError(parsedBody.error).toString()
)
);
}
const { orgId, userId } = parsedParams.data;
const { roleIds } = parsedBody.data;
if (req.user && !req.userOrg) {
return next(
createHttpError(
HttpCode.FORBIDDEN,
"You do not have access to this organization"
)
);
}
const uniqueRoleIds = [...new Set(roleIds)];
const [existingUser] = await db
.select()
.from(userOrgs)
.where(and(eq(userOrgs.userId, userId), eq(userOrgs.orgId, orgId)))
.limit(1);
if (!existingUser) {
return next(
createHttpError(
HttpCode.NOT_FOUND,
"User not found in this organization"
)
);
}
if (existingUser.isOwner) {
return next(
createHttpError(
HttpCode.FORBIDDEN,
"Cannot change the roles of the owner of the organization"
)
);
}
const orgRoles = await db
.select({ roleId: roles.roleId })
.from(roles)
.where(
and(
eq(roles.orgId, orgId),
inArray(roles.roleId, uniqueRoleIds)
)
);
if (orgRoles.length !== uniqueRoleIds.length) {
return next(
createHttpError(
HttpCode.BAD_REQUEST,
"One or more role IDs are invalid for this organization"
)
);
}
await db.transaction(async (trx) => {
await trx
.delete(userOrgRoles)
.where(
and(
eq(userOrgRoles.userId, userId),
eq(userOrgRoles.orgId, orgId)
)
);
if (uniqueRoleIds.length > 0) {
await trx.insert(userOrgRoles).values(
uniqueRoleIds.map((roleId) => ({
userId,
orgId,
roleId
}))
);
}
const orgClients = await trx
.select()
.from(clients)
.where(
and(eq(clients.userId, userId), eq(clients.orgId, orgId))
);
for (const orgClient of orgClients) {
await rebuildClientAssociationsFromClient(orgClient, trx);
}
});
return response(res, {
data: { userId, orgId, roleIds: uniqueRoleIds },
success: true,
error: false,
message: "User roles set successfully",
status: HttpCode.OK
});
} catch (error) {
logger.error(error);
return next(
createHttpError(HttpCode.INTERNAL_SERVER_ERROR, "An error occurred")
);
}
}

View File

@@ -18,12 +18,10 @@ import {
} from "#private/routers/remoteExitNode";
import { MessageHandler } from "@server/routers/ws";
import { build } from "@server/build";
import { handleConnectionLogMessage } from "#dynamic/routers/newt";
export const messageHandlers: Record<string, MessageHandler> = {
"remoteExitNode/register": handleRemoteExitNodeRegisterMessage,
"remoteExitNode/ping": handleRemoteExitNodePingMessage,
"newt/access-log": handleConnectionLogMessage,
"remoteExitNode/ping": handleRemoteExitNodePingMessage
};
if (build != "saas") {

View File

@@ -208,7 +208,7 @@ export async function listAccessTokens(
.where(
or(
eq(userResources.userId, req.user!.userId),
inArray(roleResources.roleId, req.userOrgRoleIds!)
eq(roleResources.roleId, req.userOrgRoleId!)
)
);
} else {

View File

@@ -91,50 +91,3 @@ export type QueryAccessAuditLogResponse = {
locations: string[];
};
};
export type QueryConnectionAuditLogResponse = {
log: {
sessionId: string;
siteResourceId: number | null;
orgId: string | null;
siteId: number | null;
clientId: number | null;
userId: string | null;
sourceAddr: string;
destAddr: string;
protocol: string;
startedAt: number;
endedAt: number | null;
bytesTx: number | null;
bytesRx: number | null;
resourceName: string | null;
resourceNiceId: string | null;
siteName: string | null;
siteNiceId: string | null;
clientName: string | null;
clientNiceId: string | null;
clientType: string | null;
userEmail: string | null;
}[];
pagination: {
total: number;
limit: number;
offset: number;
};
filterAttributes: {
protocols: string[];
destAddrs: string[];
clients: {
id: number;
name: string;
}[];
resources: {
id: number;
name: string | null;
}[];
users: {
id: string;
email: string | null;
}[];
};
};

View File

@@ -1,37 +1,4 @@
import { assertEquals } from "@test/assert";
import { REGIONS } from "@server/db/regions";
function isIpInRegion(
ipCountryCode: string | undefined,
checkRegionCode: string
): boolean {
if (!ipCountryCode) {
return false;
}
const upperCode = ipCountryCode.toUpperCase();
for (const region of REGIONS) {
// Check if it's a top-level region (continent)
if (region.id === checkRegionCode) {
for (const subregion of region.includes) {
if (subregion.countries.includes(upperCode)) {
return true;
}
}
return false;
}
// Check subregions
for (const subregion of region.includes) {
if (subregion.id === checkRegionCode) {
return subregion.countries.includes(upperCode);
}
}
}
return false;
}
function isPathAllowed(pattern: string, path: string): boolean {
// Normalize and split paths into segments
@@ -305,71 +272,12 @@ function runTests() {
"Root path should not match non-root path"
);
console.log("All path matching tests passed!");
}
function runRegionTests() {
console.log("\nRunning isIpInRegion tests...");
// Test undefined country code
assertEquals(
isIpInRegion(undefined, "150"),
false,
"Undefined country code should return false"
);
// Test subregion matching (Western Europe)
assertEquals(
isIpInRegion("DE", "155"),
true,
"Country should match its subregion"
);
assertEquals(
isIpInRegion("GB", "155"),
false,
"Country should NOT match wrong subregion"
);
// Test continent matching (Europe)
assertEquals(
isIpInRegion("DE", "150"),
true,
"Country should match its continent"
);
assertEquals(
isIpInRegion("GB", "150"),
true,
"Different European country should match Europe"
);
assertEquals(
isIpInRegion("US", "150"),
false,
"Non-European country should NOT match Europe"
);
// Test case insensitivity
assertEquals(
isIpInRegion("de", "155"),
true,
"Lowercase country code should work"
);
// Test invalid region code
assertEquals(
isIpInRegion("DE", "999"),
false,
"Invalid region code should return false"
);
console.log("All region tests passed!");
console.log("All tests passed!");
}
// Run all tests
try {
runTests();
runRegionTests();
console.log("\n✅ All tests passed!");
} catch (error) {
console.error("Test failed:", error);
process.exit(1);
console.error("Test failed:", error);
}

View File

@@ -4,11 +4,11 @@ import {
getResourceByDomain,
getResourceRules,
getRoleResourceAccess,
getUserOrgRole,
getUserResourceAccess,
getOrgLoginPage,
getUserSessionWithUser
} from "@server/db/queries/verifySessionQueries";
import { getUserOrgRoles } from "@server/lib/userOrgRoles";
import {
LoginPage,
Org,
@@ -30,13 +30,13 @@ import { z } from "zod";
import { fromError } from "zod-validation-error";
import { getCountryCodeForIp } from "@server/lib/geoip";
import { getAsnForIp } from "@server/lib/asn";
import { getOrgTierData } from "#dynamic/lib/billing";
import { verifyPassword } from "@server/auth/password";
import {
checkOrgAccessPolicy,
enforceResourceSessionLength
} from "#dynamic/lib/checkOrgAccessPolicy";
import { logRequestAudit } from "./logRequestAudit";
import { REGIONS } from "@server/db/regions";
import { localCache } from "#dynamic/lib/cache";
import { APP_VERSION } from "@server/lib/consts";
import { isSubscribed } from "#dynamic/lib/isSubscribed";
@@ -797,8 +797,7 @@ async function notAllowed(
) {
let loginPage: LoginPage | null = null;
if (orgId) {
const subscribed = await isSubscribed(
// this is fine because the org login page is only a saas feature
const subscribed = await isSubscribed( // this is fine because the org login page is only a saas feature
orgId,
tierMatrix.loginPageDomain
);
@@ -855,10 +854,7 @@ async function headerAuthChallenged(
) {
let loginPage: LoginPage | null = null;
if (orgId) {
const subscribed = await isSubscribed(
orgId,
tierMatrix.loginPageDomain
); // this is fine because the org login page is only a saas feature
const subscribed = await isSubscribed(orgId, tierMatrix.loginPageDomain); // this is fine because the org login page is only a saas feature
if (subscribed) {
loginPage = await getOrgLoginPage(orgId);
}
@@ -920,9 +916,9 @@ async function isUserAllowedToAccessResource(
return null;
}
const userOrgRoles = await getUserOrgRoles(user.userId, resource.orgId);
const userOrgRole = await getUserOrgRole(user.userId, resource.orgId);
if (!userOrgRoles.length) {
if (!userOrgRole) {
return null;
}
@@ -940,14 +936,15 @@ async function isUserAllowedToAccessResource(
const roleResourceAccess = await getRoleResourceAccess(
resource.resourceId,
userOrgRoles.map((r) => r.roleId)
userOrgRole.roleId
);
if (roleResourceAccess && roleResourceAccess.length > 0) {
if (roleResourceAccess) {
return {
username: user.username,
email: user.email,
name: user.name,
role: userOrgRoles.map((r) => r.roleName).join(", ")
role: userOrgRole.roleName
};
}
@@ -961,7 +958,7 @@ async function isUserAllowedToAccessResource(
username: user.username,
email: user.email,
name: user.name,
role: userOrgRoles.map((r) => r.roleName).join(", ")
role: userOrgRole.roleName
};
}
@@ -1023,12 +1020,6 @@ async function checkRules(
(await isIpInAsn(ipAsn, rule.value))
) {
return rule.action as any;
} else if (
clientIp &&
rule.match == "REGION" &&
(await isIpInRegion(ipCC, rule.value))
) {
return rule.action as any;
}
}
@@ -1214,45 +1205,6 @@ async function isIpInAsn(
return match;
}
export async function isIpInRegion(
ipCountryCode: string | undefined,
checkRegionCode: string
): Promise<boolean> {
if (!ipCountryCode) {
return false;
}
const upperCode = ipCountryCode.toUpperCase();
for (const region of REGIONS) {
// Check if it's a top-level region (continent)
if (region.id === checkRegionCode) {
for (const subregion of region.includes) {
if (subregion.countries.includes(upperCode)) {
logger.debug(`Country ${upperCode} is in region ${region.id} (${region.name})`);
return true;
}
}
logger.debug(`Country ${upperCode} is not in region ${region.id} (${region.name})`);
return false;
}
// Check subregions
for (const subregion of region.includes) {
if (subregion.id === checkRegionCode) {
if (subregion.countries.includes(upperCode)) {
logger.debug(`Country ${upperCode} is in region ${subregion.id} (${subregion.name})`);
return true;
}
logger.debug(`Country ${upperCode} is not in region ${subregion.id} (${subregion.name})`);
return false;
}
}
}
return false;
}
async function getAsnFromIp(ip: string): Promise<number | undefined> {
const asnCacheKey = `asn:${ip}`;

View File

@@ -92,7 +92,7 @@ export async function createClient(
const { orgId } = parsedParams.data;
if (req.user && (!req.userOrgRoleIds || req.userOrgRoleIds.length === 0)) {
if (req.user && !req.userOrgRoleId) {
return next(
createHttpError(HttpCode.FORBIDDEN, "User does not have a role")
);
@@ -234,7 +234,7 @@ export async function createClient(
clientId: newClient.clientId
});
if (req.user && !req.userOrgRoleIds?.includes(adminRole.roleId)) {
if (req.user && req.userOrgRoleId != adminRole.roleId) {
// make sure the user can access the client
trx.insert(userClients).values({
userId: req.user.userId,

View File

@@ -297,7 +297,7 @@ export async function listClients(
.where(
or(
eq(userClients.userId, req.user!.userId),
inArray(roleClients.roleId, req.userOrgRoleIds!)
eq(roleClients.roleId, req.userOrgRoleId!)
)
);
} else {

View File

@@ -316,7 +316,7 @@ export async function listUserDevices(
.where(
or(
eq(userClients.userId, req.user!.userId),
inArray(roleClients.roleId, req.userOrgRoleIds!)
eq(roleClients.roleId, req.userOrgRoleId!)
)
);
} else {

View File

@@ -1,54 +1,15 @@
import { sendToClient } from "#dynamic/routers/ws";
import { db, newts, olms } from "@server/db";
import {
Alias,
convertSubnetProxyTargetsV2ToV1,
SubnetProxyTarget,
SubnetProxyTargetV2
} from "@server/lib/ip";
import { db, olms, Transaction } from "@server/db";
import { canCompress } from "@server/lib/clientVersionChecks";
import { Alias, SubnetProxyTarget } from "@server/lib/ip";
import logger from "@server/logger";
import { eq } from "drizzle-orm";
import semver from "semver";
const NEWT_V2_TARGETS_VERSION = ">=1.10.3";
export async function convertTargetsIfNessicary(
newtId: string,
targets: SubnetProxyTarget[] | SubnetProxyTargetV2[]
) {
// get the newt
const [newt] = await db
.select()
.from(newts)
.where(eq(newts.newtId, newtId));
if (!newt) {
throw new Error(`No newt found for id: ${newtId}`);
}
// check the semver
if (
newt.version &&
!semver.satisfies(newt.version, NEWT_V2_TARGETS_VERSION)
) {
logger.debug(
`addTargets Newt version ${newt.version} does not support targets v2 falling back`
);
targets = convertSubnetProxyTargetsV2ToV1(
targets as SubnetProxyTargetV2[]
);
}
return targets;
}
export async function addTargets(
newtId: string,
targets: SubnetProxyTarget[] | SubnetProxyTargetV2[],
targets: SubnetProxyTarget[],
version?: string | null
) {
targets = await convertTargetsIfNessicary(newtId, targets);
await sendToClient(
newtId,
{
@@ -61,11 +22,9 @@ export async function addTargets(
export async function removeTargets(
newtId: string,
targets: SubnetProxyTarget[] | SubnetProxyTargetV2[],
targets: SubnetProxyTarget[],
version?: string | null
) {
targets = await convertTargetsIfNessicary(newtId, targets);
await sendToClient(
newtId,
{
@@ -79,39 +38,11 @@ export async function removeTargets(
export async function updateTargets(
newtId: string,
targets: {
oldTargets: SubnetProxyTarget[] | SubnetProxyTargetV2[];
newTargets: SubnetProxyTarget[] | SubnetProxyTargetV2[];
oldTargets: SubnetProxyTarget[];
newTargets: SubnetProxyTarget[];
},
version?: string | null
) {
// get the newt
const [newt] = await db
.select()
.from(newts)
.where(eq(newts.newtId, newtId));
if (!newt) {
logger.error(`addTargetsL No newt found for id: ${newtId}`);
return;
}
// check the semver
if (
newt.version &&
!semver.satisfies(newt.version, NEWT_V2_TARGETS_VERSION)
) {
logger.debug(
`addTargets Newt version ${newt.version} does not support targets v2 falling back`
);
targets = {
oldTargets: convertSubnetProxyTargetsV2ToV1(
targets.oldTargets as SubnetProxyTargetV2[]
),
newTargets: convertSubnetProxyTargetsV2ToV1(
targets.newTargets as SubnetProxyTargetV2[]
)
};
}
await sendToClient(
newtId,
{

View File

@@ -102,8 +102,6 @@ authenticated.put(
logActionAudit(ActionsEnum.createSite),
site.createSite
);
authenticated.get(
"/org/:orgId/sites",
verifyOrgAccess,
@@ -646,7 +644,6 @@ authenticated.delete(
logActionAudit(ActionsEnum.deleteRole),
role.deleteRole
);
authenticated.post(
"/role/:roleId/add/:userId",
verifyRoleAccess,
@@ -654,7 +651,7 @@ authenticated.post(
verifyLimits,
verifyUserHasAction(ActionsEnum.addUserRole),
logActionAudit(ActionsEnum.addUserRole),
user.addUserRoleLegacy
user.addUserRole
);
authenticated.post(
@@ -1205,22 +1202,6 @@ authRouter.post(
}),
newt.getNewtToken
);
authRouter.post(
"/newt/register",
rateLimit({
windowMs: 15 * 60 * 1000,
max: 30,
keyGenerator: (req) =>
`newtRegister:${req.body.provisioningKey?.split(".")[0] || ipKeyGenerator(req.ip || "")}`,
handler: (req, res, next) => {
const message = `You can only register a newt ${30} times every ${15} minutes. Please try again later.`;
return next(createHttpError(HttpCode.TOO_MANY_REQUESTS, message));
},
store: createStore()
}),
newt.registerNewt
);
authRouter.post(
"/olm/get-token",
rateLimit({

View File

@@ -96,14 +96,6 @@ async function dbQueryRows<T extends Record<string, unknown>>(
return (await anyDb.all(query)) as T[];
}
/**
* Returns true when the active database driver is SQLite (better-sqlite3).
* Used to select the appropriate bulk-update strategy.
*/
function isSQLite(): boolean {
return typeof (db as any).execute !== "function";
}
/**
* Flush all accumulated site bandwidth data to the database.
*
@@ -149,37 +141,19 @@ export async function flushSiteBandwidthToDb(): Promise<void> {
const chunk = sortedEntries.slice(i, i + BATCH_CHUNK_SIZE);
const chunkEnd = i + chunk.length - 1;
// Build a parameterised VALUES list: (pubKey, bytesIn, bytesOut), ...
// Both PostgreSQL and SQLite (≥ 3.33.0, which better-sqlite3 bundles)
// support UPDATE … FROM (VALUES …), letting us update the whole chunk
// in a single query instead of N individual round-trips.
const valuesList = chunk.map(([publicKey, { bytesIn, bytesOut }]) =>
sql`(${publicKey}::text, ${bytesIn}::real, ${bytesOut}::real)`
);
const valuesClause = sql.join(valuesList, sql`, `);
let rows: { orgId: string; pubKey: string }[] = [];
try {
rows = await withDeadlockRetry(async () => {
if (isSQLite()) {
// SQLite: one UPDATE per row — no need for batch efficiency here.
const results: { orgId: string; pubKey: string }[] = [];
for (const [publicKey, { bytesIn, bytesOut }] of chunk) {
const result = await dbQueryRows<{
orgId: string;
pubKey: string;
}>(sql`
UPDATE sites
SET
"bytesOut" = COALESCE("bytesOut", 0) + ${bytesIn},
"bytesIn" = COALESCE("bytesIn", 0) + ${bytesOut},
"lastBandwidthUpdate" = ${currentTime}
WHERE "pubKey" = ${publicKey}
RETURNING "orgId", "pubKey"
`);
results.push(...result);
}
return results;
}
// PostgreSQL: batch UPDATE … FROM (VALUES …) — single round-trip per chunk.
const valuesList = chunk.map(
([publicKey, { bytesIn, bytesOut }]) =>
sql`(${publicKey}, ${bytesIn}, ${bytesOut})`
);
const valuesClause = sql.join(valuesList, sql`, `);
return dbQueryRows<{ orgId: string; pubKey: string }>(sql`
UPDATE sites
SET

View File

@@ -25,8 +25,7 @@ const bodySchema = z.strictObject({
namePath: z.string().optional(),
scopes: z.string().nonempty(),
autoProvision: z.boolean().optional(),
tags: z.string().optional(),
variant: z.enum(["oidc", "google", "azure"]).optional().default("oidc")
tags: z.string().optional()
});
export type CreateIdpResponse = {
@@ -78,8 +77,7 @@ export async function createOidcIdp(
namePath,
name,
autoProvision,
tags,
variant
tags
} = parsedBody.data;
if (
@@ -123,8 +121,7 @@ export async function createOidcIdp(
scopes,
identifierPath,
emailPath,
namePath,
variant
namePath
});
});

View File

@@ -31,8 +31,7 @@ const bodySchema = z.strictObject({
autoProvision: z.boolean().optional(),
defaultRoleMapping: z.string().optional(),
defaultOrgMapping: z.string().optional(),
tags: z.string().optional(),
variant: z.enum(["oidc", "google", "azure"]).optional()
tags: z.string().optional()
});
export type UpdateIdpResponse = {
@@ -97,8 +96,7 @@ export async function updateOidcIdp(
autoProvision,
defaultRoleMapping,
defaultOrgMapping,
tags,
variant
tags
} = parsedBody.data;
if (process.env.IDENTITY_PROVIDER_MODE === "org") {
@@ -161,8 +159,7 @@ export async function updateOidcIdp(
scopes,
identifierPath,
emailPath,
namePath,
variant
namePath
};
keysToUpdate = Object.keys(configData).filter(

View File

@@ -13,7 +13,6 @@ import {
orgs,
Role,
roles,
userOrgRoles,
userOrgs,
users
} from "@server/db";
@@ -36,13 +35,11 @@ import { usageService } from "@server/lib/billing/usageService";
import { build } from "@server/build";
import { calculateUserClientsForOrgs } from "@server/lib/calculateUserClientsForOrgs";
import { isSubscribed } from "#dynamic/lib/isSubscribed";
import { isLicensedOrSubscribed } from "#dynamic/lib/isLicencedOrSubscribed";
import { tierMatrix } from "@server/lib/billing/tierMatrix";
import {
assignUserToOrg,
removeUserFromOrg
} from "@server/lib/userOrg";
import { unwrapRoleMapping } from "@app/lib/idpRoleMapping";
const ensureTrailingSlash = (url: string): string => {
return url;
@@ -369,7 +366,7 @@ export async function validateOidcCallback(
const defaultRoleMapping = existingIdp.idp.defaultRoleMapping;
const defaultOrgMapping = existingIdp.idp.defaultOrgMapping;
const userOrgInfo: { orgId: string; roleIds: number[] }[] = [];
const userOrgInfo: { orgId: string; roleId: number }[] = [];
for (const org of allOrgs) {
const [idpOrgRes] = await db
.select()
@@ -381,6 +378,8 @@ export async function validateOidcCallback(
)
);
let roleId: number | undefined = undefined;
const orgMapping = idpOrgRes?.orgMapping || defaultOrgMapping;
const hydratedOrgMapping = hydrateOrgMapping(
orgMapping,
@@ -405,55 +404,38 @@ export async function validateOidcCallback(
idpOrgRes?.roleMapping || defaultRoleMapping;
if (roleMapping) {
logger.debug("Role Mapping", { roleMapping });
const roleMappingJmes = unwrapRoleMapping(
roleMapping
).evaluationExpression;
const roleMappingResult = jmespath.search(
claims,
roleMappingJmes
);
const roleNames = normalizeRoleMappingResult(
roleMappingResult
);
const roleName = jmespath.search(claims, roleMapping);
const supportsMultiRole = await isLicensedOrSubscribed(
org.orgId,
tierMatrix.fullRbac
);
const effectiveRoleNames = supportsMultiRole
? roleNames
: roleNames.slice(0, 1);
if (!effectiveRoleNames.length) {
logger.error("Role mapping returned no valid roles", {
roleMappingResult
if (!roleName) {
logger.error("Role name not found in the ID token", {
roleName
});
continue;
}
const roleRes = await db
const [roleRes] = await db
.select()
.from(roles)
.where(
and(
eq(roles.orgId, org.orgId),
inArray(roles.name, effectiveRoleNames)
eq(roles.name, roleName)
)
);
if (!roleRes.length) {
logger.error("No mapped roles found in organization", {
if (!roleRes) {
logger.error("Role not found", {
orgId: org.orgId,
roleNames: effectiveRoleNames
roleName
});
continue;
}
const roleIds = [...new Set(roleRes.map((r) => r.roleId))];
roleId = roleRes.roleId;
userOrgInfo.push({
orgId: org.orgId,
roleIds
roleId
});
}
}
@@ -588,28 +570,32 @@ export async function validateOidcCallback(
}
}
// Sync roles 1:1 with IdP policy for existing auto-provisioned orgs
for (const currentOrg of autoProvisionedOrgs) {
const newRole = userOrgInfo.find(
(newOrg) => newOrg.orgId === currentOrg.orgId
);
if (!newRole) continue;
await trx
.delete(userOrgRoles)
.where(
and(
eq(userOrgRoles.userId, userId!),
eq(userOrgRoles.orgId, currentOrg.orgId)
)
// Update roles for existing auto-provisioned orgs where the role has changed
const orgsToUpdate = autoProvisionedOrgs.filter(
(currentOrg) => {
const newOrg = userOrgInfo.find(
(newOrg) => newOrg.orgId === currentOrg.orgId
);
return newOrg && newOrg.roleId !== currentOrg.roleId;
}
);
for (const roleId of newRole.roleIds) {
await trx.insert(userOrgRoles).values({
userId: userId!,
orgId: currentOrg.orgId,
roleId
});
if (orgsToUpdate.length > 0) {
for (const org of orgsToUpdate) {
const newRole = userOrgInfo.find(
(newOrg) => newOrg.orgId === org.orgId
);
if (newRole) {
await trx
.update(userOrgs)
.set({ roleId: newRole.roleId })
.where(
and(
eq(userOrgs.userId, userId!),
eq(userOrgs.orgId, org.orgId)
)
);
}
}
}
@@ -623,10 +609,6 @@ export async function validateOidcCallback(
if (orgsToAdd.length > 0) {
for (const org of orgsToAdd) {
if (org.roleIds.length === 0) {
continue;
}
const [fullOrg] = await trx
.select()
.from(orgs)
@@ -637,9 +619,9 @@ export async function validateOidcCallback(
{
orgId: org.orgId,
userId: userId!,
roleId: org.roleId,
autoProvisioned: true,
},
org.roleIds,
trx
);
}
@@ -766,25 +748,3 @@ function hydrateOrgMapping(
}
return orgMapping.split("{{orgId}}").join(orgId);
}
function normalizeRoleMappingResult(
result: unknown
): string[] {
if (typeof result === "string") {
const role = result.trim();
return role ? [role] : [];
}
if (Array.isArray(result)) {
return [
...new Set(
result
.filter((value): value is string => typeof value === "string")
.map((value) => value.trim())
.filter(Boolean)
)
];
}
return [];
}

View File

@@ -16,7 +16,6 @@ import {
verifyApiKey,
verifyApiKeyOrgAccess,
verifyApiKeyHasAction,
verifyApiKeyCanSetUserOrgRoles,
verifyApiKeySiteAccess,
verifyApiKeyResourceAccess,
verifyApiKeyTargetAccess,
@@ -596,7 +595,7 @@ authenticated.post(
verifyLimits,
verifyApiKeyHasAction(ActionsEnum.addUserRole),
logActionAudit(ActionsEnum.addUserRole),
user.addUserRoleLegacy
user.addUserRole
);
authenticated.post(

View File

@@ -16,8 +16,8 @@ import { eq, and } from "drizzle-orm";
import config from "@server/lib/config";
import {
formatEndpoint,
generateSubnetProxyTargetV2,
SubnetProxyTargetV2
generateSubnetProxyTargets,
SubnetProxyTarget
} from "@server/lib/ip";
export async function buildClientConfigurationForNewtClient(
@@ -143,7 +143,7 @@ export async function buildClientConfigurationForNewtClient(
.from(siteResources)
.where(eq(siteResources.siteId, siteId));
const targetsToSend: SubnetProxyTargetV2[] = [];
const targetsToSend: SubnetProxyTarget[] = [];
for (const resource of allSiteResources) {
// Get clients associated with this specific resource
@@ -168,14 +168,12 @@ export async function buildClientConfigurationForNewtClient(
)
);
const resourceTarget = generateSubnetProxyTargetV2(
const resourceTargets = generateSubnetProxyTargets(
resource,
resourceClients
);
if (resourceTarget) {
targetsToSend.push(resourceTarget);
}
targetsToSend.push(...resourceTargets);
}
return {

View File

@@ -46,7 +46,7 @@ export async function createNewt(
const { newtId, secret } = parsedBody.data;
if (req.user && (!req.userOrgRoleIds || req.userOrgRoleIds.length === 0)) {
if (req.user && !req.userOrgRoleId) {
return next(
createHttpError(HttpCode.FORBIDDEN, "User does not have a role")
);

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